Browse Source

Parse Package desc

v0
maze 3 years ago
parent
commit
13eb3c3b0c
8 changed files with 1018 additions and 3 deletions
  1. +51
    -0
      cmd/arch-verify-pkgdesc/main.go
  2. +322
    -3
      package.go
  3. +206
    -0
      package_test.go
  4. +73
    -0
      repository.go
  5. +102
    -0
      version/dependency.go
  6. +71
    -0
      version/dependency_test.go
  7. +166
    -0
      version/version.go
  8. +27
    -0
      version/version_test.go

+ 51
- 0
cmd/arch-verify-pkgdesc/main.go View File

@ -0,0 +1,51 @@
/*
Command arch-verify-pkgdesc verifies the checksums in an Arch Linux package desc file with the package tarball
*/
package main
import (
"flag"
"fmt"
"os"
archlinux "maze.io/archlinux.v0"
)
func fatalln(v ...interface{}) {
fmt.Fprintln(os.Stderr, v...)
os.Exit(1)
}
func dump(s string, h []byte) {
if len(h) == 0 {
return
}
fmt.Printf("%-6s %x\n", s, h)
}
func main() {
flag.Parse()
if flag.NArg() != 2 {
fatalln(os.Args[0], "<desc file>", "<package file>")
}
f, err := os.Open(flag.Arg(0))
if err != nil {
fatalln(flag.Arg(0), "error:", err)
}
defer f.Close()
pkg, err := archlinux.ReadPackageDesc(f)
if err != nil {
fatalln(flag.Arg(0), "read error:", err)
}
if err = pkg.Verify(flag.Arg(1)); err != nil {
fatalln(flag.Arg(1), err)
}
dump("MD5", pkg.MD5)
dump("SHA1", pkg.SHA1)
dump("SHA256", pkg.SHA256)
dump("SHA384", pkg.SHA384)
dump("SHA512", pkg.SHA512)
}

+ 322
- 3
package.go View File

