From ccd6faf3d10da0669631fb3e3ba17a46b9e63a09 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 15 Sep 2026 00:32:11 +0200 Subject: (matched) is unknown after an undecided rule, so a catch-all leaves an unreadable file alone --- internal/engine/engine_test.go | 2 +- internal/engine/exclude_test.go | 36 ++++++++++++++++++++++++++++++++++++ internal/engine/facts.go | 15 +++++++++------ internal/engine/match.go | 6 ++++++ 4 files changed, 52 insertions(+), 7 deletions(-) (limited to 'internal/engine') diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index c38c1fc..9de3ea4 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -53,7 +53,7 @@ func (f fakeFacts) ModTime() time.Time { return func (f fakeFacts) Now() time.Time { return time.Time{} } func (f fakeFacts) ContentContains(cond.Options, []string) (int, error) { return -1, nil } func (f fakeFacts) Duplicate([]string) (string, bool, error) { return "", false, nil } -func (f fakeFacts) Matched() bool { return false } +func (f fakeFacts) Matched() (bool, bool) { return false, false } var _ cond.Facts = fakeFacts{} diff --git a/internal/engine/exclude_test.go b/internal/engine/exclude_test.go index 9aafd7f..3398b07 100644 --- a/internal/engine/exclude_test.go +++ b/internal/engine/exclude_test.go @@ -512,3 +512,39 @@ func TestExplainAgreesWithMatchOnADuplicateExclude(t *testing.T) { } } } + +// TestNotMatchedAfterAnUnknownRuleDoesNotAct: a partly read document an +// earlier content rule could not decide is not caught by a later +// (not (matched)) catch-all (plan 11 review L6). +func TestNotMatchedAfterAnUnknownRuleDoesNotAct(t *testing.T) { + h, dl := excludeTree(t, map[string]string{}) + os.MkdirAll(dl, 0o755) + partDocx(t, filepath.Join(dl, "part.docx"), "good body text") + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(rule "acme" (when (content "acme ltd")) (move "Acme")) +(rule "rest" (when (not (matched))) (move "Unsorted")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + r, err := e.Match(context.Background(), e.Dirs[0]) + if err != nil { + t.Fatal(err) + } + for _, fm := range r.Matched { + if fm.File.Rel == "part.docx" { + t.Errorf("part.docx matched %d rules; the catch-all must not take a file an earlier rule could not decide", len(fm.Rules)) + } + } + x, err := e.Explain(context.Background(), filepath.Join(dl, "part.docx")) + if err != nil { + t.Fatal(err) + } + for _, rt := range x.Rules { + if rt.Match { + t.Errorf("explain: rule %s matches", rt.Rule.Name) + } + } +} diff --git a/internal/engine/facts.go b/internal/engine/facts.go index f8fde89..89bd71b 100644 --- a/internal/engine/facts.go +++ b/internal/engine/facts.go @@ -132,6 +132,9 @@ type facts struct { file scan.File matched bool + // undecided: an earlier rule's condition was unknown (content krino + // could not read), so (matched) is unknown while none has matched. + undecided bool contentDone bool // extraction was attempted contentErr error // why it failed @@ -146,12 +149,12 @@ func newFacts(run *matchRun, file scan.File) *facts { return &facts{run: run, file: file} } -func (f *facts) Name() string { return f.file.Name } -func (f *facts) Rel() string { return f.file.Rel } -func (f *facts) Size() int64 { return f.file.Size } -func (f *facts) ModTime() time.Time { return f.file.ModTime } -func (f *facts) Now() time.Time { return f.run.now } -func (f *facts) Matched() bool { return f.matched } +func (f *facts) Name() string { return f.file.Name } +func (f *facts) Rel() string { return f.file.Rel } +func (f *facts) Size() int64 { return f.file.Size } +func (f *facts) ModTime() time.Time { return f.file.ModTime } +func (f *facts) Now() time.Time { return f.run.now } +func (f *facts) Matched() (bool, bool) { return f.matched, f.undecided } // ContentContains answers a content test (spec ยง6.1). A file above // max-read is never read, cached or not. Before the file has been diff --git a/internal/engine/match.go b/internal/engine/match.go index c252596..2ef3ae1 100644 --- a/internal/engine/match.go +++ b/internal/engine/match.go @@ -169,6 +169,9 @@ func evalFile(run *matchRun, file scan.File) FileMatch { for _, w := range res.Warnings { fm.Warnings = append(fm.Warnings, r.Name+": "+w) } + if res.Unreadable { + f.undecided = true + } if !res.Match { continue } @@ -316,6 +319,9 @@ func (e *Engine) Explain(ctx context.Context, path string) (*Explanation, error) } trace := r.Cond.Explain(f) match := trace.Value + if trace.Unknown { + f.undecided = true + } if match { f.matched = true matched = append(matched, RuleMatch{Rule: r}) -- cgit v1.3