Files
bluekeyes-go-gitdiff/gitdiff/apply.go
T
Billy Keyes 194589f001 Add test for large binary delta application
This ensures a default size is used for large copy instructions.
2020-01-16 21:52:41 -08:00

347 lines
9.1 KiB
Go

package gitdiff
import (
"errors"
"fmt"
"io"
"io/ioutil"
)
// Conflict indicates an apply failed due to a conflict between the patch and
// the source content.
//
// Users can test if an error was caused by a conflict by using errors.Is with
// an empty Conflict:
//
// if errors.Is(err, &Conflict{}) {
// // handle conflict
// }
//
type Conflict struct {
msg string
}
func (c *Conflict) Error() string {
return "conflict: " + c.msg
}
// Is implements error matching for Conflict. Passing an empty instance of
// Conflict always returns true.
func (c *Conflict) Is(other error) bool {
if other, ok := other.(*Conflict); ok {
return other.msg == "" || other.msg == c.msg
}
return false
}
// ApplyError wraps an error that occurs during patch application with
// additional location information, if it is available.
type ApplyError struct {
// Line is the one-indexed line number in the source data
Line int
// Fragment is the one-indexed fragment number in the file
Fragment int
// FragmentLine is the one-indexed line number in the fragment
FragmentLine int
err error
}
// Unwrap returns the wrapped error.
func (e *ApplyError) Unwrap() error {
return e.err
}
func (e *ApplyError) Error() string {
return fmt.Sprintf("%v", e.err)
}
type lineNum int
type fragNum int
type fragLineNum int
// applyError creates a new *ApplyError wrapping err or augments the information
// in err with args if it is already an *ApplyError. Returns nil if err is nil.
func applyError(err error, args ...interface{}) error {
if err == nil {
return nil
}
e, ok := err.(*ApplyError)
if !ok {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
e = &ApplyError{err: err}
}
for _, arg := range args {
switch v := arg.(type) {
case lineNum:
e.Line = int(v) + 1
case fragNum:
e.Fragment = int(v) + 1
case fragLineNum:
e.FragmentLine = int(v) + 1
}
}
return e
}
// ApplyStrict writes data from src to dst, modifying it as described by the
// fragments in the file. For text files, each fragment, including all context
// lines, must exactly match src at the expected line number.
//
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
// Partial data may be written to dst in this case.
func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error {
if f.IsBinary {
if f.BinaryFragment != nil {
return f.BinaryFragment.Apply(dst, src)
}
_, err := io.Copy(dst, src)
return applyError(err)
}
lr, ok := src.(LineReader)
if !ok {
lr = NewLineReader(src, 0)
}
for i, frag := range f.TextFragments {
if err := frag.ApplyStrict(dst, lr); err != nil {
return applyError(err, fragNum(i))
}
}
_, err := io.Copy(dst, unwrapLineReader(lr))
return applyError(err)
}
// ApplyStrict writes data from src to dst, modifying it as described by the
// fragment. The fragment, including all context lines, must exactly match src
// at the expected line number.
//
// If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause.
// Partial data may be written to dst in this case. If there is no error, the
// next read from src returns the line immediately after the last line of the
// fragment.
func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error {
// application code assumes fragment fields are consistent
if err := f.Validate(); err != nil {
return applyError(err)
}
// line numbers are zero-indexed, positions are one-indexed
limit := f.OldPosition - 1
// io.EOF is acceptable here: the first line of the patch is the last of
// the source and it has no newline character
nextLine, n, err := copyLines(dst, src, limit)
if err != nil && err != io.EOF {
return applyError(err, lineNum(n))
}
used := int64(0)
for i, line := range f.Lines {
if err := applyTextLine(dst, nextLine, line); err != nil {
return applyError(err, lineNum(n), fragLineNum(i))
}
if line.Old() {
used++
}
// advance reader if the next fragment line appears in src and we're behind
if i < len(f.Lines)-1 && f.Lines[i+1].Old() && int64(n)-limit < used {
nextLine, n, err = src.ReadLine()
switch {
case err == io.EOF && f.Lines[i+1].NoEOL():
continue
case err != nil:
return applyError(err, lineNum(n), fragLineNum(i+1)) // report for _next_ line in fragment
}
}
}
return nil
}
func applyTextLine(dst io.Writer, src string, line Line) (err error) {
switch line.Op {
case OpContext, OpDelete:
if src != line.Line {
return &Conflict{"fragment line does not match src line"}
}
}
switch line.Op {
case OpContext, OpAdd:
_, err = io.WriteString(dst, line.Line)
}
return
}
// copyLines copies from src to dst until the line at limit, exclusive. Returns
// the line at limit and the line number. If the error is nil or io.EOF, the
// line number equals limit. A negative limit checks that the source has no
// more lines to read.
func copyLines(dst io.Writer, src LineReader, limit int64) (string, int64, error) {
for {
line, n, err := src.ReadLine()
switch {
case limit < 0 && err == io.EOF && line == "":
return "", limit, nil
case n == limit:
return line, n, err
case n > limit:
if limit < 0 {
return "", n, &Conflict{"cannot create new file from non-empty src"}
}
return "", n, &Conflict{"fragment overlaps with an applied fragment"}
case err != nil:
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return line, n, err
}
if _, err := io.WriteString(dst, line); err != nil {
return "", n, err
}
}
}
// Apply writes data from src to dst, modifying it as described by the
// fragment.
//
// Unlike text fragments, binary fragments do not distinguish between strict
// and non-strict application.
func (f *BinaryFragment) Apply(dst io.Writer, src io.Reader) error {
fullSrc, err := ioutil.ReadAll(src)
if err != nil {
return err
}
switch f.Method {
case BinaryPatchLiteral:
if _, err := dst.Write(f.Data); err != nil {
return applyError(err)
}
case BinaryPatchDelta:
if err := applyBinaryDeltaFragment(dst, fullSrc, f.Data); err != nil {
return applyError(err)
}
default:
return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method))
}
return nil
}
func applyBinaryDeltaFragment(dst io.Writer, src, frag []byte) error {
srcSize, delta := readBinaryDeltaSize(frag)
if srcSize != int64(len(src)) {
return &Conflict{"fragment src size does not match actual src size"}
}
dstSize, delta := readBinaryDeltaSize(delta)
for len(delta) > 0 {
op := delta[0]
if op == 0 {
return errors.New("invalid delta opcode 0")
}
var n int64
var err error
switch op & 0x80 {
case 0x80:
n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src)
case 0x00:
n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:])
}
if err != nil {
return err
}
dstSize -= n
}
if dstSize != 0 {
return errors.New("corrupt binary delta: insufficient or extra data")
}
return nil
}
// readBinaryDeltaSize reads a variable length size from a delta-encoded binary
// fragment, returing the size and the unused data. Data is encoded as:
//
// [[1xxxxxxx]...] [0xxxxxxx]
//
// in little-endian order, with 7 bits of the value per byte.
func readBinaryDeltaSize(d []byte) (size int64, rest []byte) {
shift := uint(0)
for i, b := range d {
size |= int64(b&0x7F) << shift
shift += 7
if b <= 0x7F {
return size, d[i+1:]
}
}
return size, nil
}
// applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary
// fragment, returning the amount of data written and the usused part of the
// fragment. An add operation takes the form:
//
// [0xxxxxx][[data1]...]
//
// where the lower seven bits of the opcode is the number of data bytes
// following the opcode. See also pack-format.txt in the Git source.
func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) {
size := int(op)
if len(delta) < size {
return 0, delta, errors.New("corrupt binary delta: incomplete add")
}
_, err = w.Write(delta[:size])
return int64(size), delta[size:], err
}
// applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary
// fragment, returing the amount of data written and the unused part of the
// fragment. A copy operation takes the form:
//
// [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3]
//
// where the lower seven bits of the opcode determine which non-zero offset and
// size bytes are present in little-endian order: if bit 0 is set, offset1 is
// present, etc. If no offset or size bytes are present, offset is 0 and size
// is 0x10000. See also pack-format.txt in the Git source.
func applyBinaryDeltaCopy(w io.Writer, op byte, delta, src []byte) (n int64, rest []byte, err error) {
const defaultSize = 0x10000
unpack := func(start, bits uint) (v int64) {
for i := uint(0); i < bits; i++ {
mask := byte(1 << (i + start))
if op&mask > 0 {
if len(delta) == 0 {
err = errors.New("corrupt binary delta: incomplete copy")
return
}
v |= int64(delta[0]) << (8 * i)
delta = delta[1:]
}
}
return
}
offset := unpack(0, 4)
size := unpack(4, 3)
if err != nil {
return 0, delta, err
}
if size == 0 {
size = defaultSize
}
_, err = w.Write(src[offset : offset+size])
return size, delta, err
}