aboutsummaryrefslogtreecommitdiff
path: root/internal/cond/types.go
blob: 97e761a7ca2edb8f499368141f4cf628042fd2a7 (plain) (blame)
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"

	"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"},
}