diff options
Diffstat (limited to 'internal/engine')
| -rw-r--r-- | internal/engine/exclude_test.go | 75 | ||||
| -rw-r--r-- | internal/engine/facts.go | 9 |
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) } } |
