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 } }