diff --git a/upnp/statevalue.go b/upnp/statevalue.go index 87798896..763a10e9 100644 --- a/upnp/statevalue.go +++ b/upnp/statevalue.go @@ -57,103 +57,6 @@ 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 (sv StateValue) Name() string { return sv.name @@ -638,7 +541,3 @@ func (sv *StateValue) valueToString(val interface{}) string { // Default conversion for unsupported types or fallback return fmt.Sprintf("%v", val) } - -func (sv *StateValue) Cast(val interface{}) (interface{}, error) { - return sv.valueType.Cast(val) -} diff --git a/upnp/statevalue_arithm.go b/upnp/statevalue_arithm.go new file mode 100644 index 00000000..04aca211 --- /dev/null +++ b/upnp/statevalue_arithm.go @@ -0,0 +1,39 @@ +package upnp + +// Add performs addition operation on the given parameters 'a' and 'b'. It calls the +// corresponding method from valueType which is assumed to be an interface that +// provides methods for arithmetic operations. This function returns the result of +// the operation or an error if any occurs during computation. Side effects might +// include modifying the state of the system, but this depends on the actual implementation +// of the valueType's Add method. Errors might occur due to invalid input parameters or +// failures in the addition operation itself. The function does not handle edge cases +// and therefore should be used with caution. Here is an example usage: +// +// result, err := sv.Add(5, 3) +// if err != nil { +// // Handle error +// } else { +// // Use result +// } +func (sv StateValue) Add(a, b interface{}) (interface{}, error) { + return sv.valueType.Add(a, b) +} + +// Sub performs subtraction operation on the given parameters 'a' and 'b'. It follows +// similar semantics as in the Add method, but for subtraction instead of addition. +func (sv StateValue) Sub(a, b interface{}) (interface{}, error) { + return sv.valueType.Sub(a, b) +} + +// Mul performs multiplication operation on the given parameters 'a' and 'b'. It follows +// similar semantics as in the Add method, but for multiplication instead of addition. +func (sv StateValue) Mul(a, b interface{}) (interface{}, error) { + return sv.valueType.Mul(a, b) +} + +// Div performs division operation on the given parameters 'a' and 'b'. It follows similar +// semantics as in the Add method, but for division instead of addition. Please note that +// division by zero is undefined and will result in an error being returned from valueType.Div call. +func (sv StateValue) Div(a, b interface{}) (interface{}, error) { + return sv.valueType.Div(a, b) +} diff --git a/upnp/statevalue_cast.go b/upnp/statevalue_cast.go new file mode 100644 index 00000000..e8be27b4 --- /dev/null +++ b/upnp/statevalue_cast.go @@ -0,0 +1,29 @@ +package upnp + +// Cast transforms any given value into an interface suitable for use in a StateValue object, using +// the underlying type's specific casting rules. It will return an error if the transformation fails or +// if the provided interface is not supported by the ValueType of the StateValue. +// This function does NOT mutate the original value it receives as input. +// +// The parameter 'val' is an interface that needs to be cast into a form compatible with the internal +// state representation used by the StateValue object. +// +// It returns an interface and an error: if the casting operation was successful, the first return value will +// be the casted version of 'val', and the second one (an error) will be nil. If the casting fails, it will +// return a nil for the first value and an appropriate error for the second one. +// +// This function should not modify its input value. It is idempotent and always returns consistent results given +// the same inputs. However, if the provided interface 'val' does not match with the ValueType of StateValue, it +// will return an error. +// +// Example usage: +// +// state := upnp.NewStateValue(upnp.NewTime()) +// castedVal, err := state.Cast("2022-12-31") +// if err != nil { +// log.Println(err) +// return +// } +func (sv *StateValue) Cast(val interface{}) (interface{}, error) { + return sv.valueType.Cast(val) +} diff --git a/upnp/statevalue_cmp.go b/upnp/statevalue_cmp.go new file mode 100644 index 00000000..cc2c9c77 --- /dev/null +++ b/upnp/statevalue_cmp.go @@ -0,0 +1,13 @@ +package upnp + +func (sv StateValue) Cmp(a, b interface{}) (int, error) { + return sv.valueType.Cmp(a, b) +} + +func (sv StateValue) Equal(a, b interface{}) (int, error) { + return sv.valueType.Cmp(a, b) +} + +func (sv StateValue) InRange(val interface{}, interval *ValueRange) (bool, error) { + return sv.valueType.InRange(val, interval) +} diff --git a/upnp/statevalue_typechecking.go b/upnp/statevalue_typechecking.go new file mode 100644 index 00000000..d5eaa436 --- /dev/null +++ b/upnp/statevalue_typechecking.go @@ -0,0 +1,134 @@ +package upnp + +// 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: Indicates whether the underlying value type is a string or not. +// +// Side Effects: +// - This function does not modify any state. It only reads and returns a boolean value. +// +// Errors: +// - This function does not return an error, so you don't have to check for errors. +// +// Edge Cases: +// - If the underlying type of the StateValue is not TypeString, +// this function will return false as expected. +// +// Usage example: +// +// state := upnp.StateValue{valueType: upnp.TypeInt} +// fmt.Println(state.IsString()) // Outputs: false +func (sv StateValue) IsString() bool { + return sv.valueType.IsString() +} + +// IsTime reports whether this state value represents a time instance. +// +// The function returns true if and only if the underlying type of the +// StateValue is TypeTime, else false. This method does not check for other +// types that are convertible to Time as it's assumed that these would be +// handled by the ConvertToType method beforehand. +// +// No side effects: this function is pure and doesn't change any state. +// +// Errors: This function does not return an error, so you don't have to check +// for errors. However, note that TypeTime conversion might fail if the +// StateValue isn't convertible to Time; use the ConvertToType method in such +// cases. +// +// Edge cases: If the underlying type of the StateValue is not TypeTime or is a +// non-convertible time, this function will return false as expected. +// +// Usage example: +// +// state := upnp.StateValue{valueType: upnp.TypeInt} +// fmt.Println(state.IsTime()) +// +// Outputs: false +func (sv StateValue) IsTime() bool { + return sv.valueType.IsTime() +} + +// IsURI checks if the state value type is a URI. +// The method returns a boolean indicating whether the state value type represents +// a Uniform Resource Identifier (URI) or not. +// +// Returns: bool: Indicates whether the StateValue is of URI type or not. +func (sv StateValue) IsURI() bool { + return sv.valueType.IsURI() +} + +// IsBinary checks if the state value type is binary or not. +// +// The function returns a boolean indicating whether the provided StateValue's +// value type is of binary format (bin.base64 or bin.hex). +// +// Returns: bool: If the underlying value type of the StateValue object is +// either TypeBinBase64 or TypeBinHex, this method will return true; otherwise, +// it returns false. +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() +} diff --git a/upnp/statevaluetype.go b/upnp/statevaluetype.go index e9c41606..8e5d7cff 100644 --- a/upnp/statevaluetype.go +++ b/upnp/statevaluetype.go @@ -4,16 +4,8 @@ package upnp import ( - "bytes" - "encoding/base64" - "encoding/hex" - "fmt" - "log" - "net/url" "strings" "time" - - "github.com/google/uuid" ) // StateVarType represents UPnP state variable types with corresponding Go type mappings. @@ -146,568 +138,6 @@ func (t StateVarType) BitSize() int { } } -// 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, -// 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 StateVarType represents a numeric type; false -// otherwise. -func (t StateVarType) IsNumeric() bool { - switch t { - case StateType_UI1, StateType_UI2, StateType_UI4, - StateType_I1, StateType_I2, StateType_I4, - StateType_Int, - StateType_R4, StateType_R8, - StateType_Number, - StateType_Fixed14_4: - return true - default: - return false - } -} - -// IsInteger checks if the state variable type is integer or not. -// -// It returns a boolean value indicating whether the provided StateVarType (t) -// is an integer type or not. The function takes one parameter, t of type -// StateVarType, which represents the state variable type to be checked. -// -// Parameters: - t (StateVarType): The StateVarType to check for comparability. -// -// Returns: bool: If the state variable type is any of the defined integer types -// (StateType_UI1, StateType_UI2, StateType_UI4, StateType_I1, StateType_I2, -// StateType_I4, StateType_Int), it returns true. Otherwise, it returns false. -func (t StateVarType) IsInteger() bool { - switch t { - case StateType_UI1, StateType_UI2, StateType_UI4, - StateType_I1, StateType_I2, StateType_I4, - StateType_Int: - return true - default: - return false - } -} - -// 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) -// as they are non-comparable. For all other types, it returns true indicating -// that these types can be compared. -// -// Parameters: - t (StateVarType): The StateVarType to check for comparability. -// -// Returns: bool: A boolean value indicating whether the given StateVarType is -// comparable or not. True means it's comparable, False means it isn't. -func (t StateVarType) IsComparable() bool { - // Tous les types sauf les binaires sont comparables - switch t { - case StateType_BinBase64, StateType_BinHex: - return false - default: - return true - } -} - -// Add performs addition operation on two interfaces if both are of numeric -// type, otherwise it returns an error. If the types are not numeric, it checks -// and converts them into float64 before performing the addition. The function -// then casts the result back to its original type using Cast method from -// StateVarType t and returns this value or any encountered error. -// -// Parameters: -// -// a (interface{}): First operand for addition operation. Can be of any type. -// b (interface{}): Second operand for addition operation. Can be of any type. -// -// Returns: -// -// interface{}: Result of the addition, casted back to its original type using StateVarType t if no error encountered. -// error: Encountered error in case any conversion or casting fails. This includes non-numeric types for this operation. -func (t StateVarType) Add(a, b interface{}) (interface{}, error) { - af, bf, err := valuesToNumericOperands(t, a, b) - if err != nil { - return nil, err - } - - 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 { - return nil, err - } - - 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 { - return nil, err - } - - 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 { - return nil, err - } - - return t.Cast(af / bf) -} - -// 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. -// -// Examples: -// - StateType_UI2.Cast(42) // uint16(42), nil -// - StateType_Boolean.Cast("true") // true, nil -// - StateType_UI1.Cast(300) // nil, error (overflow) -func (t StateVarType) Cast(val interface{}) (interface{}, error) { - switch t { - case StateType_UI1: - v, err := toUint(val, 8) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to UI1", val, val) - } - return uint8(v), nil - - case StateType_UI2: - v, err := toUint(val, 16) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to UI2", val, val) - } - return uint16(v), nil - - case StateType_UI4: - v, err := toUint(val, 32) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to UI4", val, val) - } - return uint32(v), nil - - case StateType_I1: - v, err := toInt(val, 8) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to I1", val, val) - } - return int8(v), nil - - case StateType_I2: - v, err := toInt(val, 16) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to I2", val, val) - } - return int16(v), nil - - case StateType_I4, StateType_Int: - v, err := toInt(val, 32) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to I4", val, val) - } - return int32(v), nil - - case StateType_R4: - v, err := toFloat(val, 32) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to R4", val, val) - } - return float32(v), nil - - case StateType_R8, StateType_Number, StateType_Fixed14_4: - v, err := toFloat(val, 64) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to R8", val, val) - } - return v, nil - - case StateType_Boolean: - b, err := toBool(val) - if err != nil { - return nil, fmt.Errorf("cannot cast %v (%T) to Boolean", val, val) - } - return b, nil - - case StateType_Char: - switch s := val.(type) { - case string: - if len(s) != 1 { - return nil, fmt.Errorf("invalid Char: string too long %q", s) - } - return rune(s[0]), nil - case rune: - return s, nil - default: - return nil, fmt.Errorf("cannot cast %v (%T) to Char", val, val) - } - - case StateType_String: - return fmt.Sprint(val), nil - - case StateType_UUID: - switch val := val.(type) { - case uuid.UUID: - return val, nil - case string: - u, err := uuid.Parse(strings.TrimSpace(val)) - if err != nil { - return nil, fmt.Errorf("invalid UUID %v: %v", val, err) - } - return u, nil - default: - return nil, fmt.Errorf("cannot cast %v (%T) to UUID", val, val) - } - - case StateType_URI: - switch val := val.(type) { - case *url.URL: - return val, nil - case string: - u, err := url.Parse(strings.TrimSpace(val)) - if err != nil { - return nil, fmt.Errorf("invalid URI %v: %v", val, err) - } - return u, nil - default: - return nil, fmt.Errorf("cannot cast %v (%T) to URI", val, val) - } - - case StateType_BinBase64, StateType_BinHex: - switch v := val.(type) { - case []byte: - return v, nil - case string: - return decodeBinary(t, v) - default: - return nil, fmt.Errorf("cannot cast %v (%T) to binary", val, val) - } - - case StateType_Date, StateType_DateTime, StateType_DateTimeTZ, - StateType_Time, StateType_TimeTZ: - switch v := val.(type) { - case time.Time: - return v, nil - case string: - return parseUPnPTime(t, v) - default: - return nil, fmt.Errorf("cannot cast %v (%T) to time", val, val) - } - - default: - return nil, fmt.Errorf("unsupported type: %v", t) - } -} - -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)", a, b, err1, err2) - } - - switch { - case t.IsInteger(): - ai, err := toInt(a, t.BitSize()) - if err != nil { - return 0, fmt.Errorf("invalid int value for a: %w", err) - } - 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 t.IsUnsignedInt(): - ai, err := toUint(a, t.BitSize()) - if err != nil { - return 0, fmt.Errorf("invalid uint value for a: %w", err) - } - bi, err := toUint(b, t.BitSize()) - if err != nil { - return 0, fmt.Errorf("invalid uint value for b: %w", err) - } - 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: - 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()) - } -} - -// InRange checks if a value falls within an inclusive range [min, max]. -// Uses the type's comparison logic. Returns true if val is between min and max (inclusive). -// -// Example: -// -// range := ValueRange{min: uint16(10), max: uint16(100)} -// StateType_UI2.InRange(uint16(50), range) // true -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 -} - // 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 @@ -762,74 +192,3 @@ func (t StateVarType) DefaultValue() interface{} { return nil } - -// decodeBinary decodes Base64 or Hex-encoded binary strings to byte slices -func decodeBinary(t StateVarType, val string) ([]byte, error) { - switch t { - case StateType_BinBase64: - data, err := base64.StdEncoding.DecodeString(val) - if err != nil { - return nil, fmt.Errorf("invalid base64: %v", err) - } - return data, nil - - case StateType_BinHex: - // Accept even-length hex string - val = strings.TrimSpace(val) - if len(val)%2 != 0 { - return nil, fmt.Errorf("invalid hex: odd-length string") - } - data := make([]byte, len(val)/2) - _, err := hex.Decode(data, []byte(val)) - if err != nil { - return nil, fmt.Errorf("invalid hex: %v", err) - } - return data, nil - - default: - return nil, fmt.Errorf("decodeBinary: unsupported binary type %v", t) - } -} - -// parseUPnPTime parses time values using UPnP-specific formats: -// - Date: "2006-01-02" -// - Time: "15:04:05" -// - DateTime: "2006-01-02T15:04:05" -// - TimeTZ: "15:04:05-07:00" -// - DateTimeTZ: "2006-01-02T15:04:05-07:00" -func parseUPnPTime(t StateVarType, s string) (time.Time, error) { - s = strings.TrimSpace(s) - - layouts := []string{} - - switch t { - case StateType_Date: - layouts = []string{"2006-01-02"} - - case StateType_Time: - layouts = []string{"15:04:05"} // HH:MM:SS - - case StateType_TimeTZ: - layouts = []string{"15:04:05Z07:00"} // HH:MM:SS+TZ - - case StateType_DateTime: - layouts = []string{"2006-01-02T15:04:05"} // ISO8601 sans TZ - - case StateType_DateTimeTZ: - layouts = []string{ - "2006-01-02T15:04:05Z07:00", // full - "2006-01-02T15:04:05-0700", // fallback no colon - "2006-01-02T15:04:05Z", // Zulu - } - - default: - return time.Time{}, fmt.Errorf("unsupported date/time type: %v", t) - } - - for _, layout := range layouts { - if ts, err := time.Parse(layout, s); err == nil { - return ts, nil - } - } - return time.Time{}, fmt.Errorf("invalid %v value: %q", t, s) -} diff --git a/upnp/statevaluetype_arithm.go b/upnp/statevaluetype_arithm.go new file mode 100644 index 00000000..0bf2cf58 --- /dev/null +++ b/upnp/statevaluetype_arithm.go @@ -0,0 +1,73 @@ +package upnp + +// Add performs addition operation on two interfaces if both are of numeric +// type, otherwise it returns an error. If the types are not numeric, it checks +// and converts them into float64 before performing the addition. The function +// then casts the result back to its original type using Cast method from +// StateVarType t and returns this value or any encountered error. +// +// Parameters: +// +// a (interface{}): First operand for addition operation. Can be of any type. +// b (interface{}): Second operand for addition operation. Can be of any type. +// +// Returns: +// +// interface{}: Result of the addition, casted back to its original type using StateVarType t if no error encountered. +// error: Encountered error in case any conversion or casting fails. This includes non-numeric types for this operation. +func (t StateVarType) Add(a, b interface{}) (interface{}, error) { + af, bf, err := valuesToNumericOperands(t, a, b) + if err != nil { + return nil, err + } + + 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 { + return nil, err + } + + 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 { + return nil, err + } + + 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 { + return nil, err + } + + return t.Cast(af / bf) +} diff --git a/upnp/statevaluetype_cast.go b/upnp/statevaluetype_cast.go new file mode 100644 index 00000000..01480364 --- /dev/null +++ b/upnp/statevaluetype_cast.go @@ -0,0 +1,153 @@ +package upnp + +import ( + "fmt" + "net/url" + "strings" + "time" + + "github.com/google/uuid" +) + +// 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. +// +// Examples: +// - StateType_UI2.Cast(42) // uint16(42), nil +// - StateType_Boolean.Cast("true") // true, nil +// - StateType_UI1.Cast(300) // nil, error (overflow) +func (t StateVarType) Cast(val interface{}) (interface{}, error) { + switch t { + case StateType_UI1: + v, err := toUint(val, 8) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to UI1", val, val) + } + return uint8(v), nil + + case StateType_UI2: + v, err := toUint(val, 16) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to UI2", val, val) + } + return uint16(v), nil + + case StateType_UI4: + v, err := toUint(val, 32) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to UI4", val, val) + } + return uint32(v), nil + + case StateType_I1: + v, err := toInt(val, 8) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to I1", val, val) + } + return int8(v), nil + + case StateType_I2: + v, err := toInt(val, 16) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to I2", val, val) + } + return int16(v), nil + + case StateType_I4, StateType_Int: + v, err := toInt(val, 32) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to I4", val, val) + } + return int32(v), nil + + case StateType_R4: + v, err := toFloat(val, 32) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to R4", val, val) + } + return float32(v), nil + + case StateType_R8, StateType_Number, StateType_Fixed14_4: + v, err := toFloat(val, 64) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to R8", val, val) + } + return v, nil + + case StateType_Boolean: + b, err := toBool(val) + if err != nil { + return nil, fmt.Errorf("cannot cast %v (%T) to Boolean", val, val) + } + return b, nil + + case StateType_Char: + switch s := val.(type) { + case string: + if len(s) != 1 { + return nil, fmt.Errorf("invalid Char: string too long %q", s) + } + return rune(s[0]), nil + case rune: + return s, nil + default: + return nil, fmt.Errorf("cannot cast %v (%T) to Char", val, val) + } + + case StateType_String: + return fmt.Sprint(val), nil + + case StateType_UUID: + switch val := val.(type) { + case uuid.UUID: + return val, nil + case string: + u, err := uuid.Parse(strings.TrimSpace(val)) + if err != nil { + return nil, fmt.Errorf("invalid UUID %v: %v", val, err) + } + return u, nil + default: + return nil, fmt.Errorf("cannot cast %v (%T) to UUID", val, val) + } + + case StateType_URI: + switch val := val.(type) { + case *url.URL: + return val, nil + case string: + u, err := url.Parse(strings.TrimSpace(val)) + if err != nil { + return nil, fmt.Errorf("invalid URI %v: %v", val, err) + } + return u, nil + default: + return nil, fmt.Errorf("cannot cast %v (%T) to URI", val, val) + } + + case StateType_BinBase64, StateType_BinHex: + switch v := val.(type) { + case []byte: + return v, nil + case string: + return decodeBinary(t, v) + default: + return nil, fmt.Errorf("cannot cast %v (%T) to binary", val, val) + } + + case StateType_Date, StateType_DateTime, StateType_DateTimeTZ, + StateType_Time, StateType_TimeTZ: + switch v := val.(type) { + case time.Time: + return v, nil + case string: + return parseUPnPTime(t, v) + default: + return nil, fmt.Errorf("cannot cast %v (%T) to time", val, val) + } + + default: + return nil, fmt.Errorf("unsupported type: %v", t) + } +} diff --git a/upnp/statevaluetype_cmp.go b/upnp/statevaluetype_cmp.go new file mode 100644 index 00000000..35d114c8 --- /dev/null +++ b/upnp/statevaluetype_cmp.go @@ -0,0 +1,185 @@ +package upnp + +import ( + "bytes" + "fmt" + "log" + "strings" +) + +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)", a, b, err1, err2) + } + + switch { + case t.IsInteger(): + ai, err := toInt(a, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid int value for a: %w", err) + } + 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 t.IsUnsignedInt(): + ai, err := toUint(a, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid uint value for a: %w", err) + } + bi, err := toUint(b, t.BitSize()) + if err != nil { + return 0, fmt.Errorf("invalid uint value for b: %w", err) + } + 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: + 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()) + } +} + +// InRange checks if a value falls within an inclusive range [min, max]. +// Uses the type's comparison logic. Returns true if val is between min and max (inclusive). +// +// Example: +// +// range := ValueRange{min: uint16(10), max: uint16(100)} +// StateType_UI2.InRange(uint16(50), range) // true +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 +} diff --git a/upnp/statevaluetype_typechecking.go b/upnp/statevaluetype_typechecking.go new file mode 100644 index 00000000..63b02915 --- /dev/null +++ b/upnp/statevaluetype_typechecking.go @@ -0,0 +1,171 @@ +package upnp + +// 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, +// 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 StateVarType represents a numeric type; false +// otherwise. +func (t StateVarType) IsNumeric() bool { + switch t { + case StateType_UI1, StateType_UI2, StateType_UI4, + StateType_I1, StateType_I2, StateType_I4, + StateType_Int, + StateType_R4, StateType_R8, + StateType_Number, + StateType_Fixed14_4: + return true + default: + return false + } +} + +// IsInteger checks if the state variable type is integer or not. +// +// It returns a boolean value indicating whether the provided StateVarType (t) +// is an integer type or not. The function takes one parameter, t of type +// StateVarType, which represents the state variable type to be checked. +// +// Parameters: - t (StateVarType): The StateVarType to check for comparability. +// +// Returns: bool: If the state variable type is any of the defined integer types +// (StateType_UI1, StateType_UI2, StateType_UI4, StateType_I1, StateType_I2, +// StateType_I4, StateType_Int), it returns true. Otherwise, it returns false. +func (t StateVarType) IsInteger() bool { + switch t { + case StateType_UI1, StateType_UI2, StateType_UI4, + StateType_I1, StateType_I2, StateType_I4, + StateType_Int: + return true + default: + return false + } +} + +// 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) +// as they are non-comparable. For all other types, it returns true indicating +// that these types can be compared. +// +// Parameters: - t (StateVarType): The StateVarType to check for comparability. +// +// Returns: bool: A boolean value indicating whether the given StateVarType is +// comparable or not. True means it's comparable, False means it isn't. +func (t StateVarType) IsComparable() bool { + // Tous les types sauf les binaires sont comparables + switch t { + case StateType_BinBase64, StateType_BinHex: + return false + default: + return true + } +} diff --git a/upnp/utils_binary.go b/upnp/utils_binary.go index 69ec9989..34a896e2 100644 --- a/upnp/utils_binary.go +++ b/upnp/utils_binary.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "strings" ) // toBinary tries to convert v into a []byte. @@ -31,3 +32,31 @@ func toBinary(v interface{}) ([]byte, error) { return nil, fmt.Errorf("cannot convert type %T to binary", v) } } + +// decodeBinary decodes Base64 or Hex-encoded binary strings to byte slices +func decodeBinary(t StateVarType, val string) ([]byte, error) { + switch t { + case StateType_BinBase64: + data, err := base64.StdEncoding.DecodeString(val) + if err != nil { + return nil, fmt.Errorf("invalid base64: %v", err) + } + return data, nil + + case StateType_BinHex: + // Accept even-length hex string + val = strings.TrimSpace(val) + if len(val)%2 != 0 { + return nil, fmt.Errorf("invalid hex: odd-length string") + } + data := make([]byte, len(val)/2) + _, err := hex.Decode(data, []byte(val)) + if err != nil { + return nil, fmt.Errorf("invalid hex: %v", err) + } + return data, nil + + default: + return nil, fmt.Errorf("decodeBinary: unsupported binary type %v", t) + } +} diff --git a/upnp/utils_time.go b/upnp/utils_time.go index fee0daf2..26ed4367 100644 --- a/upnp/utils_time.go +++ b/upnp/utils_time.go @@ -2,6 +2,7 @@ package upnp import ( "fmt" + "strings" "time" ) @@ -45,6 +46,49 @@ func toTime(v interface{}) (time.Time, error) { } } +// parseUPnPTime parses time values using UPnP-specific formats: +// - Date: "2006-01-02" +// - Time: "15:04:05" +// - DateTime: "2006-01-02T15:04:05" +// - TimeTZ: "15:04:05-07:00" +// - DateTimeTZ: "2006-01-02T15:04:05-07:00" +func parseUPnPTime(t StateVarType, s string) (time.Time, error) { + s = strings.TrimSpace(s) + + layouts := []string{} + + switch t { + case StateType_Date: + layouts = []string{"2006-01-02"} + + case StateType_Time: + layouts = []string{"15:04:05"} // HH:MM:SS + + case StateType_TimeTZ: + layouts = []string{"15:04:05Z07:00"} // HH:MM:SS+TZ + + case StateType_DateTime: + layouts = []string{"2006-01-02T15:04:05"} // ISO8601 sans TZ + + case StateType_DateTimeTZ: + layouts = []string{ + "2006-01-02T15:04:05Z07:00", // full + "2006-01-02T15:04:05-0700", // fallback no colon + "2006-01-02T15:04:05Z", // Zulu + } + + default: + return time.Time{}, fmt.Errorf("unsupported date/time type: %v", t) + } + + for _, layout := range layouts { + if ts, err := time.Parse(layout, s); err == nil { + return ts, nil + } + } + return time.Time{}, fmt.Errorf("invalid %v value: %q", t, s) +} + // cmpTime compares two time.Times, returning -1 if the first is before the second, // 1 if the first is after the second, and 0 if they're equal. func cmpTime(a, b time.Time) int {