@@ -16,6 +16,7 @@ package index
1616
1717import (
1818 "bytes"
19+ "fmt"
1920 "log"
2021 "path"
2122 "slices"
@@ -36,7 +37,6 @@ type contentProvider struct {
3637 stats * zoekt.Stats
3738
3839 // mutable
39- err error
4040 idx uint32
4141 _data []byte
4242 _nl []uint32
@@ -58,10 +58,34 @@ func (p *contentProvider) setDocument(docID uint32) {
5858 p ._data = nil
5959}
6060
61+ // panicCorrupt stops a search as soon as it detects a corrupt shard invariant.
62+ // searchOneShard recovers the panic at the shard boundary, logs the error with
63+ // the query and stack trace, and increments Stats.Crashes. Callers therefore get
64+ // partial results with an explicit crashed-shard signal rather than a silent
65+ // non-match.
66+ func (p * contentProvider ) panicCorrupt (err error ) {
67+ shard := "unknown"
68+ if p .id .file != nil {
69+ shard = p .id .file .Name ()
70+ }
71+ repo := "unknown"
72+ if p .idx < uint32 (len (p .id .repos )) {
73+ repoID := p .id .repos [p .idx ]
74+ if int (repoID ) < len (p .id .repoMetaData ) {
75+ repo = p .id .repoMetaData [repoID ].Name
76+ }
77+ }
78+ panic (fmt .Errorf ("corrupt shard %q while searching repository %q, document %d: %w" , shard , repo , p .idx , err ))
79+ }
80+
6181func (p * contentProvider ) docSections () []DocumentSection {
6282 if p ._sects == nil {
6383 var sz uint32
64- p ._sects , sz , p .err = p .id .readDocSections (p .idx , p ._sectBuf )
84+ var err error
85+ p ._sects , sz , err = p .id .readDocSections (p .idx , p ._sectBuf )
86+ if err != nil {
87+ p .panicCorrupt (fmt .Errorf ("reading document sections: %w" , err ))
88+ }
6589 p .stats .ContentBytesLoaded += int64 (sz )
6690 p ._sectBuf = p ._sects
6791 }
@@ -71,7 +95,11 @@ func (p *contentProvider) docSections() []DocumentSection {
7195func (p * contentProvider ) newlines () newlines {
7296 if p ._nl == nil {
7397 var sz uint32
74- p ._nl , sz , p .err = p .id .readNewlines (p .idx , p ._nlBuf )
98+ var err error
99+ p ._nl , sz , err = p .id .readNewlines (p .idx , p ._nlBuf )
100+ if err != nil {
101+ p .panicCorrupt (fmt .Errorf ("reading newline offsets: %w" , err ))
102+ }
75103 p ._nlBuf = p ._nl
76104 p .stats .ContentBytesLoaded += int64 (sz )
77105 }
@@ -84,7 +112,11 @@ func (p *contentProvider) data(fileName bool) []byte {
84112 }
85113
86114 if p ._data == nil {
87- p ._data , p .err = p .id .readContents (p .idx )
115+ var err error
116+ p ._data , err = p .id .readContents (p .idx )
117+ if err != nil {
118+ p .panicCorrupt (fmt .Errorf ("reading content: %w" , err ))
119+ }
88120 p .stats .FilesLoaded ++
89121 p .stats .ContentBytesLoaded += int64 (len (p ._data ))
90122 }
@@ -95,45 +127,86 @@ func (p *contentProvider) data(fileName bool) []byte {
95127// runes (relative to document start). If filename is set, the corpus
96128// is the set of filenames, with the document being the name itself.
97129func (p * contentProvider ) findOffset (filename bool , r uint32 ) uint32 {
98- if p .id .metaData .PlainASCII {
99- return r
100- }
101-
102- sample := p .id .runeOffsets
103- runeEnds := p .id .fileEndRunes
104- fileStartByte := p .id .boundaries [p .idx ]
130+ var sample runeOffsetMap
131+ var runeEnds []uint32
132+ var fileStartByte , fileEndByte uint32
133+ kind := "content"
105134 if filename {
106135 sample = p .id .fileNameRuneOffsets
107136 runeEnds = p .id .fileNameEndRunes
108137 fileStartByte = p .id .fileNameIndex [p .idx ]
138+ fileEndByte = p .id .fileNameIndex [p .idx + 1 ]
139+ kind = "filename"
140+ } else {
141+ sample = p .id .runeOffsets
142+ runeEnds = p .id .fileEndRunes
143+ fileStartByte = p .id .boundaries [p .idx ]
144+ fileEndByte = p .id .boundaries [p .idx + 1 ]
109145 }
110146
111- absR := r
147+ if p .id .metaData .PlainASCII {
148+ if r > fileEndByte - fileStartByte {
149+ p .panicCorrupt (fmt .Errorf ("%s rune offset %d is after file size %d" , kind , r , fileEndByte - fileStartByte ))
150+ return 0
151+ }
152+ return r
153+ }
154+
155+ absR64 := uint64 (r )
112156 if p .idx > 0 {
113- absR += runeEnds [p .idx - 1 ]
157+ absR64 += uint64 (runeEnds [p .idx - 1 ])
158+ }
159+ if absR64 > uint64 (^ uint32 (0 )) {
160+ p .panicCorrupt (fmt .Errorf ("%s rune offset %d overflows the corpus rune offset" , kind , r ))
161+ return 0
114162 }
163+ absR := uint32 (absR64 )
115164
116165 byteOff , left := sample .lookup (absR )
117166
118167 var data []byte
119168
120169 if filename {
121- data = p .id .fileNameContent [byteOff :]
170+ if byteOff > uint64 (len (p .id .fileNameContent )) {
171+ p .panicCorrupt (fmt .Errorf ("filename rune offset %d maps to byte offset %d past filename data size %d" , absR , byteOff , len (p .id .fileNameContent )))
172+ return 0
173+ }
174+ data = p .id .fileNameContent [uint32 (byteOff ):]
122175 } else {
123- data , p .err = p .id .readContentSlice (byteOff , 3 * runeOffsetFrequency )
124- if p .err != nil {
176+ corpusEnd := p .id .boundaries [len (p .id .boundaries )- 1 ]
177+ if byteOff > uint64 (corpusEnd ) {
178+ p .panicCorrupt (fmt .Errorf ("content rune offset %d maps to byte offset %d past content data size %d" , absR , byteOff , corpusEnd ))
179+ return 0
180+ }
181+ var err error
182+ data , err = p .id .readContentSlice (uint32 (byteOff ), 3 * runeOffsetFrequency )
183+ if err != nil {
184+ p .panicCorrupt (fmt .Errorf ("content rune offset %d cannot load bytes at offset %d: %w" , absR , byteOff , err ))
125185 return 0
126186 }
127187 }
128188 for left > 0 {
189+ if len (data ) == 0 {
190+ p .panicCorrupt (fmt .Errorf ("%s rune offset %d has no decode bytes at byte offset %d" , kind , absR , byteOff ))
191+ return 0
192+ }
129193 _ , sz := utf8 .DecodeRune (data )
130- byteOff += uint32 (sz )
194+ byteOff += uint64 (sz )
131195 data = data [sz :]
132196 left --
133197 }
134198
135- byteOff -= fileStartByte
136- return byteOff
199+ if byteOff < uint64 (fileStartByte ) {
200+ p .panicCorrupt (fmt .Errorf ("%s rune offset %d maps to byte offset %d before file start %d" , kind , absR , byteOff , fileStartByte ))
201+ return 0
202+ }
203+ if byteOff > uint64 (fileEndByte ) {
204+ p .panicCorrupt (fmt .Errorf ("%s rune offset %d maps to byte offset %d after file end %d" , kind , absR , byteOff , fileEndByte ))
205+ return 0
206+ }
207+
208+ byteOff -= uint64 (fileStartByte )
209+ return uint32 (byteOff )
137210}
138211
139212// fillMatches converts the internal candidateMatch slice into our API's LineMatch.
0 commit comments