// SPDX-License-Identifier: GPL-3.0-or-later package config import ( "fmt" "slices" "time" "git.labunix.xyz/krino/internal/sexp" ) // CaseMode says whether name, path and content matching ignores case. type CaseMode int const ( CaseIgnore CaseMode = iota CaseStrict ) // Conflict is what to do when an action's target already exists. type Conflict int const ( ConflictSuffix Conflict = iota ConflictSkip ConflictOverwrite ) // String is the word a CaseMode is written as in a configuration. func (m CaseMode) String() string { switch m { case CaseIgnore: return "ignore" case CaseStrict: return "strict" } return fmt.Sprintf("CaseMode(%d)", int(m)) } // String is the word a Conflict policy is written as in a configuration. func (c Conflict) String() string { switch c { case ConflictSuffix: return "suffix" case ConflictSkip: return "skip" case ConflictOverwrite: return "overwrite" } return fmt.Sprintf("Conflict(%d)", int(c)) } // Settings holds what one level of config sets; nil means not set there. type Settings struct { Case *CaseMode Fold *bool Recursive *bool MaxDepth *int MinAge *time.Duration MaxRead *int64 MaxSize *int64 Busy *[]string OnConflict *Conflict } // Resolved is a complete set of settings. type Resolved struct { Case CaseMode Fold bool Recursive bool MaxDepth int // 0 means unlimited MinAge time.Duration MaxRead int64 MaxSize int64 // 0 means unlimited Busy []string OnConflict Conflict } // Builtin is what applies when nothing is set. func Builtin() Resolved { return Resolved{ Case: CaseIgnore, Fold: true, MinAge: 2 * time.Minute, MaxRead: 50 << 20, Busy: []string{".part", ".aria2", ".crdownload"}, OnConflict: ConflictSuffix, } } // Over returns base with every setting that s sets replaced. func (s Settings) Over(base Resolved) Resolved { r := base if s.Case != nil { r.Case = *s.Case } if s.Fold != nil { r.Fold = *s.Fold } if s.Recursive != nil { r.Recursive = *s.Recursive } if s.MaxDepth != nil { r.MaxDepth = *s.MaxDepth } if s.MinAge != nil { r.MinAge = *s.MinAge } if s.MaxRead != nil { r.MaxRead = *s.MaxRead } if s.MaxSize != nil { r.MaxSize = *s.MaxSize } if s.Busy != nil { r.Busy = *s.Busy } if s.OnConflict != nil { r.OnConflict = *s.OnConflict } return r } var settingNames = []string{"case", "fold", "recursive", "max-depth", "min-age", "max-read", "max-size", "busy", "on-conflict"} // ruleSettings are the settings a rule may override. var ruleSettings = map[string]bool{"case": true, "fold": true, "on-conflict": true} func isSetting(name string) bool { return slices.Contains(settingNames, name) } var settingHint = map[string]string{ "case": "(case ignore) or (case strict)", "fold": "(fold yes) or (fold no)", "recursive": "(recursive yes) or (recursive no)", "max-depth": "a number, like (max-depth 3)", "min-age": "a duration, like (min-age 2m)", "max-read": "a size, like (max-read 50M)", "max-size": "a size, like (max-size 1G)", "on-conflict": "(on-conflict suffix), skip or overwrite", } // parse reads the setting form n into s, reporting problems to d. seen // catches a setting given twice at the same level. func (s *Settings) parse(n *sexp.Node, d *diags, seen map[string]*sexp.Node) { name := n.Head() if first, ok := seen[name]; ok { d.at(n, "%s set twice (first at line %d)", name, first.Pos.Line) return } seen[name] = n args := n.Args() if name == "busy" { list := []string{} for _, a := range args { if a.Kind != sexp.String { d.at(a, `busy takes strings, like ".part"; got %s`, a) continue } list = append(list, a.Text) } s.Busy = &list return } if len(args) == 1 && args[0].Kind == sexp.String { d.at(args[0], "%s values are bare words: write (%s %s)", name, name, args[0].Text) return } if len(args) != 1 || args[0].Kind != sexp.Symbol { d.at(n, "%s takes one value: %s", name, settingHint[name]) return } v, at := args[0].Text, args[0] switch name { case "case": switch v { case "ignore": c := CaseIgnore s.Case = &c case "strict": c := CaseStrict s.Case = &c default: d.at(at, "case is ignore or strict, not %s", v) } case "fold", "recursive": b, ok := yesNo(v) if !ok { d.at(at, "%s is yes or no, not %s", name, v) } else if name == "fold" { s.Fold = &b } else { s.Recursive = &b } case "max-depth": depth, err := parseCount(v) if err != nil || depth < 1 || depth > 1<<20 { d.at(at, "max-depth is a whole number from 1, not %s", v) return } i := int(depth) s.MaxDepth = &i case "min-age": dur, err := ParseDuration(v) if err != nil { d.at(at, "min-age: %v", err) return } s.MinAge = &dur case "max-read": size, err := ParseSize(v) if err != nil { d.at(at, "max-read: %v", err) return } s.MaxRead = &size case "max-size": size, err := ParseSize(v) if err != nil { d.at(at, "max-size: %v", err) return } s.MaxSize = &size case "on-conflict": c, ok := map[string]Conflict{"suffix": ConflictSuffix, "skip": ConflictSkip, "overwrite": ConflictOverwrite}[v] if !ok { d.at(at, "on-conflict is suffix, skip or overwrite, not %s", v) return } s.OnConflict = &c } } func yesNo(v string) (bool, bool) { switch v { case "yes": return true, true case "no": return false, true } return false, false }