github.com/safing/portbase@v0.19.5/config/persistence.go (about)

     1  package config
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/safing/portbase/log"
    12  )
    13  
    14  var (
    15  	configFilePath string
    16  
    17  	loadedConfigValidationErrors     []*ValidationError
    18  	loadedConfigValidationErrorsLock sync.Mutex
    19  )
    20  
    21  // GetLoadedConfigValidationErrors returns the encountered validation errors
    22  // from the last time loading config from disk.
    23  func GetLoadedConfigValidationErrors() []*ValidationError {
    24  	loadedConfigValidationErrorsLock.Lock()
    25  	defer loadedConfigValidationErrorsLock.Unlock()
    26  
    27  	return loadedConfigValidationErrors
    28  }
    29  
    30  func loadConfig(requireValidConfig bool) error {
    31  	// check if persistence is configured
    32  	if configFilePath == "" {
    33  		return nil
    34  	}
    35  
    36  	// read config file
    37  	data, err := os.ReadFile(configFilePath)
    38  	if err != nil {
    39  		return err
    40  	}
    41  
    42  	// convert to map
    43  	newValues, err := JSONToMap(data)
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	validationErrors, _ := ReplaceConfig(newValues)
    49  	if requireValidConfig && len(validationErrors) > 0 {
    50  		return fmt.Errorf("encountered %d validation errors during config loading", len(validationErrors))
    51  	}
    52  
    53  	// Save validation errors.
    54  	loadedConfigValidationErrorsLock.Lock()
    55  	defer loadedConfigValidationErrorsLock.Unlock()
    56  	loadedConfigValidationErrors = validationErrors
    57  
    58  	return nil
    59  }
    60  
    61  // SaveConfig saves the current configuration to file.
    62  // It will acquire a read-lock on the global options registry
    63  // lock and must lock each option!
    64  func SaveConfig() error {
    65  	optionsLock.RLock()
    66  	defer optionsLock.RUnlock()
    67  
    68  	// check if persistence is configured
    69  	if configFilePath == "" {
    70  		return nil
    71  	}
    72  
    73  	// extract values
    74  	activeValues := make(map[string]interface{})
    75  	for key, option := range options {
    76  		// we cannot immedately unlock the option afger
    77  		// getData() because someone could lock and change it
    78  		// while we are marshaling the value (i.e. for string slices).
    79  		// We NEED to keep the option locks until we finsihed.
    80  		option.Lock()
    81  		defer option.Unlock()
    82  
    83  		if option.activeValue != nil {
    84  			activeValues[key] = option.activeValue.getData(option)
    85  		}
    86  	}
    87  
    88  	// convert to JSON
    89  	data, err := MapToJSON(activeValues)
    90  	if err != nil {
    91  		log.Errorf("config: failed to save config: %s", err)
    92  		return err
    93  	}
    94  
    95  	// write file
    96  	return os.WriteFile(configFilePath, data, 0o0600)
    97  }
    98  
    99  // JSONToMap parses and flattens a hierarchical json object.
   100  func JSONToMap(jsonData []byte) (map[string]interface{}, error) {
   101  	loaded := make(map[string]interface{})
   102  	err := json.Unmarshal(jsonData, &loaded)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return Flatten(loaded), nil
   108  }
   109  
   110  // Flatten returns a flattened copy of the given hierarchical config.
   111  func Flatten(config map[string]interface{}) (flattenedConfig map[string]interface{}) {
   112  	flattenedConfig = make(map[string]interface{})
   113  	flattenMap(flattenedConfig, config, "")
   114  	return flattenedConfig
   115  }
   116  
   117  func flattenMap(rootMap, subMap map[string]interface{}, subKey string) {
   118  	for key, entry := range subMap {
   119  
   120  		// get next level key
   121  		subbedKey := path.Join(subKey, key)
   122  
   123  		// check for next subMap
   124  		nextSub, ok := entry.(map[string]interface{})
   125  		if ok {
   126  			flattenMap(rootMap, nextSub, subbedKey)
   127  		} else {
   128  			// only set if not on root level
   129  			rootMap[subbedKey] = entry
   130  		}
   131  	}
   132  }
   133  
   134  // MapToJSON expands a flattened map and returns it as json.
   135  func MapToJSON(config map[string]interface{}) ([]byte, error) {
   136  	return json.MarshalIndent(Expand(config), "", "  ")
   137  }
   138  
   139  // Expand returns a hierarchical copy of the given flattened config.
   140  func Expand(flattenedConfig map[string]interface{}) (config map[string]interface{}) {
   141  	config = make(map[string]interface{})
   142  	for key, entry := range flattenedConfig {
   143  		PutValueIntoHierarchicalConfig(config, key, entry)
   144  	}
   145  	return config
   146  }
   147  
   148  // PutValueIntoHierarchicalConfig injects a configuration entry into an hierarchical config map. Conflicting entries will be replaced.
   149  func PutValueIntoHierarchicalConfig(config map[string]interface{}, key string, value interface{}) {
   150  	parts := strings.Split(key, "/")
   151  
   152  	// create/check maps for all parts except the last one
   153  	subMap := config
   154  	for i, part := range parts {
   155  		if i == len(parts)-1 {
   156  			// do not process the last part,
   157  			// which is not a map, but the value key itself
   158  			break
   159  		}
   160  
   161  		var nextSubMap map[string]interface{}
   162  		// get value
   163  		value, ok := subMap[part]
   164  		if !ok {
   165  			// create new map and assign it
   166  			nextSubMap = make(map[string]interface{})
   167  			subMap[part] = nextSubMap
   168  		} else {
   169  			nextSubMap, ok = value.(map[string]interface{})
   170  			if !ok {
   171  				// create new map and assign it
   172  				nextSubMap = make(map[string]interface{})
   173  				subMap[part] = nextSubMap
   174  			}
   175  		}
   176  
   177  		// assign for next parts loop
   178  		subMap = nextSubMap
   179  	}
   180  
   181  	// assign value to last submap
   182  	subMap[parts[len(parts)-1]] = value
   183  }
   184  
   185  // CleanFlattenedConfig removes all inexistent configuration options from the given flattened config map.
   186  func CleanFlattenedConfig(flattenedConfig map[string]interface{}) {
   187  	optionsLock.RLock()
   188  	defer optionsLock.RUnlock()
   189  
   190  	for key := range flattenedConfig {
   191  		_, ok := options[key]
   192  		if !ok {
   193  			delete(flattenedConfig, key)
   194  		}
   195  	}
   196  }
   197  
   198  // CleanHierarchicalConfig removes all inexistent configuration options from the given hierarchical config map.
   199  func CleanHierarchicalConfig(config map[string]interface{}) {
   200  	optionsLock.RLock()
   201  	defer optionsLock.RUnlock()
   202  
   203  	cleanSubMap(config, "")
   204  }
   205  
   206  func cleanSubMap(subMap map[string]interface{}, subKey string) (empty bool) {
   207  	var foundValid int
   208  	for key, value := range subMap {
   209  		value, ok := value.(map[string]interface{})
   210  		if ok {
   211  			// we found another section
   212  			isEmpty := cleanSubMap(value, path.Join(subKey, key))
   213  			if isEmpty {
   214  				delete(subMap, key)
   215  			} else {
   216  				foundValid++
   217  			}
   218  			continue
   219  		}
   220  
   221  		// we found an option value
   222  		if strings.Contains(key, "/") {
   223  			delete(subMap, key)
   224  		} else {
   225  			_, ok := options[path.Join(subKey, key)]
   226  			if ok {
   227  				foundValid++
   228  			} else {
   229  				delete(subMap, key)
   230  			}
   231  		}
   232  	}
   233  	return foundValid == 0
   234  }