mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
194589f001
This ensures a default size is used for large copy instructions.
347 lines
9.1 KiB
Go
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
|
|
}
|