package firmwareversion import ( "strconv" "strings" ) type identifier struct { numeric bool number uint64 text string } type semanticVersion struct { core [3]uint64 pre []identifier } // AtLeast compares SemVer-compatible versions. Vendor-specific opaque versions // are accepted only when they exactly match the configured minimum, so an // unorderable version can never silently bypass a minimum policy. func AtLeast(current, minimum string) bool { current = strings.TrimSpace(current) minimum = strings.TrimSpace(minimum) if minimum == "" { return true } left, leftOK := parse(current) right, rightOK := parse(minimum) if !leftOK || !rightOK { return current == minimum } return compare(left, right) >= 0 } func parse(value string) (semanticVersion, bool) { value = strings.TrimSpace(value) if strings.HasPrefix(value, "v") || strings.HasPrefix(value, "V") { value = value[1:] } if build := strings.IndexByte(value, '+'); build >= 0 { value = value[:build] } coreText, preText, _ := strings.Cut(value, "-") parts := strings.Split(coreText, ".") if len(parts) != 3 { return semanticVersion{}, false } var result semanticVersion for index, part := range parts { number, ok := numericIdentifier(part) if !ok { return semanticVersion{}, false } result.core[index] = number } if preText == "" { return result, true } for _, part := range strings.Split(preText, ".") { if part == "" || !validIdentifier(part) { return semanticVersion{}, false } if number, ok := numericIdentifier(part); ok { result.pre = append(result.pre, identifier{numeric: true, number: number, text: part}) } else { result.pre = append(result.pre, identifier{text: part}) } } return result, true } func compare(left, right semanticVersion) int { for index := range left.core { if left.core[index] < right.core[index] { return -1 } if left.core[index] > right.core[index] { return 1 } } if len(left.pre) == 0 && len(right.pre) == 0 { return 0 } if len(left.pre) == 0 { return 1 } if len(right.pre) == 0 { return -1 } limit := min(len(left.pre), len(right.pre)) for index := 0; index < limit; index++ { leftID, rightID := left.pre[index], right.pre[index] if leftID.numeric && rightID.numeric { if leftID.number < rightID.number { return -1 } if leftID.number > rightID.number { return 1 } continue } if leftID.numeric != rightID.numeric { if leftID.numeric { return -1 } return 1 } if leftID.text < rightID.text { return -1 } if leftID.text > rightID.text { return 1 } } if len(left.pre) < len(right.pre) { return -1 } if len(left.pre) > len(right.pre) { return 1 } return 0 } func numericIdentifier(value string) (uint64, bool) { if value == "" || (len(value) > 1 && value[0] == '0') { return 0, false } for _, char := range value { if char < '0' || char > '9' { return 0, false } } number, err := strconv.ParseUint(value, 10, 64) return number, err == nil } func validIdentifier(value string) bool { for _, char := range value { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' { continue } return false } return true }