From aeda0fb7e2589dfbcb390d1760b3b700335647f5 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 21 Jun 2025 11:14:17 +0200 Subject: [PATCH] Definition du type des valeurs UPNP --- internal/upnp/statevaluetype.go | 825 ++++++++++++++++++++++++++++++++ 1 file changed, 825 insertions(+) create mode 100644 internal/upnp/statevaluetype.go diff --git a/internal/upnp/statevaluetype.go b/internal/upnp/statevaluetype.go new file mode 100644 index 00000000..23a72a93 --- /dev/null +++ b/internal/upnp/statevaluetype.go @@ -0,0 +1,825 @@ +// Package upnp provides comprehensive handling of UPnP state variable types. +// It includes type identification, value casting, comparison, and range validation +// for all standard UPnP state variable types. +package upnp + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "fmt" + "log" + "math" + "net/url" + "reflect" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +// StateVarType represents UPnP state variable types with corresponding Go type mappings. +type StateVarType int + +// Constants defining all supported UPnP state variable types +const ( + StateType_Unknown StateVarType = iota + StateType_UI1 // Unsigned 8-bit integer (Go: uint8) + StateType_UI2 // Unsigned 16-bit integer (Go: uint16) + StateType_UI4 // Unsigned 32-bit integer (Go: uint32) + StateType_I1 // Signed 8-bit integer (Go: int8) + StateType_I2 // Signed 16-bit integer (Go: int16) + StateType_I4 // Signed 32-bit integer (Go: int32) + StateType_Int // Synonymous with i4 (Go: int32) + StateType_R4 // 32-bit floating point (Go: float32) + StateType_R8 // 64-bit floating point (Go: float64) + StateType_Number // Synonymous with r8 (Go: float64) + StateType_Fixed14_4 // Fixed-point decimal (Go: float64) + StateType_Char // Single Unicode character (Go: rune) + StateType_String // Character string (Go: string) + StateType_Boolean // Boolean value (Go: bool) + StateType_BinBase64 // Base64-encoded binary (Go: []byte) + StateType_BinHex // Hex-encoded binary (Go: []byte) + StateType_Date // Date (YYYY-MM-DD) (Go: time.Time) + StateType_DateTime // DateTime without timezone (Go: time.Time) + StateType_DateTimeTZ // DateTime with timezone (Go: time.Time) + StateType_Time // Time without timezone (Go: time.Time) + StateType_TimeTZ // Time with timezone (Go: time.Time) + StateType_UUID // Universally unique identifier (Go: uuid.UUID) + StateType_URI // Uniform Resource Identifier (Go: *url.URL) +) + +// typeNames maps UPnP XML type names to StateVarType constants +var typeNames = map[string]StateVarType{ + "ui1": StateType_UI1, + "ui2": StateType_UI2, + "ui4": StateType_UI4, + "i1": StateType_I1, + "i2": StateType_I2, + "i4": StateType_I4, + "int": StateType_Int, + "r4": StateType_R4, + "r8": StateType_R8, + "number": StateType_Number, + "fixed.14.4": StateType_Fixed14_4, + "char": StateType_Char, + "string": StateType_String, + "boolean": StateType_Boolean, + "bin.base64": StateType_BinBase64, + "bin.hex": StateType_BinHex, + "date": StateType_Date, + "dateTime": StateType_DateTime, + "dateTime.tz": StateType_DateTimeTZ, + "time": StateType_Time, + "time.tz": StateType_TimeTZ, + "uuid": StateType_UUID, + "uri": StateType_URI, +} + +// typeStrings provides string representations for StateVarType constants +var typeStrings = [...]string{ + "unknown", + "ui1", + "ui2", + "ui4", + "i1", + "i2", + "i4", + "int", + "r4", + "r8", + "number", + "fixed.14.4", + "char", + "string", + "boolean", + "bin.base64", + "bin.hex", + "date", + "dateTime", + "dateTime.tz", + "time", + "time.tz", + "uuid", + "uri", +} + +// String returns the UPnP XML name of the type. +// Returns "unknown" for unrecognized types. +func (t StateVarType) String() string { + if int(t) >= 0 && int(t) < len(typeStrings) { + return typeStrings[t] + } + return "unknown" +} + +// 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. +// +// 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, ok := toUint(val, 8) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to UI1", val, val) + } + return uint8(v), nil + + case StateType_UI2: + v, ok := toUint(val, 16) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to UI2", val, val) + } + return uint16(v), nil + + case StateType_UI4: + v, ok := toUint(val, 32) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to UI4", val, val) + } + return uint32(v), nil + + case StateType_I1: + v, ok := toInt(val, 8) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to I1", val, val) + } + return int8(v), nil + + case StateType_I2: + v, ok := toInt(val, 16) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to I2", val, val) + } + return int16(v), nil + + case StateType_I4, StateType_Int: + v, ok := toInt(val, 32) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to I4", val, val) + } + return int32(v), nil + + case StateType_R4: + v, ok := toFloat(val, 32) + if !ok { + 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, ok := toFloat(val, 64) + if !ok { + return nil, fmt.Errorf("cannot cast %v (%T) to R8", val, val) + } + return v, nil + + case StateType_Boolean: + b, ok := toBool(val) + if !ok { + 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) + } +} + +// 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) + + if err1 != nil || err2 != nil { + log.Fatalf("Failed to cast for comparison: %v vs %v (errors: %v, %v)", v1, v2, 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 + } + + 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 + } + + case StateType_Char: + r1 := castV1.(rune) + r2 := castV2.(rune) + + switch { + case r1 < r2: + return -1 + case r1 > r2: + return 1 + default: + return 0 + } + + 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 + } + return 0 + + default: + s1 := fmt.Sprint(castV1) + s2 := fmt.Sprint(castV2) + return strings.Compare(s1, s2) + } +} + +// 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 { + return t.Cmp(val, interval.min) >= 0 && t.Cmp(val, interval.max) <= 0 +} + +// toInt converts various types to signed integer with specified bit size. +// Handles overflow/underflow. Returns converted value and success status. +func toInt(v interface{}, bits int) (int64, bool) { + min := minInt(bits) + max := maxInt(bits) + + switch val := v.(type) { + case int: + if int64(val) < min || int64(val) > max { + return 0, false + } + return int64(val), true + case int8: + if int64(val) < min || int64(val) > max { + return 0, false + } + return int64(val), true + case int16: + if int64(val) < min || int64(val) > max { + return 0, false + } + return int64(val), true + case int32: + if int64(val) < min || int64(val) > max { + return 0, false + } + return int64(val), true + case int64: + if val < min || val > max { + return 0, false + } + return val, true + case uint: + if uint64(val) > uint64(max) { + return 0, false + } + return int64(val), true + case uint8: + if uint64(val) > uint64(max) { + return 0, false + } + return int64(val), true + case uint16: + if uint64(val) > uint64(max) { + return 0, false + } + return int64(val), true + case uint32: + if uint64(val) > uint64(max) { + return 0, false + } + return int64(val), true + case uint64: + if val > uint64(max) { + return 0, false + } + return int64(val), true + case float32: + r := int64(math.Round(float64(val))) + if r < min || r > max { + return 0, false + } + return r, true + case float64: + r := int64(math.Round(val)) + if r < min || r > max { + return 0, false + } + return r, true + case string: + // Try int parse direct + if i, err := strconv.ParseInt(val, 10, bits); err == nil { + if i < min || i > max { + return 0, false + } + return i, true + } + // Try float parse then round + bounds check + if f, err := strconv.ParseFloat(val, 64); err == nil { + r := int64(math.Round(f)) + if r < min || r > max { + return 0, false + } + return r, true + } + return 0, false + default: + return 0, false + } +} + +// toUint converts various types to unsigned integer with specified bit size. +// Handles numeric types and strings. Returns converted value and success status. +func toUint(v interface{}, bits int) (uint64, bool) { + max := maxUint(bits) + + switch val := v.(type) { + case uint: + if uint64(val) > max { + return 0, false + } + return uint64(val), true + case uint8: + if uint64(val) > max { + return 0, false + } + return uint64(val), true + case uint16: + if uint64(val) > max { + return 0, false + } + return uint64(val), true + case uint32: + if uint64(val) > max { + return 0, false + } + return uint64(val), true + case uint64: + if val > max { + return 0, false + } + return val, true + + case int: + if val < 0 || uint64(val) > max { + return 0, false + } + return uint64(val), true + case int8: + if val < 0 || uint64(val) > max { + return 0, false + } + return uint64(val), true + case int16: + if val < 0 || uint64(val) > max { + return 0, false + } + return uint64(val), true + case int32: + if val < 0 || uint64(val) > max { + return 0, false + } + return uint64(val), true + case int64: + if val < 0 || uint64(val) > max { + return 0, false + } + return uint64(val), true + + case float32: + r := uint64(math.Round(float64(val))) + if r > max { + return 0, false + } + return r, true + case float64: + r := uint64(math.Round(val)) + if r > max { + return 0, false + } + return r, true + + case string: + // Try parse float first (handles int and float strings) + f, err := strconv.ParseFloat(val, 64) + if err != nil || f < 0 { + return 0, false + } + r := uint64(math.Round(f)) + if r > max { + return 0, false + } + return r, true + + default: + return 0, false + } +} + +// maxUint returns maximum unsigned integer value for specified bit size +func maxUint(bits int) uint64 { + switch bits { + case 8: + return math.MaxUint8 + case 16: + return math.MaxUint16 + case 32: + return math.MaxUint32 + case 64: + return math.MaxUint64 + default: + return math.MaxUint64 // fallback + } +} + +// minInt returns minimum signed integer value for specified bit size +func minInt(bits int) int64 { + switch bits { + case 8: + return math.MinInt8 + case 16: + return math.MinInt16 + case 32: + return math.MinInt32 + case 64: + return math.MinInt64 + default: + return math.MinInt64 // fallback + } +} + +// maxInt returns maximum signed integer value for specified bit size +func maxInt(bits int) int64 { + switch bits { + case 8: + return math.MaxInt8 + case 16: + return math.MaxInt16 + case 32: + return math.MaxInt32 + case 64: + return math.MaxInt64 + default: + return math.MaxInt64 // fallback + } +} + +// toFloat converts various types to float (32 or 64 bits). +// Checks float32 boundaries when converting to 32-bit float. +func toFloat(v interface{}, bits int) (float64, bool) { + switch val := v.(type) { + case float32: + f := float64(val) + if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) { + return 0, false + } + return f, true + case float64: + if bits == 32 && (val > math.MaxFloat32 || val < -math.MaxFloat32) { + return 0, false + } + return val, true + case int, int8, int16, int32, int64: + f := float64(reflect.ValueOf(val).Int()) + if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) { + return 0, false + } + return f, true + case uint, uint8, uint16, uint32, uint64: + f := float64(reflect.ValueOf(val).Uint()) + if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) { + return 0, false + } + return f, true + case string: + f, err := strconv.ParseFloat(val, bits) + if err != nil { + return 0, false + } + if bits == 32 && (f > math.MaxFloat32 || f < -math.MaxFloat32) { + return 0, false + } + return f, true + default: + return 0, false + } +} + +// toBool converts various types to boolean following UPnP rules: +// true: 1, "true"; false: 0, "false" +func toBool(val interface{}) (bool, bool) { + switch v := val.(type) { + case bool: + return v, true + + case int: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case int8: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case int16: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case int32: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case int64: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + + case uint: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case uint8: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case uint16: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case uint32: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + case uint64: + if v == 0 { + return false, true + } else if v == 1 { + return true, true + } + + case float32: + if v == 0.0 { + return false, true + } else if v == 1.0 { + return true, true + } + case float64: + if v == 0.0 { + return false, true + } else if v == 1.0 { + return true, true + } + + case string: + return parseUPnPBoolean(v) + } + + return false, false +} + +// parseUPnPBoolean parses boolean from string: +// "1"/"true" → true, "0"/"false" → false +func parseUPnPBoolean(s string) (bool, bool) { + switch strings.TrimSpace(strings.ToLower(s)) { + case "1", "true": + return true, true + case "0", "false": + return false, true + default: + return false, false + } +} + +// 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) +}