github.com/safing/portbase@v0.19.5/config/validate.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "reflect" 8 9 "github.com/safing/portbase/log" 10 ) 11 12 type valueCache struct { 13 stringVal string 14 stringArrayVal []string 15 intVal int64 16 boolVal bool 17 } 18 19 func (vc *valueCache) getData(opt *Option) interface{} { 20 switch opt.OptType { 21 case OptTypeBool: 22 return vc.boolVal 23 case OptTypeInt: 24 return vc.intVal 25 case OptTypeString: 26 return vc.stringVal 27 case OptTypeStringArray: 28 return vc.stringArrayVal 29 case optTypeAny: 30 return nil 31 default: 32 return nil 33 } 34 } 35 36 // isAllowedPossibleValue checks if value is defined as a PossibleValue 37 // in opt. If there are not possible values defined value is considered 38 // allowed and nil is returned. isAllowedPossibleValue ensure the actual 39 // value is an allowed primitiv value by using reflection to convert 40 // value and each PossibleValue to a comparable primitiv if possible. 41 // In case of complex value types isAllowedPossibleValue uses 42 // reflect.DeepEqual as a fallback. 43 func isAllowedPossibleValue(opt *Option, value interface{}) error { 44 if opt.PossibleValues == nil { 45 return nil 46 } 47 48 for _, val := range opt.PossibleValues { 49 compareAgainst := val.Value 50 valueType := reflect.TypeOf(value) 51 52 // loading int's from the configuration JSON does not preserve the correct type 53 // as we get float64 instead. Make sure to convert them before. 54 if reflect.TypeOf(val.Value).ConvertibleTo(valueType) { 55 compareAgainst = reflect.ValueOf(val.Value).Convert(valueType).Interface() 56 } 57 if compareAgainst == value { 58 return nil 59 } 60 61 if reflect.DeepEqual(val.Value, value) { 62 return nil 63 } 64 } 65 66 return errors.New("value is not allowed") 67 } 68 69 // migrateValue runs all value migrations. 70 func migrateValue(option *Option, value any) any { 71 for _, migration := range option.Migrations { 72 newValue := migration(option, value) 73 if newValue != value { 74 log.Debugf("config: migrated %s value from %v to %v", option.Key, value, newValue) 75 } 76 value = newValue 77 } 78 return value 79 } 80 81 // validateValue ensures that value matches the expected type of option. 82 // It does not create a copy of the value! 83 func validateValue(option *Option, value interface{}) (*valueCache, *ValidationError) { //nolint:gocyclo 84 if option.OptType != OptTypeStringArray { 85 if err := isAllowedPossibleValue(option, value); err != nil { 86 return nil, &ValidationError{ 87 Option: option.copyOrNil(), 88 Err: err, 89 } 90 } 91 } 92 93 var validated *valueCache 94 switch v := value.(type) { 95 case string: 96 if option.OptType != OptTypeString { 97 return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v) 98 } 99 if option.compiledRegex != nil { 100 if !option.compiledRegex.MatchString(v) { 101 return nil, invalid(option, "did not match validation regex") 102 } 103 } 104 validated = &valueCache{stringVal: v} 105 case []interface{}: 106 vConverted := make([]string, len(v)) 107 for pos, entry := range v { 108 s, ok := entry.(string) 109 if !ok { 110 return nil, invalid(option, "entry #%d is not a string", pos+1) 111 } 112 vConverted[pos] = s 113 } 114 // Call validation function again with converted value. 115 var vErr *ValidationError 116 validated, vErr = validateValue(option, vConverted) 117 if vErr != nil { 118 return nil, vErr 119 } 120 case []string: 121 if option.OptType != OptTypeStringArray { 122 return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v) 123 } 124 if option.compiledRegex != nil { 125 for pos, entry := range v { 126 if !option.compiledRegex.MatchString(entry) { 127 return nil, invalid(option, "entry #%d did not match validation regex", pos+1) 128 } 129 130 if err := isAllowedPossibleValue(option, entry); err != nil { 131 return nil, invalid(option, "entry #%d is not allowed", pos+1) 132 } 133 } 134 } 135 validated = &valueCache{stringArrayVal: v} 136 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, float32, float64: 137 // uint64 is omitted, as it does not fit in a int64 138 if option.OptType != OptTypeInt { 139 return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v) 140 } 141 if option.compiledRegex != nil { 142 // we need to use %v here so we handle float and int correctly. 143 if !option.compiledRegex.MatchString(fmt.Sprintf("%v", v)) { 144 return nil, invalid(option, "did not match validation regex") 145 } 146 } 147 switch v := value.(type) { 148 case int: 149 validated = &valueCache{intVal: int64(v)} 150 case int8: 151 validated = &valueCache{intVal: int64(v)} 152 case int16: 153 validated = &valueCache{intVal: int64(v)} 154 case int32: 155 validated = &valueCache{intVal: int64(v)} 156 case int64: 157 validated = &valueCache{intVal: v} 158 case uint: 159 validated = &valueCache{intVal: int64(v)} 160 case uint8: 161 validated = &valueCache{intVal: int64(v)} 162 case uint16: 163 validated = &valueCache{intVal: int64(v)} 164 case uint32: 165 validated = &valueCache{intVal: int64(v)} 166 case float32: 167 // convert if float has no decimals 168 if math.Remainder(float64(v), 1) == 0 { 169 validated = &valueCache{intVal: int64(v)} 170 } else { 171 return nil, invalid(option, "failed to convert float32 to int64") 172 } 173 case float64: 174 // convert if float has no decimals 175 if math.Remainder(v, 1) == 0 { 176 validated = &valueCache{intVal: int64(v)} 177 } else { 178 return nil, invalid(option, "failed to convert float64 to int64") 179 } 180 default: 181 return nil, invalid(option, "internal error") 182 } 183 case bool: 184 if option.OptType != OptTypeBool { 185 return nil, invalid(option, "expected type %s, got type %T", getTypeName(option.OptType), v) 186 } 187 validated = &valueCache{boolVal: v} 188 default: 189 return nil, invalid(option, "invalid option value type: %T", value) 190 } 191 192 // Check if there is an additional function to validate the value. 193 if option.ValidationFunc != nil { 194 var err error 195 switch option.OptType { 196 case optTypeAny: 197 err = errors.New("internal error") 198 case OptTypeString: 199 err = option.ValidationFunc(validated.stringVal) 200 case OptTypeStringArray: 201 err = option.ValidationFunc(validated.stringArrayVal) 202 case OptTypeInt: 203 err = option.ValidationFunc(validated.intVal) 204 case OptTypeBool: 205 err = option.ValidationFunc(validated.boolVal) 206 } 207 if err != nil { 208 return nil, &ValidationError{ 209 Option: option.copyOrNil(), 210 Err: err, 211 } 212 } 213 } 214 215 return validated, nil 216 } 217 218 // ValidationError error holds details about a config option value validation error. 219 type ValidationError struct { 220 Option *Option 221 Err error 222 } 223 224 // Error returns the formatted error. 225 func (ve *ValidationError) Error() string { 226 return fmt.Sprintf("validation of %s failed: %s", ve.Option.Key, ve.Err) 227 } 228 229 // Unwrap returns the wrapped error. 230 func (ve *ValidationError) Unwrap() error { 231 return ve.Err 232 } 233 234 func invalid(option *Option, format string, a ...interface{}) *ValidationError { 235 return &ValidationError{ 236 Option: option.copyOrNil(), 237 Err: fmt.Errorf(format, a...), 238 } 239 }