1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
// SPDX-License-Identifier: GPL-3.0-or-later
// Package cond compiles the s-expression conditions of a rule's (when ...)
// into a tree that the evaluator walks against one file's facts.
package cond
import (
"regexp"
"time"
"git.labunix.xyz/krino/internal/sexp"
)
// Options carries a rule's resolved case and fold settings into compilation.
type Options struct {
IgnoreCase bool // the rule's resolved case setting is "ignore"
Fold bool // the rule's resolved fold setting
}
// Cond is a compiled condition. A Cond compiled from no conditions (a rule
// without when) is always true.
type Cond struct {
root *node // nil: always true
opt Options // the case/fold settings conditions were compiled with; needed again at eval time
UsesContent bool // some content test exists
DupDirs [][]string // the raw directory arguments of each duplicate test, in order
Keywords []Keyword // every content keyword, as compiled, in order
}
// Keyword is one content keyword as a content test compares it: normalised
// under the options it was compiled with.
type Keyword struct {
Opt Options
Norm string
}
// Key is the keyword's identity across runs, for the keyword cache: its
// options and its normalised text.
func (k Keyword) Key() string { return KeywordKey(k.Opt, k.Norm) }
// KeywordKey is Keyword.Key for opt and an already normalised keyword.
func KeywordKey(opt Options, norm string) string {
b := []byte("sn:")
if opt.IgnoreCase {
b[0] = 'i'
}
if opt.Fold {
b[1] = 'f'
}
return string(b) + norm
}
// kind is what a compiled node tests, or how it combines its children.
type kind int
const (
kAnd kind = iota
kOr
kNot
kType
kName
kPath
kContent
kSize
kAge
kDuplicate
kMatched
)
// Costs, per the brief's cost order: cheapest first when sorting and/or
// children. not takes its child's cost; and/or take the sum of theirs.
const (
costCheap = 1 // type, size, age, matched
costRegex = 2 // name, path
costDuplicate = 5
costContent = 10
)
// pattern is one name/path regex, compiled and paired with the text it was
// written as (undecorated by (?i) or folding), for labels and reasons.
type pattern struct {
re *regexp.Regexp
src string
}
// keyword is one content keyword, normalised for matching and paired with
// the text it was written as, for labels and reasons.
type keyword struct {
norm string
src string
}
// node is one compiled condition: a leaf test, or an and/or/not combinator
// over other nodes. The evaluator walks this tree.
type node struct {
kind kind
pos sexp.Pos // the position of the node as written, for diagnostics
label string // the test as written, e.g. `content "acme ltd" "0000000000"`
cost int // this node's evaluation cost; and/or sort children by it
children []*node // and, or, not
// type
suffixes []string // leading-dot, lower-case, e.g. ".pdf"
// name, path
patterns []pattern
// content
keywords []keyword
// size, age (kind tells which is populated)
op string
sizeVal int64
ageVal time.Duration
// duplicate
dirs []string
}
// NameGroups returns the number of capture groups of every name test in the
// condition that can ever supply captures, in compile order: a name test
// nested inside and/or counts however deep, but one inside a not does not
// (B1) - it can never be the source of Result.Captures, so it must not be
// asked to justify a rule's use of {N} either. Empty when the rule has no
// such name test.
func (c *Cond) NameGroups() []int {
var out []int
collectNameGroups(c.root, &out)
return out
}
// groups maps a (type ...) group name to the extensions it expands to,
// spec Appendix A.
// Group is the extensions a built-in type group holds - what (type image)
// means - so a front end can treat a file the way the rules do rather than
// keeping a second list of its own. The result is not to be modified.
func Group(name string) []string { return groups[name] }
var groups = map[string][]string{
"image": {"jpg", "jpeg", "png", "gif", "webp", "bmp", "tif", "tiff", "heic", "heif", "avif", "svg", "ico", "raw", "cr2", "nef", "arw", "dng"},
"video": {"mp4", "mkv", "webm", "mov", "avi", "m4v", "mpg", "mpeg", "wmv", "flv", "3gp"},
"audio": {"mp3", "flac", "ogg", "opus", "m4a", "aac", "wav", "wma", "aiff"},
"archive": {"zip", "tar", "gz", "tgz", "bz2", "tbz2", "xz", "txz", "zst", "7z", "rar", "lz", "lzma", "cpio"},
"document": {"pdf", "doc", "docx", "odt", "rtf", "txt", "md", "tex"},
"spreadsheet": {"xls", "xlsx", "ods", "csv", "tsv"},
"presentation": {"ppt", "pptx", "odp"},
"ebook": {"epub", "mobi", "azw", "azw3", "fb2", "djvu"},
"code": {"go", "c", "h", "cpp", "hpp", "py", "sh", "js", "ts", "rs", "java", "rb", "pl", "lua", "html", "css", "json", "yaml", "yml", "toml", "xml", "sql"},
"text": {"txt", "md", "log", "csv", "tsv", "json", "yaml", "yml", "toml", "xml", "ini", "conf"},
"package": {"deb", "rpm", "apk", "appimage", "exe", "msi", "flatpak", "snap"},
"font": {"ttf", "otf", "woff", "woff2"},
}
|