mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-12-19 09:28:29 +00:00
455 lines
12 KiB
Go
455 lines
12 KiB
Go
|
package gitdiff
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"sort"
|
||
|
)
|
||
|
|
||
|
// 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 int64
|
||
|
// 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 = int64(v) + 1
|
||
|
case fragNum:
|
||
|
e.Fragment = int(v) + 1
|
||
|
case fragLineNum:
|
||
|
e.FragmentLine = int(v) + 1
|
||
|
}
|
||
|
}
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
errApplyInProgress = errors.New("gitdiff: incompatible apply in progress")
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
applyInitial = iota
|
||
|
applyText
|
||
|
applyBinary
|
||
|
applyFile
|
||
|
)
|
||
|
|
||
|
// Apply is a convenience function that creates an Applier for src with default
|
||
|
// settings and applies the changes in f, writing the result to dst.
|
||
|
func Apply(dst io.Writer, src io.ReaderAt, f *File) error {
|
||
|
return NewApplier(src).ApplyFile(dst, f)
|
||
|
}
|
||
|
|
||
|
// Applier applies changes described in fragments to source data. If changes
|
||
|
// are described in multiple fragments, those fragments must be applied in
|
||
|
// order, usually by calling ApplyFile.
|
||
|
//
|
||
|
// By default, Applier operates in "strict" mode, where fragment content and
|
||
|
// positions must exactly match those of the source.
|
||
|
//
|
||
|
// If an error occurs while applying, methods on Applier return instances of
|
||
|
// *ApplyError that annotate the wrapped error with additional information
|
||
|
// when available. If the error is because of a conflict between a fragment and
|
||
|
// the source, the wrapped error will be a *Conflict.
|
||
|
//
|
||
|
// While an Applier can apply both text and binary fragments, only one fragment
|
||
|
// type can be used without resetting the Applier. The first fragment applied
|
||
|
// sets the type for the Applier. Mixing fragment types or mixing
|
||
|
// fragment-level and file-level applies results in an error.
|
||
|
type Applier struct {
|
||
|
src io.ReaderAt
|
||
|
lineSrc LineReaderAt
|
||
|
nextLine int64
|
||
|
applyType int
|
||
|
}
|
||
|
|
||
|
// NewApplier creates an Applier that reads data from src. If src is a
|
||
|
// LineReaderAt, it is used directly to apply text fragments.
|
||
|
func NewApplier(src io.ReaderAt) *Applier {
|
||
|
a := new(Applier)
|
||
|
a.Reset(src)
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
// Reset resets the input and internal state of the Applier. If src is nil, the
|
||
|
// existing source is reused.
|
||
|
func (a *Applier) Reset(src io.ReaderAt) {
|
||
|
if src != nil {
|
||
|
a.src = src
|
||
|
if lineSrc, ok := src.(LineReaderAt); ok {
|
||
|
a.lineSrc = lineSrc
|
||
|
} else {
|
||
|
a.lineSrc = &lineReaderAt{r: src}
|
||
|
}
|
||
|
}
|
||
|
a.nextLine = 0
|
||
|
a.applyType = applyInitial
|
||
|
}
|
||
|
|
||
|
// ApplyFile applies the changes in all of the fragments of f and writes the
|
||
|
// result to dst.
|
||
|
func (a *Applier) ApplyFile(dst io.Writer, f *File) error {
|
||
|
if a.applyType != applyInitial {
|
||
|
return applyError(errApplyInProgress)
|
||
|
}
|
||
|
defer func() { a.applyType = applyFile }()
|
||
|
|
||
|
if f.IsBinary && len(f.TextFragments) > 0 {
|
||
|
return applyError(errors.New("binary file contains text fragments"))
|
||
|
}
|
||
|
if !f.IsBinary && f.BinaryFragment != nil {
|
||
|
return applyError(errors.New("text file contains binary fragment"))
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case f.BinaryFragment != nil:
|
||
|
return a.ApplyBinaryFragment(dst, f.BinaryFragment)
|
||
|
|
||
|
case len(f.TextFragments) > 0:
|
||
|
frags := make([]*TextFragment, len(f.TextFragments))
|
||
|
copy(frags, f.TextFragments)
|
||
|
|
||
|
sort.Slice(frags, func(i, j int) bool {
|
||
|
return frags[i].OldPosition < frags[j].OldPosition
|
||
|
})
|
||
|
|
||
|
// TODO(bkeyes): consider merging overlapping fragments
|
||
|
// right now, the application fails if fragments overlap, but it should be
|
||
|
// possible to precompute the result of applying them in order
|
||
|
|
||
|
for i, frag := range frags {
|
||
|
if err := a.ApplyTextFragment(dst, frag); err != nil {
|
||
|
return applyError(err, fragNum(i))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return applyError(a.Flush(dst))
|
||
|
}
|
||
|
|
||
|
// ApplyTextFragment applies the changes in the fragment f and writes unwritten
|
||
|
// data before the start of the fragment and the result to dst. If multiple
|
||
|
// text fragments apply to the same source, ApplyTextFragment must be called in
|
||
|
// order of increasing start position. As a result, each fragment can be
|
||
|
// applied at most once before a call to Reset.
|
||
|
func (a *Applier) ApplyTextFragment(dst io.Writer, f *TextFragment) error {
|
||
|
if a.applyType != applyInitial && a.applyType != applyText {
|
||
|
return applyError(errApplyInProgress)
|
||
|
}
|
||
|
defer func() { a.applyType = applyText }()
|
||
|
|
||
|
// application code assumes fragment fields are consistent
|
||
|
if err := f.Validate(); err != nil {
|
||
|
return applyError(err)
|
||
|
}
|
||
|
|
||
|
// lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
|
||
|
fragStart := f.OldPosition - 1
|
||
|
if fragStart < 0 {
|
||
|
fragStart = 0
|
||
|
}
|
||
|
fragEnd := fragStart + f.OldLines
|
||
|
|
||
|
start := a.nextLine
|
||
|
if fragStart < start {
|
||
|
return applyError(&Conflict{"fragment overlaps with an applied fragment"})
|
||
|
}
|
||
|
|
||
|
if f.OldPosition == 0 {
|
||
|
ok, err := isLen(a.src, 0)
|
||
|
if err != nil {
|
||
|
return applyError(err)
|
||
|
}
|
||
|
if !ok {
|
||
|
return applyError(&Conflict{"cannot create new file from non-empty src"})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
preimage := make([][]byte, fragEnd-start)
|
||
|
n, err := a.lineSrc.ReadLinesAt(preimage, start)
|
||
|
switch {
|
||
|
case err == nil:
|
||
|
case err == io.EOF && n == len(preimage): // last line of frag has no newline character
|
||
|
default:
|
||
|
return applyError(err, lineNum(start+int64(n)))
|
||
|
}
|
||
|
|
||
|
// copy leading data before the fragment starts
|
||
|
for i, line := range preimage[:fragStart-start] {
|
||
|
if _, err := dst.Write(line); err != nil {
|
||
|
a.nextLine = start + int64(i)
|
||
|
return applyError(err, lineNum(a.nextLine))
|
||
|
}
|
||
|
}
|
||
|
preimage = preimage[fragStart-start:]
|
||
|
|
||
|
// apply the changes in the fragment
|
||
|
used := int64(0)
|
||
|
for i, line := range f.Lines {
|
||
|
if err := applyTextLine(dst, line, preimage, used); err != nil {
|
||
|
a.nextLine = fragStart + used
|
||
|
return applyError(err, lineNum(a.nextLine), fragLineNum(i))
|
||
|
}
|
||
|
if line.Old() {
|
||
|
used++
|
||
|
}
|
||
|
}
|
||
|
a.nextLine = fragStart + used
|
||
|
|
||
|
// new position of +0,0 mean a full delete, so check for leftovers
|
||
|
if f.NewPosition == 0 && f.NewLines == 0 {
|
||
|
var b [1][]byte
|
||
|
n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine)
|
||
|
if err != nil && err != io.EOF {
|
||
|
return applyError(err, lineNum(a.nextLine))
|
||
|
}
|
||
|
if n > 0 {
|
||
|
return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
|
||
|
if line.Old() && string(preimage[i]) != line.Line {
|
||
|
return &Conflict{"fragment line does not match src line"}
|
||
|
}
|
||
|
if line.New() {
|
||
|
_, err = io.WriteString(dst, line.Line)
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Flush writes any data following the last applied fragment to dst.
|
||
|
func (a *Applier) Flush(dst io.Writer) (err error) {
|
||
|
switch a.applyType {
|
||
|
case applyInitial:
|
||
|
_, err = copyFrom(dst, a.src, 0)
|
||
|
case applyText:
|
||
|
_, err = copyLinesFrom(dst, a.lineSrc, a.nextLine)
|
||
|
case applyBinary:
|
||
|
// nothing to flush, binary apply "consumes" full source
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// ApplyBinaryFragment applies the changes in the fragment f and writes the
|
||
|
// result to dst. At most one binary fragment can be applied before a call to
|
||
|
// Reset.
|
||
|
func (a *Applier) ApplyBinaryFragment(dst io.Writer, f *BinaryFragment) error {
|
||
|
if a.applyType != applyInitial {
|
||
|
return applyError(errApplyInProgress)
|
||
|
}
|
||
|
defer func() { a.applyType = applyBinary }()
|
||
|
|
||
|
if f == nil {
|
||
|
return applyError(errors.New("nil fragment"))
|
||
|
}
|
||
|
|
||
|
switch f.Method {
|
||
|
case BinaryPatchLiteral:
|
||
|
if _, err := dst.Write(f.Data); err != nil {
|
||
|
return applyError(err)
|
||
|
}
|
||
|
case BinaryPatchDelta:
|
||
|
if err := applyBinaryDeltaFragment(dst, a.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(src, srcSize); 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, err
|
||
|
}
|
||
|
|
||
|
_, err = w.Write(b)
|
||
|
return size, delta, err
|
||
|
}
|
||
|
|
||
|
func checkBinarySrcSize(r io.ReaderAt, size int64) error {
|
||
|
ok, err := isLen(r, size)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if !ok {
|
||
|
return &Conflict{"fragment src size does not match actual src size"}
|
||
|
}
|
||
|
return nil
|
||
|
}
|