55 "crypto"
66 "encoding/base64"
77 "encoding/binary"
8+ "errors"
89 "io"
910 "testing"
1011
@@ -57,12 +58,13 @@ func (s *IdxfileSuite) TestDecode() {
5758}
5859
5960func (s * IdxfileSuite ) TestDecode64bitsOffsets () {
60- f := bytes .NewBufferString (fixtureLarge4GB )
61+ raw , err := io .ReadAll (base64 .NewDecoder (base64 .StdEncoding , bytes .NewBufferString (fixtureLarge4GB )))
62+ s .Require ().NoError (err )
6163
6264 idx := new (MemoryIndex )
6365
64- d := NewDecoder (base64 . NewDecoder ( base64 . StdEncoding , f ), hash .New (crypto .SHA1 ))
65- err : = d .Decode (idx )
66+ d := NewDecoder (FromBytes ( raw ), hash .New (crypto .SHA1 ))
67+ err = d .Decode (idx )
6668 s .NoError (err )
6769
6870 expected := map [string ]uint64 {
@@ -135,9 +137,8 @@ func BenchmarkDecode(b *testing.B) {
135137
136138 hasher := hash .New (crypto .SHA1 )
137139 for b .Loop () {
138- f := bytes .NewBuffer (fixture )
139140 idx := new (MemoryIndex )
140- d := NewDecoder (f , hasher )
141+ d := NewDecoder (FromBytes ( fixture ) , hasher )
141142 if err := d .Decode (idx ); err != nil {
142143 b .Errorf ("unexpected error decoding: %s" , err )
143144 }
@@ -238,7 +239,12 @@ func TestDecodeErrors(t *testing.T) {
238239 buf = append (buf , writeFanout (1 , nil )... )
239240 return buf
240241 },
241- wantErr : io .EOF ,
242+ // The size formula now rejects the input before the
243+ // reader gets a chance to report EOF: a single-object
244+ // idx v2 needs at least 1100 bytes for SHA-1, and the
245+ // header+fanout alone is only 1032.
246+ wantErr : ErrMalformedIdxFile ,
247+ wantErrContains : "inconsistent with object count" ,
242248 },
243249 {
244250 name : "checksum mismatch" ,
@@ -259,7 +265,7 @@ func TestDecodeErrors(t *testing.T) {
259265 t .Parallel ()
260266
261267 idx := new (MemoryIndex )
262- d := NewDecoder (bytes . NewReader (tt .input ()), hash .New (crypto .SHA1 ))
268+ d := NewDecoder (FromBytes (tt .input ()), hash .New (crypto .SHA1 ))
263269
264270 err := d .Decode (idx )
265271 require .Error (t , err )
@@ -297,3 +303,88 @@ func idxV2Header() []byte {
297303 binary .Write (& buf , binary .BigEndian , uint32 (2 ))
298304 return buf .Bytes ()
299305}
306+
307+ // TestDecoderSizeFormulaBoundary exercises the [minSize, maxSize] range
308+ // enforced by validateIdxV2Size. The boundary is asymmetric for nr > 1:
309+ // each extra 8-byte offset64 slot extends maxSize by 8. Inputs at the
310+ // edge of the legal range must pass the size check (failing later with
311+ // the trailing checksum mismatch, since the payload is zero-filled);
312+ // inputs one byte outside must be rejected with a size-related error.
313+ func TestDecoderSizeFormulaBoundary (t * testing.T ) {
314+ t .Parallel ()
315+
316+ const hashsz = 20 // SHA-1
317+ const headerLen = 8 + 4 * 256
318+ minSize := func (nr int64 ) int64 {
319+ return headerLen + nr * (hashsz + 8 ) + 2 * hashsz
320+ }
321+ maxSize := func (nr int64 ) int64 {
322+ m := minSize (nr )
323+ if nr > 0 {
324+ m += (nr - 1 ) * 8
325+ }
326+ return m
327+ }
328+ build := func (nr uint32 , total int64 ) []byte {
329+ buf := append (idxV2Header (), writeFanout (nr , nil )... )
330+ if int64 (len (buf )) > total {
331+ t .Fatalf ("header+fanout exceeds requested total: %d > %d" , len (buf ), total )
332+ }
333+ return append (buf , make ([]byte , total - int64 (len (buf )))... )
334+ }
335+
336+ tests := []struct {
337+ name string
338+ nr uint32
339+ size int64
340+ passesLen bool // true: size check passes (later parsing may fail)
341+ }{
342+ {"nr=1, at minSize" , 1 , minSize (1 ), true },
343+ {"nr=1, one byte below minSize" , 1 , minSize (1 ) - 1 , false },
344+ {"nr=1, one byte above maxSize" , 1 , maxSize (1 ) + 1 , false },
345+ {"nr=2, at minSize" , 2 , minSize (2 ), true },
346+ {"nr=2, at maxSize" , 2 , maxSize (2 ), true },
347+ {"nr=2, one byte below minSize" , 2 , minSize (2 ) - 1 , false },
348+ {"nr=2, one byte above maxSize" , 2 , maxSize (2 ) + 1 , false },
349+ }
350+
351+ for _ , tt := range tests {
352+ t .Run (tt .name , func (t * testing.T ) {
353+ t .Parallel ()
354+
355+ idx := new (MemoryIndex )
356+ d := NewDecoder (FromBytes (build (tt .nr , tt .size )), hash .New (crypto .SHA1 ))
357+ err := d .Decode (idx )
358+ require .Error (t , err )
359+ require .ErrorIs (t , err , ErrMalformedIdxFile )
360+ if tt .passesLen {
361+ // Payload is zero-filled, so we reach the trailing
362+ // checksum comparison and fail there. The size check
363+ // itself must not fire.
364+ require .ErrorContains (t , err , "checksum mismatch" )
365+ } else {
366+ require .ErrorContains (t , err , "inconsistent with object count" )
367+ }
368+ })
369+ }
370+ }
371+
372+ func TestDecoderRejectsInconsistentObjectCount (t * testing.T ) {
373+ t .Parallel ()
374+
375+ // Header (\xff t O c) + version 2 + fanout where fanout[0..255] all
376+ // claim 0x4C4C4C4C objects. The byte count cannot possibly accommodate
377+ // that many object names.
378+ var buf bytes.Buffer
379+ buf .Write ([]byte {0xff , 't' , 'O' , 'c' , 0 , 0 , 0 , 2 })
380+ for range 256 {
381+ _ = binary .Write (& buf , binary .BigEndian , uint32 (0x4C4C4C4C ))
382+ }
383+
384+ idx := new (MemoryIndex )
385+ d := NewDecoder (FromBytes (buf .Bytes ()), hash .New (crypto .SHA1 ))
386+ err := d .Decode (idx )
387+ if ! errors .Is (err , ErrMalformedIdxFile ) {
388+ t .Fatalf ("expected ErrMalformedIdxFile, got %v" , err )
389+ }
390+ }
0 commit comments