147 lines
3.2 KiB
Go
147 lines
3.2 KiB
Go
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
|
|
}
|