Files
aptly/deb/version_test.go
T
Phil Dibowitz 21487d4b5d Fix version parsing, validate debian package version
This patch adds validation to the version pasing logic (parseVersion) to
not allow packages to be registered which apt wouldn't accept anyway.

It then propogates validation errors through CompareVersions, the way
in which parseVersion gets called, so that appropriate decisions can be
made.

New packages with invalid versions are not accepted.

For any pre-exisiting packages with invalid versions, as much care is
taken as possible to log warnings, and not use those packages.

This PR is loosely bnased on one by by Aaron Foster
<afoster@cloudflare.com> though it has largely been written at this
point.

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>
2026-11-01 14:25:17 -08:00

311 lines
10 KiB
Go

package deb
import (
. "gopkg.in/check.v1"
)
type VersionSuite struct {
}
var _ = Suite(&VersionSuite{})
func (s *VersionSuite) TestParseVersion(c *C) {
e, u, d, err := parseVersion("1.3.4")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", ""})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("4:1.3:4")
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3:4", ""})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("1.3.4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", "1"})
c.Check(err, Equals, nil)
// upstream_version and debian_revision are separated by the LAST
// hyphen, since upstream_version may itself contain hyphens.
e, u, d, err = parseVersion("1.3-pre4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3-pre4", "1"})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("4:1.3-pre4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3-pre4", "1"})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("1:2026.07.07-0325-54-a773b756")
c.Check([]string{e, u, d}, DeepEquals, []string{"1", "2026.07.07-0325-54", "a773b756"})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("1:1.2024-")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "", ""})
c.Check(err.Error(), Equals, "could not parse version: version string ('1.2024-') includes hyphen without Debian revision")
}
func (s *VersionSuite) TestIsValidVersion(c *C) {
// valid cases
valid := isValidVersion("1.2.3-abc")
c.Check(valid, Equals, true)
valid = isValidVersion("1.0.1337~rc2-3")
c.Check(valid, Equals, true)
valid = isValidVersion("1.2.3+fdsfgs")
c.Check(valid, Equals, true)
valid = isValidVersion("1:2.3~4-5six")
c.Check(valid, Equals, true)
// upstream_version containing multiple hyphens (e.g. date/build-number/
// git-sha style versioning) is valid as long as it's split on the LAST
// hyphen for the debian_revision.
valid = isValidVersion("1:2026.07.07-0325-54-a773b756")
c.Check(valid, Equals, true)
// invalid cases
valid = isValidVersion("1:1.2.3-")
c.Check(valid, Equals, false)
valid = isValidVersion("42:")
c.Check(valid, Equals, false)
valid = isValidVersion("1:a.1.2.3~-4")
c.Check(valid, Equals, false)
// non-numeric epoch
valid = isValidVersion("abc:1.2.3")
c.Check(valid, Equals, false)
// valid upstream_version, but debian_revision contains a disallowed character
valid = isValidVersion("1.2.3-abc!")
c.Check(valid, Equals, false)
}
func (s *VersionSuite) TestCompareLexicographic(c *C) {
c.Check(compareLexicographic("", ""), Equals, 0)
c.Check(compareLexicographic("pre", "pre"), Equals, 0)
c.Check(compareLexicographic("pr", "pre"), Equals, -1)
c.Check(compareLexicographic("pre", "pr"), Equals, 1)
c.Check(compareLexicographic("pra", "prb"), Equals, -1)
c.Check(compareLexicographic("prb", "pra"), Equals, 1)
c.Check(compareLexicographic("prx", "pr+"), Equals, -1)
c.Check(compareLexicographic("pr+", "prx"), Equals, 1)
c.Check(compareLexicographic("pr~", "pra"), Equals, -1)
c.Check(compareLexicographic("pra", "pr~"), Equals, 1)
c.Check(compareLexicographic("~~", "~~a"), Equals, -1)
c.Check(compareLexicographic("~~a", "~"), Equals, -1)
c.Check(compareLexicographic("~", ""), Equals, -1)
c.Check(compareLexicographic("~~a", "~~"), Equals, 1)
c.Check(compareLexicographic("~", "~~a"), Equals, 1)
c.Check(compareLexicographic("", "~"), Equals, 1)
}
func (s *VersionSuite) TestCompareVersionPart(c *C) {
c.Check(compareVersionPart("", ""), Equals, 0)
c.Check(compareVersionPart("pre", "pre"), Equals, 0)
c.Check(compareVersionPart("12", "12"), Equals, 0)
c.Check(compareVersionPart("1.3.5", "1.3.5"), Equals, 0)
c.Check(compareVersionPart("1.3.5-pre1", "1.3.5-pre1"), Equals, 0)
c.Check(compareVersionPart("1.0~beta1~svn1245", "1.0~beta1"), Equals, -1)
c.Check(compareVersionPart("1.0~beta1", "1.0"), Equals, -1)
c.Check(compareVersionPart("1.0~beta1", "1.0~beta1~svn1245"), Equals, 1)
c.Check(compareVersionPart("1.0", "1.0~beta1"), Equals, 1)
c.Check(compareVersionPart("1.pr", "1.pre"), Equals, -1)
c.Check(compareVersionPart("1.pre", "1.pr"), Equals, 1)
c.Check(compareVersionPart("1.pra", "1.prb"), Equals, -1)
c.Check(compareVersionPart("1.prb", "1.pra"), Equals, 1)
c.Check(compareVersionPart("3.prx", "3.pr+"), Equals, -1)
c.Check(compareVersionPart("3.pr+", "3.prx"), Equals, 1)
c.Check(compareVersionPart("3.pr~", "3.pra"), Equals, -1)
c.Check(compareVersionPart("3.pra", "3.pr~"), Equals, 1)
c.Check(compareVersionPart("2~~", "2~~a"), Equals, -1)
c.Check(compareVersionPart("2~~a", "2~"), Equals, -1)
c.Check(compareVersionPart("2~", "2"), Equals, -1)
c.Check(compareVersionPart("2~~a", "2~~"), Equals, 1)
c.Check(compareVersionPart("2~", "2~~a"), Equals, 1)
c.Check(compareVersionPart("2", "2~"), Equals, 1)
}
func (s *VersionSuite) TestCompareVersions(c *C) {
check := func(left, right string, expectedCmp int, expectedErr error) {
cmp, err := CompareVersions(left, right)
c.Check(cmp, Equals, expectedCmp)
c.Check(err, Equals, expectedErr)
}
check("3:1.0~beta1~svn1245-1", "3:1.0~beta1~svn1245-1", 0, nil)
check("1:1.0~beta1~svn1245-1", "3:1.0~beta1~svn1245-1", -1, nil)
check("1:1.0~beta1~svn1245-1", "1.0~beta1~svn1245-1", 1, nil)
check("1.0~beta1~svn1245-1", "1.0~beta1~svn1245-2", -1, nil)
check("3:1.0~beta1~svn1245-1", "3:1.0~beta1-1", -1, nil)
check("1.0~beta1~svn1245", "1.0~beta1", -1, nil)
check("1.0~beta1", "1.0", -1, nil)
check("1.0-133-avc", "1.1", -1, nil)
check("1.0-133-avc", "1.0", 1, nil)
check("5.2.0.3", "5.2.0.283", -1, nil)
// upstream_version/debian_revision split on the LAST hyphen: "4.3.5-rc3-1"
// is upstream="4.3.5-rc3", debian="1", so it's actually greater than "4.3.5a"
// (confirmed against `dpkg --compare-versions`).
check("4.3.5a", "4.3.5-rc3-1", -1, nil)
// Version validation happens independently, so these exercise parse failures.
check("1:abc~1.2.3-", "1:1.2.3~abc-good", 0, LeftInvalid)
check("1:1.2.3~abc-good", "1:abc~1.2.3-", 0, RightInvalid)
check("1:abc~1.2.3-", "2:def~4.5.6-", 0, BothInvalid)
}
func (s *VersionSuite) TestParseDependency(c *C) {
d, e := ParseDependency("dpkg (>= 1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionGreaterOrEqual)
c.Check(d.Version, Equals, "1.6")
c.Check(d.Architecture, Equals, "")
d, e = ParseDependency("dpkg(>>1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionGreater)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg(1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionEqual)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg ( 1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionEqual)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg (> 1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionGreaterOrEqual)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg (< 1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionLessOrEqual)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg (= 1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionEqual)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg (<< 1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionLess)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg(>>1.6)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionGreater)
c.Check(d.Version, Equals, "1.6")
d, e = ParseDependency("dpkg (>>1.6) {i386}")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionGreater)
c.Check(d.Version, Equals, "1.6")
c.Check(d.Architecture, Equals, "i386")
d, e = ParseDependency("python:any (>= 2.7~)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "python")
c.Check(d.Relation, Equals, VersionGreaterOrEqual)
c.Check(d.Version, Equals, "2.7~")
c.Check(d.Architecture, Equals, "")
d, e = ParseDependency("python:amd64 (>= 2.7~)")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "python")
c.Check(d.Relation, Equals, VersionGreaterOrEqual)
c.Check(d.Version, Equals, "2.7~")
c.Check(d.Architecture, Equals, "amd64")
d, e = ParseDependency("dpkg{i386}")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionDontCare)
c.Check(d.Version, Equals, "")
c.Check(d.Architecture, Equals, "i386")
d, e = ParseDependency("dpkg ")
c.Check(e, IsNil)
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Relation, Equals, VersionDontCare)
c.Check(d.Version, Equals, "")
d, e = ParseDependency("dpkg(==1.6)")
c.Check(e, ErrorMatches, "relation unknown.*")
d, e = ParseDependency("dpkg==1.6)")
c.Check(e, ErrorMatches, "unable to parse.*")
d, e = ParseDependency("dpkg i386}")
c.Check(e, ErrorMatches, "unable to parse.*")
d, e = ParseDependency("dpkg ) {i386}")
c.Check(e, ErrorMatches, "unable to parse.*")
}
func (s *VersionSuite) TestParseDependencyVariants(c *C) {
l, e := ParseDependencyVariants("dpkg (>= 1.6)")
c.Check(e, IsNil)
c.Check(l, HasLen, 1)
c.Check(l[0].Pkg, Equals, "dpkg")
c.Check(l[0].Relation, Equals, VersionGreaterOrEqual)
c.Check(l[0].Version, Equals, "1.6")
l, e = ParseDependencyVariants("dpkg (>= 1.6) | mailer-agent")
c.Check(e, IsNil)
c.Check(l, HasLen, 2)
c.Check(l[0].Pkg, Equals, "dpkg")
c.Check(l[0].Relation, Equals, VersionGreaterOrEqual)
c.Check(l[0].Version, Equals, "1.6")
c.Check(l[1].Pkg, Equals, "mailer-agent")
c.Check(l[1].Relation, Equals, VersionDontCare)
_, e = ParseDependencyVariants("dpkg(==1.6)")
c.Check(e, ErrorMatches, "relation unknown.*")
}
func (s *VersionSuite) TestDependencyString(c *C) {
d, _ := ParseDependency("dpkg(>>1.6)")
d.Architecture = "i386"
c.Check(d.String(), Equals, "dpkg (>> 1.6) [i386]")
d, _ = ParseDependency("dpkg")
d.Architecture = "i386"
c.Check(d.String(), Equals, "dpkg [i386]")
d, _ = ParseDependency("dpkg:any")
c.Check(d.Pkg, Equals, "dpkg")
c.Check(d.Architecture, Equals, "")
}