|
|
|
|
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
|
|
|
|
|
}
|