Files
stalwart-migrator/internal/preflight/storebackend.go
T
jcoffey-dev 4b0bec8956 Add SPDX headers to every Go file
GPLv3's "How to Apply These Terms" asks for a notice in each source file;
this is the modern two-line SPDX form of it rather than the full paragraph.
82 files, including tests.

The blank line after the header is load-bearing. In Go a comment block
immediately preceding `package X` becomes the package doc comment, so
without the separator the SPDX lines would be absorbed into the doc for the
eleven packages whose doc.go (or main.go) opens with one, and `go doc` would
print them. Verified it doesn't.
2026-08-23 18:03:15 -07:00

110 lines
3.3 KiB
Go

// SPDX-FileCopyrightText: 2026 LINUXexpert-org
// SPDX-License-Identifier: GPL-3.0-or-later
package preflight
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
)
// knownBackends is the set of store/blob/FTS backend identifiers Stalwart's
// documentation names: RocksDB, SQLite, FoundationDB, PostgreSQL, MySQL,
// S3-compatible storage, and Elasticsearch.
var knownBackends = map[string]bool{
"rocksdb": true, "sqlite": true, "foundationdb": true,
"postgresql": true, "mysql": true, "s3": true, "elasticsearch": true,
}
// BackendMatch is one "type = <backend>" assignment found in a config
// file, tagged with where it was found (the enclosing TOML section, or the
// dotted JSON key path).
type BackendMatch struct {
Path string
Backend string
}
// DetectStoreBackends scans a Stalwart config file for store/blob/FTS
// backend declarations. It deliberately does not assume one fixed schema
// path: the exact TOML (pre-0.16) or JSON (0.16+) layout has already
// changed once between those versions and may change again, so this
// searches structurally for `type = "<known backend>"` assignments wherever
// they appear and reports every match with its location. Treat the result
// as "here's what to confirm before an unattended run", not ground truth.
func DetectStoreBackends(configPath string) ([]BackendMatch, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("preflight: read config %s: %w", configPath, err)
}
if json.Valid(data) {
return scanJSONBackends(data)
}
return scanTOMLBackends(data)
}
var (
tomlSectionRe = regexp.MustCompile(`^\[(.+)\]$`)
tomlKVRe = regexp.MustCompile(`^([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"$`)
)
func scanTOMLBackends(data []byte) ([]BackendMatch, error) {
var matches []BackendMatch
section := ""
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if m := tomlSectionRe.FindStringSubmatch(line); m != nil {
section = m[1]
continue
}
if m := tomlKVRe.FindStringSubmatch(line); m != nil {
key, value := m[1], strings.ToLower(m[2])
if key == "type" && knownBackends[value] {
matches = append(matches, BackendMatch{Path: section, Backend: value})
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("preflight: scan toml config: %w", err)
}
return matches, nil
}
func scanJSONBackends(data []byte) ([]BackendMatch, error) {
var root any
if err := json.Unmarshal(data, &root); err != nil {
return nil, fmt.Errorf("preflight: parse json config: %w", err)
}
var matches []BackendMatch
walkJSONForBackends(root, "", &matches)
return matches, nil
}
func walkJSONForBackends(node any, path string, matches *[]BackendMatch) {
switch v := node.(type) {
case map[string]any:
if t, ok := v["type"].(string); ok && knownBackends[strings.ToLower(t)] {
*matches = append(*matches, BackendMatch{Path: path, Backend: strings.ToLower(t)})
}
for k, child := range v {
childPath := k
if path != "" {
childPath = path + "." + k
}
walkJSONForBackends(child, childPath, matches)
}
case []any:
for i, child := range v {
walkJSONForBackends(child, fmt.Sprintf("%s[%d]", path, i), matches)
}
}
}