├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cmd ├── rpmdump │ └── main.go └── rpminfo │ └── main.go ├── dependency.go ├── dependency_test.go ├── doc.go ├── fileinfo.go ├── go.mod ├── go.sum ├── header.go ├── lead.go ├── package.go ├── package_test.go ├── signature.go ├── signature_test.go ├── tag.go ├── testdata ├── RPM-GPG-KEY-CentOS-2 ├── RPM-GPG-KEY-CentOS-3 ├── RPM-GPG-KEY-CentOS-4 ├── RPM-GPG-KEY-CentOS-5 ├── RPM-GPG-KEY-CentOS-6 ├── RPM-GPG-KEY-CentOS-7 ├── centos-release-3.1-1.i386.rpm ├── centos-release-4-0.1.i386.rpm ├── centos-release-4-0.1.x86_64.rpm ├── centos-release-5-0.0.el5.centos.2.i386.rpm ├── centos-release-5-0.0.el5.centos.2.x86_64.rpm ├── centos-release-6-0.el6.centos.5.i686.rpm ├── centos-release-6-0.el6.centos.5.x86_64.rpm ├── centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm ├── centos-release-as-2.1AS-4.noarch.rpm ├── epel-release-7-5.noarch.rpm ├── vercmp.json └── vercmp.py ├── util.go ├── util_test.go ├── version.go └── version_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/rpmdump/rpmdump 2 | cmd/rpminfo/rpminfo 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.17 5 | - 1.16 6 | - 1.15 7 | - 1.14 8 | - 1.13 9 | - 1.12 10 | - 1.11 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ryan Armstrong. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpm 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/cavaliergopher/rpm.svg)](https://pkg.go.dev/github.com/cavaliergopher/rpm) [![Build Status](https://app.travis-ci.com/cavaliergopher/rpm.svg?branch=main)](https://app.travis-ci.com/cavaliergopher/rpm) [![Go Report Card](https://goreportcard.com/badge/github.com/cavaliergopher/rpm)](https://goreportcard.com/report/github.com/cavaliergopher/rpm) 3 | 4 | Package rpm implements the rpm package file format. 5 | 6 | $ go get github.com/cavaliergopher/rpm 7 | 8 | See the [package documentation](https://pkg.go.dev/github.com/cavaliergopher/rpm) 9 | or the examples programs in `cmd/` to get started. 10 | 11 | ## Extracting rpm packages 12 | 13 | The following working example demonstrates how to extract files from an rpm 14 | package. In this example, only the cpio format and xz compression are supported 15 | which will cover most cases. 16 | 17 | Implementations should consider additional formats and compressions algorithms, 18 | as well as support for extracting irregular file types and configuring 19 | permissions, uids and guids, etc. 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "io" 26 | "log" 27 | "os" 28 | "path/filepath" 29 | 30 | "github.com/cavaliergopher/cpio" 31 | "github.com/cavaliergopher/rpm" 32 | "github.com/ulikunitz/xz" 33 | ) 34 | 35 | func ExtractRPM(name string) { 36 | // Open a package file for reading 37 | f, err := os.Open(name) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | defer f.Close() 42 | 43 | // Read the package headers 44 | pkg, err := rpm.Read(f) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | // Check the compression algorithm of the payload 50 | if compression := pkg.PayloadCompression(); compression != "xz" { 51 | log.Fatalf("Unsupported compression: %s", compression) 52 | } 53 | 54 | // Attach a reader to decompress the payload 55 | xzReader, err := xz.NewReader(f) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | // Check the archive format of the payload 61 | if format := pkg.PayloadFormat(); format != "cpio" { 62 | log.Fatalf("Unsupported payload format: %s", format) 63 | } 64 | 65 | // Attach a reader to unarchive each file in the payload 66 | cpioReader := cpio.NewReader(xzReader) 67 | for { 68 | // Move to the next file in the archive 69 | hdr, err := cpioReader.Next() 70 | if err == io.EOF { 71 | break // no more files 72 | } 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | // Skip directories and other irregular file types in this example 78 | if !hdr.Mode.IsRegular() { 79 | continue 80 | } 81 | 82 | // Create the target directory 83 | if dirName := filepath.Dir(hdr.Name); dirName != "" { 84 | if err := os.MkdirAll(dirName, 0o755); err != nil { 85 | log.Fatal(err) 86 | } 87 | } 88 | 89 | // Create and write the file 90 | outFile, err := os.Create(hdr.Name) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | if _, err := io.Copy(outFile, cpioReader); err != nil { 95 | outFile.Close() 96 | log.Fatal(err) 97 | } 98 | outFile.Close() 99 | } 100 | } 101 | ``` 102 | -------------------------------------------------------------------------------- /cmd/rpmdump/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | rpmdump displays all headers and tags in rpm packages as a YAML document. 3 | 4 | usage: rpmdump [package ...] 5 | 6 | Example: 7 | 8 | $ rpmdump golang-1.6.3-2.el7.x86_64.rpm 9 | --- 10 | - path: golang-1.6.3-2.el7.x86_64.rpm 11 | signature: 12 | version: 1 13 | tags: 14 | - tag: 1000 15 | type: INT32 16 | value: [13140] 17 | - tag: 1002 18 | type: BIN 19 | value: | 20 | 00000000 89 02 15 03 05 00 54 89 c9 9c 24 c6 a8 a7 f4 a8 |......T...$.....| 21 | 00000010 0e b5 01 08 a9 dd 0f fe 3b 48 ad dc 55 d5 ec a8 |........;H..U...| 22 | 00000020 a7 e3 64 13 4b ee 3c d4 fb 6e fa b4 c0 bf a9 57 |..d.K.<..n.....W| 23 | 00000030 b9 4a d9 ac 01 3c 34 3a c9 18 99 08 d8 c2 25 12 |.J...<4:......%.| 24 | 00000040 4c 7a 5e 4b 05 41 94 4d d4 85 8f 8a a5 12 60 67 |Lz^K.A.M......`g| 25 | 00000050 15 67 fd 8c 7a 26 a0 24 26 81 a2 d9 f0 c5 fb 8d |.g..z&.$&.......| 26 | 00000060 e3 47 15 f7 9f 81 cf 84 3d 40 a2 37 31 c7 e1 b0 |.G......=@.71...| 27 | 00000070 1e b7 3f 5e fd 9c 63 06 83 44 3e 84 39 f2 8e c6 |..?^..c..D>.9...| 28 | 00000080 a6 ae 47 47 83 0c 16 62 42 07 4d 94 fe 1d 9c f1 |..GG...bB.M.....| 29 | 00000090 24 cc 35 55 07 76 2f a7 9f e6 ed 94 39 7c 3f b6 |$.5U.v/.....9|?.| 30 | 000000a0 27 82 22 e9 83 79 6b 6e 74 ac 72 38 db ea 65 e4 |'."..yknt.r8..e.| 31 | 000000b0 14 78 cc bd 37 b5 ef 35 c0 17 04 3e 2c b6 f7 fd |.x..7..5...>,...| 32 | 000000c0 90 e5 12 1f 69 bd 1c 3e 31 83 cd 44 6b d1 c7 37 |....i..>1..Dk..7| 33 | 000000d0 b6 4a 5e 5d fa fa f2 04 c9 51 9a 56 26 8e fb 0e |.J^].....Q.V&...| 34 | 000000e0 2c b4 d3 f4 a4 10 39 97 d0 be 99 6d 24 00 6d 59 |,.....9....m$.mY| 35 | 000000f0 4e fc 58 0e 7c 8f 6f 88 a9 cb c2 03 80 37 b3 c0 |N.X.|.o......7..| 36 | 00000100 ab c0 46 e6 29 85 4b 8b 5b f5 18 de b4 c4 77 b7 |..F.).K.[.....w.| 37 | 00000110 61 43 5e 2c f2 f3 ea c6 1d f6 36 11 46 16 50 e2 |aC^,......6.F.P.| 38 | 00000120 b5 b3 9c 9f 81 2f f7 03 b3 f3 83 9d d4 53 48 07 |...../.......SH.| 39 | 00000130 49 cd 8d 3a f8 2d 24 50 db 69 3c 99 e0 37 4e 5d |I..:.-$P.i<..7N]| 40 | 00000140 38 19 96 69 ea d4 30 2f 4b 61 d6 69 8c ee 06 ee |8..i..0/Ka.i....| 41 | 00000150 ee 78 af 9a 34 70 0d b8 4a 86 30 a2 31 48 de 98 |.x..4p..J.0.1H..| 42 | 00000160 55 5b 57 cb 6b 4b 81 52 1a ab d5 2c 0e b0 e0 03 |U[W.kK.R...,....| 43 | 00000170 88 46 3e 9d 2d 71 98 27 8a 40 b3 81 4b 29 4f 98 |.F>.-q.'.@..K)O.| 44 | 00000180 9a ea b8 c9 ec 6d f6 09 15 62 8b c2 72 73 87 2d |.....m...b..rs.-| 45 | 00000190 8b af 52 de b0 a4 c8 5d 59 f3 5c 7a de 98 d0 7f |..R....]Y.\z....| 46 | 000001a0 a6 c6 96 89 ca 85 12 35 90 c5 fe 73 67 28 c1 65 |.......5...sg(.e| 47 | 000001b0 36 15 db ce 50 f4 fc 74 f8 77 92 6a 65 2a cf fe |6...P..t.w.je*..| 48 | 000001c0 eb 1e 22 81 fa 87 f2 32 fa fd 10 6d 23 86 92 c5 |.."....2...m#...| 49 | 000001d0 c8 25 a6 51 51 24 11 57 0c 4d bf 4d 38 e9 59 ff |.%.QQ$.W.M.M8.Y.| 50 | 000001e0 66 73 d5 5b 63 c6 89 1c b0 ba ec bd be d8 ff 51 |fs.[c..........Q| 51 | 000001f0 80 18 3d ea e3 b9 87 b1 d8 28 58 1e eb 6b ee 03 |..=......(X..k..| 52 | 00000200 5d 3d 37 9b 92 3f c1 58 55 8a c9 a9 34 46 3a df |]=7..?.XU...4F:.| 53 | 00000210 e3 3f c3 97 6f 21 37 ff |.?..o!7.........| 54 | - tag: 1004 55 | type: BIN 56 | value: [74 e3 cd 32 88 e6 9c 33 fb e4 75 ba df ac 0e 7c] 57 | - tag: 1007 58 | type: INT32 59 | value: [26088] 60 | - tag: 62 61 | type: BIN 62 | value: [00 00 00 3e 00 00 00 07 ff ff ff 90 00 00 00 10 63 | ... 64 | 65 | */ 66 | package main 67 | 68 | import ( 69 | "fmt" 70 | "math" 71 | "os" 72 | "strings" 73 | 74 | "github.com/cavaliergopher/rpm" 75 | ) 76 | 77 | func main() { 78 | if len(os.Args) < 2 || strings.HasPrefix(os.Args[1], "-") { 79 | os.Exit(usage(1)) 80 | } 81 | fmt.Printf("---\n") 82 | for i, name := range os.Args[1:] { 83 | if i > 0 { 84 | fmt.Printf("\n") 85 | } 86 | printPackage(name) 87 | } 88 | } 89 | 90 | func printPackage(name string) { 91 | fmt.Printf("- path: %v\n", name) 92 | p, err := rpm.Open(name) 93 | if err != nil { 94 | fmt.Printf(" error: %v\n", err) 95 | return 96 | } 97 | fmt.Printf(" signature:\n") 98 | printHeader(&p.Signature) 99 | fmt.Println() 100 | fmt.Printf(" header:\n") 101 | printHeader(&p.Header) 102 | } 103 | 104 | func printHeader(h *rpm.Header) { 105 | fmt.Printf(" version: %v\n", h.Version) 106 | fmt.Printf(" tags:\n") 107 | for _, tag := range h.Tags { 108 | fmt.Printf(" - tag: %v\n", tag.ID) 109 | fmt.Printf(" type: %v\n", tag.Type) 110 | switch tag.Value.(type) { 111 | case []string: 112 | ss := tag.Value.([]string) 113 | if len(ss) == 1 && !strings.Contains(ss[0], "\n") { 114 | fmt.Printf(" value: [\"%v\"]\n", ss[0]) 115 | } else { 116 | fmt.Printf(" value:\n") 117 | for _, s := range ss { 118 | if !strings.Contains(s, "\n") { 119 | fmt.Printf(" - \"%v\"\n", s) 120 | } else { 121 | fmt.Printf(" - |\n") 122 | lines := strings.Split(s, "\n") 123 | for _, line := range lines { 124 | fmt.Printf(" %v\n", line) 125 | } 126 | } 127 | } 128 | } 129 | 130 | case []byte: 131 | b := tag.Value.([]byte) 132 | if len(b) <= 16 { 133 | fmt.Print(" value: [") 134 | for i, x := range b { 135 | if i > 0 { 136 | fmt.Print(" ") 137 | } 138 | fmt.Printf("%02x", x) 139 | } 140 | fmt.Println("]") 141 | } else { 142 | fmt.Println(" value: |") 143 | for i := 0; i < len(b); i += 16 { 144 | fmt.Printf(" %08x ", i) 145 | l := int(math.Min(16, float64(len(b)-i))) 146 | for j := 0; j < l; j++ { 147 | fmt.Printf("%02x ", b[i+j]) 148 | if j == 7 { 149 | fmt.Print(" ") 150 | } 151 | } 152 | 153 | for j := 0; j < 16-l; j++ { 154 | fmt.Print(" ") 155 | } 156 | if l < 8 { 157 | fmt.Print(" ") 158 | } 159 | 160 | s := [16]byte{} 161 | copy(s[:], b[i:]) 162 | for j := 0; j < 16; j++ { 163 | // print '.' if char is not printable ascii 164 | if s[j] < 32 || s[j] > 126 { 165 | s[j] = 46 166 | } 167 | } 168 | fmt.Printf(" |%s|\n", s) 169 | } 170 | } 171 | 172 | default: 173 | fmt.Printf(" value: %v\n", tag.Value) 174 | } 175 | } 176 | } 177 | 178 | func usage(exitCode int) int { 179 | w := os.Stdout 180 | if exitCode != 0 { 181 | w = os.Stderr 182 | } 183 | 184 | fmt.Fprintf(w, "usage: %v [package ...]\n", os.Args[0]) 185 | return exitCode 186 | } 187 | -------------------------------------------------------------------------------- /cmd/rpminfo/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | rpminfo displays package information, akin to rpm --info. 3 | 4 | usage: rpminfo [package ...] 5 | 6 | Example: 7 | 8 | $ rpminfo golang-1.6.3-2.el7.x86_64.rpm 9 | Name : golang 10 | Version : 1.6.3 11 | Release : 2.el7 12 | Architecture: x86_64 13 | Group : Unspecified 14 | Size : 11809071 15 | License : BSD and Public Domain 16 | Signature : RSA/SHA256, Sun Nov 20 18:01:16 2016, Key ID 24c6a8a7f4a80eb5 17 | Source RPM : golang-1.6.3-2.el7.src.rpm 18 | Build Date : Tue Nov 15 12:20:30 2016 19 | Build Host : c1bm.rdu2.centos.org 20 | Packager : CentOS BuildSystem 21 | Vendor : CentOS 22 | URL : http://golang.org/ 23 | Summary : The Go Programming Language 24 | Description : 25 | The Go Programming Language. 26 | */ 27 | package main 28 | 29 | import ( 30 | "fmt" 31 | "os" 32 | "strings" 33 | "text/template" 34 | "time" 35 | 36 | "github.com/cavaliergopher/rpm" 37 | ) 38 | 39 | var tmpl = template.Must(template.New("rpminfo"). 40 | Funcs(template.FuncMap{ 41 | "join": func(a []string) string { 42 | return strings.Join(a, ", ") 43 | }, 44 | "strftime": func(t time.Time) string { 45 | return t.Format(rpm.TimeFormat) 46 | }, 47 | }). 48 | Parse(`Name : {{ .Name }} 49 | Version : {{ .Version }} 50 | Release : {{ .Release }} 51 | Architecture: {{ .Architecture }} 52 | Group : {{ .Groups | join }} 53 | Size : {{ .Size }} 54 | License : {{ .License }} 55 | Signature : {{ .GPGSignature }} 56 | Source RPM : {{ .SourceRPM }} 57 | Build Date : {{ strftime .BuildTime }} 58 | Build Host : {{ .BuildHost }} 59 | Packager : {{ .Packager }} 60 | Vendor : {{ .Vendor }} 61 | URL : {{ .URL }} 62 | Summary : {{ .Summary }} 63 | Description : 64 | {{ .Description }} 65 | `)) 66 | 67 | func main() { 68 | if len(os.Args) < 2 || strings.HasPrefix(os.Args[1], "-") { 69 | os.Exit(usage(1)) 70 | } 71 | for i, name := range os.Args[1:] { 72 | if i > 0 { 73 | fmt.Printf("\n") 74 | } 75 | p, err := rpm.Open(name) 76 | if err != nil { 77 | fmt.Fprintf(os.Stderr, "error reading %s: %v\n", name, err) 78 | continue 79 | } 80 | if err := tmpl.Execute(os.Stdout, p); err != nil { 81 | fmt.Fprintf(os.Stderr, "error formatting %s: %v\n", name, err) 82 | continue 83 | } 84 | } 85 | } 86 | 87 | func usage(exitCode int) int { 88 | w := os.Stdout 89 | if exitCode != 0 { 90 | w = os.Stderr 91 | } 92 | fmt.Fprintf(w, "usage: %v [path ...]\n", os.Args[0]) 93 | return exitCode 94 | } 95 | -------------------------------------------------------------------------------- /dependency.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Dependency flags indicate how versions comparisons should be computed when 8 | // comparing versions of dependent packages. 9 | const ( 10 | DepFlagAny = 0 11 | DepFlagLesser = (1 << 1) 12 | DepFlagGreater = (1 << 2) 13 | DepFlagEqual = (1 << 3) 14 | DepFlagLesserOrEqual = (DepFlagEqual | DepFlagLesser) 15 | DepFlagGreaterOrEqual = (DepFlagEqual | DepFlagGreater) 16 | DepFlagPrereq = (1 << 6) 17 | DepFlagScriptPre = (1 << 9) 18 | DepFlagScriptPost = (1 << 10) 19 | DepFlagScriptPreUn = (1 << 11) 20 | DepFlagScriptPostUn = (1 << 12) 21 | DepFlagRpmlib = (1 << 24) 22 | ) 23 | 24 | // See: https://github.com/rpm-software-management/rpm/blob/master/lib/rpmds.h#L25 25 | 26 | // Dependency is an interface which represents a relationship between two 27 | // packages. It might indicate that one package requires, conflicts with, 28 | // obsoletes or provides another package. 29 | // 30 | // Dependency implements the Version interface and so may be used when comparing 31 | // versions. 32 | type Dependency interface { 33 | Version // Version of the other package 34 | 35 | Name() string // Name of the other package 36 | Flags() int // See the DepFlag constants 37 | } 38 | 39 | // private basic implementation of a package dependency. 40 | type dependency struct { 41 | flags int 42 | name string 43 | epoch int 44 | version string 45 | release string 46 | } 47 | 48 | var _ Dependency = &dependency{} 49 | 50 | // Flags determines the nature of the package relationship and the comparison 51 | // used for the given version constraint. 52 | func (c *dependency) Flags() int { 53 | return c.flags 54 | } 55 | 56 | // Name is the name of the package target package. 57 | func (c *dependency) Name() string { 58 | return c.name 59 | } 60 | 61 | // Epoch is the epoch constraint of the target package. 62 | func (c *dependency) Epoch() int { 63 | return c.epoch 64 | } 65 | 66 | // Version is the version constraint of the target package. 67 | func (c *dependency) Version() string { 68 | return c.version 69 | } 70 | 71 | // Release is the release constraint of the target package. 72 | func (c *dependency) Release() string { 73 | return c.release 74 | } 75 | 76 | // String returns a string representation of a package dependency in a similar 77 | // format to `rpm -qR`. 78 | func (c *dependency) String() string { 79 | s := c.name 80 | switch { 81 | case DepFlagLesserOrEqual == (c.flags & DepFlagLesserOrEqual): 82 | s = fmt.Sprintf("%s <=", s) 83 | 84 | case DepFlagLesser == (c.flags & DepFlagLesser): 85 | s = fmt.Sprintf("%s <", s) 86 | 87 | case DepFlagGreaterOrEqual == (c.flags & DepFlagGreaterOrEqual): 88 | s = fmt.Sprintf("%s >=", s) 89 | 90 | case DepFlagGreater == (c.flags & DepFlagGreater): 91 | s = fmt.Sprintf("%s >", s) 92 | 93 | case DepFlagEqual == (c.flags & DepFlagEqual): 94 | s = fmt.Sprintf("%s =", s) 95 | } 96 | if c.version != "" { 97 | s = fmt.Sprintf("%s %s", s, c.version) 98 | } 99 | if c.release != "" { 100 | s = fmt.Sprintf("%s.%s", s, c.release) 101 | } 102 | return s 103 | } 104 | -------------------------------------------------------------------------------- /dependency_test.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type DepTest struct { 9 | dep Dependency 10 | str string 11 | } 12 | 13 | func TestDependencyStrings(t *testing.T) { 14 | tests := []DepTest{ 15 | {&dependency{DepFlagAny, "test", 0, "", ""}, "test"}, 16 | {&dependency{DepFlagAny, "test", 0, "1", ""}, "test 1"}, 17 | {&dependency{DepFlagAny, "test", 0, "1", "2"}, "test 1.2"}, 18 | {&dependency{DepFlagAny, "test", 1, "2", "3"}, "test 2.3"}, 19 | {&dependency{DepFlagLesser, "test", 0, "1", ""}, "test < 1"}, 20 | {&dependency{DepFlagLesser, "test", 0, "1", "2"}, "test < 1.2"}, 21 | {&dependency{DepFlagLesser, "test", 1, "2", "3"}, "test < 2.3"}, 22 | {&dependency{DepFlagLesserOrEqual, "test", 0, "1", ""}, "test <= 1"}, 23 | {&dependency{DepFlagLesserOrEqual, "test", 0, "1", "2"}, "test <= 1.2"}, 24 | {&dependency{DepFlagLesserOrEqual, "test", 1, "2", "3"}, "test <= 2.3"}, 25 | {&dependency{DepFlagGreaterOrEqual, "test", 0, "1", ""}, "test >= 1"}, 26 | {&dependency{DepFlagGreaterOrEqual, "test", 0, "1", "2"}, "test >= 1.2"}, 27 | {&dependency{DepFlagGreaterOrEqual, "test", 1, "2", "3"}, "test >= 2.3"}, 28 | {&dependency{DepFlagLesser, "test", 0, "1", ""}, "test < 1"}, 29 | {&dependency{DepFlagLesser, "test", 0, "1", "2"}, "test < 1.2"}, 30 | {&dependency{DepFlagLesser, "test", 1, "2", "3"}, "test < 2.3"}, 31 | } 32 | for i, test := range tests { 33 | if str := fmt.Sprintf("%v", test.dep); str != test.str { 34 | t.Errorf("Expected '%s' for test %d, got: '%s'", test.str, i+1, str) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package rpm implements the rpm package file format. 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/cavaliergopher/rpm" 11 | ) 12 | 13 | func main() { 14 | pkg, err := rpm.Open("golang-1.17.2-1.el7.x86_64.rpm") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | fmt.Println("Package:", pkg) 19 | fmt.Println("Summary:", pkg.Summary()) 20 | 21 | // Output: 22 | // Package: golang-1.17.2-1.el7.x86_64 23 | // Summary: The Go Programming Language 24 | } 25 | 26 | For more information about the rpm file format, see: 27 | 28 | http://ftp.rpm.org/max-rpm/s1-rpm-file-format-rpm-file-format.html 29 | 30 | Packages are composed of two headers: the Signature header and the "Header" 31 | header. Each contains key-value pairs called tags. Tags map an integer key to a 32 | value whose data type will be one of the TagType types. Tag values can be 33 | decoded with the appropriate Tag method for the data type. 34 | 35 | Many known tags are available as Package methods. For example, RPMTAG_NAME and 36 | RPMTAG_BUILDTIME are available as Package.Name and Package.BuildTime 37 | respectively. 38 | 39 | fmt.Println(pkg.Name(), pkg.BuildTime()) 40 | 41 | Tags can be retrieved and decoded from the Signature or Header headers directly 42 | using Header.GetTag and their tag identifier. 43 | 44 | const ( 45 | RPMTagName = 1000 46 | RPMTagBuidlTime = 1006 47 | ) 48 | 49 | fmt.Println( 50 | pkg.Header.GetTag(RPMTagName).String()), 51 | time.Unix(pkg.Header.GetTag(RPMTagBuildTime).Int64(), 0), 52 | ) 53 | 54 | Header.GetTag and all Tag methods will return a zero value if the header or the 55 | tag do not exist, or if the tag has a different data type. 56 | 57 | You may enumerate all tags in a header with Header.Tags: 58 | 59 | for id, tag := range pkg.Header.Tags { 60 | fmt.Println(id, tag.Type, tag.Value) 61 | } 62 | 63 | Comparing versions 64 | 65 | In the rpm ecosystem, package versions are compared using EVR; epoch, version, 66 | release. Versions may be compared using the Compare function. 67 | 68 | if rpm.Compare(pkgA, pkgB) == 1 { 69 | fmt.Println("A is more recent than B") 70 | } 71 | 72 | Packages may be be sorted using the PackageSlice type which implements 73 | sort.Interface. Packages are sorted lexically by name ascending and then by 74 | version descending. Version is evaluated first by epoch, then by version string, 75 | then by release. 76 | 77 | sort.Sort(PackageSlice(pkgs)) 78 | 79 | The Sort function is provided for your convenience. 80 | 81 | rpm.Sort(pkgs) 82 | 83 | Checksum validation 84 | 85 | Packages may be validated using MD5Check or GPGCheck. See the example for each 86 | function. 87 | 88 | Extracting files 89 | 90 | The payload of an rpm package is typically archived in cpio format and 91 | compressed with xz. To decompress and unarchive an rpm payload, the reader that 92 | read the rpm package headers will be positioned at the beginning of the payload 93 | and can be reused with the appropriate Go packages for the rpm payload format. 94 | 95 | You can check the archive format with Package.PayloadFormat and the compression 96 | algorithm with Package.PayloadCompression. 97 | 98 | For the cpio archive format, the following package is recommended: 99 | 100 | https://github.com/cavaliergopher/cpio 101 | 102 | For xz compression, the following package is recommended: 103 | 104 | https://github.com/ulikunitz/xz 105 | 106 | See README.md for a working example of extracting files from a cpio/xz rpm 107 | package using these packages. 108 | 109 | Example programs 110 | 111 | See cmd/rpmdump and cmd/rpminfo for example programs that emulate tools from the 112 | rpm ecosystem. 113 | */ 114 | package rpm 115 | -------------------------------------------------------------------------------- /fileinfo.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "os" 5 | "time" 6 | ) 7 | 8 | // File flags make up some attributes of files depending on how they were 9 | // specified in the rpmspec 10 | const ( 11 | FileFlagNone = 0 12 | FileFlagConfig = (1 << 0) // %%config 13 | FileFlagDoc = (1 << 1) // %%doc 14 | FileFlagIcon = (1 << 2) // %%donotuse 15 | FileFlagMissingOk = (1 << 3) // %%config(missingok) 16 | FileFlagNoReplace = (1 << 4) // %%config(noreplace) 17 | FileFlagGhost = (1 << 6) // %%ghost 18 | FileFlagLicense = (1 << 7) // %%license 19 | FileFlagReadme = (1 << 8) // %%readme 20 | FileFlagPubkey = (1 << 11) // %%pubkey 21 | FileFlagArtifact = (1 << 12) // %%artifact 22 | ) 23 | 24 | // A FileInfo describes a file in an rpm package. 25 | // 26 | // FileInfo implements the os.FileInfo interface. 27 | type FileInfo struct { 28 | name string 29 | size int64 30 | mode os.FileMode 31 | modTime time.Time 32 | flags int64 33 | owner string 34 | group string 35 | digest string 36 | linkname string 37 | } 38 | 39 | // compile-time check that rpm.FileInfo implements os.FileInfo interface 40 | var _ os.FileInfo = new(FileInfo) 41 | 42 | func (f *FileInfo) String() string { 43 | return f.Name() 44 | } 45 | 46 | // Name is the full path of a file in an rpm package. 47 | func (f *FileInfo) Name() string { 48 | return f.name 49 | } 50 | 51 | // Size is the size in bytes of a file in an rpm package. 52 | func (f *FileInfo) Size() int64 { 53 | return f.size 54 | } 55 | 56 | // Mode is the file mode in bits of a file in an rpm package. 57 | func (f *FileInfo) Mode() os.FileMode { 58 | return f.mode 59 | } 60 | 61 | // ModTime is the modification time of a file in an rpm package. 62 | func (f *FileInfo) ModTime() time.Time { 63 | return f.modTime 64 | } 65 | 66 | // IsDir returns true if a file is a directory in an rpm package. 67 | func (f *FileInfo) IsDir() bool { 68 | return f.mode.IsDir() 69 | } 70 | 71 | func (f *FileInfo) Flags() int64 { 72 | return f.flags 73 | } 74 | 75 | // Owner is the name of the owner of a file in an rpm package. 76 | func (f *FileInfo) Owner() string { 77 | return f.owner 78 | } 79 | 80 | // Group is the name of the owner group of a file in an rpm package. 81 | func (f *FileInfo) Group() string { 82 | return f.group 83 | } 84 | 85 | // Digest is the md5sum of a file in an rpm package. 86 | func (f *FileInfo) Digest() string { 87 | return f.digest 88 | } 89 | 90 | // Linkname is the link target of a link file in an rpm package. 91 | func (f *FileInfo) Linkname() string { 92 | return f.linkname 93 | } 94 | 95 | // Sys implements os.FileInfo and always returns nil. 96 | func (f *FileInfo) Sys() interface{} { 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cavaliergopher/rpm 2 | 3 | go 1.17 4 | 5 | require golang.org/x/crypto v0.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 4 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= 5 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 6 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 8 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 9 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 10 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 11 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 12 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 20 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 21 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 24 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 25 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 26 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 27 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 28 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 29 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 30 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | const r_MaxHeaderSize = 33554432 10 | 11 | // A Header stores metadata about an rpm package. 12 | type Header struct { 13 | Version int 14 | Tags map[int]*Tag 15 | Size int 16 | } 17 | 18 | // GetTag returns the tag with the given identifier. 19 | // 20 | // Nil is returned if the specified tag does not exist or the header is nil. 21 | func (c *Header) GetTag(id int) *Tag { 22 | if c == nil || len(c.Tags) == 0 { 23 | return nil 24 | } 25 | return c.Tags[id] 26 | } 27 | 28 | type rpmHeader [16]byte 29 | 30 | func (b rpmHeader) Magic() []byte { return b[:3] } 31 | func (b rpmHeader) Version() int { return int(b[3]) } 32 | func (b rpmHeader) IndexCount() int { return int(binary.BigEndian.Uint32(b[8:12])) } 33 | func (b rpmHeader) Size() int { return int(binary.BigEndian.Uint32(b[12:16])) } 34 | 35 | type rpmIndex [16]byte 36 | 37 | func (b rpmIndex) Tag() int { return int(binary.BigEndian.Uint32(b[:4])) } 38 | func (b rpmIndex) Type() TagType { return TagType(binary.BigEndian.Uint32(b[4:8])) } 39 | func (b rpmIndex) Offset() int { return int(binary.BigEndian.Uint32(b[8:12])) } 40 | func (b rpmIndex) ValueCount() int { return int(binary.BigEndian.Uint32(b[12:16])) } 41 | 42 | // readHeader reads an RPM package file header structure from r. 43 | func readHeader(r io.Reader, pad bool) (*Header, error) { 44 | // decode the header structure header 45 | var hdrBytes rpmHeader 46 | if _, err := r.Read(hdrBytes[:]); err != nil { 47 | return nil, err 48 | } 49 | if hdrBytes.Size() > r_MaxHeaderSize { 50 | return nil, errorf( 51 | "header size exceeds the maximum of %d: %d", 52 | r_MaxHeaderSize, 53 | hdrBytes.Size(), 54 | ) 55 | } 56 | if hdrBytes.IndexCount()*len(hdrBytes) > r_MaxHeaderSize { 57 | return nil, errorf( 58 | "header index size exceeds the maximum of %d: %d", 59 | r_MaxHeaderSize, 60 | hdrBytes.Size(), 61 | ) 62 | } 63 | 64 | // decode the index 65 | indexBytes := make([]rpmIndex, hdrBytes.IndexCount()) 66 | for i := 0; i < len(indexBytes); i++ { 67 | if _, err := r.Read(indexBytes[i][:]); err != nil { 68 | return nil, err 69 | } 70 | if indexBytes[i].Offset() >= hdrBytes.Size() { 71 | return nil, errorf( 72 | "offset of index %d is out of range: %s", 73 | i, 74 | indexBytes[i].Offset(), 75 | ) 76 | } 77 | } 78 | 79 | // decode the store 80 | tags := make(map[int]*Tag, len(indexBytes)) 81 | buf := make([]byte, hdrBytes.Size()) 82 | if _, err := io.ReadFull(r, buf); err != nil { 83 | return nil, err 84 | } 85 | for i, ix := range indexBytes { 86 | if ix.ValueCount() < 1 { 87 | return nil, errorf("invalid value count for index %d: %d", i, ix.ValueCount()) 88 | } 89 | o := ix.Offset() 90 | var v interface{} 91 | switch ix.Type() { 92 | case TagTypeBinary, TagTypeChar, TagTypeInt8: 93 | if o+ix.ValueCount() > len(buf) { 94 | switch ix.Type() { 95 | case TagTypeBinary: 96 | return nil, errorf("binary value for index %d is out of range", i+1) 97 | case TagTypeChar: 98 | return nil, errorf("uint8 value for index %d is out of range", i+1) 99 | case TagTypeInt8: 100 | return nil, errorf("int8 value for index %d is out of range", i+1) 101 | } 102 | return nil, errorf("value for index %d is out of range", i+1) 103 | } 104 | a := make([]byte, ix.ValueCount()) 105 | copy(a, buf[o:o+ix.ValueCount()]) 106 | v = a 107 | 108 | case TagTypeInt16: 109 | a := make([]int64, ix.ValueCount()) 110 | for v := 0; v < ix.ValueCount(); v++ { 111 | if o+2 > len(buf) { 112 | return nil, errorf("int16 value for index %d is out of range", i+1) 113 | } 114 | a[v] = int64(binary.BigEndian.Uint16(buf[o : o+2])) 115 | o += 2 116 | } 117 | v = a 118 | 119 | case TagTypeInt32: 120 | a := make([]int64, ix.ValueCount()) 121 | for v := 0; v < ix.ValueCount(); v++ { 122 | if o+4 > len(buf) { 123 | return nil, errorf("int32 value for index %d is out of range", i+1) 124 | } 125 | a[v] = int64(binary.BigEndian.Uint32(buf[o : o+4])) 126 | o += 4 127 | } 128 | 129 | v = a 130 | 131 | case TagTypeInt64: 132 | a := make([]int64, ix.ValueCount()) 133 | for v := 0; v < ix.ValueCount(); v++ { 134 | if o+8 > len(buf) { 135 | // TODO: better errors 136 | return nil, errorf("int64 value for index %d is out of range", i+1) 137 | } 138 | a[v] = int64(binary.BigEndian.Uint64(buf[o : o+8])) 139 | o += 8 140 | } 141 | v = a 142 | 143 | case TagTypeString, TagTypeStringArray, TagTypeI18NString: 144 | // allow at least one byte per string 145 | if o+ix.ValueCount() > len(buf) { 146 | return nil, errorf("[]string value for index %d is out of range", i+1) 147 | } 148 | a := make([]string, ix.ValueCount()) 149 | for s := 0; s < ix.ValueCount(); s++ { 150 | // calculate string length 151 | var j int 152 | for j = 0; (o+j) < len(buf) && buf[o+j] != 0; j++ { 153 | } 154 | if j == len(buf) { 155 | return nil, errorf("string value for index %d is out of range", i+1) 156 | } 157 | a[s] = string(buf[o : o+j]) 158 | o += j + 1 159 | } 160 | v = a 161 | 162 | case TagTypeNull: 163 | // nothing to do here 164 | 165 | default: 166 | // unknown data type 167 | return nil, errorf("unknown index data type: %0X", ix.Type()) 168 | } 169 | tags[ix.Tag()] = &Tag{ 170 | ID: ix.Tag(), 171 | Type: ix.Type(), 172 | Value: v, 173 | } 174 | } 175 | 176 | // pad to next header 177 | var padding int64 178 | if pad { 179 | if padding = int64(8-(hdrBytes.Size()%8)) % 8; padding != 0 { 180 | if _, err := io.CopyN(ioutil.Discard, r, padding); err != nil { 181 | return nil, err 182 | } 183 | } 184 | } 185 | 186 | return &Header{ 187 | Version: hdrBytes.Version(), 188 | Tags: tags, 189 | Size: 16 + hdrBytes.Size() + hdrBytes.IndexCount()*16 + int(padding), 190 | }, nil 191 | } 192 | -------------------------------------------------------------------------------- /lead.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | ) 8 | 9 | // ErrNotRPMFile indicates that the file is not an rpm package. 10 | var ErrNotRPMFile = errorf("invalid file descriptor") 11 | 12 | // Lead is the deprecated lead section of an rpm file which is used in legacy 13 | // rpm versions to store package metadata. 14 | type Lead struct { 15 | VersionMajor int 16 | VersionMinor int 17 | Name string 18 | Type int 19 | Architecture int 20 | OperatingSystem int 21 | SignatureType int 22 | } 23 | 24 | type leadBytes [96]byte 25 | 26 | func (c leadBytes) Magic() []byte { return c[:4] } 27 | func (c leadBytes) VersionMajor() int { return int(c[4]) } 28 | func (c leadBytes) VersionMinor() int { return int(c[5]) } 29 | func (c leadBytes) Type() int { return int(binary.BigEndian.Uint16(c[6:8])) } 30 | func (c leadBytes) Architecture() int { return int(binary.BigEndian.Uint16(c[8:10])) } 31 | func (c leadBytes) Name() string { return string(c[10:76]) } 32 | func (c leadBytes) OperatingSystem() int { return int(binary.BigEndian.Uint16(c[76:78])) } 33 | func (c leadBytes) SignatureType() int { return int(binary.BigEndian.Uint16(c[78:80])) } 34 | 35 | // readLead reads the deprecated lead section of an rpm package which is used in 36 | // legacy rpm versions to store package metadata. 37 | func readLead(r io.Reader) (*Lead, error) { 38 | var lead leadBytes 39 | _, err := io.ReadFull(r, lead[:]) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if !bytes.Equal(lead.Magic(), []byte{0xED, 0xAB, 0xEE, 0xDB}) { 44 | return nil, ErrNotRPMFile 45 | } 46 | if lead.VersionMajor() < 3 || lead.VersionMajor() > 4 { 47 | return nil, errorf("unsupported rpm version: %d", lead.VersionMajor()) 48 | } 49 | // TODO: validate lead value ranges 50 | return &Lead{ 51 | VersionMajor: lead.VersionMajor(), 52 | VersionMinor: lead.VersionMinor(), 53 | Type: lead.Type(), 54 | Architecture: lead.Architecture(), 55 | Name: lead.Name(), 56 | OperatingSystem: lead.OperatingSystem(), 57 | SignatureType: lead.SignatureType(), 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "sort" 9 | "strings" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | // A Package is an rpm package file. 15 | type Package struct { 16 | Lead Lead 17 | Signature Header 18 | Header Header 19 | } 20 | 21 | var _ Version = &Package{} 22 | 23 | // Read reads an rpm package from r. 24 | // 25 | // When this function returns, the reader will be positioned at the start of the 26 | // package payload. Use Package.PayloadFormat and Package.PayloadCompression to 27 | // determine how to decompress and unarchive the payload. 28 | func Read(r io.Reader) (*Package, error) { 29 | lead, err := readLead(r) 30 | if err != nil { 31 | return nil, err 32 | } 33 | sig, err := readHeader(r, true) 34 | if err != nil { 35 | return nil, err 36 | } 37 | hdr, err := readHeader(r, false) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return &Package{ 42 | Lead: *lead, 43 | Signature: *sig, 44 | Header: *hdr, 45 | }, nil 46 | } 47 | 48 | // Open opens an rpm package from the file system. 49 | // 50 | // Once the package headers are read, the underlying reader is closed and cannot 51 | // be used to read the package payload. To read the package payload, open the 52 | // package with os.Open and read the headers with Read. You may then use the 53 | // same reader to read the payload. 54 | func Open(name string) (*Package, error) { 55 | f, err := os.Open(name) 56 | if err != nil { 57 | return nil, err 58 | } 59 | defer f.Close() 60 | return Read(bufio.NewReader(f)) 61 | } 62 | 63 | // dependencies translates the given tag values into a slice of package 64 | // relationships such as provides, conflicts, obsoletes and requires. 65 | func (c *Package) dependencies(nevrsTagID, flagsTagID, namesTagID, versionsTagID int) []Dependency { 66 | // TODO: Implement NEVRS tags 67 | // TODO: error handling 68 | flgs := c.Header.GetTag(flagsTagID).Int64Slice() 69 | names := c.Header.GetTag(namesTagID).StringSlice() 70 | vers := c.Header.GetTag(versionsTagID).StringSlice() 71 | deps := make([]Dependency, len(names)) 72 | for i := 0; i < len(names); i++ { 73 | epoch, ver, rel := parseVersion(vers[i]) 74 | deps[i] = &dependency{ 75 | flags: int(flgs[i]), 76 | name: names[i], 77 | epoch: epoch, 78 | version: ver, 79 | release: rel, 80 | } 81 | } 82 | return deps 83 | } 84 | 85 | // String returns the package identifier in the form 86 | // '[name]-[version]-[release].[architecture]'. 87 | func (c *Package) String() string { 88 | return fmt.Sprintf("%s-%s-%s.%s", c.Name(), c.Version(), c.Release(), c.Architecture()) 89 | } 90 | 91 | func (c *Package) GPGSignature() GPGSignature { 92 | return c.Signature.GetTag(1002).Bytes() 93 | } 94 | 95 | // For tag definitions, see: 96 | // https://github.com/rpm-software-management/rpm/blob/master/include/rpm/rpmtag.h#L34 97 | 98 | func (c *Package) Name() string { 99 | return c.Header.GetTag(1000).String() 100 | } 101 | 102 | func (c *Package) Version() string { 103 | return c.Header.GetTag(1001).String() 104 | } 105 | 106 | func (c *Package) Release() string { 107 | return c.Header.GetTag(1002).String() 108 | } 109 | 110 | func (c *Package) Epoch() int { 111 | return int(c.Header.GetTag(1003).Int64()) 112 | } 113 | 114 | // HeaderRange returns the byte offsets of the RPM header. 115 | func (c *Package) HeaderRange() (start, end int) { 116 | start = 96 + c.Signature.Size 117 | end = start + c.Header.Size 118 | return start, end 119 | } 120 | 121 | func (c *Package) Requires() []Dependency { 122 | return c.dependencies(5041, 1048, 1049, 1050) 123 | } 124 | 125 | func (c *Package) Provides() []Dependency { 126 | return c.dependencies(5042, 1112, 1047, 1113) 127 | } 128 | 129 | func (c *Package) Conflicts() []Dependency { 130 | return c.dependencies(5044, 1053, 1054, 1055) 131 | } 132 | 133 | func (c *Package) Obsoletes() []Dependency { 134 | return c.dependencies(5043, 1114, 1090, 1115) 135 | } 136 | 137 | func (c *Package) Suggests() []Dependency { 138 | return c.dependencies(5059, 5051, 5049, 5050) 139 | } 140 | 141 | func (c *Package) Enhances() []Dependency { 142 | return c.dependencies(5061, 5057, 5055, 5056) 143 | } 144 | 145 | func (c *Package) Recommends() []Dependency { 146 | return c.dependencies(5058, 5048, 5046, 5047) 147 | } 148 | 149 | func (c *Package) Supplements() []Dependency { 150 | return c.dependencies(5060, 5051, 5052, 5053) 151 | } 152 | 153 | // Files returns file information for each file that is installed by this RPM 154 | // package. 155 | func (c *Package) Files() []FileInfo { 156 | ixs := c.Header.GetTag(1116).Int64Slice() 157 | names := c.Header.GetTag(1117).StringSlice() 158 | dirs := c.Header.GetTag(1118).StringSlice() 159 | modes := c.Header.GetTag(1030).Int64Slice() 160 | sizes := c.Header.GetTag(1028).Int64Slice() 161 | times := c.Header.GetTag(1034).Int64Slice() 162 | flags := c.Header.GetTag(1037).Int64Slice() 163 | owners := c.Header.GetTag(1039).StringSlice() 164 | groups := c.Header.GetTag(1040).StringSlice() 165 | digests := c.Header.GetTag(1035).StringSlice() 166 | linknames := c.Header.GetTag(1036).StringSlice() 167 | a := make([]FileInfo, len(names)) 168 | for i := 0; i < len(names); i++ { 169 | a[i] = FileInfo{ 170 | name: dirs[ixs[i]] + names[i], 171 | mode: fileModeFromInt64(modes[i]), 172 | size: sizes[i], 173 | modTime: time.Unix(times[i], 0), 174 | flags: flags[i], 175 | owner: owners[i], 176 | group: groups[i], 177 | digest: digests[i], 178 | linkname: linknames[i], 179 | } 180 | } 181 | return a 182 | } 183 | 184 | // fileModeFromInt64 converts the 16 bit value returned from a typical 185 | // unix/linux stat call to the bitmask that go uses to produce an os 186 | // neutral representation. It is incorrect to just cast the 16 bit 187 | // value directly to a os.FileMode. The result of stat is 4 bits to 188 | // specify the type of the object, this is a value in the range 0 to 189 | // 15, rather than a bitfield, 3 bits to note suid, sgid and sticky, 190 | // and 3 sets of 3 bits for rwx permissions for user, group and other. 191 | // An os.FileMode has the same 9 bits for permissions, but rather than 192 | // using an enum for the type it has individual bits. As a concrete 193 | // example, a block device has the 1<<26 bit set (os.ModeDevice) in 194 | // the os.FileMode, but has type 0x6000 (syscall.S_IFBLK). A regular 195 | // file is represented in os.FileMode by not having any of the bits in 196 | // os.ModeType set (i.e. is not a directory, is not a symlink, is not 197 | // a named pipe...) whilst a regular file has value syscall.S_IFREG 198 | // (0x8000) in the mode field from stat. 199 | func fileModeFromInt64(mode int64) os.FileMode { 200 | fm := os.FileMode(mode & 0777) 201 | switch mode & syscall.S_IFMT { 202 | case syscall.S_IFBLK: 203 | fm |= os.ModeDevice 204 | case syscall.S_IFCHR: 205 | fm |= os.ModeDevice | os.ModeCharDevice 206 | case syscall.S_IFDIR: 207 | fm |= os.ModeDir 208 | case syscall.S_IFIFO: 209 | fm |= os.ModeNamedPipe 210 | case syscall.S_IFLNK: 211 | fm |= os.ModeSymlink 212 | case syscall.S_IFREG: 213 | // nothing to do 214 | case syscall.S_IFSOCK: 215 | fm |= os.ModeSocket 216 | } 217 | if mode&syscall.S_ISGID != 0 { 218 | fm |= os.ModeSetgid 219 | } 220 | if mode&syscall.S_ISUID != 0 { 221 | fm |= os.ModeSetuid 222 | } 223 | if mode&syscall.S_ISVTX != 0 { 224 | fm |= os.ModeSticky 225 | } 226 | return fm 227 | } 228 | 229 | func (c *Package) Summary() string { 230 | return strings.Join(c.Header.GetTag(1004).StringSlice(), "\n") 231 | } 232 | 233 | func (c *Package) Description() string { 234 | return strings.Join(c.Header.GetTag(1005).StringSlice(), "\n") 235 | } 236 | 237 | func (c *Package) BuildTime() time.Time { 238 | return time.Unix(c.Header.GetTag(1006).Int64(), 0) 239 | } 240 | 241 | func (c *Package) BuildHost() string { 242 | return c.Header.GetTag(1007).String() 243 | } 244 | 245 | func (c *Package) InstallTime() time.Time { 246 | return time.Unix(c.Header.GetTag(1008).Int64(), 0) 247 | } 248 | 249 | // Size specifies the disk space consumed by installation of the package. 250 | func (c *Package) Size() uint64 { 251 | if i := uint64(c.Header.GetTag(5009).Int64()); i > 0 { 252 | return i 253 | } 254 | return uint64(c.Header.GetTag(1009).Int64()) 255 | } 256 | 257 | // ArchiveSize specifies the size of the archived payload of the package in 258 | // bytes. 259 | func (c *Package) ArchiveSize() uint64 { 260 | if i := uint64(c.Signature.GetTag(271).Int64()); i > 0 { 261 | return i 262 | } 263 | if i := uint64(c.Signature.GetTag(1007).Int64()); i > 0 { 264 | return i 265 | } 266 | if i := uint64(c.Header.GetTag(271).Int64()); i > 0 { 267 | return i 268 | } 269 | return uint64(c.Header.GetTag(1046).Int64()) 270 | } 271 | 272 | func (c *Package) Distribution() string { 273 | return c.Header.GetTag(1010).String() 274 | } 275 | 276 | func (c *Package) Vendor() string { 277 | return c.Header.GetTag(1011).String() 278 | } 279 | 280 | func (c *Package) GIFImage() []byte { 281 | return c.Header.GetTag(1012).Bytes() 282 | } 283 | 284 | func (c *Package) XPMImage() []byte { 285 | return c.Header.GetTag(1013).Bytes() 286 | } 287 | 288 | func (c *Package) License() string { 289 | return c.Header.GetTag(1014).String() 290 | } 291 | 292 | func (c *Package) Packager() string { 293 | return c.Header.GetTag(1015).String() 294 | } 295 | 296 | func (c *Package) Groups() []string { 297 | return c.Header.GetTag(1016).StringSlice() 298 | } 299 | 300 | func (c *Package) ChangeLog() []string { 301 | return c.Header.GetTag(1017).StringSlice() 302 | } 303 | 304 | func (c *Package) Source() []string { 305 | return c.Header.GetTag(1018).StringSlice() 306 | } 307 | 308 | func (c *Package) Patch() []string { 309 | return c.Header.GetTag(1019).StringSlice() 310 | } 311 | 312 | func (c *Package) URL() string { 313 | return c.Header.GetTag(1020).String() 314 | } 315 | 316 | func (c *Package) OperatingSystem() string { 317 | return c.Header.GetTag(1021).String() 318 | } 319 | 320 | func (c *Package) Architecture() string { 321 | return c.Header.GetTag(1022).String() 322 | } 323 | 324 | func (c *Package) PreInstallScript() string { 325 | return c.Header.GetTag(1023).String() 326 | } 327 | 328 | func (c *Package) PostInstallScript() string { 329 | return c.Header.GetTag(1024).String() 330 | } 331 | 332 | func (c *Package) PreUninstallScript() string { 333 | return c.Header.GetTag(1025).String() 334 | } 335 | 336 | func (c *Package) PostUninstallScript() string { 337 | return c.Header.GetTag(1026).String() 338 | } 339 | 340 | func (c *Package) OldFilenames() []string { 341 | return c.Header.GetTag(1027).StringSlice() 342 | } 343 | 344 | func (c *Package) Icon() []byte { 345 | return c.Header.GetTag(1043).Bytes() 346 | } 347 | 348 | func (c *Package) SourceRPM() string { 349 | return c.Header.GetTag(1044).String() 350 | } 351 | 352 | func (c *Package) RPMVersion() string { 353 | return c.Header.GetTag(1064).String() 354 | } 355 | 356 | func (c *Package) Platform() string { 357 | return c.Header.GetTag(1132).String() 358 | } 359 | 360 | // PayloadFormat returns the name of the format used for the package payload. 361 | // Typically cpio. 362 | func (c *Package) PayloadFormat() string { 363 | return c.Header.GetTag(1124).String() 364 | } 365 | 366 | // PayloadCompression returns the name of the compression used for the package 367 | // payload. Typically xz. 368 | func (c *Package) PayloadCompression() string { 369 | return c.Header.GetTag(1125).String() 370 | } 371 | 372 | // Sort sorts a slice of packages lexically by name ascending and then by 373 | // version descending. Version is evaluated first by epoch, then by version 374 | // string, then by release. 375 | func Sort(x []*Package) { sort.Sort(PackageSlice(x)) } 376 | 377 | // PackageSlice implements sort.Interface for a slice of packages. Packages are 378 | // sorted lexically by name ascending and then by version descending. Version is 379 | // evaluated first by epoch, then by version string, then by release. 380 | type PackageSlice []*Package 381 | 382 | // Sort is a convenience method: x.Sort() calls sort.Sort(x). 383 | func (x PackageSlice) Sort() { sort.Sort(x) } 384 | 385 | func (x PackageSlice) Len() int { return len(x) } 386 | 387 | func (x PackageSlice) Less(i, j int) bool { 388 | a, b := x[i].Name(), x[j].Name() 389 | if a == b { 390 | return Compare(x[i], x[j]) == 1 391 | } 392 | return a < b 393 | } 394 | 395 | func (x PackageSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 396 | 397 | var _ sort.Interface = PackageSlice{} 398 | -------------------------------------------------------------------------------- /package_test.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "hash/crc32" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func getTestFiles() map[string][]byte { 17 | path := os.Getenv("RPM_DIR") 18 | if path == "" { 19 | path = "./testdata" 20 | } 21 | dir, err := ioutil.ReadDir(path) 22 | if err != nil { 23 | panic(err) 24 | } 25 | files := make([]string, 0) 26 | for _, f := range dir { 27 | if strings.HasSuffix(f.Name(), ".rpm") { 28 | files = append(files, filepath.Join(path, f.Name())) 29 | } 30 | } 31 | if len(files) == 0 { 32 | panic("No rpm packages found for testing") 33 | } 34 | testFiles := make(map[string][]byte, len(files)) 35 | for _, filename := range files { 36 | b, err := ioutil.ReadFile(filename) 37 | if err != nil { 38 | panic(err) 39 | } 40 | testFiles[filename] = b 41 | } 42 | return testFiles 43 | } 44 | 45 | func openPackage(name string) *Package { 46 | p, err := Open(name) 47 | if err != nil { 48 | panic(err) 49 | } 50 | return p 51 | } 52 | 53 | func openPackages(path string) []*Package { 54 | dir, err := ioutil.ReadDir(path) 55 | if err != nil { 56 | panic(err) 57 | } 58 | files := make([]string, 0) 59 | for _, f := range dir { 60 | if strings.HasSuffix(f.Name(), ".rpm") { 61 | files = append(files, filepath.Join(path, f.Name())) 62 | } 63 | } 64 | packages := make([]*Package, len(files)) 65 | for i, f := range files { 66 | p := openPackage(f) 67 | packages[i] = p 68 | } 69 | return packages 70 | } 71 | 72 | func TestReadRPMFile(t *testing.T) { 73 | // load package file paths 74 | files := getTestFiles() 75 | 76 | valid := 0 77 | for path, b := range files { 78 | // Load package info 79 | rpm, err := Read(bytes.NewReader(b)) 80 | if err != nil { 81 | t.Errorf("Error loading rpm file %s: %s", path, err) 82 | } else { 83 | t.Logf("Loaded package: %v", rpm) 84 | valid++ 85 | } 86 | } 87 | 88 | t.Logf("Validated %d rpm files", valid) 89 | } 90 | 91 | func TestPackageFiles(t *testing.T) { 92 | names := []string{ 93 | "/etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7", 94 | "/etc/yum.repos.d/epel-testing.repo", 95 | "/etc/yum.repos.d/epel.repo", 96 | "/usr/lib/rpm/macros.d/macros.epel", 97 | "/usr/lib/systemd/system-preset/90-epel.preset", 98 | "/usr/share/doc/epel-release-7", 99 | "/usr/share/doc/epel-release-7/GPL", 100 | } 101 | modes := []int64{0644, 0644, 0644, 0644, 0644, 0755, 0644} 102 | sizes := []int64{1662, 1056, 957, 41, 2813, 4096, 18385} 103 | owners := []string{"root", "root", "root", "root", "root", "root", "root"} 104 | groups := []string{"root", "root", "root", "root", "root", "root", "root"} 105 | modtimes := []time.Time{ 106 | time.Unix(1416932629, 0), 107 | time.Unix(1416932629, 0), 108 | time.Unix(1416932629, 0), 109 | time.Unix(1416932629, 0), 110 | time.Unix(1416932629, 0), 111 | time.Unix(1416932778, 0), 112 | time.Unix(1416932629, 0), 113 | } 114 | digests := []string{ 115 | "028b9accc59bab1d21f2f3f544df5469910581e728a64fd8c411a725a82300c2", 116 | "d9662befdbfb661b20b3af4a7feb34c6f58b4dc689bbeb0f29c73438015701b9", 117 | "87d225d205a6263509508ac5cd4ca1bf1dc3e87960c9d305b3eb6c560f270297", 118 | "6a43fe82450861a67ab673151972515069fe7fab44679f60345c826ac37e3e08", 119 | "3de82a16cbc9eba0aa7c7edd7ef5e326a081afc8325aaf21ad11a68698b6b1d0", 120 | "", // digests field only populated for regular files 121 | "03a55cfbbbfcdfc75fed8aeca5383fef12de4f019d5ff15c58f1e6581465007e", 122 | } 123 | // the test RPM has no links 124 | linknames := []string{"", "", "", "", "", "", ""} 125 | path := "./testdata/epel-release-7-5.noarch.rpm" 126 | p := openPackage(path) 127 | files := p.Files() 128 | if len(files) != len(names) { 129 | t.Fatalf("expected %v files in rpm package but got %v", len(names), len(files)) 130 | } 131 | for i, fi := range files { 132 | name := fi.Name() 133 | if name != names[i] { 134 | t.Errorf("expected file %v with name %v but got %v", i, names[i], name) 135 | continue 136 | } 137 | if mode := int64(fi.Mode().Perm()); mode != modes[i] { 138 | t.Errorf("expected mode %v but got %v for %v", modes[i], mode, name) 139 | } 140 | if size := fi.Size(); size != sizes[i] { 141 | t.Errorf("expected size %v but got %v for %v", sizes[i], size, name) 142 | } 143 | if owner := fi.Owner(); owner != owners[i] { 144 | t.Errorf("expected owner %v but got %v for %v", owners[i], owner, name) 145 | } 146 | if group := fi.Group(); group != groups[i] { 147 | t.Errorf("expected group %v but got %v for %v", groups[i], group, name) 148 | } 149 | if modtime := fi.ModTime(); modtime != modtimes[i] { 150 | t.Errorf("expected modtime %v but got %v for %v", modtimes[i], modtime.Unix(), name) 151 | } 152 | if digest := fi.Digest(); digest != digests[i] { 153 | t.Errorf("expected digest %v but got %v for %v", digests[i], digest, name) 154 | } 155 | if linkname := fi.Linkname(); linkname != linknames[i] { 156 | t.Errorf("expected linkname %v but got %v for %v", linknames[i], linkname, name) 157 | } 158 | } 159 | } 160 | 161 | func TestByteTags(t *testing.T) { 162 | tests := []struct { 163 | Path string 164 | GPGSignatureCRC uint32 165 | }{ 166 | { 167 | Path: "testdata/centos-release-6-0.el6.centos.5.i686.rpm", 168 | GPGSignatureCRC: 1788312322, 169 | }, 170 | { 171 | Path: "testdata/centos-release-6-0.el6.centos.5.x86_64.rpm", 172 | GPGSignatureCRC: 3194808352, 173 | }, 174 | { 175 | Path: "testdata/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm", 176 | GPGSignatureCRC: 3466078337, 177 | }, 178 | { 179 | Path: "testdata/epel-release-7-5.noarch.rpm", 180 | GPGSignatureCRC: 2817187108, 181 | }, 182 | } 183 | for _, test := range tests { 184 | p := openPackage(test.Path) 185 | if crc := crc32.ChecksumIEEE(p.GPGSignature()); crc != test.GPGSignatureCRC { 186 | t.Errorf("expected GPG Signature CRC %v, got %v for %v", test.GPGSignatureCRC, crc, test.Path) 187 | } 188 | } 189 | } 190 | 191 | // Lists all the files in an rpm package. 192 | func ExamplePackage_Files() { 193 | // open a package file 194 | pkg, err := Open("./testdata/epel-release-7-5.noarch.rpm") 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | 199 | // list each file 200 | files := pkg.Files() 201 | fmt.Printf("total %v\n", len(files)) 202 | for _, fi := range files { 203 | fmt.Printf("%v %v %v %5v %v %v\n", 204 | fi.Mode().Perm(), 205 | fi.Owner(), 206 | fi.Group(), 207 | fi.Size(), 208 | fi.ModTime().UTC().Format("Jan 02 15:04"), 209 | fi.Name()) 210 | } 211 | 212 | // Output: 213 | // total 7 214 | // -rw-r--r-- root root 1662 Nov 25 16:23 /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 215 | // -rw-r--r-- root root 1056 Nov 25 16:23 /etc/yum.repos.d/epel-testing.repo 216 | // -rw-r--r-- root root 957 Nov 25 16:23 /etc/yum.repos.d/epel.repo 217 | // -rw-r--r-- root root 41 Nov 25 16:23 /usr/lib/rpm/macros.d/macros.epel 218 | // -rw-r--r-- root root 2813 Nov 25 16:23 /usr/lib/systemd/system-preset/90-epel.preset 219 | // -rwxr-xr-x root root 4096 Nov 25 16:26 /usr/share/doc/epel-release-7 220 | // -rw-r--r-- root root 18385 Nov 25 16:23 /usr/share/doc/epel-release-7/GPL 221 | } 222 | -------------------------------------------------------------------------------- /signature.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "golang.org/x/crypto/openpgp" 11 | "golang.org/x/crypto/openpgp/armor" 12 | "golang.org/x/crypto/openpgp/errors" 13 | "golang.org/x/crypto/openpgp/packet" 14 | ) 15 | 16 | var ( 17 | // ErrMD5CheckFailed indicates that an rpm package failed MD5 checksum 18 | // validation. 19 | ErrMD5CheckFailed = fmt.Errorf("MD5 checksum validation failed") 20 | 21 | // ErrGPGCheckFailed indicates that an rpm package failed GPG signature 22 | // validation. 23 | ErrGPGCheckFailed = fmt.Errorf("GPG signature validation failed") 24 | ) 25 | 26 | // in order of precedence 27 | var gpgTags = []int{ 28 | 1002, // RPMSIGTAG_PGP 29 | 1006, // RPMSIGTAG_PGP5 30 | 1005, // RPMSIGTAG_GPG 31 | } 32 | 33 | // see: https://github.com/rpm-software-management/rpm/blob/3b1f4b0c6c9407b08620a5756ce422df10f6bd1a/rpmio/rpmpgp.c#L51 34 | var gpgPubkeyTbl = map[packet.PublicKeyAlgorithm]string{ 35 | packet.PubKeyAlgoRSA: "RSA", 36 | packet.PubKeyAlgoRSASignOnly: "RSA(Sign-Only)", 37 | packet.PubKeyAlgoRSAEncryptOnly: "RSA(Encrypt-Only)", 38 | packet.PubKeyAlgoElGamal: "Elgamal", 39 | packet.PubKeyAlgoDSA: "DSA", 40 | packet.PubKeyAlgoECDH: "Elliptic Curve", 41 | packet.PubKeyAlgoECDSA: "ECDSA", 42 | } 43 | 44 | // Map Go hashes to rpm info name 45 | // See: https://golang.org/src/crypto/crypto.go?s=#L23 46 | // https://github.com/rpm-software-management/rpm/blob/3b1f4b0c6c9407b08620a5756ce422df10f6bd1a/rpmio/rpmpgp.c#L88 47 | var gpgHashTbl = []string{ 48 | "Unknown hash algorithm", 49 | "MD4", 50 | "MD5", 51 | "SHA1", 52 | "SHA224", 53 | "SHA256", 54 | "SHA384", 55 | "SHA512", 56 | "MD5SHA1", 57 | "RIPEMD160", 58 | "SHA3_224", 59 | "SHA3_256", 60 | "SHA3_384", 61 | "SHA3_512", 62 | "SHA512_224", 63 | "SHA512_256", 64 | } 65 | 66 | // GPGSignature is the raw byte representation of a package's signature. 67 | type GPGSignature []byte 68 | 69 | func (b GPGSignature) String() string { 70 | pkt, err := packet.Read(bytes.NewReader(b)) 71 | if err != nil { 72 | return "" 73 | } 74 | switch sig := pkt.(type) { 75 | case *packet.SignatureV3: 76 | algo, ok := gpgPubkeyTbl[sig.PubKeyAlgo] 77 | if !ok { 78 | algo = "Unknown public key algorithm" 79 | } 80 | hasher := gpgHashTbl[0] 81 | if int(sig.Hash) < len(gpgHashTbl) { 82 | hasher = gpgHashTbl[sig.Hash] 83 | } 84 | ctime := sig.CreationTime.UTC().Format(TimeFormat) 85 | return fmt.Sprintf("%v/%v, %v, Key ID %x", algo, hasher, ctime, sig.IssuerKeyId) 86 | } 87 | return "" 88 | } 89 | 90 | // readSigHeader reads the lead and signature header of a rpm package and stops 91 | // the reader at the beginning of the header header. 92 | func readSigHeader(r io.Reader) (*Header, error) { 93 | lead, err := readLead(r) 94 | if err != nil { 95 | return nil, err 96 | } 97 | if lead.SignatureType != 5 { // RPMSIGTYPE_HEADERSIG 98 | return nil, errorf("unknown signature type: %x", lead.SignatureType) 99 | } 100 | sig, err := readHeader(r, true) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return sig, nil 105 | } 106 | 107 | // GPGCheck validates the integrity of an rpm package file. Public keys in the 108 | // given keyring are used to validate the package signature. 109 | // 110 | // If validation fails, ErrGPGCheckFailed is returned. 111 | func GPGCheck(r io.Reader, keyring openpgp.KeyRing) (string, error) { 112 | sig, err := readSigHeader(r) 113 | if err != nil { 114 | return "", err 115 | } 116 | var sigval []byte 117 | for _, tag := range gpgTags { 118 | if sigval = sig.GetTag(tag).Bytes(); sigval != nil { 119 | break 120 | } 121 | } 122 | if sigval == nil { 123 | return "", errorf("package signature not found") 124 | } 125 | signer, err := openpgp.CheckDetachedSignature(keyring, r, bytes.NewReader(sigval)) 126 | if err == errors.ErrUnknownIssuer { 127 | return "", ErrGPGCheckFailed 128 | } else if err != nil { 129 | return "", err 130 | } 131 | for id := range signer.Identities { 132 | return id, nil 133 | } 134 | return "", errorf("no identity found in public key") 135 | } 136 | 137 | // MD5Check validates the integrity of an rpm package file. The MD5 checksum is 138 | // computed for the package payload and compared with the checksum specified in 139 | // the package header. 140 | // 141 | // If validation fails, ErrMD5CheckFailed is returned. 142 | func MD5Check(r io.Reader) error { 143 | sigheader, err := readSigHeader(r) 144 | if err != nil { 145 | return err 146 | } 147 | payloadSize := sigheader.GetTag(270).Int64() // RPMSIGTAG_LONGSIGSIZE 148 | if payloadSize == 0 { 149 | payloadSize = sigheader.GetTag(1000).Int64() // RPMSIGTAG_SIGSIZE 150 | if payloadSize == 0 { 151 | return fmt.Errorf("tag not found: RPMSIGTAG_SIZE") 152 | } 153 | } 154 | expect := sigheader.GetTag(1004).Bytes() // RPMSIGTAG_MD5 155 | if expect == nil { 156 | return errorf("tag not found: RPMSIGTAG_MD5") 157 | } 158 | h := md5.New() 159 | if n, err := io.Copy(h, r); err != nil { 160 | return err 161 | } else if n != payloadSize { 162 | return ErrMD5CheckFailed 163 | } 164 | actual := h.Sum(nil) 165 | if !bytes.Equal(expect, actual) { 166 | return ErrMD5CheckFailed 167 | } 168 | return nil 169 | } 170 | 171 | // ReadKeyRing reads a openpgp.KeyRing from the given io.Reader which may then 172 | // be used to validate GPG keys in rpm packages. 173 | func ReadKeyRing(r io.Reader) (openpgp.KeyRing, error) { 174 | // decode gpgkey file 175 | p, err := armor.Decode(r) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | // extract keys 181 | return openpgp.ReadKeyRing(p.Body) 182 | } 183 | 184 | func openKeyRing(name string) (openpgp.KeyRing, error) { 185 | f, err := os.Open(name) 186 | if err != nil { 187 | return nil, err 188 | } 189 | defer f.Close() 190 | return ReadKeyRing(f) 191 | } 192 | 193 | // KeyRingFromFiles reads a openpgp.KeyRing from the given file paths which may 194 | // then be used to validate GPG keys in rpm packages. 195 | // 196 | // This function might typically be used to read all keys in /etc/pki/rpm-gpg. 197 | func OpenKeyRing(name ...string) (openpgp.KeyRing, error) { 198 | entityList := make(openpgp.EntityList, 0) 199 | for _, path := range name { 200 | keyring, err := openKeyRing(path) 201 | if err != nil { 202 | return nil, err 203 | } 204 | entityList = append(entityList, keyring.(openpgp.EntityList)...) 205 | } 206 | return entityList, nil 207 | } 208 | -------------------------------------------------------------------------------- /signature_test.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestMD5Check(t *testing.T) { 15 | files := getTestFiles() 16 | 17 | valid := 0 18 | for filename, b := range files { 19 | if err := MD5Check(bytes.NewReader(b)); err != nil { 20 | t.Errorf("Validation error for %s: %v", filename, err) 21 | } else { 22 | valid++ 23 | } 24 | } 25 | 26 | t.Logf("Validated MD5 checksum for %d packages", valid) 27 | } 28 | 29 | func TestGPGCheck(t *testing.T) { 30 | // read testdata directory 31 | dir, err := ioutil.ReadDir("testdata") 32 | if err != nil { 33 | t.Fatalf(err.Error()) 34 | } 35 | 36 | // filter for gpgkey files 37 | keyfiles := make([]string, 0) 38 | for _, fi := range dir { 39 | if strings.HasPrefix(fi.Name(), "RPM-GPG-KEY-") { 40 | keyfiles = append(keyfiles, filepath.Join("testdata", fi.Name())) 41 | } 42 | } 43 | 44 | // build keyring 45 | keyring, err := OpenKeyRing(keyfiles...) 46 | if err != nil { 47 | t.Fatalf(err.Error()) 48 | } 49 | 50 | // load package file paths 51 | files := getTestFiles() 52 | 53 | // check each package 54 | valid := 0 55 | for filename, b := range files { 56 | if signer, err := GPGCheck(bytes.NewReader(b), keyring); err != nil { 57 | t.Errorf("Validation error for %s: %v", filepath.Base(filename), err) 58 | } else { 59 | t.Logf("%s signed by '%v'", filepath.Base(filename), signer) 60 | valid++ 61 | } 62 | } 63 | 64 | t.Logf("Validated GPG signature for %d packages", valid) 65 | } 66 | 67 | // ExampleGPGCheck reads a public GPG key and uses it to validate the signature 68 | // of a local rpm package. 69 | func ExampleGPGCheck() { 70 | // read public key from gpgkey file 71 | keyring, err := OpenKeyRing("testdata/RPM-GPG-KEY-CentOS-7") 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | // open a rpm package for reading 77 | f, err := os.Open("testdata/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm") 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | defer f.Close() 82 | 83 | // validate gpg signature 84 | if signer, err := GPGCheck(f, keyring); err == nil { 85 | fmt.Printf("Package signed by '%s'\n", signer) 86 | } else if err == ErrGPGCheckFailed { 87 | fmt.Printf("Package failed GPG signature validation\n") 88 | } else { 89 | log.Fatal(err) 90 | } 91 | 92 | // Output: Package signed by 'CentOS-7 Key (CentOS 7 Official Signing Key) ' 93 | } 94 | 95 | // ExampleMD5Check validates a local rpm package named using the MD5 checksum 96 | // value specified in the package header. 97 | func ExampleMD5Check() { 98 | // open a rpm package for reading 99 | f, err := os.Open("testdata/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm") 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | 104 | defer f.Close() 105 | 106 | // validate md5 checksum 107 | if err := MD5Check(f); err == nil { 108 | fmt.Printf("Package passed checksum validation\n") 109 | } else if err == ErrMD5CheckFailed { 110 | fmt.Printf("Package failed checksum validation\n") 111 | } else { 112 | log.Fatal(err) 113 | } 114 | 115 | // Output: Package passed checksum validation 116 | } 117 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | const ( 4 | TagTypeNull TagType = iota 5 | TagTypeChar 6 | TagTypeInt8 7 | TagTypeInt16 8 | TagTypeInt32 9 | TagTypeInt64 10 | TagTypeString 11 | TagTypeBinary 12 | TagTypeStringArray 13 | TagTypeI18NString 14 | ) 15 | 16 | var tagTypeNames = []string{ 17 | "NULL", 18 | "CHAR", 19 | "INT8", 20 | "INT16", 21 | "INT32", 22 | "INT64", 23 | "STRING", 24 | "BIN", 25 | "STRING_ARRAY", 26 | "I18NSTRING", 27 | } 28 | 29 | // TagType describes the data type of a tag's value. 30 | type TagType int 31 | 32 | func (i TagType) String() string { 33 | if i > 0 && int(i) < len(tagTypeNames) { 34 | return tagTypeNames[i] 35 | } 36 | return "UNKNOWN" 37 | } 38 | 39 | // Tag is an rpm header entry and its associated data value. Once the data type 40 | // is known, use the associated value method to retrieve the tag value. 41 | // 42 | // All Tag methods will return their zero value if the underlying data type is 43 | // a different type or if the tag is nil. 44 | type Tag struct { 45 | ID int 46 | Type TagType 47 | Value interface{} 48 | } 49 | 50 | // StringSlice returns a slice of strings or nil if the index is not a string 51 | // slice value. 52 | // 53 | // Use StringSlice for all STRING, STRING_ARRAY and I18NSTRING data types. 54 | func (c *Tag) StringSlice() []string { 55 | if c == nil || c.Value == nil { 56 | return nil 57 | } 58 | if v, ok := c.Value.([]string); ok { 59 | return v 60 | } 61 | return nil 62 | } 63 | 64 | // String returns a string or an empty string if the index is not a string 65 | // value. 66 | // 67 | // Use String for all STRING, STRING_ARRAY and I18NSTRING data types. 68 | // 69 | // This is not intended to implement fmt.Stringer. To format the tag using its 70 | // identifier, use Tag.ID. To format the tag's value, use Tag.Value. 71 | func (c *Tag) String() string { 72 | v := c.StringSlice() 73 | if len(v) == 0 { 74 | return "" 75 | } 76 | return v[0] 77 | } 78 | 79 | // Int64Slice returns a slice of int64s or nil if the index is not a numerical 80 | // slice value. All integer types are cast to int64. 81 | // 82 | // Use Int64Slice for all INT16, INT32 and INT64 data types. 83 | func (c *Tag) Int64Slice() []int64 { 84 | if c == nil || c.Value == nil { 85 | return nil 86 | } 87 | if v, ok := c.Value.([]int64); ok { 88 | return v 89 | } 90 | return nil 91 | } 92 | 93 | // Int64 returns an int64 if the index is not a numerical value. All integer 94 | // types are cast to int64. 95 | // 96 | // Use Int64 for all INT16, INT32 and INT64 data types. 97 | func (c *Tag) Int64() int64 { 98 | v := c.Int64Slice() 99 | if len(v) > 0 { 100 | return v[0] 101 | } 102 | return 0 103 | } 104 | 105 | // Bytes returns a slice of bytes or nil if the index is not a byte slice value. 106 | // 107 | // Use Bytes for all CHAR, INT8 and BIN data types. 108 | func (c *Tag) Bytes() []byte { 109 | if c == nil || c.Value == nil { 110 | return nil 111 | } 112 | if v, ok := c.Value.([]byte); ok { 113 | return v 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /testdata/RPM-GPG-KEY-CentOS-2: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.0.7 (GNU/Linux) 3 | 4 | mQGiBEBWEN0RBACW17hrZa8oxqdv1CrEDvFbYmdilaFQbAQ4s/OTaNzlUMHGDYwo 5 | lHLMUFCADOqATpFZeHWTuqBLTUwGmIyuqJMrwU53w7qtjDPN8Ui1Gos4F29Bw85/ 6 | 2MecG6YiefVhEgIlG54mZPFQNYMiJmbVoFn/5tJsLfuk12sa7+8obxDW1wCgx3iA 7 | kxoMx+6nu7979LX7BQS0DokD/1Asb2gwhtoyejOdhHAHQbahxX6XQY4nRJRzpBZI 8 | x9yX/443IKC4v/Pf1bUj5pRigdeAKX3VXETnm8vpG8fENMVQHBy8+W68/tQF7W2n 9 | lJWqpwDQmjfCtyyIftPhwf8pB76aa45iixbYX4t2bYdKQIz5cK+UVeCSAN+QkiNf 10 | 3JjFA/4j9XzZYuGbhWKOGOnU0wAeFurlrH9cVMoprFRNg+lOPKB/juCOCtMcRAIH 11 | yI17NwuLoUUrKyDJOwK2bEnlS74yx0bOO8+wtxWdBp8sZuObCAg1DeLNIqOLYxX8 12 | vHqB2HRBS3TK2fIbh9XfyT3JTgW4CGyb7twXwnpSggjUCrU/B7QmQ2VudE9TLTIg 13 | S2V5IDxjZW50b3MtMmtleUBjYW9zaXR5Lm9yZz6ITAQTEQIADAUCQFYfEgWDCWXz 14 | SwAKCRAo3ojWuZEbkkTiAJ0TOAJ85LOxhtYT1dzUPMM3V5CxUQCgpNW+7ESfPbNT 15 | kGj2pUBkp6ILZYOIXwQTEQIAHwUCQFYQ3QUJCWYBgAQLBwMCAxUCAwMWAgECHgEC 16 | F4AACgkQKALokhb/DkYxxACgm0Bh1fb1pHn/KB4Df19BDXxoPxQAnRvdlgRuA+nd 17 | OP5mIPdKxUK/Y/qpuQINBEBWEQYQCACt1UOOzDC9ugzxghp7OWJtyRG2TDE9zkrK 18 | aMfGvCH67s3Col87ogirag6w7+asmQbmnFCy1UAEzGqDgaiYO4rBnZD+Gu+ArCRW 19 | gsxbdGmYdlrJ9Mlnv6Is+3NbJonHGNM9CHWpIMelMnzYmBhdfLvZgVCVWb9Bba+n 20 | yCjZZNmovZ6UcTaQiozbQfqP+AbU01jdL9cG4YnEkeG3hXV5oCy3iEJSpUbB2U04 21 | gICRbtrZceukayKuh/5kTrh+k9oBgybCHEttHSuhQVP1y8+Ca0J3qOa0PdGAwl89 22 | jhoIBUZCND32fOWNGFqUnKlccnR98ZQ6A5pLggxDrhF4WZtS9iHPAAMFB/9/8RDf 23 | z/2afDHDjU8ne2SJkXv8YqtMHcjA+uHxfsc0B9A38y0vjsxd/DGsBxMjut0EGBMf 24 | IUF4RN1RWJJNfrt83/xwW+MW81ve/NN23wlRhbGYkvEqkMYaOR5NuYjOFIG4sunE 25 | QHlMqCeCGZFEtvVeyVEMjL+ktwbHLNxpMwQf0mdWghkBn0pT2Y2dl28ZgR2muNBT 26 | KIK9oo/dq80TjEG7nNwdVJ16Yy9EQItWyPrnKoY1X/f570WlkX/f0nIWYro752sV 27 | /AJUpZjnnrxx2uSAt2B07NBwXRiTmWEuIvPXUNMvIRA1WYMXpkV7oVfsaXRrV9/j 28 | C8bicR1fqvVesY4hiEwEGBECAAwFAkBWEQYFCQlmAYAACgkQKALokhb/DkbAhgCf 29 | TUOhNwnCpFGBtoWdvJQJK6B7jBkAn3IvRicxt/MO2iV0bG1XD/pA6t2Y 30 | =6kke 31 | -----END PGP PUBLIC KEY BLOCK----- 32 | -------------------------------------------------------------------------------- /testdata/RPM-GPG-KEY-CentOS-3: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.2.1 (GNU/Linux) 3 | 4 | mQGiBEBWEXMRBACwv6ybAZQEyqFvVXnVmTk5Pvt/k701xbdYFkh/GbolGKhvW0OQ 5 | zSK0zffeBXrqJHnfWGxOjXvhQVq9miOQUtTCk65s6dZF4HbXRmgQynVAj19S0Axw 6 | wqzVZzZAbbNz4Y5Mu/UOflR7n5WcNmES1ynFqRsmFWsypIJFrT3gGMC4gwCgmHev 7 | ld/ebabzVqkk1JjFMoxA0DED/3OJ0IohANLmDe7eAmT8IRIlWb+HPfi2pfhYCjYv 8 | /g1nc+0hBPaZtrvFuboaQfUh7vqcVza+Ti/A1kPsTwDiXClV/njzow04Qx9K2TKL 9 | E+Jad9kd9/mJGM2JeUFvbPYFQaIBP6ZW8pibZI7YKGQ1Rwmv6wCjlQ06ABHetIgV 10 | 5sOcA/sHFomRwY+aTGxM4Bd0XUZW0Hjj2FP5/38yfWflhDLGSDeJPuwMk+Y5JlTz 11 | FfMMeNsMMdGk9Hn7vysKrQwXbZcpS+WmopA8jWg77jhHIXZKLJYdySCDlxsOixG2 12 | NBmRS9HQHh/d8OfCEh4tg1VLKhVmkZRpWdHcmxueJfkx3zzMX7QmQ2VudE9TLTMg 13 | S2V5IDxjZW50b3MtM2tleUBjYW9zaXR5Lm9yZz6IXwQTEQIAHwUCQFYRcwUJCWYB 14 | gAQLBwMCAxUCAwMWAgECHgECF4AACgkQcEnkTQJeUTsPjgCdFVLLKgeMpqc8gFdm 15 | HW9pQxtRRKoAoIYpvPOIb1nj88gpx4aYbxcw+S/5iEwEExECAAwFAkBWHykFgwll 16 | 88oACgkQKN6I1rmRG5KRxACfQOzZp6ComvGfvkPFuJRd9QvVx48AnAvY/AqekJxd 17 | /y6TGOQvrmm8CoxWuQINBEBWEZEQCADQ48xpVDl9w3PlwgauoGaysDgyc8OAkzAM 18 | 6IPb/Ma068l73y58KJXdBLCTDFbCQb/O29wE7iw8V/MXqBRl6aWzKkdnkA2RzDHo 19 | MqYGHrMUwO8y74l/Crw24b5gzhOkHwZGeuCEHbXyt6h1oYJBs239vyvQF/l5EIUF 20 | CRiaYT1Y4tGdeGoCwNEdVCOBz5r9F7ebJaT0rK+cFwMEk3mLXmxYz2w9NdKS9R8w 21 | ytIRZDCLY2XadmPKfcZje1tdp0b/z+jbXw1iIs1hbje0kLvC88OSp5Ss0oN0zN2H 22 | NZT59mdnqlLlUpNApS1Ev6pPB5ZAvdi43IJdVDKgsp+SSwCy/85TAAMFB/92Z967 23 | /FsFxShW+6t7ShI3Y1PF4hVyK3wDF1I83b7Ff+IwFh8YhPt2x8AWTxJRWAoIrtzh 24 | G+zEmUjm19dRqeB3tI3zTqNSkl15IIVGElhv8PFET9UwZblHLVW4LQEqbOIeFlTn 25 | jSnbdfh05Qzx0J+eJnMdgBhQajItE7R74O5Yx5Dqaat641FxT6cZUtRUcd+tQ58t 26 | MkIcK1ZvRiRTC6WkwfULpiRUvG4N+NhCWlfjGUfvcoMuThqkjU3qeVS3hlH2T1Mq 27 | JMilUUZFN5laLXv9oQFJZReF64mrQBQzueqjtkWEbVqoji1ThdPQTeLgn9SSewZf 28 | X584+BMZSkhNBP8ViEwEGBECAAwFAkBWEZEFCQlmAYAACgkQcEnkTQJeUTsx4wCf 29 | ZZPhJKRCXgwX7UB9YwDaSFQK8IgAoJgy+aCPMUCH6WUFPVRf49q56+9r 30 | =4XKu 31 | -----END PGP PUBLIC KEY BLOCK----- 32 | -------------------------------------------------------------------------------- /testdata/RPM-GPG-KEY-CentOS-4: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.2.1 (GNU/Linux) 3 | 4 | mQGiBEIfIY8RBACIjFavOQNbs4bjTtOblq4X5/oxuTJtv41nfqSFNeUAQke0qoxx 5 | AUlBesWxDsOXp5VppgNEA07hGjPvzoxabLAsTccQplvHMNzmRezyukYrSTVR/F7g 6 | ywpvlhaAFkL9jZxodXzWKk2cmBLVWvuyzlLEUBeijm2amyEHcIGAczxPawCgmVcM 7 | 9WpA6SOKivd9qTXK2XP+9BUD/2xV4OR4L7q8CSiaDMwPLo6P6D6VDc9LpVy16Wmu 8 | iYPFJIcIpp309biKZhGZgd+gHDhld9EJcZ3A2v43GY/xCdJqZ7Uh5QIGDafnil87 9 | 2AbMIBYpcOpvAshTM10S3Qj06pIQE47oONZT5A80O/hn+Yd8ySCEswpbWCmtAxnc 10 | iNw3A/0Qk/bKrhT6J9Um2JhMfxx/nB80mM+Jlsn58B8i4sjrIVdzc3b45Y2wbXN3 11 | uVGuvvAFolAco3cpVy3oY1wMVuh8UlJFNESmxZL/Z7BXyKhiKUZrNxEvQt9OtD1F 12 | d36ur8Ky8zFE5GL903Nx/dEVBvIDq2/2K3Wy9Yq3YIC0PW7fkrQlQ2VudE9TLTQg 13 | a2V5IDxjZW50b3MtNGtleUBjZW50b3Mub3JnPohfBBMRAgAfBQJCHyGPBQkSzAMA 14 | BAsHAwIDFQIDAxYCAQIeAQIXgAAKCRClPQurRD4YISH0AJ9zmx2JPGt8ELKo3aE0 15 | YoGg6EYipwCdH3kRVJHQtDeRs/5v5Ghn92XZS4KITAQTEQIADAUCQh8hxAWDEswC 16 | ywAKCRA4whYWOWygpCumAJwOseF0mAV+j/0kGrKXf/FKboFScgCdEITVqtB1CCyn 17 | +q+IqnCmgEF8rYy5Ag0EQh8hqhAIAKwNu60J+AnfVjNk0eN26sKBQOHFVQX9M3bd 18 | NBVWruocb7dro6DG4daPVB66ZI9RqBusll0jz5nUhBO3GZ3rn/KLVhMO2uCtvdcw 19 | WYtY6188lO6lOm3aYadIqafcPPiiLnF3zm/E8hI/trbPpaoW1dFBOiSlOY4bSpSC 20 | nTuHYd5fjYu77wQhnSsl19XfqwuvHQKW1vhXCaM2GrsLA5tgjLOlJhYJ4yPY2LTo 21 | yxoWC/JMMM0Vwi7BaVoa/G2uamC6sL5f6KXei5QftemUvw1uM/2fkLbuHtwETq6Z 22 | yUZlsL1H5K5G4h+GDVByBF6Y2P1csi7oXK13sdzhkewLaMjmah8ABAsH/3zhD0Gy 23 | 1jlMs9dGKSi9kq3jcUE/4o3vvjOPbxqT9psJu0jMEAfUVCWX9BWgZXyE2u+nBxcY 24 | AnNyqdmQzs6wTgJWGeGKpyC1jIKtO888RpPShvXtt/aNF4LaoielWZY9xu5oYEhn 25 | mBoww3VTbVxFNaPjglZOWnTxWfysHwG0H/dnXMp1sJjfdNsiB7zNniRRurlIiy0x 26 | hQSkDLe4tUr9Q9u4ztZKbwVX/fBzJC/u4Smi4VYx+HfOAP3OqzcGKNcb68GpIVo3 27 | 1RUQq1JqpPSM5U41kW8u+S5n+zhjZsb/Ix3ks18gI8wz5u5yzfGacqp65NLisqVe 28 | OKEf/MQ1xWytG4SITAQYEQIADAUCQh8hqgUJEswDAAAKCRClPQurRD4YIXC1AKCF 29 | 3t5xKJnEXJfgvhvldOzDIFjajwCgkX/MZI0O0SxYQAc2hEQJqCI/LJU= 30 | =Qsai 31 | -----END PGP PUBLIC KEY BLOCK----- 32 | -------------------------------------------------------------------------------- /testdata/RPM-GPG-KEY-CentOS-5: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.2.6 (GNU/Linux) 3 | 4 | mQGiBEWfB6MRBACrnYW6yKMT+MwJlCIhoyTxGf3mAxmnAiDEy6HcYN8rivssVTJk 5 | CFtQBlBOpLV/OW2YtKrCO2xHn46eNfnMri8FGT8g+9JF3MUVi7kiV1He4iJynHXB 6 | +F2ZqIvHf3IaUj1ys+p8TK64FDFxDQDrGQfIsD/+pkSGx53/877IrvdwjwCguQcr 7 | Ioip5TH0Fj0OLUY4asYVZH8EAIqFHEqsY+9ziP+2R3/FyxSllKkjwcMLrBug+cYO 8 | LYDD6eQXE9Mq8XKGFDj9ZB/0+JzK/XQeStheeFG75q3noq5oCPVFO4czuKErIRAB 9 | qKbDBhaTj3JhOgM12XsUYn+rI6NeMV2ZogoQCC2tWmDETfRpYp2moo53NuFWHbAy 10 | XjETA/sHEeQT9huHzdi/lebNBj0L8nBGfLN1nSRP1GtvagBvkR4RZ6DTQyl0UzOJ 11 | RA3ywWlrL9IV9mrpb1Fmn60l2jTMMCc7J6LacmPK906N+FcN/Docj1M4s/4CNanQ 12 | NhzcFhAFtQL56SNyLTCk1XzhssGZ/jwGnNbU/aaj4wOj0Uef5LRGQ2VudE9TLTUg 13 | S2V5IChDZW50T1MgNSBPZmZpY2lhbCBTaWduaW5nIEtleSkgPGNlbnRvcy01LWtl 14 | eUBjZW50b3Mub3JnPohkBBMRAgAkBQJFnwekAhsDBQkSzAMABgsJCAcDAgMVAgMD 15 | FgIBAh4BAheAAAoJEKikR9zoViiXKlEAmwSoZDvZo+WChcg3s/SpNoWCKhMAAJwI 16 | E2aXpZVrpsQnInUQWwkdrTiL5YhMBBMRAgAMBQJFnwiSBYMSzAIRAAoJEDjCFhY5 17 | bKCk0hAAn134bIx3wSbq58E6P6U5RT7Z2Zx4AJ9VxnVkoGHkVIgSdsxHUgRjo27N 18 | F7kBDQRFnwezEAQA/HnJ5yiozwgtf6jt+kii8iua+WnjqBKomPHOQ8moxbWdv5Ks 19 | 4e1DPhzRqxhshjmub4SuJ93sgMSAF2ayC9t51mSJV33KfzPF2gIahcMqfABe/2hJ 20 | aMzcQZHrGJCEX6ek8l8SFKou7vICzyajRSIK8gxWKBuQknP/9LKsoczV+xsAAwUD 21 | /idXPkk4vRRHsCwc6I23fdI0ur52bzEqHiAIswNfO521YgLk2W1xyCLc2aYjc8Ni 22 | nrMX1tCnEx0/gK7ICyJoWH1Vc7//79sWFtX2EaTO+Q07xjFX4E66WxJlCo9lOjos 23 | Vk5qc7R+xzLDoLGFtbzaTRQFzf6yr7QTu+BebWLoPwNTiE8EGBECAA8FAkWfB7MC 24 | GwwFCRLMAwAACgkQqKRH3OhWKJfvvACfbsF1WK193zM7vSc4uq51XsceLwgAoI0/ 25 | 9GxdNhGQEAweSlQfhPa3yYXH 26 | =o/Mx 27 | -----END PGP PUBLIC KEY BLOCK----- 28 | 29 | -------------------------------------------------------------------------------- /testdata/RPM-GPG-KEY-CentOS-6: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.4.5 (GNU/Linux) 3 | 4 | mQINBE4P06MBEACqn48FZgYkG2QrtUAVDV58H6LpDYEcTcv4CIFSkgs6dJ9TavCW 5 | NyPBZRpM2R+Rg5eVqlborp7TmktBP/sSsxc8eJ+3P2aQWSWc5ol74Y0OznJUCrBr 6 | bIdypJllsD9Fe+h7gLBXTh3vdBEWr2lR+xA+Oou8UlO2gFbVFQqMafUgU1s0vqaE 7 | /hHH0TzwD0/tJ6eqIbHwVR/Bu6kHFK4PwePovhfvyYD9Y+C0vOYd5Ict2vbLHz1f 8 | QBDZObv4M6KN3j7nzme47hKtdMd+LwFqxM5cXfM6b5doDulWPmuGV78VoX6OR7el 9 | x1tlfpuiFeuXYnImm5nTawArcQ1UkXUSYcTUKShJebRDLR3BycxR39Q9jtbOQ29R 10 | FumHginovEhdUcinRr22eRXgcmzpR00zFIWoFCwHh/OCtG14nFhefuZ8Z80qbVhW 11 | 2J9+/O4tksv9HtQBmQNOK5S8C4HNF2M8AfOWNTr8esFSDc0YA5/cxzdfOOtWam/w 12 | lBpNcUUSSgddRsBwijPuWhVA3NmA/uQlJtAo4Ji5vo8cj5MTPG3+U+rfNqRxu1Yc 13 | ioXRo4LzggPscaTZX6V24n0fzw0J2k7TT4sX007k+7YXwEMqmHpcMYbDNzdCzUer 14 | Zilh5hihJwvGfdi234W3GofttoO+jaAZjic7a3p6cO1ICMgfVqrbZCUQVQARAQAB 15 | tEZDZW50T1MtNiBLZXkgKENlbnRPUyA2IE9mZmljaWFsIFNpZ25pbmcgS2V5KSA8 16 | Y2VudG9zLTYta2V5QGNlbnRvcy5vcmc+iQI8BBMBAgAmBQJOD9OjAhsDBQkSzAMA 17 | BgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQCUb8osEFud6ajRAAnb6d+w6Y/v/d 18 | MSy7UEy4rNquArix8xhqBwwjoGXpa37OqTvvcJrftZ1XgtzmTbkqXc+9EFch0C+w 19 | ST10f+H0SPTUGuPwqLkg27snUkDAv1B8laub+l2L9erzCaRriH8MnFyxt5v1rqWA 20 | mVlRymzgXK+EQDr+XOgMm1CvxVY3OwdjdoHNox4TdVQWlZl83xdLXBxkd5IRciNm 21 | sg5fJAzAMeg8YsoDee3m4khg9gEm+/Rj5io8Gfk0nhQpgGGeS1HEXl5jzTb44zQW 22 | qudkfcLEdUMOECbu7IC5Z1wrcj559qcp9C94IwQQO+LxLwg4kHffvZjCaOXDRiya 23 | h8KGsEDuiqwjU9HgGq9fa0Ceo3OyUazUi+WnOxBLVIQ8cUZJJ2Ia5PDnEsz59kCp 24 | JmBZaYPxUEteMtG3yDTa8c8jUnJtMPpkwpSkeMBeNr/rEH4YcBoxuFjppHzQpJ7G 25 | hZRbOfY8w97TgJbfDElwTX0/xX9ypsmBezgGoOvOkzP9iCy9YUBc9q/SNnflRWPO 26 | sMVrjec0vc6ffthu2xBdigBXhL7x2bphWzTXf2T067k+JOdoh5EGney6LhQzcp8m 27 | YCTENStCR+L/5XwrvNgRBnoXe4e0ZHet1CcCuBCBvSmsPHp5ml21ahsephnHx+rl 28 | JNGtzulnNP07RyfzQcpCNFH7W4lXzqM= 29 | =jrWY 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /testdata/RPM-GPG-KEY-CentOS-7: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v1.4.5 (GNU/Linux) 3 | 4 | mQINBFOn/0sBEADLDyZ+DQHkcTHDQSE0a0B2iYAEXwpPvs67cJ4tmhe/iMOyVMh9 5 | Yw/vBIF8scm6T/vPN5fopsKiW9UsAhGKg0epC6y5ed+NAUHTEa6pSOdo7CyFDwtn 6 | 4HF61Esyb4gzPT6QiSr0zvdTtgYBRZjAEPFVu3Dio0oZ5UQZ7fzdZfeixMQ8VMTQ 7 | 4y4x5vik9B+cqmGiq9AW71ixlDYVWasgR093fXiD9NLT4DTtK+KLGYNjJ8eMRqfZ 8 | Ws7g7C+9aEGHfsGZ/SxLOumx/GfiTloal0dnq8TC7XQ/JuNdB9qjoXzRF+faDUsj 9 | WuvNSQEqUXW1dzJjBvroEvgTdfCJfRpIgOrc256qvDMp1SxchMFltPlo5mbSMKu1 10 | x1p4UkAzx543meMlRXOgx2/hnBm6H6L0FsSyDS6P224yF+30eeODD4Ju4BCyQ0jO 11 | IpUxmUnApo/m0eRelI6TRl7jK6aGqSYUNhFBuFxSPKgKYBpFhVzRM63Jsvib82rY 12 | 438q3sIOUdxZY6pvMOWRkdUVoz7WBExTdx5NtGX4kdW5QtcQHM+2kht6sBnJsvcB 13 | JYcYIwAUeA5vdRfwLKuZn6SgAUKdgeOtuf+cPR3/E68LZr784SlokiHLtQkfk98j 14 | NXm6fJjXwJvwiM2IiFyg8aUwEEDX5U+QOCA0wYrgUQ/h8iathvBJKSc9jQARAQAB 15 | tEJDZW50T1MtNyBLZXkgKENlbnRPUyA3IE9mZmljaWFsIFNpZ25pbmcgS2V5KSA8 16 | c2VjdXJpdHlAY2VudG9zLm9yZz6JAjUEEwECAB8FAlOn/0sCGwMGCwkIBwMCBBUC 17 | CAMDFgIBAh4BAheAAAoJECTGqKf0qA61TN0P/2730Th8cM+d1pEON7n0F1YiyxqG 18 | QzwpC2Fhr2UIsXpi/lWTXIG6AlRvrajjFhw9HktYjlF4oMG032SnI0XPdmrN29lL 19 | F+ee1ANdyvtkw4mMu2yQweVxU7Ku4oATPBvWRv+6pCQPTOMe5xPG0ZPjPGNiJ0xw 20 | 4Ns+f5Q6Gqm927oHXpylUQEmuHKsCp3dK/kZaxJOXsmq6syY1gbrLj2Anq0iWWP4 21 | Tq8WMktUrTcc+zQ2pFR7ovEihK0Rvhmk6/N4+4JwAGijfhejxwNX8T6PCuYs5Jiv 22 | hQvsI9FdIIlTP4XhFZ4N9ndnEwA4AH7tNBsmB3HEbLqUSmu2Rr8hGiT2Plc4Y9AO 23 | aliW1kOMsZFYrX39krfRk2n2NXvieQJ/lw318gSGR67uckkz2ZekbCEpj/0mnHWD 24 | 3R6V7m95R6UYqjcw++Q5CtZ2tzmxomZTf42IGIKBbSVmIS75WY+cBULUx3PcZYHD 25 | ZqAbB0Dl4MbdEH61kOI8EbN/TLl1i077r+9LXR1mOnlC3GLD03+XfY8eEBQf7137 26 | YSMiW5r/5xwQk7xEcKlbZdmUJp3ZDTQBXT06vavvp3jlkqqH9QOE8ViZZ6aKQLqv 27 | pL+4bs52jzuGwTMT7gOR5MzD+vT0fVS7Xm8MjOxvZgbHsAgzyFGlI1ggUQmU7lu3 28 | uPNL0eRx4S1G4Jn5 29 | =OGYX 30 | -----END PGP PUBLIC KEY BLOCK----- 31 | -------------------------------------------------------------------------------- /testdata/centos-release-3.1-1.i386.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-3.1-1.i386.rpm -------------------------------------------------------------------------------- /testdata/centos-release-4-0.1.i386.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-4-0.1.i386.rpm -------------------------------------------------------------------------------- /testdata/centos-release-4-0.1.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-4-0.1.x86_64.rpm -------------------------------------------------------------------------------- /testdata/centos-release-5-0.0.el5.centos.2.i386.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-5-0.0.el5.centos.2.i386.rpm -------------------------------------------------------------------------------- /testdata/centos-release-5-0.0.el5.centos.2.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-5-0.0.el5.centos.2.x86_64.rpm -------------------------------------------------------------------------------- /testdata/centos-release-6-0.el6.centos.5.i686.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-6-0.el6.centos.5.i686.rpm -------------------------------------------------------------------------------- /testdata/centos-release-6-0.el6.centos.5.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-6-0.el6.centos.5.x86_64.rpm -------------------------------------------------------------------------------- /testdata/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-7-2.1511.el7.centos.2.10.x86_64.rpm -------------------------------------------------------------------------------- /testdata/centos-release-as-2.1AS-4.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/centos-release-as-2.1AS-4.noarch.rpm -------------------------------------------------------------------------------- /testdata/epel-release-7-5.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cavaliergopher/rpm/541f2bea49bd9d285872b4a3da8bdcc6917e9cb7/testdata/epel-release-7-5.noarch.rpm -------------------------------------------------------------------------------- /testdata/vercmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Generate test cases for version_test.go 5 | """ 6 | 7 | from json import dumps 8 | from rpm import labelCompare 9 | from typing import Iterable, Tuple 10 | 11 | VERSIONS = [ 12 | "", 13 | "0", 14 | "1", 15 | "2", 16 | "10", 17 | "100", 18 | "0.0", 19 | "0.1", 20 | "0.10", 21 | "0.99", 22 | "1.0", 23 | "1.99", 24 | "2.0", 25 | "0.0.0", 26 | "0.0.1", 27 | "0.0.2", 28 | "0.0.10", 29 | "0.0.99", 30 | "0.1.0", 31 | "0.2.0", 32 | "0.10.0", 33 | "0.99.0", 34 | "0.100.0", 35 | "0.0.0.0", 36 | "0.0.0.1", 37 | "0.0.0.10", 38 | "0.0.1.0", 39 | "0.0.01.0", 40 | "1.2.3.4", 41 | "1-2-3-4", 42 | "20150101", 43 | "20151212", 44 | "20151212.0", 45 | "20151212.1", 46 | "2015.1.1", 47 | "2015.02.02", 48 | "2015.12.12", 49 | "1.2.3a", 50 | "1.2.3b", 51 | "R16B", 52 | "R16C", 53 | "1.2.3.2016.1.1", 54 | "0.5a1.dev", 55 | "1.8.B59BrZX", 56 | "0.07b4p1", 57 | "3.99.5final.SP07", 58 | "3.99.5final.SP08", 59 | "0.4.tbb.20100203", 60 | "0.5.20120830CVS.el7", 61 | "1.el7", 62 | "1.el6", 63 | "10.el7", 64 | "01.el7", 65 | "0.17.20140318svn632.el7", 66 | "0.17.20140318svn633.el7", 67 | "1.20140522gitad6fb3e.el7", 68 | "1.20140522hitad6fb3e.el7", 69 | "8.20140605hgacf1c26e3019.el7", 70 | "8.20140605hgacf1c26e3029.el7", 71 | "22.svn457.el7", 72 | "22.svn458.el7", 73 | "~", 74 | "~~", 75 | "~1", 76 | "~a", 77 | "1~", 78 | "2~", 79 | ] 80 | 81 | 82 | def get_test_cases(versions: Iterable[str]) -> Iterable[Tuple[str, str, int]]: 83 | for a in versions: 84 | for b in versions: 85 | expect = labelCompare(("0", "0", a), ("0", "0", b)) 86 | yield {"a": a, b: "b", "expect": expect} 87 | 88 | 89 | if __name__ == "__main__": 90 | print( 91 | dumps( 92 | list(get_test_cases(VERSIONS)), 93 | separators=(",", ":"), 94 | ) 95 | ) 96 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // TimeFormat is the time format used by the rpm ecosystem. The time being 9 | // formatted must be in UTC for Format to generate the correct format. 10 | const TimeFormat = "Mon Jan _2 15:04:05 2006" 11 | 12 | func errorf(format string, a ...interface{}) error { 13 | return fmt.Errorf("rpm: "+format, a...) 14 | } 15 | 16 | func parseVersion(v string) (epoch int, version, release string) { 17 | if i := strings.IndexByte(v, ':'); i >= 0 { 18 | epoch, v = parseInt(v[:i]), v[i+1:] 19 | } 20 | 21 | if i := strings.IndexByte(v, '-'); i >= 0 { 22 | return epoch, v[:i], v[i+1:] 23 | } 24 | 25 | return epoch, v, "" 26 | } 27 | 28 | func parseInt(s string) int { 29 | var n int 30 | for _, dec := range s { 31 | if dec < '0' || dec > '9' { 32 | return 0 33 | } 34 | n = n*10 + (int(dec) - '0') 35 | } 36 | return n 37 | } 38 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import "testing" 4 | 5 | func TestParseVersion(t *testing.T) { 6 | tests := []struct { 7 | in string 8 | epoch int 9 | version string 10 | release string 11 | }{ 12 | {"", 0, "", ""}, 13 | {"1.0", 0, "1.0", ""}, 14 | {"1:1.0", 1, "1.0", ""}, 15 | {"1:1.0-test", 1, "1.0", "test"}, 16 | {"1.0-test", 0, "1.0", "test"}, 17 | {":1.0-", 0, "1.0", ""}, // Ensure malformed version doesn't panic. 18 | } 19 | 20 | for _, test := range tests { 21 | epoch, ver, rel := parseVersion(test.in) 22 | if epoch != test.epoch { 23 | t.Errorf("Expected epoch %d for %q; got %d", test.epoch, test.in, epoch) 24 | } 25 | if ver != test.version { 26 | t.Errorf("Expected version %s for %q; got %s", test.version, test.in, ver) 27 | } 28 | if rel != test.release { 29 | t.Errorf("Expected release %s for %q; got %s", test.release, test.in, rel) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "math" 5 | "regexp" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | // alphanumPattern is a regular expression to match all sequences of numeric 11 | // characters or alphanumeric characters. 12 | var alphanumPattern = regexp.MustCompile("([a-zA-Z]+)|([0-9]+)|(~)") 13 | 14 | // Version is an interface which holds version information for a package in EVR 15 | // form. 16 | type Version interface { 17 | Epoch() int 18 | Version() string 19 | Release() string 20 | } 21 | 22 | // Compare compares the version details of two packages. Versions are 23 | // compared by Epoch, Version and Release (EVR) in descending order of 24 | // precedence. 25 | // 26 | // If a is more recent than b, 1 is returned. If a is less recent than b, -1 is 27 | // returned. If a and b are equal, 0 is returned. 28 | // 29 | // This function does not consider if the two packages have the same name or if 30 | // either package has been made obsolete by the other. 31 | func Compare(a, b Version) int { 32 | // compare nils 33 | if a == nil && b == nil { 34 | return 0 35 | } else if a == nil { 36 | return -1 37 | } else if b == nil { 38 | return 1 39 | } 40 | 41 | // compare epoch 42 | ae := a.Epoch() 43 | be := b.Epoch() 44 | if ae != be { 45 | if ae > be { 46 | return 1 47 | } 48 | return -1 49 | } 50 | 51 | // compare version 52 | if rc := CompareVersions(a.Version(), b.Version()); rc != 0 { 53 | return rc 54 | } 55 | 56 | // compare release 57 | return CompareVersions(a.Release(), b.Release()) 58 | } 59 | 60 | // CompareVersion compares version strings. It does not consider package epochs 61 | // or release numbers like Compare. 62 | // 63 | // If a is more recent than b, 1 is returned. If a is less recent than b, -1 is 64 | // returned. If a and b are equal, 0 is returned. 65 | func CompareVersions(a, b string) int { 66 | // For the original C implementation, see: 67 | // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c#L16 68 | if a == b { 69 | return 0 70 | } 71 | 72 | // get alpha/numeric segements 73 | segsa := alphanumPattern.FindAllString(a, -1) 74 | segsb := alphanumPattern.FindAllString(b, -1) 75 | segs := int(math.Min(float64(len(segsa)), float64(len(segsb)))) 76 | 77 | // compare each segment 78 | for i := 0; i < segs; i++ { 79 | a := segsa[i] 80 | b := segsb[i] 81 | 82 | // compare tildes 83 | if []rune(a)[0] == '~' || []rune(b)[0] == '~' { 84 | if []rune(a)[0] != '~' { 85 | return 1 86 | } 87 | if []rune(b)[0] != '~' { 88 | return -1 89 | } 90 | } 91 | 92 | if unicode.IsNumber([]rune(a)[0]) { 93 | // numbers are always greater than alphas 94 | if !unicode.IsNumber([]rune(b)[0]) { 95 | // a is numeric, b is alpha 96 | return 1 97 | } 98 | 99 | // trim leading zeros 100 | a = strings.TrimLeft(a, "0") 101 | b = strings.TrimLeft(b, "0") 102 | 103 | // longest string wins without further comparison 104 | if len(a) > len(b) { 105 | return 1 106 | } else if len(b) > len(a) { 107 | return -1 108 | } 109 | 110 | } else if unicode.IsNumber([]rune(b)[0]) { 111 | // a is alpha, b is numeric 112 | return -1 113 | } 114 | 115 | // string compare 116 | if a < b { 117 | return -1 118 | } else if a > b { 119 | return 1 120 | } 121 | } 122 | 123 | // segments were all the same but separators must have been different 124 | if len(segsa) == len(segsb) { 125 | return 0 126 | } 127 | 128 | // If there is a tilde in a segment past the min number of segments, find it. 129 | if len(segsa) > segs && []rune(segsa[segs])[0] == '~' { 130 | return -1 131 | } else if len(segsb) > segs && []rune(segsb[segs])[0] == '~' { 132 | return 1 133 | } 134 | 135 | // whoever has the most segments wins 136 | if len(segsa) > len(segsb) { 137 | return 1 138 | } 139 | return -1 140 | } 141 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package rpm 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | type VerTest struct { 10 | A string `json:"a"` 11 | B string `json:"b"` 12 | Expect int `json:"expect"` 13 | } 14 | 15 | type TestPkg struct { 16 | E int 17 | V string 18 | R string 19 | } 20 | 21 | func (c *TestPkg) Name() string { return "test" } 22 | func (c *TestPkg) Epoch() int { return c.E } 23 | func (c *TestPkg) Version() string { return c.V } 24 | func (c *TestPkg) Release() string { return c.R } 25 | 26 | func sign(r int) string { 27 | if r < 0 { 28 | return "<" 29 | } else if r > 0 { 30 | return ">" 31 | } 32 | return "==" 33 | } 34 | 35 | func TestCompare(t *testing.T) { 36 | tests := make([]*VerTest, 0) 37 | f, err := os.Open("./testdata/vercmp.json") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | defer f.Close() 42 | if err := json.NewDecoder(f).Decode(&tests); err != nil { 43 | t.Fatal(err) 44 | } 45 | if len(tests) == 0 { 46 | t.Fatal("version tests are empty") 47 | } 48 | for _, test := range tests { 49 | // compare 'version' 50 | a := &TestPkg{0, test.A, ""} 51 | b := &TestPkg{0, test.B, ""} 52 | if r := Compare(a, b); r != test.Expect { 53 | t.Errorf( 54 | "Expected %s %s %s; got %s %s %s", 55 | test.A, 56 | sign(test.Expect), 57 | test.B, 58 | test.A, 59 | sign(r), 60 | test.B, 61 | ) 62 | } 63 | 64 | // compare 'release' 65 | a = &TestPkg{0, "", test.A} 66 | b = &TestPkg{0, "", test.B} 67 | if r := Compare(a, b); r != test.Expect { 68 | t.Errorf( 69 | "Expected %s %s %s; got %s %s %s", 70 | test.A, 71 | sign(test.Expect), 72 | test.B, 73 | test.A, 74 | sign(r), 75 | test.B, 76 | ) 77 | } 78 | } 79 | if r := Compare(nil, nil); r != 0 { 80 | t.Errorf("Expected == ; got %s ", sign(r)) 81 | } 82 | if r := Compare(nil, &TestPkg{1, "", ""}); r != -1 { 83 | t.Errorf("Expected < ; got %s ", sign(r)) 84 | } 85 | if r := Compare(&TestPkg{1, "", ""}, nil); r != 1 { 86 | t.Errorf("Expected > ; got %s ", sign(r)) 87 | } 88 | } 89 | --------------------------------------------------------------------------------