From 1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 14 Sep 2026 10:56:21 +0200 Subject: krino: duplicates are found, never deleted A rule combining (duplicate) with a delete action is refused at load, and a file that is a duplicate under any duplicate scope its directory uses gets no delete step from any rule: the plan shows it skipped and the chain continues. A failed duplicate check blocks the delete too. Tests cover (matched), another rule's own condition, a test evaluation skipped, two scopes and a failed check, each checking every copy is still on disk. --- internal/engine/match.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) (limited to 'internal/engine/match.go') diff --git a/internal/engine/match.go b/internal/engine/match.go index e68a6e8..ffee7f0 100644 --- a/internal/engine/match.go +++ b/internal/engine/match.go @@ -33,6 +33,12 @@ type FileMatch struct { File scan.File Rules []RuleMatch // matching rules in order, ending at the first with (stop) Warnings []string // ": ", e.g. "acme: content unreadable: needs pdftotext, not installed" + + // NoDelete is non-empty when no delete step may run for this file (spec + // §5.5 rule 2), and says why: the file is a duplicate under a scope its + // directory's rules use, or that check failed. Only set for a file some + // matching rule would delete. + NoDelete string } // Result is everything Match found in one directory. @@ -127,9 +133,45 @@ func evalFile(run *matchRun, file scan.File) FileMatch { break } } + if len(run.d.DupScopes) > 0 && deletes(fm.Rules) { + fm.NoDelete = noDelete(f, run.d.DupScopes) + } return fm } +// NeverDeleted is the reason a duplicate's delete step is skipped (spec +// §5.5). +const NeverDeleted = "a duplicate is never deleted" + +// deletes reports whether any of rules has a delete action. +func deletes(rules []RuleMatch) bool { + for _, rm := range rules { + for _, a := range rm.Rule.Conf.Actions { + if a.Kind == config.Delete || a.Kind == config.DeletePermanent { + return true + } + } + } + return false +} + +// noDelete looks f up under every scope, whether or not evaluating the +// rules reached that test (spec §5.5 rule 2), and returns why f must not be +// deleted, or "" when it may be. A failed lookup blocks the delete too: +// krino cannot show the file is not a duplicate, so it keeps it. +func noDelete(f *facts, scopes [][]string) string { + for _, dirs := range scopes { + _, dup, err := f.Duplicate(dirs) + if err != nil { + return "duplicate check failed, so not deleted: " + err.Error() + } + if dup { + return NeverDeleted + } + } + return "" +} + // RuleTrace is one rule's outcome in an Explain call. type RuleTrace struct { Rule *Rule -- cgit v1.3