staros/sysconf/document.go

1193 lines
28 KiB
Go
Raw Permalink Normal View History

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
}