@ -2,21 +2,33 @@ package archlinux
import (
"bufio"
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"os"
"strconv"
"strings"
"time"
"maze.io/archlinux.v0/tarball"
"maze.io/archlinux.v0/version"
)
// Errors
var (
ErrNoPackageName = errors.New("archlinux: name is missing")
ErrNoPackageVersion = errors.New("archlinux: version is missing")
ErrNoPackageRelease = errors.New("archlinux: release is missing")
ErrPackageNameInvalid = errors.New("archlinux: invalid package name")
ErrPackageNoChecksums = errors.New("archlinux: package has no checksums")
ErrNoPackageName = errors.New("archlinux: name is missing")
ErrNoPackageVersion = errors.New("archlinux: version is missing")
ErrNoPackageRelease = errors.New("archlinux: release is missing")
)
// Namer is an interface for instances that have a name, such as *os.File
@ -204,6 +216,27 @@ func ReadPackageInfo(r io.Reader) (*Package, error) {
return pkg, nil
}
// ReadPackageDesc reads a package description from a desc file
func ReadPackageDesc(r io.Reader) (*Package, error) {
pkg := new(Package)
if n, ok := r.(Namer); ok {
pkg.Filename = n.Name()
}
if err := pkg.readPackageDesc(r); err != nil {
return nil, err
}
if pkg.Name == "" {
return nil, ErrNoPackageName
}
if pkg.Version == "" {
return nil, ErrNoPackageVersion
}
if pkg.Release == "" {
return nil, ErrNoPackageRelease
}
return pkg, nil
}
func (pkg *Package) readBuildInfo(r io.Reader) (err error) {
s := bufio.NewScanner(r)
for s.Scan() {
@ -301,3 +334,289 @@ func (pkg *Package) readPackageInfo(r io.Reader) (err error) {
}
return
}
// WritePackageInfo writes a PKGINFO to the supplied Writer
func (pkg *Package) WritePackageInfo(w io.Writer) error {
var (
fields = []struct {
Key string
Value interface{}
}{
{"pkgname", pkg.Name},
{"pkgbase", pkg.Base},
{"pkgver", pkg.Version + "-" + pkg.Release},
{"pkgdesc", pkg.Description},
{"url", pkg.URL},
{"builddate", pkg.BuildDate},
{"packager", pkg.Packager},
{"size", pkg.Size},
{"arch", pkg.Arch},
{"license", pkg.License},
{"provides", pkg.Provides},
{"conflicts", pkg.Conflicts},
{"replaces", pkg.Replaces},
{"backup", pkg.Backup},
{"depend", pkg.Depends},
{"optdepend", pkg.OptionalDepends},
{"makedepend", pkg.MakeDepends},
{"checkdepend", pkg.CheckDepends},
}
err error
)
if _, err = fmt.Fprintln(w, "# Generated by maze.io/archlinux.v0\n#", time.Now().UTC().Format(time.UnixDate)); err != nil {
return err
}
for _, field := range fields {
switch value := field.Value.(type) {
case string:
if value == "" {
continue
}
if _, err = fmt.Fprintln(w, field.Key, "=", value); err != nil {
return err
}
case []string:
if len(value) > 0 {
for _, v := range value {
if v == "" {
continue
}
if _, err = fmt.Fprintln(w, field.Key, "=", v); err != nil {
return err
}
}
}
case int64:
if value > 0 {
if _, err = fmt.Fprintln(w, field.Key, "=", value); err != nil {
return err
}
}
case *URL:
if value == nil {
continue
}
if _, err = fmt.Fprintln(w, field.Key, "=", value.String()); err != nil {
return err
}
}
}
return nil
}
func (pkg *Package) readPackageDesc(r io.Reader) (err error) {
var (
s = bufio.NewScanner(r)
line string
lines []string
section string
)
for s.Scan() {
if err = s.Err(); err != nil {
if err == io.EOF {
err = nil
}
break
}
line = s.Text()
if len(line) == 0 {
switch section {
case "FILENAME":
pkg.Filename = lines[0]
case "NAME":
pkg.Name = lines[0]
case "VERSION":
i := strings.LastIndexByte(lines[0], '-')
if i == -1 {
return fmt.Errorf("archlinux: invalid version %q", lines[0])
}
pkg.Version, pkg.Release = lines[0][:i], lines[0][i+1:]
case "DESC":
pkg.Description = strings.TrimSpace(strings.Join(lines, "\n"))
case "ARCH":
pkg.Arch = make([]string, len(lines))
copy(pkg.Arch, lines)
case "LICENSE":
pkg.License = make([]string, len(lines))
copy(pkg.License, lines)
case "GROUPS":
pkg.Groups = make([]string, len(lines))
copy(pkg.Groups, lines)
case "CSIZE":
if pkg.CompressedSize, err = strconv.ParseInt(lines[0], 10, 64); err != nil {
return
}
case "ISIZE":
if pkg.Size, err = strconv.ParseInt(lines[0], 10, 64); err != nil {
return
}
case "MD5SUM":
if pkg.MD5, err = hex.DecodeString(lines[0]); err != nil {
return
}
case "SHA1SUM":
if pkg.SHA1, err = hex.DecodeString(lines[0]); err != nil {
return
}
case "SHA256SUM":
if pkg.SHA256, err = hex.DecodeString(lines[0]); err != nil {
return
}
case "SHA384SUM":
if pkg.SHA384, err = hex.DecodeString(lines[0]); err != nil {
return
}
case "SHA512SUM":
if pkg.SHA512, err = hex.DecodeString(lines[0]); err != nil {
return
}
case "PGPSIG":
if pkg.PGPSignature, err = base64.StdEncoding.DecodeString(lines[0]); err != nil {
return
}
case "URL":
if pkg.URL, err = ParseURL(lines[0]); err != nil {
return
}
case "DEPENDS":
pkg.Depends = make([]string, len(lines))
copy(pkg.Depends, lines)
case "OPTDEPENDS":
pkg.OptionalDepends = make([]string, len(lines))
copy(pkg.OptionalDepends, lines)
case "MAKEDEPENDS":
pkg.MakeDepends = make([]string, len(lines))
copy(pkg.MakeDepends, lines)
case "CHECKDEPENDS":
pkg.CheckDepends = make([]string, len(lines))
copy(pkg.CheckDepends, lines)
case "PROVIDES":
pkg.Provides = make([]string, len(lines))
copy(pkg.Provides, lines)
case "CONFLICTS":
pkg.Conflicts = make([]string, len(lines))
copy(pkg.Conflicts, lines)
case "REPLACES":
pkg.Replaces = make([]string, len(lines))
copy(pkg.Replaces, lines)
case "BUILDDATE":
var i int64
if i, err = strconv.ParseInt(lines[0], 10, 64); err != nil {
return
}
pkg.BuildDate = Timestamp(time.Unix(i, 0))
case "PACKAGER":
pkg.Packager = lines[0]
default:
return fmt.Errorf("archlinux: unsupported desc section %q", section)
}
} else if line[0] == '%' && line[len(line)-1] == '%' {
section = line[1 : len(line)-1]
lines = lines[:0]
} else {
lines = append(lines, line)
}
}
return
}
type hasher struct {
hash.Hash
Name string
Sum []byte
//*sync.WaitGroup
//In chan []byte
}
func newHasher(name string, sum []byte, h hash.Hash) hasher {
n := hasher{
Hash: h,
Name: name,
Sum: sum,
//WaitGroup: new(sync.WaitGroup),
//In: make(chan []byte, 16),
}
/*
n.Add(1)
go n.Run()
*/
return n
}
/*
func (h *hasher) Run() {
defer h.Done()
for {
select {
case b := <-h.In:
if b == nil {
return
}
if _, err := h.Hash.Write(b); err != nil {
panic(err)
}
}
}
}
*/
// Verify verifies the package file checksums
func (pkg *Package) Verify(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
var hashers []hasher
addHasher := func(name string, sum []byte, fn func() hash.Hash) {
if len(sum) == 0 {
return
}
hashers = append(hashers, newHasher(name, sum, fn()))
}
addHasher("MD5", pkg.MD5, md5.New)
addHasher("SHA1", pkg.SHA1, sha1.New)
addHasher("SHA256", pkg.SHA256, sha256.New)
addHasher("SHA384", pkg.SHA384, sha512.New384)
addHasher("SHA512", pkg.SHA512, sha512.New)
if len(hashers) == 0 {
return ErrPackageNoChecksums
}
var block [4096]byte
for {
n, err := f.Read(block[:])
if err != nil {
if err == io.EOF {
break
}
}
for _, h := range hashers {
//h.In <- block[:n]
if _, err = h.Write(block[:n]); err != nil {
return err
}
}
}
for _, h := range hashers {
// h.Wait()
if s := h.Hash.Sum(nil); !bytes.Equal(s, h.Sum) {
return fmt.Errorf("archlinux: %s checksum mismatch, got %x, expected %x", h.Name, s, h.Sum)
}
}
return nil
}
func ParsePackageName(s string) (name string, v version.Version, err error) {
i := strings.LastIndexByte(s, '-')
if i == -1 {
err = ErrPackageNameInvalid
return
}
name, v = s[:i], version.Parse(s[i+1:])
return
}

+ 206
- 0
package_test.go View File

@ -3,8 +3,10 @@ package archlinux
import (
"bytes"
"io"
"os"
"strings"
"testing"
"time"
)
type testReadNamer struct {
@ -230,3 +232,207 @@ pkgver = 330`
testPackageInfoErrorRelease = `pkgname = xterm
pkgver = 330-`
)
func TestWritePackageInfo(t *testing.T) {
if err := testWritePackageInfo.WritePackageInfo(os.Stdout); err != nil {
t.Fatal(err)
}
}
var (
testWritePackageInfo = &Package{
Name: "imagemagick",
Base: "imagemagick",
Version: "6.9.9.15",
Release: "1",
Description: "An image viewing/manipulation program",
Arch: []string{"x86_64"},
License: []string{"custom"},
Depends: []string{"libltdl", "lcms2", "libxt", "fontconfig", "libxext", "liblqr", "libraqm", "opencl-icd-loader", "perl>=5.26", "perl<5.27"},
MakeDepends: []string{"libltdl", "lcms2", "libxt", "fontconfig", "libxext", "ghostscript", "openexr", "libwmf", "librsvg", "libxml2", "liblqr", "openjpeg2", "libraw", "libraqm", "opencl-headers", "opencl-icd-loader", "libwebp", "subversion", "glu"},
OptionalDepends: []string{"imagemagick-doc: for additional information", "ghostscript: for Ghostscript support", "openexr: for OpenEXR support", "openjpeg2: for JP2 support", "libwmf: for WMF support", "librsvg: for SVG support", "libxml2: for XML support", "libpng: for PNG support", "libwebp: for WEBP support", "libraw: for DNG support"},
Backup: []string{"etc/ImageMagick-6/coder.xml", "etc/ImageMagick-6/colors.xml", "etc/ImageMagick-6/delegates.xml", "etc/ImageMagick-6/log.xml", "etc/ImageMagick-6/magic.xml", "etc/ImageMagick-6/mime.xml", "etc/ImageMagick-6/policy.xml", "etc/ImageMagick-6/quantization-table.xml", "etc/ImageMagick-6/thresholds.xml", "etc/ImageMagick-6/type.xml", "etc/ImageMagick-6/type-dejavu.xml", "etc/ImageMagick-6/type-ghostscript.xml", "etc/ImageMagick-6/type-windows.xml"},
Size: 9897984,
BuildDate: Timestamp(time.Unix(1506203625, 0)),
Packager: "Antonio Rojas <arojas@archlinux.org>",
}
)
func TestReadPackageDesc(t *testing.T) {
pkg, err := ReadPackageDesc(bytes.NewBufferString(testReadPackageDesc))
if err != nil {
t.Fatal(err)
}
t.Logf("%#+v\n", pkg)
}
var (
testReadPackageDesc = `%FILENAME%
perl-5.26.0-4-x86_64.pkg.tar.xz
%NAME%
perl
%VERSION%
5.26.0-4
%DESC%
A highly capable, feature-rich programming language
%GROUPS%
base
%CSIZE%
14343632
%ISIZE%
54492160
%MD5SUM%
08a8b1fcab42ec43fdf07c4a5b1b2f50
%SHA256SUM%
ab958e0796650557a227f7eff56924ea8d03160803f8d061717b6013ee0045c1
%PGPSIG%
iQEzBAABCAAdFiEEhs/8qRjPOvRxR1iAUeixSKmZnDQFAlmew3MACgkQUeixSKmZnDRRhwf/Q0Ve+pIQAxknR0v3p2FyWgfuanwYCpWhIZgTCKGCmCntayBKZ0U9v41Mac7PJ11AJvntNWJ9vhmR3dqpj6AOw6lXa4AGXUCHOmo21FMYEPltcp27zRCk/7FHFHwxDd+8y0cU8x4l7QVmTq4odMjAQiimMEcxY/jXsoUX084SzOQ02ix2DA67spFLAD7BC9z6tmdH9ABXaRA8QL51uQxGaQ+fxc8FKXFjOhbNyPcjOZasiwvrLboB/nQRfygf0baBSzzbM/hOnHVgqc3WfAqE1y4Qve8cE1L+2hhXXHaFCj9/v6vI1JZGQlMYisbhYMuK0n4kM4RbX7N6I78IHuRuyg==
%URL%
http://www.perl.org
%LICENSE%
GPL
PerlArtistic
%ARCH%
x86_64
%BUILDDATE%
1503575763
%PACKAGER%
Evangelos Foutras <evangelos@foutrelis.com>
%PROVIDES%
perl-archive-tar=2.24
perl-attribute-handlers=0.99
perl-autodie=2.29
perl-autoloader=5.74
perl-autouse=1.11
perl-b-debug=1.24
perl-base=2.25
perl-bignum=0.47
perl-carp=1.42
perl-compress-raw-bzip2=2.074
perl-compress-raw-zlib=2.074
perl-config-perl-v=0.28
perl-constant=1.33
perl-cpan-meta-requirements=2.140
perl-cpan-meta-yaml=0.018
perl-cpan-meta=2.150010
perl-cpan=2.18
perl-data-dumper=2.167
perl-db_file=1.840
perl-devel-ppport=3.35
perl-devel-selfstubber=1.06
perl-digest-md5=2.55
perl-digest-sha=5.96
perl-digest=1.17_01
perl-dumpvalue=1.18
perl-encode=2.88
perl-encoding-warnings=0.13
perl-env=1.04
perl-experimental=0.016
perl-exporter=5.72
perl-extutils-cbuilder=0.280225
perl-extutils-constant=0.23
perl-extutils-install=2.04
perl-extutils-makemaker=7.24
perl-extutils-manifest=1.70
perl-extutils-parsexs=3.34
perl-file-fetch=0.52
perl-file-path=2.12_01
perl-file-temp=0.2304
perl-filter-simple=0.93
perl-filter-util-call=1.55
perl-getopt-long=2.49
perl-http-tiny=0.070
perl-i18n-collate=1.02
perl-i18n-langtags=0.42
perl-if=0.0606
perl-io-compress=2.074
perl-io-socket-ip=0.38
perl-io-zlib=1.10
perl-io=1.38
perl-ipc-cmd=0.96
perl-ipc-sysv=2.07
perl-json-pp=2.27400_02
perl-lib=0.64
perl-libnet=3.10
perl-locale-codes=3.42
perl-locale-maketext-simple=0.21_01
perl-locale-maketext=1.28
perl-math-bigint-fastcalc=0.5005
perl-math-bigint=1.999806
perl-math-bigrat=0.2611
perl-math-complex=1.5901
perl-memoize=1.03_01
perl-mime-base64=3.15
perl-module-corelist=5.20170530
perl-module-load-conditional=0.68
perl-module-load=0.32
perl-module-loaded=0.08
perl-module-metadata=1.000033
perl-net-ping=2.55
perl-params-check=0.38
perl-parent=0.236
perl-pathtools=3.67
perl-perl-ostype=1.010
perl-perlfaq=5.021011
perl-perlio-via-quotedprint=0.08
perl-pod-checker=1.73
perl-pod-escapes=1.07
perl-pod-parser=1.63
perl-pod-perldoc=3.28
perl-pod-simple=3.35
perl-pod-usage=1.69
perl-podlators=5.006
perl-safe=2.40
perl-scalar-list-utils=1.46_02
perl-search-dict=1.07
perl-selfloader=1.23
perl-socket=2.020_03
perl-storable=2.62
perl-sys-syslog=0.35
perl-term-ansicolor=4.06
perl-term-cap=1.17
perl-term-complete=1.403
perl-term-readline=1.16
perl-test-harness=3.38
perl-test-simple=1.302073
perl-test=1.30
perl-text-abbrev=1.02
perl-text-balanced=2.03
perl-text-parsewords=3.30
perl-text-tabs=2013.0523
perl-thread-queue=3.12
perl-thread-semaphore=2.13
perl-threads-shared=1.56
perl-threads=2.15
perl-tie-file=1.02
perl-tie-refhash=1.39
perl-time-hires=1.9741
perl-time-local=1.25
perl-time-piece=1.31
perl-unicode-collate=1.19
perl-unicode-normalize=1.25
perl-version=0.9917
perl-xsloader=0.27
%DEPENDS%
gdbm
db
glibc
`
)

+ 73
- 0
repository.go View File

@ -0,0 +1,73 @@
package archlinux
import (
"fmt"
"io"
"path/filepath"
"strings"
"maze.io/archlinux.v0/tarball"
)
// Repository holds 0 or more Packages
type Repository struct {
// Name of the repository
Name string
// Packages in the repository
Packages []*Package
}
// ReadRepositoryDB reads a compressed repository tarball
func ReadRepositoryDB(r io.Reader) (*Repository, error) {
t, err := tarball.NewReader(r)
if err != nil {
return nil, err
}
repo := new(Repository)
if n, ok := r.(Namer); ok {
repo.Name = strings.SplitN(filepath.Base(n.Name()), ".db", 2)[0]
}
for {
h, err := t.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
var (
base = filepath.Base(strings.Trim(h.Name, "/"))
part = strings.Split(strings.Trim(h.Name, "/"), "/")
seen = make(map[string]*Package)
name string
)
if len(part) == 1 || base == "" || base[0] == '.' || h.FileInfo().IsDir() {
continue
}
name = part[0]
i := strings.LastIndexByte(name, '-')
if i == -1 {
return nil, fmt.Errorf("archlinux: invalid package name %q", name)
}
pkg, ok := seen[name]
if !ok {
pkg = &Package{
Name: name[:i],
Version: name[i+1:],
}
seen[name] = pkg
}
switch base {
case "desc":
if err = pkg.readPackageDesc(t); err != nil {
return nil, err
}
}
}
return repo, nil
}

+ 102
- 0
version/dependency.go View File

@ -0,0 +1,102 @@
package version
import (
"fmt"
"strings"
)
// Operator is a dependancy comparison operator
type Operator string
// ParseOperator parses a string as operator
func ParseOperator(s string) (Operator, error) {
op, _, err := parseOperator(s, true)
return op, err
}
func parseOperator(s string, strict bool) (op Operator, ends int, err error) {
op = Operator(s)
if ends = strings.IndexFunc(s, func(r rune) bool {
return r != '<' && r != '>' && r != '='
}); ends != -1 {
// We've hit something not <, > or =
if !strict {
op = op[:ends]
}
} else {
ends = len(op)
}
if _, known := operators[op]; !known {
err = fmt.Errorf("version: invalid operator %q", op)
op = None
return
}
return
}
// Operator types
const (
None Operator = ""
LessThan Operator = "<"
LessThenOrEqualTo Operator = "<="
EqualTo Operator = "="
MoreThanOrEqualTo Operator = ">="
MoreThan Operator = ">"
)
var operators = map[Operator]bool{
None: true,
LessThan: true,
LessThenOrEqualTo: true,
EqualTo: true,
MoreThanOrEqualTo: true,
MoreThan: true,
}
func (op Operator) String() string {
return string(op)
}
// Dependency describes a dependency
type Dependency struct {
Operator
Version
}
// ParseDependency parses a string into its prefix and the dependency specifier
func ParseDependency(s string) (string, Dependency, error) {
var (
name = s
ops string
dep Dependency
offset, ends int
err error
)
if offset = strings.Index(name, ": "); offset != -1 {
// Dependancies may have a comment, which is separated by ": " (notice the
// space, which is different from epoch)
name = name[:offset]
}
if offset = strings.IndexFunc(name, func(r rune) bool {
return r == '<' || r == '>' || r == '='
}); offset != -1 {
// We have an operator, probably
name, ops = name[:offset], name[offset:]
if dep.Operator, ends, err = parseOperator(ops, false); err != nil {
return "", Dependency{}, err
}
if strings.ContainsAny(ops[ends:], ":-") {
dep.Version = Parse(ops[ends:])
} else {
dep.Version.Version = ops[ends:]
}
}
return name, dep, nil
}
func (dep Dependency) String() string {
if dep.Operator == None {
return ""
}
return dep.Operator.String() + dep.Version.String()
}

+ 71
- 0
version/dependency_test.go View File

@ -0,0 +1,71 @@
package version
import "testing"
func TestOperatorStrict(t *testing.T) {
var tests = []struct {
String string
Operator
}{
{"", None},
{"<", LessThan},
{"<=", LessThenOrEqualTo},
{"=", EqualTo},
{">=", MoreThanOrEqualTo},
{">", MoreThan},
}
for _, test := range tests {
op, err := ParseOperator(test.String)
if err != nil {
t.Fatal(err)
}
if op != test.Operator {
t.Fatalf(`expected operator %q, got %q`, test.Operator, op)
}
}
}
func TestOperatorStrictError(t *testing.T) {
var tests = []string{
"foo",
"foo<",
"<>",
"=>",
}
for _, test := range tests {
if _, err := ParseOperator(test); err == nil {
t.Fatalf(`invalid operator %q passed`, test)
}
}
}
func TestDependency(t *testing.T) {
var tests = []struct {
String string
Name string
Dependency
Output string
}{
{"perl", "perl", Dependency{}, ""},
{"perl<5.27", "perl", Dependency{LessThan, Version{Version: "5.27"}}, "<5.27"},
{"perl>=5.26", "perl", Dependency{MoreThanOrEqualTo, Version{Version: "5.26"}}, ">=5.26"},
{"python>3.4: perl is no good", "python", Dependency{MoreThan, Version{Version: "3.4"}}, ">3.4"},
}
for _, test := range tests {
t.Run(test.String, func(t *testing.T) {
name, dep, err := ParseDependency(test.String)
if err != nil {
t.Fatal(err)
}
if name != test.Name {
t.Fatalf(`expected name %q, got %q`, test.Name, name)
}
if cmp := Compare(test.Version, dep.Version); cmp != 0 {
t.Fatalf(`expected version %q, got %q (%d)`, test.Version, dep.Version, cmp)
}
if s := dep.String(); s != test.Output {
t.Fatalf(`expected string %q, got %q`, test.Output, s)
}
})
}
}

+ 166
- 0
version/version.go View File

@ -0,0 +1,166 @@
package version
import (
"fmt"
"math"
"regexp"
"strings"
"unicode"
)
// Version is an EVR (epoch, version, release) tuple
type Version struct {
Epoch string
Version string
Release string
}
func (v Version) String() string {
if v.Epoch != "" && v.Epoch != "0" {
return fmt.Sprintf("%s:%s-%s", v.Epoch, v.Version, v.Release)
}
if v.Release != "" {
return fmt.Sprintf("%s-%s", v.Version, v.Release)
}
return v.Version
}
// Parse parses an EVR (Epoch:Version-Release) version string.
func Parse(evr string) Version {
var epoch, version, release string
if i := strings.IndexByte(evr, ':'); i != -1 {
epoch, evr = evr[:i], evr[i+1:]
} else {
epoch = "0"
}
if i := strings.IndexByte(evr, '-'); i != -1 {
version, release = evr[:i], evr[i+1:]
} else {
version = evr
}
return Version{epoch, version, release}
}
// LessThan returns true if this version is less than the other version.
func (v Version) LessThan(other Version) bool {
return Compare(v, other) < 0
}
var alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)")
func rpmvercmp(a, b string) int {
// shortcut for equality
if a == b {
return 0
}
// get alpha/numeric segements
segsa := alphanumPattern.FindAllString(a, -1)
segsb := alphanumPattern.FindAllString(b, -1)
segs := int(math.Min(float64(len(segsa)), float64(len(segsb))))
// compare each segment
for i := 0; i < segs; i++ {
a := segsa[i]
b := segsb[i]
// compare tildes
if []rune(a)[0] == '~' || []rune(b)[0] == '~' {
if []rune(a)[0] != '~' {
return 1
}
if []rune(b)[0] != '~' {
return -1
}
}
if unicode.IsNumber([]rune(a)[0]) {
// numbers are always greater than alphas
if !unicode.IsNumber([]rune(b)[0]) {
// a is numeric, b is alpha
return 1
}
// trim leading zeros
a = strings.TrimLeft(a, "0")
b = strings.TrimLeft(b, "0")
// longest string wins without further comparison
if len(a) > len(b) {
return 1
} else if len(b) > len(a) {
return -1
}
} else if unicode.IsNumber([]rune(b)[0]) {
// a is alpha, b is numeric
return -1
}
// string compare
if a < b {
return -1
} else if a > b {
return 1
}
}
// segments were all the same but separators must have been different
if len(segsa) == len(segsb) {
return 0
}
// If there is a tilde in a segment past the min number of segments, find it.
if len(segsa) > segs && []rune(segsa[segs])[0] == '~' {
return -1
} else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' {
return 1
}
// whoever has the most segments wins
if len(segsa) > len(segsb) {
return 1
}
return -1
}
// Compare compares two versions
func Compare(a, b Version) int {
var r int
if r = rpmvercmp(a.Epoch, b.Epoch); r == 0 {
if r = rpmvercmp(a.Version, b.Version); r == 0 {
return rpmvercmp(a.Release, b.Release)
}
}
return r
}
// CompareString compares two version strings
func CompareString(a, b string) int {
if a == "" && b == "" || a == b {
return 0
} else if a == "" {
return -1
} else if b == "" {
return 1
}
av := Parse(a)
bv := Parse(b)
return Compare(av, bv)
}
func isalpha(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
func isdigit(b byte) bool {
return b >= '0' && b <= '9'
}
func isalnum(b byte) bool {
return isdigit(b) || isalpha(b)
}

+ 27
- 0
version/version_test.go View File

@ -0,0 +1,27 @@
package version
import "testing"
func TestVersion(t *testing.T) {
var tests = []struct {
A, B string
Cmp int
}{
{"", "", 0},
{"a", "a", 0},
{"a", "aa", -1},
{"b", "ab", 1},
{"1:1", "1", 1},
{"0:1", "1", 0},
{"1:0.1.2-3", "1.2.3-4", 1},
{"1.2.3-4", "1.2.3-3", 1},
{"", "a", -1},
{"a", "", 1},
}
for _, test := range tests {
c := CompareString(test.A, test.B)
if c != test.Cmp {
t.Fatalf(`expected %q <> %q to return %d, got %d`, test.A, test.B, test.Cmp, c)
}
}
}

Loading…
Cancel
Save