package config import ( "encoding/json" "fmt" "reflect" "regexp" "sync" "github.com/mitchellh/copystructure" "github.com/tidwall/sjson" "github.com/safing/portmaster/base/database/record" "github.com/safing/structures/dsd" ) // OptionType defines the value type of an option. type OptionType uint8 // Various attribute options. Use ExternalOptType for extended types in the frontend. const ( optTypeAny OptionType = 0 OptTypeString OptionType = 1 OptTypeStringArray OptionType = 2 OptTypeInt OptionType = 3 OptTypeBool OptionType = 4 ) func getTypeName(t OptionType) string { switch t { case optTypeAny: return "any" case OptTypeString: return "string" case OptTypeStringArray: return "[]string" case OptTypeInt: return "int" case OptTypeBool: return "bool" default: return "unknown" } } // PossibleValue defines a value that is possible for // a configuration setting. type PossibleValue struct { // Name is a human readable name of the option. Name string // Description is a human readable description of // this value. Description string // Value is the actual value of the option. The type // must match the option's value type. Value interface{} } // Annotations can be attached to configuration options to // provide hints for user interfaces or other systems working // or setting configuration options. // Annotation keys should follow the below format to ensure // future well-known annotation additions do not conflict // with vendor/product/package specific annoations. // // Format: <vendor/package>:<scope>:<identifier> //. type Annotations map[string]interface{} // MigrationFunc is a function that migrates a config option value. type MigrationFunc func(option *Option, value any) any // Well known annotations defined by this package. const ( // DisplayHintAnnotation provides a hint for the user // interface on how to render an option. // The value of DisplayHintAnnotation is expected to // be a string. See DisplayHintXXXX constants below // for a list of well-known display hint annotations. DisplayHintAnnotation = "safing/portbase:ui:display-hint" // DisplayOrderAnnotation provides a hint for the user // interface in which order settings should be displayed. // The value of DisplayOrderAnnotations is expected to be // an number (int). DisplayOrderAnnotation = "safing/portbase:ui:order" // UnitAnnotations defines the SI unit of an option (if any). UnitAnnotation = "safing/portbase:ui:unit" // CategoryAnnotations can provide an additional category // to each settings. This category can be used by a user // interface to group certain options together. // User interfaces should treat a CategoryAnnotation, if // supported, with higher priority as a DisplayOrderAnnotation. CategoryAnnotation = "safing/portbase:ui:category" // SubsystemAnnotation can be used to mark an option as part // of a module subsystem. SubsystemAnnotation = "safing/portbase:module:subsystem" // StackableAnnotation can be set on configuration options that // stack on top of the default (or otherwise related) options. // The value of StackableAnnotaiton is expected to be a boolean but // may be extended to hold references to other options in the // future. StackableAnnotation = "safing/portbase:options:stackable" // RestartPendingAnnotation is automatically set on a configuration option // that requires a restart and has been changed. // The value must always be a boolean with value "true". RestartPendingAnnotation = "safing/portbase:options:restart-pending" // QuickSettingAnnotation can be used to add quick settings to // a configuration option. A quick setting can support the user // by switching between pre-configured values. // The type of a quick-setting annotation is []QuickSetting or QuickSetting. QuickSettingsAnnotation = "safing/portbase:ui:quick-setting" // RequiresAnnotation can be used to mark another option as a // requirement. The type of RequiresAnnotation is []ValueRequirement // or ValueRequirement. RequiresAnnotation = "safing/portbase:config:requires" // RequiresFeatureIDAnnotation can be used to mark a setting as only available // when the user has a certain feature ID in the subscription plan. // The type is []string or string. RequiresFeatureIDAnnotation = "safing/portmaster:ui:config:requires-feature" // SettablePerAppAnnotation can be used to mark a setting as settable per-app and // is a boolean. SettablePerAppAnnotation = "safing/portmaster:settable-per-app" // RequiresUIReloadAnnotation can be used to inform the UI that changing the value // of the annotated setting requires a full reload of the user interface. // The value of this annotation does not matter as the sole presence of // the annotation key is enough. Though, users are advised to set the value // of this annotation to true. RequiresUIReloadAnnotation = "safing/portmaster:ui:requires-reload" ) // QuickSettingsAction defines the action of a quick setting. type QuickSettingsAction string const ( // QuickReplace replaces the current setting with the one from // the quick setting. QuickReplace = QuickSettingsAction("replace") // QuickMergeTop merges the value of the quick setting with the // already configured one adding new values on the top. Merging // is only supported for OptTypeStringArray. QuickMergeTop = QuickSettingsAction("merge-top") // QuickMergeBottom merges the value of the quick setting with the // already configured one adding new values at the bottom. Merging // is only supported for OptTypeStringArray. QuickMergeBottom = QuickSettingsAction("merge-bottom") ) // QuickSetting defines a quick setting for a configuration option and // should be used together with the QuickSettingsAnnotation. type QuickSetting struct { // Name is the name of the quick setting. Name string // Value is the value that the quick-setting configures. It must match // the expected value type of the annotated option. Value interface{} // Action defines the action of the quick setting. Action QuickSettingsAction } // ValueRequirement defines a requirement on another configuration option. type ValueRequirement struct { // Key is the key of the configuration option that is required. Key string // Value that is required. Value interface{} } // Values for the DisplayHintAnnotation. const ( // DisplayHintOneOf is used to mark an option // as a "select"-style option. That is, only one of // the supported values may be set. This option makes // only sense together with the PossibleValues property // of Option. DisplayHintOneOf = "one-of" // DisplayHintOrdered is used to mark a list option as ordered. // That is, the order of items is important and a user interface // is encouraged to provide the user with re-ordering support // (like drag'n'drop). DisplayHintOrdered = "ordered" // DisplayHintFilePicker is used to mark the option as being a file, which // should give the option to use a file picker to select a local file from disk. DisplayHintFilePicker = "file-picker" ) // Option describes a configuration option. type Option struct { sync.Mutex // Name holds the name of the configuration options. // It should be human readable and is mainly used for // presentation purposes. // Name is considered immutable after the option has // been created. Name string // Key holds the database path for the option. It should // follow the path format `category/sub/key`. // Key is considered immutable after the option has // been created. Key string // Description holds a human readable description of the // option and what is does. The description should be short. // Use the Help property for a longer support text. // Description is considered immutable after the option has // been created. Description string // Help may hold a long version of the description providing // assistance with the configuration option. // Help is considered immutable after the option has // been created. Help string // Sensitive signifies that the configuration values may contain sensitive // content, such as authentication keys. Sensitive bool // OptType defines the type of the option. // OptType is considered immutable after the option has // been created. OptType OptionType // ExpertiseLevel can be used to set the required expertise // level for the option to be displayed to a user. // ExpertiseLevel is considered immutable after the option has // been created. ExpertiseLevel ExpertiseLevel // ReleaseLevel is used to mark the stability of the option. // ReleaseLevel is considered immutable after the option has // been created. ReleaseLevel ReleaseLevel // RequiresRestart should be set to true if a modification of // the options value requires a restart of the whole application // to take effect. // RequiresRestart is considered immutable after the option has // been created. RequiresRestart bool // DefaultValue holds the default value of the option. Note that // this value can be overwritten during runtime (see activeDefaultValue // and activeFallbackValue). // DefaultValue is considered immutable after the option has // been created. DefaultValue interface{} // ValidationRegex may contain a regular expression used to validate // the value of option. If the option type is set to OptTypeStringArray // the validation regex is applied to all entries of the string slice. // Note that it is recommended to keep the validation regex simple so // it can also be used in other languages (mainly JavaScript) to provide // a better user-experience by pre-validating the expression. // ValidationRegex is considered immutable after the option has // been created. ValidationRegex string // ValidationFunc may contain a function to validate more complex values. // The error is returned beyond the scope of this package and may be // displayed to a user. ValidationFunc func(value interface{}) error `json:"-"` // PossibleValues may be set to a slice of values that are allowed // for this configuration setting. Note that PossibleValues makes most // sense when ExternalOptType is set to HintOneOf // PossibleValues is considered immutable after the option has // been created. PossibleValues []PossibleValue `json:",omitempty"` // Annotations adds additional annotations to the configuration options. // See documentation of Annotations for more information. // Annotations is considered mutable and setting/reading annotation keys // must be performed while the option is locked. Annotations Annotations // Migrations holds migration functions that are given the raw option value // before any validation is run. The returned value is then used. Migrations []MigrationFunc `json:"-"` activeValue *valueCache // runtime value (loaded from config file or set by user) activeDefaultValue *valueCache // runtime default value (may be set internally) activeFallbackValue *valueCache // default value from option registration compiledRegex *regexp.Regexp } // AddAnnotation adds the annotation key to option if it's not already set. func (option *Option) AddAnnotation(key string, value interface{}) { option.Lock() defer option.Unlock() if option.Annotations == nil { option.Annotations = make(Annotations) } if _, ok := option.Annotations[key]; ok { return } option.Annotations[key] = value } // SetAnnotation sets the value of the annotation key overwritting an // existing value if required. func (option *Option) SetAnnotation(key string, value interface{}) { option.Lock() defer option.Unlock() option.setAnnotation(key, value) } // setAnnotation sets the value of the annotation key overwritting an // existing value if required. Does not lock the Option. func (option *Option) setAnnotation(key string, value interface{}) { if option.Annotations == nil { option.Annotations = make(Annotations) } option.Annotations[key] = value } // GetAnnotation returns the value of the annotation key. func (option *Option) GetAnnotation(key string) (interface{}, bool) { option.Lock() defer option.Unlock() if option.Annotations == nil { return nil, false } val, ok := option.Annotations[key] return val, ok } // AnnotationEquals returns whether the annotation of the given key matches the // given value. func (option *Option) AnnotationEquals(key string, value any) bool { option.Lock() defer option.Unlock() if option.Annotations == nil { return false } setValue, ok := option.Annotations[key] if !ok { return false } return reflect.DeepEqual(value, setValue) } // copyOrNil returns a copy of the option, or nil if copying failed. func (option *Option) copyOrNil() *Option { copied, err := copystructure.Copy(option) if err != nil { return nil } return copied.(*Option) //nolint:forcetypeassert } // IsSetByUser returns whether the option has been set by the user. func (option *Option) IsSetByUser() bool { option.Lock() defer option.Unlock() return option.activeValue != nil } // UserValue returns the value set by the user or nil if the value has not // been changed from the default. func (option *Option) UserValue() any { option.Lock() defer option.Unlock() if option.activeValue == nil { return nil } return option.activeValue.getData(option) } // ValidateValue checks if the given value is valid for the option. func (option *Option) ValidateValue(value any) error { option.Lock() defer option.Unlock() value = migrateValue(option, value) if _, err := validateValue(option, value); err != nil { return err } return nil } // Export expors an option to a Record. func (option *Option) Export() (record.Record, error) { option.Lock() defer option.Unlock() return option.export() } func (option *Option) export() (record.Record, error) { data, err := json.Marshal(option) if err != nil { return nil, err } if option.activeValue != nil { data, err = sjson.SetBytes(data, "Value", option.activeValue.getData(option)) if err != nil { return nil, err } } if option.activeDefaultValue != nil { data, err = sjson.SetBytes(data, "DefaultValue", option.activeDefaultValue.getData(option)) if err != nil { return nil, err } } r, err := record.NewWrapper(fmt.Sprintf("config:%s", option.Key), nil, dsd.JSON, data) if err != nil { return nil, err } r.SetMeta(&record.Meta{}) return r, nil } type sortByKey []*Option func (opts sortByKey) Len() int { return len(opts) } func (opts sortByKey) Less(i, j int) bool { return opts[i].Key < opts[j].Key } func (opts sortByKey) Swap(i, j int) { opts[i], opts[j] = opts[j], opts[i] }