package sysconf import ( "errors" "fmt" "os" "reflect" "sort" ) type Ini struct { *Document } type IniProfile func(*Ini) func NewIni() *Ini { return &Ini{Document: NewDocument()} } func NewIniWithProfiles(profiles ...IniProfile) *Ini { ini := NewIni() for _, profile := range profiles { if profile != nil { profile(ini) } } return ini } func DefaultINIProfile() IniProfile { return func(ini *Ini) { if ini == nil { return } ini.Document = NewDocument() } } func StrictINIProfile() IniProfile { return func(ini *Ini) { if ini == nil { return } if ini.Document == nil { ini.Document = NewDocument() } ini.Strict = true ini.AllowNoValue = false } } func LinuxConfProfile(equal string) IniProfile { return func(ini *Ini) { if ini == nil { return } if ini.Document == nil { ini.Document = NewDocument() } ini.SectionOpen = "" ini.SectionClose = "" ini.CommentHeads = []string{"#"} if equal != "" { ini.Assign = equal } ini.AssignDelimiters = []string{ini.Assign} } } func (i *Ini) ApplyProfile(profile IniProfile) *Ini { if profile != nil { profile(i) } return i } func NewSysConf(equal string) *Ini { ini := NewIni() if equal != "" { ini.Assign = equal } ini.AssignDelimiters = []string{ini.Assign} return ini } func NewLinuxConf(equal string) *Ini { return NewIniWithProfiles(LinuxConfProfile(equal)) } func (i *Ini) Parse(data []byte) error { if i == nil || i.Document == nil { return ErrDocumentClosed } return i.Document.Parse(data) } func (i *Ini) ParseFromFile(path string) error { data, err := os.ReadFile(path) if err != nil { return err } return i.Parse(data) } func (i *Ini) Build() []byte { if i == nil || i.Document == nil { return nil } return i.Document.Bytes() } func (i *Ini) Save(path string) error { if i == nil || i.Document == nil { return ErrDocumentClosed } return i.Document.Save(path) } func (i *Ini) SaveAtomic(path string) error { if i == nil || i.Document == nil { return ErrDocumentClosed } return i.Document.SaveAtomic(path) } func (i *Ini) Section(name string) *Section { if i == nil || i.Document == nil { return nil } return i.Document.Section(name) } func (i *Ini) Sections(name string) []*Section { if i == nil || i.Document == nil { return nil } return i.Document.SectionsByName(name) } func (i *Ini) AddSection(name string) *Section { if i == nil || i.Document == nil { return nil } i.Document.mu.Lock() defer i.Document.mu.Unlock() return i.Document.appendSection(name, "", "", "\n") } func (i *Ini) DeleteSection(name string) bool { if i == nil || i.Document == nil { return false } i.Document.mu.Lock() defer i.Document.mu.Unlock() i.Document.rebuildSectionIndexLocked() normalized := normalize(name, i.CaseSensitive) sections := i.Document.sectionIndex[normalized] if len(sections) == 0 { return false } delete(i.Document.sectionIndex, normalized) filtered := i.Document.sections[:0] for _, section := range i.Document.sections { if normalize(section.Name, i.CaseSensitive) == normalized { continue } filtered = append(filtered, section) } i.Document.sections = filtered return true } func (i *Ini) Get(section, key string) string { for _, s := range i.Sections(section) { if s != nil && s.Exist(key) { return s.Get(key) } } return "" } func (i *Ini) GetAll(section, key string) []string { sections := i.Sections(section) if len(sections) == 0 { return nil } values := make([]string, 0) for _, s := range sections { if s == nil { continue } values = append(values, s.GetAll(key)...) } if len(values) == 0 { return nil } return values } func (i *Ini) Has(section, key string) bool { for _, s := range i.Sections(section) { if s != nil && s.Exist(key) { return true } } return false } func (i *Ini) Set(section, key, value string) { if i == nil || i.Document == nil { return } i.Document.mu.Lock() s := i.Document.ensureSection(section) i.Document.mu.Unlock() if s != nil { _ = s.Set(key, value, "") } } func (i *Ini) AddValue(section, key, value string) { if i == nil || i.Document == nil { return } i.Document.mu.Lock() s := i.Document.ensureSection(section) i.Document.mu.Unlock() if s != nil { _ = s.AddValue(key, value, "") } } func (i *Ini) Delete(section, key string) bool { if s := i.Section(section); s != nil { return s.Delete(key) == nil } return false } func (i *Ini) SectionsMap() map[string][]*Section { if i == nil || i.Document == nil { return nil } i.Document.mu.Lock() defer i.Document.mu.Unlock() i.Document.rebuildSectionIndexLocked() out := make(map[string][]*Section, len(i.Document.sectionIndex)) for name, sections := range i.Document.sectionIndex { out[name] = append([]*Section(nil), sections...) } return out } func (i *Ini) Unmarshal(dst interface{}) error { return bindINI(i, dst) } func (i *Ini) Marshal(src interface{}) ([]byte, error) { tmp := newIniLike(i) if err := marshalINI(tmp, src); err != nil { return nil, err } return tmp.Build(), nil } func bindINI(i *Ini, dst interface{}) error { if dst == nil { return errors.New("destination is nil") } v := reflect.ValueOf(dst) if v.Kind() != reflect.Ptr || v.IsNil() { return errors.New("destination must be a non-nil pointer") } v = v.Elem() if v.Kind() != reflect.Struct { return errors.New("destination must point to a struct") } return bindStruct(i, v, "") } func bindStruct(i *Ini, v reflect.Value, inheritedSection string) error { t := v.Type() for idx := 0; idx < t.NumField(); idx++ { field := t.Field(idx) value := v.Field(idx) if !value.CanSet() { continue } section := field.Tag.Get("seg") key := field.Tag.Get("key") if section == "" { section = inheritedSection } if key == "-" { continue } if isNestedConfigStruct(value, key) { nested := value for nested.Kind() == reflect.Ptr { if nested.IsNil() { nested.Set(reflect.New(nested.Type().Elem())) } nested = nested.Elem() } if err := bindStruct(i, nested, section); err != nil { return err } continue } if key == "" { continue } items := configValuesFromSections(i.Sections(section), key) if len(items) == 0 { continue } if err := setINIField(value, items); err != nil { return err } } return nil } func setINIField(value reflect.Value, items []configValue) error { return setConfigValueItems(value, items) } func marshalINI(dst *Ini, src interface{}) error { v := reflect.ValueOf(src) if !v.IsValid() { return errors.New("nil source") } if v.Kind() == reflect.Ptr { if v.IsNil() { return errors.New("nil source") } v = v.Elem() } if v.Kind() != reflect.Struct { return errors.New("source must be struct") } return marshalStruct(dst, v, "") } func marshalStruct(dst *Ini, v reflect.Value, inheritedSection string) error { t := v.Type() for i := 0; i < t.NumField(); i++ { field := t.Field(i) fv := v.Field(i) if !fv.CanInterface() { continue } section := field.Tag.Get("seg") if section == "" { section = inheritedSection } key := field.Tag.Get("key") comment := field.Tag.Get("comment") if key == "-" { continue } if nested, ok := nestedConfigValueForWrite(fv, key); ok { if nested.IsValid() { if err := marshalStruct(dst, nested, section); err != nil { return err } } continue } if key == "" { continue } if err := setINIValue(dst, section, key, fv); err != nil { return err } if comment != "" { sec := dst.Section(section) if sec == nil { sec = dst.AddSection(section) } if sec != nil { _ = sec.SetComment(key, comment) } } } return nil } func marshalSection(dst *Ini, section string, value reflect.Value) error { return marshalStruct(dst, value, section) } func setINIValue(dst *Ini, section, key string, value reflect.Value) error { for value.Kind() == reflect.Ptr { if value.IsNil() { return nil } value = value.Elem() } switch value.Kind() { case reflect.Slice, reflect.Array: if value.Type().Elem().Kind() != reflect.String { dst.Set(section, key, fmt.Sprint(value.Interface())) return nil } sec := dst.Section(section) if sec == nil { sec = dst.AddSection(section) } if sec == nil { return ErrDocumentClosed } values := make([]string, 0, value.Len()) for idx := 0; idx < value.Len(); idx++ { values = append(values, value.Index(idx).String()) } return sec.SetAll(key, values, "") case reflect.Map: if value.Type().Key().Kind() != reflect.String || value.Type().Elem().Kind() != reflect.String { dst.Set(section, key, fmt.Sprint(value.Interface())) return nil } keys := make([]string, 0, value.Len()) for _, mapKey := range value.MapKeys() { keys = append(keys, mapKey.String()) } sort.Strings(keys) values := make([]string, 0, len(keys)) for _, mapKey := range keys { values = append(values, mapKey+"="+value.MapIndex(reflect.ValueOf(mapKey)).String()) } sec := dst.Section(section) if sec == nil { sec = dst.AddSection(section) } if sec == nil { return ErrDocumentClosed } return sec.SetAll(key, values, "") default: dst.Set(section, key, fmt.Sprint(value.Interface())) return nil } }