// SPDX-License-Identifier: GPL-3.0-or-later // Package cond compiles the s-expression conditions of a rule's (when ...) // into a tree that Task 8's 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; Task 8 needs them 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. Task 8 evaluates 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"}, }