mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
131a046908
This better matches the actual contract of the delta patch, which reads fixed size chunks of the source files at arbitrary positions.
370 lines
9.6 KiB
Go
370 lines
9.6 KiB
Go
package gitdiff
|
|
|
|
import (
|
|
"bytes"
|
|
"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 {
|
|
e = &ApplyError{err: wrapEOF(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 {
|
|
data, err := ioutil.ReadAll(src)
|
|
if err != nil {
|
|
return applyError(err)
|
|
}
|
|
if f.BinaryFragment != nil {
|
|
return f.BinaryFragment.Apply(dst, bytes.NewReader(data))
|
|
}
|
|
_, err = dst.Write(data)
|
|
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:
|
|
return line, n, wrapEOF(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.ReaderAt) error {
|
|
switch f.Method {
|
|
case BinaryPatchLiteral:
|
|
if _, err := dst.Write(f.Data); err != nil {
|
|
return applyError(err)
|
|
}
|
|
case BinaryPatchDelta:
|
|
if err := applyBinaryDeltaFragment(dst, src, 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 io.ReaderAt, frag []byte) error {
|
|
srcSize, delta := readBinaryDeltaSize(frag)
|
|
if err := checkBinarySrcSize(srcSize, src); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 []byte, src io.ReaderAt) (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
|
|
}
|
|
|
|
// TODO(bkeyes): consider pooling these buffers
|
|
b := make([]byte, size)
|
|
if _, err := src.ReadAt(b, offset); err != nil {
|
|
return 0, delta, wrapEOF(err)
|
|
}
|
|
|
|
_, err = w.Write(b)
|
|
return size, delta, err
|
|
}
|
|
|
|
func checkBinarySrcSize(size int64, src io.ReaderAt) error {
|
|
start := size
|
|
if start > 0 {
|
|
start--
|
|
}
|
|
var b [2]byte
|
|
n, err := src.ReadAt(b[:], start)
|
|
if err == io.EOF && (size == 0 && n == 0) || (size > 0 && n == 1) {
|
|
return nil
|
|
}
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
return &Conflict{"fragment src size does not match actual src size"}
|
|
}
|
|
|
|
func wrapEOF(err error) error {
|
|
if err == io.EOF {
|
|
err = io.ErrUnexpectedEOF
|
|
}
|
|
return err
|
|
}
|