├── .bazelversion ├── .gitignore ├── BUILD.bazel ├── CONTRIBUTING.md ├── LICENSE ├── MODULE.bazel ├── MODULE.bazel.lock ├── README.md ├── WORKSPACE ├── cloudbuild.yaml ├── cmd ├── rpmsample │ ├── BUILD.bazel │ └── main.go └── tar2rpm │ ├── BUILD.bazel │ └── main.go ├── def.bzl ├── dir.go ├── dir_test.go ├── example_bazel ├── .bazelrc ├── .bazelversion ├── BUILD.bazel ├── MODULE.bazel ├── MODULE.bazel.lock ├── allowlist_var_lib_rpmpack.txt ├── content1.txt ├── diff_test.sh ├── dir_allowlist.txt ├── docker_run.sh └── testing.bzl ├── file_types.go ├── file_types_test.go ├── go.mod ├── go.sum ├── header.go ├── header_test.go ├── rpm.go ├── rpm_test.go ├── sense.go ├── sense_test.go ├── tags.go ├── tar.go └── tar_test.go /.bazelversion: -------------------------------------------------------------------------------- 1 | 7.3.2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /bazel-* 3 | /example_bazel/bazel-* 4 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | # A build file for rpmpack. 4 | # For running basic build/run/test you can also use the regular go tools, 5 | # this is currently added to assist in external testing. 6 | 7 | load("@gazelle//:def.bzl", "gazelle") 8 | 9 | gazelle(name = "gazelle") 10 | 11 | go_library( 12 | name = "rpmpack", 13 | srcs = [ 14 | "dir.go", 15 | "file_types.go", 16 | "header.go", 17 | "rpm.go", 18 | "sense.go", 19 | "tags.go", 20 | "tar.go", 21 | ], 22 | importpath = "github.com/google/rpmpack", 23 | visibility = ["//visibility:public"], 24 | deps = [ 25 | "@com_github_cavaliergopher_cpio//:cpio", 26 | "@com_github_klauspost_compress//zstd", 27 | "@com_github_klauspost_pgzip//:pgzip", 28 | "@com_github_ulikunitz_xz//:xz", 29 | "@com_github_ulikunitz_xz//lzma", 30 | ], 31 | ) 32 | 33 | go_test( 34 | name = "rpmpack_test", 35 | srcs = [ 36 | "dir_test.go", 37 | "file_types_test.go", 38 | "header_test.go", 39 | "rpm_test.go", 40 | "sense_test.go", 41 | "tar_test.go", 42 | ], 43 | embed = [":rpmpack"], 44 | deps = [ 45 | "@com_github_google_go_cmp//cmp", 46 | "@com_github_klauspost_compress//zstd", 47 | "@com_github_klauspost_pgzip//:pgzip", 48 | "@com_github_ulikunitz_xz//:xz", 49 | "@com_github_ulikunitz_xz//lzma", 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | module( 2 | name = "rpmpack", 3 | version = "0.6.0", 4 | ) 5 | 6 | bazel_dep(name = "rules_go", version = "0.46.0") 7 | bazel_dep(name = "gazelle", version = "0.35.0") 8 | 9 | go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") 10 | go_deps.from_file(go_mod = "//:go.mod") 11 | 12 | # All *direct* Go dependencies of the module have to be listed explicitly. 13 | use_repo( 14 | go_deps, 15 | "com_github_cavaliergopher_cpio", 16 | "com_github_google_go_cmp", 17 | "com_github_klauspost_compress", 18 | "com_github_klauspost_pgzip", 19 | "com_github_ulikunitz_xz", 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpmpack (tar2rpm) - package rpms the easy way 2 | 3 | ## Disclaimer 4 | 5 | This is not an official Google product, it is just code that happens to be owned 6 | by Google. 7 | 8 | ## Overview 9 | 10 | `tar2rpm` is a tool that takes a tar and outputs an rpm. `rpmpack` is a golang library to create rpms. Both are written in pure go, without using rpmbuild or spec files. API documentation for `rpmpack` can be found in [![GoDoc](https://godoc.org/github.com/google/rpmpack?status.svg)](https://godoc.org/github.com/google/rpmpack). 11 | 12 | ## Installation 13 | 14 | ```bash 15 | $ go get -u github.com/google/rpmpack/... 16 | ``` 17 | 18 | This will make the `tar2rpm` tool available in `${GOPATH}/bin`, which by default means `~/go/bin`. 19 | 20 | ## Usage of the binary (tar2rpm) 21 | 22 | `tar2rpm` takes a `tar` file (from `stdin` or a specified filename), and outputs an `rpm`. 23 | 24 | ``` 25 | Usage: 26 | tar2rpm -name NAME -version VERSION [OPTION] [TARFILE] 27 | Read tar content from stdin, or TARFILE if present. Write rpm to stdout, or the file given 28 | by -file RPMFILE. If a filename is '-' use stdin/stdout without printing a notice. 29 | Options: 30 | -file RPMFILE 31 | write rpm to RPMFILE instead of stdout 32 | -name string 33 | the package name 34 | -release string 35 | the rpm release 36 | -version string 37 | the package version 38 | ``` 39 | 40 | ## Usage of the library (rpmpack) 41 | 42 | API documentation for `rpmpack` can be found in [![GoDoc](https://godoc.org/github.com/google/rpmpack?status.svg)](https://godoc.org/github.com/google/rpmpack). 43 | 44 | ```go 45 | import "github.com/google/rpmpack" 46 | ... 47 | r, err := rpmpack.NewRPM(rpmpack.RPMMetaData{Name: "example", Version: "3"}) 48 | if err != nil { 49 | ... 50 | } 51 | r.AddFile(rpmpack.RPMFile{ 52 | Name: "/usr/local/hello", 53 | Body: []byte("content of the file"), 54 | }) 55 | if err := r.Write(w); err != nil { 56 | ... 57 | } 58 | ``` 59 | 60 | ## Usage in the bazel build system (pkg_tar2rpm) 61 | 62 | There is a working example inside [example_bazel](example_bazel/) 63 | 64 | In `WORKSPACE` 65 | ``` 66 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") 67 | git_repository( 68 | name = "rpmpack", 69 | remote = "https://github.com/google/rpmpack.git", 70 | branch = "master", 71 | ) 72 | 73 | # The following will load the requirements to build rpmpack 74 | http_archive( 75 | name = "io_bazel_rules_go", 76 | sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b", 77 | urls = [ 78 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz", 79 | "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz", 80 | ], 81 | ) 82 | 83 | http_archive( 84 | name = "bazel_gazelle", 85 | sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f", 86 | urls = [ 87 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz", 88 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz", 89 | ], 90 | ) 91 | 92 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 93 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") 94 | 95 | go_rules_dependencies() 96 | 97 | go_register_toolchains(version = "1.16") 98 | 99 | gazelle_dependencies() 100 | 101 | load("@com_github_google_rpmpack//:deps.bzl", "rpmpack_dependencies") 102 | 103 | rpmpack_dependencies() 104 | ``` 105 | 106 | In `BUILD` or `BUILD.bazel` 107 | ``` 108 | load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") 109 | load("@com_github_google_rpmpack//:def.bzl", "pkg_tar2rpm") 110 | 111 | pkg_tar( 112 | name = "rpmtest-tar", 113 | srcs = [":content1.txt"], 114 | mode = "0644", 115 | ownername = "root.root", 116 | package_dir = "var/lib/rpmpack", 117 | ) 118 | 119 | pkg_tar2rpm( 120 | name = "rpmtest", 121 | data = ":rpmtest-tar", 122 | pkg_name = "rpmtest", 123 | release = "3.4", 124 | version = "1.2", 125 | prein = "echo \"This is preinst\" > /tmp/preinst.txt", 126 | ) 127 | ``` 128 | 129 | 130 | ## Features 131 | 132 | - You put files into the rpm, so that rpm/yum will install them on a host. 133 | - Simple. 134 | - No `spec` files. 135 | - Does not build anything. 136 | - Does not try to auto-detect dependencies. 137 | - Does not try to magically deduce on which computer architecture you run. 138 | - Does not require any rpm database or other state, and does not use the 139 | filesystem. 140 | 141 | ## Downsides 142 | 143 | - Is not related to the team the builds rpmlib. 144 | - May easily wreak havoc on rpm based systems. It is surprisingly easy to cause 145 | rpm to segfault on corrupt rpm files. 146 | - Many features are missing. 147 | - All of the artifacts are stored in memory, sometimes more than once. 148 | - Less backwards compatible than `rpmbuild`. 149 | 150 | ## Philosophy 151 | 152 | Sometimes you just want files to make it to hosts, and be managed by the package 153 | manager. `rpmbuild` can use a `spec` file, together with a specific directory 154 | layout and local database, to build/install/package your files. But you don't 155 | need all that. You want something similar to tar. 156 | 157 | As the project progresses, we must maintain the complexity/value ratio. This 158 | includes both code complexity and interface complexity. 159 | 160 | ## Disclaimer 161 | 162 | This is not an official Google product, it is just code that happens to be owned 163 | by Google. 164 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/rpmpack/758cc6896cbcd952af5d143a5365a5df357eee2f/WORKSPACE -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/bazel:7.3.2' 3 | entrypoint: 'bazel' 4 | args: ['build', '--curses=no', '//:all'] 5 | - name: 'gcr.io/cloud-builders/bazel:7.3.2' 6 | entrypoint: 'bazel' 7 | args: ['test', '--test_output=all', '--curses=no', '//:all'] 8 | - name: 'gcr.io/cloud-builders/bazel:7.3.2' 9 | entrypoint: 'bazel' 10 | dir: 'example_bazel' 11 | args: ['build', '--curses=no', '//:all'] 12 | - name: 'gcr.io/cloud-builders/bazel:7.3.2' 13 | entrypoint: 'bazel' 14 | dir: 'example_bazel' 15 | args: ['test', '--test_output=all', '--curses=no', '//:all'] 16 | -------------------------------------------------------------------------------- /cmd/rpmsample/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_go//go:def.bzl", "go_binary", "go_cross_binary", "go_library") 2 | 3 | go_library( 4 | name = "rpmsample_lib", 5 | srcs = ["main.go"], 6 | importpath = "github.com/google/rpmpack/cmd/rpmsample", 7 | visibility = ["//visibility:private"], 8 | deps = ["//:rpmpack"], 9 | ) 10 | 11 | go_binary( 12 | name = "rpmsample_bin", 13 | embed = [":rpmsample_lib"], 14 | visibility = ["//visibility:public"], 15 | ) 16 | 17 | # This was the easiest way to get rid of libc mismatch errors. 18 | # We run the tests on docker with older version of libc. 19 | go_cross_binary( 20 | name = "rpmsample", 21 | platform = "@rules_go//go/toolchain:linux_amd64", 22 | target = ":rpmsample_bin", 23 | visibility = ["//visibility:public"], 24 | ) 25 | -------------------------------------------------------------------------------- /cmd/rpmsample/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // rpmsample creates an rpm file with some known files, which 16 | // can be used to test rpmpack's output against other rpm implementations. 17 | // It is also an instructive example for using rpmpack. 18 | package main 19 | 20 | import ( 21 | "flag" 22 | "log" 23 | "os" 24 | 25 | "github.com/google/rpmpack" 26 | ) 27 | 28 | func main() { 29 | 30 | sign := flag.Bool("sign", false, "sign the package with a fake sig") 31 | flag.Parse() 32 | 33 | r, err := rpmpack.NewRPM(rpmpack.RPMMetaData{ 34 | Name: "rpmsample", 35 | Version: "0.1", 36 | Release: "A", 37 | Arch: "noarch", 38 | }) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | r.AddFile( 43 | rpmpack.RPMFile{ 44 | Name: "/var/lib/rpmpack", 45 | Mode: 040755, 46 | Owner: "root", 47 | Group: "root", 48 | }) 49 | r.AddFile( 50 | rpmpack.RPMFile{ 51 | Name: "/var/lib/rpmpack/sample.txt", 52 | Body: []byte("testsample\n"), 53 | Mode: 0600, 54 | Owner: "root", 55 | Group: "root", 56 | }) 57 | r.AddFile( 58 | rpmpack.RPMFile{ 59 | Name: "/var/lib/rpmpack/sample2.txt", 60 | Body: []byte("testsample2\n"), 61 | Mode: 0644, 62 | Owner: "root", 63 | Group: "root", 64 | }) 65 | r.AddFile( 66 | rpmpack.RPMFile{ 67 | Name: "/var/lib/rpmpack/sample3_link.txt", 68 | Body: []byte("/var/lib/rpmpack/sample.txt"), 69 | Mode: 0120777, 70 | Owner: "root", 71 | Group: "root", 72 | }) 73 | r.AddFile( 74 | rpmpack.RPMFile{ 75 | Name: "/var/lib/rpmpack/sample4_ghost.txt", 76 | Mode: 0644, 77 | Owner: "root", 78 | Group: "root", 79 | Type: rpmpack.GhostFile, 80 | }) 81 | r.AddFile( 82 | rpmpack.RPMFile{ 83 | Name: "/var/lib/thisdoesnotexist/sample.txt", 84 | Mode: 0644, 85 | Body: []byte("testsample\n"), 86 | Owner: "root", 87 | Group: "root", 88 | }) 89 | if *sign { 90 | r.SetPGPSigner(func([]byte) ([]byte, error) { 91 | return []byte(`this is not a signature`), nil 92 | }) 93 | } 94 | if err := r.Write(os.Stdout); err != nil { 95 | log.Fatalf("write failed: %v", err) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /cmd/tar2rpm/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "tar2rpm_lib", 5 | srcs = ["main.go"], 6 | importpath = "github.com/google/rpmpack/cmd/tar2rpm", 7 | visibility = ["//visibility:private"], 8 | deps = ["//:rpmpack"], 9 | ) 10 | 11 | go_binary( 12 | name = "tar2rpm", 13 | embed = [":tar2rpm_lib"], 14 | visibility = ["//visibility:public"], 15 | ) 16 | -------------------------------------------------------------------------------- /cmd/tar2rpm/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "flag" 20 | "fmt" 21 | "io" 22 | "log" 23 | "math" 24 | "os" 25 | "strings" 26 | "time" 27 | 28 | "github.com/google/rpmpack" 29 | ) 30 | 31 | const ( 32 | // "Magic" filename: instead of reading/writing to that file use stdin/stdout (can still be used via './-'). 33 | DashStdinStdout = "-" 34 | ) 35 | 36 | var ( 37 | provides, 38 | obsoletes, 39 | suggests, 40 | recommends, 41 | requires, 42 | conflicts rpmpack.Relations 43 | name = flag.String("name", "", "the package name") 44 | version = flag.String("version", "", "the package version") 45 | release = flag.String("release", "", "the rpm release") 46 | epoch = flag.Uint64("epoch", 0, "the rpm epoch") 47 | arch = flag.String("arch", "noarch", "the rpm architecture") 48 | prefixes = flag.String("prefixes", "", "comma separated prefixes for relocatable packages") 49 | buildTime = flag.Int64("build_time", 0, "the build_time unix timestamp") 50 | compressor = flag.String("compressor", "gzip", "the rpm compressor") 51 | osName = flag.String("os", "linux", "the rpm os") 52 | summary = flag.String("summary", "", "the rpm summary") 53 | description = flag.String("description", "", "the rpm description") 54 | vendor = flag.String("vendor", "", "the rpm vendor") 55 | packager = flag.String("packager", "", "the rpm packager") 56 | group = flag.String("group", "", "the rpm group") 57 | url = flag.String("url", "", "the rpm url") 58 | licence = flag.String("licence", "", "the rpm licence name") 59 | 60 | prein = flag.String("prein", "", "prein scriptlet contents (not filename)") 61 | postin = flag.String("postin", "", "postin scriptlet contents (not filename)") 62 | preun = flag.String("preun", "", "preun scriptlet contents (not filename)") 63 | postun = flag.String("postun", "", "postun scriptlet contents (not filename)") 64 | 65 | useDirAllowlist = flag.Bool("use_dir_allowlist", false, "Only include dirs in the explicit allow list") 66 | dirAllowlistFile = flag.String("dir_allowlist_file", "", "A file with one directory per line to include from the tar to the rpm") 67 | 68 | outputfile = flag.String("file", "", "write rpm to `RPMFILE` instead of stdout") 69 | ) 70 | 71 | func usage() { 72 | fmt.Fprintf(os.Stderr, 73 | `Usage: 74 | %s -name NAME -version VERSION [OPTION] [TARFILE] 75 | Read tar content from stdin, or TARFILE if present. Write rpm to stdout, or the file given 76 | by -file RPMFILE. If a filename is '%s' use stdin/stdout without printing a notice. 77 | Options: 78 | `, os.Args[0], DashStdinStdout) 79 | flag.PrintDefaults() 80 | } 81 | 82 | func main() { 83 | flag.Var(&provides, "provides", "rpm provides values, can be just name or in the form of name=version (eg. bla=1.2.3)") 84 | flag.Var(&obsoletes, "obsoletes", "rpm obsoletes values, can be just name or in the form of name=version (eg. bla=1.2.3)") 85 | flag.Var(&suggests, "suggests", "rpm suggests values, can be just name or in the form of name=version (eg. bla=1.2.3)") 86 | flag.Var(&recommends, "recommends", "rpm recommends values, can be just name or in the form of name=version (eg. bla=1.2.3)") 87 | flag.Var(&requires, "requires", "rpm requires values, can be just name or in the form of name=version (eg. bla=1.2.3)") 88 | flag.Var(&conflicts, "conflicts", "rpm provides values, can be just name or in the form of name=version (eg. bla=1.2.3)") 89 | flag.Usage = usage 90 | flag.Parse() 91 | if *name == "" || *version == "" { 92 | fmt.Fprintln(os.Stderr, "name and version are required") 93 | flag.Usage() 94 | os.Exit(2) 95 | } 96 | if *epoch > math.MaxUint32 { 97 | fmt.Fprintf(os.Stderr, "epoch has to be less than %d\n", math.MaxUint32) 98 | flag.Usage() 99 | os.Exit(2) 100 | } 101 | var buildTimeStamp time.Time 102 | if *buildTime != 0 { 103 | buildTimeStamp = time.Unix(*buildTime, 0) 104 | } 105 | 106 | noticeStdinStdout := "" 107 | var i io.Reader 108 | switch flag.NArg() { 109 | case 0: 110 | // Only print notice if no explicit '-' is given: 111 | noticeStdinStdout = "reading tar content from stdin" 112 | i = os.Stdin 113 | case 1: 114 | if flag.Arg(0) == DashStdinStdout { 115 | i = os.Stdin 116 | } else { 117 | f, err := os.Open(flag.Arg(0)) 118 | if err != nil { 119 | log.Fatalf("Failed to open file %s for reading\n", flag.Arg(0)) 120 | } 121 | i = f 122 | } 123 | 124 | default: 125 | fmt.Fprintln(os.Stderr, "expecting 0 or 1 positional arguments") 126 | flag.Usage() 127 | os.Exit(2) 128 | } 129 | 130 | w := os.Stdout 131 | if *outputfile != DashStdinStdout { 132 | if *outputfile != "" { 133 | f, err := os.Create(*outputfile) 134 | if err != nil { 135 | log.Fatalf("Failed to open file %s for writing", *outputfile) 136 | } 137 | defer f.Close() 138 | w = f 139 | } else { 140 | // Only print notice if no explicit '-' is given, merge with tar notice: 141 | if noticeStdinStdout != "" { 142 | noticeStdinStdout += ", " 143 | } 144 | noticeStdinStdout += "writing rpm to stdout" 145 | 146 | } 147 | } 148 | if noticeStdinStdout != "" { 149 | fmt.Fprintln(os.Stderr, "tar2rpm: "+noticeStdinStdout+".") 150 | } 151 | r, err := rpmpack.FromTar( 152 | i, 153 | rpmpack.RPMMetaData{ 154 | Name: *name, 155 | Version: *version, 156 | Release: *release, 157 | Epoch: uint32(*epoch), 158 | BuildTime: buildTimeStamp, 159 | Prefixes: strings.Split(*prefixes, ","), 160 | Arch: *arch, 161 | OS: *osName, 162 | Vendor: *vendor, 163 | Packager: *packager, 164 | Group: *group, 165 | URL: *url, 166 | Licence: *licence, 167 | Description: *description, 168 | Summary: *summary, 169 | Compressor: *compressor, 170 | Provides: provides, 171 | Obsoletes: obsoletes, 172 | Suggests: suggests, 173 | Recommends: recommends, 174 | Requires: requires, 175 | Conflicts: conflicts, 176 | }) 177 | if err != nil { 178 | fmt.Fprintf(os.Stderr, "tar2rpm error: %v\n", err) 179 | os.Exit(1) 180 | } 181 | if *useDirAllowlist { 182 | al := map[string]bool{} 183 | if *dirAllowlistFile != "" { 184 | f, err := os.Open(*dirAllowlistFile) 185 | if err != nil { 186 | log.Fatalf("Failed to open dir allowlist %q for reading\n: %s", *dirAllowlistFile, err) 187 | } 188 | defer f.Close() 189 | scan := bufio.NewScanner(f) 190 | for scan.Scan() { 191 | t := scan.Text() 192 | al[t] = true 193 | } 194 | } 195 | r.AllowListDirs(al) 196 | } 197 | 198 | r.AddPrein(*prein) 199 | r.AddPostin(*postin) 200 | r.AddPreun(*preun) 201 | r.AddPostun(*postun) 202 | 203 | if err := r.Write(w); err != nil { 204 | fmt.Fprintf(os.Stderr, "rpm write error: %v\n", err) 205 | os.Exit(1) 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /def.bzl: -------------------------------------------------------------------------------- 1 | def _pkg_tar2rpm_impl(ctx): 2 | files = [ctx.file.data] 3 | args = ctx.actions.args() 4 | args.add("--name", ctx.attr.pkg_name) 5 | args.add("--version", ctx.attr.version) 6 | args.add("--release", ctx.attr.release) 7 | args.add("--arch", ctx.attr.arch) 8 | args.add("--packager", ctx.attr.packager) 9 | args.add("--epoch", ctx.attr.epoch) 10 | args.add("--prein", ctx.attr.prein) 11 | args.add("--postin", ctx.attr.postin) 12 | args.add("--preun", ctx.attr.preun) 13 | args.add("--postun", ctx.attr.postun) 14 | args.add_all("--requires", ctx.attr.requires) 15 | args.add_joined("--prefixes", ctx.attr.prefixes, join_with = ",") 16 | if ctx.attr.build_time != "": 17 | args.add("--build_time", ctx.attr.build_time) 18 | if ctx.attr.use_dir_allowlist: 19 | args.add("--use_dir_allowlist") 20 | if ctx.file.dir_allowlist_file: 21 | args.add("--dir_allowlist_file", ctx.file.dir_allowlist_file) 22 | files.append(ctx.file.dir_allowlist_file) 23 | args.add("--file", ctx.outputs.out) 24 | args.add(ctx.file.data) 25 | ctx.actions.run( 26 | executable = ctx.executable.tar2rpm, 27 | arguments = [args], 28 | inputs = files, 29 | outputs = [ctx.outputs.out], 30 | mnemonic = "tar2rpm", 31 | ) 32 | 33 | # A rule for generating rpm files 34 | pkg_tar2rpm = rule( 35 | implementation = _pkg_tar2rpm_impl, 36 | attrs = { 37 | "data": attr.label(mandatory = True, allow_single_file = [".tar"]), 38 | "pkg_name": attr.string(mandatory = True), 39 | "version": attr.string(mandatory = True), 40 | "release": attr.string(), 41 | "arch": attr.string(), 42 | "packager": attr.string(), 43 | "epoch": attr.int(), 44 | "prein": attr.string(), 45 | "postin": attr.string(), 46 | "preun": attr.string(), 47 | "postun": attr.string(), 48 | "requires": attr.string_list(), 49 | "prefixes": attr.string_list(), 50 | "build_time": attr.string(), 51 | "use_dir_allowlist": attr.bool(default = False, doc = """Only include 52 | directories themselves if they are in the allowlist file. Using this without an allowlist means do not include directories at all, only files."""), 53 | "dir_allowlist_file": attr.label(allow_single_file = True, doc = "A file with a list of directories to include in the rpm. The files contained in the directories are always added."), 54 | "tar2rpm": attr.label( 55 | default = Label("//cmd/tar2rpm"), 56 | cfg = "exec", 57 | executable = True, 58 | ), 59 | }, 60 | outputs = { 61 | "out": "%{name}.rpm", 62 | }, 63 | ) 64 | -------------------------------------------------------------------------------- /dir.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | // dirIndex holds the index from files to directory names. 18 | type dirIndex struct { 19 | m map[string]uint32 20 | l []string 21 | } 22 | 23 | func newDirIndex() *dirIndex { 24 | return &dirIndex{m: make(map[string]uint32)} 25 | } 26 | 27 | func (d *dirIndex) Get(value string) uint32 { 28 | 29 | if idx, ok := d.m[value]; ok { 30 | return idx 31 | } 32 | newIdx := uint32(len(d.l)) 33 | d.l = append(d.l, value) 34 | 35 | d.m[value] = newIdx 36 | return newIdx 37 | } 38 | 39 | func (d *dirIndex) AllDirs() []string { 40 | return d.l 41 | } 42 | -------------------------------------------------------------------------------- /dir_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/google/go-cmp/cmp" 21 | ) 22 | 23 | func TestDirIndex(t *testing.T) { 24 | testCases := []struct { 25 | name string 26 | before []string 27 | dir string 28 | wantGet uint32 29 | wantAllDirs []string 30 | }{{ 31 | name: "first", 32 | dir: "/first", 33 | wantGet: 0, 34 | wantAllDirs: []string{"/first"}, 35 | }, { 36 | name: "second", 37 | dir: "second", 38 | before: []string{"first"}, 39 | wantGet: 1, 40 | wantAllDirs: []string{"first", "second"}, 41 | }, { 42 | name: "repeat", 43 | dir: "second", 44 | before: []string{"first", "second", "third"}, 45 | wantGet: 1, 46 | wantAllDirs: []string{"first", "second", "third"}, 47 | }} 48 | for _, tc := range testCases { 49 | tc := tc 50 | t.Run(tc.name, func(t *testing.T) { 51 | d := newDirIndex() 52 | for _, b := range tc.before { 53 | d.Get(b) 54 | } 55 | if got := d.Get(tc.dir); got != tc.wantGet { 56 | t.Errorf("d.Get(%q) = %d, want: %d", tc.dir, got, tc.wantGet) 57 | } 58 | if df := cmp.Diff(tc.wantAllDirs, d.AllDirs()); df != "" { 59 | t.Errorf("d.AllDirs() diff (want->got):\n%s", df) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example_bazel/.bazelrc: -------------------------------------------------------------------------------- 1 | common --enable_bzlmod 2 | -------------------------------------------------------------------------------- /example_bazel/.bazelversion: -------------------------------------------------------------------------------- 1 | 7.3.2 2 | -------------------------------------------------------------------------------- /example_bazel/BUILD.bazel: -------------------------------------------------------------------------------- 1 | # A build file for rpmpack. 2 | # For running basic build/run/test you can also use the regular go tools, 3 | # this is currently added to assist in external testing. 4 | 5 | load("@rpmpack//:def.bzl", "pkg_tar2rpm") 6 | load("@rules_pkg//pkg:tar.bzl", "pkg_tar") 7 | load("//:testing.bzl", "docker_diff") 8 | 9 | CENTOS_IMAGE = "centos@sha256:365fc7f33107869dfcf2b3ba220ce0aa42e16d3f8e8b3c21d72af1ee622f0cf0" 10 | 11 | FEDORA_IMAGE = "fedora@sha256:3f3fc6a4714e44fae9147bc2b9542ac627491c13c4a3375e5066bdddc7710c9e" 12 | 13 | pkg_tar( 14 | name = "rpmtest-tar", 15 | srcs = [":content1.txt"], 16 | mode = "0644", 17 | ownername = "root.root", 18 | package_dir = "var/lib/rpmpack", 19 | ) 20 | 21 | pkg_tar( 22 | name = "rpmtest-tar-otherdir", 23 | srcs = [":content1.txt"], 24 | mode = "0644", 25 | ownername = "root.root", 26 | package_dir = "/doesnot/exist/rpmpack", 27 | ) 28 | 29 | pkg_tar( 30 | name = "rpmtest-tar-bothdirs", 31 | mode = "0644", 32 | ownername = "root.root", 33 | deps = [ 34 | ":rpmtest-tar", 35 | ":rpmtest-tar-otherdir", 36 | ], 37 | ) 38 | 39 | pkg_tar2rpm( 40 | name = "rpmtest", 41 | data = ":rpmtest-tar", 42 | epoch = 42, 43 | pkg_name = "rpmtest", 44 | prein = "echo \"This is preinst\" > /tmp/preinst.txt", 45 | release = "3.4", 46 | version = "1.2", 47 | ) 48 | 49 | pkg_tar2rpm( 50 | name = "rpmtest_with_prefixes", 51 | data = ":rpmtest-tar", 52 | dir_allowlist_file = ":allowlist_var_lib_rpmpack.txt", 53 | epoch = 42, 54 | pkg_name = "rpmtest_with_prefixes", 55 | prefixes = ["/var/lib"], 56 | release = "3.4", 57 | use_dir_allowlist = True, 58 | version = "1.2", 59 | ) 60 | 61 | pkg_tar2rpm( 62 | name = "rpmtest_withtime", 63 | build_time = "17", 64 | data = ":rpmtest-tar", 65 | pkg_name = "rpmtest_withtime", 66 | version = "1.2", 67 | ) 68 | 69 | pkg_tar2rpm( 70 | name = "rpmtest_bothdirs", 71 | data = ":rpmtest-tar-bothdirs", 72 | epoch = 42, 73 | pkg_name = "rpmtest_bothdirs", 74 | release = "3.4", 75 | version = "1.2", 76 | ) 77 | 78 | pkg_tar2rpm( 79 | name = "rpmtest_withoutbothdirs", 80 | data = ":rpmtest-tar-bothdirs", 81 | pkg_name = "rpmtest_withoutbothdirs", 82 | use_dir_allowlist = True, 83 | version = "1.2", 84 | ) 85 | 86 | pkg_tar2rpm( 87 | name = "rpmtest_withonlyonedir", 88 | data = ":rpmtest-tar-bothdirs", 89 | dir_allowlist_file = ":dir_allowlist.txt", 90 | pkg_name = "rpmtest_withonlyonedir", 91 | use_dir_allowlist = True, 92 | version = "1.2", 93 | ) 94 | 95 | pkg_tar( 96 | name = "rpms", 97 | srcs = [ 98 | ":rpmtest.rpm", 99 | ":rpmtest_bothdirs", 100 | ":rpmtest_with_prefixes", 101 | ":rpmtest_withonlyonedir", 102 | ":rpmtest_withoutbothdirs", 103 | ":rpmtest_withtime", 104 | ], 105 | ) 106 | 107 | docker_diff( 108 | name = "centos_V", 109 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && rpm -Vv rpmtest", 110 | golden = """ 111 | ......... /var 112 | ......... /var/lib 113 | ......... /var/lib/rpmpack 114 | ......... /var/lib/rpmpack/content1.txt""", 115 | image = CENTOS_IMAGE, 116 | tar = ":rpms", 117 | ) 118 | 119 | docker_diff( 120 | name = "centos_ls", 121 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && ls -l /var/lib/rpmpack", 122 | golden = """ 123 | total 4 124 | -rw-r--r-- 1 root root 22 Jan 1 2000 content1.txt""", 125 | image = CENTOS_IMAGE, 126 | tar = ":rpms", 127 | ) 128 | 129 | docker_diff( 130 | name = "centos_preinst", 131 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && cat /tmp/preinst.txt", 132 | golden = "This is preinst", 133 | image = CENTOS_IMAGE, 134 | tar = ":rpms", 135 | ) 136 | 137 | docker_diff( 138 | name = "centos_epoch", 139 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && rpm -q rpmtest --queryformat '%{EPOCH}\n'", 140 | golden = "42", 141 | image = CENTOS_IMAGE, 142 | tar = ":rpms", 143 | ) 144 | 145 | pkg_tar( 146 | name = "rpmsample_tar", 147 | srcs = [ 148 | "@rpmpack//cmd/rpmsample", 149 | ], 150 | ) 151 | 152 | docker_diff( 153 | name = "centos_rpmsample_signed", 154 | cmd = "echo ===marker=== && /root/rpmsample -sign > /root/rpmsample.rpm && rpm --nosignature -i /root/rpmsample.rpm && rpm --nosignature -q rpmsample --queryformat '%{SIGPGP}\n'", 155 | # "74686973206973206e6f742061207369676e6174757265" is "this is not a signature" in hex. 156 | golden = "74686973206973206e6f742061207369676e6174757265", 157 | image = CENTOS_IMAGE, 158 | tar = ":rpmsample_tar", 159 | ) 160 | 161 | docker_diff( 162 | name = "centos_rpmsample_ghost_provides", 163 | cmd = "echo ===marker=== && /root/rpmsample > /root/rpmsample.rpm && rpm -i /root/rpmsample.rpm && rpm -q --whatprovides /var/lib/rpmpack/sample4_ghost.txt", 164 | golden = "rpmsample-0.1-A.noarch", 165 | image = CENTOS_IMAGE, 166 | tar = ":rpmsample_tar", 167 | ) 168 | 169 | docker_diff( 170 | name = "centos_rpmsample_ghost_not_on_fs", 171 | cmd = "echo ===marker=== && /root/rpmsample > /root/rpmsample.rpm && rpm -i /root/rpmsample.rpm && ls /var/lib/rpmpack", 172 | golden = """ 173 | sample.txt 174 | sample2.txt 175 | sample3_link.txt 176 | """, 177 | image = CENTOS_IMAGE, 178 | tar = ":rpmsample_tar", 179 | ) 180 | 181 | docker_diff( 182 | name = "centos_rpmsample_directory_doesnotexist", 183 | cmd = "echo ===marker=== && /root/rpmsample > /root/rpmsample.rpm && rpm -i /root/rpmsample.rpm && cat /var/lib/thisdoesnotexist/sample.txt", 184 | golden = """ 185 | testsample 186 | """, 187 | image = CENTOS_IMAGE, 188 | tar = ":rpmsample_tar", 189 | ) 190 | 191 | docker_diff( 192 | name = "fedora_V", 193 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && rpm -Vv rpmtest", 194 | golden = """ 195 | ......... /var 196 | ......... /var/lib 197 | ......... /var/lib/rpmpack 198 | ......... /var/lib/rpmpack/content1.txt""", 199 | image = FEDORA_IMAGE, 200 | tar = ":rpms", 201 | ) 202 | 203 | docker_diff( 204 | name = "fedora_epoch", 205 | base = "fedora_with_rpm", 206 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && rpm -q rpmtest --queryformat '%{EPOCH}\n'", 207 | golden = "42", 208 | image = FEDORA_IMAGE, 209 | tar = ":rpms", 210 | ) 211 | 212 | pkg_tar( 213 | name = "rpmmetatest-tar", 214 | ) 215 | 216 | pkg_tar2rpm( 217 | name = "rpmmetatest", 218 | data = ":rpmmetatest-tar", 219 | epoch = 42, 220 | pkg_name = "rpmmetatest", 221 | release = "3.4", 222 | requires = ["bash"], 223 | version = "1.2", 224 | ) 225 | 226 | pkg_tar( 227 | name = "rpmmeta", 228 | srcs = [ 229 | ":rpmmetatest.rpm", 230 | ], 231 | ) 232 | 233 | docker_diff( 234 | name = "centos_meta_deps", 235 | cmd = "echo ===marker=== && rpm -i /root/rpmmetatest.rpm && rpm -qpR /root/rpmmetatest.rpm", 236 | golden = "bash", 237 | image = CENTOS_IMAGE, 238 | tar = ":rpmmeta", 239 | ) 240 | 241 | docker_diff( 242 | name = "centos_with_prefixes_meta", 243 | cmd = "echo ===marker=== && rpm -i /root/rpmtest_with_prefixes.rpm && rpm -q rpmtest_with_prefixes --queryformat '%{PREFIXES}\n'", 244 | golden = "/var/lib", 245 | image = CENTOS_IMAGE, 246 | tar = ":rpms", 247 | ) 248 | 249 | docker_diff( 250 | name = "centos_with_prefixes_ls", 251 | cmd = "echo ===marker=== && rpm -i --prefix=/opt /root/rpmtest_with_prefixes.rpm && rpm -Vv rpmtest_with_prefixes", 252 | golden = """ 253 | ......... /opt/rpmpack 254 | ......... /opt/rpmpack/content1.txt""", 255 | image = CENTOS_IMAGE, 256 | tar = ":rpms", 257 | ) 258 | 259 | docker_diff( 260 | name = "centos_empty_timestamp", 261 | cmd = "echo ===marker=== && rpm -i /root/rpmtest.rpm && rpm -q rpmtest --queryformat '%{BUILDTIME}\n'", 262 | golden = "(none)", 263 | image = CENTOS_IMAGE, 264 | tar = ":rpms", 265 | ) 266 | 267 | docker_diff( 268 | name = "centos_with_timestamp", 269 | cmd = "echo ===marker=== && rpm -i /root/rpmtest_withtime.rpm && rpm -q rpmtest_withtime --queryformat '%{BUILDTIME}\n'", 270 | golden = "17", 271 | image = CENTOS_IMAGE, 272 | tar = ":rpms", 273 | ) 274 | 275 | docker_diff( 276 | name = "centos_bothdirs", 277 | cmd = "echo ===marker=== && rpm -i --force /root/rpmtest_bothdirs.rpm && rpm -Vv rpmtest_bothdirs", 278 | golden = """ 279 | ......... /doesnot 280 | ......... /doesnot/exist 281 | ......... /doesnot/exist/rpmpack 282 | ......... /doesnot/exist/rpmpack/content1.txt 283 | ......... /var 284 | ......... /var/lib 285 | ......... /var/lib/rpmpack 286 | ......... /var/lib/rpmpack/content1.txt""", 287 | image = CENTOS_IMAGE, 288 | tar = ":rpms", 289 | ) 290 | 291 | docker_diff( 292 | name = "centos_withoutbothdirs", 293 | cmd = "echo ===marker=== && rpm -i /root/rpmtest_withoutbothdirs.rpm && rpm -Vv rpmtest_withoutbothdirs", 294 | golden = """ 295 | ......... /doesnot/exist/rpmpack/content1.txt 296 | ......... /var/lib/rpmpack/content1.txt""", 297 | image = CENTOS_IMAGE, 298 | tar = ":rpms", 299 | ) 300 | 301 | docker_diff( 302 | name = "centos_withonlyonedir", 303 | cmd = "echo ===marker=== && rpm -i /root/rpmtest_withonlyonedir.rpm && rpm -Vv rpmtest_withonlyonedir", 304 | golden = """ 305 | ......... /doesnot/exist 306 | ......... /doesnot/exist/rpmpack 307 | ......... /doesnot/exist/rpmpack/content1.txt 308 | ......... /var/lib/rpmpack/content1.txt""", 309 | image = CENTOS_IMAGE, 310 | tar = ":rpms", 311 | ) 312 | -------------------------------------------------------------------------------- /example_bazel/MODULE.bazel: -------------------------------------------------------------------------------- 1 | module( 2 | name = "rpmpack_example_bazel", 3 | version = "0.6.0", 4 | ) 5 | 6 | ############################################################################## 7 | # Workaround. By Default the python tool chain does not allow running as root. 8 | # pkg_tar uses this toolchain. 9 | # ughh 10 | # https://github.com/bazelbuild/rules_python/issues/1169 11 | bazel_dep(name = "rules_python", version = "0.37.1") 12 | 13 | python = use_extension( 14 | "@rules_python//python/extensions:python.bzl", 15 | "python", 16 | ) 17 | python.toolchain( 18 | ignore_root_user_error = True, 19 | is_default = True, 20 | python_version = "3.12", 21 | ) 22 | # End of workaround. 23 | ############################################################################## 24 | 25 | bazel_dep(name = "rpmpack", version = "0.6.0") 26 | local_path_override( 27 | module_name = "rpmpack", 28 | path = "../", 29 | ) 30 | 31 | bazel_dep(name = "rules_pkg", version = "1.0.1") 32 | -------------------------------------------------------------------------------- /example_bazel/allowlist_var_lib_rpmpack.txt: -------------------------------------------------------------------------------- 1 | /var/lib/rpmpack 2 | -------------------------------------------------------------------------------- /example_bazel/content1.txt: -------------------------------------------------------------------------------- 1 | test data for tar2rpm 2 | -------------------------------------------------------------------------------- /example_bazel/diff_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o nounset 18 | 19 | diff_test () { 20 | local result="$1" 21 | local want="$2" 22 | local got 23 | got="$(cat "$result" | sed '1,/===marker===/ d')" 24 | 25 | if ! diff <( echo "$want") <(echo "$got"); then 26 | echo "diff failed or differences found" >&2 27 | return 1 28 | fi 29 | return 0 30 | } 31 | 32 | read -r -d '' GOLDEN_STR << EOF 33 | {GOLDEN} 34 | EOF 35 | 36 | diff_test "{RESULT}" "${GOLDEN_STR}" 37 | 38 | exit $? 39 | -------------------------------------------------------------------------------- /example_bazel/dir_allowlist.txt: -------------------------------------------------------------------------------- 1 | /doesnot/exist/rpmpack 2 | /doesnot/exist 3 | -------------------------------------------------------------------------------- /example_bazel/docker_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -euo pipefail 18 | 19 | docker_run () { 20 | local cmd="$1" 21 | local tar="$2" 22 | local image="$3" 23 | 24 | docker run --rm \ 25 | --mount "type=bind,source=$PWD/${tar},destination=/rpms.tar,readonly" \ 26 | "${image}" \ 27 | bash -c "tar -C root -xvf /rpms.tar && ${cmd}" 28 | } 29 | OUT="$1" 30 | shift 31 | 32 | docker_run "$@" > $OUT 33 | 34 | exit $? 35 | -------------------------------------------------------------------------------- /example_bazel/testing.bzl: -------------------------------------------------------------------------------- 1 | 2 | def _docker_run_impl(ctx): 3 | out = ctx.actions.declare_file(ctx.label.name) 4 | args = ctx.actions.args() 5 | args.add(out) 6 | args.add(ctx.attr.cmd) 7 | args.add(ctx.file.tar) 8 | args.add(ctx.attr.image) 9 | ctx.actions.run( 10 | outputs = [out], 11 | inputs = [ctx.file.tar], 12 | executable = ctx.file._script, 13 | arguments = [args], 14 | ) 15 | return DefaultInfo(files = depset([out])) 16 | 17 | docker_run = rule( 18 | attrs = { 19 | "tar": attr.label( 20 | mandatory = True, 21 | allow_single_file = True, 22 | ), 23 | "image": attr.string( 24 | mandatory = True, 25 | ), 26 | "cmd": attr.string( 27 | mandatory = True, 28 | ), 29 | "_script": attr.label( 30 | default = "//:docker_run.sh", 31 | allow_single_file = True, 32 | ), 33 | }, 34 | implementation = _docker_run_impl, 35 | ) 36 | 37 | def _diff_test_impl(ctx): 38 | ctx.actions.expand_template( 39 | template = ctx.file._template, 40 | output = ctx.outputs.file, 41 | substitutions = { 42 | "{RESULT}": ctx.file.result.short_path, 43 | "{GOLDEN}": ctx.attr.golden, 44 | }, 45 | ) 46 | return DefaultInfo(runfiles = ctx.runfiles(files = ctx.files.result)) 47 | 48 | diff_test_expand = rule( 49 | attrs = { 50 | "result": attr.label( 51 | mandatory = True, 52 | allow_single_file = True, 53 | ), 54 | "golden": attr.string( 55 | mandatory = True, 56 | ), 57 | "_template": attr.label( 58 | default = "//:diff_test.sh", 59 | allow_single_file = True, 60 | ), 61 | }, 62 | outputs = {"file": "%{name}.sh"}, 63 | implementation = _diff_test_impl, 64 | ) 65 | 66 | def docker_diff(name,cmd, golden, tar=":rpms", image="", base=""): 67 | docker_run( 68 | name = name + "_run", 69 | testonly = True, 70 | tar = tar, 71 | image = image, 72 | cmd = cmd, 73 | ) 74 | diff_test_expand( 75 | name = name + "_diff", 76 | result = ":%s_run" % name, 77 | golden = golden, 78 | testonly = True, 79 | ) 80 | native.sh_test( 81 | name = name + "_diff_test", 82 | srcs = [":{}_diff".format(name)], 83 | data = [tar, ":{}_run".format(name)], 84 | ) 85 | -------------------------------------------------------------------------------- /file_types.go: -------------------------------------------------------------------------------- 1 | package rpmpack 2 | 3 | // FileType is the type of a file inside a RPM package. 4 | type FileType int32 5 | 6 | // https://refspecs.linuxbase.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/pkgformat.html#AEN27560 7 | // The RPMFile.Type tag value shall identify various characteristics of the file in the payload that it describes. 8 | // It shall be an INT32 value consisting of either the value GenericFile (0) or 9 | // the bitwise inclusive or of one or more of the following values. Some of these combinations may make no sense 10 | const ( 11 | // GenericFile is just a basic file in an RPM 12 | GenericFile FileType = 1 << iota >> 1 13 | // ConfigFile is a configuration file, and an existing file should be saved during a 14 | // package upgrade operation and not removed during a package removal operation. 15 | ConfigFile 16 | // DocFile is a file that contains documentation. 17 | DocFile 18 | // DoNotUseFile is reserved for future use; conforming packages may not use this flag. 19 | DoNotUseFile 20 | // MissingOkFile need not exist on the installed system. 21 | MissingOkFile 22 | // NoReplaceFile similar to the ConfigFile, this flag indicates that during an upgrade operation 23 | // the original file on the system should not be altered. 24 | NoReplaceFile 25 | // SpecFile is the package specification file. 26 | SpecFile 27 | // GhostFile is not actually included in the payload, but should still be considered as a part of the package. 28 | // For example, a log file generated by the application at run time. 29 | GhostFile 30 | // LicenceFile contains the license conditions. 31 | LicenceFile 32 | // ReadmeFile contains high level notes about the package. 33 | ReadmeFile 34 | // ExcludeFile is not a part of the package, and should not be installed. 35 | ExcludeFile 36 | ) 37 | 38 | // RPMFile contains a particular file's entry and data. 39 | type RPMFile struct { 40 | Name string 41 | Body []byte 42 | Mode uint 43 | Owner string 44 | Group string 45 | MTime uint32 46 | Type FileType 47 | } 48 | -------------------------------------------------------------------------------- /file_types_test.go: -------------------------------------------------------------------------------- 1 | package rpmpack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFileTypeSetting(t *testing.T) { 8 | f := &RPMFile{} 9 | 10 | if f.Type != GenericFile { 11 | t.Error("New RPMFile.Type should be a generic type") 12 | } 13 | 14 | f.Type |= ConfigFile 15 | if (f.Type & ConfigFile) == 0 { 16 | t.Error("Setting to config file should have the ConfigFile bitmask") 17 | } 18 | } 19 | 20 | func TestFileTypeCombining(t *testing.T) { 21 | f := &RPMFile{} 22 | 23 | f.Type |= ConfigFile | NoReplaceFile 24 | 25 | if (f.Type&ConfigFile) == 0 || f.Type&NoReplaceFile == 0 { 26 | t.Error("Combining file types should have the bitmask of both") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/rpmpack 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cavaliergopher/cpio v1.0.1 7 | github.com/google/go-cmp v0.3.1 8 | github.com/klauspost/compress v1.16.6 9 | github.com/klauspost/pgzip v1.2.6 10 | github.com/ulikunitz/xz v0.5.11 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= 2 | github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= 3 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 4 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 5 | github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= 6 | github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 7 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 8 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 9 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 10 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 11 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | import ( 18 | "bytes" 19 | "encoding/binary" 20 | "fmt" 21 | "sort" 22 | ) 23 | 24 | const ( 25 | signatures = 0x3e 26 | immutable = 0x3f 27 | 28 | typeInt16 = 0x03 29 | typeInt32 = 0x04 30 | typeString = 0x06 31 | typeBinary = 0x07 32 | typeStringArray = 0x08 33 | ) 34 | 35 | // Only integer types are aligned. This is not just an optimization - some versions 36 | // of rpm fail when integers are not aligned. Other versions fail when non-integers are aligned. 37 | var boundaries = map[int]int{ 38 | typeInt16: 2, 39 | typeInt32: 4, 40 | } 41 | 42 | type IndexEntry struct { 43 | rpmtype, count int 44 | data []byte 45 | } 46 | 47 | func (e IndexEntry) indexBytes(tag, contentOffset int) []byte { 48 | b := &bytes.Buffer{} 49 | if err := binary.Write(b, binary.BigEndian, []int32{int32(tag), int32(e.rpmtype), int32(contentOffset), int32(e.count)}); err != nil { 50 | // binary.Write can fail if the underlying Write fails, or the types are invalid. 51 | // bytes.Buffer's write never error out, it can only panic with OOM. 52 | panic(err) 53 | } 54 | return b.Bytes() 55 | } 56 | 57 | func intEntry(rpmtype, size int, value interface{}) IndexEntry { 58 | b := &bytes.Buffer{} 59 | if err := binary.Write(b, binary.BigEndian, value); err != nil { 60 | // binary.Write can fail if the underlying Write fails, or the types are invalid. 61 | // bytes.Buffer's write never error out, it can only panic with OOM. 62 | panic(err) 63 | } 64 | return IndexEntry{rpmtype, size, b.Bytes()} 65 | } 66 | 67 | func EntryInt16(value []int16) IndexEntry { 68 | return intEntry(typeInt16, len(value), value) 69 | } 70 | func EntryUint16(value []uint16) IndexEntry { 71 | return intEntry(typeInt16, len(value), value) 72 | } 73 | func EntryInt32(value []int32) IndexEntry { 74 | return intEntry(typeInt32, len(value), value) 75 | } 76 | func EntryUint32(value []uint32) IndexEntry { 77 | return intEntry(typeInt32, len(value), value) 78 | } 79 | func EntryString(value string) IndexEntry { 80 | return IndexEntry{typeString, 1, append([]byte(value), byte(00))} 81 | } 82 | func EntryBytes(value []byte) IndexEntry { 83 | return IndexEntry{typeBinary, len(value), value} 84 | } 85 | 86 | func EntryStringSlice(value []string) IndexEntry { 87 | b := [][]byte{} 88 | for _, v := range value { 89 | b = append(b, []byte(v)) 90 | } 91 | bb := append(bytes.Join(b, []byte{00}), byte(00)) 92 | return IndexEntry{typeStringArray, len(value), bb} 93 | } 94 | 95 | type index struct { 96 | entries map[int]IndexEntry 97 | h int 98 | } 99 | 100 | func newIndex(h int) *index { 101 | return &index{entries: make(map[int]IndexEntry), h: h} 102 | } 103 | func (i *index) Add(tag int, e IndexEntry) { 104 | i.entries[tag] = e 105 | } 106 | func (i *index) AddEntries(m map[int]IndexEntry) { 107 | for t, e := range m { 108 | i.Add(t, e) 109 | } 110 | } 111 | 112 | func (i *index) sortedTags() []int { 113 | t := []int{} 114 | for k := range i.entries { 115 | t = append(t, k) 116 | } 117 | sort.Ints(t) 118 | return t 119 | } 120 | 121 | func pad(w *bytes.Buffer, rpmtype, offset int) { 122 | // We need to align integer entries... 123 | if b, ok := boundaries[rpmtype]; ok && offset%b != 0 { 124 | if _, err := w.Write(make([]byte, b-offset%b)); err != nil { 125 | // binary.Write can fail if the underlying Write fails, or the types are invalid. 126 | // bytes.Buffer's write never error out, it can only panic with OOM. 127 | panic(err) 128 | } 129 | } 130 | } 131 | 132 | // Bytes returns the bytes of the index. 133 | func (i *index) Bytes() ([]byte, error) { 134 | w := &bytes.Buffer{} 135 | // Even the header has three parts: The lead, the index entries, and the entries. 136 | // Because of alignment, we can only tell the actual size and offset after writing 137 | // the entries. 138 | entryData := &bytes.Buffer{} 139 | tags := i.sortedTags() 140 | offsets := make([]int, len(tags)) 141 | for ii, tag := range tags { 142 | e := i.entries[tag] 143 | pad(entryData, e.rpmtype, entryData.Len()) 144 | offsets[ii] = entryData.Len() 145 | entryData.Write(e.data) 146 | } 147 | entryData.Write(i.eigenHeader().data) 148 | 149 | // 4 magic and 4 reserved 150 | w.Write([]byte{0x8e, 0xad, 0xe8, 0x01, 0, 0, 0, 0}) 151 | // 4 count and 4 size 152 | // We add the pseudo-entry "eigenHeader" to count. 153 | if err := binary.Write(w, binary.BigEndian, []int32{int32(len(i.entries)) + 1, int32(entryData.Len())}); err != nil { 154 | return nil, fmt.Errorf("failed to write eigenHeader: %w", err) 155 | } 156 | // Write the eigenHeader index entry 157 | w.Write(i.eigenHeader().indexBytes(i.h, entryData.Len()-0x10)) 158 | // Write all of the other index entries 159 | for ii, tag := range tags { 160 | e := i.entries[tag] 161 | w.Write(e.indexBytes(tag, offsets[ii])) 162 | } 163 | w.Write(entryData.Bytes()) 164 | return w.Bytes(), nil 165 | } 166 | 167 | // the eigenHeader is a weird entry. Its index entry is sorted first, but its content 168 | // is last. The content is a 16 byte index entry, which is almost the same as the index 169 | // entry except for the offset. The offset here is ... minus the length of the index entry region. 170 | // Which is always 0x10 * number of entries. 171 | // I kid you not. 172 | func (i *index) eigenHeader() IndexEntry { 173 | b := &bytes.Buffer{} 174 | if err := binary.Write(b, binary.BigEndian, []int32{int32(i.h), int32(typeBinary), -int32(0x10 * (len(i.entries) + 1)), int32(0x10)}); err != nil { 175 | // binary.Write can fail if the underlying Write fails, or the types are invalid. 176 | // bytes.Buffer's write never error out, it can only panic with OOM. 177 | panic(err) 178 | } 179 | 180 | return EntryBytes(b.Bytes()) 181 | } 182 | 183 | func lead(name, fullVersion string) []byte { 184 | // RPM format = 0xedabeedb 185 | // version 3.0 = 0x0300 186 | // type binary = 0x0000 187 | // machine archnum (i386?) = 0x0001 188 | // name ( 66 bytes, with null termination) 189 | // osnum (linux?) = 0x0001 190 | // sig type (header-style) = 0x0005 191 | // reserved 16 bytes of 0x00 192 | n := []byte(fmt.Sprintf("%s-%s", name, fullVersion)) 193 | if len(n) > 65 { 194 | n = n[:65] 195 | } 196 | n = append(n, make([]byte, 66-len(n))...) 197 | b := []byte{0xed, 0xab, 0xee, 0xdb, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01} 198 | b = append(b, n...) 199 | b = append(b, []byte{0x00, 0x01, 0x00, 0x05}...) 200 | b = append(b, make([]byte, 16)...) 201 | return b 202 | } 203 | -------------------------------------------------------------------------------- /header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/google/go-cmp/cmp" 22 | ) 23 | 24 | func TestLead(t *testing.T) { 25 | // Only check that the length is always right 26 | names := []string{ 27 | "a", 28 | "ab", 29 | "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc", 30 | "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabca", 31 | "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcab", 32 | "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc", 33 | } 34 | for _, n := range names { 35 | if got := len(lead(n, "1-2")); got != 0x60 { 36 | t.Errorf("len(lead(%s)) = %#x, want %#x", n, got, 0x60) 37 | } 38 | } 39 | } 40 | 41 | func TestEntry(t *testing.T) { 42 | testCases := []struct { 43 | name string 44 | value interface{} 45 | tag int 46 | offset int 47 | wantIndexBytes string 48 | wantData string 49 | }{{ 50 | name: "simple int", 51 | value: []int32{0x42}, 52 | tag: 0x010d, 53 | offset: 5, 54 | wantIndexBytes: "0000010d000000040000000500000001", 55 | wantData: "00000042", 56 | }, { 57 | name: "simple string", 58 | value: "simple string", 59 | tag: 0x010e, 60 | offset: 0x111, 61 | wantIndexBytes: "0000010e000000060000011100000001", 62 | wantData: "73696d706c6520737472696e6700", 63 | }, { 64 | name: "string array", 65 | value: []string{"string", "array"}, 66 | tag: 0x010f, 67 | offset: 0x222, 68 | wantIndexBytes: "0000010f000000080000022200000002", 69 | wantData: "737472696e6700617272617900", 70 | }} 71 | for _, tc := range testCases { 72 | tc := tc 73 | t.Run(tc.name, func(t *testing.T) { 74 | var e IndexEntry 75 | switch v := tc.value.(type) { 76 | case []string: 77 | e = EntryStringSlice(v) 78 | case string: 79 | e = EntryString(v) 80 | case []int32: 81 | e = EntryInt32(v) 82 | } 83 | gotBytes := e.indexBytes(tc.tag, tc.offset) 84 | if d := cmp.Diff(tc.wantIndexBytes, fmt.Sprintf("%x", gotBytes)); d != "" { 85 | t.Errorf("entry.indexBytes() unexpected value (want->got):\n%s", d) 86 | } 87 | if d := cmp.Diff(tc.wantData, fmt.Sprintf("%x", e.data)); d != "" { 88 | t.Errorf("entry.data unexpected value (want->got):\n%s", d) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func TestIndex(t *testing.T) { 95 | i := newIndex(0x3e) 96 | i.AddEntries(map[int]IndexEntry{ 97 | 0x1111: EntryUint16([]uint16{0x4444, 0x8888, 0xcccc}), 98 | 0x2222: EntryUint32([]uint32{0x3333, 0x5555}), 99 | }) 100 | got, err := i.Bytes() 101 | if err != nil { 102 | t.Errorf("i.Bytes() returned error: %v", err) 103 | } 104 | want := "8eade80100000000" + // header lead 105 | "0000000300000020" + // count and size 106 | "0000003e000000070000001000000010" + // eigen header entry 107 | "00001111000000030000000000000003" + 108 | "00002222000000040000000800000002" + 109 | "44448888cccc00000000333300005555" + // values, with padding 110 | "0000003e00000007ffffffd000000010" // eigen header value 111 | if d := cmp.Diff(want, fmt.Sprintf("%x", got)); d != "" { 112 | t.Errorf("i.Bytes() unexpected value (want-> got): \n%s", d) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /rpm.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package rpmpack packs files to rpm files. 16 | // It is designed to be simple to use and deploy, not requiring any filesystem access 17 | // to create rpm files. 18 | package rpmpack 19 | 20 | import ( 21 | "bytes" 22 | "crypto/sha256" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "path" 27 | "sort" 28 | "strconv" 29 | "strings" 30 | "time" 31 | 32 | "github.com/cavaliergopher/cpio" 33 | "github.com/klauspost/compress/zstd" 34 | gzip "github.com/klauspost/pgzip" 35 | "github.com/ulikunitz/xz" 36 | "github.com/ulikunitz/xz/lzma" 37 | ) 38 | 39 | const ( 40 | // NoEpoch skips adding the epoch tag to the RPM. 41 | // 42 | // We decided to use this approach instead of making epoch a *uint32 to 43 | // avoid a breaking change. 44 | // For reference, this is the max uint32 value, which is 4294967295. 45 | NoEpoch = ^uint32(0) 46 | ) 47 | 48 | var ( 49 | // ErrWriteAfterClose is returned when a user calls Write() on a closed rpm. 50 | ErrWriteAfterClose = errors.New("rpm write after close") 51 | // ErrWrongFileOrder is returned when files are not sorted by name. 52 | ErrWrongFileOrder = errors.New("wrong file addition order") 53 | ) 54 | 55 | // RPMMetaData contains meta info about the whole package. 56 | type RPMMetaData struct { 57 | Name, 58 | Summary, 59 | Description, 60 | Version, 61 | Release, 62 | Arch, 63 | OS, 64 | Vendor, 65 | URL, 66 | Packager, 67 | Group, 68 | Licence, 69 | BuildHost, 70 | Compressor string 71 | Epoch uint32 72 | BuildTime time.Time 73 | // Prefixes is used for relocatable packages, usually with a one item 74 | // slice, e.g. `["/opt"]`. 75 | Prefixes []string 76 | Provides, 77 | Obsoletes, 78 | Suggests, 79 | Recommends, 80 | Requires, 81 | Conflicts Relations 82 | } 83 | 84 | // RPM holds the state of a particular rpm file. Please use NewRPM to instantiate it. 85 | type RPM struct { 86 | RPMMetaData 87 | di *dirIndex 88 | payload *bytes.Buffer 89 | payloadSize uint 90 | cpio *cpio.Writer 91 | basenames []string 92 | dirindexes []uint32 93 | filesizes []uint32 94 | filemodes []uint16 95 | fileowners []string 96 | filegroups []string 97 | filemtimes []uint32 98 | filedigests []string 99 | filelinktos []string 100 | fileflags []uint32 101 | closed bool 102 | compressedPayload io.WriteCloser 103 | files map[string]RPMFile 104 | prein string 105 | postin string 106 | preun string 107 | postun string 108 | pretrans string 109 | posttrans string 110 | verifyscript string 111 | customTags map[int]IndexEntry 112 | customSigs map[int]IndexEntry 113 | pgpSigner func([]byte) ([]byte, error) 114 | } 115 | 116 | // NewRPM creates and returns a new RPM struct. 117 | func NewRPM(m RPMMetaData) (*RPM, error) { 118 | var err error 119 | 120 | if m.OS == "" { 121 | m.OS = "linux" 122 | } 123 | 124 | if m.Arch == "" { 125 | m.Arch = "noarch" 126 | } 127 | 128 | p := &bytes.Buffer{} 129 | 130 | z, compressorName, err := setupCompressor(m.Compressor, p) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | // only use compressor name for the rpm tag, not the level 136 | m.Compressor = compressorName 137 | 138 | rpm := &RPM{ 139 | RPMMetaData: m, 140 | di: newDirIndex(), 141 | payload: p, 142 | compressedPayload: z, 143 | cpio: cpio.NewWriter(z), 144 | files: make(map[string]RPMFile), 145 | customTags: make(map[int]IndexEntry), 146 | customSigs: make(map[int]IndexEntry), 147 | } 148 | 149 | // A package must provide itself... 150 | rpm.Provides.addIfMissing(&Relation{ 151 | Name: rpm.Name, 152 | Version: rpm.FullVersion(), 153 | Sense: SenseEqual, 154 | }) 155 | 156 | return rpm, nil 157 | } 158 | 159 | func setupCompressor( 160 | compressorSetting string, 161 | w io.Writer, 162 | ) (wc io.WriteCloser, compressorType string, err error) { 163 | parts := strings.Split(compressorSetting, ":") 164 | if len(parts) > 2 { 165 | return nil, "", fmt.Errorf("malformed compressor setting: %s", compressorSetting) 166 | } 167 | 168 | compressorType = parts[0] 169 | compressorLevel := "" 170 | if len(parts) == 2 { 171 | compressorLevel = parts[1] 172 | } 173 | 174 | switch compressorType { 175 | case "": 176 | compressorType = "gzip" 177 | fallthrough 178 | case "gzip": 179 | level := 9 180 | 181 | if compressorLevel != "" { 182 | var err error 183 | 184 | level, err = strconv.Atoi(compressorLevel) 185 | if err != nil { 186 | return nil, "", fmt.Errorf("parse gzip compressor level: %w", err) 187 | } 188 | } 189 | 190 | wc, err = gzip.NewWriterLevel(w, level) 191 | case "lzma": 192 | if compressorLevel != "" { 193 | return nil, "", fmt.Errorf("no compressor level supported for lzma: %s", compressorLevel) 194 | } 195 | 196 | wc, err = lzma.NewWriter(w) 197 | case "xz": 198 | if compressorLevel != "" { 199 | return nil, "", fmt.Errorf("no compressor level supported for xz: %s", compressorLevel) 200 | } 201 | 202 | wc, err = xz.NewWriter(w) 203 | case "zstd": 204 | level := zstd.SpeedBetterCompression 205 | 206 | if compressorLevel != "" { 207 | var ok bool 208 | 209 | if intLevel, err := strconv.Atoi(compressorLevel); err == nil { 210 | level = zstd.EncoderLevelFromZstd(intLevel) 211 | } else { 212 | ok, level = zstd.EncoderLevelFromString(compressorLevel) 213 | if !ok { 214 | return nil, "", fmt.Errorf("invalid zstd compressor level: %s", compressorLevel) 215 | } 216 | } 217 | } 218 | 219 | wc, err = zstd.NewWriter(w, zstd.WithEncoderLevel(level)) 220 | default: 221 | return nil, "", fmt.Errorf("unknown compressor type: %s", compressorType) 222 | } 223 | 224 | return wc, compressorType, err 225 | } 226 | 227 | // FullVersion properly combines version and release fields to a version string 228 | func (r *RPM) FullVersion() string { 229 | if r.Release != "" { 230 | return fmt.Sprintf("%s-%s", r.Version, r.Release) 231 | } 232 | 233 | return r.Version 234 | } 235 | 236 | // AllowListDirs removes all directories which are not explicitly allowlisted. 237 | func (r *RPM) AllowListDirs(allowList map[string]bool) { 238 | for fn, ff := range r.files { 239 | if ff.Mode&040000 == 040000 { 240 | if !allowList[fn] { 241 | delete(r.files, fn) 242 | } 243 | } 244 | } 245 | } 246 | 247 | // Write closes the rpm and writes the whole rpm to an io.Writer 248 | func (r *RPM) Write(w io.Writer) error { 249 | if r.closed { 250 | return ErrWriteAfterClose 251 | } 252 | // Add all of the files, sorted alphabetically. 253 | fnames := []string{} 254 | for fn := range r.files { 255 | fnames = append(fnames, fn) 256 | } 257 | sort.Strings(fnames) 258 | for _, fn := range fnames { 259 | if err := r.writeFile(r.files[fn]); err != nil { 260 | return fmt.Errorf("failed to write file %q: %w", fn, err) 261 | } 262 | } 263 | if err := r.cpio.Close(); err != nil { 264 | return fmt.Errorf("failed to close cpio payload: %w", err) 265 | } 266 | if err := r.compressedPayload.Close(); err != nil { 267 | return fmt.Errorf("failed to close %s payload: %w", r.RPMMetaData.Compressor, err) 268 | } 269 | 270 | if _, err := w.Write(lead(r.Name, r.FullVersion())); err != nil { 271 | return fmt.Errorf("failed to write lead: %w", err) 272 | } 273 | // Write the regular header. 274 | h := newIndex(immutable) 275 | r.writeGenIndexes(h) 276 | 277 | // do not write file indexes if there are no files (meta package) 278 | // doing so will result in an invalid package 279 | if (len(r.files)) > 0 { 280 | r.writeFileIndexes(h) 281 | } 282 | 283 | if err := r.writeRelationIndexes(h); err != nil { 284 | return err 285 | } 286 | // CustomTags must be the last to be added, because they can overwrite values. 287 | h.AddEntries(r.customTags) 288 | hb, err := h.Bytes() 289 | if err != nil { 290 | return fmt.Errorf("failed to retrieve header: %w", err) 291 | } 292 | // Write the signatures 293 | s := newIndex(signatures) 294 | if err := r.writeSignatures(s, hb); err != nil { 295 | return fmt.Errorf("failed to create signatures: %w", err) 296 | } 297 | 298 | s.AddEntries(r.customSigs) 299 | sb, err := s.Bytes() 300 | if err != nil { 301 | return fmt.Errorf("failed to retrieve signatures header: %w", err) 302 | } 303 | 304 | if _, err := w.Write(sb); err != nil { 305 | return fmt.Errorf("failed to write signature bytes: %w", err) 306 | } 307 | // Signatures are padded to 8-byte boundaries 308 | if _, err := w.Write(make([]byte, (8-len(sb)%8)%8)); err != nil { 309 | return fmt.Errorf("failed to write signature padding: %w", err) 310 | } 311 | if _, err := w.Write(hb); err != nil { 312 | return fmt.Errorf("failed to write header body: %w", err) 313 | } 314 | if _, err := w.Write(r.payload.Bytes()); err != nil { 315 | return fmt.Errorf("failed to write payload: %w", err) 316 | } 317 | return nil 318 | } 319 | 320 | // SetPGPSigner registers a function that will accept the header and payload as bytes, 321 | // and return a signature as bytes. The function should simulate what gpg does, 322 | // probably by using golang.org/x/crypto/openpgp or by forking a gpg process. 323 | func (r *RPM) SetPGPSigner(f func([]byte) ([]byte, error)) { 324 | r.pgpSigner = f 325 | } 326 | 327 | // Only call this after the payload and header were written. 328 | func (r *RPM) writeSignatures(sigHeader *index, regHeader []byte) error { 329 | sigHeader.Add(sigSize, EntryInt32([]int32{int32(r.payload.Len() + len(regHeader))})) 330 | sigHeader.Add(sigSHA256, EntryString(fmt.Sprintf("%x", sha256.Sum256(regHeader)))) 331 | sigHeader.Add(sigPayloadSize, EntryInt32([]int32{int32(r.payloadSize)})) 332 | if r.pgpSigner != nil { 333 | // For sha 256 you need to sign the header and payload separately 334 | header := append([]byte{}, regHeader...) 335 | headerSig, err := r.pgpSigner(header) 336 | if err != nil { 337 | return fmt.Errorf("call to signer failed: %w", err) 338 | } 339 | sigHeader.Add(sigRSA, EntryBytes(headerSig)) 340 | 341 | body := append(header, r.payload.Bytes()...) 342 | bodySig, err := r.pgpSigner(body) 343 | if err != nil { 344 | return fmt.Errorf("call to signer failed: %w", err) 345 | } 346 | sigHeader.Add(sigPGP, EntryBytes(bodySig)) 347 | } 348 | return nil 349 | } 350 | 351 | func (r *RPM) writeRelationIndexes(h *index) error { 352 | // add all relation categories 353 | if err := r.Provides.AddToIndex(h, tagProvides, tagProvideVersion, tagProvideFlags); err != nil { 354 | return fmt.Errorf("failed to add provides: %w", err) 355 | } 356 | if err := r.Obsoletes.AddToIndex(h, tagObsoletes, tagObsoleteVersion, tagObsoleteFlags); err != nil { 357 | return fmt.Errorf("failed to add obsoletes: %w", err) 358 | } 359 | if err := r.Suggests.AddToIndex(h, tagSuggests, tagSuggestVersion, tagSuggestFlags); err != nil { 360 | return fmt.Errorf("failed to add suggests: %w", err) 361 | } 362 | if err := r.Recommends.AddToIndex(h, tagRecommends, tagRecommendVersion, tagRecommendFlags); err != nil { 363 | return fmt.Errorf("failed to add recommends: %w", err) 364 | } 365 | if err := r.Requires.AddToIndex(h, tagRequires, tagRequireVersion, tagRequireFlags); err != nil { 366 | return fmt.Errorf("failed to add requires: %w", err) 367 | } 368 | if err := r.Conflicts.AddToIndex(h, tagConflicts, tagConflictVersion, tagConflictFlags); err != nil { 369 | return fmt.Errorf("failed to add conflicts: %w", err) 370 | } 371 | 372 | return nil 373 | } 374 | 375 | // AddCustomTag adds or overwrites a tag value in the index. 376 | func (r *RPM) AddCustomTag(tag int, e IndexEntry) { 377 | r.customTags[tag] = e 378 | } 379 | 380 | // AddCustomSig adds or overwrites a signature tag value. 381 | func (r *RPM) AddCustomSig(tag int, e IndexEntry) { 382 | r.customSigs[tag] = e 383 | } 384 | 385 | func (r *RPM) writeGenIndexes(h *index) { 386 | h.Add(tagHeaderI18NTable, EntryString("C")) 387 | h.Add(tagSize, EntryInt32([]int32{int32(r.payloadSize)})) 388 | h.Add(tagName, EntryString(r.Name)) 389 | h.Add(tagVersion, EntryString(r.Version)) 390 | if r.Epoch != NoEpoch { 391 | h.Add(tagEpoch, EntryUint32([]uint32{r.Epoch})) 392 | } 393 | h.Add(tagSummary, EntryString(r.Summary)) 394 | h.Add(tagDescription, EntryString(r.Description)) 395 | h.Add(tagBuildHost, EntryString(r.BuildHost)) 396 | if !r.BuildTime.IsZero() { 397 | // time.Time zero value is confusing, avoid if not supplied 398 | // see https://github.com/google/rpmpack/issues/43 399 | h.Add(tagBuildTime, EntryInt32([]int32{int32(r.BuildTime.Unix())})) 400 | } 401 | if len(r.Prefixes) != 0 { 402 | h.Add(tagPrefixes, EntryStringSlice(r.Prefixes)) 403 | } 404 | h.Add(tagRelease, EntryString(r.Release)) 405 | h.Add(tagPayloadFormat, EntryString("cpio")) 406 | h.Add(tagPayloadCompressor, EntryString(r.Compressor)) 407 | h.Add(tagPayloadFlags, EntryString("9")) 408 | h.Add(tagArch, EntryString(r.Arch)) 409 | h.Add(tagOS, EntryString(r.OS)) 410 | if r.Vendor != "" { 411 | h.Add(tagVendor, EntryString(r.Vendor)) 412 | } 413 | h.Add(tagLicence, EntryString(r.Licence)) 414 | if r.Packager != "" { 415 | h.Add(tagPackager, EntryString(r.Packager)) 416 | } 417 | if r.Group != "" { 418 | h.Add(tagGroup, EntryString(r.Group)) 419 | } 420 | if r.URL != "" { 421 | h.Add(tagURL, EntryString(r.URL)) 422 | } 423 | h.Add(tagPayloadDigest, EntryStringSlice([]string{fmt.Sprintf("%x", sha256.Sum256(r.payload.Bytes()))})) 424 | h.Add(tagPayloadDigestAlgo, EntryInt32([]int32{hashAlgoSHA256})) 425 | 426 | // rpm utilities look for the sourcerpm tag to deduce if this is not a source rpm (if it has a sourcerpm, 427 | // it is NOT a source rpm). 428 | h.Add(tagSourceRPM, EntryString(fmt.Sprintf("%s-%s.src.rpm", r.Name, r.FullVersion()))) 429 | if r.pretrans != "" { 430 | h.Add(tagPretrans, EntryString(r.pretrans)) 431 | h.Add(tagPretransProg, EntryString("/bin/sh")) 432 | } 433 | if r.prein != "" { 434 | h.Add(tagPrein, EntryString(r.prein)) 435 | h.Add(tagPreinProg, EntryString("/bin/sh")) 436 | } 437 | if r.postin != "" { 438 | h.Add(tagPostin, EntryString(r.postin)) 439 | h.Add(tagPostinProg, EntryString("/bin/sh")) 440 | } 441 | if r.preun != "" { 442 | h.Add(tagPreun, EntryString(r.preun)) 443 | h.Add(tagPreunProg, EntryString("/bin/sh")) 444 | } 445 | if r.postun != "" { 446 | h.Add(tagPostun, EntryString(r.postun)) 447 | h.Add(tagPostunProg, EntryString("/bin/sh")) 448 | } 449 | if r.posttrans != "" { 450 | h.Add(tagPosttrans, EntryString(r.posttrans)) 451 | h.Add(tagPosttransProg, EntryString("/bin/sh")) 452 | } 453 | if r.verifyscript != "" { 454 | h.Add(tagVerifyScript, EntryString(r.verifyscript)) 455 | h.Add(tagVerifyScriptProg, EntryString("/bin/sh")) 456 | } 457 | } 458 | 459 | // WriteFileIndexes writes file related index headers to the header 460 | func (r *RPM) writeFileIndexes(h *index) { 461 | h.Add(tagBasenames, EntryStringSlice(r.basenames)) 462 | h.Add(tagDirindexes, EntryUint32(r.dirindexes)) 463 | h.Add(tagDirnames, EntryStringSlice(r.di.AllDirs())) 464 | h.Add(tagFileSizes, EntryUint32(r.filesizes)) 465 | h.Add(tagFileModes, EntryUint16(r.filemodes)) 466 | h.Add(tagFileUserName, EntryStringSlice(r.fileowners)) 467 | h.Add(tagFileGroupName, EntryStringSlice(r.filegroups)) 468 | h.Add(tagFileMTimes, EntryUint32(r.filemtimes)) 469 | h.Add(tagFileDigests, EntryStringSlice(r.filedigests)) 470 | h.Add(tagFileLinkTos, EntryStringSlice(r.filelinktos)) 471 | h.Add(tagFileFlags, EntryUint32(r.fileflags)) 472 | 473 | inodes := make([]int32, len(r.dirindexes)) 474 | devices := make([]int32, len(r.dirindexes)) 475 | digestAlgo := make([]int32, len(r.dirindexes)) 476 | verifyFlags := make([]int32, len(r.dirindexes)) 477 | fileRDevs := make([]int16, len(r.dirindexes)) 478 | fileLangs := make([]string, len(r.dirindexes)) 479 | 480 | for ii := range inodes { 481 | // is inodes just a range from 1..len(dirindexes)? maybe different with hard links 482 | inodes[ii] = int32(ii + 1) 483 | // is devices number from which the file was copied 484 | // from rpm original tools https://github.com/rpm-software-management/rpm/blob/c167ef8bdaecdd2e306ec896c919607ba9cceb6f/build/files.c#L1226 485 | devices[ii] = int32(1) 486 | digestAlgo[ii] = hashAlgoSHA256 487 | // With regular files, it seems like we can always enable all of the verify flags 488 | verifyFlags[ii] = int32(-1) 489 | fileRDevs[ii] = int16(1) 490 | } 491 | h.Add(tagFileINodes, EntryInt32(inodes)) 492 | h.Add(tagFileDevices, EntryInt32(devices)) 493 | h.Add(tagFileDigestAlgo, EntryInt32(digestAlgo)) 494 | h.Add(tagFileVerifyFlags, EntryInt32(verifyFlags)) 495 | h.Add(tagFileRDevs, EntryInt16(fileRDevs)) 496 | h.Add(tagFileLangs, EntryStringSlice(fileLangs)) 497 | } 498 | 499 | // AddPretrans adds a pretrans scriptlet 500 | func (r *RPM) AddPretrans(s string) { 501 | r.pretrans = s 502 | } 503 | 504 | // AddPrein adds a prein scriptlet 505 | func (r *RPM) AddPrein(s string) { 506 | r.prein = s 507 | } 508 | 509 | // AddPostin adds a postin scriptlet 510 | func (r *RPM) AddPostin(s string) { 511 | r.postin = s 512 | } 513 | 514 | // AddPreun adds a preun scriptlet 515 | func (r *RPM) AddPreun(s string) { 516 | r.preun = s 517 | } 518 | 519 | // AddPostun adds a postun scriptlet 520 | func (r *RPM) AddPostun(s string) { 521 | r.postun = s 522 | } 523 | 524 | // AddPosttrans adds a posttrans scriptlet 525 | func (r *RPM) AddPosttrans(s string) { 526 | r.posttrans = s 527 | } 528 | 529 | // AddVerifyScript adds a verifyscript scriptlet 530 | func (r *RPM) AddVerifyScript(s string) { 531 | r.verifyscript = s 532 | } 533 | 534 | // AddFile adds an RPMFile to an existing rpm. 535 | func (r *RPM) AddFile(f RPMFile) { 536 | if f.Name == "/" { // rpm does not allow the root dir to be included. 537 | return 538 | } 539 | r.files[f.Name] = f 540 | } 541 | 542 | // writeFile writes the file to the indexes and cpio. 543 | func (r *RPM) writeFile(f RPMFile) error { 544 | dir, file := path.Split(f.Name) 545 | r.dirindexes = append(r.dirindexes, r.di.Get(dir)) 546 | r.basenames = append(r.basenames, file) 547 | r.fileowners = append(r.fileowners, f.Owner) 548 | r.filegroups = append(r.filegroups, f.Group) 549 | r.filemtimes = append(r.filemtimes, f.MTime) 550 | r.fileflags = append(r.fileflags, uint32(f.Type)) 551 | 552 | links := 1 553 | switch { 554 | case f.Mode&040000 != 0: // directory 555 | r.filesizes = append(r.filesizes, 4096) 556 | r.filedigests = append(r.filedigests, "") 557 | r.filelinktos = append(r.filelinktos, "") 558 | links = 2 559 | case f.Mode&0120000 == 0120000: // symlink 560 | r.filesizes = append(r.filesizes, uint32(len(f.Body))) 561 | r.filedigests = append(r.filedigests, "") 562 | r.filelinktos = append(r.filelinktos, string(f.Body)) 563 | default: // regular file 564 | f.Mode = f.Mode | 0100000 565 | r.filesizes = append(r.filesizes, uint32(len(f.Body))) 566 | r.filedigests = append(r.filedigests, fmt.Sprintf("%x", sha256.Sum256(f.Body))) 567 | r.filelinktos = append(r.filelinktos, "") 568 | } 569 | r.filemodes = append(r.filemodes, uint16(f.Mode)) 570 | 571 | // Ghost files have no payload 572 | if f.Type == GhostFile { 573 | return nil 574 | } 575 | return r.writePayload(f, links) 576 | } 577 | 578 | func (r *RPM) writePayload(f RPMFile, links int) error { 579 | hdr := &cpio.Header{ 580 | Name: f.Name, 581 | Mode: cpio.FileMode(f.Mode), 582 | Size: int64(len(f.Body)), 583 | Links: links, 584 | } 585 | if err := r.cpio.WriteHeader(hdr); err != nil { 586 | return fmt.Errorf("failed to write payload file header: %w", err) 587 | } 588 | if _, err := r.cpio.Write(f.Body); err != nil { 589 | return fmt.Errorf("failed to write payload file content: %w", err) 590 | } 591 | r.payloadSize += uint(len(f.Body)) 592 | return nil 593 | } 594 | -------------------------------------------------------------------------------- /rpm_test.go: -------------------------------------------------------------------------------- 1 | package rpmpack 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | 11 | "github.com/klauspost/compress/zstd" 12 | gzip "github.com/klauspost/pgzip" 13 | "github.com/ulikunitz/xz" 14 | "github.com/ulikunitz/xz/lzma" 15 | ) 16 | 17 | func TestFileOwner(t *testing.T) { 18 | r, err := NewRPM(RPMMetaData{}) 19 | if err != nil { 20 | t.Fatalf("NewRPM returned error %v", err) 21 | } 22 | group := "testGroup" 23 | user := "testUser" 24 | 25 | r.AddFile(RPMFile{ 26 | Name: "/usr/local/hello", 27 | Body: []byte("content of the file"), 28 | Group: group, 29 | Owner: user, 30 | }) 31 | 32 | if err := r.Write(io.Discard); err != nil { 33 | t.Errorf("NewRPM returned error %v", err) 34 | } 35 | if r.fileowners[0] != user { 36 | t.Errorf("File owner shoud be %s but is %s", user, r.fileowners[0]) 37 | } 38 | if r.filegroups[0] != group { 39 | t.Errorf("File owner shoud be %s but is %s", group, r.filegroups[0]) 40 | } 41 | } 42 | 43 | // https://github.com/google/rpmpack/issues/49 44 | func Test100644(t *testing.T) { 45 | r, err := NewRPM(RPMMetaData{}) 46 | if err != nil { 47 | t.Fatalf("NewRPM returned error %v", err) 48 | } 49 | r.AddFile(RPMFile{ 50 | Name: "/usr/local/hello", 51 | Body: []byte("content of the file"), 52 | Mode: 0100644, 53 | }) 54 | 55 | if err := r.Write(io.Discard); err != nil { 56 | t.Errorf("Write returned error %v", err) 57 | } 58 | if r.filemodes[0] != 0100644 { 59 | t.Errorf("file mode want 0100644, got %o", r.filemodes[0]) 60 | } 61 | if r.filelinktos[0] != "" { 62 | t.Errorf("linktos want empty (not a symlink), got %q", r.filelinktos[0]) 63 | } 64 | } 65 | 66 | func TestCompression(t *testing.T) { 67 | testCases := []struct { 68 | Type string 69 | Compressors []string 70 | ExpectedWriter io.Writer 71 | }{ 72 | { 73 | Type: "gzip", 74 | Compressors: []string{ 75 | "", "gzip", "gzip:1", "gzip:2", "gzip:3", 76 | "gzip:4", "gzip:5", "gzip:6", "gzip:7", "gzip:8", "gzip:9", 77 | }, 78 | ExpectedWriter: &gzip.Writer{}, 79 | }, 80 | { 81 | Type: "gzip", 82 | Compressors: []string{"gzip:fast", "gzip:10"}, 83 | ExpectedWriter: nil, // gzip requires an integer level from -2 to 9 84 | }, 85 | { 86 | Type: "lzma", 87 | Compressors: []string{"lzma"}, 88 | ExpectedWriter: &lzma.Writer{}, 89 | }, 90 | { 91 | Type: "lzma", 92 | Compressors: []string{"lzma:fast", "lzma:1"}, 93 | ExpectedWriter: nil, // lzma does not support specifying the compression level 94 | }, 95 | { 96 | Type: "xz", 97 | Compressors: []string{"xz"}, 98 | ExpectedWriter: &xz.Writer{}, 99 | }, 100 | { 101 | Type: "xz", 102 | Compressors: []string{"xz:fast", "xz:1"}, 103 | ExpectedWriter: nil, // xz does not support specifying the compression level 104 | }, 105 | { 106 | Type: "zstd", 107 | Compressors: []string{ 108 | "zstd", "zstd:fastest", "zstd:default", "zstd:better", 109 | "zstd:best", "zstd:BeSt", "zstd:0", "zstd:4", "zstd:8", "zstd:15", 110 | }, 111 | ExpectedWriter: &zstd.Encoder{}, 112 | }, 113 | { 114 | Type: "zstd", 115 | Compressors: []string{"xz:worst"}, 116 | ExpectedWriter: nil, // only integers levels or one of the pre-defined string values 117 | }, 118 | } 119 | 120 | for _, testCase := range testCases { 121 | testCase := testCase 122 | 123 | for _, compressor := range testCase.Compressors { 124 | t.Run(compressor, func(t *testing.T) { 125 | r, err := NewRPM(RPMMetaData{ 126 | Compressor: compressor, 127 | }) 128 | if err != nil { 129 | if testCase.ExpectedWriter == nil { 130 | return // an error is expected 131 | } 132 | 133 | t.Fatalf("NewRPM returned error %v", err) 134 | } 135 | 136 | if testCase.ExpectedWriter == nil { 137 | t.Fatalf("compressor %q should have produced an error", compressor) 138 | } 139 | 140 | if r.RPMMetaData.Compressor != testCase.Type { 141 | t.Fatalf("expected compressor %q, got %q", compressor, 142 | r.RPMMetaData.Compressor) 143 | } 144 | 145 | expectedWriterType := reflect.Indirect(reflect.ValueOf( 146 | testCase.ExpectedWriter)).String() 147 | actualWriterType := reflect.Indirect(reflect.ValueOf( 148 | r.compressedPayload)).String() 149 | 150 | if expectedWriterType != actualWriterType { 151 | t.Fatalf("expected writer to be %T, got %T instead", 152 | testCase.ExpectedWriter, r.compressedPayload) 153 | } 154 | }) 155 | } 156 | } 157 | } 158 | 159 | func TestAllowListDirs(t *testing.T) { 160 | r, err := NewRPM(RPMMetaData{}) 161 | if err != nil { 162 | t.Fatalf("NewRPM returned error %v", err) 163 | } 164 | 165 | r.AddFile(RPMFile{ 166 | Name: "/usr/local/dir1", 167 | Mode: 040000, 168 | }) 169 | r.AddFile(RPMFile{ 170 | Name: "/usr/local/dir2", 171 | Mode: 040000, 172 | }) 173 | 174 | r.AllowListDirs(map[string]bool{"/usr/local/dir1": true}) 175 | 176 | if err := r.Write(io.Discard); err != nil { 177 | t.Errorf("NewRPM returned error %v", err) 178 | } 179 | expected := map[string]RPMFile{"/usr/local/dir1": {Name: "/usr/local/dir1", Mode: 040000}} 180 | if d := cmp.Diff(expected, r.files); d != "" { 181 | t.Errorf("Expected dirs differs (want->got):\n%v", d) 182 | } 183 | } 184 | 185 | func TestMinimalSpec(t *testing.T) { 186 | r, err := NewRPM(RPMMetaData{ 187 | Name: "test", 188 | Version: "1.0", 189 | Release: "1", 190 | Summary: "test summary", 191 | Description: "test description", 192 | Licence: "test license", 193 | }) 194 | if err != nil { 195 | t.Fatalf("NewRPM returned error %v", err) 196 | } 197 | 198 | var b bytes.Buffer 199 | if err := r.Write(&b); err != nil { 200 | t.Errorf("Write returned error %v", err) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /sense.go: -------------------------------------------------------------------------------- 1 | package rpmpack 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type rpmSense uint32 10 | 11 | // https://github.com/rpm-software-management/rpm/blob/ab01b5eacf9ec6a07a5d9e1991ef476a12d264fd/include/rpm/rpmds.h#L27 12 | // SenseAny (0) specifies no specific version compare 13 | // SenseLess (2) specifies less then the specified version 14 | // SenseGreater (4) specifies greater then the specified version 15 | // SenseEqual (8) specifies equal to the specified version 16 | const ( 17 | SenseAny rpmSense = 0 18 | SenseLess = 1 << iota 19 | SenseGreater 20 | SenseEqual 21 | SenseRPMLIB rpmSense = 1 << 24 22 | ) 23 | 24 | var relationMatch = regexp.MustCompile(`([^=<>\s]*)\s*((?:=|>|<)*)\s*(.*)?`) 25 | 26 | // Relation is the structure of rpm sense relationships 27 | type Relation struct { 28 | Name string 29 | Version string 30 | Sense rpmSense 31 | } 32 | 33 | // String return the string representation of the Relation 34 | func (r *Relation) String() string { 35 | return fmt.Sprintf("%s%v%s", r.Name, r.Sense, r.Version) 36 | } 37 | 38 | // Equal compare the equality of two relations 39 | func (r *Relation) Equal(o *Relation) bool { 40 | return r.Name == o.Name && r.Version == o.Version && r.Sense == o.Sense 41 | } 42 | 43 | // Relations is a slice of Relation pointers 44 | type Relations []*Relation 45 | 46 | // String return the string representation of the Relations 47 | func (r *Relations) String() string { 48 | var val []string 49 | for _, rel := range *r { 50 | val = append(val, rel.String()) 51 | } 52 | return strings.Join(val, ",") 53 | } 54 | 55 | // Set parse a string into a Relation and append it to the Relations slice if it is missing 56 | // this is used by the flag package 57 | func (r *Relations) Set(value string) error { 58 | relation, err := NewRelation(value) 59 | if err != nil { 60 | return err 61 | } 62 | r.addIfMissing(relation) 63 | 64 | return nil 65 | } 66 | 67 | func (r *Relations) addIfMissing(value *Relation) { 68 | for _, relation := range *r { 69 | if relation.Equal(value) { 70 | return 71 | } 72 | } 73 | 74 | *r = append(*r, value) 75 | } 76 | 77 | // AddToIndex add the relations to the specified category on the index 78 | func (r *Relations) AddToIndex(h *index, nameTag, versionTag, flagsTag int) error { 79 | var ( 80 | num = len(*r) 81 | names = make([]string, num) 82 | versions = make([]string, num) 83 | flags = make([]uint32, num) 84 | ) 85 | 86 | if num == 0 { 87 | return nil 88 | } 89 | 90 | for idx, relation := range *r { 91 | names[idx] = relation.Name 92 | versions[idx] = relation.Version 93 | flags[idx] = uint32(relation.Sense) 94 | } 95 | 96 | h.Add(nameTag, EntryStringSlice(names)) 97 | h.Add(versionTag, EntryStringSlice(versions)) 98 | h.Add(flagsTag, EntryUint32(flags)) 99 | 100 | return nil 101 | } 102 | 103 | // NewRelation parse a string into a Relation 104 | func NewRelation(related string) (*Relation, error) { 105 | var ( 106 | err error 107 | sense rpmSense 108 | name, 109 | version string 110 | ) 111 | 112 | if strings.HasPrefix(related, "(") && strings.HasSuffix(related, ")") { 113 | // This is a `rich` dependency which must be parsed at install time 114 | // https://rpm-software-management.github.io/rpm/manual/boolean_dependencies.html 115 | sense = SenseAny 116 | name = related 117 | } else { 118 | parts := relationMatch.FindStringSubmatch(related) 119 | if sense, err = parseSense(parts[2]); err != nil { 120 | return nil, err 121 | } 122 | name = parts[1] 123 | version = parts[3] 124 | } 125 | 126 | return &Relation{ 127 | Name: name, 128 | Version: version, 129 | Sense: sense, 130 | }, nil 131 | } 132 | 133 | var stringToSense = map[string]rpmSense{ 134 | "": SenseAny, 135 | "<": SenseLess, 136 | ">": SenseGreater, 137 | "=": SenseEqual, 138 | "<=": SenseLess | SenseEqual, 139 | ">=": SenseGreater | SenseEqual, 140 | } 141 | 142 | // String return the string representation of the rpmSense 143 | func (r rpmSense) String() string { 144 | var ( 145 | val rpmSense 146 | ret string 147 | ) 148 | 149 | for ret, val = range stringToSense { 150 | if r == val { 151 | return ret 152 | } 153 | } 154 | 155 | return "unknown" 156 | } 157 | 158 | func parseSense(sense string) (rpmSense, error) { 159 | var ( 160 | ret rpmSense 161 | ok bool 162 | ) 163 | if ret, ok = stringToSense[sense]; !ok { 164 | return SenseAny, fmt.Errorf("unknown sense value: %s", sense) 165 | } 166 | 167 | return ret, nil 168 | } 169 | -------------------------------------------------------------------------------- /sense_test.go: -------------------------------------------------------------------------------- 1 | package rpmpack 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewRelation(t *testing.T) { 8 | testCases := []struct { 9 | input, output string 10 | errExpected bool 11 | }{ 12 | { 13 | input: "python >= 3.7", 14 | output: "python>=3.7", 15 | }, 16 | { 17 | input: "python", 18 | output: "python", 19 | }, 20 | { 21 | input: "python=2", 22 | output: "python=2", 23 | }, 24 | { 25 | input: "python >=3.5", 26 | output: "python>=3.5", 27 | }, 28 | { 29 | input: "python >< 3.5", 30 | output: "", 31 | errExpected: true, 32 | }, 33 | { 34 | input: "python <> 3.5", 35 | output: "", 36 | errExpected: true, 37 | }, 38 | { 39 | input: "python == 3.5", 40 | output: "", 41 | errExpected: true, 42 | }, 43 | { 44 | input: "python =< 3.5", 45 | output: "", 46 | errExpected: true, 47 | }, 48 | { 49 | input: "python => 3.5", 50 | output: "", 51 | errExpected: true, 52 | }, 53 | } 54 | 55 | for _, tc := range testCases { 56 | testCase := tc 57 | t.Run(testCase.input, func(tt *testing.T) { 58 | relation, err := NewRelation(testCase.input) 59 | switch { 60 | case testCase.errExpected && err == nil: 61 | tt.Errorf("%s should have returned an error", testCase.input) 62 | return 63 | case !testCase.errExpected && err != nil: 64 | tt.Errorf("%s should not have returned an error: %v", testCase.input, err) 65 | return 66 | case testCase.errExpected && err != nil: 67 | return 68 | } 69 | 70 | if relation == nil { 71 | tt.Errorf("%s should not have returned a nil relation", testCase.input) 72 | return 73 | } 74 | 75 | val := relation.String() 76 | if !testCase.errExpected && val != testCase.output { 77 | tt.Errorf("%s should have returned %s not %s", testCase.input, testCase.output, val) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | // Define only tags which we actually use 18 | // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmtag.h 19 | const ( 20 | tagHeaderI18NTable = 0x64 // 100 21 | // Signature tags are obiously overlapping regular header tags.. 22 | sigRSA = 0x010c // 256 23 | sigSHA256 = 0x0111 // 273 24 | sigSize = 0x03e8 // 1000 25 | sigPGP = 0x03ea // 1002 26 | sigPayloadSize = 0x03ef // 1007 27 | 28 | // https://github.com/rpm-software-management/rpm/blob/92eadae94c48928bca90693ad63c46ceda37d81f/rpmio/rpmpgp.h#L258 29 | hashAlgoSHA256 = 0x0008 // 8 30 | 31 | tagName = 0x03e8 // 1000 32 | tagVersion = 0x03e9 // 1001 33 | tagRelease = 0x03ea // 1002 34 | tagEpoch = 0x03eb // 1003 35 | tagSummary = 0x03ec // 1004 36 | tagDescription = 0x03ed // 1005 37 | tagBuildTime = 0x03ee // 1006 38 | tagBuildHost = 0x03ef // 1007 39 | tagSize = 0x03f1 // 1009 40 | tagVendor = 0x03f3 // 1011 41 | tagLicence = 0x03f6 // 1014 42 | tagPackager = 0x03f7 // 1015 43 | tagGroup = 0x03f8 // 1016 44 | tagURL = 0x03fc // 1020 45 | tagOS = 0x03fd // 1021 46 | tagArch = 0x03fe // 1022 47 | 48 | tagPrein = 0x03ff // 1023 49 | tagPostin = 0x0400 // 1024 50 | tagPreun = 0x0401 // 1025 51 | tagPostun = 0x0402 // 1026 52 | 53 | tagFileSizes = 0x0404 // 1028 54 | tagFileModes = 0x0406 // 1030 55 | tagFileRDevs = 0x0409 // 1033 56 | tagFileMTimes = 0x040a // 1034 57 | tagFileDigests = 0x040b // 1035 58 | tagFileLinkTos = 0x040c // 1036 59 | tagFileFlags = 0x040d // 1037 60 | tagFileUserName = 0x040f // 1039 61 | tagFileGroupName = 0x0410 // 1040 62 | tagSourceRPM = 0x0414 // 1044 63 | tagFileVerifyFlags = 0x0415 // 1045 64 | tagProvides = 0x0417 // 1047 65 | tagRequireFlags = 0x0418 // 1048 66 | tagRequires = 0x0419 // 1049 67 | tagRequireVersion = 0x041a // 1050 68 | tagConflictFlags = 0x041d // 1053 69 | tagConflicts = 0x041e // 1054 70 | tagConflictVersion = 0x041f // 1055 71 | tagVerifyScript = 0x0437 // 1079 72 | tagPreinProg = 0x043d // 1085 73 | tagPostinProg = 0x043e // 1086 74 | tagPreunProg = 0x043f // 1087 75 | tagPostunProg = 0x0440 // 1088 76 | tagObsoletes = 0x0442 // 1090 77 | tagFileDevices = 0x0447 // 1095 78 | tagVerifyScriptProg = 0x0443 // 1091 79 | tagFileINodes = 0x0448 // 1096 80 | tagFileLangs = 0x0449 // 1097 81 | tagPrefixes = 0x044a // 1098 82 | tagProvideFlags = 0x0458 // 1112 83 | tagProvideVersion = 0x0459 // 1113 84 | tagObsoleteFlags = 0x045a // 1114 85 | tagObsoleteVersion = 0x045b // 1115 86 | tagDirindexes = 0x045c // 1116 87 | tagBasenames = 0x045d // 1117 88 | tagDirnames = 0x045e // 1118 89 | tagPayloadFormat = 0x0464 // 1124 90 | tagPayloadCompressor = 0x0465 // 1125 91 | tagPayloadFlags = 0x0466 // 1126 92 | tagPretrans = 0x047f // 1151 93 | tagPosttrans = 0x0480 // 1152 94 | tagPretransProg = 0x0481 // 1153 95 | tagPosttransProg = 0x0482 // 1154 96 | tagFileDigestAlgo = 0x1393 // 5011 97 | tagRecommends = 0x13b6 // 5046 98 | tagRecommendVersion = 0x13b7 // 5047 99 | tagRecommendFlags = 0x13b8 // 5048 100 | tagSuggests = 0x13b9 // 5049 101 | tagSuggestVersion = 0x13ba // 5050 102 | tagSuggestFlags = 0x13bb // 5051 103 | tagPayloadDigest = 0x13e4 // 5092 104 | tagPayloadDigestAlgo = 0x13e5 // 5093 105 | ) 106 | -------------------------------------------------------------------------------- /tar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | import ( 18 | "archive/tar" 19 | "fmt" 20 | "io" 21 | "path" 22 | ) 23 | 24 | // FromTar reads a tar file and creates an rpm stuct. 25 | func FromTar(inp io.Reader, md RPMMetaData) (*RPM, error) { 26 | r, err := NewRPM(md) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed to create RPM structure: %w", err) 29 | } 30 | t := tar.NewReader(inp) 31 | for { 32 | h, err := t.Next() 33 | if err == io.EOF { 34 | return r, nil 35 | } else if err != nil { 36 | return nil, fmt.Errorf("failed to read tar file: %w", err) 37 | } 38 | var body []byte 39 | switch h.Typeflag { 40 | case tar.TypeDir: 41 | h.Mode |= 040000 42 | case tar.TypeSymlink: 43 | body = []byte(h.Linkname) 44 | h.Mode |= 0120000 45 | case tar.TypeReg: 46 | b, err := io.ReadAll(t) 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to read file (%q): %w", h.Name, err) 49 | } 50 | body = b 51 | default: 52 | return nil, fmt.Errorf("unknown tar type: %d, (%q)", h.Typeflag, h.Name) 53 | } 54 | mtime := uint32(h.ModTime.Unix()) 55 | 56 | // Sometimes the tar has no uname and gname. RPM expects these to always exist. 57 | owner := h.Uname 58 | if owner == "" { 59 | owner = "root" 60 | } 61 | group := h.Gname 62 | if group == "" { 63 | group = "root" 64 | } 65 | 66 | r.AddFile( 67 | RPMFile{ 68 | Name: path.Join("/", h.Name), 69 | Body: body, 70 | Mode: uint(h.Mode), 71 | Owner: owner, 72 | Group: group, 73 | MTime: mtime, 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tar_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rpmpack 16 | 17 | import ( 18 | "archive/tar" 19 | "bytes" 20 | "io" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | ) 25 | 26 | // create a test tar file 27 | func createTar(t *testing.T) io.Reader { 28 | t.Helper() 29 | b := &bytes.Buffer{} 30 | ta := tar.NewWriter(b) 31 | entries := []struct { 32 | hdr *tar.Header 33 | body []byte 34 | }{{ 35 | hdr: &tar.Header{ 36 | Name: "dir1/", 37 | Mode: 0755, 38 | }, 39 | }, { 40 | hdr: &tar.Header{ 41 | Typeflag: tar.TypeSymlink, 42 | Name: "dir1/symlink1", 43 | Linkname: "../symtarget", 44 | }, 45 | }, { 46 | hdr: &tar.Header{ 47 | Name: "dir1/testfile1.txt", 48 | Mode: 0644, 49 | Size: int64(len("content1")), 50 | }, 51 | body: []byte("content1"), 52 | }} 53 | 54 | for _, e := range entries { 55 | if err := ta.WriteHeader(e.hdr); err != nil { 56 | t.Errorf("failed to write header %s: %v", e.hdr.Name, err) 57 | } 58 | if e.hdr.Size != 0 { 59 | if _, err := ta.Write(e.body); err != nil { 60 | t.Errorf("failed to write body %s: %v", e.hdr.Name, err) 61 | } 62 | } 63 | 64 | } 65 | return b 66 | } 67 | 68 | func TestFromTar(t *testing.T) { 69 | testCases := []struct { 70 | name string 71 | input io.Reader 72 | wantBasenames []string 73 | wantFileModes []uint16 74 | }{{ 75 | name: "simple tar", 76 | input: createTar(t), 77 | wantBasenames: []string{"dir1", "symlink1", "testfile1.txt"}, 78 | wantFileModes: []uint16{040755, 0120000, 0100644}, 79 | }} 80 | for _, tc := range testCases { 81 | tc := tc 82 | t.Run(tc.name, func(t *testing.T) { 83 | r, err := FromTar(tc.input, RPMMetaData{}) 84 | if err != nil { 85 | t.Errorf("FromTar returned err: %v", err) 86 | } 87 | if err := r.Write(io.Discard); err != nil { 88 | t.Errorf("r.Write() returned err: %v", err) 89 | } 90 | if r == nil { 91 | t.Fatalf("FromTar returned nil pointer") 92 | } 93 | if d := cmp.Diff(tc.wantBasenames, r.basenames); d != "" { 94 | t.Errorf("FromTar basenames differs (want->got):\n%v", d) 95 | } 96 | if d := cmp.Diff(tc.wantFileModes, r.filemodes); d != "" { 97 | t.Errorf("FromTar filemodes differs (want->got):\n%v", d) 98 | } 99 | }) 100 | } 101 | } 102 | --------------------------------------------------------------------------------