Files
nexus/internal/leviathan/document/document.go
T
2026-06-16 11:51:07 +01:00

440 lines
9.7 KiB
Go

// Package document implements the chunked binary document model.
//
// The document holds three conceptual layers per slot:
//
// source — immutable original bytes, loaded once.
// patch — sparse overlay of modified bytes keyed by offset.
// working — logical view = source with patch applied on read.
//
// Reads happen through windowed Range() calls so neither the editor
// nor analysis services need to materialise the whole file.
package document
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sync"
)
// ChunkSize is the granularity at which source bytes are exposed to
// analysis services. It is independent of the view row width.
const ChunkSize = 64 * 1024
// Document represents one loaded artefact. A Session may hold many.
type Document struct {
mu sync.RWMutex
name string
source []byte // original immutable content
path string // optional immutable file-backed source
size int64
patch map[int64]byte
history []EditOp
redo []EditOp
cursor int
}
// EditOp records a single byte-level mutation for undo/redo.
type EditOp struct {
Offset int64
Old byte
New byte
Kind string // "set", "fill", "xor"
}
// New creates a Document from raw source bytes. The slice is not
// copied; callers must not mutate it after handoff.
func New(name string, src []byte) *Document {
return &Document{
name: name,
source: src,
size: int64(len(src)),
patch: make(map[int64]byte),
}
}
// Open creates a Document backed by an immutable file on disk. Source
// bytes are read on demand through Range rather than loaded eagerly.
func Open(path string) (*Document, error) {
st, err := os.Stat(path)
if err != nil {
return nil, err
}
if st.IsDir() {
return nil, errors.New("document path is a directory")
}
return &Document{
name: filepath.Base(path),
path: path,
size: st.Size(),
patch: make(map[int64]byte),
}, nil
}
// Name returns the document's display name.
func (d *Document) Name() string {
d.mu.RLock()
defer d.mu.RUnlock()
return d.name
}
// Size returns the logical document length in bytes.
func (d *Document) Size() int64 {
d.mu.RLock()
defer d.mu.RUnlock()
return d.size
}
// At returns the logical byte at offset (patch takes precedence).
func (d *Document) At(offset int64) (byte, error) {
d.mu.RLock()
defer d.mu.RUnlock()
if offset < 0 || offset >= d.size {
return 0, errors.New("offset out of range")
}
if b, ok := d.patch[offset]; ok {
return b, nil
}
return d.sourceByteLocked(offset)
}
// Range copies [start, start+length) into dst. It applies the patch
// overlay. Callers get a fresh slice they are free to retain.
func (d *Document) Range(start, length int64) ([]byte, error) {
d.mu.RLock()
defer d.mu.RUnlock()
n := d.size
if start < 0 || start > n {
return nil, errors.New("range start out of bounds")
}
if length < 0 {
return nil, errors.New("negative range")
}
if length > n-start {
length = n - start
}
out, err := d.sourceRangeLocked(start, length)
if err != nil {
return nil, err
}
if len(d.patch) > 0 {
end := start + length
for off, b := range d.patch {
if off >= start && off < end {
out[off-start] = b
}
}
}
return out, nil
}
// IsDirty reports whether any byte has been patched.
func (d *Document) IsDirty() bool {
d.mu.RLock()
defer d.mu.RUnlock()
return len(d.patch) > 0
}
// Set patches a single byte. Returns the previous value.
func (d *Document) Set(offset int64, b byte) (byte, error) {
d.mu.Lock()
defer d.mu.Unlock()
if offset < 0 || offset >= d.size {
return 0, errors.New("offset out of range")
}
orig, err := d.sourceByteLocked(offset)
if err != nil {
return 0, err
}
var old byte
if v, ok := d.patch[offset]; ok {
old = v
} else {
old = orig
}
if b == orig {
delete(d.patch, offset)
} else {
d.patch[offset] = b
}
d.history = append(d.history, EditOp{Offset: offset, Old: old, New: b, Kind: "set"})
d.redo = nil
return old, nil
}
// Fill patches [start,start+length) with b.
func (d *Document) Fill(start, length int64, b byte) error {
d.mu.Lock()
defer d.mu.Unlock()
if err := d.validateRangeLocked("fill", start, length); err != nil {
return err
}
orig, err := d.sourceRangeLocked(start, length)
if err != nil {
return err
}
for i := int64(0); i < length; i++ {
off := start + i
origB := orig[i]
var old byte
if v, ok := d.patch[off]; ok {
old = v
} else {
old = origB
}
if b == origB {
delete(d.patch, off)
} else {
d.patch[off] = b
}
d.history = append(d.history, EditOp{Offset: off, Old: old, New: b, Kind: "fill"})
}
d.redo = nil
return nil
}
// XOR patches [start,start+length) with a repeating key.
func (d *Document) XOR(start, length int64, key []byte) error {
if len(key) == 0 {
return errors.New("empty key")
}
d.mu.Lock()
defer d.mu.Unlock()
if err := d.validateRangeLocked("xor", start, length); err != nil {
return err
}
orig, err := d.sourceRangeLocked(start, length)
if err != nil {
return err
}
for i := int64(0); i < length; i++ {
off := start + i
origB := orig[i]
var cur byte
if v, ok := d.patch[off]; ok {
cur = v
} else {
cur = origB
}
newB := cur ^ key[i%int64(len(key))]
if newB == origB {
delete(d.patch, off)
} else {
d.patch[off] = newB
}
d.history = append(d.history, EditOp{Offset: off, Old: cur, New: newB, Kind: "xor"})
}
d.redo = nil
return nil
}
// Copy copies [src,src+length) onto [dst,dst+length) as a patch.
func (d *Document) Copy(src, length, dst int64) error {
d.mu.Lock()
defer d.mu.Unlock()
if err := d.validateRangeLocked("copy source", src, length); err != nil {
return err
}
if err := d.validateRangeLocked("copy destination", dst, length); err != nil {
return err
}
buf, err := d.workingRangeLocked(src, length)
if err != nil {
return err
}
dstOrig, err := d.sourceRangeLocked(dst, length)
if err != nil {
return err
}
for i := int64(0); i < length; i++ {
off := dst + i
origB := dstOrig[i]
var cur byte
if v, ok := d.patch[off]; ok {
cur = v
} else {
cur = origB
}
b := buf[i]
if b == origB {
delete(d.patch, off)
} else {
d.patch[off] = b
}
d.history = append(d.history, EditOp{Offset: off, Old: cur, New: b, Kind: "fill"})
}
d.redo = nil
return nil
}
// Undo reverts the last edit. Returns false if there is no history.
func (d *Document) Undo() bool {
d.mu.Lock()
defer d.mu.Unlock()
if len(d.history) == 0 {
return false
}
op := d.history[len(d.history)-1]
d.history = d.history[:len(d.history)-1]
orig, err := d.sourceByteLocked(op.Offset)
if err != nil {
return false
}
if op.Old == orig {
delete(d.patch, op.Offset)
} else {
d.patch[op.Offset] = op.Old
}
d.redo = append(d.redo, op)
return true
}
// Redo reapplies the last undone edit. Returns false if there is no redo.
func (d *Document) Redo() bool {
d.mu.Lock()
defer d.mu.Unlock()
if len(d.redo) == 0 {
return false
}
op := d.redo[len(d.redo)-1]
d.redo = d.redo[:len(d.redo)-1]
orig, err := d.sourceByteLocked(op.Offset)
if err != nil {
return false
}
if op.New == orig {
delete(d.patch, op.Offset)
} else {
d.patch[op.Offset] = op.New
}
d.history = append(d.history, op)
return true
}
// Revert drops all patches and history.
func (d *Document) Revert() {
d.mu.Lock()
defer d.mu.Unlock()
d.patch = make(map[int64]byte)
d.history = nil
d.redo = nil
}
// Patches returns a snapshot of the patch overlay.
func (d *Document) Patches() map[int64]byte {
d.mu.RLock()
defer d.mu.RUnlock()
out := make(map[int64]byte, len(d.patch))
for k, v := range d.patch {
out[k] = v
}
return out
}
// HistoryLen returns the number of recorded edits.
func (d *Document) HistoryLen() int {
d.mu.RLock()
defer d.mu.RUnlock()
return len(d.history)
}
// RedoLen returns the number of redoable edits.
func (d *Document) RedoLen() int {
d.mu.RLock()
defer d.mu.RUnlock()
return len(d.redo)
}
// ForEachChunk yields chunks covering [start,start+length) with the
// working-copy bytes visible. It is suitable for analysis passes that
// want bounded memory use.
func (d *Document) ForEachChunk(start, length int64, fn func(off int64, chunk []byte) error) error {
d.mu.RLock()
n := d.size
d.mu.RUnlock()
if start < 0 {
start = 0
}
end := start + length
if length < 0 || end > n {
end = n
}
off := start
for off < end {
chunkEnd := off + ChunkSize
if chunkEnd > end {
chunkEnd = end
}
buf, err := d.Range(off, chunkEnd-off)
if err != nil {
return err
}
if err := fn(off, buf); err != nil {
return err
}
off = chunkEnd
}
return nil
}
func (d *Document) sourceByteLocked(offset int64) (byte, error) {
buf, err := d.sourceRangeLocked(offset, 1)
if err != nil {
return 0, err
}
if len(buf) == 0 {
return 0, errors.New("offset out of range")
}
return buf[0], nil
}
func (d *Document) workingRangeLocked(start, length int64) ([]byte, error) {
buf, err := d.sourceRangeLocked(start, length)
if err != nil {
return nil, err
}
for off, b := range d.patch {
if off >= start && off < start+length {
buf[off-start] = b
}
}
return buf, nil
}
func (d *Document) sourceRangeLocked(start, length int64) ([]byte, error) {
if err := d.validateRangeLocked("range", start, length); err != nil {
return nil, err
}
if length == 0 {
return []byte{}, nil
}
if d.path == "" {
out := make([]byte, length)
copy(out, d.source[start:start+length])
return out, nil
}
f, err := os.Open(d.path)
if err != nil {
return nil, err
}
defer f.Close()
out := make([]byte, length)
_, err = f.ReadAt(out, start)
if errors.Is(err, io.EOF) {
err = nil
}
if err != nil {
return nil, err
}
return out, nil
}
func (d *Document) validateRangeLocked(label string, start, length int64) error {
if start < 0 || start > d.size || length < 0 || length > d.size-start {
return fmt.Errorf("%s range invalid", label)
}
return nil
}