This commit is contained in:
2026-03-19 15:00:10 +00:00
commit b7987248c1
7 changed files with 835 additions and 0 deletions

297
parser.go Normal file
View File

@@ -0,0 +1,297 @@
package main
import (
"bufio"
"fmt"
"os"
"slices"
"strings"
)
type Document struct {
Preamble []string
Sections []*Section
}
type Section struct {
Name string
Lines []Line
}
type Line struct {
Raw string
Key string
Value string
IsKV bool
Disabled bool
}
func ParseConfigFile(path string) (*Document, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseConfig(string(data))
}
func ParseConfig(contents string) (*Document, error) {
doc := &Document{}
var current *Section
scanner := bufio.NewScanner(strings.NewReader(contents))
for scanner.Scan() {
raw := scanner.Text()
trimmed := strings.TrimSpace(raw)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
name := strings.TrimSpace(trimmed[1 : len(trimmed)-1])
current = &Section{Name: name}
doc.Sections = append(doc.Sections, current)
continue
}
line := parseLine(raw)
if current == nil {
doc.Preamble = append(doc.Preamble, raw)
continue
}
current.Lines = append(current.Lines, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return doc, nil
}
func parseLine(raw string) Line {
trimmed := strings.TrimSpace(raw)
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
return Line{Raw: raw}
}
disabled := false
parseTarget := trimmed
if strings.HasPrefix(parseTarget, "#") || strings.HasPrefix(parseTarget, ";") {
disabled = true
parseTarget = strings.TrimSpace(parseTarget[1:])
}
key, value, ok := strings.Cut(parseTarget, "=")
if !ok {
return Line{Raw: raw}
}
return Line{
Raw: raw,
Key: strings.TrimSpace(strings.ToLower(key)),
Value: strings.TrimSpace(value),
IsKV: true,
Disabled: disabled,
}
}
func (d *Document) ShareSections() []*Section {
var shares []*Section
for _, section := range d.Sections {
if !strings.EqualFold(section.Name, "global") {
shares = append(shares, section)
}
}
return shares
}
func (d *Document) Section(name string) *Section {
for _, section := range d.Sections {
if strings.EqualFold(section.Name, name) {
return section
}
}
return nil
}
func (d *Document) DeleteSection(name string) bool {
for i, section := range d.Sections {
if strings.EqualFold(section.Name, name) {
d.Sections = append(d.Sections[:i], d.Sections[i+1:]...)
return true
}
}
return false
}
func (d *Document) UpsertSection(section *Section) {
for i, existing := range d.Sections {
if strings.EqualFold(existing.Name, section.Name) {
d.Sections[i] = section
return
}
}
d.Sections = append(d.Sections, section)
}
func (d *Document) Serialize() string {
var b strings.Builder
for _, line := range d.Preamble {
b.WriteString(line)
b.WriteString("\n")
}
if len(d.Preamble) > 0 && len(d.Sections) > 0 {
b.WriteString("\n")
}
for i, section := range d.Sections {
b.WriteString(fmt.Sprintf("[%s]\n", section.Name))
for _, line := range section.Lines {
if line.IsKV {
if line.Disabled {
b.WriteString(fmt.Sprintf("# %s = %s\n", line.Key, line.Value))
} else {
b.WriteString(fmt.Sprintf(" %s = %s\n", line.Key, line.Value))
}
continue
}
b.WriteString(line.Raw)
b.WriteString("\n")
}
if i < len(d.Sections)-1 {
b.WriteString("\n")
}
}
return b.String()
}
type ShareConfig struct {
Name string
Path string
Comment string
Browseable string
ReadOnly string
GuestOK string
ValidUsers []string
}
func ShareFromSection(section *Section) ShareConfig {
cfg := ShareConfig{
Name: section.Name,
Browseable: "yes",
ReadOnly: "no",
GuestOK: "no",
}
for _, line := range section.Lines {
if !line.IsKV || line.Disabled {
continue
}
switch line.Key {
case "path":
cfg.Path = line.Value
case "comment":
cfg.Comment = line.Value
case "browseable", "browsable":
cfg.Browseable = line.Value
case "read only", "readonly":
cfg.ReadOnly = line.Value
case "guest ok":
cfg.GuestOK = line.Value
case "valid users":
cfg.ValidUsers = splitUsers(line.Value)
}
}
return cfg
}
func BuildShareSection(existing *Section, cfg ShareConfig) *Section {
var lines []Line
if existing != nil {
lines = slices.Clone(existing.Lines)
} else {
lines = []Line{}
}
section := &Section{
Name: cfg.Name,
Lines: lines,
}
section.SetValue("path", cfg.Path)
if cfg.Comment != "" {
section.SetValue("comment", cfg.Comment)
} else {
section.DeleteValue("comment")
}
section.SetValue("browseable", normalizeBoolish(cfg.Browseable, "yes"))
section.SetValue("read only", normalizeBoolish(cfg.ReadOnly, "no"))
section.SetValue("guest ok", normalizeBoolish(cfg.GuestOK, "no"))
if len(cfg.ValidUsers) > 0 {
section.SetValue("valid users", strings.Join(cfg.ValidUsers, ", "))
} else {
section.DeleteValue("valid users")
}
return section
}
func (s *Section) SetValue(key, value string) {
canonical := strings.ToLower(strings.TrimSpace(key))
for i, line := range s.Lines {
if line.IsKV && line.Key == canonical {
s.Lines[i].Value = value
s.Lines[i].Disabled = false
return
}
}
s.Lines = append(s.Lines, Line{
Key: canonical,
Value: value,
IsKV: true,
})
}
func (s *Section) DeleteValue(key string) {
canonical := strings.ToLower(strings.TrimSpace(key))
dst := s.Lines[:0]
for _, line := range s.Lines {
if line.IsKV && line.Key == canonical {
continue
}
dst = append(dst, line)
}
s.Lines = dst
}
func splitUsers(raw string) []string {
fields := strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == ' ' || r == '\t'
})
var users []string
for _, field := range fields {
field = strings.TrimSpace(field)
if field != "" {
users = append(users, field)
}
}
return users
}
func normalizeBoolish(value, fallback string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "yes", "true", "1":
return "yes"
case "no", "false", "0":
return "no"
case "":
return fallback
default:
return value
}
}