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 }