aboutsummaryrefslogtreecommitdiff
path: root/internal/engine
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 23:39:37 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 23:39:37 +0200
commitc09020c2fb01da24d5befbed78ce3b73b5efbe4c (patch)
tree8cd1086588988fafaf238aed8a38a7ec85b0afa3 /internal/engine
parent3990586a85e91923c0a3ae1a1f0f61420db555ac (diff)
downloadkrino-c09020c2fb01da24d5befbed78ce3b73b5efbe4c.tar.gz
krino-c09020c2fb01da24d5befbed78ce3b73b5efbe4c.zip
content tests are three-valued: unknown when unreadable or read in part
Diffstat (limited to 'internal/engine')
-rw-r--r--internal/engine/exclude_test.go75
-rw-r--r--internal/engine/facts.go9
2 files changed, 82 insertions, 2 deletions
diff --git a/internal/engine/exclude_test.go b/internal/engine/exclude_test.go
index e183268..ceb8442 100644
--- a/internal/engine/exclude_test.go
+++ b/internal/engine/exclude_test.go
@@ -3,6 +3,8 @@
package engine
import (
+ "archive/zip"
+ "bytes"
"context"
"os"
"path/filepath"
@@ -351,3 +353,76 @@ func TestPlannedDestinationKeepsDiacritics(t *testing.T) {
t.Fatalf("chains = %+v; want the move into Out/Łódź", dp.Chains)
}
}
+
+// partDocx writes a docx whose body reads and whose footer uses a
+// compression method Go cannot read: a partly readable document.
+func partDocx(t *testing.T, path, body string) {
+ t.Helper()
+ var buf bytes.Buffer
+ w := zip.NewWriter(&buf)
+ f, err := w.Create("word/document.xml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.Write([]byte(`<w:document xmlns:w="w"><w:body><w:p><w:r><w:t>` + body + `</w:t></w:r></w:p></w:body></w:document>`))
+ raw, err := w.CreateRaw(&zip.FileHeader{Name: "word/footer1.xml", Method: 12})
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw.Write([]byte("BZh9 not really bzip2 confidential"))
+ if err := w.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ os.Chtimes(path, old, old)
+}
+
+// TestPartlyReadableDocument: a docx with an unreadable part answers a
+// keyword it holds in the readable part, but a keyword not found there is
+// unknown: a content exclude sets the file aside, a rule warns (plan 11,
+// re-review cache F3).
+func TestPartlyReadableDocument(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")
+(exclude (content "confidential"))
+(rule "all" (move "Out"))
+`})
+ 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)
+ }
+ if len(r.Matched) != 1 || !strings.HasSuffix(r.Matched[0].Excluded, "(content unreadable)") {
+ t.Fatalf("matched = %+v; want part.docx set aside as unreadable", r.Matched)
+ }
+
+ os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(`
+(path "~/dl")
+(rule "body" (when (content "good body")) (move "Docs"))
+(rule "other" (when (content "nowhere")) (move "Other"))
+`), 0o644)
+ 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)
+ }
+ fm := r.Matched[0]
+ if len(fm.Rules) != 1 || fm.Rules[0].Rule.Name != "body" {
+ t.Errorf("rules = %+v; want only body, found in the readable part", fm.Rules)
+ }
+ if len(fm.Warnings) == 0 {
+ t.Errorf("no warning for the keyword the unreadable part might hold")
+ }
+}
diff --git a/internal/engine/facts.go b/internal/engine/facts.go
index ccdf18d..c510b20 100644
--- a/internal/engine/facts.go
+++ b/internal/engine/facts.go
@@ -118,6 +118,7 @@ type facts struct {
contentDone bool // extraction was attempted
contentErr error // why it failed
+ partialErr error // extract.ErrPartial: a keyword not found may be in the unread part
answers map[string]bool // by cond.KeywordKey, once extracted
}
@@ -171,7 +172,7 @@ func (f *facts) ContentContains(opt cond.Options, keywords []string) (int, error
return i, nil
}
}
- return -1, nil
+ return -1, f.partialErr
}
// extract reads the file's text and answers every keyword of the directory,
@@ -180,6 +181,10 @@ 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)
switch {
+ case errors.Is(err, extract.ErrPartial):
+ // Partly read: the keywords found in it are answered; one not found
+ // is unknown (ContentContains), and nothing is cached (plan 11).
+ f.partialErr = err
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
@@ -203,7 +208,7 @@ func (f *facts) extract(opt cond.Options, keywords []string) {
}
f.answers[k.Key()] = strings.Contains(t, k.Norm)
}
- if id, ok := f.cacheID(); ok {
+ if id, ok := f.cacheID(); ok && f.partialErr == nil {
f.run.cache.Store(id, f.answers)
}
}