411 lines
8.6 KiB
Go
411 lines
8.6 KiB
Go
package deps
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// VersionOp defines version comparison operator for dependency constraints.
|
|
type VersionOp int
|
|
|
|
const (
|
|
OpAny VersionOp = iota
|
|
OpEqual
|
|
OpGreater
|
|
OpGreaterOrEqual
|
|
OpLess
|
|
OpLessOrEqual
|
|
)
|
|
|
|
// Constraint describes one dependency alternative like "pkg>=1.2".
|
|
type Constraint struct {
|
|
Name string
|
|
Op VersionOp
|
|
Version string
|
|
}
|
|
|
|
// Requirement describes one dependency expression with alternatives: "a | b>=2".
|
|
type Requirement struct {
|
|
Raw string
|
|
Alternatives []Constraint
|
|
}
|
|
|
|
// ParseRequirements parses dependency expressions.
|
|
func ParseRequirements(raw []string) ([]Requirement, error) {
|
|
reqs := make([]Requirement, 0, len(raw))
|
|
for _, entry := range raw {
|
|
req, err := ParseRequirement(entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reqs = append(reqs, req)
|
|
}
|
|
return reqs, nil
|
|
}
|
|
|
|
// ParseRequirement parses one dependency expression.
|
|
func ParseRequirement(raw string) (Requirement, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return Requirement{}, fmt.Errorf("dependency cannot be empty")
|
|
}
|
|
|
|
parts := strings.Split(raw, "|")
|
|
alts := make([]Constraint, 0, len(parts))
|
|
for _, part := range parts {
|
|
c, err := parseConstraint(part)
|
|
if err != nil {
|
|
return Requirement{}, fmt.Errorf("invalid dependency %q: %w", raw, err)
|
|
}
|
|
alts = append(alts, c)
|
|
}
|
|
if len(alts) == 0 {
|
|
return Requirement{}, fmt.Errorf("invalid dependency %q", raw)
|
|
}
|
|
|
|
return Requirement{
|
|
Raw: raw,
|
|
Alternatives: alts,
|
|
}, nil
|
|
}
|
|
|
|
func parseConstraint(raw string) (Constraint, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return Constraint{}, fmt.Errorf("empty alternative")
|
|
}
|
|
|
|
// Debian style: "pkg (>= 1.2)".
|
|
if open := strings.IndexByte(raw, '('); open >= 0 {
|
|
close := strings.LastIndexByte(raw, ')')
|
|
if close < open || close != len(raw)-1 {
|
|
return Constraint{}, fmt.Errorf("invalid parentheses syntax")
|
|
}
|
|
|
|
name := strings.TrimSpace(raw[:open])
|
|
if !isValidDepName(name) {
|
|
return Constraint{}, fmt.Errorf("invalid package name %q", name)
|
|
}
|
|
|
|
inner := strings.TrimSpace(raw[open+1 : close])
|
|
op, version, err := splitVersionConstraint(inner)
|
|
if err != nil {
|
|
return Constraint{}, err
|
|
}
|
|
return Constraint{Name: name, Op: op, Version: version}, nil
|
|
}
|
|
|
|
// Generic style: "pkg>=1.2" or "pkg >= 1.2" or plain "pkg".
|
|
name, op, version, err := splitNameAndConstraint(raw)
|
|
if err != nil {
|
|
return Constraint{}, err
|
|
}
|
|
return Constraint{Name: name, Op: op, Version: version}, nil
|
|
}
|
|
|
|
func splitNameAndConstraint(raw string) (string, VersionOp, string, error) {
|
|
name := strings.TrimSpace(raw)
|
|
op := OpAny
|
|
version := ""
|
|
|
|
idx, opFound := findOp(raw)
|
|
if opFound {
|
|
name = strings.TrimSpace(raw[:idx])
|
|
right := strings.TrimSpace(raw[idx:])
|
|
var err error
|
|
op, version, err = splitVersionConstraint(right)
|
|
if err != nil {
|
|
return "", OpAny, "", err
|
|
}
|
|
}
|
|
|
|
if !isValidDepName(name) {
|
|
return "", OpAny, "", fmt.Errorf("invalid package name %q", name)
|
|
}
|
|
return name, op, version, nil
|
|
}
|
|
|
|
func findOp(raw string) (int, bool) {
|
|
for i, r := range raw {
|
|
switch r {
|
|
case '<', '>', '=':
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func splitVersionConstraint(raw string) (VersionOp, string, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return OpAny, "", fmt.Errorf("empty version constraint")
|
|
}
|
|
|
|
opToken := ""
|
|
version := ""
|
|
|
|
switch {
|
|
case strings.HasPrefix(raw, ">="):
|
|
opToken = ">="
|
|
version = strings.TrimSpace(raw[2:])
|
|
case strings.HasPrefix(raw, "<="):
|
|
opToken = "<="
|
|
version = strings.TrimSpace(raw[2:])
|
|
case strings.HasPrefix(raw, "="):
|
|
opToken = "="
|
|
version = strings.TrimSpace(raw[1:])
|
|
case strings.HasPrefix(raw, ">"):
|
|
opToken = ">"
|
|
version = strings.TrimSpace(raw[1:])
|
|
case strings.HasPrefix(raw, "<"):
|
|
opToken = "<"
|
|
version = strings.TrimSpace(raw[1:])
|
|
default:
|
|
parts := strings.Fields(raw)
|
|
if len(parts) == 2 {
|
|
opToken = parts[0]
|
|
version = parts[1]
|
|
}
|
|
}
|
|
|
|
if opToken == "" || version == "" {
|
|
return OpAny, "", fmt.Errorf("invalid version constraint %q", raw)
|
|
}
|
|
if strings.HasPrefix(version, "<") || strings.HasPrefix(version, ">") || strings.HasPrefix(version, "=") {
|
|
return OpAny, "", fmt.Errorf("invalid version %q", version)
|
|
}
|
|
if strings.ContainsAny(version, "()|") {
|
|
return OpAny, "", fmt.Errorf("invalid version %q", version)
|
|
}
|
|
|
|
op, err := parseVersionOp(opToken)
|
|
if err != nil {
|
|
return OpAny, "", err
|
|
}
|
|
return op, version, nil
|
|
}
|
|
|
|
func parseVersionOp(raw string) (VersionOp, error) {
|
|
switch strings.TrimSpace(raw) {
|
|
case "=":
|
|
return OpEqual, nil
|
|
case ">":
|
|
return OpGreater, nil
|
|
case ">=":
|
|
return OpGreaterOrEqual, nil
|
|
case "<":
|
|
return OpLess, nil
|
|
case "<=":
|
|
return OpLessOrEqual, nil
|
|
default:
|
|
return OpAny, fmt.Errorf("unsupported operator %q", raw)
|
|
}
|
|
}
|
|
|
|
func isValidDepName(name string) bool {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return false
|
|
}
|
|
for i, r := range name {
|
|
if i == 0 {
|
|
if !unicode.IsDigit(r) && !unicode.IsLetter(r) {
|
|
return false
|
|
}
|
|
continue
|
|
}
|
|
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
|
continue
|
|
}
|
|
switch r {
|
|
case '+', '.', '-', '_':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MatchesVersion checks whether package version satisfies this constraint.
|
|
func (c Constraint) MatchesVersion(installedVersion string) bool {
|
|
switch c.Op {
|
|
case OpAny:
|
|
return true
|
|
case OpEqual:
|
|
return CompareVersions(installedVersion, c.Version) == 0
|
|
case OpGreater:
|
|
return CompareVersions(installedVersion, c.Version) > 0
|
|
case OpGreaterOrEqual:
|
|
return CompareVersions(installedVersion, c.Version) >= 0
|
|
case OpLess:
|
|
return CompareVersions(installedVersion, c.Version) < 0
|
|
case OpLessOrEqual:
|
|
return CompareVersions(installedVersion, c.Version) <= 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// CompareVersions compares package versions.
|
|
// Returns -1 if a < b, 0 if a == b, and 1 if a > b.
|
|
func CompareVersions(a, b string) int {
|
|
epochA, restA := splitEpoch(a)
|
|
epochB, restB := splitEpoch(b)
|
|
if epochA != epochB {
|
|
if epochA < epochB {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
|
|
mainA, relA := splitRelease(restA)
|
|
mainB, relB := splitRelease(restB)
|
|
|
|
if c := compareVersionPart(mainA, mainB); c != 0 {
|
|
return c
|
|
}
|
|
return compareVersionPart(relA, relB)
|
|
}
|
|
|
|
func splitEpoch(raw string) (int64, string) {
|
|
raw = strings.TrimSpace(raw)
|
|
if idx := strings.IndexByte(raw, ':'); idx > 0 {
|
|
epochRaw := strings.TrimSpace(raw[:idx])
|
|
if epoch, err := strconv.ParseInt(epochRaw, 10, 64); err == nil {
|
|
return epoch, strings.TrimSpace(raw[idx+1:])
|
|
}
|
|
}
|
|
return 0, raw
|
|
}
|
|
|
|
func splitRelease(raw string) (string, string) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "0", "0"
|
|
}
|
|
|
|
if idx := strings.LastIndex(raw, "-"); idx > 0 && idx < len(raw)-1 {
|
|
return raw[:idx], raw[idx+1:]
|
|
}
|
|
return raw, "0"
|
|
}
|
|
|
|
func compareVersionPart(a, b string) int {
|
|
a = strings.TrimSpace(a)
|
|
b = strings.TrimSpace(b)
|
|
|
|
i, j := 0, 0
|
|
for i < len(a) || j < len(b) {
|
|
// Tilde sorts before everything, including end of string.
|
|
if i < len(a) && a[i] == '~' || j < len(b) && b[j] == '~' {
|
|
switch {
|
|
case i < len(a) && a[i] == '~' && j < len(b) && b[j] == '~':
|
|
i++
|
|
j++
|
|
continue
|
|
case i < len(a) && a[i] == '~':
|
|
return -1
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
for i < len(a) && isVersionSeparator(a[i]) {
|
|
i++
|
|
}
|
|
for j < len(b) && isVersionSeparator(b[j]) {
|
|
j++
|
|
}
|
|
|
|
if i >= len(a) && j >= len(b) {
|
|
return 0
|
|
}
|
|
if i >= len(a) {
|
|
return -1
|
|
}
|
|
if j >= len(b) {
|
|
return 1
|
|
}
|
|
|
|
aNum := isDigit(a[i])
|
|
bNum := isDigit(b[j])
|
|
|
|
segA, nextI := readVersionSegment(a, i, aNum)
|
|
segB, nextJ := readVersionSegment(b, j, bNum)
|
|
i, j = nextI, nextJ
|
|
|
|
var c int
|
|
switch {
|
|
case aNum && bNum:
|
|
c = compareNumericSegment(segA, segB)
|
|
case aNum && !bNum:
|
|
c = 1
|
|
case !aNum && bNum:
|
|
c = -1
|
|
default:
|
|
c = strings.Compare(strings.ToLower(segA), strings.ToLower(segB))
|
|
}
|
|
if c < 0 {
|
|
return -1
|
|
}
|
|
if c > 0 {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func isVersionSeparator(ch byte) bool {
|
|
if ch == '~' {
|
|
return false
|
|
}
|
|
return !isDigit(ch) && !isLetter(ch)
|
|
}
|
|
|
|
func readVersionSegment(s string, start int, numeric bool) (string, int) {
|
|
i := start
|
|
for i < len(s) {
|
|
ch := s[i]
|
|
if numeric {
|
|
if !isDigit(ch) {
|
|
break
|
|
}
|
|
} else {
|
|
if !isLetter(ch) {
|
|
break
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
return s[start:i], i
|
|
}
|
|
|
|
func compareNumericSegment(a, b string) int {
|
|
a = strings.TrimLeft(a, "0")
|
|
b = strings.TrimLeft(b, "0")
|
|
if a == "" {
|
|
a = "0"
|
|
}
|
|
if b == "" {
|
|
b = "0"
|
|
}
|
|
|
|
if len(a) < len(b) {
|
|
return -1
|
|
}
|
|
if len(a) > len(b) {
|
|
return 1
|
|
}
|
|
return strings.Compare(a, b)
|
|
}
|
|
|
|
func isDigit(ch byte) bool {
|
|
return ch >= '0' && ch <= '9'
|
|
}
|
|
|
|
func isLetter(ch byte) bool {
|
|
return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z'
|
|
}
|