package sysconf import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "sort" "strconv" "strings" "sync" ) var ( ErrDocumentClosed = errors.New("document is nil") ErrSectionNotFound = errors.New("section not found") ErrKeyNotFound = errors.New("key not found") ) type Document struct { mu sync.RWMutex sections []*Section sectionIndex map[string][]*Section SectionOpen string SectionClose string Assign string AssignDelimiters []string CommentHeads []string AllowInline bool InlineCommentRequiresSpace bool AllowNoValue bool AllowMulti bool AllowContinuation bool TrimSpace bool CaseSensitive bool Strict bool } type Section struct { Name string HeaderComment string Raw string Newline string Entries []*Entry entryIndex map[string][]*Entry CaseSensitive bool rawName string parsedHeaderComment string } type Entry struct { Key string Values []string Comment string Raw string NoValue bool Delimiter string Newline string kind lineKind parsedKey string parsedValues []string parsedComment string parsedNoValue bool parsedDelimiter string } type parsedLine struct { kind lineKind raw string newline string key string value string comment string sectionName string noValue bool delimiter string line int } type lineKind int const ( lineEmpty lineKind = iota lineComment lineSection linePair lineRaw ) func NewDocument() *Document { return &Document{ sectionIndex: make(map[string][]*Section), SectionOpen: "[", SectionClose: "]", Assign: "=", AssignDelimiters: []string{"=", ":"}, CommentHeads: []string{"#", ";"}, AllowInline: true, InlineCommentRequiresSpace: true, AllowNoValue: true, AllowMulti: true, AllowContinuation: true, TrimSpace: true, } } type ParseError struct { Line int Column int Message string } func (e *ParseError) Error() string { if e == nil { return "" } if e.Column > 0 { return fmt.Sprintf("sysconf: parse error at line %d, column %d: %s", e.Line, e.Column, e.Message) } return fmt.Sprintf("sysconf: parse error at line %d: %s", e.Line, e.Message) } func normalize(text string, caseSensitive bool) string { if caseSensitive { return text } return strings.ToLower(text) } func (d *Document) Parse(data []byte) error { if d == nil { return ErrDocumentClosed } d.mu.Lock() defer d.mu.Unlock() d.sections = nil d.sectionIndex = make(map[string][]*Section) current := d.appendSection("", "", "", "") lines := splitSourceLines(string(data)) for idx := 0; idx < len(lines); idx++ { source := lines[idx] raw := source.text text := source.text newline := source.newline if d.AllowContinuation { var err error raw, text, newline, idx, err = d.consumeContinuation(lines, idx) if err != nil { return err } } line, err := parseRawLine(raw, text, d, source.line) if err != nil { return err } line.newline = newline switch line.kind { case lineSection: current = d.appendSection(line.sectionName, line.comment, line.raw, line.newline) case linePair, lineComment, lineEmpty, lineRaw: if current == nil { current = d.ensureSection("") } current.addParsed(line) } } return nil } type sourceLine struct { text string newline string line int } func splitSourceLines(text string) []sourceLine { if text == "" { return nil } lines := make([]sourceLine, 0) start := 0 line := 1 for idx := 0; idx < len(text); idx++ { switch text[idx] { case '\n': lines = append(lines, sourceLine{text: text[start:idx], newline: "\n", line: line}) start = idx + 1 line++ case '\r': newline := "\r" end := idx + 1 if end < len(text) && text[end] == '\n' { newline = "\r\n" end++ } lines = append(lines, sourceLine{text: text[start:idx], newline: newline, line: line}) start = end idx = end - 1 line++ } } if start < len(text) { lines = append(lines, sourceLine{text: text[start:], line: line}) } return lines } func (d *Document) consumeContinuation(lines []sourceLine, idx int) (string, string, string, int, error) { source := lines[idx] raw := source.text text := source.text newline := source.newline for hasContinuation(text) { if idx+1 >= len(lines) { if d.Strict { return raw, text, newline, idx, &ParseError{Line: source.line, Column: continuationColumn(text), Message: "line continuation has no following line"} } return raw, text, newline, idx, nil } next := lines[idx+1] raw += newline + next.text text = trimContinuation(text) + strings.TrimLeft(next.text, " \t") newline = next.newline idx++ } return raw, text, newline, idx, nil } func hasContinuation(text string) bool { trimmed := strings.TrimRight(text, " \t") if !strings.HasSuffix(trimmed, `\`) { return false } count := 0 for idx := len(trimmed) - 1; idx >= 0 && trimmed[idx] == '\\'; idx-- { count++ } return count%2 == 1 } func trimContinuation(text string) string { trimmed := strings.TrimRight(text, " \t") return trimmed[:len(trimmed)-1] } func continuationColumn(text string) int { trimmed := strings.TrimRight(text, " \t") return len(trimmed) } func parseRawLine(raw, text string, doc *Document, lineNo int) (parsedLine, error) { trimmed := text if doc.TrimSpace { trimmed = strings.TrimSpace(text) } if trimmed == "" { return parsedLine{kind: lineEmpty, raw: raw, line: lineNo}, nil } for _, head := range doc.CommentHeads { if strings.HasPrefix(trimmed, head) { return parsedLine{kind: lineComment, raw: raw, comment: strings.TrimSpace(strings.TrimPrefix(trimmed, head)), line: lineNo}, nil } } if doc.SectionOpen != "" && doc.SectionClose != "" && strings.HasPrefix(trimmed, doc.SectionOpen) { line, err := parseSectionLine(raw, trimmed, doc, lineNo) if err != nil { return parsedLine{}, err } return line, nil } delimiter, idx := findAssignDelimiter(trimmed, doc.assignDelimiters()) if idx < 0 { if doc.AllowNoValue { key, comment, err := splitInlineComment(trimmed, doc, lineNo) if err != nil { return parsedLine{}, err } key = strings.TrimSpace(key) if key == "" && doc.Strict { return parsedLine{}, &ParseError{Line: lineNo, Column: 1, Message: "empty key"} } return parsedLine{kind: linePair, raw: raw, key: key, comment: comment, noValue: true, delimiter: doc.writeDelimiter(), line: lineNo}, nil } if doc.Strict { return parsedLine{}, &ParseError{Line: lineNo, Column: 1, Message: "missing key/value delimiter"} } return parsedLine{kind: lineRaw, raw: raw, line: lineNo}, nil } key := strings.TrimSpace(trimmed[:idx]) if key == "" && doc.Strict { return parsedLine{}, &ParseError{Line: lineNo, Column: 1, Message: "empty key"} } right := strings.TrimSpace(trimmed[idx+len(delimiter):]) value, comment, err := splitInlineComment(right, doc, lineNo) if err != nil { return parsedLine{}, err } value, err = parseValue(value, doc, lineNo) if err != nil { return parsedLine{}, err } return parsedLine{kind: linePair, raw: raw, key: key, value: value, comment: comment, delimiter: delimiter, line: lineNo}, nil } func parseSectionLine(raw, trimmed string, doc *Document, lineNo int) (parsedLine, error) { end := strings.Index(trimmed[len(doc.SectionOpen):], doc.SectionClose) if end < 0 { if doc.Strict { return parsedLine{}, &ParseError{Line: lineNo, Column: len(trimmed), Message: "section header is missing closing marker"} } return parsedLine{kind: lineRaw, raw: raw, line: lineNo}, nil } end += len(doc.SectionOpen) name := strings.TrimSpace(trimmed[len(doc.SectionOpen):end]) if name == "" && doc.Strict { return parsedLine{}, &ParseError{Line: lineNo, Column: len(doc.SectionOpen) + 1, Message: "empty section name"} } tail := strings.TrimSpace(trimmed[end+len(doc.SectionClose):]) comment := "" if tail != "" { var err error rest, inlineComment, err := splitInlineComment(tail, doc, lineNo) if err != nil { return parsedLine{}, err } if strings.TrimSpace(rest) != "" { if doc.Strict { return parsedLine{}, &ParseError{Line: lineNo, Column: end + len(doc.SectionClose) + 1, Message: "unexpected text after section header"} } return parsedLine{kind: lineRaw, raw: raw, line: lineNo}, nil } comment = inlineComment } return parsedLine{kind: lineSection, raw: raw, sectionName: name, comment: comment, line: lineNo}, nil } func (d *Document) assignDelimiters() []string { if d == nil { return nil } if len(d.AssignDelimiters) > 0 { out := make([]string, 0, len(d.AssignDelimiters)) for _, delimiter := range d.AssignDelimiters { if delimiter != "" { out = append(out, delimiter) } } sort.SliceStable(out, func(i, j int) bool { return len(out[i]) > len(out[j]) }) return out } if d.Assign == "" { return nil } return []string{d.Assign} } func (d *Document) writeDelimiter() string { if d == nil { return "=" } if d.Assign != "" { return d.Assign } if len(d.AssignDelimiters) > 0 { for _, delimiter := range d.AssignDelimiters { if delimiter != "" { return delimiter } } } return "=" } func findAssignDelimiter(text string, delimiters []string) (string, int) { if len(delimiters) == 0 { return "", -1 } var quote byte escaped := false for idx := 0; idx < len(text); idx++ { ch := text[idx] if escaped { escaped = false continue } if ch == '\\' { escaped = true continue } if ch == '"' || ch == '\'' { if quote == 0 { quote = ch continue } if quote == ch { quote = 0 continue } } if quote != 0 { continue } for _, delimiter := range delimiters { if delimiter != "" && strings.HasPrefix(text[idx:], delimiter) { return delimiter, idx } } } return "", -1 } func splitInlineComment(text string, doc *Document, lineNo int) (string, string, error) { if !doc.AllowInline { return strings.TrimSpace(text), "", nil } var quote byte escaped := false for i := 0; i < len(text); i++ { ch := text[i] if escaped { escaped = false continue } if ch == '\\' { escaped = true continue } if ch == '"' || ch == '\'' { if quote == 0 { quote = ch continue } if quote == ch { quote = 0 continue } } if quote != 0 { continue } for _, head := range doc.CommentHeads { if head == "" || !strings.HasPrefix(text[i:], head) { continue } if doc.InlineCommentRequiresSpace && i > 0 && text[i-1] != ' ' && text[i-1] != '\t' { continue } return strings.TrimSpace(text[:i]), strings.TrimSpace(text[i+len(head):]), nil } } if quote != 0 && doc.Strict { return "", "", &ParseError{Line: lineNo, Column: len(text), Message: "unterminated quoted value"} } return strings.TrimSpace(text), "", nil } func parseValue(value string, doc *Document, lineNo int) (string, error) { value = strings.TrimSpace(value) if value == "" { return "", nil } quote := value[0] if quote != '"' && quote != '\'' { return value, nil } escaped := false end := -1 for idx := 1; idx < len(value); idx++ { ch := value[idx] if escaped { escaped = false continue } if ch == '\\' { escaped = true continue } if ch == quote { end = idx break } } if end < 0 { if doc.Strict { return "", &ParseError{Line: lineNo, Column: 1, Message: "unterminated quoted value"} } return value, nil } if strings.TrimSpace(value[end+1:]) != "" { if doc.Strict { return "", &ParseError{Line: lineNo, Column: end + 2, Message: "unexpected text after quoted value"} } return value, nil } return unescapeQuotedValue(value[1:end], quote), nil } func unescapeQuotedValue(value string, quote byte) string { var builder strings.Builder builder.Grow(len(value)) escaped := false for idx := 0; idx < len(value); idx++ { ch := value[idx] if !escaped { if ch == '\\' { escaped = true continue } builder.WriteByte(ch) continue } switch ch { case 'n': builder.WriteByte('\n') case 'r': builder.WriteByte('\r') case 't': builder.WriteByte('\t') case '\\': builder.WriteByte('\\') case '"', '\'': if ch == quote { builder.WriteByte(ch) } else { builder.WriteByte('\\') builder.WriteByte(ch) } default: builder.WriteByte('\\') builder.WriteByte(ch) } escaped = false } if escaped { builder.WriteByte('\\') } return builder.String() } func (p parsedLine) toEntry() *Entry { switch p.kind { case lineComment: return &Entry{Raw: p.raw, Comment: p.comment, Newline: p.newline, kind: p.kind} case lineEmpty: return &Entry{Raw: p.raw, Newline: p.newline, kind: p.kind} case lineRaw: return &Entry{Raw: p.raw, Newline: p.newline, kind: p.kind} } entry := &Entry{ Key: p.key, Comment: p.comment, Raw: p.raw, NoValue: p.noValue, Delimiter: p.delimiter, Newline: p.newline, kind: p.kind, } if !p.noValue { entry.Values = []string{p.value} } entry.rememberParsed() return entry } func (e *Entry) rememberParsed() { if e == nil { return } e.parsedKey = e.Key e.parsedValues = append([]string(nil), e.Values...) e.parsedComment = e.Comment e.parsedNoValue = e.NoValue e.parsedDelimiter = e.Delimiter } func (e *Entry) parsedPairUnchanged() bool { if e == nil || e.kind != linePair || e.Raw == "" { return false } if e.Key != e.parsedKey || e.Comment != e.parsedComment || e.NoValue != e.parsedNoValue || e.Delimiter != e.parsedDelimiter { return false } if len(e.Values) != len(e.parsedValues) { return false } for idx := range e.Values { if e.Values[idx] != e.parsedValues[idx] { return false } } return true } func (d *Document) ensureSection(name string) *Section { d.rebuildSectionIndexLocked() key := normalize(name, d.CaseSensitive) if sections := d.sectionIndex[key]; len(sections) > 0 { return sections[0] } return d.appendSection(name, "", "", "\n") } func (d *Document) rebuildSectionIndexLocked() { if d.sectionIndex == nil { d.sectionIndex = make(map[string][]*Section) } for key := range d.sectionIndex { delete(d.sectionIndex, key) } for _, section := range d.sections { if section == nil { continue } key := normalize(section.Name, d.CaseSensitive) d.sectionIndex[key] = append(d.sectionIndex[key], section) } } func (d *Document) appendSection(name, comment, raw, newline string) *Section { section := &Section{ Name: name, HeaderComment: comment, Raw: raw, Newline: newline, CaseSensitive: d.CaseSensitive, entryIndex: make(map[string][]*Entry), rawName: name, parsedHeaderComment: comment, } if section.Newline == "" && raw == "" { section.Newline = "\n" } key := normalize(name, d.CaseSensitive) d.sections = append(d.sections, section) d.sectionIndex[key] = append(d.sectionIndex[key], section) return section } func (s *Section) addParsed(line parsedLine) { switch line.kind { case linePair: entry := line.toEntry() s.addEntry(entry) case lineComment, lineEmpty, lineRaw: s.Entries = append(s.Entries, line.toEntry()) } } func (s *Section) addEntry(entry *Entry) { if s.entryIndex == nil { s.entryIndex = make(map[string][]*Entry) } key := normalize(entry.Key, s.CaseSensitive) s.Entries = append(s.Entries, entry) s.entryIndex[key] = append(s.entryIndex[key], entry) } func (d *Document) Section(name string) *Section { if d == nil { return nil } d.mu.Lock() defer d.mu.Unlock() d.rebuildSectionIndexLocked() sections := d.sectionIndex[normalize(name, d.CaseSensitive)] if len(sections) == 0 { return nil } return sections[0] } func (d *Document) SectionsByName(name string) []*Section { if d == nil { return nil } d.mu.Lock() defer d.mu.Unlock() d.rebuildSectionIndexLocked() sections := d.sectionIndex[normalize(name, d.CaseSensitive)] return append([]*Section(nil), sections...) } func (d *Document) Sections() []*Section { if d == nil { return nil } d.mu.RLock() defer d.mu.RUnlock() return append([]*Section(nil), d.sections...) } func (s *Section) Entry(key string) *Entry { if s == nil { return nil } entries := s.entryIndex[normalize(key, s.CaseSensitive)] if len(entries) == 0 { return nil } return entries[0] } func (s *Section) EntriesByKey(key string) []*Entry { if s == nil { return nil } entries := s.entryIndex[normalize(key, s.CaseSensitive)] return append([]*Entry(nil), entries...) } func (s *Section) Keys() []string { if s == nil { return nil } keys := make([]string, 0, len(s.entryIndex)) for _, entries := range s.entryIndex { if len(entries) == 0 { continue } keys = append(keys, entries[0].Key) } sort.Strings(keys) return keys } func (s *Section) Get(key string) string { if entry := s.Entry(key); entry != nil && len(entry.Values) > 0 { return entry.Values[0] } return "" } func (s *Section) GetAll(key string) []string { if s == nil { return nil } entries := s.EntriesByKey(key) if len(entries) == 0 { return nil } values := make([]string, 0) for _, entry := range entries { values = append(values, entry.Values...) } return values } func (s *Section) Int(key string) int { v, _ := strconv.Atoi(s.Get(key)) return v } func (s *Section) Int64(key string) int64 { v, _ := strconv.ParseInt(s.Get(key), 10, 64) return v } func (s *Section) Int32(key string) int32 { v, _ := strconv.ParseInt(s.Get(key), 10, 32) return int32(v) } func (s *Section) Float64(key string) float64 { v, _ := strconv.ParseFloat(s.Get(key), 64) return v } func (s *Section) Float32(key string) float32 { v, _ := strconv.ParseFloat(s.Get(key), 32) return float32(v) } func (s *Section) Bool(key string) bool { v, _ := strconv.ParseBool(s.Get(key)) return v } func (s *Section) SetBool(key string, value bool, comment string) error { return s.Set(key, strconv.FormatBool(value), comment) } func (s *Section) SetFloat64(key string, prec int, value float64, comment string) error { return s.Set(key, strconv.FormatFloat(value, 'f', prec, 64), comment) } func (s *Section) SetFloat32(key string, prec int, value float32, comment string) error { return s.Set(key, strconv.FormatFloat(float64(value), 'f', prec, 32), comment) } func (s *Section) SetUint64(key string, value uint64, comment string) error { return s.Set(key, strconv.FormatUint(value, 10), comment) } func (s *Section) SetInt64(key string, value int64, comment string) error { return s.Set(key, strconv.FormatInt(value, 10), comment) } func (s *Section) SetInt32(key string, value int32, comment string) error { return s.Set(key, strconv.FormatInt(int64(value), 10), comment) } func (s *Section) SetInt(key string, value int, comment string) error { return s.Set(key, strconv.Itoa(value), comment) } type countingWriter struct { w io.Writer n int64 } func (w *countingWriter) Write(data []byte) (int, error) { n, err := w.w.Write(data) w.n += int64(n) return n, err } func (d *Document) WriteTo(w io.Writer) (int64, error) { if d == nil { return 0, ErrDocumentClosed } d.mu.RLock() defer d.mu.RUnlock() counting := &countingWriter{w: w} for _, section := range d.sections { if section == nil { continue } if section.Raw != "" && section.Name == section.rawName && section.HeaderComment == section.parsedHeaderComment { if err := writeLine(counting, section.Raw, section.Newline, true); err != nil { return counting.n, err } } else if section.Name != "" && d.SectionOpen != "" { if err := writeLine(counting, d.formatSectionHeader(section), section.Newline, false); err != nil { return counting.n, err } } for _, entry := range section.Entries { if err := d.writeEntry(counting, entry); err != nil { return counting.n, err } } } return counting.n, nil } func writeLine(w io.Writer, text, newline string, preserveNoNewline bool) error { if text != "" { if _, err := io.WriteString(w, text); err != nil { return err } } if newline != "" { _, err := io.WriteString(w, newline) return err } if preserveNoNewline { return nil } _, err := io.WriteString(w, "\n") return err } func (d *Document) writeEntry(w io.Writer, entry *Entry) error { if entry == nil { return nil } if entry.kind != linePair && entry.Raw != "" { return writeLine(w, entry.Raw, entry.Newline, true) } if entry.parsedPairUnchanged() { return writeLine(w, entry.Raw, entry.Newline, true) } newline := entry.Newline if newline == "" { newline = "\n" } delimiter := entry.Delimiter if delimiter == "" { delimiter = d.Assign } if delimiter == "" { delimiter = "=" } if entry.NoValue || len(entry.Values) == 0 { text := entry.Key if entry.Comment != "" && d.AllowInline && len(d.CommentHeads) > 0 { text += " " + d.CommentHeads[0] + entry.Comment return writeLine(w, text, newline, false) } if err := writeLine(w, text, newline, false); err != nil { return err } if entry.Comment != "" && len(d.CommentHeads) > 0 { return writeLine(w, d.CommentHeads[0]+entry.Comment, newline, false) } return nil } value := d.formatValue(entry.Values[0]) text := entry.Key + delimiter + value if entry.Comment != "" && d.AllowInline && len(d.CommentHeads) > 0 { text += " " + d.CommentHeads[0] + entry.Comment } if err := writeLine(w, text, newline, false); err != nil { return err } if d.AllowMulti && len(entry.Values) > 1 { for _, extra := range entry.Values[1:] { extraValue := d.formatValue(extra) if err := writeLine(w, entry.Key+delimiter+extraValue, newline, false); err != nil { return err } } } return nil } func (d *Document) formatSectionHeader(section *Section) string { text := d.SectionOpen + section.Name + d.SectionClose if section.HeaderComment != "" && d.AllowInline && len(d.CommentHeads) > 0 { text += " " + d.CommentHeads[0] + section.HeaderComment } return text } func (d *Document) formatValue(value string) string { if !d.valueNeedsQuotes(value) { return value } return quoteValue(value) } func (d *Document) valueNeedsQuotes(value string) bool { if value == "" { return false } if strings.TrimSpace(value) != value || strings.ContainsAny(value, "\r\n\t") { return true } if value[0] == '"' || value[0] == '\'' { return true } if d != nil && d.AllowInline { for _, head := range d.CommentHeads { if head != "" && strings.Contains(value, head) { return true } } } return false } func quoteValue(value string) string { var builder strings.Builder builder.Grow(len(value) + 2) builder.WriteByte('"') for idx := 0; idx < len(value); idx++ { switch value[idx] { case '\\': builder.WriteString(`\\`) case '"': builder.WriteString(`\"`) case '\n': builder.WriteString(`\n`) case '\r': builder.WriteString(`\r`) case '\t': builder.WriteString(`\t`) default: builder.WriteByte(value[idx]) } } builder.WriteByte('"') return builder.String() } func (d *Document) Bytes() []byte { var buf bytes.Buffer _, _ = d.WriteTo(&buf) return buf.Bytes() } func (d *Document) Save(path string) error { if d == nil { return ErrDocumentClosed } return os.WriteFile(path, d.Bytes(), 0o644) } func (d *Document) SaveAtomic(path string) error { if d == nil { return ErrDocumentClosed } return writeFileAtomic(path, d.Bytes(), 0o644) } func writeFileAtomic(path string, data []byte, defaultPerm os.FileMode) error { perm := defaultPerm if info, err := os.Stat(path); err == nil { perm = info.Mode().Perm() } else if !os.IsNotExist(err) { return err } dir := filepath.Dir(path) base := filepath.Base(path) tmp, err := os.CreateTemp(dir, "."+base+".tmp-*") if err != nil { return err } tmpPath := tmp.Name() keepTmp := false defer func() { if !keepTmp { _ = os.Remove(tmpPath) } }() if _, err := tmp.Write(data); err != nil { _ = tmp.Close() return err } if err := tmp.Chmod(perm); err != nil { _ = tmp.Close() return err } if err := tmp.Sync(); err != nil { _ = tmp.Close() return err } if err := tmp.Close(); err != nil { return err } if err := os.Rename(tmpPath, path); err != nil { return err } keepTmp = true syncParentDir(dir) return nil } func syncParentDir(dir string) { f, err := os.Open(dir) if err != nil { return } _ = f.Sync() _ = f.Close() } func (s *Section) Exist(key string) bool { return s.Entry(key) != nil } func (s *Section) Comment(key string) string { if entry := s.Entry(key); entry != nil { return entry.Comment } return "" } func (s *Section) SetComment(key, comment string) error { entry := s.Entry(key) if entry == nil { return ErrKeyNotFound } entry.Comment = comment return nil } func (s *Section) Set(key, value, comment string) error { return s.SetAll(key, []string{value}, comment) } func (s *Section) SetAll(key string, values []string, comment string) error { if s == nil { return ErrSectionNotFound } if s.entryIndex == nil { s.entryIndex = make(map[string][]*Entry) } normalized := normalize(key, s.CaseSensitive) entries := s.entryIndex[normalized] if len(entries) == 0 { entry := &Entry{Key: key, Values: append([]string(nil), values...), Comment: comment, Newline: s.Newline} if len(values) == 0 { entry.NoValue = true } s.addEntry(entry) return nil } first := entries[0] first.Values = append([]string(nil), values...) first.Comment = comment first.NoValue = len(values) == 0 if len(entries) > 1 { s.entryIndex[normalized] = []*Entry{first} filtered := s.Entries[:0] for _, entry := range s.Entries { if normalize(entry.Key, s.CaseSensitive) != normalized { filtered = append(filtered, entry) continue } if entry == first { filtered = append(filtered, entry) } } s.Entries = filtered } return nil } func (s *Section) AddValue(key, value, comment string) error { if s == nil { return ErrSectionNotFound } if s.entryIndex == nil { s.entryIndex = make(map[string][]*Entry) } normalized := normalize(key, s.CaseSensitive) entry := &Entry{Key: key, Values: []string{value}, Comment: comment, Newline: s.Newline} s.Entries = append(s.Entries, entry) s.entryIndex[normalized] = append(s.entryIndex[normalized], entry) return nil } func (s *Section) Delete(key string) error { if s == nil { return ErrSectionNotFound } normalized := normalize(key, s.CaseSensitive) if len(s.entryIndex[normalized]) == 0 { return ErrKeyNotFound } delete(s.entryIndex, normalized) filtered := s.Entries[:0] for _, entry := range s.Entries { if normalize(entry.Key, s.CaseSensitive) == normalized { continue } filtered = append(filtered, entry) } s.Entries = filtered return nil } func (s *Section) DeleteValue(key, value string) error { normalized := normalize(key, s.CaseSensitive) entries := s.entryIndex[normalized] if len(entries) == 0 { return ErrKeyNotFound } kept := make([]*Entry, 0, len(entries)) for _, entry := range entries { filtered := entry.Values[:0] for _, item := range entry.Values { if item == value { continue } filtered = append(filtered, item) } if len(filtered) == 0 { continue } entry.Values = filtered kept = append(kept, entry) } if len(kept) == 0 { delete(s.entryIndex, normalized) } else { s.entryIndex[normalized] = kept } filteredEntries := s.Entries[:0] for _, entry := range s.Entries { if normalize(entry.Key, s.CaseSensitive) != normalized { filteredEntries = append(filteredEntries, entry) continue } for _, keptEntry := range kept { if entry == keptEntry { filteredEntries = append(filteredEntries, entry) break } } } s.Entries = filteredEntries return nil }