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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
// SPDX-License-Identifier: GPL-3.0-or-later
package cond
import (
"reflect"
"strings"
"testing"
"krino/internal/sexp"
)
func nodes(t *testing.T, src string) []*sexp.Node {
t.Helper()
n, err := sexp.Parse("d.conf", []byte(src))
if err != nil {
t.Fatalf("parse %q: %v", src, err)
}
return n
}
func TestCompileGood(t *testing.T) {
for _, src := range []string{
``,
`(type pdf)`,
`(type document image tar.gz)`,
`(and (type pdf) (or (content "acme ltd" "0000000000") (name "\bacme\b")) (not (name "^draft")))`,
`(path "^Work/") (size >= 10M) (age < 2w) (matched)`,
`(duplicate) (duplicate "Work" "~/docs")`,
`(not (not (type pdf)))`,
} {
if _, errs := Compile("d.conf", nodes(t, src), Options{IgnoreCase: true, Fold: true}); len(errs) > 0 {
t.Errorf("%s: %v", src, errs)
}
}
}
func TestCompileErrors(t *testing.T) {
tests := []struct{ src, want string }{
{`pdf`, `d.conf:1:1: a condition is a form like (type pdf), not pdf`},
{`("type" pdf)`, `d.conf:1:1: a condition is a form like (type pdf), not ("type" pdf)`},
{`(foo 1)`, `d.conf:1:1: unknown test (foo ...); tests are type, name, path, content, size, age, duplicate, matched, and, or, not`},
{`(and)`, `d.conf:1:1: and needs at least one condition`},
{`(or)`, `d.conf:1:1: or needs at least one condition`},
{`(not)`, `d.conf:1:1: not takes exactly one condition`},
{`(not (type pdf) (type doc))`, `d.conf:1:1: not takes exactly one condition`},
{`(type)`, `d.conf:1:1: type needs at least one extension or group, like (type pdf)`},
{`(type "pdf")`, `d.conf:1:7: type names are bare words: write (type pdf)`},
{`(name)`, `d.conf:1:1: name needs at least one regex in quotes`},
{`(path)`, `d.conf:1:1: path needs at least one regex in quotes`},
{`(name x)`, `d.conf:1:7: name takes regexes in quotes: write (name "x")`},
{`(name "(abc")`, `d.conf:1:7: name: bad regex "(abc": missing closing )`},
{`(path "[z-a]")`, `d.conf:1:7: path: bad regex "[z-a]": invalid character class range`},
{`(content)`, `d.conf:1:1: content needs at least one keyword in quotes`},
{`(content acme)`, `d.conf:1:10: content takes keywords in quotes: write (content "acme")`},
{`(content " ")`, `d.conf:1:10: content keyword is empty`},
{`(size 10M)`, `d.conf:1:1: size takes an operator and a size, like (size > 10M)`},
{`(age 30d)`, `d.conf:1:1: age takes an operator and a duration, like (age > 30d)`},
{`(size >> 10M)`, `d.conf:1:7: size operator is one of > >= < <= =, not >>`},
{`(size > 10Q)`, `d.conf:1:9: size: bad size "10Q": want a whole number with an optional K, M, G or T, like 50M`},
{`(age > 30)`, `d.conf:1:8: age: bad duration "30": want a whole number followed by s, m, h, d or w, like 30d`},
{`(duplicate Work)`, `d.conf:1:12: duplicate takes directories in quotes: write (duplicate "Work")`},
{`(matched x)`, `d.conf:1:1: matched takes nothing: write (matched)`},
}
for _, tt := range tests {
_, errs := Compile("d.conf", nodes(t, tt.src), Options{IgnoreCase: true})
if len(errs) != 1 || errs[0].Error() != tt.want {
t.Errorf("%s:\n got %v\n want %s", tt.src, errs, tt.want)
}
}
}
func TestCompileCollectsAllErrors(t *testing.T) {
_, errs := Compile("d.conf", nodes(t, `(type "a") (size 1) (matched x)`), Options{})
if len(errs) != 3 {
t.Fatalf("got %d errors: %v", len(errs), errs)
}
}
func TestDepthLimit(t *testing.T) {
deep := func(n int) string { return strings.Repeat("(not ", n) + "(type pdf)" + strings.Repeat(")", n) }
if _, errs := Compile("d.conf", nodes(t, deep(63)), Options{}); len(errs) != 0 {
t.Fatalf("64 levels rejected: %v", errs)
}
_, errs := Compile("d.conf", nodes(t, deep(64)), Options{})
if len(errs) != 1 || !strings.HasSuffix(errs[0].Error(), "conditions nest deeper than 64 levels") {
t.Fatalf("65 levels: %v", errs)
}
}
func TestFlagsAndDupDirs(t *testing.T) {
c, _ := Compile("d.conf", nodes(t, `(or (type pdf) (content "x")) (duplicate) (duplicate "Work" "~/docs")`), Options{})
if !c.UsesContent {
t.Error("UsesContent not set")
}
if want := [][]string{{}, {"Work", "~/docs"}}; !reflect.DeepEqual(c.DupDirs, want) {
t.Errorf("DupDirs = %#v, want %#v", c.DupDirs, want)
}
c, _ = Compile("d.conf", nodes(t, `(type pdf)`), Options{})
if c.UsesContent || len(c.DupDirs) != 0 {
t.Errorf("flags set without content/duplicate tests: %+v", c)
}
}
func TestCompileErrorReturnsNilCond(t *testing.T) {
c, errs := Compile("d.conf", nodes(t, `(type "pdf")`), Options{})
if len(errs) != 1 || c != nil {
t.Fatalf("got c=%v errs=%v, want c=nil and exactly one error", c, errs)
}
}
// TestEmptyChildrenGuard is a regression test for combine's empty-children
// guard: an and/or all of whose children failed to compile must not become
// a hollow node that evaluates vacuously (and -> true). Without the guard,
// the inner (and (name "[")) would compile to an empty and, evaluate to
// true, and the outer or would match on any file.
func TestEmptyChildrenGuard(t *testing.T) {
c, errs := compile("d.conf", nodes(t, `(or (type pdf) (and (name "[")))`), Options{}, true)
if len(errs) != 1 {
t.Fatalf("got %d errors, want 1: %v", len(errs), errs)
}
if c.Eval(&fake{name: "x.txt"}).Match {
t.Error("hollow and inside or vacuously matched")
}
}
// TestNameGroups: B1b - NameGroups() reports the capture-group count of
// every name test reachable without crossing a not, and skips one reachable
// only under a not (B1): here the outer (name ...) has two groups and the
// (not (name ...)) one has one, but the result must carry only the outer's
// count.
func TestNameGroups(t *testing.T) {
c, errs := Compile("d.conf", nodes(t, `(and (name "inv-(\d+)-(\d+)") (not (name "draft-(\d+)")))`), Options{})
if len(errs) > 0 {
t.Fatal(errs)
}
got := c.NameGroups()
want := []int{2}
if !reflect.DeepEqual(got, want) {
t.Errorf("NameGroups() = %v, want %v", got, want)
}
}
func TestGroupsMatchSpec(t *testing.T) {
want := 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",
}
if len(groups) != len(want) {
t.Fatalf("%d groups, want %d", len(groups), len(want))
}
for g, exts := range want {
if got := strings.Join(groups[g], " "); got != exts {
t.Errorf("group %s = %q, want %q", g, got, exts)
}
}
}
// TestCompileCollectsKeywords: every content keyword is recorded as
// compared, normalised under the compile options, and its key names those
// options.
func TestCompileCollectsKeywords(t *testing.T) {
opt := Options{IgnoreCase: true, Fold: true}
c, errs := Compile("d.conf", nodes(t, `(or (content "Spółka" "x") (not (content "NIP")))`), opt)
if len(errs) > 0 {
t.Fatal(errs)
}
want := []Keyword{{Opt: opt, Norm: "spolka"}, {Opt: opt, Norm: "x"}, {Opt: opt, Norm: "nip"}}
if !reflect.DeepEqual(c.Keywords, want) {
t.Errorf("Keywords = %+v, want %+v", c.Keywords, want)
}
if got := want[0].Key(); got != "if:spolka" {
t.Errorf("Key = %q", got)
}
if got := KeywordKey(Options{}, "NIP"); got != "sn:NIP" {
t.Errorf("strict, unfolded key = %q", got)
}
}
|