From 394d117b90ffb64c87adfc8c2436b57007840c2b Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 14 Sep 2026 22:23:42 +0200 Subject: plan 10: a format with no text is no match, not unreadable --- docs/design.md | 7 +++++-- internal/engine/exclude_test.go | 42 +++++++++++++++++++++++++++++++++++++++++ internal/engine/facts.go | 9 ++++++++- man/krino.conf.5 | 2 ++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/docs/design.md b/docs/design.md index 5b70183..a232baf 100644 --- a/docs/design.md +++ b/docs/design.md @@ -202,7 +202,9 @@ no rule has run yet, `(matched)` is never true inside an exclude. An exclude fails closed: when evaluating it reaches a content test that cannot read the file (over `max-read`, a tool missing, failing or timing out), the exclude holds, and the file is set aside as "(content -unreadable)" with the warning. An exclude protects files, so a file krino +unreadable)" with the warning. A file whose format has no text at all (an +image, an archive) is not unreadable: it contains no keyword, so its +content tests are simply false and raise no warning. An exclude protects files, so a file krino could not check is left alone. Mistakes in krino.conf's excludes are reported even while no directory is included. @@ -255,7 +257,8 @@ UTF-8 is replaced by U+FFFD before folding. `path`, then `duplicate`, then `content`. Content is extracted at most once per file, and only if evaluation reaches a `content` test. 4. If a file's text cannot be extracted, `content` is false and the plan - shows a warning naming the rule that wanted it. Inside an `exclude` the + shows a warning naming the rule that wanted it. A format with no text + (§6, "anything else") is not a failure: `content` is false, silently. Inside an `exclude` the whole exclude then holds instead (§4.6). ### 5.5 Duplicates diff --git a/internal/engine/exclude_test.go b/internal/engine/exclude_test.go index 87fa16c..4bb1512 100644 --- a/internal/engine/exclude_test.go +++ b/internal/engine/exclude_test.go @@ -260,3 +260,45 @@ func TestLoadChecksMainExcludesWithoutDirectories(t *testing.T) { t.Error("a broken krino.conf exclude was not reported") } } + +// TestNoTextFormatIsNoMatch: a file whose format has no text cannot contain +// a keyword, so a content exclude without a type does not set it aside, and +// no "content unreadable" warning is raised - while a real read failure (over +// max-read) still fails closed (re-review N2, Łukasz's decision). +func TestNoTextFormatIsNoMatch(t *testing.T) { + big := "confidential " + strings.Repeat("x", 2048) + h, dl := excludeTree(t, map[string]string{"photo.jpg": "\xff\xd8\xff\x00\x01binary", "big.txt": big}) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(max-read 1K) +(exclude (content "confidential")) +(rule "pics" (when (type jpg)) (move "Pictures")) +`}) + 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 { + switch fm.File.Rel { + case "photo.jpg": + if fm.Excluded != "" || len(fm.Rules) != 1 || len(fm.Warnings) != 0 { + t.Errorf("photo.jpg: Excluded %q, rules %d, warnings %v; want the pics rule and no warning", fm.Excluded, len(fm.Rules), fm.Warnings) + } + case "big.txt": + if !strings.HasSuffix(fm.Excluded, "(content unreadable)") { + t.Errorf("big.txt: Excluded %q; a real read failure must still fail closed", fm.Excluded) + } + } + } + x, err := e.Explain(context.Background(), filepath.Join(dl, "photo.jpg")) + if err != nil { + t.Fatal(err) + } + if x.Excluded != "" { + t.Errorf("explain: Excluded %q, want none", x.Excluded) + } +} diff --git a/internal/engine/facts.go b/internal/engine/facts.go index 487604b..ccdf18d 100644 --- a/internal/engine/facts.go +++ b/internal/engine/facts.go @@ -4,6 +4,7 @@ package engine import ( "context" + "errors" "path/filepath" "sort" "strings" @@ -178,7 +179,13 @@ func (f *facts) ContentContains(opt cond.Options, keywords []string) (int, error func (f *facts) extract(opt cond.Options, keywords []string) { f.contentDone = true text, err := f.run.e.Extract.Text(f.run.ctx, f.file.Path, f.file.Size, f.run.d.Settings.MaxRead) - if err != nil { + switch { + case errors.Is(err, extract.ErrUnsupported): + // A format with no text cannot contain a keyword: every answer is + // no, with no warning, and the answers are cached like any other + // (re-review N2). Only a real read failure is "unreadable". + text = "" + case err != nil: f.contentErr = err return } diff --git a/man/krino.conf.5 b/man/krino.conf.5 index 5153255..1fc113d 100644 --- a/man/krino.conf.5 +++ b/man/krino.conf.5 @@ -279,6 +279,8 @@ fails closed: when a content test it reaches cannot read the file the exclude holds and the file is set aside as .Dq (content unreadable) , with a warning. +A file whose format has no text at all, such as an image or an archive, +is not unreadable: its content tests are false, with no warning. .Sh RULES .Bd -literal -offset indent (rule NAME ITEM...) -- cgit v1.3