summaryrefslogtreecommitdiff
path: root/internal/engine/engine.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 13:59:52 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 13:59:52 +0200
commitb44222fc2b061382dc601014cde287cc41b857ca (patch)
tree3181b6d3336bb6766803a9fc0ab76e09198aa253 /internal/engine/engine.go
parent9ab6686b98499c745024a474a71e3d99b6e14973 (diff)
downloadkrino-b44222fc2b061382dc601014cde287cc41b857ca.tar.gz
krino-b44222fc2b061382dc601014cde287cc41b857ca.zip
a duplicate test in an exclude protects like one in a rule
The scopes that drive "no rule deletes a file krino found to be a duplicate" were collected from rules only. A directory whose duplicate tests lived in (exclude ...) forms had no scopes at all, so the protection never engaged: krino explain said "yes duplicate" and the next rule permanently deleted every copy. The README's promise was false in that shape, and the spec's wording permitted it. What matters is what krino knows, not which form taught it. The spec and krino.conf(5) now say so too.
Diffstat (limited to 'internal/engine/engine.go')
-rw-r--r--internal/engine/engine.go32
1 files changed, 23 insertions, 9 deletions
diff --git a/internal/engine/engine.go b/internal/engine/engine.go
index 0c3186d..61c310e 100644
--- a/internal/engine/engine.go
+++ b/internal/engine/engine.go
@@ -159,7 +159,7 @@ func LoadWith(mainFile string, overrides map[string][]byte, names ...string) (*E
}
}
dir.ContentKeywords = contentKeywords(dir.Rules, dir.Excludes)
- dir.DupScopes = dupScopes(dir.Rules)
+ dir.DupScopes = dupScopes(dir.Rules, dir.Excludes)
dirs = append(dirs, dir)
}
@@ -267,16 +267,24 @@ func contentKeywords(rules []*Rule, excludes []*Exclude) []cond.Keyword {
return out
}
-// dupScopes returns the distinct Cond.DupDirs lists of rules, in first-seen
-// order. Lists are compared as written, with their length in the key so
-// (duplicate) and (duplicate "") stay apart; two spellings of one directory
-// stay two entries, which costs a second lookup but never a wrong answer,
-// since facts.Duplicate resolves and shares the index itself.
-func dupScopes(rules []*Rule) [][]string {
+// dupScopes returns the distinct Cond.DupDirs lists a directory uses, in
+// first-seen order, from its rules and its excludes alike. Lists are
+// compared as written, with their length in the key so (duplicate) and
+// (duplicate "") stay apart; two spellings of one directory stay two
+// entries, which costs a second lookup but never a wrong answer, since
+// facts.Duplicate resolves and shares the index itself.
+//
+// The excludes count because the protection these scopes drive - no rule
+// deletes a file krino has found to be a duplicate - is about what krino
+// knows, not about which form taught it. A directory whose only duplicate
+// test sat in an (exclude ...) had no scopes at all, so a later rule could
+// permanently delete every copy of a file krino had just called a
+// duplicate.
+func dupScopes(rules []*Rule, excludes []*Exclude) [][]string {
var out [][]string
seen := map[string]bool{}
- for _, r := range rules {
- for _, dirs := range r.Cond.DupDirs {
+ add := func(lists [][]string) {
+ for _, dirs := range lists {
key := fmt.Sprintf("%d\x00%s", len(dirs), strings.Join(dirs, "\x00"))
if seen[key] {
continue
@@ -285,6 +293,12 @@ func dupScopes(rules []*Rule) [][]string {
out = append(out, dirs)
}
}
+ for _, r := range rules {
+ add(r.Cond.DupDirs)
+ }
+ for _, x := range excludes {
+ add(x.Cond.DupDirs)
+ }
return out
}