Skip to content

Commit dbbf57b

Browse files
committed
Resolve panic in cs boyter/cs#61
1 parent 6c83f39 commit dbbf57b

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

processor/workers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,14 @@ func verifyIgnoreEscape(langFeatures LanguageFeature, fileJob *FileJob, index in
538538
if isMatch {
539539
ignoreEscape = true
540540
index = index + len(langFeatures.Quotes[i].Start)
541+
542+
// Clamp to the last byte when the start token ends the file, such as a
543+
// Python file whose final bytes are """ with no trailing newline. Left
544+
// unbounded the caller lands on len(Content) and blankState writes past
545+
// the end of ContentByteType, and the final line is never counted.
546+
if index >= len(fileJob.Content) {
547+
index = len(fileJob.Content) - 1
548+
}
541549
}
542550
}
543551
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
package processor
4+
5+
import "testing"
6+
7+
// Reported downstream by cs (github.com/boyter/cs), which vendors this package:
8+
//
9+
// panic: runtime error: index out of range [1411] with length 1411
10+
// processor.blankState(...) workers.go:377
11+
//
12+
// The trigger is a file whose final bytes are a docstring/ignore-escape quote
13+
// start token (Python """ or ''', C# @") with no trailing newline.
14+
//
15+
// verifyIgnoreEscape advances index past the matched start token
16+
// (index += len(Quotes[i].Start)) without bounding it against len(Content).
17+
// When the token ends the file, index lands exactly on len(Content), and
18+
// blankState then writes ContentByteType[index] out of bounds.
19+
//
20+
// The out-of-bounds write only fires when ClassifyContent is set (cs sets it;
21+
// scc's own CLI does not), but the same over-advance corrupts line counting for
22+
// every caller: the final line is skipped entirely.
23+
24+
var docstringEOFCases = []struct {
25+
name string
26+
language string
27+
content string
28+
lines int
29+
}{
30+
{"python triple double quote at eof", "Python", "def greet():\n return \"hi\"\n\"\"\"", 3},
31+
{"python triple single quote at eof", "Python", "x = 1\n'''", 2},
32+
{"python docstring is whole file", "Python", "\"\"\"", 1},
33+
{"csharp ignore escape quote at eof", "C#", "class A {\n@\"", 2},
34+
// Control: the same token followed by a newline has always worked.
35+
{"python docstring with trailing newline", "Python", "x = 1\n\"\"\"\n", 2},
36+
}
37+
38+
// A quote start token at EOF must not write past the end of ContentByteType.
39+
func TestCountStatsDocStringAtEOFNoPanic(t *testing.T) {
40+
ProcessConstants()
41+
42+
for _, tc := range docstringEOFCases {
43+
t.Run(tc.name, func(t *testing.T) {
44+
fileJob := FileJob{Language: tc.language, ClassifyContent: true}
45+
fileJob.SetContent(tc.content)
46+
47+
CountStats(&fileJob)
48+
49+
if len(fileJob.ContentByteType) != len(fileJob.Content) {
50+
t.Errorf("Expected ContentByteType of length %d got %d", len(fileJob.Content), len(fileJob.ContentByteType))
51+
}
52+
})
53+
}
54+
}
55+
56+
// The same over-advance silently drops the last line for every caller, whether
57+
// or not content classification is enabled.
58+
func TestCountStatsDocStringAtEOFLineCount(t *testing.T) {
59+
ProcessConstants()
60+
61+
for _, tc := range docstringEOFCases {
62+
t.Run(tc.name, func(t *testing.T) {
63+
fileJob := FileJob{Language: tc.language}
64+
fileJob.SetContent(tc.content)
65+
66+
CountStats(&fileJob)
67+
68+
if fileJob.Lines != int64(tc.lines) {
69+
t.Errorf("Expected %d lines got %d", tc.lines, fileJob.Lines)
70+
}
71+
if fileJob.Lines != fileJob.Code+fileJob.Comment+fileJob.Blank {
72+
t.Errorf("Expected lines %d to equal code %d + comment %d + blank %d",
73+
fileJob.Lines, fileJob.Code, fileJob.Comment, fileJob.Blank)
74+
}
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)