Skip to content

Commit c86ce0b

Browse files
committed
Fix version parsing, validate debian package version
This patch is based on one originally by Aaron Foster <afoster@cloudflare.com>, to prevent invalid versions. However, applying it revealed an underlying issue in the Aptly version parsing - the spec says the "debian version" is the part of the the _last_ hyphen, not the first hyphen, and that caused packages that were valid to fail validation, so I've fixed the parsing here, and updated the tests. Signed off with internal and external emails for clarity, but commit is under personal email where we typically do open source contributions from. Signed-off-by: Phil Dibowitz <pdibowitz@cloudflare.com> Signed-off-by: Phil Dibowitz <pdibowitz@ipom.com>
1 parent f59b0d2 commit c86ce0b

6 files changed

Lines changed: 188 additions & 16 deletions

File tree

deb/import.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ func ImportPackageFiles(list *PackageList, packageFiles []string, forceReplace b
124124
continue
125125
}
126126

127+
if !isValidVersion(p.Version) {
128+
reporter.Warning("Version number ('%s') for the '%s' package is invalid", p.Version, p.Name)
129+
failedFiles = append(failedFiles, file)
130+
continue
131+
}
132+
127133
if p.Architecture == "" {
128134
reporter.Warning("Empty architecture on %s", file)
129135
failedFiles = append(failedFiles, file)

deb/package.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,13 @@ func versionSatisfiesDependency(version string, dep Dependency) bool {
412412
return r == 0
413413
case VersionLess:
414414
return r < 0
415+
// 2 is returned when a package with an invalid version is detected; setting boundary for VersionGreater and GreaterOrEqual cases
415416
case VersionGreater:
416-
return r > 0
417+
return r > 0 && r < 2
417418
case VersionLessOrEqual:
418419
return r <= 0
419420
case VersionGreaterOrEqual:
420-
return r >= 0
421+
return r >= 0 && r < 2
421422
case VersionPatternMatch:
422423
matched, err := filepath.Match(dep.Version, version)
423424
return err == nil && matched

deb/query.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,13 @@ func (q *FieldQuery) Matches(pkg PackageLike) bool {
169169
return field != ""
170170
case VersionEqual:
171171
return CompareVersions(field, q.Value) == 0
172+
// 2 is returned when a package with an invalid version is detected; setting boundary for VersionGreater and VersionGreaterOrEqual cases
172173
case VersionGreater:
173-
return CompareVersions(field, q.Value) > 0
174+
result := CompareVersions(field, q.Value)
175+
return result > 0 && result < 2
174176
case VersionGreaterOrEqual:
175-
return CompareVersions(field, q.Value) >= 0
177+
result := CompareVersions(field, q.Value)
178+
return result >= 0 && result < 2
176179
case VersionLess:
177180
return CompareVersions(field, q.Value) < 0
178181
case VersionLessOrEqual:

deb/query_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,43 @@ func (s *QuerySuite) TestVersionCompare(c *C) {
2121
c.Check(q.Matches(&p100), Equals, false)
2222
c.Check(q.Matches(&p1), Equals, true)
2323
}
24+
25+
func (s *QuerySuite) TestVersionCompareGreater(c *C) {
26+
q := FieldQuery{"Version", VersionGreater, "5.0.0.2", nil}
27+
28+
p100 := Package{}
29+
p100.Version = "5.0.0.100"
30+
31+
p1 := Package{}
32+
p1.Version = "5.0.0.1"
33+
34+
c.Check(q.Matches(&p100), Equals, true)
35+
c.Check(q.Matches(&p1), Equals, false)
36+
37+
// invalid version on either side of the comparison must not match
38+
pInvalid := Package{}
39+
pInvalid.Version = "1.2.3-"
40+
c.Check(q.Matches(&pInvalid), Equals, false)
41+
}
42+
43+
func (s *QuerySuite) TestVersionCompareGreaterOrEqual(c *C) {
44+
q := FieldQuery{"Version", VersionGreaterOrEqual, "5.0.0.2", nil}
45+
46+
p100 := Package{}
47+
p100.Version = "5.0.0.100"
48+
49+
pEqual := Package{}
50+
pEqual.Version = "5.0.0.2"
51+
52+
p1 := Package{}
53+
p1.Version = "5.0.0.1"
54+
55+
c.Check(q.Matches(&p100), Equals, true)
56+
c.Check(q.Matches(&pEqual), Equals, true)
57+
c.Check(q.Matches(&p1), Equals, false)
58+
59+
// invalid version on either side of the comparison must not match
60+
pInvalid := Package{}
61+
pInvalid.Version = "1.2.3-"
62+
c.Check(q.Matches(&pInvalid), Equals, false)
63+
}

deb/version.go

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,26 @@ import (
88
"unicode"
99
)
1010

11+
var (
12+
upstreamVersionRegex = regexp.MustCompile(`^[0-9][A-Za-z0-9.+~\-]*$`)
13+
debianRevisionRegex = regexp.MustCompile(`^[A-Za-z0-9.+~]*$`)
14+
)
15+
1116
// Using documentation from: http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
1217

1318
// CompareVersions compares two package versions
1419
func CompareVersions(ver1, ver2 string) int {
15-
e1, u1, d1 := parseVersion(ver1)
16-
e2, u2, d2 := parseVersion(ver2)
20+
e1, u1, d1, err := parseVersion(ver1)
21+
// if an error is caught during parse, return 2 to signal
22+
// an invalid version and handle as needed
23+
if err != nil {
24+
return 2
25+
}
26+
27+
e2, u2, d2, err := parseVersion(ver2)
28+
if err != nil {
29+
return 2
30+
}
1731

1832
r := compareVersionPart(e1, e2)
1933
if r != 0 {
@@ -29,22 +43,67 @@ func CompareVersions(ver1, ver2 string) int {
2943
}
3044

3145
// parseVersions breaks down full version to components (possibly empty)
32-
func parseVersion(ver string) (epoch, upstream, debian string) {
46+
func parseVersion(ver string) (epoch, upstream, debian string, err error) {
3347
i := strings.Index(ver, ":")
3448
if i != -1 {
3549
epoch, ver = ver[:i], ver[i+1:]
3650
}
3751

38-
i = strings.Index(ver, "-")
52+
// Debian policy specifies that the upstream_version and debian_revision
53+
// are separated by the LAST hyphen in the string, since upstream_version
54+
// itself may legitimately contain hyphens.
55+
i = strings.LastIndex(ver, "-")
3956
if i != -1 {
4057
debian, ver = ver[i+1:], ver[:i]
58+
if debian == "" {
59+
// if a hyphen is detected in the upstream version
60+
// string without a debian revision following it, the
61+
// version is invalid
62+
return "", "", "", fmt.Errorf("could not parse version: version string ('%s-') includes hyphen without Debian revision", ver)
63+
}
4164
}
4265

4366
upstream = ver
4467

4568
return
4669
}
4770

71+
// isValidVersion checks whether package version is compliant with control field spec
72+
// source: https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version
73+
func isValidVersion(ver string) bool {
74+
epoch, upstream, deb, err := parseVersion(ver)
75+
if err != nil {
76+
return false
77+
}
78+
79+
// validate epoch component
80+
if epoch != "" {
81+
// uint64 for unexpectedly high epoch values
82+
_, err := strconv.ParseUint(epoch, 10, 64)
83+
if err != nil {
84+
return false
85+
}
86+
}
87+
88+
if upstream == "" || !isValidUpstreamVersion(upstream) {
89+
return false
90+
}
91+
92+
if deb != "" && !isValidDebianRevision(deb) {
93+
return false
94+
}
95+
96+
return true
97+
}
98+
99+
func isValidUpstreamVersion(upstream string) bool {
100+
return upstreamVersionRegex.MatchString(upstream)
101+
}
102+
103+
func isValidDebianRevision(debian string) bool {
104+
return debianRevisionRegex.MatchString(debian)
105+
}
106+
48107
// compareLexicographic compares in "Debian lexicographic" way, see below compareVersionPart for details
49108
func compareLexicographic(s1, s2 string) int {
50109
i := 0

deb/version_test.go

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,74 @@ type VersionSuite struct {
1010
var _ = Suite(&VersionSuite{})
1111

1212
func (s *VersionSuite) TestParseVersion(c *C) {
13-
e, u, d := parseVersion("1.3.4")
13+
e, u, d, err := parseVersion("1.3.4")
1414
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", ""})
15+
c.Check(err, Equals, nil)
1516

16-
e, u, d = parseVersion("4:1.3:4")
17+
e, u, d, err = parseVersion("4:1.3:4")
1718
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3:4", ""})
19+
c.Check(err, Equals, nil)
1820

19-
e, u, d = parseVersion("1.3.4-1")
21+
e, u, d, err = parseVersion("1.3.4-1")
2022
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", "1"})
23+
c.Check(err, Equals, nil)
2124

22-
e, u, d = parseVersion("1.3-pre4-1")
23-
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3", "pre4-1"})
25+
// upstream_version and debian_revision are separated by the LAST
26+
// hyphen, since upstream_version may itself contain hyphens.
27+
e, u, d, err = parseVersion("1.3-pre4-1")
28+
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3-pre4", "1"})
29+
c.Check(err, Equals, nil)
2430

25-
e, u, d = parseVersion("4:1.3-pre4-1")
26-
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3", "pre4-1"})
31+
e, u, d, err = parseVersion("4:1.3-pre4-1")
32+
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3-pre4", "1"})
33+
c.Check(err, Equals, nil)
34+
35+
e, u, d, err = parseVersion("1:2026.07.07-0325-54-a773b756")
36+
c.Check([]string{e, u, d}, DeepEquals, []string{"1", "2026.07.07-0325-54", "a773b756"})
37+
c.Check(err, Equals, nil)
38+
39+
e, u, d, err = parseVersion("1:1.2024-")
40+
c.Check([]string{e, u, d}, DeepEquals, []string{"", "", ""})
41+
c.Check(err.Error(), Equals, "could not parse version: version string ('1.2024-') includes hyphen without Debian revision")
42+
}
43+
44+
func (s *VersionSuite) TestIsValidVersion(c *C) {
45+
// valid cases
46+
valid := isValidVersion("1.2.3-abc")
47+
c.Check(valid, Equals, true)
48+
49+
valid = isValidVersion("1.0.1337~rc2-3")
50+
c.Check(valid, Equals, true)
51+
52+
valid = isValidVersion("1.2.3+fdsfgs")
53+
c.Check(valid, Equals, true)
54+
55+
valid = isValidVersion("1:2.3~4-5six")
56+
c.Check(valid, Equals, true)
57+
58+
// upstream_version containing multiple hyphens (e.g. date/build-number/
59+
// git-sha style versioning) is valid as long as it's split on the LAST
60+
// hyphen for the debian_revision.
61+
valid = isValidVersion("1:2026.07.07-0325-54-a773b756")
62+
c.Check(valid, Equals, true)
63+
64+
// invalid cases
65+
valid = isValidVersion("1:1.2.3-")
66+
c.Check(valid, Equals, false)
67+
68+
valid = isValidVersion("42:")
69+
c.Check(valid, Equals, false)
70+
71+
valid = isValidVersion("1:a.1.2.3~-4")
72+
c.Check(valid, Equals, false)
73+
74+
// non-numeric epoch
75+
valid = isValidVersion("abc:1.2.3")
76+
c.Check(valid, Equals, false)
77+
78+
// valid upstream_version, but debian_revision contains a disallowed character
79+
valid = isValidVersion("1.2.3-abc!")
80+
c.Check(valid, Equals, false)
2781
}
2882

2983
func (s *VersionSuite) TestCompareLexicographic(c *C) {
@@ -100,7 +154,16 @@ func (s *VersionSuite) TestCompareVersions(c *C) {
100154
c.Check(CompareVersions("1.0-133-avc", "1.0"), Equals, 1)
101155

102156
c.Check(CompareVersions("5.2.0.3", "5.2.0.283"), Equals, -1)
103-
c.Check(CompareVersions("4.3.5a", "4.3.5-rc3-1"), Equals, 1)
157+
// upstream_version/debian_revision split on the LAST hyphen: "4.3.5-rc3-1"
158+
// is upstream="4.3.5-rc3", debian="1", so it's actually greater than "4.3.5a"
159+
// (confirmed against `dpkg --compare-versions`).
160+
c.Check(CompareVersions("4.3.5a", "4.3.5-rc3-1"), Equals, -1)
161+
162+
// version validation happens independent of CompareVersions, so only testing the
163+
// edge case where a package is missing a Debian version during parseVersion
164+
c.Check(CompareVersions("1:abc~1.2.3-", "1:1.2.3~abc-good"), Equals, 2)
165+
// same edge case, but with the unparsable version as the second argument
166+
c.Check(CompareVersions("1:1.2.3~abc-good", "1:abc~1.2.3-"), Equals, 2)
104167
}
105168

106169
func (s *VersionSuite) TestParseDependency(c *C) {

0 commit comments

Comments
 (0)