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  }