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.
150 lines
4.3 KiB
150 lines
4.3 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
|
|
}
|
|
|
|
// spliceGap is the least run of equal bytes that separates two edits of
|
|
// Splice: shorter runs cost less inside an edit than a new edit costs.
|
|
const spliceGap = 16
|
|
|
|
// Splice returns the edits that turn base into out, none when they are
|
|
// equal. Between their common prefix and their common suffix, the bytes
|
|
// that both have at the same offset are compared one by one, and each run
|
|
// of changed bytes is an edit, runs closer than spliceGap bytes merged; what
|
|
// one has beyond the other is one more edit, a deletion or an insertion at
|
|
// its end. A change sealed again with the same key and nonce, such as a
|
|
// head, changes its bytes and the tag of their chunk, 64 KiB apart: two
|
|
// small edits instead of one of a whole chunk.
|
|
func Splice(base, out []byte) []Edit {
|
|
edits := trimEdits(base, []Edit{{At: 0, Delete: len(base), Insert: out}})
|
|
if len(edits) == 0 {
|
|
return edits
|
|
}
|
|
e := edits[0]
|
|
old, ins := base[e.At:e.At+e.Delete], e.Insert
|
|
m := min(len(old), len(ins))
|
|
var res []Edit
|
|
for i := 0; i < m; {
|
|
if old[i] == ins[i] {
|
|
i++
|
|
continue
|
|
}
|
|
j := i + 1
|
|
for j < m {
|
|
if old[j] != ins[j] {
|
|
j++
|
|
continue
|
|
}
|
|
k := j
|
|
for k < m && old[k] == ins[k] && k-j < spliceGap {
|
|
k++
|
|
}
|
|
if k-j >= spliceGap || k == m {
|
|
break
|
|
}
|
|
j = k
|
|
}
|
|
res = append(res, Edit{At: e.At + i, Delete: j - i, Insert: bytes.Clone(ins[i:j])})
|
|
i = j
|
|
}
|
|
switch {
|
|
case len(old) > m:
|
|
res = append(res, Edit{At: e.At + m, Delete: len(old) - m})
|
|
case len(ins) > m:
|
|
res = append(res, Edit{At: e.At + m, Insert: bytes.Clone(ins[m:])})
|
|
}
|
|
return res
|
|
}
|
|
|
|
// 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
|
|
}
|