mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-01-11 03:11:50 +00:00
Initially found by automated repository health checks used by Termux in https://github.com/termux/termux-packages/issues/27472 The root problem was 4.3.5a comparing less than 4.3.5-rc1-1 by aptly According to debian "4.3.5a" > "4.3.5-rc1-1" This is because dpkg splits hyphen for revision at the first hyphen, whereas aptly was splitting at the last hyphen which is different from dpkg's behaviour. dpkg behaviour: https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/parsehelp.c#n242 Perhaps this wasn't detected as there was broken tests in the repository since the initial commit of aptly. This also fixes those tests
305 lines
6.7 KiB
Go
305 lines
6.7 KiB
Go
package deb
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// Using documentation from: http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
|
|
|
|
// CompareVersions compares two package versions
|
|
func CompareVersions(ver1, ver2 string) int {
|
|
e1, u1, d1 := parseVersion(ver1)
|
|
e2, u2, d2 := parseVersion(ver2)
|
|
|
|
r := compareVersionPart(e1, e2)
|
|
if r != 0 {
|
|
return r
|
|
}
|
|
|
|
r = compareVersionPart(u1, u2)
|
|
if r != 0 {
|
|
return r
|
|
}
|
|
|
|
return compareVersionPart(d1, d2)
|
|
}
|
|
|
|
// parseVersions breaks down full version to components (possibly empty)
|
|
func parseVersion(ver string) (epoch, upstream, debian string) {
|
|
i := strings.Index(ver, ":")
|
|
if i != -1 {
|
|
epoch, ver = ver[:i], ver[i+1:]
|
|
}
|
|
|
|
i = strings.Index(ver, "-")
|
|
if i != -1 {
|
|
debian, ver = ver[i+1:], ver[:i]
|
|
}
|
|
|
|
upstream = ver
|
|
|
|
return
|
|
}
|
|
|
|
// compareLexicographic compares in "Debian lexicographic" way, see below compareVersionPart for details
|
|
func compareLexicographic(s1, s2 string) int {
|
|
i := 0
|
|
l1, l2 := len(s1), len(s2)
|
|
|
|
for !(i == l1 && i == l2) { // break if s1 equal to s2
|
|
|
|
if i == l2 {
|
|
// s1 is longer than s2
|
|
if s1[i] == '~' {
|
|
return -1 // s1 < s2
|
|
}
|
|
return 1 // s1 > s2
|
|
}
|
|
|
|
if i == l1 {
|
|
// s2 is longer than s1
|
|
if s2[i] == '~' {
|
|
return 1 // s1 > s2
|
|
}
|
|
return -1 // s1 < s2
|
|
}
|
|
|
|
if s1[i] == s2[i] {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
if s1[i] == '~' {
|
|
return -1
|
|
}
|
|
|
|
if s2[i] == '~' {
|
|
return 1
|
|
}
|
|
|
|
c1, c2 := unicode.IsLetter(rune(s1[i])), unicode.IsLetter(rune(s2[i]))
|
|
if c1 && !c2 {
|
|
return -1
|
|
}
|
|
if !c1 && c2 {
|
|
return 1
|
|
}
|
|
|
|
if s1[i] < s2[i] {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// compareVersionPart compares parts of full version
|
|
//
|
|
// From Debian Policy Manual:
|
|
//
|
|
// "The strings are compared from left to right.
|
|
//
|
|
// First the initial part of each string consisting entirely of non-digit characters is
|
|
// determined. These two parts (one of which may be empty) are compared lexically. If a
|
|
// difference is found it is returned. The lexical comparison is a comparison of ASCII values
|
|
// modified so that all the letters sort earlier than all the non-letters and so that a tilde
|
|
// sorts before anything, even the end of a part. For example, the following parts are in sorted
|
|
// order from earliest to latest: ~~, ~~a, ~, the empty part.
|
|
//
|
|
// Then the initial part of the remainder of each string which consists entirely of digit
|
|
// characters is determined. The numerical values of these two parts are compared, and any difference
|
|
// found is returned as the result of the comparison. For these purposes an empty string (which can only occur at
|
|
// the end of one or both version strings being compared) counts as zero.
|
|
|
|
// These two steps (comparing and removing initial non-digit strings and initial digit strings) are
|
|
// repeated until a difference is found or both strings are exhausted."
|
|
func compareVersionPart(part1, part2 string) int {
|
|
i1, i2 := 0, 0
|
|
l1, l2 := len(part1), len(part2)
|
|
|
|
for {
|
|
j1, j2 := i1, i2
|
|
for j1 < l1 && !unicode.IsDigit(rune(part1[j1])) {
|
|
j1++
|
|
}
|
|
|
|
for j2 < l2 && !unicode.IsDigit(rune(part2[j2])) {
|
|
j2++
|
|
}
|
|
|
|
s1, s2 := part1[i1:j1], part2[i2:j2]
|
|
r := compareLexicographic(s1, s2)
|
|
if r != 0 {
|
|
return r
|
|
}
|
|
|
|
i1, i2 = j1, j2
|
|
|
|
for j1 < l1 && unicode.IsDigit(rune(part1[j1])) {
|
|
j1++
|
|
}
|
|
|
|
for j2 < l2 && unicode.IsDigit(rune(part2[j2])) {
|
|
j2++
|
|
}
|
|
|
|
s1, s2 = part1[i1:j1], part2[i2:j2]
|
|
n1, _ := strconv.Atoi(s1)
|
|
n2, _ := strconv.Atoi(s2)
|
|
|
|
if n1 < n2 {
|
|
return -1
|
|
}
|
|
if n1 > n2 {
|
|
return 1
|
|
}
|
|
|
|
i1, i2 = j1, j2
|
|
|
|
if i1 == l1 && i2 == l2 {
|
|
break
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Version relations
|
|
const (
|
|
VersionDontCare = iota
|
|
VersionLess
|
|
VersionLessOrEqual
|
|
VersionEqual
|
|
VersionGreaterOrEqual
|
|
VersionGreater
|
|
VersionPatternMatch
|
|
VersionRegexp
|
|
)
|
|
|
|
// Dependency is a parsed version of Debian dependency to package
|
|
type Dependency struct {
|
|
Pkg string
|
|
Relation int
|
|
Version string
|
|
Architecture string
|
|
Regexp *regexp.Regexp
|
|
}
|
|
|
|
// Hash calculates some predefined unique ID of Dependency
|
|
func (d *Dependency) Hash() string {
|
|
return fmt.Sprintf("%s:%s:%d:%s", d.Architecture, d.Pkg, d.Relation, d.Version)
|
|
}
|
|
|
|
// String produces human-readable representation
|
|
func (d *Dependency) String() string {
|
|
var rel string
|
|
switch d.Relation {
|
|
case VersionEqual:
|
|
rel = "="
|
|
case VersionGreater:
|
|
rel = ">>"
|
|
case VersionLess:
|
|
rel = "<<"
|
|
case VersionGreaterOrEqual:
|
|
rel = ">="
|
|
case VersionLessOrEqual:
|
|
rel = "<="
|
|
case VersionPatternMatch:
|
|
rel = "%"
|
|
case VersionRegexp:
|
|
rel = "~"
|
|
case VersionDontCare:
|
|
return fmt.Sprintf("%s [%s]", d.Pkg, d.Architecture)
|
|
}
|
|
return fmt.Sprintf("%s (%s %s) [%s]", d.Pkg, rel, d.Version, d.Architecture)
|
|
}
|
|
|
|
// ParseDependencyVariants parses dependencies in format "pkg (>= 1.35) | other-package"
|
|
func ParseDependencyVariants(variants string) (l []Dependency, err error) {
|
|
parts := strings.Split(variants, "|")
|
|
l = make([]Dependency, len(parts))
|
|
|
|
for i, part := range parts {
|
|
l[i], err = ParseDependency(strings.TrimSpace(part))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ParseDependencyArch parses the dependency name in format "pkg:any" into name and architecture
|
|
func ParseDependencyArch(d *Dependency) {
|
|
if strings.ContainsRune(d.Pkg, ':') {
|
|
parts := strings.SplitN(d.Pkg, ":", 2)
|
|
d.Pkg, d.Architecture = parts[0], parts[1]
|
|
if d.Architecture == "any" {
|
|
d.Architecture = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// ParseDependency parses dependency in format "pkg (>= 1.35) [arch]" into parts
|
|
func ParseDependency(dep string) (d Dependency, err error) {
|
|
if strings.HasSuffix(dep, "}") {
|
|
i := strings.LastIndex(dep, "{")
|
|
if i == -1 {
|
|
err = fmt.Errorf("unable to parse dependency: %s", dep)
|
|
return
|
|
}
|
|
d.Architecture = dep[i+1 : len(dep)-1]
|
|
|
|
dep = strings.TrimSpace(dep[:i])
|
|
}
|
|
|
|
if !strings.HasSuffix(dep, ")") {
|
|
d.Pkg = strings.TrimSpace(dep)
|
|
d.Relation = VersionDontCare
|
|
ParseDependencyArch(&d)
|
|
return
|
|
}
|
|
|
|
i := strings.Index(dep, "(")
|
|
if i == -1 {
|
|
err = fmt.Errorf("unable to parse dependency: %s", dep)
|
|
return
|
|
}
|
|
|
|
d.Pkg = strings.TrimSpace(dep[0:i])
|
|
ParseDependencyArch(&d)
|
|
|
|
rel := ""
|
|
if dep[i+1] == '>' || dep[i+1] == '<' || dep[i+1] == '=' {
|
|
rel += dep[i+1 : i+2]
|
|
if dep[i+2] == '>' || dep[i+2] == '<' || dep[i+2] == '=' {
|
|
rel += dep[i+2 : i+3]
|
|
d.Version = strings.TrimSpace(dep[i+3 : len(dep)-1])
|
|
} else {
|
|
d.Version = strings.TrimSpace(dep[i+2 : len(dep)-1])
|
|
}
|
|
} else {
|
|
d.Version = strings.TrimSpace(dep[i+1 : len(dep)-1])
|
|
}
|
|
|
|
switch rel {
|
|
case "<", "<=":
|
|
d.Relation = VersionLessOrEqual
|
|
case ">", ">=":
|
|
d.Relation = VersionGreaterOrEqual
|
|
case "<<":
|
|
d.Relation = VersionLess
|
|
case ">>":
|
|
d.Relation = VersionGreater
|
|
case "", "=":
|
|
d.Relation = VersionEqual
|
|
default:
|
|
err = fmt.Errorf("relation unknown %#v in dependency %s", rel, dep)
|
|
}
|
|
|
|
return
|
|
}
|