// Hex Editor — Core Library
// HexBuffer with Gap Buffer for O(1) insert/delete at cursor.
//
// Gap Buffer Layout:
// [data_before_gap | ___gap___ | data_after_gap]
// 0..gap_start gap_start..gap_end gap_end..data.length()
//
// Logical view (what the user sees):
// [data_before_gap | data_after_gap] (gap is invisible)
// len = data.length() - (gap_end - gap_start)
//
// Gap size = file disk allocation (rounded to 4KB cluster) / 16.
// Insert/delete at gap: O(1). Moving gap: O(distance). Read: O(1).
///|
/// A mutable buffer for hex editing with Gap Buffer optimization.
///
/// Physical layout: `[bytes | gap | bytes]`
/// - `gap_start`: first index of the gap
/// - `gap_end`: first index after the gap
/// - `len`: logical byte count (excluding gap)
///
/// The gap moves to the edit cursor on insert/delete.
/// Consecutive edits at the same position are O(1).
struct HexBuffer {
mut data : FixedArray[Byte]
mut gap_start : Int // first gap index (inclusive)
mut gap_end : Int // first post-gap index (exclusive)
mut len : Int // logical length = data.length() - gap_size
mut modified : Bool
mut file_path : String?
}
///|
/// Gap size = disk allocation / 16, minimum 256 bytes.
/// Disk allocation = file_size rounded up to 4KB cluster boundary.
fn calc_gap_size(file_size : Int) -> Int {
let alloc = (file_size + 4095) / 4096 * 4096
let g = alloc / 16
if g < 256 {
256
} else {
g
}
}
///|
/// Creates a new empty HexBuffer with 256-byte initial gap.
pub fn HexBuffer::new() -> HexBuffer {
{
data: FixedArray::make(256, Byte::default()),
gap_start: 0,
gap_end: 256,
len: 0,
modified: false,
file_path: None,
}
}
///|
/// Creates a HexBuffer from bytes. Gap is placed at the end.
/// Layout: [file_bytes | gap]
pub fn HexBuffer::from_bytes(bytes : Bytes) -> HexBuffer {
let file_size = bytes.length()
let gap = calc_gap_size(file_size)
let total = file_size + gap
let data = FixedArray::make(total, Byte::default())
data.blit_from_bytes(0, bytes, 0, file_size)
{
data,
gap_start: file_size,
gap_end: total,
len: file_size,
modified: false,
file_path: None,
}
}
///|
pub fn HexBuffer::set_file_path(self : HexBuffer, path : String) -> Unit {
self.file_path = Some(path)
}
///|
pub fn HexBuffer::from_file(path : String) -> HexBuffer raise {
let bytes = @fs.read_file_to_bytes(path)
let buf = HexBuffer::from_bytes(bytes)
buf.file_path = Some(path)
buf
}
///|
pub fn HexBuffer::length(self : HexBuffer) -> Int {
self.len
}
///|
pub fn HexBuffer::is_empty(self : HexBuffer) -> Bool {
self.len == 0
}
///|
pub fn HexBuffer::is_modified(self : HexBuffer) -> Bool {
self.modified
}
///|
pub fn HexBuffer::get_file_path(self : HexBuffer) -> String? {
self.file_path
}
///|
/// Convert logical index to physical index, skipping over the gap.
/// logical 0..gap_start → physical same
/// logical gap_start.. → physical + gap_size
fn HexBuffer::to_physical(self : HexBuffer, idx : Int) -> Int {
if idx < self.gap_start {
idx
} else {
idx + self.gap_end - self.gap_start
}
}
///|
/// Safe byte read with bounds check.
pub fn HexBuffer::get_byte(self : HexBuffer, offset : Int) -> Byte? {
guard offset >= 0 && offset < self.len else { None }
Some(self.data[self.to_physical(offset)])
}
///|
/// Fast byte read for rendering (no bounds check, no Option).
/// Caller must ensure idx < self.len.
pub fn HexBuffer::byte_at(self : HexBuffer, idx : Int) -> Byte {
self.data[self.to_physical(idx)]
}
///|
/// Set byte at logical offset. Returns false if out of bounds.
pub fn HexBuffer::set_byte(
self : HexBuffer,
offset : Int,
value : Byte,
) -> Bool {
guard offset >= 0 && offset < self.len else { false }
self.data[self.to_physical(offset)] = value
self.modified = true
true
}
///|
/// Move the gap so that gap_start == pos.
///
/// Two cases:
/// pos < gap_start: move bytes [pos, gap_start) to end of gap
/// [AAA|_____|BBB] → [A|_____|AABBB]
///
/// pos > gap_start: move bytes [gap_end, gap_end+delta) to start of gap
/// [AAA|_____|BBB] → [AAAB|_____|BB]
fn HexBuffer::move_gap(self : HexBuffer, pos : Int) -> Unit {
if pos == self.gap_start {
return
}
let gap_size = self.gap_end - self.gap_start
if pos < self.gap_start {
// Shift bytes leftward into the gap's tail
let n = self.gap_start - pos
for i = n - 1; i >= 0; i = i - 1 {
self.data[self.gap_end - n + i] = self.data[pos + i]
}
self.gap_start = pos
self.gap_end = pos + gap_size
} else {
// Shift bytes rightward into the gap's head
let n = pos - self.gap_start
for i = 0; i < n; i = i + 1 {
self.data[self.gap_start + i] = self.data[self.gap_end + i]
}
self.gap_start = pos
self.gap_end = pos + gap_size
}
}
///|
/// Grow the gap when exhausted (gap_start == gap_end).
/// Allocates new array with fresh gap at current gap_start position.
fn HexBuffer::grow_gap(self : HexBuffer) -> Unit {
let new_gap = calc_gap_size(self.len)
let new_total = self.len + new_gap
let new_data = FixedArray::make(new_total, Byte::default())
// Copy bytes before gap
new_data.unsafe_blit(0, self.data, 0, self.gap_start)
// Copy bytes after gap (skip old gap, place after new gap)
let after_len = self.len - self.gap_start
let new_gap_end = self.gap_start + new_gap
if after_len > 0 {
new_data.unsafe_blit(new_gap_end, self.data, self.gap_end, after_len)
}
self.data = new_data
self.gap_end = new_gap_end
}
///|
/// Insert a byte at logical offset. O(1) if gap is already at offset.
/// 1. Grow gap if exhausted
/// 2. Move gap to offset
/// 3. Write byte into gap_start, advance gap_start
pub fn HexBuffer::insert_byte(
self : HexBuffer,
offset : Int,
value : Byte,
) -> Bool {
guard offset >= 0 && offset <= self.len else { false }
if self.gap_start == self.gap_end {
self.grow_gap()
}
self.move_gap(offset)
self.data[self.gap_start] = value
self.gap_start = self.gap_start + 1
self.len = self.len + 1
self.modified = true
true
}
///|
/// Delete byte at logical offset. O(1) if gap is already at offset.
/// 1. Move gap to offset
/// 2. Expand gap_end by 1 (the byte at gap_end is "deleted")
pub fn HexBuffer::delete_byte(self : HexBuffer, offset : Int) -> Bool {
guard offset >= 0 && offset < self.len else { false }
self.move_gap(offset)
self.gap_end = self.gap_end + 1
self.len = self.len - 1
self.modified = true
true
}
///|
/// Compact copy of logical bytes (no gap).
pub fn HexBuffer::to_bytes(self : HexBuffer) -> Bytes {
if self.len == 0 {
return b""
}
Bytes::makei(self.len, fn(i) { self.data[self.to_physical(i)] })
}
///|
pub fn HexBuffer::to_fixedarray(self : HexBuffer) -> FixedArray[Byte] {
if self.len == 0 {
return []
}
let arr = FixedArray::make(self.len, Byte::default())
if self.gap_start > 0 {
arr.unsafe_blit(0, self.data, 0, self.gap_start)
}
let after = self.len - self.gap_start
if after > 0 {
arr.unsafe_blit(self.gap_start, self.data, self.gap_end, after)
}
arr
}
///|
/// Returns internal data array (includes gap). Use byte_at() for correct access.
pub fn HexBuffer::data_ref(self : HexBuffer) -> FixedArray[Byte] {
self.data
}
///|
pub fn HexBuffer::save(self : HexBuffer, path? : String) -> Unit raise {
let save_path = match path {
Some(p) => p
None =>
match self.file_path {
Some(p) => p
None => raise @fs.IOError::IOError("No file path specified for save")
}
}
let bytes = self.to_bytes()
@fs.write_bytes_to_file(save_path, bytes)
self.modified = false
self.file_path = Some(save_path)
}