// SPDX-License-Identifier: GPL-3.0-or-later package model import ( "crypto/sha256" "fmt" "os" "strings" "krino/internal/config" "krino/internal/engine" "krino/internal/sexp" ) // MainSettings are the settings krino.conf carries for every directory, // inside its (defaults ...) form, in the order the window shows them. var MainSettings = []string{ "case", "fold", "recursive", "min-age", "max-read", "max-size", "busy", "on-conflict", } // MainConf is krino.conf open for editing. Only its (defaults ...) block // and the log path are edited here - the includes and the excludes are the // file's own business, and the Text tab is where those are written (GUI // design §5). type MainConf struct { File string Text string e *engine.Engine saved string stamp [32]byte } // OpenMain reads krino.conf. func OpenMain(e *engine.Engine) (*MainConf, error) { text, err := os.ReadFile(e.MainFile) if err != nil { return nil, err } return &MainConf{File: e.MainFile, Text: string(text), e: e, saved: string(text), stamp: sha256.Sum256(text)}, nil } // Modified reports whether the text differs from what is on disk. func (m *MainConf) Modified() bool { return m.Text != m.saved } // Check loads the whole configuration with this text in place of the file // and reports what is wrong with it. func (m *MainConf) Check() []*config.Diag { over := map[string][]byte{m.File: []byte(m.Text)} _, diags := engine.LoadWith(m.File, over) return diags } // Setting reads one default as it is written: "log" from the top level, // everything else from inside (defaults ...). func (m *MainConf) Setting(head string) (args string, set bool, err error) { if head != "log" && !isMainSetting(head) { return "", false, fmt.Errorf("model: %s is not a setting of krino.conf", head) } node, err := m.node(head) if err != nil || node == nil { return "", false, err } return argsOf(node, m.Text), true, nil } // SetSetting writes one default, or takes it out when args is empty. A // setting that is not in the file yet is written into (defaults ...), which // is created if the file has none. func (m *MainConf) SetSetting(head, args string) error { if head != "log" && !isMainSetting(head) { return fmt.Errorf("model: %s is not a setting of krino.conf", head) } args = strings.TrimSpace(args) node, err := m.node(head) if err != nil { return err } switch { case node != nil && args == "": start, end := lineStart(m.Text, node.Pos.Offset), lineEnd(m.Text, node.End.Offset) m.Text = join(m.Text[:start], m.Text[end:]) case node != nil: m.Text = m.Text[:node.Pos.Offset] + "(" + head + " " + args + ")" + m.Text[node.End.Offset:] case head == "log": m.Text = join(m.Text, "\n(log "+args+")\n") default: if err := m.setInDefaults(head, args); err != nil { return err } } return nil } // setInDefaults writes a setting inside (defaults ...), adding the form // when the file has none. func (m *MainConf) setInDefaults(head, args string) error { defaults, err := m.topLevel("defaults") if err != nil { return err } if defaults == nil { m.Text = join(m.Text, "\n(defaults\n ("+head+" "+args+"))\n") return nil } // Just inside the closing paren, on a line of its own. at := defaults.End.Offset - 1 m.Text = m.Text[:at] + "\n (" + head + " " + args + ")" + m.Text[at:] return nil } // node is the form for a setting: (log ...) at the top level, the rest // inside (defaults ...). func (m *MainConf) node(head string) (*sexp.Node, error) { if head == "log" { return m.topLevel("log") } defaults, err := m.topLevel("defaults") if err != nil || defaults == nil { return nil, err } for _, n := range defaults.Args() { if n.Kind == sexp.List && n.Head() == head { return n, nil } } return nil, nil } // topLevel is the top-level form with that head, or nil. func (m *MainConf) topLevel(head string) (*sexp.Node, error) { nodes, err := sexp.Parse(m.File, []byte(m.Text)) if err != nil { return nil, err } for _, n := range nodes { if n.Kind == sexp.List && n.Head() == head { return n, nil } } return nil, nil } // Save writes krino.conf, refusing a file that will not load and one that // changed on disk since it was opened - the rules the directory files are // saved by (GUI design §5.3). The previous text is kept as krino.conf.bak. func (m *MainConf) Save() error { if diags := m.Check(); len(diags) > 0 { return fmt.Errorf("%s", diags[0]) } on, err := os.ReadFile(m.File) if err != nil { return err } if sha256.Sum256(on) != m.stamp { return ErrChangedOnDisk } mode := os.FileMode(0o600) if fi, err := os.Stat(m.File); err == nil { mode = fi.Mode().Perm() } if err := os.WriteFile(m.File+".bak", on, mode); err != nil { return err } text := []byte(m.Text) if err := config.Replace(m.File, text); err != nil { return err } m.saved, m.stamp = m.Text, sha256.Sum256(text) return nil } // isMainSetting reports whether head is one of the defaults. func isMainSetting(head string) bool { for _, s := range MainSettings { if s == head { return true } } return false }