Skip to content

Commit 5e23dfd

Browse files
authored
Merge pull request #1937 from pjbgf/idx-v5
[v5] plumbing: format/idxfile, Fix version and fanout checks
2 parents 6b38a32 + cd757fc commit 5e23dfd

2 files changed

Lines changed: 152 additions & 21 deletions

File tree

plumbing/format/idxfile/decoder.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ func readVersion(idx *MemoryIndex, r io.Reader) error {
9191
return err
9292
}
9393

94-
if v > VersionSupported {
95-
return ErrUnsupportedVersion
94+
if v != VersionSupported {
95+
return fmt.Errorf("%w: v%d", ErrUnsupportedVersion, v)
9696
}
9797

9898
idx.Version = v
@@ -106,6 +106,10 @@ func readFanout(idx *MemoryIndex, r io.Reader) error {
106106
return err
107107
}
108108

109+
if k > 0 && n < idx.Fanout[k-1] {
110+
return fmt.Errorf("%w: fanout table is not monotonically non-decreasing at entry %d", ErrMalformedIdxFile, k)
111+
}
112+
109113
idx.Fanout[k] = n
110114
idx.FanoutMapping[k] = noMapping
111115
}
@@ -155,7 +159,7 @@ func readCRC32(idx *MemoryIndex, r io.Reader) error {
155159
}
156160

