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

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
}
Format 3, step 6d: the mutations of format 3 The mutation corpus of spec 64 gains format 3: 209 cases, 169 of the spec. - The 33 mutations of the first two lists on format3_single and format3_time_and_key_portable, named "format 3: ...", with a sibling written by EncryptFiles and built capsules that hold a BODY. - The list of format 3, 47 cases: one for each value of a line with several (AREA_LEN 0, 511, 513 and 66048; SECURITY_LEN 0 and 513; HEAD_LEN 0 and 2^24 + 1), and the three that open without a code, with the verdicts X, F1 and S1. VERSION 4 is "format 3: version changed", as in format 2. Each seals BODY again with FK_PAYLOAD and the nonce of its fixture, and the control with the new L when L changes; the two that need a head followed by another STREAM chunk derive from format3_tree, whose comment takes the bytes the path loses so that only the head and its chunk change. - Further cases: format 3 relabeled 1, and time_and_key relabeled 2 with the identity and with the .dkk. - A mutation may expect the capsule to open with its verdicts; the exported case records them, with the result ok at step 0. - Splice gives an edit for each run of changed bytes, runs closer than 16 bytes merged, and one more for what one side has beyond the other: a head sealed again changes its bytes and the tag of its chunk, 64 KiB apart. Earlier cases are written with more edits and give the same capsules. The corpus is 706 KB, 476 KB of them the capsule of 65536 implicit folders, whose head is 235 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// 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 {
Format 3, step 6d: the mutations of format 3 The mutation corpus of spec 64 gains format 3: 209 cases, 169 of the spec. - The 33 mutations of the first two lists on format3_single and format3_time_and_key_portable, named "format 3: ...", with a sibling written by EncryptFiles and built capsules that hold a BODY. - The list of format 3, 47 cases: one for each value of a line with several (AREA_LEN 0, 511, 513 and 66048; SECURITY_LEN 0 and 513; HEAD_LEN 0 and 2^24 + 1), and the three that open without a code, with the verdicts X, F1 and S1. VERSION 4 is "format 3: version changed", as in format 2. Each seals BODY again with FK_PAYLOAD and the nonce of its fixture, and the control with the new L when L changes; the two that need a head followed by another STREAM chunk derive from format3_tree, whose comment takes the bytes the path loses so that only the head and its chunk change. - Further cases: format 3 relabeled 1, and time_and_key relabeled 2 with the identity and with the .dkk. - A mutation may expect the capsule to open with its verdicts; the exported case records them, with the result ok at step 0. - Splice gives an edit for each run of changed bytes, runs closer than 16 bytes merged, and one more for what one side has beyond the other: a head sealed again changes its bytes and the tag of its chunk, 64 KiB apart. Earlier cases are written with more edits and give the same capsules. The corpus is 706 KB, 476 KB of them the capsule of 65536 implicit folders, whose head is 235 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
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
}

Powered by TurnKey Linux.