// Package config implements the harness.toml surface for the MOPAC harness. // // The TOML parser in toml.go is a deliberate stdlib-only subset: tables, // bare keys, basic strings, integers, booleans, and (single- or multi-line) // string/int/bool arrays. Anything richer is a parse error so misconfigured // files fail loudly instead of silently. package config import ( "fmt" "os" "sort" "strings" ) // TOMLDoc is the parsed representation of harness.toml: nested tables are // TOMLDoc values; leaves are string, int64, bool, or []any of those. type TOMLDoc map[string]any // Table returns the nested table at the given key path, or an empty doc. func (d TOMLDoc) Table(keys ...string) TOMLDoc { cur := any(d) for _, k := range keys { m, ok := cur.(TOMLDoc) if !ok { return TOMLDoc{} } cur = m[k] } if m, ok := cur.(TOMLDoc); ok { return m } return TOMLDoc{} } func (d TOMLDoc) String(key string) (string, bool) { v, ok := d[key].(string) return v, ok } func (d TOMLDoc) Int(key string) (int64, bool) { v, ok := d[key].(int64) return v, ok } func (d TOMLDoc) Bool(key string) (bool, bool) { v, ok := d[key].(bool) return v, ok } func (d TOMLDoc) StringList(key string) ([]string, bool) { raw, ok := d[key].([]any) if !ok { return nil, false } out := make([]string, 0, len(raw)) for _, e := range raw { s, ok := e.(string) if !ok { return nil, false } out = append(out, s) } return out, true } // Keys returns the table's key names in sorted order. func (d TOMLDoc) Keys() []string { out := make([]string, 0, len(d)) for k := range d { out = append(out, k) } sort.Strings(out) return out } // ParseTOML parses the harness.toml subset. func ParseTOML(src string) (TOMLDoc, error) { lines, err := logicalLines(src) if err != nil { return nil, err } root := TOMLDoc{} cur := root for _, ln := range lines { if strings.HasPrefix(ln.text, "[") { path, err := parseTableName(ln.text, ln.no) if err != nil { return nil, err } cur, err = descend(root, path, ln.no) if err != nil { return nil, err } continue } if err := parseKeyValue(cur, ln.text, ln.no); err != nil { return nil, err } } return root, nil } type logicalLine struct { no int text string } // logicalLines joins physical lines into logical ones: comments are stripped // (quote-aware), blank lines dropped, and unbalanced array brackets pull the // next line in so multi-line arrays work. Strings may not span lines. func logicalLines(src string) ([]logicalLine, error) { var out []logicalLine var buf strings.Builder curNo := 0 depth := 0 for i, raw := range strings.Split(src, "\n") { no := i + 1 line := stripComment(strings.TrimSuffix(raw, "\r")) if buf.Len() == 0 { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } curNo = no buf.WriteString(trimmed) } else { buf.WriteString(" ") buf.WriteString(strings.TrimSpace(line)) } inStr, esc := false, false d := 0 for _, r := range line { switch { case esc: esc = false case inStr && r == '\\': esc = true case r == '"': inStr = !inStr case !inStr && r == '[': d++ case !inStr && r == ']': d-- } } if inStr { return nil, fmt.Errorf("harness.toml:%d: unterminated string", no) } if depth+d < 0 { return nil, fmt.Errorf("harness.toml:%d: unbalanced ']'", no) } depth += d if depth == 0 { out = append(out, logicalLine{no: curNo, text: buf.String()}) buf.Reset() } } if depth != 0 { return nil, fmt.Errorf("harness.toml:%d: unterminated array (missing ']')", curNo) } return out, nil } // stripComment removes a trailing # comment, ignoring # inside strings. func stripComment(line string) string { inStr, esc := false, false for i, r := range line { switch { case esc: esc = false case inStr && r == '\\': esc = true case r == '"': inStr = !inStr case r == '#' && !inStr: return line[:i] } } return line } func parseTableName(text string, no int) ([]string, error) { if !strings.HasSuffix(text, "]") { return nil, fmt.Errorf("harness.toml:%d: malformed table header %q", no, text) } inner := strings.TrimSuffix(strings.TrimPrefix(text, "["), "]") if inner == "" { return nil, fmt.Errorf("harness.toml:%d: empty table name", no) } parts := strings.Split(inner, ".") for _, p := range parts { if !isBareKey(p) { return nil, fmt.Errorf("harness.toml:%d: invalid table name segment %q", no, p) } } return parts, nil } func descend(root TOMLDoc, path []string, no int) (TOMLDoc, error) { cur := root for _, k := range path { if existing, ok := cur[k]; ok { sub, ok := existing.(TOMLDoc) if !ok { return nil, fmt.Errorf("harness.toml:%d: key %q already has a value, cannot become table", no, k) } cur = sub continue } sub := TOMLDoc{} cur[k] = sub cur = sub } return cur, nil } func parseKeyValue(m TOMLDoc, text string, no int) error { eq := strings.IndexByte(text, '=') if eq < 0 { return fmt.Errorf("harness.toml:%d: expected key = value, got %q", no, text) } key := strings.TrimSpace(text[:eq]) if strings.HasPrefix(key, `"`) { // Quoted key (e.g. `"In Progress" = "Done"` in a status map): // the key must be exactly one quoted string. q, err := parseStringValue(key, no) if err != nil { return fmt.Errorf("harness.toml:%d: invalid quoted key: %w", no, err) } key = q } else if !isBareKey(key) { return fmt.Errorf("harness.toml:%d: invalid key %q", no, key) } valStr := strings.TrimSpace(text[eq+1:]) if valStr == "" { return fmt.Errorf("harness.toml:%d: missing value for key %q", no, key) } val, err := parseValue(valStr, no) if err != nil { return err } if _, exists := m[key]; exists { return fmt.Errorf("harness.toml:%d: duplicate key %q", no, key) } m[key] = val return nil } func isBareKey(s string) bool { if s == "" { return false } for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-': default: return false } } return true } func parseValue(v string, no int) (any, error) { switch { case strings.HasPrefix(v, `"`): return parseStringValue(v, no) case strings.HasPrefix(v, "["): return parseArrayValue(v, no) default: return parseScalar(v, no) } } func parseScalar(v string, no int) (any, error) { switch v { case "true": return true, nil case "false": return false, nil } n, err := parseTOMLInt(v) if err != nil { return nil, fmt.Errorf("harness.toml:%d: unsupported value %q (want string, int, bool, or array)", no, v) } return n, nil } func parseTOMLInt(v string) (int64, error) { s := strings.ReplaceAll(v, "_", "") return parseSignedInt(s) } func parseSignedInt(s string) (int64, error) { var n int64 i := 0 neg := false if i < len(s) && (s[i] == '+' || s[i] == '-') { neg = s[i] == '-' i++ } if i >= len(s) { return 0, fmt.Errorf("not an integer: %q", s) } for ; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return 0, fmt.Errorf("not an integer: %q", s) } n = n*10 + int64(s[i]-'0') if n < 0 { return 0, fmt.Errorf("integer overflow: %q", s) } } if neg { n = -n } return n, nil } func parseStringValue(v string, no int) (string, error) { if len(v) < 2 { return "", fmt.Errorf("harness.toml:%d: unterminated string %q", no, v) } var b strings.Builder i := 1 for i < len(v) { c := v[i] if c == '"' { if rest := strings.TrimSpace(v[i+1:]); rest != "" { return "", fmt.Errorf("harness.toml:%d: unexpected trailing content after string: %q", no, rest) } return b.String(), nil } if c != '\\' { b.WriteByte(c) i++ continue } if i+1 >= len(v) { return "", fmt.Errorf("harness.toml:%d: dangling escape in %q", no, v) } switch v[i+1] { case '"': b.WriteByte('"') case '\\': b.WriteByte('\\') case 'n': b.WriteByte('\n') case 't': b.WriteByte('\t') case 'r': b.WriteByte('\r') default: return "", fmt.Errorf("harness.toml:%d: unsupported escape \\%c", no, v[i+1]) } i += 2 } return "", fmt.Errorf("harness.toml:%d: unterminated string", no) } func parseArrayValue(v string, no int) ([]any, error) { if !strings.HasSuffix(v, "]") { return nil, fmt.Errorf("harness.toml:%d: unterminated array %q", no, v) } inner := strings.TrimSpace(v[1 : len(v)-1]) if inner == "" { return []any{}, nil } var out []any elems := splitTopLevel(inner, ',') if len(elems) > 0 && strings.TrimSpace(elems[len(elems)-1]) == "" { elems = elems[:len(elems)-1] // tolerate a trailing comma } for _, elem := range elems { elem = strings.TrimSpace(elem) if elem == "" { return nil, fmt.Errorf("harness.toml:%d: empty array element in %q", no, v) } val, err := parseValue(elem, no) if err != nil { return nil, err } out = append(out, val) } return out, nil } // splitTopLevel splits on sep outside of double-quoted strings. func splitTopLevel(s string, sep rune) []string { var out []string var cur strings.Builder inStr, esc := false, false for _, r := range s { switch { case esc: esc = false cur.WriteRune(r) case inStr && r == '\\': esc = true cur.WriteRune(r) case r == '"': inStr = !inStr cur.WriteRune(r) case r == sep && !inStr: out = append(out, cur.String()) cur.Reset() default: cur.WriteRune(r) } } out = append(out, cur.String()) return out } // Load reads, parses, and validates harness.toml at path. func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("config: read %s: %w", path, err) } doc, err := ParseTOML(string(data)) if err != nil { return nil, fmt.Errorf("config: %w", err) } cfg := Default() if err := cfg.apply(doc); err != nil { return nil, fmt.Errorf("config: %w", err) } if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("config: %w", err) } return cfg, nil }