157161
func readOffsets(idx *MemoryIndex, r io.Reader) error {
158-
var o64cnt int
162+
var o64cnt int64
159163
for k := 0; k < fanout; k++ {
160164
if pos := idx.FanoutMapping[k]; pos != noMapping {
161165
if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil {

plumbing/format/idxfile/decoder_test.go

Lines changed: 145 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package idxfile_test
33
import (
44
"bytes"
55
"encoding/base64"
6+
"encoding/binary"
67
"fmt"
78
"io"
8-
"os"
99
"testing"
1010

1111
"github.com/go-git/go-git/v5/plumbing"
@@ -137,28 +137,155 @@ func BenchmarkDecode(b *testing.B) {
137137
}
138138
}
139139

140-
func TestChecksumMismatch(t *testing.T) {
140+
func TestDecodeErrors(t *testing.T) {
141141
t.Parallel()
142142

143-
f, err := os.CreateTemp(t.TempDir(), "temp.idx")
143+
idx := fixtures.Basic().One().Idx()
144+
t.Cleanup(func() { idx.Close() })
145+
validIdx, err := io.ReadAll(idx)
144146
require.NoError(t, err)
145-
defer f.Close()
146147

147-
_, err = io.Copy(f, fixtures.Basic().One().Idx())
148-
require.NoError(t, err)
149-
150-
_, err = f.Seek(-1, io.SeekEnd)
151-
require.NoError(t, err)
152-
153-
_, err = f.Write([]byte{0})
154-
require.NoError(t, err)
148+
tests := []struct {
149+
name string
150+
input func() []byte
151+
wantErr error
152+
wantErrContains string
153+
}{
154+
{
155+
name: "empty input",
156+
input: func() []byte { return nil },
157+
wantErr: io.EOF,
158+
},
159+
{
160+
name: "wrong magic",
161+
input: func() []byte { return []byte{0, 0, 0, 0, 0, 0, 0, 2} },
162+
wantErr: ErrMalformedIdxFile,
163+
},
164+
{
165+
name: "truncated header",
166+
input: func() []byte { return []byte{255, 't'} },
167+
wantErr: io.ErrUnexpectedEOF,
168+
},
169+
{
170+
name: "unsupported version 1",
171+
input: func() []byte {
172+
var buf bytes.Buffer
173+
buf.Write([]byte{255, 't', 'O', 'c'})
174+
binary.Write(&buf, binary.BigEndian, uint32(1))
175+
return buf.Bytes()
176+
},
177+
wantErr: ErrUnsupportedVersion,
178+
wantErrContains: "v1",
179+
},
180+
{
181+
name: "unsupported version 3",
182+
input: func() []byte {
183+
var buf bytes.Buffer
184+
buf.Write([]byte{255, 't', 'O', 'c'})
185+
binary.Write(&buf, binary.BigEndian, uint32(3))
186+
return buf.Bytes()
187+
},
188+
wantErr: ErrUnsupportedVersion,
189+
wantErrContains: "v3",
190+
},
191+
{
192+
name: "truncated fanout table",
193+
input: func() []byte {
194+
buf := idxV2Header()
195+
// Only 10 fanout entries instead of 256.
196+
for range 10 {
197+
buf = binary.BigEndian.AppendUint32(buf, 0)
198+
}
199+
return buf
200+
},
201+
wantErr: io.EOF,
202+
},
203+
{
204+
name: "non-monotonic fanout at entry 1",
205+
input: func() []byte {
206+
buf := idxV2Header()
207+
// entry[0]=5, entry[1]=3 (decrease), rest=5
208+
buf = append(buf, writeFanout(5, map[int]uint32{0: 5, 1: 3})...)
209+
return buf
210+
},
211+
wantErr: ErrMalformedIdxFile,
212+
wantErrContains: "not monotonically non-decreasing",
213+
},
214+
{
215+
name: "non-monotonic fanout at last entry",
216+
input: func() []byte {
217+
buf := idxV2Header()
218+
// all entries = 10, except entry[255] = 5
219+
buf = append(buf, writeFanout(10, map[int]uint32{255: 5})...)
220+
return buf
221+
},
222+
wantErr: ErrMalformedIdxFile,
223+
wantErrContains: "not monotonically non-decreasing",
224+
},
225+
{
226+
name: "truncated object names",
227+
input: func() []byte {
228+
buf := idxV2Header()
229+
// Fanout claims 1 object, but no name data follows.
230+
buf = append(buf, writeFanout(1, nil)...)
231+
return buf
232+
},
233+
wantErr: io.EOF,
234+
},
235+
{
236+
name: "checksum mismatch",
237+
input: func() []byte {
238+
corrupted := make([]byte, len(validIdx))
239+
copy(corrupted, validIdx)
240+
// Flip the last byte of the idx checksum.
241+
corrupted[len(corrupted)-1] ^= 0xff
242+
return corrupted
243+
},
244+
wantErr: ErrMalformedIdxFile,
245+
wantErrContains: "checksum mismatch",
246+
},
247+
}
155248

156-
_, err = f.Seek(0, io.SeekStart)
157-
require.NoError(t, err)
249+
for _, tt := range tests {
250+
t.Run(tt.name, func(t *testing.T) {
251+
t.Parallel()
252+
253+
idx := new(MemoryIndex)
254+
d := NewDecoder(bytes.NewReader(tt.input()))
255+
256+
err := d.Decode(idx)
257+
require.Error(t, err)
258+
if tt.wantErr != nil {
259+
require.ErrorIs(t, err, tt.wantErr)
260+
}
261+
if tt.wantErrContains != "" {
262+
require.ErrorContains(t, err, tt.wantErrContains)
263+
}
264+
})
265+
}
266+
}
158267

159-
idx := new(MemoryIndex)
160-
d := NewDecoder(f)
268+
// writeFanout writes a 256-entry fanout table where every entry is set to total,
269+
// except for overrides specified as index→value pairs applied afterwards.
270+
func writeFanout(total uint32, overrides map[int]uint32) []byte {
271+
var buf bytes.Buffer
272+
entries := [256]uint32{}
273+
for i := range entries {
274+
entries[i] = total
275+
}
276+
for k, v := range overrides {
277+
entries[k] = v
278+
}
279+
for _, v := range entries {
280+
binary.Write(&buf, binary.BigEndian, v)
281+
}
282+
return buf.Bytes()
283+
}
161284

162-
err = d.Decode(idx)
163-
require.ErrorContains(t, err, "checksum mismatch")
285+
// idxV2Header returns the 8-byte idx v2 header (magic + version).
286+
func idxV2Header() []byte {
287+
var buf bytes.Buffer
288+
buf.Write([]byte{255, 't', 'O', 'c'})
289+
binary.Write(&buf, binary.BigEndian, uint32(2))
290+
return buf.Bytes()
164291
}

0 commit comments

Comments
 (0)