You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/internal/testkit/edits.go

104 lines
3.0 KiB

package testkit
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"slices"
)
// Edit replaces Delete bytes at offset At of a base file with Insert. In JSON
// it is the array [at, delete, "insert in lowercase hex"].
//
// The edits of one file refer to offsets of the unmodified base, are sorted by
// At and do not overlap: the result is the base with every range
// [At, At+Delete) replaced by its Insert.
type Edit struct {
At, Delete int
Insert []byte
}
// MarshalJSON writes e as [at, delete, "hex"].
func (e Edit) MarshalJSON() ([]byte, error) {
return json.Marshal([]any{e.At, e.Delete, hex.EncodeToString(e.Insert)})
}
// UnmarshalJSON reads [at, delete, "hex"].
func (e *Edit) UnmarshalJSON(b []byte) error {
var raw []json.RawMessage
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
if len(raw) != 3 {
return fmt.Errorf("testkit: edit of %d elements, want 3", len(raw))
}
var s string
if err := errors.Join(json.Unmarshal(raw[0], &e.At), json.Unmarshal(raw[1], &e.Delete), json.Unmarshal(raw[2], &s)); err != nil {
return err
}
var err error
e.Insert, err = hex.DecodeString(s)
return err
}
// ApplyEdits returns a new file: base with edits applied.
func ApplyEdits(base []byte, edits []Edit) ([]byte, error) {
var out []byte
next := 0
for _, e := range edits {
if e.At < next || e.Delete < 0 || e.At+e.Delete > len(base) {
return nil, fmt.Errorf("testkit: edit [%d, %d] outside the base or out of order", e.At, e.Delete)
}
out = append(out, base[next:e.At]...)
out = append(out, e.Insert...)
next = e.At + e.Delete
}
return append(out, base[next:]...), nil
}
// Splice returns the edits that turn base into out: one edit covering the
// bytes between their common prefix and their common suffix, or none when
// they are equal.
func Splice(base, out []byte) []Edit {
return trimEdits(base, []Edit{{At: 0, Delete: len(base), Insert: out}})
}
// trimEdits drops from each edit the leading and trailing bytes it leaves
// unchanged, and then the edits that change nothing.
func trimEdits(base []byte, edits []Edit) []Edit {
var out []Edit
for _, e := range edits {
old, ins := base[e.At:e.At+e.Delete], e.Insert
p := 0
for p < len(old) && p < len(ins) && old[p] == ins[p] {
p++
}
old, ins = old[p:], ins[p:]
s := 0
for s < len(old) && s < len(ins) && old[len(old)-1-s] == ins[len(ins)-1-s] {
s++
}
old, ins = old[:len(old)-s], ins[:len(ins)-s]
if len(old) == 0 && len(ins) == 0 {
continue
}
out = append(out, Edit{At: e.At + p, Delete: len(old), Insert: bytes.Clone(ins)})
}
return out
}
// normalizeEdits sorts edits by offset and trims them. Overlapping edits are
// an error of the caller.
func normalizeEdits(base []byte, edits []Edit) ([]Edit, error) {
edits = slices.Clone(edits)
slices.SortStableFunc(edits, func(a, b Edit) int { return a.At - b.At })
for i := 1; i < len(edits); i++ {
if edits[i].At < edits[i-1].At+edits[i-1].Delete {
return nil, fmt.Errorf("testkit: overlapping edits at %d and %d", edits[i-1].At, edits[i].At)
}
}
return trimEdits(base, edits), nil
}

Powered by TurnKey Linux.