aboutsummaryrefslogtreecommitdiff
path: root/internal/engine/facts.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/engine/facts.go')
-rw-r--r--internal/engine/facts.go111
1 files changed, 83 insertions, 28 deletions
diff --git a/internal/engine/facts.go b/internal/engine/facts.go
index 5790e95..9035245 100644
--- a/internal/engine/facts.go
+++ b/internal/engine/facts.go
@@ -12,6 +12,8 @@ import (
"krino/internal/cond"
"krino/internal/dup"
+ "krino/internal/extract"
+ "krino/internal/kwcache"
"krino/internal/norm"
"krino/internal/plan"
"krino/internal/scan"
@@ -30,6 +32,7 @@ type matchRun struct {
ctx context.Context
now time.Time
files []scan.File
+ cache *kwcache.Cache // nil: no keyword cache
mu sync.Mutex
dupOnce map[string]*sync.Once
@@ -103,26 +106,25 @@ func (run *matchRun) dupIndex(key string, dirs []string) *dup.Index {
}
// facts is one file's cond.Facts. It is used by exactly one goroutine, so
-// its own memoised state (content, its normalised variants, and whether an
-// earlier rule matched) needs no locking of its own; only the matchRun it
-// points at is shared.
+// its own memoised state (the keyword answers, and whether an earlier rule
+// matched) needs no locking of its own; only the matchRun it points at is
+// shared.
type facts struct {
run *matchRun
file scan.File
matched bool
- contentDone bool
- content string
- contentErr error
- normCache map[[2]bool]string
+ contentDone bool // extraction was attempted
+ contentErr error // why it failed
+ answers map[string]bool // by cond.KeywordKey, once extracted
}
var _ cond.Facts = (*facts)(nil)
// newFacts builds the Facts for one scanned file.
func newFacts(run *matchRun, file scan.File) *facts {
- return &facts{run: run, file: file, normCache: make(map[[2]bool]string)}
+ return &facts{run: run, file: file}
}
func (f *facts) Name() string { return f.file.Name }
@@ -132,32 +134,85 @@ 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 }
-// Content extracts the file's text once, then normalises it per
-// (ignoreCase, fold) variant, memoising each. B2: when the directory's
-// rules use exactly one variant (Dir.ContentVariants), the raw text is
-// released as soon as that variant's normalised copy exists — no other
-// variant will ever be asked for, so there is no reason to keep both the
-// raw text and its normalised copy in memory at once. A directory using
-// more than one variant keeps the raw text for as long as f lives, exactly
-// as before.
-func (f *facts) Content(ignoreCase, fold bool) (string, error) {
+// ContentContains answers a content test (spec §6.1). A file above
+// max-read is never read, cached or not. Before the file has been
+// extracted this run, the keyword cache answers when it knows every one of
+// keywords for this file as it is now; otherwise the text is extracted
+// once, every keyword of the directory (and of this test) is answered from
+// it and stored in the cache, and the text itself is dropped. A failed
+// extraction is not cached: the next run tries again.
+func (f *facts) ContentContains(opt cond.Options, keywords []string) (int, error) {
+ if max := f.run.d.Settings.MaxRead; max > 0 && f.file.Size > max {
+ return -1, extract.ErrTooLarge
+ }
+ keys := make([]string, len(keywords))
+ for i, kw := range keywords {
+ keys[i] = cond.KeywordKey(opt, kw)
+ }
if !f.contentDone {
- f.content, f.contentErr = f.run.e.Extract.Text(f.run.ctx, f.file.Path, f.file.Size, f.run.d.Settings.MaxRead)
- f.contentDone = true
+ if id, ok := f.cacheID(); ok {
+ if hits, ok := f.run.cache.Lookup(id, keys); ok {
+ for i, hit := range hits {
+ if hit {
+ return i, nil
+ }
+ }
+ return -1, nil
+ }
+ }
+ f.extract(opt, keywords)
}
if f.contentErr != nil {
- return "", f.contentErr
+ return -1, f.contentErr
+ }
+ for i, k := range keys {
+ if f.answers[k] {
+ return i, nil
+ }
+ }
+ return -1, nil
+}
+
+// extract reads the file's text and answers every keyword of the directory,
+// plus the asking test's (opt, keywords), from it.
+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 {
+ f.contentErr = err
+ return
+ }
+ all := append([]cond.Keyword(nil), f.run.d.ContentKeywords...)
+ for _, kw := range keywords {
+ all = append(all, cond.Keyword{Opt: opt, Norm: kw})
+ }
+ normed := map[cond.Options]string{}
+ f.answers = make(map[string]bool, len(all))
+ for _, k := range all {
+ t, ok := normed[k.Opt]
+ if !ok {
+ t = norm.Text(text, k.Opt.IgnoreCase, k.Opt.Fold)
+ normed[k.Opt] = t
+ }
+ f.answers[k.Key()] = strings.Contains(t, k.Norm)
}
- key := [2]bool{ignoreCase, fold}
- if v, ok := f.normCache[key]; ok {
- return v, nil
+ if id, ok := f.cacheID(); ok {
+ f.run.cache.Store(id, f.answers)
}
- v := norm.Text(f.content, ignoreCase, fold)
- f.normCache[key] = v
- if len(f.run.d.ContentVariants) == 1 {
- f.content = ""
+}
+
+// cacheID is the file's keyword cache identity; ok is false when there is
+// no cache, or the platform gave the file no inode.
+func (f *facts) cacheID() (kwcache.ID, bool) {
+ if f.run.cache == nil || f.file.Ino == 0 {
+ return kwcache.ID{}, false
}
- return v, nil
+ return fileCacheID(f.file), true
+}
+
+// fileCacheID is file's kwcache.ID.
+func fileCacheID(file scan.File) kwcache.ID {
+ return kwcache.ID{Dev: file.Dev, Ino: file.Ino, Size: file.Size, MTime: file.ModTime.UnixNano()}
}
// Duplicate resolves dirs against the directory's root, builds (or reuses)