fork of go-gitdiff with jj support
1package gitdiff
2
3import (
4 "errors"
5 "fmt"
6 "io"
7)
8
9// BinaryApplier applies binary changes described in a fragment to source data.
10// The applier must be closed after use.
11type BinaryApplier struct {
12 dst io.Writer
13 src io.ReaderAt
14
15 closed bool
16 dirty bool
17}
18
19// NewBinaryApplier creates an BinaryApplier that reads data from src and
20// writes modified data to dst.
21func NewBinaryApplier(dst io.Writer, src io.ReaderAt) *BinaryApplier {
22 a := BinaryApplier{
23 dst: dst,
24 src: src,
25 }
26 return &a
27}
28
29// ApplyFragment applies the changes in the fragment f and writes the result to
30// dst. ApplyFragment can be called at most once.
31//
32// If an error occurs while applying, ApplyFragment returns an *ApplyError that
33// annotates the error with additional information. If the error is because of
34// a conflict between a fragment and the source, the wrapped error will be a
35// *Conflict.
36func (a *BinaryApplier) ApplyFragment(f *BinaryFragment) error {
37 if f == nil {
38 return applyError(errors.New("nil fragment"))
39 }
40 if a.closed {
41 return applyError(errApplierClosed)
42 }
43 if a.dirty {
44 return applyError(errApplyInProgress)
45 }
46
47 // mark an apply as in progress, even if it fails before making changes
48 a.dirty = true
49
50 switch f.Method {
51 case BinaryPatchLiteral:
52 if _, err := a.dst.Write(f.Data); err != nil {
53 return applyError(err)
54 }
55 case BinaryPatchDelta:
56 if err := applyBinaryDeltaFragment(a.dst, a.src, f.Data); err != nil {
57 return applyError(err)
58 }
59 default:
60 return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method))
61 }
62 return nil
63}
64
65// Close writes any data following the last applied fragment and prevents
66// future calls to ApplyFragment.
67func (a *BinaryApplier) Close() (err error) {
68 if a.closed {
69 return nil
70 }
71
72 a.closed = true
73 if !a.dirty {
74 _, err = copyFrom(a.dst, a.src, 0)
75 } else {
76 // do nothing, applying a binary fragment copies all data
77 }
78 return err
79}
80
81func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error {
82 srcSize, delta := readBinaryDeltaSize(frag)
83 if err := checkBinarySrcSize(src, srcSize); err != nil {
84 return err
85 }
86
87 dstSize, delta := readBinaryDeltaSize(delta)
88
89 for len(delta) > 0 {
90 op := delta[0]
91 if op == 0 {
92 return errors.New("invalid delta opcode 0")
93 }
94
95 var n int64
96 var err error
97 switch op & 0x80 {
98 case 0x80:
99 n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src)
100 case 0x00:
101 n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:])
102 }
103 if err != nil {
104 return err
105 }
106 dstSize -= n
107 }
108
109 if dstSize != 0 {
110 return errors.New("corrupt binary delta: insufficient or extra data")
111 }
112 return nil
113}
114
115// readBinaryDeltaSize reads a variable length size from a delta-encoded binary
116// fragment, returing the size and the unused data. Data is encoded as:
117//
118// [[1xxxxxxx]...] [0xxxxxxx]
119//
120// in little-endian order, with 7 bits of the value per byte.
121func readBinaryDeltaSize(d []byte) (size int64, rest []byte) {
122 shift := uint(0)
123 for i, b := range d {
124 size |= int64(b&0x7F) << shift
125 shift += 7
126 if b <= 0x7F {
127 return size, d[i+1:]
128 }
129 }
130 return size, nil
131}
132
133// applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary
134// fragment, returning the amount of data written and the usused part of the
135// fragment. An add operation takes the form:
136//
137// [0xxxxxx][[data1]...]
138//
139// where the lower seven bits of the opcode is the number of data bytes
140// following the opcode. See also pack-format.txt in the Git source.
141func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) {
142 size := int(op)
143 if len(delta) < size {
144 return 0, delta, errors.New("corrupt binary delta: incomplete add")
145 }
146 _, err = w.Write(delta[:size])
147 return int64(size), delta[size:], err
148}
149
150// applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary
151// fragment, returing the amount of data written and the unused part of the
152// fragment. A copy operation takes the form:
153//
154// [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3]
155//
156// where the lower seven bits of the opcode determine which non-zero offset and
157// size bytes are present in little-endian order: if bit 0 is set, offset1 is
158// present, etc. If no offset or size bytes are present, offset is 0 and size
159// is 0x10000. See also pack-format.txt in the Git source.
160func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (n int64, rest []byte, err error) {
161 const defaultSize = 0x10000
162
163 unpack := func(start, bits uint) (v int64) {
164 for i := uint(0); i < bits; i++ {
165 mask := byte(1 << (i + start))
166 if op&mask > 0 {
167 if len(delta) == 0 {
168 err = errors.New("corrupt binary delta: incomplete copy")
169 return
170 }
171 v |= int64(delta[0]) << (8 * i)
172 delta = delta[1:]
173 }
174 }
175 return
176 }
177
178 offset := unpack(0, 4)
179 size := unpack(4, 3)
180 if err != nil {
181 return 0, delta, err
182 }
183 if size == 0 {
184 size = defaultSize
185 }
186
187 // TODO(bkeyes): consider pooling these buffers
188 b := make([]byte, size)
189 if _, err := src.ReadAt(b, offset); err != nil {
190 return 0, delta, err
191 }
192
193 _, err = w.Write(b)
194 return size, delta, err
195}
196
197func checkBinarySrcSize(r io.ReaderAt, size int64) error {
198 ok, err := isLen(r, size)
199 if err != nil {
200 return err
201 }
202 if !ok {
203 return &Conflict{"fragment src size does not match actual src size"}
204 }
205 return nil
206}