Skip to content

Commit 19ec1a2

Browse files
committed
Improve detection of in-progress application
Applying a file now rejects future text fragment applies under the assumption that all relevant fragments were part of the file.
1 parent 0f3872a commit 19ec1a2

File tree

2 files changed

+98
-21
lines changed

2 files changed

+98
-21
lines changed

gitdiff/apply.go

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"io"
7+
"sort"
78
)
89

910
// Conflict indicates an apply failed due to a conflict between the patch and
@@ -68,7 +69,10 @@ func applyError(err error, args ...interface{}) error {
6869

6970
e, ok := err.(*ApplyError)
7071
if !ok {
71-
e = &ApplyError{err: wrapEOF(err)}
72+
if err == io.EOF {
73+
err = io.ErrUnexpectedEOF
74+
}
75+
e = &ApplyError{err: err}
7276
}
7377
for _, arg := range args {
7478
switch v := arg.(type) {
@@ -91,6 +95,7 @@ const (
9195
applyInitial = iota
9296
applyText
9397
applyBinary
98+
applyFile
9499
)
95100

96101
// Applier applies changes described in fragments to source data. If changes
@@ -145,17 +150,35 @@ func (a *Applier) ApplyFile(dst io.Writer, f *File) error {
145150
if a.applyType != applyInitial {
146151
return applyError(errApplyInProgress)
147152
}
153+
defer func() { a.applyType = applyFile }()
148154

149-
if f.IsBinary && f.BinaryFragment != nil {
150-
return a.ApplyBinaryFragment(dst, f.BinaryFragment)
155+
if f.IsBinary && len(f.TextFragments) > 0 {
156+
return applyError(errors.New("binary file contains text fragments"))
151157
}
158+
if !f.IsBinary && f.BinaryFragment != nil {
159+
return applyError(errors.New("text file contains binary fragment"))
160+
}
161+
162+
switch {
163+
case f.BinaryFragment != nil:
164+
return a.ApplyBinaryFragment(dst, f.BinaryFragment)
165+
166+
case len(f.TextFragments) > 0:
167+
frags := make([]*TextFragment, len(f.TextFragments))
168+
copy(frags, f.TextFragments)
169+
170+
sort.Slice(frags, func(i, j int) bool {
171+
return frags[i].OldPosition < frags[j].OldPosition
172+
})
152173

153-
// TODO(bkeyes): sort fragments by start position
154-
// TODO(bkeyes): merge overlapping fragments
174+
// TODO(bkeyes): consider merging overlapping fragments
175+
// right now, the application fails if fragments overlap, but it should be
176+
// possible to precompute the result of applying them in order
155177

156-
for i, frag := range f.TextFragments {
157-
if err := a.ApplyTextFragment(dst, frag); err != nil {
158-
return applyError(err, fragNum(i))
178+
for i, frag := range frags {
179+
if err := a.ApplyTextFragment(dst, frag); err != nil {
180+
return applyError(err, fragNum(i))
181+
}
159182
}
160183
}
161184

@@ -168,12 +191,10 @@ func (a *Applier) ApplyFile(dst io.Writer, f *File) error {
168191
// order of increasing start position. As a result, each fragment can be
169192
// applied at most once before a call to Reset.
170193
func (a *Applier) ApplyTextFragment(dst io.Writer, f *TextFragment) error {
171-
switch a.applyType {
172-
case applyInitial, applyText:
173-
default:
194+
if a.applyType != applyInitial && a.applyType != applyText {
174195
return applyError(errApplyInProgress)
175196
}
176-
a.applyType = applyText
197+
defer func() { a.applyType = applyText }()
177198

178199
// application code assumes fragment fields are consistent
179200
if err := f.Validate(); err != nil {
@@ -265,7 +286,7 @@ func (a *Applier) ApplyBinaryFragment(dst io.Writer, f *BinaryFragment) error {
265286
if a.applyType != applyInitial {
266287
return applyError(errApplyInProgress)
267288
}
268-
a.applyType = applyText
289+
defer func() { a.applyType = applyBinary }()
269290

270291
if f == nil {
271292
return applyError(errors.New("nil fragment"))
@@ -395,7 +416,7 @@ func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (
395416
// TODO(bkeyes): consider pooling these buffers
396417
b := make([]byte, size)
397418
if _, err := src.ReadAt(b, offset); err != nil {
398-
return 0, delta, wrapEOF(err)
419+
return 0, delta, err
399420
}
400421

401422
_, err = w.Write(b)
@@ -412,10 +433,3 @@ func checkBinarySrcSize(r io.ReaderAt, size int64) error {
412433
}
413434
return nil
414435
}
415-
416-
func wrapEOF(err error) error {
417-
if err == io.EOF {
418-
err = io.ErrUnexpectedEOF
419-
}
420-
return err
421-
}

gitdiff/apply_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,69 @@ import (
1010
"testing"
1111
)
1212

13+
func TestApplierInvariants(t *testing.T) {
14+
binary := &BinaryFragment{
15+
Method: BinaryPatchLiteral,
16+
Size: 2,
17+
Data: []byte("\xbe\xef"),
18+
}
19+
20+
text := &TextFragment{
21+
NewPosition: 1,
22+
NewLines: 1,
23+
LinesAdded: 1,
24+
Lines: []Line{
25+
{Op: OpAdd, Line: "new line\n"},
26+
},
27+
}
28+
29+
file := &File{
30+
TextFragments: []*TextFragment{text},
31+
}
32+
33+
src := bytes.NewReader(nil)
34+
dst := ioutil.Discard
35+
36+
assertInProgress := func(t *testing.T, kind string, err error) {
37+
if !errors.Is(err, errApplyInProgress) {
38+
t.Fatalf("expected in-progress error for %s apply, but got: %v", kind, err)
39+
}
40+
}
41+
42+
t.Run("binaryFirst", func(t *testing.T) {
43+
a := NewApplier(src)
44+
if err := a.ApplyBinaryFragment(dst, binary); err != nil {
45+
t.Fatalf("unexpected error applying fragment: %v", err)
46+
}
47+
assertInProgress(t, "text", a.ApplyTextFragment(dst, text))
48+
assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary))
49+
assertInProgress(t, "file", a.ApplyFile(dst, file))
50+
})
51+
52+
t.Run("textFirst", func(t *testing.T) {
53+
a := NewApplier(src)
54+
if err := a.ApplyTextFragment(dst, text); err != nil {
55+
t.Fatalf("unexpected error applying fragment: %v", err)
56+
}
57+
// additional text fragments are allowed
58+
if err := a.ApplyTextFragment(dst, text); err != nil {
59+
t.Fatalf("unexpected error applying second fragment: %v", err)
60+
}
61+
assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary))
62+
assertInProgress(t, "file", a.ApplyFile(dst, file))
63+
})
64+
65+
t.Run("fileFirst", func(t *testing.T) {
66+
a := NewApplier(src)
67+
if err := a.ApplyFile(dst, file); err != nil {
68+
t.Fatalf("unexpected error applying file: %v", err)
69+
}
70+
assertInProgress(t, "text", a.ApplyTextFragment(dst, text))
71+
assertInProgress(t, "binary", a.ApplyBinaryFragment(dst, binary))
72+
assertInProgress(t, "file", a.ApplyFile(dst, file))
73+
})
74+
}
75+
1376
func TestTextFragmentApplyStrict(t *testing.T) {
1477
tests := map[string]struct {
1578
Files applyFiles

0 commit comments

Comments
 (0)