Skip to content

Commit a17e456

Browse files
devGregAclaude
andcommitted
Exempt testdata only on filesystem scans, never detector crashes
An image's paths are chosen by whoever built it, so a /testdata/ path in an image no longer hides a coverage gap. Only real files on a filesystem scan earn the test-fixture exemption, and a file a detector crashed on is always a gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 62f83d0 commit a17e456

5 files changed

Lines changed: 20 additions & 10 deletions

File tree

‎SNAPSHOT‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ php2shell-scan: no commits yet; source/ and image/ copied 2026-10-21
33
Engine-only changes since the copy (port these back to emudhra-scan and php2shell-scan):
44
- internal/archive: PyInstaller zlib entries cap the untrusted ulen hint at clen*1032+1024, and read() copies a short single chunk so a lying hint cannot keep a large allocation alive
55
- internal/source: readOne opens with O_NOFOLLOW|O_NONBLOCK and checks the opened file is the same regular file the walk saw
6-
- internal/engine: a detector panic on one file is contained and reported as not scanned; archive members named testdata/ no longer escape the coverage gap count (emudhra-scan's report.Gaps has the same exemption)
6+
- internal/engine: a detector panic on one file is contained and reported as not scanned; the testdata/ exemption from the coverage-gap count applies only to real files on a filesystem scan, never image paths, archive members or detector crashes (emudhra-scan exempts testdata/ everywhere)

‎internal/engine/engine.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ type File struct {
119119
type Skip struct {
120120
Path string
121121
Reason string
122+
failed bool // set by the engine when the detector panicked
122123
}
123124

124125
// TargetContext is what a detector can ask about the rest of the target when

‎internal/engine/engine_test.go‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,11 @@ func TestRegistry(t *testing.T) {
188188
}
189189

190190
func TestGaps(t *testing.T) {
191-
tg := &Target{Skipped: nil}
191+
img := &Target{Type: Image, Skipped: []source.Skip{skip("/src/testdata/bad.pem", "unparseable", false)}}
192+
if g := img.Gaps(); g != 1 {
193+
t.Errorf("image gaps %d, want 1: image paths never earn the fixture exemption", g)
194+
}
195+
tg := &Target{Type: Filesystem}
192196
tg.Skipped = append(tg.Skipped,
193197
skip("/proc", "excluded: pseudo-filesystem", true),
194198
skip("/x/testdata/bad.pem", "unparseable", false),
@@ -201,14 +205,14 @@ func TestGaps(t *testing.T) {
201205

202206
// A detector that panics on one file loses that file, not the scan.
203207
func TestDetectorPanicIsContained(t *testing.T) {
204-
root := tree(t, map[string][]byte{"bad": []byte("x"), "good": []byte("y")})
208+
root := tree(t, map[string][]byte{"testdata/bad": []byte("x"), "good": []byte("y")})
205209
seen := &[]string{}
206210
p := &panicky{fake: fake{id: "t-panic", seen: seen}}
207211
tg := ScanFS([]Detector{p}, root, Options{AsRoot: true})
208212
if tg.Error != "" || len(*seen) != 1 || (*seen)[0] != "/good" {
209213
t.Fatalf("error %q seen %v", tg.Error, *seen)
210214
}
211-
if len(tg.Skipped) != 1 || tg.Skipped[0].Path != "/bad" || tg.Gaps() != 1 {
215+
if len(tg.Skipped) != 1 || tg.Skipped[0].Path != "/testdata/bad" || tg.Gaps() != 1 {
212216
t.Fatalf("the panicking file must be a coverage gap: %+v", tg.Skipped)
213217
}
214218
}
@@ -217,7 +221,7 @@ type panicky struct{ fake }
217221

218222
func (p *panicky) NewScan(TargetKind) Scan { return p }
219223
func (p *panicky) Visit(f File) []Skip {
220-
if f.Path == "/bad" {
224+
if f.Path == "/testdata/bad" {
221225
panic("hostile input")
222226
}
223227
return p.fake.Visit(f)

‎internal/engine/run.go‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ func (t *Target) Count(v Verdict) int {
4747
func (t *Target) Gaps() int {
4848
n := 0
4949
for _, s := range t.Skipped {
50-
// The test-fixture exemption applies to real paths only: names inside an
51-
// archive are chosen by whoever built it, so they never earn it.
52-
fixture := strings.Contains(s.Path, "/testdata/") && !strings.Contains(s.Path, "!/")
50+
// The test-fixture exemption applies only to paths the operator controls:
51+
// real files on a scanned filesystem. Image paths and archive member names
52+
// are chosen by whoever built them, and a detector failure is never exempt.
53+
fixture := t.Type == Filesystem && strings.Contains(s.Path, "/testdata/") &&
54+
!strings.Contains(s.Path, "!/") && !s.DetectorFailed
5355
if s.Excluded || strings.HasPrefix(s.Reason, "excluded:") || fixture {
5456
continue
5557
}
@@ -198,7 +200,7 @@ func (w *walker) send(f File, skip func(int) bool) {
198200
continue
199201
}
200202
for _, sk := range w.visitSafely(i, s, f) {
201-
w.t.Skipped = append(w.t.Skipped, source.Skip{Path: sk.Path, Reason: sk.Reason})
203+
w.t.Skipped = append(w.t.Skipped, source.Skip{Path: sk.Path, Reason: sk.Reason, DetectorFailed: sk.failed})
202204
}
203205
}
204206
}
@@ -208,7 +210,7 @@ func (w *walker) send(f File, skip func(int) bool) {
208210
func (w *walker) visitSafely(i int, s Scan, f File) (skips []Skip) {
209211
defer func() {
210212
if r := recover(); r != nil {
211-
skips = []Skip{{Path: f.Path, Reason: fmt.Sprintf("detector %s failed on this file (%v); not checked", w.dets[i].Manifest().ID, r)}}
213+
skips = []Skip{{Path: f.Path, Reason: fmt.Sprintf("detector %s failed on this file (%v); not checked", w.dets[i].Manifest().ID, r), failed: true}}
212214
}
213215
}()
214216
return s.Visit(f)

‎internal/source/source.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ type Skip struct {
3434
// Excluded is true for deliberate exclusions (pseudo-filesystems, container
3535
// storage), which do not make a scan incomplete.
3636
Excluded bool `json:"excluded,omitempty"`
37+
// DetectorFailed is true when a detector crashed on the file. It is
38+
// always a coverage gap.
39+
DetectorFailed bool `json:"detector_failed,omitempty"`
3740
}
3841

3942
// Options control what is read.

0 commit comments

Comments
 (0)