1 package systemdconfig
2
3 import (
4 "fmt"
5 "regexp"
6 "strings"
7
8 "slices"
9 )
10
11 var defaultSectionHeader = "DEFAULT"
12
13 type SystemdConfig struct {
14 sections map[string]*Section
15 orderedSectionHeaders []string
16 }
17
18
19 func LoadFromBytes(data []byte) (*SystemdConfig, error) {
20 lines := getLinesFromData(data)
21 sectionsLines, orderedSectionHeaders := splitLinesBySection(lines)
22
23 sections := map[string]*Section{}
24 for sectionHeader, sectionLines := range sectionsLines {
25 section, err := getSectionFromSectionLines(sectionLines)
26 if err != nil {
27 return nil, err
28 }
29 sections[sectionHeader] = section
30 }
31
32 return &SystemdConfig{sections, orderedSectionHeaders}, nil
33 }
34
35
36 func getLinesFromData(bytes []byte) []string {
37 return strings.Split(string(bytes), "\n")
38 }
39
40
41 func splitLinesBySection(lines []string) (map[string][]string, []string) {
42 currentSection := defaultSectionHeader
43 sections := map[string][]string{}
44 orderedSectionHeaders := []string{currentSection}
45 for _, line := range lines {
46 if sectionHeaderIsValid(line) {
47 currentSection = line
48 orderedSectionHeaders = append(orderedSectionHeaders, currentSection)
49 } else {
50 sections[currentSection] = append(sections[currentSection], line)
51 }
52 }
53
54 sections = removeTrailingEmptyLinesFromSectionsLines(sections)
55 return sections, orderedSectionHeaders
56 }
57
58
59 func sectionHeaderIsValid(sectionHeader string) bool {
60 return regexp.MustCompile(`^\[(\w+\s)*\w+\]$`).MatchString(sectionHeader)
61 }
62
63
64 func removeTrailingEmptyLinesFromSectionsLines(sections map[string][]string) map[string][]string {
65 for sectionHeader, section := range sections {
66 for idx := len(section) - 1; idx >= 0; idx-- {
67 if section[idx] == "" {
68 section = section[:idx]
69 } else {
70 break
71 }
72 }
73 sections[sectionHeader] = section
74 }
75 return sections
76 }
77
78
79 func getSectionFromSectionLines(sectionLines []string) (*Section, error) {
80 fields := map[string]*Field{}
81 orderedFieldKeys := []string{}
82 for idx, line := range sectionLines {
83 field, err := getFieldFromLine(line)
84 if err != nil {
85 return nil, err
86 }
87 fieldKey := generateKeyForField(field.GetKey(), idx)
88 fields[fieldKey] = field
89 orderedFieldKeys = append(orderedFieldKeys, fieldKey)
90 }
91 return &Section{fields, orderedFieldKeys}, nil
92 }
93
94
95 func getFieldFromLine(line string) (*Field, error) {
96
97 trimmedLine := strings.TrimSpace(line)
98 if trimmedLine == "" {
99 return &Field{}, nil
100 }
101
102
103 key, remainder, found := strings.Cut(line, "=")
104
105
106 if !found {
107 if !isCommented(trimmedLine) {
108 return nil, &SyntaxError{line, errSystemdConfigSyntax}
109 }
110 return &Field{comment: line}, nil
111 }
112
113 if !keyIsValid(key) {
114 return nil, &SyntaxError{line, errKeySyntax}
115 }
116
117 value, comment, err := splitValueFromComment(remainder)
118 if err != nil {
119 return nil, err
120 }
121
122 key, value, comment = strings.TrimSpace(key), strings.TrimSpace(value), strings.TrimSpace(comment)
123 return &Field{key, value, comment}, nil
124 }
125
126
127 func splitValueFromComment(line string) (string, string, error) {
128
129 value, comment, found := strings.Cut(line, "#")
130 if found {
131 comment = "#" + comment
132 }
133
134 if !found || strings.Contains(value, ";") {
135 value, comment, found = strings.Cut(line, ";")
136 if found {
137 comment = ";" + comment
138 }
139 }
140
141 if strings.HasSuffix(line, "\\") {
142 return "", "", &UnsupportedError{value, errMultiLineValuesUnsupported}
143 }
144 return value, comment, nil
145 }
146
147
148 func keyIsValid(key string) bool {
149 key = strings.TrimSpace(key)
150 if key == "" {
151 return false
152 }
153 if isCommented(key) {
154 if len(key) == 1 {
155 return false
156 }
157 key = strings.TrimSpace(key[1:])
158 }
159 return regexp.MustCompile(`^[A-Za-z0-9-]+$`).MatchString(key)
160 }
161
162 func isCommented(line string) bool {
163 line = strings.TrimSpace(line)
164 return strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";")
165 }
166
167
168 func valueIsValid(value string) bool {
169 value = strings.TrimSpace(value)
170 if value == "" {
171 return true
172 }
173 return !strings.ContainsAny(value, "#;")
174 }
175
176
177 func generateKeyForField(key string, lineIndex int) string {
178 if key == "" {
179
180 return fmt.Sprintf("!%d", lineIndex)
181 }
182 if isCommented(key) {
183
184 key = key[1:]
185 }
186 return key
187 }
188
189
190 func (systemdConfig *SystemdConfig) DeepCopy() *SystemdConfig {
191 copiedSections := map[string]*Section{}
192 for sectionHeader, section := range systemdConfig.sections {
193 copiedFields := map[string]*Field{}
194 for key, field := range section.fields {
195 copiedField := Field{field.GetKey(), field.GetValue(), field.GetComment()}
196 copiedFields[key] = &copiedField
197 }
198 copiedOrderedKeys := slices.Clone(section.orderedFieldKeys)
199 section := Section{copiedFields, copiedOrderedKeys}
200 copiedSections[sectionHeader] = §ion
201 }
202 copiedOrderedSectionHeaders := slices.Clone(systemdConfig.orderedSectionHeaders)
203 copiedSystemdConfig := SystemdConfig{copiedSections, copiedOrderedSectionHeaders}
204 return &copiedSystemdConfig
205 }
206
207
208 func (systemdConfig *SystemdConfig) Section(targetSectionHeader string) *Section {
209 return systemdConfig.sections[targetSectionHeader]
210 }
211
212
213 func (systemdConfig *SystemdConfig) ListSections() []string {
214 sectionHeaders := []string{}
215 for _, sectionHeader := range systemdConfig.orderedSectionHeaders {
216 if sectionHeader != defaultSectionHeader {
217 sectionHeaders = append(sectionHeaders, sectionHeader)
218 }
219 }
220 return sectionHeaders
221 }
222
223
224 func (systemdConfig *SystemdConfig) AddSection(targetSectionHeader string) error {
225 if !sectionHeaderIsValid(targetSectionHeader) {
226 return &SyntaxError{targetSectionHeader, errSectionHeaderSyntax}
227 }
228 if _, ok := systemdConfig.sections[targetSectionHeader]; !ok {
229 systemdConfig.sections[targetSectionHeader] = &Section{map[string]*Field{}, []string{}}
230 systemdConfig.orderedSectionHeaders = append(systemdConfig.orderedSectionHeaders, targetSectionHeader)
231 }
232 return nil
233 }
234
235
236 func (systemdConfig *SystemdConfig) RemoveSection(targetSectionHeader string) error {
237 if !sectionHeaderIsValid(targetSectionHeader) {
238 return &SyntaxError{targetSectionHeader, errSectionHeaderSyntax}
239 }
240 if _, ok := systemdConfig.sections[targetSectionHeader]; ok {
241 delete(systemdConfig.sections, targetSectionHeader)
242 updatedSectionHeaders := []string{}
243 for _, sectionHeader := range systemdConfig.orderedSectionHeaders {
244 if sectionHeader != targetSectionHeader {
245 updatedSectionHeaders = append(updatedSectionHeaders, sectionHeader)
246 }
247 }
248 systemdConfig.orderedSectionHeaders = updatedSectionHeaders
249 }
250 return nil
251 }
252
253
254 func (systemdConfig *SystemdConfig) HasSection(targetSectionHeader string) bool {
255 _, ok := systemdConfig.sections[targetSectionHeader]
256 return ok
257 }
258
259
260 func (systemdConfig *SystemdConfig) Bytes() []byte {
261 return joinLinesToBytes(systemdConfig.Lines())
262 }
263
264
265 func (systemdConfig *SystemdConfig) Lines() []string {
266 lines := []string{}
267 for _, sectionHeader := range systemdConfig.orderedSectionHeaders {
268 if sectionHeader != defaultSectionHeader {
269 lines = append(lines, "", sectionHeader)
270 }
271 for _, key := range systemdConfig.Section(sectionHeader).orderedFieldKeys {
272 lines = append(lines, systemdConfig.Section(sectionHeader).Field(key).ToFileLine())
273 }
274 }
275 return lines
276 }
277
278
279 func joinLinesToBytes(lines []string) []byte {
280 return []byte(strings.Join(lines, "\n"))
281 }
282
View as plain text