From 9f88ea5e219d7924c61ef735d23cf34fdef4382d Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 12 Jul 2025 16:19:20 +0200 Subject: [PATCH] Add more robust value comparison --- upnp/statevalue.go | 180 ++++++++++++++--- upnp/statevaluetype.go | 427 +++++++++++++++++++++++++++++++---------- upnp/utils_binary.go | 33 ++++ upnp/utils_bool.go | 10 + upnp/utils_float.go | 11 ++ upnp/utils_int.go | 22 +++ upnp/utils_string.go | 37 ++++ upnp/utils_time.go | 55 ++++++ upnp/utils_uri.go | 25 +++ upnp/utils_uuid.go | 32 +++ upnp/valuerange.go | 2 +- 11 files changed, 702 insertions(+), 132 deletions(-) create mode 100644 upnp/utils_binary.go create mode 100644 upnp/utils_string.go create mode 100644 upnp/utils_time.go create mode 100644 upnp/utils_uri.go create mode 100644 upnp/utils_uuid.go diff --git a/upnp/statevalue.go b/upnp/statevalue.go index e83a2b90..87798896 100644 --- a/upnp/statevalue.go +++ b/upnp/statevalue.go @@ -36,9 +36,127 @@ type StateValue struct { marshal ValueSerializer } +// IsNumeric checks whether a given StateValue model represents a numeric value or +// not. Numeric types are defined as those that can be used to store number-like +// values. The following types are considered numeric: UI1, UI2, UI4, I1, I2, +// I4, Int, R4, R8, Number and Fixed14_4. +// +// t: StateVarType to check if it's a numeric type or not. +// +// Returns true if the given StateValue represents a numeric value; false +// otherwise. +func (sv StateValue) IsNumeric() bool { + return sv.valueType.IsNumeric() +} + +// BitSize returns the number of bits that will be used to represent values +// when this type is used in an UPnP State Variable. The returned value can be +// either 8, 16, 24, 32, or 64, depending on whether t is Byte, Boolean, I2, Ui2, +// I4, Ui4 respectively. If none of these types match, it will return -1. +func (sv StateValue) BitSize() int { + return sv.valueType.BitSize() +} + +// IsInteger checks if the state variable type is integer or not. +// +// It returns a boolean value indicating whether the provided StateValue +// is an integer type or not. +// +// Returns: bool: If the state variable type is any of the defined integer types +// (UI1, UI2, UI4, I1, I2, I4, Int), it returns true. Otherwise, it returns false. +func (sv StateValue) IsInteger() bool { + return sv.valueType.IsInteger() +} + +// IsUnsignedInt checks if the state value type represents an unsigned integer. +// The method returns a boolean indicating whether the state value type is an +// unsigned integer. +func (sv StateValue) IsUnsignedInt() bool { + return sv.valueType.IsUnsignedInt() +} + +// IsSignedInt checks if the state value type is a signed int. +// +// The return value will be a boolean indicating whether the state value type is +// a signed integer (true) or not (false). +func (sv StateValue) IsSignedInt() bool { + return sv.valueType.IsSignedInt() +} + +// IsFloat returns true if the StateValue's value type represents a floating point +// number; false otherwise. +func (sv StateValue) IsFloat() bool { + return sv.valueType.IsFloat() +} + +// IsBool checks if the state value is of boolean type. +// +// Parameters: +// +// None +// +// Returns: +// +// (bool) : Indicates whether the StateValue is of boolean type or not. +func (sv StateValue) IsBool() bool { + return sv.valueType.IsBool() +} + +// isString checks if the underlying value type of a StateValue object represents a string. +// +// Parameters: +// - None. +// +// Returns: +// +// bool: True if the underlying value type is a string, false otherwise. +func (sv StateValue) IsString() bool { + return sv.valueType.IsString() +} + +func (sv StateValue) IsTime() bool { + return sv.valueType.IsTime() +} + +func (sv StateValue) IsURI() bool { + return sv.valueType.IsURI() +} + +func (sv StateValue) IsBinary() bool { + return sv.valueType.IsBinary() +} + +// IsComparable function checks if a state variable is comparable or not. +// +// It returns false for binary types (bin.base64 and bin.hex) +// as they are non-comparable. For all other types, it returns true indicating +// that these types can be compared. +// // +// Returns: bool: A boolean value indicating whether the given StateValue is +// comparable or not. True means it's comparable, False means it isn't. +func (sv StateValue) IsComparable() bool { + return sv.valueType.IsComparable() +} + +func (sv StateValue) Add(a, b interface{}) (interface{}, error) { + return sv.valueType.Add(a, b) +} + +func (sv StateValue) Sub(a, b interface{}) (interface{}, error) { + return sv.valueType.Sub(a, b) +} + +func (sv StateValue) Mul(a, b interface{}) (interface{}, error) { + return sv.valueType.Mul(a, b) +} + +func (sv StateValue) Div(a, b interface{}) (interface{}, error) { + return sv.valueType.Div(a, b) +} + // Name returns the state variable's name (e.g., "Volume", "Brightness"). -func (state *StateValue) Name() string { - return state.name +func (sv StateValue) Name() string { + return sv.name } // Type returns the UPnP data type of the state variable. @@ -70,35 +188,37 @@ func (sv *StateValue) SetMinDelta(minDelta interface{}) error { minDelta, err := sv.Cast(minDelta) if err != nil { - return fmt.Errorf("%s: invalid minimum delta value %v : %v", sv.name, minDelta,err) + return fmt.Errorf("%s: invalid minimum delta value %v : %v", sv.name, minDelta, err) } - mdf := func(instance *StateValueInstance) bool { - o := instance.previousValue - n := instance.value + // mdf := func(instance *StateValueInstance) bool { + // o := instance.previousValue + // n := instance.value - - r,err := instance.model.valueType.ValueRange(o, n) + // r,err := instance.model.valueType.ValueRange(o, n) - if err != nil { - return false - } + // if err != nil { + // return false + // } - return instance.model.valueType.InRange() - } + // return instance.model.valueType.InRange() + // } - sv.eventConditions["MinDelta"] = mdf + // sv.eventConditions["MinDelta"] = mdf return nil } func (state *StateValue) SetDefault(value interface{}) error { - if state.IsValidValue(value) { + var err error + var valid bool + + if valid, err = state.IsValidValue(value); valid && err == nil { cvalue, _ := state.valueType.Cast(value) state.defaultValue = cvalue log.Debugf("🐞 Setting default value for %v to %v", state.name, cvalue) return nil } - return fmt.Errorf("invalid default value for %v (%v)", state.name, value) + return fmt.Errorf("invalid default value for %v (%v) : %v", state.name, value, err) } func (state *StateValue) HasDefault() bool { @@ -224,7 +344,7 @@ func (state *StateValue) UpdateMaximalValue(value interface{}) error { // Returns: // // bool: True if within range or no range defined -func (state *StateValue) IsValueInRange(value interface{}) bool { +func (state *StateValue) IsValueInRange(value interface{}) (bool, error) { return state.valueType.InRange(value, state.valueRange) } @@ -294,22 +414,22 @@ func (state *StateValue) AppendAllowedValue(value ...interface{}) error { // Returns: // // bool: True if value is permitted or no list defined -func (state *StateValue) IsValueAllowed(value interface{}) bool { +func (state *StateValue) IsValueAllowed(value interface{}) (bool, error) { if !state.HasAllowedValues() { - return true // No list = any value valid + return true, nil // No list = any value valid } cvalue, err := state.valueType.Cast(value) if err != nil { - return false + return false, err } for _, allowed := range state.allowedValues { if reflect.DeepEqual(cvalue, allowed) { - return true + return true, nil } } - return false + return false, nil } // IsValidValue performs full validation against all constraints. @@ -321,12 +441,22 @@ func (state *StateValue) IsValueAllowed(value interface{}) bool { // Returns: // // bool: True if value passes all applicable constraints -func (state *StateValue) IsValidValue(value interface{}) bool { +func (state *StateValue) IsValidValue(value interface{}) (bool, error) { cvalue, err := state.valueType.Cast(value) if err != nil { - return false + return false, err } - return state.IsValueInRange(cvalue) && state.IsValueAllowed(cvalue) + + inrange, err1 := state.IsValueInRange(cvalue) + allowed, err2 := state.IsValueAllowed(cvalue) + if err1 != nil || err2 != nil { + if err1 != nil { + err = err1 + } else { + err = err2 + } + } + return inrange && allowed, err } func (state *StateValue) HasDescription() bool { diff --git a/upnp/statevaluetype.go b/upnp/statevaluetype.go index 78758b8b..e9c41606 100644 --- a/upnp/statevaluetype.go +++ b/upnp/statevaluetype.go @@ -10,7 +10,6 @@ import ( "fmt" "log" "net/url" - "reflect" "strings" "time" @@ -103,6 +102,18 @@ var typeStrings = [...]string{ "uri", } +// StateVarTypeFactory takes a string and attempts to build from it a valid +// StateVarType. The input string is cleaned before processing - +// leading/trailing spaces are trimmed, the case is lowered for comparison with +// known types, and if no match is found 'StateType_Unknown' is returned. +func StateVarTypeFactory(s string) StateVarType { + s = strings.ToLower(strings.TrimSpace(s)) + if val, ok := typeNames[s]; ok { + return val + } + return StateType_Unknown +} + // String returns a string representation of the StateVarType. It defaults to // "unknown" if the type is not recognized. func (t StateVarType) String() string { @@ -112,6 +123,29 @@ func (t StateVarType) String() string { return "unknown" } +// BitSize returns the bit size of the StateVarType value, or -1 if not numeric. +// The possible return values are 8 for StateTypes I1, UI1, and 64 for others. +func (t StateVarType) BitSize() int { + // If t isn't numeric, return -1 + if !t.IsNumeric() { + return -1 + } + + // Check the value of t and return the appropriate bit size + switch t { + case StateType_I1, StateType_UI1: + return 8 + case StateType_I2, StateType_UI2: + return 16 + case StateType_I4, StateType_UI4, StateType_Int, StateType_R4: + return 32 + case StateType_R8, StateType_Number, StateType_Fixed14_4: + return 64 + default: + return 64 + } +} + // IsNumeric checks whether a given StateVarType represents a numeric type or // not. Numeric types are defined as those that can be used to store number-like // values. The following types are considered numeric: UI1, UI2, UI4, I1, I2, @@ -157,6 +191,111 @@ func (t StateVarType) IsInteger() bool { } } +// IsSignedInt checks if the state variable type is a signed integer type. +// The function returns true for StateType_I1, StateType_I2, StateType_I4, and +// StateType_Int, otherwise it will return false. This method is part of the +// StateVarType enumeration in statevaluetype package. It takes no parameters +// but operates on the receiver 't' of type StateVarType. +// +// The returned value is a boolean. +func (t StateVarType) IsSignedInt() bool { + switch t { + case StateType_I1, StateType_I2, StateType_I4, StateType_Int: + return true + default: + return false + } +} + +// IsUnsignedInt checks if the state variable type is an unsigned integer. It +// returns a boolean indicating whether or not the current StateVarType +// represents an unsigned integer type, namely: StateType_UI1, StateType_UI2, +// and StateType_UI4. +func (t StateVarType) IsUnsignedInt() bool { + switch t { + case StateType_UI1, StateType_UI2, StateType_UI4: + return true + default: + return false + } +} + +// IsFloat returns a boolean indicating whether the given state variable type +// represents a float number. If the state variable type is one of R4, R8, Number or +// Fixed14_4 it returns true; otherwise, it returns false. +func (t StateVarType) IsFloat() bool { + switch t { + case StateType_R4, StateType_R8, StateType_Number, StateType_Fixed14_4: + return true + default: + return false + } +} + +// IsString reports whether or not the state variable type represents a string +// value. +// +// This method returns true if the StateVarType is either StateType_String or +// StateType_Char, otherwise it returns false. +func (t StateVarType) IsString() bool { + switch t { + case StateType_String, StateType_Char: + return true + default: + return false + } +} + +// IsBool checks if a StateVarType is of type Boolean. It returns true if the +// StateVarType equals to StateType_Boolean, false otherwise. +func (t StateVarType) IsBool() bool { + return t == StateType_Boolean +} + +// IsTime checks whether a given StateVarType is of time type or not. It accepts +// a StateVarType parameter 't' and returns a boolean value based on the check. +// +// The possible values for 't' are: StateType_Date, StateType_DateTime, +// StateType_DateTimeTZ, StateType_Time, StateType_TimeTZ. If 't' is any of +// these types, the function returns true; otherwise, it returns false. +func (t StateVarType) IsTime() bool { + switch t { + case StateType_Date, StateType_DateTime, StateType_DateTimeTZ, + StateType_Time, StateType_TimeTZ: + return true + default: + return false + } +} + +// IsUUID reports whether the receiver represents a UUID (Universally Unique +// Identifier). The StateVarType should be of type StateType_UUID to return +// true. Otherwise, it returns false. +func (t StateVarType) IsUUID() bool { + return t == StateType_UUID +} + +// IsURI checks if the given state variable type is a URI. +// +// This function returns true if and only if the receiver (StateVarType t) +// equals StateType_URI, which represents URIs in UPnP protocol. Otherwise, it +// returns false. +func (t StateVarType) IsURI() bool { + return t == StateType_URI +} + +// IsBinary checks if the given StateVarType is binary type or not. It returns +// true for types StateType_BinBase64 and StateType_BinHex, otherwise it returns +// false. +func (t StateVarType) IsBinary() bool { + switch t { + case StateType_BinBase64, StateType_BinHex: + return true + default: + return false + } +} + // IsComparable function checks if a StateVarType is comparable or not. // // It returns false for binary types (StateType_BinBase64 and StateType_BinHex) @@ -201,6 +340,10 @@ func (t StateVarType) Add(a, b interface{}) (interface{}, error) { return t.Cast(af + bf) } +// Sub subtracts 'b' from 'a'. It converts both values to numeric types +// and then performs a subtraction operation, casting the result back to its +// original type. If either conversion fails or an unsupported type is used, +// it returns an error. func (t StateVarType) Sub(a, b interface{}) (interface{}, error) { af, bf, err := valuesToNumericOperands(t, a, b) if err != nil { @@ -210,6 +353,14 @@ func (t StateVarType) Sub(a, b interface{}) (interface{}, error) { return t.Cast(af - bf) } +// Mul takes in two interface types 'a' and 'b', multiplies them together +// and returns the result along with any error encountered during this +// process. If either of the inputs is not compatible with numeric values, an +// error will be returned. The multiplication operation is performed between two +// numbers represented as 'float64' types (since Go does not support generic +// types on its own). The resulting value will be cast to the type represented +// by the receiver of this method 't'. If a casting error occurs, it will also +// be returned along with nil for the result. func (t StateVarType) Mul(a, b interface{}) (interface{}, error) { af, bf, err := valuesToNumericOperands(t, a, b) if err != nil { @@ -219,6 +370,15 @@ func (t StateVarType) Mul(a, b interface{}) (interface{}, error) { return t.Cast(af * bf) } +// Div performs division between the provided arguments 'a' and 'b'. The function casts both operands to their numeric equivalents using valuesToNumericOperands() before performing the division. +// +// Parameters: +// - a: first operand of type interface{}, can be of any type, will be converted if necessary +// - b: second operand of type interface{}, can be of any type, will be converted if necessary +// +// Returns: +// - result: the division result in numeric form after casting it with function t.Cast() +// - err: error that might occur during the conversion or division operation func (t StateVarType) Div(a, b interface{}) (interface{}, error) { af, bf, err := valuesToNumericOperands(t, a, b) if err != nil { @@ -228,16 +388,6 @@ func (t StateVarType) Div(a, b interface{}) (interface{}, error) { return t.Cast(af / bf) } -// ParseStateVarType converts a UPnP type name to its StateVarType constant. -// Case-insensitive and trims whitespace. Returns StateType_Unknown for unrecognized types. -func ParseStateVarType(s string) StateVarType { - s = strings.ToLower(strings.TrimSpace(s)) - if val, ok := typeNames[s]; ok { - return val - } - return StateType_Unknown -} - // Cast converts a value to the Go type corresponding to the UPnP type. // Supports conversion from various primitive types and strings. // Returns an error for unsupported conversions or invalid values. @@ -381,108 +531,157 @@ func (t StateVarType) Cast(val interface{}) (interface{}, error) { } } -// Cmp compares two values of the UPnP type. Returns: -// - -1 if v1 < v2 -// - 0 if v1 == v2 -// - 1 if v1 > v2 -// -// Panics if values can't be cast to the type. Handles all supported types -// including binaries, times, and UUIDs. -func (t StateVarType) Cmp(v1, v2 interface{}) int { - // Helper: compare float64 - compareFloat := func(f1, f2 float64) int { - switch { - case f1 < f2: - return -1 - case f1 > f2: - return 1 - default: - return 0 - } - } - - castV1, err1 := t.Cast(v1) - castV2, err2 := t.Cast(v2) +func (t StateVarType) Cmp(a, b interface{}) (int, error) { + a, err1 := t.Cast(a) + b, err2 := t.Cast(b) if err1 != nil || err2 != nil { - log.Fatalf("Failed to cast for comparison: %v vs %v (errors: %v, %v)", v1, v2, err1, err2) + log.Fatalf("Failed to cast for comparison: %v vs %v (errors: %v, %v)", a, b, err1, err2) } - switch t { - case StateType_UI1, StateType_UI2, StateType_UI4, - StateType_Int, StateType_I1, StateType_I2, StateType_I4: - i1 := reflect.ValueOf(castV1).Int() - i2 := reflect.ValueOf(castV2).Int() - - switch { - case i1 < i2: - return -1 - case i1 > i2: - return 1 - default: - return 0 + switch { + case t.IsInteger(): + ai, err := toInt(a, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid int value for a: %w", err) } - - case StateType_R4: - f1 := float64(castV1.(float32)) - f2 := float64(castV2.(float32)) - return compareFloat(f1, f2) - - case StateType_R8, StateType_Number, StateType_Fixed14_4: - f1 := castV1.(float64) - f2 := castV2.(float64) - return compareFloat(f1, f2) - - case StateType_Boolean: - b1 := castV1.(bool) - b2 := castV2.(bool) - switch { - case b1 == b2: - return 0 - case !b1 && b2: - return -1 - default: - return 1 + bi, err := toInt(b, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid int value for b: %w", err) } + return cmpInt(ai, bi), nil - case StateType_Char: - r1 := castV1.(rune) - r2 := castV2.(rune) - - switch { - case r1 < r2: - return -1 - case r1 > r2: - return 1 - default: - return 0 + case t.IsUnsignedInt(): + ai, err := toUint(a, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid uint value for a: %w", err) } - - case StateType_String, StateType_UUID, StateType_URI: - s1 := castV1.(string) - s2 := castV2.(string) - return strings.Compare(s1, s2) - - case StateType_BinBase64, StateType_BinHex: - b1 := castV1.([]byte) - b2 := castV2.([]byte) - return bytes.Compare(b1, b2) - - case StateType_Date, StateType_DateTime, StateType_DateTimeTZ, - StateType_Time, StateType_TimeTZ: - t1 := castV1.(time.Time) - t2 := castV2.(time.Time) - if t1.Before(t2) { - return -1 - } else if t1.After(t2) { - return 1 + bi, err := toUint(b, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid uint value for b: %w", err) } - return 0 + return cmpUint(ai, bi), nil + + case t.IsFloat(): + af, err := toFloat(a, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid float value for a: %w", err) + } + bf, err := toFloat(b, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid float value for b: %w", err) + } + return cmpFloat64(af, bf), nil + + case t == StateType_Boolean: + ab, err := toBool(a) + if err != nil { + return 0, fmt.Errorf("invalid bool value for a: %w", err) + } + bb, err := toBool(b) + if err != nil { + return 0, fmt.Errorf("invalid bool value for b: %w", err) + } + return cmpBool(ab, bb), nil + + case t == StateType_String || t == StateType_Char: + as, err := toString(a) + if err != nil { + return 0, fmt.Errorf("invalid string value for a") + } + bs, err := toString(b) + if err != nil { + return 0, fmt.Errorf("invalid string value for b") + } + return strings.Compare(as, bs), nil + + case t.IsTime(): + at, err := toTime(a) + if err != nil { + return 0, fmt.Errorf("invalid time value for a") + } + bt, err := toTime(b) + if err != nil { + return 0, fmt.Errorf("invalid time value for b") + } + return cmpTime(at, bt), nil default: - s1 := fmt.Sprint(castV1) - s2 := fmt.Sprint(castV2) - return strings.Compare(s1, s2) + return 0, fmt.Errorf("comparison not supported for type %v", t) + } +} + +func (t StateVarType) Equal(a, b interface{}) (bool, error) { + switch { + case t.IsInteger(): + ai, err1 := toInt(a, 64) + bi, err2 := toInt(b, 64) + if err1 != nil || err2 != nil { + return false, fmt.Errorf("invalid integer value for type %s", t.String()) + } + return ai == bi, nil + + case t.IsFloat(): + af, err := toFloat(a, 64) + if err != nil { + return false, fmt.Errorf("invalid float value for type %s: %v", t.String(), err) + } + bf, err := toFloat(b, 64) + if err != nil { + return false, fmt.Errorf("invalid float value for type %s: %v", t.String(), err) + } + return af == bf, nil + + case t.IsString(): + as, ok1 := a.(string) + bs, ok2 := b.(string) + if !ok1 || !ok2 { + return false, fmt.Errorf("invalid string value for type %s", t.String()) + } + return as == bs, nil + + case t.IsBool(): + ab, err1 := toBool(a) + bb, err2 := toBool(b) + if err1 != nil || err2 != nil { + return false, fmt.Errorf("invalid boolean value for type %s", t.String()) + } + return ab == bb, nil + + case t.IsTime(): + at, err1 := toTime(a) + bt, err2 := toTime(b) + if err1 != nil || err2 != nil { + return false, fmt.Errorf("invalid time.Time value for type %s", t.String()) + } + return at.Equal(bt), nil + + case t.IsUUID(): + au, err1 := toUUID(a) + bu, err2 := toUUID(b) + if err1 != nil || err2 != nil { + return false, fmt.Errorf("invalid uuid.UUID value for type %s", t.String()) + } + return au == bu, nil + + case t.IsURI(): + au, err1 := toURI(a) + bu, err2 := toURI(b) + if err1 != nil || err2 != nil { + return false, fmt.Errorf("invalid *url.URL value for type %s", t.String()) + } + return au.String() == bu.String(), nil + + case t.IsBinary(): + ab, err1 := toBinary(a) + bb, err2 := toBinary(b) + if err1 != nil || err2 != nil { + return false, fmt.Errorf("invalid []byte value for type %s", t.String()) + } + return bytes.Equal(ab, bb), nil + + default: + return false, fmt.Errorf("equality not supported for type %s", t.String()) } } @@ -493,12 +692,28 @@ func (t StateVarType) Cmp(v1, v2 interface{}) int { // // range := ValueRange{min: uint16(10), max: uint16(100)} // StateType_UI2.InRange(uint16(50), range) // true -func (t StateVarType) InRange(val interface{}, interval *ValueRange) bool { - return interval == nil || t.Cmp(val, interval.min) >= 0 && t.Cmp(val, interval.max) <= 0 +func (t StateVarType) InRange(val interface{}, interval *ValueRange) (bool, error) { + if interval == nil { + return true, nil + } + cmp1, err1 := t.Cmp(val, interval.min) + cmp2, err2 := t.Cmp(val, interval.max) + if err1 != nil || err2 != nil { + err := err1 + if err == nil { + err = err2 + } + return false, err + } + return cmp1 >= 0 && cmp2 <= 0, nil } -// NewAtomicValue crée une valeur simple -func (t StateVarType) NewAtomicValue(name string) *StateValue { +// NewStateValue creates and returns a new StateValue struct instance with the given name +// and the receiver's state variable type. The created StateValue is initialized with an +// empty map for event conditions. If name is an empty string, it will cause panic in later +// usage. Name is typically used to identify or represent a specific value or condition +// associated with the variable type 't'. +func (t StateVarType) NewStateValue(name string) *StateValue { return &StateValue{ name: name, valueType: t, diff --git a/upnp/utils_binary.go b/upnp/utils_binary.go new file mode 100644 index 00000000..69ec9989 --- /dev/null +++ b/upnp/utils_binary.go @@ -0,0 +1,33 @@ +package upnp + +import ( + "encoding/base64" + "encoding/hex" + "fmt" +) + +// toBinary tries to convert v into a []byte. +// - if v is []byte, returns it directly +// - if v is string, attempts base64 decode, then hex decode +func toBinary(v interface{}) ([]byte, error) { + switch val := v.(type) { + case []byte: + return val, nil + + case string: + // Try base64 first + if data, err := base64.StdEncoding.DecodeString(val); err == nil { + return data, nil + } + + // Try hex next + if data, err := hex.DecodeString(val); err == nil { + return data, nil + } + + return nil, fmt.Errorf("cannot parse string as base64 or hex: %q", val) + + default: + return nil, fmt.Errorf("cannot convert type %T to binary", v) + } +} diff --git a/upnp/utils_bool.go b/upnp/utils_bool.go index 1f2b8a9a..130fe566 100644 --- a/upnp/utils_bool.go +++ b/upnp/utils_bool.go @@ -47,3 +47,13 @@ func toBool(val interface{}) (bool, error) { return false, errors.New("numeric value cannot be converted to bool unless 0 or 1") } } + +func cmpBool(a, b bool) int { + if a == b { + return 0 + } + if !a && b { + return -1 + } + return 1 +} diff --git a/upnp/utils_float.go b/upnp/utils_float.go index c3b0d0a8..a749ec60 100644 --- a/upnp/utils_float.go +++ b/upnp/utils_float.go @@ -75,3 +75,14 @@ func toFloat(v interface{}, bits int) (float64, error) { return f, nil } + +func cmpFloat64(a, b float64) int { + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } +} diff --git a/upnp/utils_int.go b/upnp/utils_int.go index 5e81b052..846d06d3 100644 --- a/upnp/utils_int.go +++ b/upnp/utils_int.go @@ -114,3 +114,25 @@ func checkIntBounds(v int64, bits int) (int64, error) { } return v, nil } + +func cmpInt(a, b int64) int { + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } +} + +func cmpUint(a, b uint64) int { + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } +} diff --git a/upnp/utils_string.go b/upnp/utils_string.go new file mode 100644 index 00000000..97ed172f --- /dev/null +++ b/upnp/utils_string.go @@ -0,0 +1,37 @@ +package upnp + +import ( + "fmt" +) + +// toString converts v to string if possible. +func toString(v interface{}) (string, error) { + switch val := v.(type) { + case string: + return val, nil + + case []byte: + return string(val), nil + + case fmt.Stringer: + return val.String(), nil + + case int, int8, int16, int32, int64: + return fmt.Sprintf("%d", val), nil + + case uint, uint8, uint16, uint32, uint64: + return fmt.Sprintf("%d", val), nil + + case float32, float64: + return fmt.Sprintf("%v", val), nil + + case bool: + if val { + return "true", nil + } + return "false", nil + + default: + return "", fmt.Errorf("cannot convert type %T to string", v) + } +} diff --git a/upnp/utils_time.go b/upnp/utils_time.go new file mode 100644 index 00000000..0b206af0 --- /dev/null +++ b/upnp/utils_time.go @@ -0,0 +1,55 @@ +package upnp + +import ( + "fmt" + "time" +) + +func toTime(v interface{}) (time.Time, error) { + switch val := v.(type) { + case time.Time: + return val, nil + + case string: + // Liste de layouts communs à UPnP / ISO-8601 / RFC + layouts := []string{ + time.RFC3339Nano, // 2006-01-02T15:04:05.999999999Z07:00 + time.RFC3339, // 2006-01-02T15:04:05Z07:00 + "2006-01-02", // date only + "15:04:05", // time only + "15:04:05Z07:00", // time with TZ + "2006-01-02T15:04:05", // no TZ + "2006-01-02T15:04:05Z", // UTC + "2006-01-02 15:04:05", // space-separated + } + + for _, layout := range layouts { + if t, err := time.Parse(layout, val); err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("cannot parse time string: %q", val) + + case int64: + return time.Unix(val, 0), nil + + case float64: + sec := int64(val) + nsec := int64((val - float64(sec)) * 1e9) + return time.Unix(sec, nsec), nil + + default: + return time.Time{}, fmt.Errorf("unsupported type for time conversion: %T", v) + } +} + +func cmpTime(a, b time.Time) int { + switch { + case a.Before(b): + return -1 + case a.After(b): + return 1 + default: + return 0 + } +} diff --git a/upnp/utils_uri.go b/upnp/utils_uri.go new file mode 100644 index 00000000..955a48f0 --- /dev/null +++ b/upnp/utils_uri.go @@ -0,0 +1,25 @@ +package upnp + +import ( + "fmt" + "net/url" +) + +// toURI converts a value to *url.URL if possible. +func toURI(v interface{}) (*url.URL, error) { + switch val := v.(type) { + case *url.URL: + // Already a URL + return val, nil + + case string: + u, err := url.Parse(val) + if err != nil { + return nil, fmt.Errorf("invalid URI string %q: %v", val, err) + } + return u, nil + + default: + return nil, fmt.Errorf("cannot convert type %T to URI", v) + } +} diff --git a/upnp/utils_uuid.go b/upnp/utils_uuid.go new file mode 100644 index 00000000..1b1e0395 --- /dev/null +++ b/upnp/utils_uuid.go @@ -0,0 +1,32 @@ +package upnp + +import ( + "fmt" + + "github.com/google/uuid" +) + +// toUUID converts a value to uuid.UUID if possible. +func toUUID(v interface{}) (uuid.UUID, error) { + switch val := v.(type) { + case uuid.UUID: + return val, nil + + case string: + u, err := uuid.Parse(val) + if err != nil { + return uuid.UUID{}, fmt.Errorf("invalid UUID string %q: %v", val, err) + } + return u, nil + + case []byte: + u, err := uuid.FromBytes(val) + if err != nil { + return uuid.UUID{}, fmt.Errorf("invalid UUID bytes: %v", err) + } + return u, nil + + default: + return uuid.UUID{}, fmt.Errorf("cannot convert type %T to UUID", v) + } +} diff --git a/upnp/valuerange.go b/upnp/valuerange.go index 6f696f3f..55a15f8b 100644 --- a/upnp/valuerange.go +++ b/upnp/valuerange.go @@ -53,7 +53,7 @@ func (t StateVarType) ValueRange(min, max interface{}) (*ValueRange, error) { return nil, fmt.Errorf("max value %v is not castable to type %s", min, t.String()) } - if t.Cmp(cmin, cmax) > 0 { + if cmp, err := t.Cmp(cmin, cmax); err != nil && cmp > 0 { cmax, cmin = cmin, cmax }