Skip to content

Commit 3ec0d70

Browse files
committed
plumbing: format/index, Fix tree extension invalidated entry parsing
The treeExtensionDecoder.readEntry early-returned on invalidated entries (entry_count == -1) before consuming the subtree count and newline, leaving stale bytes in the stream that corrupted subsequent entries. Move the invalidation check after fully parsing the entry line so the reader is always left at the correct position. Signed-off-by: Paulo Gomes <[email protected]>
1 parent dbe10b6 commit 3ec0d70

2 files changed

Lines changed: 65 additions & 8 deletions

File tree

plumbing/format/index/decoder.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -406,24 +406,26 @@ func (d *treeExtensionDecoder) readEntry() (*TreeEntry, error) {
406406
return nil, err
407407
}
408408

409-
// An entry can be in an invalidated state and is represented by having a
410-
// negative number in the entry_count field.
411-
if i == -1 {
412-
return nil, nil
413-
}
414-
415409
e.Entries = i
416410
trees, err := binary.ReadUntil(d.r, '\n')
417411
if err != nil {
418412
return nil, err
419413
}
420414

421-
i, err = strconv.Atoi(string(trees))
415+
subtrees, err := strconv.Atoi(string(trees))
422416
if err != nil {
423417
return nil, err
424418
}
425419

426-
e.Trees = i
420+
e.Trees = subtrees
421+
422+
// An entry can be in an invalidated state and is represented by having a
423+
// negative number in the entry_count field. In this case, there is no
424+
// object name and the next entry starts immediately after the newline.
425+
if i < 0 {
426+
return nil, nil
427+
}
428+
427429
_, err = io.ReadFull(d.r, e.Hash[:])
428430
if err != nil {
429431
return nil, err

plumbing/format/index/decoder_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package index
22

33
import (
4+
"bufio"
45
"bytes"
56
"crypto"
67
"errors"
@@ -598,3 +599,57 @@ func TestDecodeAllIndexFixtures(t *testing.T) {
598599

599600
assert.Equal(t, want, got, "not all wanted index versions found")
600601
}
602+
603+
func TestTreeExtensionInvalidatedEntry(t *testing.T) {
604+
t.Parallel()
605+
606+
// TREE extension payload: three entries where the middle one is
607+
// invalidated (entry_count == -1). The on-disk format per entry is:
608+
// <path>\0<entry_count> <subtree_nr>\n[<OID> only if entry_count >= 0]
609+
//
610+
// Before the fix, an invalidated entry returned before consuming the
611+
// subtree_nr and newline, leaving stale bytes in the stream that
612+
// corrupted every subsequent entry.
613+
h := crypto.SHA1.New()
614+
hashSize := h.Size()
615+
616+
var buf bytes.Buffer
617+
618+
// Entry 1 (root, valid): path="", entry_count=5, subtrees=2
619+
buf.WriteByte('\x00')
620+
buf.WriteString("5 2\n")
621+
rootHash := make([]byte, hashSize)
622+
rootHash[0] = 0xaa
623+
buf.Write(rootHash)
624+
625+
// Entry 2 (invalidated): path="stale", entry_count=-1, subtrees=0
626+
// No OID follows an invalidated entry.
627+
buf.WriteString("stale\x00")
628+
buf.WriteString("-1 0\n")
629+
630+
// Entry 3 (valid): path="good", entry_count=2, subtrees=0
631+
buf.WriteString("good\x00")
632+
buf.WriteString("2 0\n")
633+
goodHash := make([]byte, hashSize)
634+
goodHash[0] = 0xbb
635+
buf.Write(goodHash)
636+
637+
r := bufio.NewReader(&buf)
638+
d := &treeExtensionDecoder{r}
639+
tree := &Tree{}
640+
err := d.Decode(tree)
641+
require.NoError(t, err)
642+
643+
// The invalidated entry is skipped; only the two valid entries remain.
644+
require.Len(t, tree.Entries, 2)
645+
646+
assert.Equal(t, "", tree.Entries[0].Path)
647+
assert.Equal(t, 5, tree.Entries[0].Entries)
648+
assert.Equal(t, 2, tree.Entries[0].Trees)
649+
assert.Equal(t, rootHash, tree.Entries[0].Hash[:])
650+
651+
assert.Equal(t, "good", tree.Entries[1].Path)
652+
assert.Equal(t, 2, tree.Entries[1].Entries)
653+
assert.Equal(t, 0, tree.Entries[1].Trees)
654+
assert.Equal(t, goodHash, tree.Entries[1].Hash[:])
655+
}

0 commit comments

Comments
 (0)