diff --git a/.golangci.yml b/.golangci.yml index 12cdbd2..82dbad2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,6 +11,7 @@ linters: - golint - govet - ineffassign + - misspell - typecheck - unconvert - varcheck diff --git a/README.md b/README.md index b7522e3..58bb675 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ if err != nil { // apply the changes in the patch to a source file var output bytes.Buffer -if err := gitdiff.NewApplier(code).ApplyFile(&output, files[0]); err != nil { +if err := gitdiff.Apply(&output, code, files[0]); err != nil { log.Fatal(err) } ``` diff --git a/gitdiff/apply.go b/gitdiff/apply.go index 05a4526..cc4c3f0 100644 --- a/gitdiff/apply.go +++ b/gitdiff/apply.go @@ -89,85 +89,36 @@ func applyError(err error, args ...interface{}) error { var ( errApplyInProgress = errors.New("gitdiff: incompatible apply in progress") + errApplierClosed = errors.New("gitdiff: applier is closed") ) -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. +// Apply applies the changes in f to src, writing the result to dst. It can +// apply both text and binary changes. // -// 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} +// If an error occurs while applying, Apply returns an *ApplyError that +// annotates the error with additional information. If the error is because of +// a conflict with the source, the wrapped error will be a *Conflict. +func Apply(dst io.Writer, src io.ReaderAt, f *File) error { + if f.IsBinary { + if len(f.TextFragments) > 0 { + return applyError(errors.New("binary file contains text fragments")) + } + if f.BinaryFragment == nil { + return applyError(errors.New("binary file does not contain a binary fragment")) + } + } else { + if f.BinaryFragment != nil { + return applyError(errors.New("text file contains a binary fragment")) } - } - 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) + applier := NewBinaryApplier(dst, src) + if err := applier.ApplyFragment(f.BinaryFragment); err != nil { + return err + } + return applier.Close() case len(f.TextFragments) > 0: frags := make([]*TextFragment, len(f.TextFragments)) @@ -181,271 +132,17 @@ func (a *Applier) ApplyFile(dst io.Writer, f *File) error { // right now, the application fails if fragments overlap, but it should be // possible to precompute the result of applying them in order + applier := NewTextApplier(dst, src) for i, frag := range frags { - if err := a.ApplyTextFragment(dst, frag); err != nil { + if err := applier.ApplyFragment(frag); err != nil { return applyError(err, fragNum(i)) } } - } - - return applyError(a.Flush(dst)) -} + return applier.Close() -// 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) - if err != nil { - 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 { + // nothing to apply, just copy all the data + _, err := copyFrom(dst, src, 0) return err } - if !ok { - return &Conflict{"fragment src size does not match actual src size"} - } - return nil } diff --git a/gitdiff/apply_binary.go b/gitdiff/apply_binary.go new file mode 100644 index 0000000..b1ff4c1 --- /dev/null +++ b/gitdiff/apply_binary.go @@ -0,0 +1,206 @@ +package gitdiff + +import ( + "errors" + "fmt" + "io" +) + +// BinaryApplier applies binary changes described in a fragment to source data. +// The applier must be closed after use. +type BinaryApplier struct { + dst io.Writer + src io.ReaderAt + + closed bool + dirty bool +} + +// NewBinaryApplier creates an BinaryApplier that reads data from src and +// writes modified data to dst. +func NewBinaryApplier(dst io.Writer, src io.ReaderAt) *BinaryApplier { + a := BinaryApplier{ + dst: dst, + src: src, + } + return &a +} + +// ApplyFragment applies the changes in the fragment f and writes the result to +// dst. ApplyFragment can be called at most once. +// +// If an error occurs while applying, ApplyFragment returns an *ApplyError that +// annotates the error with additional information. If the error is because of +// a conflict between a fragment and the source, the wrapped error will be a +// *Conflict. +func (a *BinaryApplier) ApplyFragment(f *BinaryFragment) error { + if f == nil { + return applyError(errors.New("nil fragment")) + } + if a.closed { + return applyError(errApplierClosed) + } + if a.dirty { + return applyError(errApplyInProgress) + } + + // mark an apply as in progress, even if it fails before making changes + a.dirty = true + + switch f.Method { + case BinaryPatchLiteral: + if _, err := a.dst.Write(f.Data); err != nil { + return applyError(err) + } + case BinaryPatchDelta: + if err := applyBinaryDeltaFragment(a.dst, a.src, f.Data); err != nil { + return applyError(err) + } + default: + return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) + } + return nil +} + +// Close writes any data following the last applied fragment and prevents +// future calls to ApplyFragment. +func (a *BinaryApplier) Close() (err error) { + if a.closed { + return nil + } + + a.closed = true + if !a.dirty { + _, err = copyFrom(a.dst, a.src, 0) + } else { + // do nothing, applying a binary fragment copies all data + } + return err +} + +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 +} diff --git a/gitdiff/apply_test.go b/gitdiff/apply_test.go index d981e96..dd076bb 100644 --- a/gitdiff/apply_test.go +++ b/gitdiff/apply_test.go @@ -9,69 +9,6 @@ import ( "testing" ) -func TestApplierInvariants(t *testing.T) { - binary := &BinaryFragment{ - Method: BinaryPatchLiteral, - Size: 2, - Data: []byte("\xbe\xef"), - } - - text := &TextFragment{ - NewPosition: 1, - NewLines: 1, - LinesAdded: 1, - Lines: []Line{ - {Op: OpAdd, Line: "new line\n"}, - }, - } - - file := &File{ - TextFragments: []*TextFragment{text}, - } - - src := bytes.NewReader(nil) - dst := ioutil.Discard - - assertInProgress := func(t *testing.T, kind string, err error) { - if !errors.Is(err, errApplyInProgress) { - t.Fatalf("expected in-progress error for %s apply, but got: %v", kind, err) - } - } - - t.Run("binaryFirst", func(t *testing.T) { - a := NewApplier(src) - if err := a.ApplyBinaryFragment(dst, binary); err != nil { - t.Fatalf("unexpected error applying fragment: %v", err) - } - assertInProgress(t, "text", a.ApplyTextFragment(dst, text)) - assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary)) - assertInProgress(t, "file", a.ApplyFile(dst, file)) - }) - - t.Run("textFirst", func(t *testing.T) { - a := NewApplier(src) - if err := a.ApplyTextFragment(dst, text); err != nil { - t.Fatalf("unexpected error applying fragment: %v", err) - } - // additional text fragments are allowed - if err := a.ApplyTextFragment(dst, text); err != nil { - t.Fatalf("unexpected error applying second fragment: %v", err) - } - assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary)) - assertInProgress(t, "file", a.ApplyFile(dst, file)) - }) - - t.Run("fileFirst", func(t *testing.T) { - a := NewApplier(src) - if err := a.ApplyFile(dst, file); err != nil { - t.Fatalf("unexpected error applying file: %v", err) - } - assertInProgress(t, "text", a.ApplyTextFragment(dst, text)) - assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary)) - assertInProgress(t, "file", a.ApplyFile(dst, file)) - }) -} - func TestApplyTextFragment(t *testing.T) { tests := map[string]applyTest{ "createFile": {Files: getApplyFiles("text_fragment_new")}, @@ -127,11 +64,12 @@ func TestApplyTextFragment(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - test.run(t, func(w io.Writer, applier *Applier, file *File) error { + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { if len(file.TextFragments) != 1 { t.Fatalf("patch should contain exactly one fragment, but it has %d", len(file.TextFragments)) } - return applier.ApplyTextFragment(w, file.TextFragments[0]) + applier := NewTextApplier(dst, src) + return applier.ApplyFragment(file.TextFragments[0]) }) }) } @@ -176,8 +114,9 @@ func TestApplyBinaryFragment(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - test.run(t, func(w io.Writer, applier *Applier, file *File) error { - return applier.ApplyBinaryFragment(w, file.BinaryFragment) + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { + applier := NewBinaryApplier(dst, src) + return applier.ApplyFragment(file.BinaryFragment) }) }) } @@ -216,8 +155,8 @@ func TestApplyFile(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - test.run(t, func(w io.Writer, applier *Applier, file *File) error { - return applier.ApplyFile(w, file) + test.run(t, func(dst io.Writer, src io.ReaderAt, file *File) error { + return Apply(dst, src, file) }) }) } @@ -228,7 +167,7 @@ type applyTest struct { Err interface{} } -func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) error) { +func (at applyTest) run(t *testing.T, apply func(io.Writer, io.ReaderAt, *File) error) { src, patch, out := at.Files.Load(t) files, _, err := Parse(bytes.NewReader(patch)) @@ -239,10 +178,8 @@ func (at applyTest) run(t *testing.T, apply func(io.Writer, *Applier, *File) err t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) } - applier := NewApplier(bytes.NewReader(src)) - var dst bytes.Buffer - err = apply(&dst, applier, files[0]) + err = apply(&dst, bytes.NewReader(src), files[0]) if at.Err != nil { assertError(t, at.Err, err, "applying fragment") return diff --git a/gitdiff/apply_text.go b/gitdiff/apply_text.go new file mode 100644 index 0000000..a404552 --- /dev/null +++ b/gitdiff/apply_text.go @@ -0,0 +1,153 @@ +package gitdiff + +import ( + "io" +) + +// TextApplier applies changes described in text fragments to source data. If +// changes are described in multiple fragments, those fragments must be applied +// in order. The applier must be closed after use. +// +// By default, TextApplier operates in "strict" mode, where fragment content +// and positions must exactly match those of the source. +// +type TextApplier struct { + dst io.Writer + src io.ReaderAt + lineSrc LineReaderAt + nextLine int64 + + closed bool + dirty bool +} + +// NewTextApplier creates a TextApplier that reads data from src and writes +// modified data to dst. If src implements LineReaderAt, it is used directly. +func NewTextApplier(dst io.Writer, src io.ReaderAt) *TextApplier { + a := TextApplier{ + dst: dst, + src: src, + } + + if lineSrc, ok := src.(LineReaderAt); ok { + a.lineSrc = lineSrc + } else { + a.lineSrc = &lineReaderAt{r: src} + } + + return &a +} + +// ApplyFragment applies the changes in the fragment f, writing unwritten data +// before the start of the fragment and any changes from the fragment. If +// multiple text fragments apply to the same content, ApplyFragment must be +// called in order of increasing start position. As a result, each fragment can +// be applied at most once. +// +// If an error occurs while applying, ApplyFragment returns an *ApplyError that +// annotates the error with additional information. If the error is because of +// a conflict between the fragment and the source, the wrapped error will be a +// *Conflict. +func (a *TextApplier) ApplyFragment(f *TextFragment) error { + if a.closed { + return applyError(errApplierClosed) + } + + // mark an apply as in progress, even if it fails before making changes + a.dirty = true + + // 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) + if err != nil { + return applyError(err, lineNum(start+int64(n))) + } + + // copy leading data before the fragment starts + for i, line := range preimage[:fragStart-start] { + if _, err := a.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(a.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 +} + +// Close writes any data following the last applied fragment and prevents +// future calls to ApplyFragment. +func (a *TextApplier) Close() (err error) { + if a.closed { + return nil + } + + a.closed = true + if !a.dirty { + _, err = copyFrom(a.dst, a.src, 0) + } else { + _, err = copyLinesFrom(a.dst, a.lineSrc, a.nextLine) + } + return err +}