...

Source file src/edge-infra.dev/pkg/sds/lib/systemd/systemdconfig/systemdconfig.go

Documentation: edge-infra.dev/pkg/sds/lib/systemd/systemdconfig

     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 // sections of the config file, mapping section headers sections
    15  	orderedSectionHeaders []string            // section headers, ordered as they appear in the file
    16  }
    17  
    18  // Creates new SystemdConfig from byte data
    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  // Splits bytes on newlines into a slice of (string) lines
    36  func getLinesFromData(bytes []byte) []string {
    37  	return strings.Split(string(bytes), "\n")
    38  }
    39  
    40  // Splits lines into sections, mapping section header to string lines
    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  	// removing trailing whitespace allows for uniform spacing between sections
    54  	sections = removeTrailingEmptyLinesFromSectionsLines(sections)
    55  	return sections, orderedSectionHeaders
    56  }
    57  
    58  // Given a line, return if it is a section header, i.e. '[Section]'
    59  func sectionHeaderIsValid(sectionHeader string) bool {
    60  	return regexp.MustCompile(`^\[(\w+\s)*\w+\]$`).MatchString(sectionHeader) // exactly match `[word(s)]` with single spaces between
    61  }
    62  
    63  // Removes trailing empty lines from section lines
    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  // Given a section's string lines, create a Section object
    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  // Given a line, create Field object
    95  func getFieldFromLine(line string) (*Field, error) {
    96  	// check if line is empty
    97  	trimmedLine := strings.TrimSpace(line)
    98  	if trimmedLine == "" {
    99  		return &Field{}, nil
   100  	}
   101  
   102  	// split on =, for 'key=value...'
   103  	key, remainder, found := strings.Cut(line, "=")
   104  
   105  	// if no key:value, there should be a comment
   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  // Split value from comment in string
   127  func splitValueFromComment(line string) (string, string, error) {
   128  	// split into 'value #comment'
   129  	value, comment, found := strings.Cut(line, "#")
   130  	if found {
   131  		comment = "#" + comment // add '#' to start of comment
   132  	}
   133  	// comments can also start with ';', so check if this occurs before '#'
   134  	if !found || strings.Contains(value, ";") {
   135  		value, comment, found = strings.Cut(line, ";")
   136  		if found {
   137  			comment = ";" + comment // add ';' to start of comment
   138  		}
   139  	}
   140  	// multi-line values are not supported
   141  	if strings.HasSuffix(line, "\\") {
   142  		return "", "", &UnsupportedError{value, errMultiLineValuesUnsupported}
   143  	}
   144  	return value, comment, nil
   145  }
   146  
   147  // Returns if the field key commented-out key is valid
   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  // Returns if the field value is valid
   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  // Generates a unique key for a field in a section. If there is no key value, one is created from line idx.
   177  func generateKeyForField(key string, lineIndex int) string {
   178  	if key == "" {
   179  		// give blank or commented lines unique key for section with invalid char to avoid collisions
   180  		return fmt.Sprintf("!%d", lineIndex)
   181  	}
   182  	if isCommented(key) {
   183  		// remove comment symbol from key
   184  		key = key[1:]
   185  	}
   186  	return key
   187  }
   188  
   189  // Creates a (deep) copy of the SystemConfig object
   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] = &section
   201  	}
   202  	copiedOrderedSectionHeaders := slices.Clone(systemdConfig.orderedSectionHeaders)
   203  	copiedSystemdConfig := SystemdConfig{copiedSections, copiedOrderedSectionHeaders}
   204  	return &copiedSystemdConfig
   205  }
   206  
   207  // Gets the section for a given section header
   208  func (systemdConfig *SystemdConfig) Section(targetSectionHeader string) *Section {
   209  	return systemdConfig.sections[targetSectionHeader]
   210  }
   211  
   212  // Lists the section headers in the systemd config
   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  // Adds a given section to the systemd config, if it didn't already exist
   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  // Removes a given section for the systemd config, if it exists
   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  // Returns if the systemd config has a given section
   254  func (systemdConfig *SystemdConfig) HasSection(targetSectionHeader string) bool {
   255  	_, ok := systemdConfig.sections[targetSectionHeader]
   256  	return ok
   257  }
   258  
   259  // Returns systemd config data as bytes
   260  func (systemdConfig *SystemdConfig) Bytes() []byte {
   261  	return joinLinesToBytes(systemdConfig.Lines())
   262  }
   263  
   264  // Returns lines of systemd config data
   265  func (systemdConfig *SystemdConfig) Lines() []string {
   266  	lines := []string{}
   267  	for _, sectionHeader := range systemdConfig.orderedSectionHeaders {
   268  		if sectionHeader != defaultSectionHeader {
   269  			lines = append(lines, "", sectionHeader) // add whitespace between sections
   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  // Joins (string) lines into bytes with newlines
   279  func joinLinesToBytes(lines []string) []byte {
   280  	return []byte(strings.Join(lines, "\n"))
   281  }
   282  

View as plain text