From eeea9ba459e5059dc676970650d19a0cb4a23962 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Fri, 23 Sep 2022 09:59:50 -0700 Subject: [PATCH 1/4] feat: allow env expansion in conflicts, suggests, recommends, depends, provides, and replaces. Ensuring empty env vars get stripped to not cause issues * added test for boolean dependencies for rpm, deb, and apk. --- nfpm.go | 31 +++++++++++++++++++++---- nfpm_test.go | 22 ++++++++++++++++++ testdata/acceptance/core.overrides.yaml | 6 +++++ www/docs/configuration.md | 18 ++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/nfpm.go b/nfpm.go index a5cc5e49..5430ded2 100644 --- a/nfpm.go +++ b/nfpm.go @@ -8,6 +8,7 @@ import ( "io" "io/fs" "os" + "strings" "sync" "github.com/AlekSi/pointer" @@ -148,16 +149,38 @@ func (c *Config) Validate() error { return nil } +func remove(slice []string, s int) []string { + return append(slice[:s], slice[s+1:]...) +} + +func (c *Config) expandEnvVarsStringSlice(items []string) []string { + for i, dep := range items { + val := strings.TrimSpace(os.Expand(dep, c.envMappingFunc)) + items[i] = val + } + for i, val := range items { + if val == "" { + items = remove(items, i) + } + } + + return items +} + func (c *Config) expandEnvVars() { // Version related fields c.Info.Release = os.Expand(c.Info.Release, c.envMappingFunc) c.Info.Version = os.Expand(c.Info.Version, c.envMappingFunc) c.Info.Prerelease = os.Expand(c.Info.Prerelease, c.envMappingFunc) c.Info.Arch = os.Expand(c.Info.Arch, c.envMappingFunc) - for _, override := range c.Overrides { - for i, dep := range override.Depends { - override.Depends[i] = os.Expand(dep, c.envMappingFunc) - } + for or, override := range c.Overrides { + override.Conflicts = c.expandEnvVarsStringSlice(override.Conflicts) + override.Depends = c.expandEnvVarsStringSlice(override.Depends) + override.Replaces = c.expandEnvVarsStringSlice(override.Replaces) + override.Recommends = c.expandEnvVarsStringSlice(override.Recommends) + override.Provides = c.expandEnvVarsStringSlice(override.Provides) + override.Suggests = c.expandEnvVarsStringSlice(override.Suggests) + c.Overrides[or] = override } // Maintainer and vendor fields diff --git a/nfpm_test.go b/nfpm_test.go index 37799c94..536abafc 100644 --- a/nfpm_test.go +++ b/nfpm_test.go @@ -340,6 +340,28 @@ overrides: require.Len(t, info.Overrides["rpm"].Depends, 1) require.Equal(t, "package = 1.0.0", info.Overrides["rpm"].Depends[0]) }) + + t.Run("depends-strips-empty", func(t *testing.T) { + os.Clearenv() + os.Setenv("VERSION", version) + os.Setenv("PKG", "") + info, err := nfpm.Parse(strings.NewReader(`--- +name: foo +overrides: + deb: + depends: + - ${PKG} + - package (= ${VERSION}) + rpm: + depends: + - package = ${VERSION} + - ${PKG}`)) + require.NoError(t, err) + require.Len(t, info.Overrides["deb"].Depends, 1) + require.Equal(t, "package (= 1.0.0)", info.Overrides["deb"].Depends[0]) + require.Len(t, info.Overrides["rpm"].Depends, 1) + require.Equal(t, "package = 1.0.0", info.Overrides["rpm"].Depends[0]) + }) } func TestOverrides(t *testing.T) { diff --git a/testdata/acceptance/core.overrides.yaml b/testdata/acceptance/core.overrides.yaml index 0f31da91..32177fce 100644 --- a/testdata/acceptance/core.overrides.yaml +++ b/testdata/acceptance/core.overrides.yaml @@ -27,14 +27,20 @@ contents: type: config overrides: rpm: + depends: + - (bash >= 4.4 or fish) scripts: preinstall: ./testdata/acceptance/scripts/preinstall.sh postremove: ./testdata/acceptance/scripts/postremove.sh deb: + depends: + - bash (>= 4.4) | fish scripts: postinstall: ./testdata/acceptance/scripts/postinstall.sh preremove: ./testdata/acceptance/scripts/preremove.sh apk: + depends: + - bash (>= 4.4) | fish scripts: postinstall: ./testdata/acceptance/scripts/postinstall.sh preremove: ./testdata/acceptance/scripts/preremove.sh diff --git a/www/docs/configuration.md b/www/docs/configuration.md index 7781e8f4..7501d137 100644 --- a/www/docs/configuration.md +++ b/www/docs/configuration.md @@ -81,28 +81,46 @@ changelog: "changelog.yaml" disable_globbing: false # Packages it replaces. (overridable) +# This will expand any env var you set in the field, eg ${REPLACE_BLA} +# the env var approach can be used to account for differences in platforms replaces: - foobar + - ${REPLACE_BLA} # Packages it provides. (overridable) +# This will expand any env var you set in the field, eg ${PROVIDES_BLA} +# the env var approach can be used to account for differences in platforms provides: - bar + - ${PROVIDES_BLA} # Dependencies. (overridable) +# This will expand any env var you set in the field, eg ${DEPENDS_NGINX} +# the env var approach can be used to account for differences in platforms +# eg rhel needs nginx >= 1:1.18 and deb needs nginx (>= 1.18.0) depends: - git + - ${DEPENDS_NGINX} # Recommended packages. (overridable) +# This will expand any env var you set in the field, eg ${RECOMMENDS_BLA} +# the env var approach can be used to account for differences in platforms recommends: - golang + - ${RECOMMENDS_BLA} # Suggested packages. (overridable) +# This will expand any env var you set in the field, eg ${SUGGESTS_BLA} +# the env var approach can be used to account for differences in platforms suggests: - bzr # Packages it conflicts with. (overridable) +# This will expand any env var you set in the field, eg ${CONFLICTS_BLA} +# the env var approach can be used to account for differences in platforms conflicts: - mercurial + - ${CONFLICTS_BLA} # Contents to add to the package # This can be binaries or any other files. From bcbbcbb2d0d72e4d180ad3635d9be9eaab17ed51 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Fri, 23 Sep 2022 19:55:41 -0700 Subject: [PATCH 2/4] fix: fork rpmpack to make it easier to add features and adapt it --- go.mod | 11 +- go.sum | 8 +- internal/rpmpack/LICENSE | 202 ++++++++++ internal/rpmpack/dir.go | 41 +++ internal/rpmpack/dir_test.go | 63 ++++ internal/rpmpack/file_types.go | 47 +++ internal/rpmpack/file_types_test.go | 32 ++ internal/rpmpack/header.go | 204 +++++++++++ internal/rpmpack/header_test.go | 114 ++++++ internal/rpmpack/rpm.go | 550 ++++++++++++++++++++++++++++ internal/rpmpack/rpm_test.go | 183 +++++++++ internal/rpmpack/sense.go | 168 +++++++++ internal/rpmpack/sense_test.go | 81 ++++ internal/rpmpack/tags.go | 101 +++++ rpm/rpm.go | 2 +- 15 files changed, 1797 insertions(+), 10 deletions(-) create mode 100644 internal/rpmpack/LICENSE create mode 100644 internal/rpmpack/dir.go create mode 100644 internal/rpmpack/dir_test.go create mode 100644 internal/rpmpack/file_types.go create mode 100644 internal/rpmpack/file_types_test.go create mode 100644 internal/rpmpack/header.go create mode 100644 internal/rpmpack/header_test.go create mode 100644 internal/rpmpack/rpm.go create mode 100644 internal/rpmpack/rpm_test.go create mode 100644 internal/rpmpack/sense.go create mode 100644 internal/rpmpack/sense_test.go create mode 100644 internal/rpmpack/tags.go diff --git a/go.mod b/go.mod index 31845314..df49047e 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/ProtonMail/gopenpgp/v2 v2.2.2 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11 - github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e github.com/goreleaser/chglog v0.2.2 github.com/goreleaser/fileglob v1.3.0 github.com/imdario/mergo v0.3.13 @@ -24,6 +23,13 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require ( + github.com/cavaliergopher/cpio v1.0.1 + github.com/google/go-cmp v0.5.8 + github.com/klauspost/compress v1.15.1 + github.com/pkg/errors v0.9.1 +) + require ( github.com/DataDog/zstd v1.4.5 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -32,7 +38,6 @@ require ( github.com/Microsoft/go-winio v0.5.1 // indirect github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f // indirect github.com/acomagu/bufpipe v1.0.3 // indirect - github.com/cavaliergopher/cpio v1.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.12.0 // indirect @@ -46,13 +51,11 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect - github.com/klauspost/compress v1.13.6 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect diff --git a/go.sum b/go.sum index 809aac92..d8e92b9c 100644 --- a/go.sum +++ b/go.sum @@ -61,10 +61,8 @@ github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e h1:6Jn9JtfCn20uycra92LxTkq5yfBKNSFlRJPBk8/Cxhg= -github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e/go.mod h1:83rLnx5vhPyN/mDzBYJWtiPf+9xnSVQynTpqZWe7OnY= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -92,8 +90,8 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o= github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/internal/rpmpack/LICENSE b/internal/rpmpack/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/internal/rpmpack/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/internal/rpmpack/dir.go b/internal/rpmpack/dir.go new file mode 100644 index 00000000..631b29d4 --- /dev/null +++ b/internal/rpmpack/dir.go @@ -0,0 +1,41 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpmpack + +// dirIndex holds the index from files to directory names. +type dirIndex struct { + m map[string]uint32 + l []string +} + +func newDirIndex() *dirIndex { + return &dirIndex{m: make(map[string]uint32)} +} + +func (d *dirIndex) Get(value string) uint32 { + + if idx, ok := d.m[value]; ok { + return idx + } + newIdx := uint32(len(d.l)) + d.l = append(d.l, value) + + d.m[value] = newIdx + return newIdx +} + +func (d *dirIndex) AllDirs() []string { + return d.l +} diff --git a/internal/rpmpack/dir_test.go b/internal/rpmpack/dir_test.go new file mode 100644 index 00000000..1eadd8ee --- /dev/null +++ b/internal/rpmpack/dir_test.go @@ -0,0 +1,63 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpmpack + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestDirIndex(t *testing.T) { + testCases := []struct { + name string + before []string + dir string + wantGet uint32 + wantAllDirs []string + }{{ + name: "first", + dir: "/first", + wantGet: 0, + wantAllDirs: []string{"/first"}, + }, { + name: "second", + dir: "second", + before: []string{"first"}, + wantGet: 1, + wantAllDirs: []string{"first", "second"}, + }, { + name: "repeat", + dir: "second", + before: []string{"first", "second", "third"}, + wantGet: 1, + wantAllDirs: []string{"first", "second", "third"}, + }} + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + d := newDirIndex() + for _, b := range tc.before { + d.Get(b) + } + if got := d.Get(tc.dir); got != tc.wantGet { + t.Errorf("d.Get(%q) = %d, want: %d", tc.dir, got, tc.wantGet) + } + if df := cmp.Diff(tc.wantAllDirs, d.AllDirs()); df != "" { + t.Errorf("d.AllDirs() diff (want->got):\n%s", df) + } + }) + } +} diff --git a/internal/rpmpack/file_types.go b/internal/rpmpack/file_types.go new file mode 100644 index 00000000..720f7335 --- /dev/null +++ b/internal/rpmpack/file_types.go @@ -0,0 +1,47 @@ +package rpmpack + +// FileType is the type of a file inside a RPM package. +type FileType int32 + +// https://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/pkgformat.html#AEN27560 +// The RPMFile.Type tag value shall identify various characteristics of the file in the payload that it describes. +// It shall be an INT32 value consisting of either the value GenericFile (0) or +// the bitwise inclusive or of one or more of the following values. Some of these combinations may make no sense +const ( + // GenericFile is just a basic file in an RPM + GenericFile FileType = 1 << iota >> 1 + // ConfigFile is a configuration file, and an existing file should be saved during a + // package upgrade operation and not removed during a package removal operation. + ConfigFile + // DocFile is a file that contains documentation. + DocFile + // DoNotUseFile is reserved for future use; conforming packages may not use this flag. + DoNotUseFile + // MissingOkFile need not exist on the installed system. + MissingOkFile + // NoReplaceFile similar to the ConfigFile, this flag indicates that during an upgrade operation + // the original file on the system should not be altered. + NoReplaceFile + // SpecFile is the package specification file. + SpecFile + // GhostFile is not actually included in the payload, but should still be considered as a part of the package. + // For example, a log file generated by the application at run time. + GhostFile + // LicenceFile contains the license conditions. + LicenceFile + // ReadmeFile contains high level notes about the package. + ReadmeFile + // ExcludeFile is not a part of the package, and should not be installed. + ExcludeFile +) + +// RPMFile contains a particular file's entry and data. +type RPMFile struct { + Name string + Body []byte + Mode uint + Owner string + Group string + MTime uint32 + Type FileType +} diff --git a/internal/rpmpack/file_types_test.go b/internal/rpmpack/file_types_test.go new file mode 100644 index 00000000..76f4c795 --- /dev/null +++ b/internal/rpmpack/file_types_test.go @@ -0,0 +1,32 @@ +package rpmpack + +import ( + "testing" +) + +func TestFileTypeSetting(t *testing.T) { + f := &RPMFile{ + Name: "Test", + } + + if f.Type != GenericFile { + t.Error("New RPMFile.Type should be a generic type") + } + + f.Type |= ConfigFile + if (f.Type & ConfigFile) == 0 { + t.Error("Setting to config file should have the ConfigFile bitmask") + } +} + +func TestFileTypeCombining(t *testing.T) { + f := &RPMFile{ + Name: "Test", + } + + f.Type |= ConfigFile | NoReplaceFile + + if (f.Type&ConfigFile) == 0 || f.Type&NoReplaceFile == 0 { + t.Error("Combining file types should have the bitmask of both") + } +} diff --git a/internal/rpmpack/header.go b/internal/rpmpack/header.go new file mode 100644 index 00000000..1ad9896a --- /dev/null +++ b/internal/rpmpack/header.go @@ -0,0 +1,204 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpmpack + +import ( + "bytes" + "encoding/binary" + "fmt" + "sort" + + "github.com/pkg/errors" +) + +const ( + signatures = 0x3e + immutable = 0x3f + + typeInt16 = 0x03 + typeInt32 = 0x04 + typeString = 0x06 + typeBinary = 0x07 + typeStringArray = 0x08 +) + +// Only integer types are aligned. This is not just an optimization - some versions +// of rpm fail when integers are not aligned. Other versions fail when non-integers are aligned. +var boundaries = map[int]int{ + typeInt16: 2, + typeInt32: 4, +} + +type IndexEntry struct { + rpmtype, count int + data []byte +} + +func (e IndexEntry) indexBytes(tag, contentOffset int) []byte { + b := &bytes.Buffer{} + if err := binary.Write(b, binary.BigEndian, []int32{int32(tag), int32(e.rpmtype), int32(contentOffset), int32(e.count)}); err != nil { + // binary.Write can fail if the underlying Write fails, or the types are invalid. + // bytes.Buffer's write never error out, it can only panic with OOM. + panic(err) + } + return b.Bytes() +} + +func intEntry(rpmtype, size int, value interface{}) IndexEntry { + b := &bytes.Buffer{} + if err := binary.Write(b, binary.BigEndian, value); err != nil { + // binary.Write can fail if the underlying Write fails, or the types are invalid. + // bytes.Buffer's write never error out, it can only panic with OOM. + panic(err) + } + return IndexEntry{rpmtype, size, b.Bytes()} +} + +func EntryInt16(value []int16) IndexEntry { + return intEntry(typeInt16, len(value), value) +} +func EntryUint16(value []uint16) IndexEntry { + return intEntry(typeInt16, len(value), value) +} +func EntryInt32(value []int32) IndexEntry { + return intEntry(typeInt32, len(value), value) +} +func EntryUint32(value []uint32) IndexEntry { + return intEntry(typeInt32, len(value), value) +} +func EntryString(value string) IndexEntry { + return IndexEntry{typeString, 1, append([]byte(value), byte(00))} +} +func EntryBytes(value []byte) IndexEntry { + return IndexEntry{typeBinary, len(value), value} +} + +func EntryStringSlice(value []string) IndexEntry { + b := [][]byte{} + for _, v := range value { + b = append(b, []byte(v)) + } + bb := append(bytes.Join(b, []byte{00}), byte(00)) + return IndexEntry{typeStringArray, len(value), bb} +} + +type index struct { + entries map[int]IndexEntry + h int +} + +func newIndex(h int) *index { + return &index{entries: make(map[int]IndexEntry), h: h} +} +func (i *index) Add(tag int, e IndexEntry) { + i.entries[tag] = e +} +func (i *index) AddEntries(m map[int]IndexEntry) { + for t, e := range m { + i.Add(t, e) + } +} + +func (i *index) sortedTags() []int { + t := []int{} + for k := range i.entries { + t = append(t, k) + } + sort.Ints(t) + return t +} + +func pad(w *bytes.Buffer, rpmtype, offset int) { + // We need to align integer entries... + if b, ok := boundaries[rpmtype]; ok && offset%b != 0 { + if _, err := w.Write(make([]byte, b-offset%b)); err != nil { + // binary.Write can fail if the underlying Write fails, or the types are invalid. + // bytes.Buffer's write never error out, it can only panic with OOM. + panic(err) + } + } +} + +// Bytes returns the bytes of the index. +func (i *index) Bytes() ([]byte, error) { + w := &bytes.Buffer{} + // Even the header has three parts: The lead, the index entries, and the entries. + // Because of alignment, we can only tell the actual size and offset after writing + // the entries. + entryData := &bytes.Buffer{} + tags := i.sortedTags() + offsets := make([]int, len(tags)) + for ii, tag := range tags { + e := i.entries[tag] + pad(entryData, e.rpmtype, entryData.Len()) + offsets[ii] = entryData.Len() + entryData.Write(e.data) + } + entryData.Write(i.eigenHeader().data) + + // 4 magic and 4 reserved + w.Write([]byte{0x8e, 0xad, 0xe8, 0x01, 0, 0, 0, 0}) + // 4 count and 4 size + // We add the pseudo-entry "eigenHeader" to count. + if err := binary.Write(w, binary.BigEndian, []int32{int32(len(i.entries)) + 1, int32(entryData.Len())}); err != nil { + return nil, errors.Wrap(err, "failed to write eigenHeader") + } + // Write the eigenHeader index entry + w.Write(i.eigenHeader().indexBytes(i.h, entryData.Len()-0x10)) + // Write all of the other index entries + for ii, tag := range tags { + e := i.entries[tag] + w.Write(e.indexBytes(tag, offsets[ii])) + } + w.Write(entryData.Bytes()) + return w.Bytes(), nil +} + +// the eigenHeader is a weird entry. Its index entry is sorted first, but its content +// is last. The content is a 16 byte index entry, which is almost the same as the index +// entry except for the offset. The offset here is ... minus the length of the index entry region. +// Which is always 0x10 * number of entries. +// I kid you not. +func (i *index) eigenHeader() IndexEntry { + b := &bytes.Buffer{} + if err := binary.Write(b, binary.BigEndian, []int32{int32(i.h), int32(typeBinary), -int32(0x10 * (len(i.entries) + 1)), int32(0x10)}); err != nil { + // binary.Write can fail if the underlying Write fails, or the types are invalid. + // bytes.Buffer's write never error out, it can only panic with OOM. + panic(err) + } + + return EntryBytes(b.Bytes()) +} + +func lead(name, fullVersion string) []byte { + // RPM format = 0xedabeedb + // version 3.0 = 0x0300 + // type binary = 0x0000 + // machine archnum (i386?) = 0x0001 + // name ( 66 bytes, with null termination) + // osnum (linux?) = 0x0001 + // sig type (header-style) = 0x0005 + // reserved 16 bytes of 0x00 + n := []byte(fmt.Sprintf("%s-%s", name, fullVersion)) + if len(n) > 65 { + n = n[:65] + } + n = append(n, make([]byte, 66-len(n))...) + b := []byte{0xed, 0xab, 0xee, 0xdb, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01} + b = append(b, n...) + b = append(b, []byte{0x00, 0x01, 0x00, 0x05}...) + b = append(b, make([]byte, 16)...) + return b +} diff --git a/internal/rpmpack/header_test.go b/internal/rpmpack/header_test.go new file mode 100644 index 00000000..2d45a612 --- /dev/null +++ b/internal/rpmpack/header_test.go @@ -0,0 +1,114 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpmpack + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestLead(t *testing.T) { + // Only check that the length is always right + names := []string{ + "a", + "ab", + "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc", + "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca", + "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab", + "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc", + } + for _, n := range names { + if got := len(lead(n, "1-2")); got != 0x60 { + t.Errorf("len(lead(%s)) = %#x, want %#x", n, got, 0x60) + } + } +} + +func TestEntry(t *testing.T) { + testCases := []struct { + name string + value interface{} + tag int + offset int + wantIndexBytes string + wantData string + }{{ + name: "simple int", + value: []int32{0x42}, + tag: 0x010d, + offset: 5, + wantIndexBytes: "0000010d000000040000000500000001", + wantData: "00000042", + }, { + name: "simple string", + value: "simple string", + tag: 0x010e, + offset: 0x111, + wantIndexBytes: "0000010e000000060000011100000001", + wantData: "73696d706c6520737472696e6700", + }, { + name: "string array", + value: []string{"string", "array"}, + tag: 0x010f, + offset: 0x222, + wantIndexBytes: "0000010f000000080000022200000002", + wantData: "737472696e6700617272617900", + }} + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var e IndexEntry + switch v := tc.value.(type) { + case []string: + e = EntryStringSlice(v) + case string: + e = EntryString(v) + case []int32: + e = EntryInt32(v) + } + gotBytes := e.indexBytes(tc.tag, tc.offset) + if d := cmp.Diff(tc.wantIndexBytes, fmt.Sprintf("%x", gotBytes)); d != "" { + t.Errorf("entry.indexBytes() unexpected value (want->got):\n%s", d) + } + if d := cmp.Diff(tc.wantData, fmt.Sprintf("%x", e.data)); d != "" { + t.Errorf("entry.data unexpected value (want->got):\n%s", d) + } + }) + } +} + +func TestIndex(t *testing.T) { + i := newIndex(0x3e) + i.AddEntries(map[int]IndexEntry{ + 0x1111: EntryUint16([]uint16{0x4444, 0x8888, 0xcccc}), + 0x2222: EntryUint32([]uint32{0x3333, 0x5555}), + }) + got, err := i.Bytes() + if err != nil { + t.Errorf("i.Bytes() returned error: %v", err) + } + want := "8eade80100000000" + // header lead + "0000000300000020" + // count and size + "0000003e000000070000001000000010" + // eigen header entry + "00001111000000030000000000000003" + + "00002222000000040000000800000002" + + "44448888cccc00000000333300005555" + // values, with padding + "0000003e00000007ffffffd000000010" // eigen header value + if d := cmp.Diff(want, fmt.Sprintf("%x", got)); d != "" { + t.Errorf("i.Bytes() unexpected value (want-> got): \n%s", d) + } +} diff --git a/internal/rpmpack/rpm.go b/internal/rpmpack/rpm.go new file mode 100644 index 00000000..e89509ea --- /dev/null +++ b/internal/rpmpack/rpm.go @@ -0,0 +1,550 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package rpmpack packs files to rpm files. +// It is designed to be simple to use and deploy, not requiring any filesystem access +// to create rpm files. +package rpmpack + +import ( + "bytes" + "crypto/sha256" + "fmt" + "io" + "path" + "sort" + "strconv" + "strings" + "time" + + "github.com/cavaliergopher/cpio" + "github.com/klauspost/compress/zstd" + gzip "github.com/klauspost/pgzip" + "github.com/pkg/errors" + "github.com/ulikunitz/xz" + "github.com/ulikunitz/xz/lzma" +) + +var ( + // ErrWriteAfterClose is returned when a user calls Write() on a closed rpm. + ErrWriteAfterClose = errors.New("rpm write after close") + // ErrWrongFileOrder is returned when files are not sorted by name. + ErrWrongFileOrder = errors.New("wrong file addition order") +) + +// RPMMetaData contains meta info about the whole package. +type RPMMetaData struct { + Name, + Summary, + Description, + Version, + Release, + Arch, + OS, + Vendor, + URL, + Packager, + Group, + Licence, + BuildHost, + Compressor string + Epoch uint32 + BuildTime time.Time + Provides, + Obsoletes, + Suggests, + Recommends, + Requires, + Conflicts Relations +} + +// RPM holds the state of a particular rpm file. Please use NewRPM to instantiate it. +type RPM struct { + RPMMetaData + di *dirIndex + payload *bytes.Buffer + payloadSize uint + cpio *cpio.Writer + basenames []string + dirindexes []uint32 + filesizes []uint32 + filemodes []uint16 + fileowners []string + filegroups []string + filemtimes []uint32 + filedigests []string + filelinktos []string + fileflags []uint32 + closed bool + compressedPayload io.WriteCloser + files map[string]RPMFile + prein string + postin string + preun string + postun string + pretrans string + posttrans string + customTags map[int]IndexEntry + customSigs map[int]IndexEntry + pgpSigner func([]byte) ([]byte, error) +} + +// NewRPM creates and returns a new RPM struct. +func NewRPM(m RPMMetaData) (*RPM, error) { + var err error + + if m.OS == "" { + m.OS = "linux" + } + + if m.Arch == "" { + m.Arch = "noarch" + } + + p := &bytes.Buffer{} + + z, compressorName, err := setupCompressor(m.Compressor, p) + if err != nil { + return nil, err + } + + // only use compressor name for the rpm tag, not the level + m.Compressor = compressorName + + rpm := &RPM{ + RPMMetaData: m, + di: newDirIndex(), + payload: p, + compressedPayload: z, + cpio: cpio.NewWriter(z), + files: make(map[string]RPMFile), + customTags: make(map[int]IndexEntry), + customSigs: make(map[int]IndexEntry), + } + + // A package must provide itself... + rpm.Provides.addIfMissing(&Relation{ + Name: rpm.Name, + Version: rpm.FullVersion(), + Sense: SenseEqual, + }) + + return rpm, nil +} + +func setupCompressor(compressorSetting string, w io.Writer) (wc io.WriteCloser, + compressorType string, err error) { + + parts := strings.Split(compressorSetting, ":") + if len(parts) > 2 { + return nil, "", fmt.Errorf("malformed compressor setting: %s", compressorSetting) + } + + compressorType = parts[0] + compressorLevel := "" + if len(parts) == 2 { + compressorLevel = parts[1] + } + + switch compressorType { + case "": + compressorType = "gzip" + fallthrough + case "gzip": + level := 9 + + if compressorLevel != "" { + var err error + + level, err = strconv.Atoi(compressorLevel) + if err != nil { + return nil, "", fmt.Errorf("parse gzip compressor level: %w", err) + } + } + + wc, err = gzip.NewWriterLevel(w, level) + case "lzma": + if compressorLevel != "" { + return nil, "", fmt.Errorf("no compressor level supported for lzma: %s", compressorLevel) + } + + wc, err = lzma.NewWriter(w) + case "xz": + if compressorLevel != "" { + return nil, "", fmt.Errorf("no compressor level supported for xz: %s", compressorLevel) + } + + wc, err = xz.NewWriter(w) + case "zstd": + level := zstd.SpeedBetterCompression + + if compressorLevel != "" { + var ok bool + + if intLevel, err := strconv.Atoi(compressorLevel); err == nil { + level = zstd.EncoderLevelFromZstd(intLevel) + } else { + ok, level = zstd.EncoderLevelFromString(compressorLevel) + if !ok { + return nil, "", fmt.Errorf("invalid zstd compressor level: %s", compressorLevel) + } + } + } + + wc, err = zstd.NewWriter(w, zstd.WithEncoderLevel(level)) + default: + return nil, "", fmt.Errorf("unknown compressor type: %s", compressorType) + } + + return wc, compressorType, err +} + +// FullVersion properly combines version and release fields to a version string +func (r *RPM) FullVersion() string { + if r.Release != "" { + return fmt.Sprintf("%s-%s", r.Version, r.Release) + } + + return r.Version +} + +// AllowListDirs removes all directories which are not explicitly allowlisted. +func (r *RPM) AllowListDirs(allowList map[string]bool) { + for fn, ff := range r.files { + if ff.Mode&040000 == 040000 { + if !allowList[fn] { + delete(r.files, fn) + } + } + } +} + +// Write closes the rpm and writes the whole rpm to an io.Writer +func (r *RPM) Write(w io.Writer) error { + if r.closed { + return ErrWriteAfterClose + } + // Add all of the files, sorted alphabetically. + fnames := []string{} + for fn := range r.files { + fnames = append(fnames, fn) + } + sort.Strings(fnames) + for _, fn := range fnames { + if err := r.writeFile(r.files[fn]); err != nil { + return errors.Wrapf(err, "failed to write file %q", fn) + } + } + if err := r.cpio.Close(); err != nil { + return errors.Wrap(err, "failed to close cpio payload") + } + if err := r.compressedPayload.Close(); err != nil { + return errors.Wrap(err, "failed to close gzip payload") + } + + if _, err := w.Write(lead(r.Name, r.FullVersion())); err != nil { + return errors.Wrap(err, "failed to write lead") + } + // Write the regular header. + h := newIndex(immutable) + r.writeGenIndexes(h) + + // do not write file indexes if there are no files (meta package) + // doing so will result in an invalid package + if (len(r.files)) > 0 { + r.writeFileIndexes(h) + } + + if err := r.writeRelationIndexes(h); err != nil { + return err + } + // CustomTags must be the last to be added, because they can overwrite values. + h.AddEntries(r.customTags) + hb, err := h.Bytes() + if err != nil { + return errors.Wrap(err, "failed to retrieve header") + } + // Write the signatures + s := newIndex(signatures) + if err := r.writeSignatures(s, hb); err != nil { + return errors.Wrap(err, "failed to create signatures") + } + + s.AddEntries(r.customSigs) + sb, err := s.Bytes() + if err != nil { + return errors.Wrap(err, "failed to retrieve signatures header") + } + + if _, err := w.Write(sb); err != nil { + return errors.Wrap(err, "failed to write signature bytes") + } + // Signatures are padded to 8-byte boundaries + if _, err := w.Write(make([]byte, (8-len(sb)%8)%8)); err != nil { + return errors.Wrap(err, "failed to write signature padding") + } + if _, err := w.Write(hb); err != nil { + return errors.Wrap(err, "failed to write header body") + } + _, err = w.Write(r.payload.Bytes()) + return errors.Wrap(err, "failed to write payload") +} + +// SetPGPSigner registers a function that will accept the header and payload as bytes, +// and return a signature as bytes. The function should simulate what gpg does, +// probably by using golang.org/x/crypto/openpgp or by forking a gpg process. +func (r *RPM) SetPGPSigner(f func([]byte) ([]byte, error)) { + r.pgpSigner = f +} + +// Only call this after the payload and header were written. +func (r *RPM) writeSignatures(sigHeader *index, regHeader []byte) error { + sigHeader.Add(sigSize, EntryInt32([]int32{int32(r.payload.Len() + len(regHeader))})) + sigHeader.Add(sigSHA256, EntryString(fmt.Sprintf("%x", sha256.Sum256(regHeader)))) + sigHeader.Add(sigPayloadSize, EntryInt32([]int32{int32(r.payloadSize)})) + if r.pgpSigner != nil { + // For sha 256 you need to sign the header and payload separately + header := append([]byte{}, regHeader...) + headerSig, err := r.pgpSigner(header) + if err != nil { + return errors.Wrap(err, "call to signer failed") + } + sigHeader.Add(sigRSA, EntryBytes(headerSig)) + + body := append(header, r.payload.Bytes()...) + bodySig, err := r.pgpSigner(body) + if err != nil { + return errors.Wrap(err, "call to signer failed") + } + sigHeader.Add(sigPGP, EntryBytes(bodySig)) + } + return nil +} + +func (r *RPM) writeRelationIndexes(h *index) error { + // add all relation categories + if err := r.Provides.AddToIndex(h, tagProvides, tagProvideVersion, tagProvideFlags); err != nil { + return errors.Wrap(err, "failed to add provides") + } + if err := r.Obsoletes.AddToIndex(h, tagObsoletes, tagObsoleteVersion, tagObsoleteFlags); err != nil { + return errors.Wrap(err, "failed to add obsoletes") + } + if err := r.Suggests.AddToIndex(h, tagSuggests, tagSuggestVersion, tagSuggestFlags); err != nil { + return errors.Wrap(err, "failed to add suggests") + } + if err := r.Recommends.AddToIndex(h, tagRecommends, tagRecommendVersion, tagRecommendFlags); err != nil { + return errors.Wrap(err, "failed to add recommends") + } + if err := r.Requires.AddToIndex(h, tagRequires, tagRequireVersion, tagRequireFlags); err != nil { + return errors.Wrap(err, "failed to add requires") + } + if err := r.Conflicts.AddToIndex(h, tagConflicts, tagConflictVersion, tagConflictFlags); err != nil { + return errors.Wrap(err, "failed to add conflicts") + } + + return nil +} + +// AddCustomTag adds or overwrites a tag value in the index. +func (r *RPM) AddCustomTag(tag int, e IndexEntry) { + r.customTags[tag] = e +} + +// AddCustomSig adds or overwrites a signature tag value. +func (r *RPM) AddCustomSig(tag int, e IndexEntry) { + r.customSigs[tag] = e +} + +func (r *RPM) writeGenIndexes(h *index) { + h.Add(tagHeaderI18NTable, EntryString("C")) + h.Add(tagSize, EntryInt32([]int32{int32(r.payloadSize)})) + h.Add(tagName, EntryString(r.Name)) + h.Add(tagVersion, EntryString(r.Version)) + h.Add(tagEpoch, EntryUint32([]uint32{r.Epoch})) + h.Add(tagSummary, EntryString(r.Summary)) + h.Add(tagDescription, EntryString(r.Description)) + h.Add(tagBuildHost, EntryString(r.BuildHost)) + if !r.BuildTime.IsZero() { + // time.Time zero value is confusing, avoid if not supplied + // see https://github.com/google/rpmpack/issues/43 + h.Add(tagBuildTime, EntryInt32([]int32{int32(r.BuildTime.Unix())})) + } + h.Add(tagRelease, EntryString(r.Release)) + h.Add(tagPayloadFormat, EntryString("cpio")) + h.Add(tagPayloadCompressor, EntryString(r.Compressor)) + h.Add(tagPayloadFlags, EntryString("9")) + h.Add(tagArch, EntryString(r.Arch)) + h.Add(tagOS, EntryString(r.OS)) + h.Add(tagVendor, EntryString(r.Vendor)) + h.Add(tagLicence, EntryString(r.Licence)) + h.Add(tagPackager, EntryString(r.Packager)) + h.Add(tagGroup, EntryString(r.Group)) + h.Add(tagURL, EntryString(r.URL)) + h.Add(tagPayloadDigest, EntryStringSlice([]string{fmt.Sprintf("%x", sha256.Sum256(r.payload.Bytes()))})) + h.Add(tagPayloadDigestAlgo, EntryInt32([]int32{hashAlgoSHA256})) + + // rpm utilities look for the sourcerpm tag to deduce if this is not a source rpm (if it has a sourcerpm, + // it is NOT a source rpm). + h.Add(tagSourceRPM, EntryString(fmt.Sprintf("%s-%s.src.rpm", r.Name, r.FullVersion()))) + if r.pretrans != "" { + h.Add(tagPretrans, EntryString(r.pretrans)) + h.Add(tagPretransProg, EntryString("/bin/sh")) + } + if r.prein != "" { + h.Add(tagPrein, EntryString(r.prein)) + h.Add(tagPreinProg, EntryString("/bin/sh")) + } + if r.postin != "" { + h.Add(tagPostin, EntryString(r.postin)) + h.Add(tagPostinProg, EntryString("/bin/sh")) + } + if r.preun != "" { + h.Add(tagPreun, EntryString(r.preun)) + h.Add(tagPreunProg, EntryString("/bin/sh")) + } + if r.postun != "" { + h.Add(tagPostun, EntryString(r.postun)) + h.Add(tagPostunProg, EntryString("/bin/sh")) + } + if r.posttrans != "" { + h.Add(tagPosttrans, EntryString(r.posttrans)) + h.Add(tagPosttransProg, EntryString("/bin/sh")) + } +} + +// WriteFileIndexes writes file related index headers to the header +func (r *RPM) writeFileIndexes(h *index) { + h.Add(tagBasenames, EntryStringSlice(r.basenames)) + h.Add(tagDirindexes, EntryUint32(r.dirindexes)) + h.Add(tagDirnames, EntryStringSlice(r.di.AllDirs())) + h.Add(tagFileSizes, EntryUint32(r.filesizes)) + h.Add(tagFileModes, EntryUint16(r.filemodes)) + h.Add(tagFileUserName, EntryStringSlice(r.fileowners)) + h.Add(tagFileGroupName, EntryStringSlice(r.filegroups)) + h.Add(tagFileMTimes, EntryUint32(r.filemtimes)) + h.Add(tagFileDigests, EntryStringSlice(r.filedigests)) + h.Add(tagFileLinkTos, EntryStringSlice(r.filelinktos)) + h.Add(tagFileFlags, EntryUint32(r.fileflags)) + + inodes := make([]int32, len(r.dirindexes)) + digestAlgo := make([]int32, len(r.dirindexes)) + verifyFlags := make([]int32, len(r.dirindexes)) + fileRDevs := make([]int16, len(r.dirindexes)) + fileLangs := make([]string, len(r.dirindexes)) + + for ii := range inodes { + // is inodes just a range from 1..len(dirindexes)? maybe different with hard links + inodes[ii] = int32(ii + 1) + digestAlgo[ii] = hashAlgoSHA256 + // With regular files, it seems like we can always enable all of the verify flags + verifyFlags[ii] = int32(-1) + fileRDevs[ii] = int16(1) + } + h.Add(tagFileINodes, EntryInt32(inodes)) + h.Add(tagFileDigestAlgo, EntryInt32(digestAlgo)) + h.Add(tagFileVerifyFlags, EntryInt32(verifyFlags)) + h.Add(tagFileRDevs, EntryInt16(fileRDevs)) + h.Add(tagFileLangs, EntryStringSlice(fileLangs)) +} + +// AddPretrans adds a pretrans scriptlet +func (r *RPM) AddPretrans(s string) { + r.pretrans = s +} + +// AddPrein adds a prein scriptlet +func (r *RPM) AddPrein(s string) { + r.prein = s +} + +// AddPostin adds a postin scriptlet +func (r *RPM) AddPostin(s string) { + r.postin = s +} + +// AddPreun adds a preun scriptlet +func (r *RPM) AddPreun(s string) { + r.preun = s +} + +// AddPostun adds a postun scriptlet +func (r *RPM) AddPostun(s string) { + r.postun = s +} + +// AddPosttrans adds a posttrans scriptlet +func (r *RPM) AddPosttrans(s string) { + r.posttrans = s +} + +// AddFile adds an RPMFile to an existing rpm. +func (r *RPM) AddFile(f RPMFile) { + if f.Name == "/" { // rpm does not allow the root dir to be included. + return + } + r.files[f.Name] = f +} + +// writeFile writes the file to the indexes and cpio. +func (r *RPM) writeFile(f RPMFile) error { + dir, file := path.Split(f.Name) + r.dirindexes = append(r.dirindexes, r.di.Get(dir)) + r.basenames = append(r.basenames, file) + r.fileowners = append(r.fileowners, f.Owner) + r.filegroups = append(r.filegroups, f.Group) + r.filemtimes = append(r.filemtimes, f.MTime) + r.fileflags = append(r.fileflags, uint32(f.Type)) + + links := 1 + switch { + case f.Mode&040000 != 0: // directory + r.filesizes = append(r.filesizes, 4096) + r.filedigests = append(r.filedigests, "") + r.filelinktos = append(r.filelinktos, "") + links = 2 + case f.Mode&0120000 == 0120000: // symlink + r.filesizes = append(r.filesizes, uint32(len(f.Body))) + r.filedigests = append(r.filedigests, "") + r.filelinktos = append(r.filelinktos, string(f.Body)) + default: // regular file + f.Mode = f.Mode | 0100000 + r.filesizes = append(r.filesizes, uint32(len(f.Body))) + r.filedigests = append(r.filedigests, fmt.Sprintf("%x", sha256.Sum256(f.Body))) + r.filelinktos = append(r.filelinktos, "") + } + r.filemodes = append(r.filemodes, uint16(f.Mode)) + + // Ghost files have no payload + if f.Type == GhostFile { + return nil + } + return r.writePayload(f, links) +} + +func (r *RPM) writePayload(f RPMFile, links int) error { + hdr := &cpio.Header{ + Name: f.Name, + Mode: cpio.FileMode(f.Mode), + Size: int64(len(f.Body)), + Links: links, + } + if err := r.cpio.WriteHeader(hdr); err != nil { + return errors.Wrap(err, "failed to write payload file header") + } + if _, err := r.cpio.Write(f.Body); err != nil { + return errors.Wrap(err, "failed to write payload file content") + } + r.payloadSize += uint(len(f.Body)) + return nil +} diff --git a/internal/rpmpack/rpm_test.go b/internal/rpmpack/rpm_test.go new file mode 100644 index 00000000..5713b120 --- /dev/null +++ b/internal/rpmpack/rpm_test.go @@ -0,0 +1,183 @@ +package rpmpack + +import ( + "io" + "io/ioutil" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/klauspost/compress/zstd" + gzip "github.com/klauspost/pgzip" + "github.com/ulikunitz/xz" + "github.com/ulikunitz/xz/lzma" +) + +func TestFileOwner(t *testing.T) { + r, err := NewRPM(RPMMetaData{}) + if err != nil { + t.Fatalf("NewRPM returned error %v", err) + } + group := "testGroup" + user := "testUser" + + r.AddFile(RPMFile{ + Name: "/usr/local/hello", + Body: []byte("content of the file"), + Group: group, + Owner: user, + }) + + if err := r.Write(ioutil.Discard); err != nil { + t.Errorf("NewRPM returned error %v", err) + } + if r.fileowners[0] != user { + t.Errorf("File owner shoud be %s but is %s", user, r.fileowners[0]) + } + if r.filegroups[0] != group { + t.Errorf("File owner shoud be %s but is %s", group, r.filegroups[0]) + } +} + +// https://github.com/google/rpmpack/issues/49 +func Test100644(t *testing.T) { + r, err := NewRPM(RPMMetaData{}) + if err != nil { + t.Fatalf("NewRPM returned error %v", err) + } + r.AddFile(RPMFile{ + Name: "/usr/local/hello", + Body: []byte("content of the file"), + Mode: 0100644, + }) + + if err := r.Write(ioutil.Discard); err != nil { + t.Errorf("Write returned error %v", err) + } + if r.filemodes[0] != 0100644 { + t.Errorf("file mode want 0100644, got %o", r.filemodes[0]) + } + if r.filelinktos[0] != "" { + t.Errorf("linktos want empty (not a symlink), got %q", r.filelinktos[0]) + } +} + +func TestCompression(t *testing.T) { + testCases := []struct { + Type string + Compressors []string + ExpectedWriter io.Writer + }{ + { + Type: "gzip", + Compressors: []string{ + "", "gzip", "gzip:1", "gzip:2", "gzip:3", + "gzip:4", "gzip:5", "gzip:6", "gzip:7", "gzip:8", "gzip:9", + }, + ExpectedWriter: &gzip.Writer{}, + }, + { + Type: "gzip", + Compressors: []string{"gzip:fast", "gzip:10"}, + ExpectedWriter: nil, // gzip requires an integer level from -2 to 9 + }, + { + Type: "lzma", + Compressors: []string{"lzma"}, + ExpectedWriter: &lzma.Writer{}, + }, + { + Type: "lzma", + Compressors: []string{"lzma:fast", "lzma:1"}, + ExpectedWriter: nil, // lzma does not support specifying the compression level + }, + { + Type: "xz", + Compressors: []string{"xz"}, + ExpectedWriter: &xz.Writer{}, + }, + { + Type: "xz", + Compressors: []string{"xz:fast", "xz:1"}, + ExpectedWriter: nil, // xz does not support specifying the compression level + }, + { + Type: "zstd", + Compressors: []string{ + "zstd", "zstd:fastest", "zstd:default", "zstd:better", + "zstd:best", "zstd:BeSt", "zstd:0", "zstd:4", "zstd:8", "zstd:15", + }, + ExpectedWriter: &zstd.Encoder{}, + }, + { + Type: "zstd", + Compressors: []string{"xz:worst"}, + ExpectedWriter: nil, // only integers levels or one of the pre-defined string values + }, + } + + for _, testCase := range testCases { + testCase := testCase + + for _, compressor := range testCase.Compressors { + t.Run(compressor, func(t *testing.T) { + r, err := NewRPM(RPMMetaData{ + Compressor: compressor, + }) + if err != nil { + if testCase.ExpectedWriter == nil { + return // an error is expected + } + + t.Fatalf("NewRPM returned error %v", err) + } + + if testCase.ExpectedWriter == nil { + t.Fatalf("compressor %q should have produced an error", compressor) + } + + if r.RPMMetaData.Compressor != testCase.Type { + t.Fatalf("expected compressor %q, got %q", compressor, + r.RPMMetaData.Compressor) + } + + expectedWriterType := reflect.Indirect(reflect.ValueOf( + testCase.ExpectedWriter)).String() + actualWriterType := reflect.Indirect(reflect.ValueOf( + r.compressedPayload)).String() + + if expectedWriterType != actualWriterType { + t.Fatalf("expected writer to be %T, got %T instead", + testCase.ExpectedWriter, r.compressedPayload) + } + }) + } + } +} + +func TestAllowListDirs(t *testing.T) { + r, err := NewRPM(RPMMetaData{}) + if err != nil { + t.Fatalf("NewRPM returned error %v", err) + } + + r.AddFile(RPMFile{ + Name: "/usr/local/dir1", + Mode: 040000, + }) + r.AddFile(RPMFile{ + Name: "/usr/local/dir2", + Mode: 040000, + }) + + r.AllowListDirs(map[string]bool{"/usr/local/dir1": true}) + + if err := r.Write(ioutil.Discard); err != nil { + t.Errorf("NewRPM returned error %v", err) + } + expected := map[string]RPMFile{"/usr/local/dir1": {Name: "/usr/local/dir1", Mode: 040000}} + if d := cmp.Diff(expected, r.files); d != "" { + t.Errorf("Expected dirs differs (want->got):\n%v", d) + } +} diff --git a/internal/rpmpack/sense.go b/internal/rpmpack/sense.go new file mode 100644 index 00000000..cf97cad0 --- /dev/null +++ b/internal/rpmpack/sense.go @@ -0,0 +1,168 @@ +package rpmpack + +import ( + "fmt" + "regexp" + "strings" +) + +type rpmSense uint32 + +// https://github.com/rpm-software-management/rpm/blob/ab01b5eacf9ec6a07a5d9e1991ef476a12d264fd/include/rpm/rpmds.h#L27 +// SenseAny (0) specifies no specific version compare +// SenseLess (2) specifies less then the specified version +// SenseGreater (4) specifies greater then the specified version +// SenseEqual (8) specifies equal to the specified version +const ( + SenseAny rpmSense = 0 + SenseLess = 1 << iota + SenseGreater + SenseEqual + SenseRPMLIB rpmSense = 1 << 24 +) + +var relationMatch = regexp.MustCompile(`([^=<>\s]*)\s*((?:=|>|<)*)\s*(.*)?`) + +// Relation is the structure of rpm sense relationships +type Relation struct { + Name string + Version string + Sense rpmSense +} + +// String return the string representation of the Relation +func (r *Relation) String() string { + return fmt.Sprintf("%s%v%s", r.Name, r.Sense, r.Version) +} + +// Equal compare the equality of two relations +func (r *Relation) Equal(o *Relation) bool { + return r.Name == o.Name && r.Version == o.Version && r.Sense == o.Sense +} + +// Relations is a slice of Relation pointers +type Relations []*Relation + +// String return the string representation of the Relations +func (r *Relations) String() string { + var val []string + for _, rel := range *r { + val = append(val, rel.String()) + } + return strings.Join(val, ",") +} + +// Set parse a string into a Relation and append it to the Relations slice if it is missing +// this is used by the flag package +func (r *Relations) Set(value string) error { + relation, err := NewRelation(value) + if err != nil { + return err + } + r.addIfMissing(relation) + + return nil +} + +func (r *Relations) addIfMissing(value *Relation) { + for _, relation := range *r { + if relation.Equal(value) { + return + } + } + + *r = append(*r, value) +} + +// AddToIndex add the relations to the specified category on the index +func (r *Relations) AddToIndex(h *index, nameTag, versionTag, flagsTag int) error { + var ( + num = len(*r) + names = make([]string, num) + versions = make([]string, num) + flags = make([]uint32, num) + ) + + if num == 0 { + return nil + } + + for idx, relation := range *r { + names[idx] = relation.Name + versions[idx] = relation.Version + flags[idx] = uint32(relation.Sense) + } + + h.Add(nameTag, EntryStringSlice(names)) + h.Add(versionTag, EntryStringSlice(versions)) + h.Add(flagsTag, EntryUint32(flags)) + + return nil +} + +// NewRelation parse a string into a Relation +func NewRelation(related string) (*Relation, error) { + var ( + err error + sense rpmSense + name, + version string + ) + + if strings.HasPrefix(related, "(") && strings.HasSuffix(related, ")") { + // This is a `rich` dependency which must be parsed at install time + // https://rpm-software-management.github.io/rpm/manual/boolean_dependencies.html + sense = SenseAny + name = related + } else { + parts := relationMatch.FindStringSubmatch(related) + if sense, err = parseSense(parts[2]); err != nil { + return nil, err + } + name = parts[1] + version = parts[3] + } + + return &Relation{ + Name: name, + Version: version, + Sense: sense, + }, nil +} + +var stringToSense = map[string]rpmSense{ + "": SenseAny, + "<": SenseLess, + ">": SenseGreater, + "=": SenseEqual, + "<=": SenseLess | SenseEqual, + ">=": SenseGreater | SenseEqual, +} + +// String return the string representation of the rpmSense +func (r rpmSense) String() string { + var ( + val rpmSense + ret string + ) + + for ret, val = range stringToSense { + if r == val { + return ret + } + } + + return "unknown" +} + +func parseSense(sense string) (rpmSense, error) { + var ( + ret rpmSense + ok bool + ) + if ret, ok = stringToSense[sense]; !ok { + return SenseAny, fmt.Errorf("unknown sense value: %s", sense) + } + + return ret, nil +} diff --git a/internal/rpmpack/sense_test.go b/internal/rpmpack/sense_test.go new file mode 100644 index 00000000..17f76203 --- /dev/null +++ b/internal/rpmpack/sense_test.go @@ -0,0 +1,81 @@ +package rpmpack + +import ( + "testing" +) + +func TestNewRelation(t *testing.T) { + testCases := []struct { + input, output string + errExpected bool + }{ + { + input: "python >= 3.7", + output: "python>=3.7", + }, + { + input: "python", + output: "python", + }, + { + input: "python=2", + output: "python=2", + }, + { + input: "python >=3.5", + output: "python>=3.5", + }, + { + input: "python >< 3.5", + output: "", + errExpected: true, + }, + { + input: "python <> 3.5", + output: "", + errExpected: true, + }, + { + input: "python == 3.5", + output: "", + errExpected: true, + }, + { + input: "python =< 3.5", + output: "", + errExpected: true, + }, + { + input: "python => 3.5", + output: "", + errExpected: true, + }, + } + + for _, tc := range testCases { + testCase := tc + t.Run(testCase.input, func(tt *testing.T) { + relation, err := NewRelation(testCase.input) + switch { + case testCase.errExpected && err == nil: + tt.Errorf("%s should have returned an error", testCase.input) + return + case !testCase.errExpected && err != nil: + tt.Errorf("%s should not have returned an error: %v", testCase.input, err) + return + case testCase.errExpected && err != nil: + return + } + + if relation == nil { + tt.Errorf("%s should not have returned a nil relation", testCase.input) + return + } + + val := relation.String() + if !testCase.errExpected && val != testCase.output { + tt.Errorf("%s should have returned %s not %s", testCase.input, testCase.output, val) + } + }) + } +} diff --git a/internal/rpmpack/tags.go b/internal/rpmpack/tags.go new file mode 100644 index 00000000..43599298 --- /dev/null +++ b/internal/rpmpack/tags.go @@ -0,0 +1,101 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rpmpack + +// Define only tags which we actually use +// https://github.com/rpm-software-management/rpm/blob/master/lib/rpmtag.h +const ( + tagHeaderI18NTable = 0x64 // 100 + // Signature tags are obiously overlapping regular header tags.. + sigRSA = 0x010c // 256 + sigSHA256 = 0x0111 // 273 + sigSize = 0x03e8 // 1000 + sigPGP = 0x03ea // 1002 + sigPayloadSize = 0x03ef // 1007 + + // https://github.com/rpm-software-management/rpm/blob/92eadae94c48928bca90693ad63c46ceda37d81f/rpmio/rpmpgp.h#L258 + hashAlgoSHA256 = 0x0008 // 8 + + tagName = 0x03e8 // 1000 + tagVersion = 0x03e9 // 1001 + tagRelease = 0x03ea // 1002 + tagEpoch = 0x03eb // 1003 + tagSummary = 0x03ec // 1004 + tagDescription = 0x03ed // 1005 + tagBuildTime = 0x03ee // 1006 + tagBuildHost = 0x03ef // 1007 + tagSize = 0x03f1 // 1009 + tagVendor = 0x03f3 // 1011 + tagLicence = 0x03f6 // 1014 + tagPackager = 0x03f7 // 1015 + tagGroup = 0x03f8 // 1016 + tagURL = 0x03fc // 1020 + tagOS = 0x03fd // 1021 + tagArch = 0x03fe // 1022 + + tagPrein = 0x03ff // 1023 + tagPostin = 0x0400 // 1024 + tagPreun = 0x0401 // 1025 + tagPostun = 0x0402 // 1026 + + tagFileSizes = 0x0404 // 1028 + tagFileModes = 0x0406 // 1030 + tagFileRDevs = 0x0409 // 1033 + tagFileMTimes = 0x040a // 1034 + tagFileDigests = 0x040b // 1035 + tagFileLinkTos = 0x040c // 1036 + tagFileFlags = 0x040d // 1037 + tagFileUserName = 0x040f // 1039 + tagFileGroupName = 0x0410 // 1040 + tagSourceRPM = 0x0414 // 1044 + tagFileVerifyFlags = 0x0415 // 1045 + tagProvides = 0x0417 // 1047 + tagRequireFlags = 0x0418 // 1048 + tagRequires = 0x0419 // 1049 + tagRequireVersion = 0x041a // 1050 + tagConflictFlags = 0x041d // 1053 + tagConflicts = 0x041e // 1054 + tagConflictVersion = 0x041f // 1055 + tagPreinProg = 0x043d // 1085 + tagPostinProg = 0x043e // 1086 + tagPreunProg = 0x043f // 1087 + tagPostunProg = 0x0440 // 1088 + tagObsoletes = 0x0442 // 1090 + tagFileINodes = 0x0448 // 1096 + tagFileLangs = 0x0449 // 1097 + tagProvideFlags = 0x0458 // 1112 + tagProvideVersion = 0x0459 // 1113 + tagObsoleteFlags = 0x045a // 1114 + tagObsoleteVersion = 0x045b // 1115 + tagDirindexes = 0x045c // 1116 + tagBasenames = 0x045d // 1117 + tagDirnames = 0x045e // 1118 + tagPayloadFormat = 0x0464 // 1124 + tagPayloadCompressor = 0x0465 // 1125 + tagPayloadFlags = 0x0466 // 1126 + tagPretrans = 0x047f // 1151 + tagPosttrans = 0x0480 // 1152 + tagPretransProg = 0x0481 // 1153 + tagPosttransProg = 0x0482 // 1154 + tagFileDigestAlgo = 0x1393 // 5011 + tagRecommends = 0x13b6 // 5046 + tagRecommendVersion = 0x13b7 // 5047 + tagRecommendFlags = 0x13b8 // 5048 + tagSuggests = 0x13b9 // 5049 + tagSuggestVersion = 0x13ba // 5050 + tagSuggestFlags = 0x13bb // 5051 + tagPayloadDigest = 0x13e4 // 5092 + tagPayloadDigestAlgo = 0x13e5 // 5093 +) diff --git a/rpm/rpm.go b/rpm/rpm.go index bf33e992..fde06017 100644 --- a/rpm/rpm.go +++ b/rpm/rpm.go @@ -11,10 +11,10 @@ import ( "strings" "time" - "github.com/google/rpmpack" "github.com/goreleaser/chglog" "github.com/goreleaser/nfpm/v2" "github.com/goreleaser/nfpm/v2/files" + "github.com/goreleaser/nfpm/v2/internal/rpmpack" "github.com/goreleaser/nfpm/v2/internal/sign" ) From 61f0e0b6dfbdb5675c03851fd5190905862f89a0 Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Sat, 24 Sep 2022 09:48:25 -0700 Subject: [PATCH 3/4] test: fix linter issues --- internal/rpmpack/dir.go | 1 - internal/rpmpack/header.go | 11 +++++++++-- internal/rpmpack/rpm.go | 12 ++++++------ internal/rpmpack/rpm_test.go | 17 ++++++++--------- nfpm.go | 17 ++++++++--------- nfpm_test.go | 10 +++++----- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/internal/rpmpack/dir.go b/internal/rpmpack/dir.go index 631b29d4..031cad14 100644 --- a/internal/rpmpack/dir.go +++ b/internal/rpmpack/dir.go @@ -25,7 +25,6 @@ func newDirIndex() *dirIndex { } func (d *dirIndex) Get(value string) uint32 { - if idx, ok := d.m[value]; ok { return idx } diff --git a/internal/rpmpack/header.go b/internal/rpmpack/header.go index 1ad9896a..b223672e 100644 --- a/internal/rpmpack/header.go +++ b/internal/rpmpack/header.go @@ -69,18 +69,23 @@ func intEntry(rpmtype, size int, value interface{}) IndexEntry { func EntryInt16(value []int16) IndexEntry { return intEntry(typeInt16, len(value), value) } + func EntryUint16(value []uint16) IndexEntry { return intEntry(typeInt16, len(value), value) } + func EntryInt32(value []int32) IndexEntry { return intEntry(typeInt32, len(value), value) } + func EntryUint32(value []uint32) IndexEntry { return intEntry(typeInt32, len(value), value) } + func EntryString(value string) IndexEntry { - return IndexEntry{typeString, 1, append([]byte(value), byte(00))} + return IndexEntry{typeString, 1, append([]byte(value), byte(0o0))} } + func EntryBytes(value []byte) IndexEntry { return IndexEntry{typeBinary, len(value), value} } @@ -90,7 +95,7 @@ func EntryStringSlice(value []string) IndexEntry { for _, v := range value { b = append(b, []byte(v)) } - bb := append(bytes.Join(b, []byte{00}), byte(00)) + bb := append(bytes.Join(b, []byte{0o0}), byte(0o0)) return IndexEntry{typeStringArray, len(value), bb} } @@ -102,9 +107,11 @@ type index struct { func newIndex(h int) *index { return &index{entries: make(map[int]IndexEntry), h: h} } + func (i *index) Add(tag int, e IndexEntry) { i.entries[tag] = e } + func (i *index) AddEntries(m map[int]IndexEntry) { for t, e := range m { i.Add(t, e) diff --git a/internal/rpmpack/rpm.go b/internal/rpmpack/rpm.go index e89509ea..2c243e41 100644 --- a/internal/rpmpack/rpm.go +++ b/internal/rpmpack/rpm.go @@ -144,8 +144,8 @@ func NewRPM(m RPMMetaData) (*RPM, error) { } func setupCompressor(compressorSetting string, w io.Writer) (wc io.WriteCloser, - compressorType string, err error) { - + compressorType string, err error, +) { parts := strings.Split(compressorSetting, ":") if len(parts) > 2 { return nil, "", fmt.Errorf("malformed compressor setting: %s", compressorSetting) @@ -222,7 +222,7 @@ func (r *RPM) FullVersion() string { // AllowListDirs removes all directories which are not explicitly allowlisted. func (r *RPM) AllowListDirs(allowList map[string]bool) { for fn, ff := range r.files { - if ff.Mode&040000 == 040000 { + if ff.Mode&0o40000 == 0o40000 { if !allowList[fn] { delete(r.files, fn) } @@ -508,17 +508,17 @@ func (r *RPM) writeFile(f RPMFile) error { links := 1 switch { - case f.Mode&040000 != 0: // directory + case f.Mode&0o40000 != 0: // directory r.filesizes = append(r.filesizes, 4096) r.filedigests = append(r.filedigests, "") r.filelinktos = append(r.filelinktos, "") links = 2 - case f.Mode&0120000 == 0120000: // symlink + case f.Mode&0o120000 == 0o120000: // symlink r.filesizes = append(r.filesizes, uint32(len(f.Body))) r.filedigests = append(r.filedigests, "") r.filelinktos = append(r.filelinktos, string(f.Body)) default: // regular file - f.Mode = f.Mode | 0100000 + f.Mode = f.Mode | 0o100000 r.filesizes = append(r.filesizes, uint32(len(f.Body))) r.filedigests = append(r.filedigests, fmt.Sprintf("%x", sha256.Sum256(f.Body))) r.filelinktos = append(r.filelinktos, "") diff --git a/internal/rpmpack/rpm_test.go b/internal/rpmpack/rpm_test.go index 5713b120..f21597a6 100644 --- a/internal/rpmpack/rpm_test.go +++ b/internal/rpmpack/rpm_test.go @@ -2,7 +2,6 @@ package rpmpack import ( "io" - "io/ioutil" "reflect" "testing" @@ -29,7 +28,7 @@ func TestFileOwner(t *testing.T) { Owner: user, }) - if err := r.Write(ioutil.Discard); err != nil { + if err := r.Write(io.Discard); err != nil { t.Errorf("NewRPM returned error %v", err) } if r.fileowners[0] != user { @@ -49,13 +48,13 @@ func Test100644(t *testing.T) { r.AddFile(RPMFile{ Name: "/usr/local/hello", Body: []byte("content of the file"), - Mode: 0100644, + Mode: 0o100644, }) - if err := r.Write(ioutil.Discard); err != nil { + if err := r.Write(io.Discard); err != nil { t.Errorf("Write returned error %v", err) } - if r.filemodes[0] != 0100644 { + if r.filemodes[0] != 0o100644 { t.Errorf("file mode want 0100644, got %o", r.filemodes[0]) } if r.filelinktos[0] != "" { @@ -164,19 +163,19 @@ func TestAllowListDirs(t *testing.T) { r.AddFile(RPMFile{ Name: "/usr/local/dir1", - Mode: 040000, + Mode: 0o40000, }) r.AddFile(RPMFile{ Name: "/usr/local/dir2", - Mode: 040000, + Mode: 0o40000, }) r.AllowListDirs(map[string]bool{"/usr/local/dir1": true}) - if err := r.Write(ioutil.Discard); err != nil { + if err := r.Write(io.Discard); err != nil { t.Errorf("NewRPM returned error %v", err) } - expected := map[string]RPMFile{"/usr/local/dir1": {Name: "/usr/local/dir1", Mode: 040000}} + expected := map[string]RPMFile{"/usr/local/dir1": {Name: "/usr/local/dir1", Mode: 0o40000}} if d := cmp.Diff(expected, r.files); d != "" { t.Errorf("Expected dirs differs (want->got):\n%v", d) } diff --git a/nfpm.go b/nfpm.go index 5430ded2..9f6301d4 100644 --- a/nfpm.go +++ b/nfpm.go @@ -106,7 +106,7 @@ type Packager interface { // Config contains the top level configuration for packages. type Config struct { Info `yaml:",inline" json:",inline"` - Overrides map[string]Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager,enum=apk,enum=deb,enum=rpm"` + Overrides map[string]*Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager,enum=apk,enum=deb,enum=rpm"` envMappingFunc func(string) string } @@ -173,14 +173,13 @@ func (c *Config) expandEnvVars() { c.Info.Version = os.Expand(c.Info.Version, c.envMappingFunc) c.Info.Prerelease = os.Expand(c.Info.Prerelease, c.envMappingFunc) c.Info.Arch = os.Expand(c.Info.Arch, c.envMappingFunc) - for or, override := range c.Overrides { - override.Conflicts = c.expandEnvVarsStringSlice(override.Conflicts) - override.Depends = c.expandEnvVarsStringSlice(override.Depends) - override.Replaces = c.expandEnvVarsStringSlice(override.Replaces) - override.Recommends = c.expandEnvVarsStringSlice(override.Recommends) - override.Provides = c.expandEnvVarsStringSlice(override.Provides) - override.Suggests = c.expandEnvVarsStringSlice(override.Suggests) - c.Overrides[or] = override + for or := range c.Overrides { + c.Overrides[or].Conflicts = c.expandEnvVarsStringSlice(c.Overrides[or].Conflicts) + c.Overrides[or].Depends = c.expandEnvVarsStringSlice(c.Overrides[or].Depends) + c.Overrides[or].Replaces = c.expandEnvVarsStringSlice(c.Overrides[or].Replaces) + c.Overrides[or].Recommends = c.expandEnvVarsStringSlice(c.Overrides[or].Recommends) + c.Overrides[or].Provides = c.expandEnvVarsStringSlice(c.Overrides[or].Provides) + c.Overrides[or].Suggests = c.expandEnvVarsStringSlice(c.Overrides[or].Suggests) } // Maintainer and vendor fields diff --git a/nfpm_test.go b/nfpm_test.go index 536abafc..35f62f78 100644 --- a/nfpm_test.go +++ b/nfpm_test.go @@ -163,11 +163,11 @@ func TestValidateError(t *testing.T) { Arch: "asd", }, } { - err := err - info := info - t.Run(err, func(t *testing.T) { - require.EqualError(t, nfpm.Validate(&info), err) - }) + func(inf *nfpm.Info, e string) { + t.Run(e, func(t *testing.T) { + require.EqualError(t, nfpm.Validate(inf), e) + }) + }(&info, err) } } From a99a443b30586ff1f701bfca1d97b559c7989e5b Mon Sep 17 00:00:00 2001 From: Dj Gilcrease Date: Sat, 24 Sep 2022 09:59:10 -0700 Subject: [PATCH 4/4] test: fix ruleguard issues --- nfpm_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nfpm_test.go b/nfpm_test.go index 35f62f78..437dbbec 100644 --- a/nfpm_test.go +++ b/nfpm_test.go @@ -153,7 +153,7 @@ func TestValidate(t *testing.T) { } func TestValidateError(t *testing.T) { - for err, info := range map[string]nfpm.Info{ + for err, info := range map[string]*nfpm.Info{ "package name must be provided": {}, "package arch must be provided": { Name: "fo", @@ -167,7 +167,7 @@ func TestValidateError(t *testing.T) { t.Run(e, func(t *testing.T) { require.EqualError(t, nfpm.Validate(inf), e) }) - }(&info, err) + }(info, err) } }