|
1 | 1 | package gitdiff |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
4 | 5 | "fmt" |
5 | 6 | "io" |
| 7 | + "io/ioutil" |
6 | 8 | ) |
7 | 9 |
|
8 | 10 | // Conflict indicates an apply failed due to a conflict between the patch and |
@@ -212,5 +214,128 @@ func copyLines(dst io.Writer, src LineReader, limit int64) (string, int64, error |
212 | 214 | // Unlike text fragments, binary fragments do not distinguish between strict |
213 | 215 | // and non-strict application. |
214 | 216 | func (f *BinaryFragment) Apply(dst io.Writer, src io.Reader) error { |
215 | | - panic("TODO(bkeyes): unimplemented") |
| 217 | + fullSrc, err := ioutil.ReadAll(src) |
| 218 | + if err != nil { |
| 219 | + return err |
| 220 | + } |
| 221 | + |
| 222 | + switch f.Method { |
| 223 | + case BinaryPatchLiteral: |
| 224 | + if _, err := dst.Write(f.Data); err != nil { |
| 225 | + return applyError(err) |
| 226 | + } |
| 227 | + case BinaryPatchDelta: |
| 228 | + if err := applyBinaryDeltaFragment(dst, fullSrc, f.Data); err != nil { |
| 229 | + return applyError(err) |
| 230 | + } |
| 231 | + } |
| 232 | + return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) |
| 233 | +} |
| 234 | + |
| 235 | +func applyBinaryDeltaFragment(dst io.Writer, src, frag []byte) error { |
| 236 | + srcSize, delta := readBinaryDeltaSize(frag) |
| 237 | + if srcSize != int64(len(src)) { |
| 238 | + return &Conflict{"fragment src size does not match actual src size"} |
| 239 | + } |
| 240 | + |
| 241 | + dstSize, delta := readBinaryDeltaSize(delta) |
| 242 | + |
| 243 | + for len(delta) > 0 { |
| 244 | + op := delta[0] |
| 245 | + if op == 0 { |
| 246 | + return errors.New("invalid delta opcode 0") |
| 247 | + } |
| 248 | + |
| 249 | + var n int64 |
| 250 | + var err error |
| 251 | + switch op & 0x80 { |
| 252 | + case 0x80: |
| 253 | + n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src) |
| 254 | + case 0x00: |
| 255 | + n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:]) |
| 256 | + } |
| 257 | + if err != nil { |
| 258 | + return err |
| 259 | + } |
| 260 | + dstSize -= n |
| 261 | + } |
| 262 | + |
| 263 | + if dstSize != 0 { |
| 264 | + return errors.New("corrupt binary delta: insufficient or extra data") |
| 265 | + } |
| 266 | + return nil |
| 267 | +} |
| 268 | + |
| 269 | +// readBinaryDeltaSize reads a variable length size from a delta-encoded binary |
| 270 | +// fragment, returing the size and the unused data. Data is encoded as: |
| 271 | +// |
| 272 | +// [[1xxxxxxx]...] [0xxxxxxx] |
| 273 | +// |
| 274 | +// in little-endian order, with 7 bits of the value per byte. |
| 275 | +func readBinaryDeltaSize(d []byte) (size int64, rest []byte) { |
| 276 | + shift := uint(0) |
| 277 | + for i, b := range d { |
| 278 | + size |= int64(b&0x7F) << shift |
| 279 | + shift += 7 |
| 280 | + if b <= 0x7F { |
| 281 | + return size, d[i+1:] |
| 282 | + } |
| 283 | + } |
| 284 | + return size, nil |
| 285 | +} |
| 286 | + |
| 287 | +// applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary |
| 288 | +// fragment, returning the amount of data written and the usused part of the |
| 289 | +// fragment. An add operation takes the form: |
| 290 | +// |
| 291 | +// [0xxxxxx][[data1]...] |
| 292 | +// |
| 293 | +// where the lower seven bits of the opcode is the number of data bytes |
| 294 | +// following the opcode. See also pack-format.txt in the Git source. |
| 295 | +func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) { |
| 296 | + size := int(op) |
| 297 | + if len(delta) < size { |
| 298 | + return 0, delta, errors.New("corrupt binary delta: incomplete add") |
| 299 | + } |
| 300 | + _, err = w.Write(delta[:size]) |
| 301 | + return int64(size), delta[size:], err |
| 302 | +} |
| 303 | + |
| 304 | +// applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary |
| 305 | +// fragment, returing the amount of data written and the unused part of the |
| 306 | +// fragment. A copy operation takes the form: |
| 307 | +// |
| 308 | +// [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3] |
| 309 | +// |
| 310 | +// where the lower seven bits of the opcode determine which non-zero offset and |
| 311 | +// size bytes are present in little-endian order: if bit 0 is set, offset1 is |
| 312 | +// present, etc. If no offset or size bytes are present, offset is 0 and size |
| 313 | +// is 0x10000. See also pack-format.txt in the Git source. |
| 314 | +func applyBinaryDeltaCopy(w io.Writer, op byte, delta, src []byte) (n int64, rest []byte, err error) { |
| 315 | + unpack := func(start, bits uint) (v int64) { |
| 316 | + for i := uint(0); i < bits; i++ { |
| 317 | + mask := byte(1 << (i + start)) |
| 318 | + if op&mask > 0 { |
| 319 | + if len(delta) == 0 { |
| 320 | + err = errors.New("corrupt binary delta: incomplete copy") |
| 321 | + return |
| 322 | + } |
| 323 | + v |= int64(delta[0]) << (8 * i) |
| 324 | + delta = delta[1:] |
| 325 | + } |
| 326 | + } |
| 327 | + return |
| 328 | + } |
| 329 | + |
| 330 | + offset := unpack(0, 4) |
| 331 | + size := unpack(4, 3) |
| 332 | + if err != nil { |
| 333 | + return 0, delta, err |
| 334 | + } |
| 335 | + if size == 0 { |
| 336 | + size = 0x10000 |
| 337 | + } |
| 338 | + |
| 339 | + _, err = w.Write(src[offset : offset+size]) |
| 340 | + return size, delta, err |
216 | 341 | } |
0 commit comments