├── .cargo └── config.toml ├── .github ├── FUNDING.yml ├── actions │ ├── install-just │ │ └── action.yml │ └── rust-bootstrap │ │ └── action.yml └── workflows │ └── workspace.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Justfile ├── LICENSE ├── README.md ├── debian-packaging ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── binary_package_control.rs │ ├── binary_package_list.rs │ ├── changelog.rs │ ├── control.rs │ ├── deb │ ├── builder.rs │ ├── mod.rs │ └── reader.rs │ ├── debian_source_control.rs │ ├── debian_source_package_list.rs │ ├── dependency.rs │ ├── dependency_resolution.rs │ ├── error.rs │ ├── io.rs │ ├── keys │ ├── debian-10-archive.asc │ ├── debian-10-release.asc │ ├── debian-10-security.asc │ ├── debian-11-archive.asc │ ├── debian-11-release.asc │ ├── debian-11-security.asc │ ├── debian-8-archive.asc │ ├── debian-8-release.asc │ ├── debian-8-security.asc │ ├── debian-9-archive.asc │ ├── debian-9-release.asc │ └── debian-9-security.asc │ ├── lib.rs │ ├── package_version.rs │ ├── repository │ ├── builder.rs │ ├── contents.rs │ ├── copier.rs │ ├── filesystem.rs │ ├── http.rs │ ├── mod.rs │ ├── proxy_writer.rs │ ├── release.rs │ ├── s3.rs │ └── sink_writer.rs │ ├── signing_key.rs │ ├── source_package_control.rs │ └── testdata │ ├── inrelease-debian-bullseye │ ├── libzstd_1.4.8+dfsg-3.dsc │ ├── release-debian-bullseye │ └── release-ubuntu-focal ├── debian-repo-tool ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── src │ ├── cli.rs │ └── main.rs └── tests │ ├── cli_tests.rs │ └── cmd │ └── help.trycmd ├── linux-package-analyzer ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── src │ ├── binary.rs │ ├── cli.rs │ ├── db.rs │ ├── import.rs │ └── main.rs └── tests │ ├── cli_tests.rs │ └── cmd │ └── help.trycmd ├── release.toml ├── rpm-repository ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── http.rs │ ├── io.rs │ ├── lib.rs │ ├── metadata │ ├── mod.rs │ ├── primary.rs │ └── repomd.rs │ └── testdata │ └── fedora-35-repodata.xml └── scripts └── secure_download.py /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Crank up optimization level of packages in critical paths of tests. 2 | # This speeds up test execution substantially. 3 | [profile.dev.package.adler] 4 | opt-level = 3 5 | [profile.test.package.adler] 6 | opt-level = 3 7 | [profile.dev.package.adler32] 8 | opt-level = 3 9 | [profile.test.package.adler32] 10 | opt-level = 3 11 | [profile.dev.package.byteorder] 12 | opt-level = 3 13 | [profile.test.package.byteorder] 14 | opt-level = 3 15 | [profile.dev.package.bzip2] 16 | opt-level = 3 17 | [profile.test.package.bzip2] 18 | opt-level = 3 19 | [profile.dev.package.crc32fast] 20 | opt-level = 3 21 | [profile.test.package.crc32fast] 22 | opt-level = 3 23 | [profile.dev.package.difference] 24 | opt-level = 3 25 | [profile.test.package.difference] 26 | opt-level = 3 27 | [profile.dev.package.flate2] 28 | opt-level = 3 29 | [profile.test.package.flate2] 30 | opt-level = 3 31 | [profile.dev.package.libflate] 32 | opt-level = 3 33 | [profile.test.package.libflate] 34 | opt-level = 3 35 | [profile.dev.package.libflate_lz77] 36 | opt-level = 3 37 | [profile.test.package.libflate_lz77] 38 | opt-level = 3 39 | [profile.dev.package.miniz_oxide] 40 | opt-level = 3 41 | [profile.test.package.miniz_oxide] 42 | opt-level = 3 43 | [profile.dev.package.num-bigint-dig] 44 | opt-level = 3 45 | [profile.test.package.num-bigint-dig] 46 | opt-level = 3 47 | [profile.dev.package.rle-decode-fast] 48 | opt-level = 3 49 | [profile.test.package.rle-decode-fast] 50 | opt-level = 3 51 | [profile.dev.package.serde_yaml] 52 | opt-level = 3 53 | [profile.test.package.serde_yaml] 54 | opt-level = 3 55 | [profile.dev.package.sha2] 56 | opt-level = 3 57 | [profile.test.package.sha2] 58 | opt-level = 3 59 | [profile.dev.package.yaml-rust] 60 | opt-level = 3 61 | [profile.test.package.yaml-rust] 62 | opt-level = 3 63 | [profile.dev.package.zip] 64 | opt-level = 3 65 | [profile.test.package.zip] 66 | opt-level = 3 67 | [profile.dev.package.zstd] 68 | opt-level = 3 69 | [profile.test.package.zstd] 70 | opt-level = 3 71 | [profile.dev.package.zstd-safe] 72 | opt-level = 3 73 | [profile.test.package.zstd-safe] 74 | opt-level = 3 75 | [profile.dev.package.zstd-sys] 76 | opt-level = 3 77 | [profile.test.package.zstd-sys] 78 | opt-level = 3 79 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [indygreg] 2 | -------------------------------------------------------------------------------- /.github/actions/install-just/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Just 2 | description: Installs the Just CLI tool 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Install Linux 7 | if: runner.os == 'Linux' 8 | shell: bash 9 | run: | 10 | python3 scripts/secure_download.py \ 11 | https://github.com/casey/just/releases/download/1.1.1/just-1.1.1-x86_64-unknown-linux-musl.tar.gz \ 12 | ee7bf76941e8d7a41bab6716390a293e381a4a32bc46ad4d9d112f540aad34ba \ 13 | just.tar.gz 14 | tar -xzf just.tar.gz just 15 | mv just /usr/local/bin/just 16 | rm just* 17 | 18 | - name: Install macOS 19 | if: runner.os == 'macOS' 20 | shell: bash 21 | run: | 22 | python3 scripts/secure_download.py \ 23 | https://github.com/casey/just/releases/download/1.1.1/just-1.1.1-x86_64-apple-darwin.tar.gz \ 24 | afdc9eed21fdc3eedc6e853715232b982cd167d63b741afaf47462e7c61bfd83 \ 25 | just.tar.gz 26 | tar -xzf just.tar.gz just 27 | mv just /usr/local/bin/just 28 | rm just* 29 | 30 | - name: Install Windows 31 | if: runner.os == 'Windows' 32 | shell: pwsh 33 | run: | 34 | python3 scripts/secure_download.py https://github.com/casey/just/releases/download/1.1.1/just-1.1.1-x86_64-pc-windows-msvc.zip 446e6091b2aa2b40bc57857f1104cfe4153e757379141ae5ded1dec3da59c10b just.zip 35 | Expand-Archive -Path just.zip -DestinationPath c:/temp 36 | Move-Item c:/temp/just.exe c:/Windows/just.exe 37 | -------------------------------------------------------------------------------- /.github/actions/rust-bootstrap/action.yml: -------------------------------------------------------------------------------- 1 | name: Bootstrap Rust Building 2 | description: Configures the system environment for building Rust 3 | inputs: 4 | rust_toolchain: 5 | description: rustup toolchain to install 6 | default: stable 7 | required: false 8 | rust_target: 9 | description: rust target triple to install 10 | required: true 11 | runs: 12 | using: composite 13 | steps: 14 | - uses: ./.github/actions/install-just 15 | 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: ${{ inputs.rust_toolchain }} 19 | target: ${{ inputs.rust_target }} 20 | default: true 21 | profile: minimal 22 | components: clippy 23 | 24 | - uses: taiki-e/install-action@v1 25 | with: 26 | tool: nextest 27 | 28 | - name: Bootstrap Environment (Linux) 29 | if: runner.os == 'Linux' 30 | shell: bash 31 | run: | 32 | just actions-bootstrap-rust-linux 33 | 34 | - name: Bootstrap Environment (macOS) 35 | if: runner.os == 'macOS' 36 | shell: bash 37 | run: | 38 | just actions-bootstrap-rust-macos 39 | 40 | - name: Bootstrap Environment (Windows) 41 | if: runner.os == 'Windows' 42 | shell: pwsh 43 | run: | 44 | just actions-bootstrap-rust-windows 45 | 46 | - name: Start sccache 47 | shell: bash 48 | run: | 49 | sccache --start-server 50 | -------------------------------------------------------------------------------- /.github/workflows/workspace.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches-ignore: 4 | - 'ci-test' 5 | tags-ignore: 6 | - '**' 7 | pull_request: 8 | schedule: 9 | - cron: '12 15 * * *' 10 | workflow_dispatch: 11 | jobs: 12 | build-and-test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | rust_toolchain: 17 | - 'stable' 18 | - 'beta' 19 | - 'nightly' 20 | - '1.75.0' 21 | target: 22 | - os: 'ubuntu-22.04' 23 | triple: 'x86_64-unknown-linux-gnu' 24 | - os: 'macos-12' 25 | triple: 'x86_64-apple-darwin' 26 | - os: 'windows-2022' 27 | triple: 'x86_64-pc-windows-msvc' 28 | continue-on-error: ${{ matrix.rust_toolchain == 'nightly' }} 29 | runs-on: ${{ matrix.target.os }} 30 | env: 31 | IN_CI: '1' 32 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 33 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 34 | CARGO_TERM_COLOR: always 35 | SCCACHE_BUCKET: 'linux-packaging-rs-sccache' 36 | SCCACHE_S3_USE_SSL: '1' 37 | # Prevent sccache server from stopping due to inactivity. 38 | SCCACHE_IDLE_TIMEOUT: '0' 39 | steps: 40 | - uses: actions/checkout@v3 41 | 42 | - uses: ./.github/actions/rust-bootstrap 43 | with: 44 | rust_toolchain: ${{ matrix.rust_toolchain }} 45 | rust_target: ${{ matrix.target.triple }} 46 | 47 | - name: Build Workspace 48 | env: 49 | RUSTC_WRAPPER: sccache 50 | run: | 51 | rustc --version 52 | cargo build --workspace --no-default-features 53 | cargo build --workspace 54 | cargo nextest run --no-run --workspace 55 | 56 | - name: Test Workspace 57 | env: 58 | RUSTC_WRAPPER: sccache 59 | run: | 60 | cargo nextest run --no-fail-fast --success-output immediate-final --workspace 61 | 62 | - uses: actions-rs/clippy@master 63 | if: ${{ matrix.rust_toolchain == 'stable' || matrix.rust_toolchain == 'beta' }} 64 | env: 65 | RUSTC_WRAPPER: sccache 66 | with: 67 | args: --workspace 68 | 69 | - name: Stop sccache 70 | run: | 71 | sccache --stop-server 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | 'debian-packaging', 4 | "debian-repo-tool", 5 | 'linux-package-analyzer', 6 | 'rpm-repository', 7 | ] 8 | resolver = "2" 9 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | default: 2 | cargo build 3 | 4 | actions-install-sccache-linux: 5 | python3 scripts/secure_download.py \ 6 | https://github.com/mozilla/sccache/releases/download/v0.3.0/sccache-v0.3.0-x86_64-unknown-linux-musl.tar.gz \ 7 | e6cd8485f93d683a49c83796b9986f090901765aa4feb40d191b03ea770311d8 \ 8 | sccache.tar.gz 9 | tar -xvzf sccache.tar.gz 10 | mv sccache-v0.3.0-x86_64-unknown-linux-musl/sccache /home/runner/.cargo/bin/sccache 11 | rm -rf sccache* 12 | chmod +x /home/runner/.cargo/bin/sccache 13 | 14 | actions-install-sccache-macos: 15 | python3 scripts/secure_download.py \ 16 | https://github.com/mozilla/sccache/releases/download/v0.3.0/sccache-v0.3.0-x86_64-apple-darwin.tar.gz \ 17 | 61c16fd36e32cdc923b66e4f95cb367494702f60f6d90659af1af84c3efb11eb \ 18 | sccache.tar.gz 19 | tar -xvzf sccache.tar.gz 20 | mv sccache-v0.3.0-x86_64-apple-darwin/sccache /Users/runner/.cargo/bin/sccache 21 | rm -rf sccache* 22 | chmod +x /Users/runner/.cargo/bin/sccache 23 | 24 | actions-install-sccache-windows: 25 | python3 scripts/secure_download.py \ 26 | https://github.com/mozilla/sccache/releases/download/v0.3.0/sccache-v0.3.0-x86_64-pc-windows-msvc.tar.gz \ 27 | f25e927584d79d0d5ad489e04ef01b058dad47ef2c1633a13d4c69dfb83ba2be \ 28 | sccache.tar.gz 29 | tar -xvzf sccache.tar.gz 30 | mv sccache-v0.3.0-x86_64-pc-windows-msvc/sccache.exe C:/Users/runneradmin/.cargo/bin/sccache.exe 31 | 32 | actions-bootstrap-rust-linux: actions-install-sccache-linux 33 | 34 | actions-bootstrap-rust-macos: actions-install-sccache-macos 35 | 36 | actions-bootstrap-rust-windows: actions-install-sccache-windows 37 | 38 | # Trigger a workflow on a branch. 39 | ci-run workflow branch="ci-test": 40 | gh workflow run {{workflow}} --ref {{branch}} 41 | 42 | # Trigger all workflows on a given branch. 43 | ci-run-all branch="ci-test": 44 | just ci-run workspace.yml {{branch}} 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linux-packaging-rs 2 | 3 | This repository contains a collection of Rust crates related to Linux 4 | packaging. 5 | 6 | See the various project directories for more. 7 | -------------------------------------------------------------------------------- /debian-packaging/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `debian-packaging` History 2 | 3 | 4 | 5 | ## Unreleased 6 | 7 | Released on ReleaseDate. 8 | 9 | ## 0.18.0 10 | 11 | Released on 2024-11-02. 12 | 13 | * Fixed compile error when building without the `http` feature. 14 | * MSRV 1.70 -> 1.75. 15 | * `tokio` is now an optional dependency and is dependent on the `http` feature. 16 | * `async-std` 1.12 -> 1.13. 17 | * `async-tar` 0.4 -> 0.5. 18 | * `bytes` 1.5 -> 1.8. 19 | * `libflate` 2.0 -> 2.1. 20 | * `mailparse` 0.14 -> 0.15. 21 | * `once_cell` 1.18 -> 1.20. 22 | * `os_str_bytes` 6.6 -> 7.0. 23 | * `pgp` 0.10 -> 0.14. 24 | * `regex` 1.10 -> 1.11. 25 | * `reqwest` 0.11 -> 0.12. 26 | * `smallvec` 1.11 -> 1.13. 27 | * `strum` 0.25 -> 0.26. 28 | * `strum_macros` 0.25 -> 0.26. 29 | * `tokio` 1.33 -> 1.41. 30 | * `url` 2.4 -> 2.5. 31 | * `tempfile` 3.8 -> 3.13. 32 | 33 | ## 0.17.0 34 | 35 | Released on 2023-11-03. 36 | 37 | * MSRV 1.62 -> 1.70. 38 | * Package version lexical comparison reworked to avoid sorting. 39 | * `.deb` tar archives now correctly encode directories as directory entries. 40 | * Release files with `Contents*` files in the top-level directory are now 41 | parsed without error. The stored `component` field is now an 42 | `Option` and various APIs taking a `component: &str` now take 43 | `Option<&str>` since components are now optional. 44 | * Various package updates to latest versions. 45 | 46 | ## 0.16.0 and Earlier 47 | 48 | * No changelog kept. 49 | -------------------------------------------------------------------------------- /debian-packaging/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "debian-packaging" 3 | version = "0.18.0" 4 | authors = ["Gregory Szorc "] 5 | edition = "2021" 6 | rust-version = "1.75" 7 | license = "MPL-2.0" 8 | description = "Debian packaging primitives" 9 | keywords = ["debian", "package", "apt", "deb"] 10 | homepage = "https://github.com/indygreg/linux-packaging-rs" 11 | repository = "https://github.com/indygreg/linux-packaging-rs.git" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | ar = "0.9.0" 16 | async-std = { version = "1.13.0", features = ["unstable"] } 17 | async-tar = "0.5.0" 18 | async-trait = "0.1.83" 19 | bytes = "1.8.0" 20 | chrono = "0.4.38" 21 | digest = "0.10.7" 22 | futures = "0.3.31" 23 | hex = "0.4.3" 24 | libflate = "2.1.0" 25 | mailparse = "0.15.0" 26 | md-5 = "0.10.6" 27 | once_cell = "1.18.0" 28 | os_str_bytes = { version = "7.0.0", features = ["conversions"] } 29 | pin-project = "1.1.3" 30 | pgp = "0.14.0" 31 | pgp-cleartext = "0.11.0" 32 | rand = "0.8.5" 33 | regex = "1.11.1" 34 | serde = { version = "1.0.214", features = ["derive"] } 35 | sha-1 = "0.10.1" 36 | sha2 = "0.10.8" 37 | simple-file-manifest = "0.11.0" 38 | smallvec = "1.13.2" 39 | strum = { version = "0.26.3", features = ["derive"] } 40 | strum_macros = "0.26.4" 41 | tar = "0.4.43" 42 | thiserror = "1.0.66" 43 | tokio = { version = "1.41.0", default-features = false, optional = true } 44 | url = "2.5.2" 45 | xz2 = { version = "0.1.7", features = ["static"] } 46 | zstd = "0.13.2" 47 | 48 | [dependencies.async-compression] 49 | version = "0.4.17" 50 | features = ["bzip2", "futures-io", "gzip", "lzma", "xz", "zstd"] 51 | 52 | [dependencies.reqwest] 53 | version = "0.12.9" 54 | optional = true 55 | default-features = false 56 | features = ["rustls-tls", "stream"] 57 | 58 | [dependencies.rusoto_core] 59 | version = "0.48.0" 60 | optional = true 61 | default-features = false 62 | features = ["rustls"] 63 | 64 | [dependencies.rusoto_s3] 65 | version = "0.48.0" 66 | optional = true 67 | default-features = false 68 | features = ["rustls"] 69 | 70 | [dev-dependencies] 71 | glob = "0.3.1" 72 | indoc = "2.0.5" 73 | tempfile = "3.13.0" 74 | tokio = { version = "1.41.0", features = ["macros", "rt"] } 75 | 76 | [features] 77 | default = ["http", "s3"] 78 | http = ["reqwest"] 79 | s3 = ["dep:rusoto_core", "dep:rusoto_s3", "dep:tokio"] 80 | -------------------------------------------------------------------------------- /debian-packaging/README.md: -------------------------------------------------------------------------------- 1 | # debian-packaging 2 | 3 | `debian-packaging` is a library crate implementing functionality related 4 | to Debian packaging. The following functionality is (partially) 5 | implemented: 6 | 7 | * Parsing and serializing control files 8 | * Parsing `Release` and `InRelease` files. 9 | * Parsing `Packages` files. 10 | * Fetching Debian repository files from an HTTP server. 11 | * Writing changelog files. 12 | * Reading and writing `.deb` files. 13 | * Creating repositories. 14 | * PGP signing and verification operations. 15 | 16 | See the crate's documentation for more. 17 | -------------------------------------------------------------------------------- /debian-packaging/src/binary_package_control.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Debian binary package control files. */ 6 | 7 | use { 8 | crate::{ 9 | control::ControlParagraph, 10 | dependency::{DependencyList, PackageDependencyFields}, 11 | error::{DebianError, Result}, 12 | io::ContentDigest, 13 | package_version::PackageVersion, 14 | repository::{builder::DebPackageReference, release::ChecksumType}, 15 | }, 16 | std::ops::{Deref, DerefMut}, 17 | }; 18 | 19 | /// A Debian binary package control file/paragraph. 20 | /// 21 | /// See . 22 | /// 23 | /// Binary package control files are defined by a single paragraph with well-defined 24 | /// fields. This type is a low-level wrapper around an inner [ControlParagraph]. 25 | /// [Deref] and [DerefMut] can be used to operate on the inner [ControlParagraph]. 26 | /// [From] and [Into] are implemented in both directions to enable cheap coercion 27 | /// between the types. 28 | /// 29 | /// Binary package control paragraphs are seen in `DEBIAN/control` files. Variations 30 | /// also exist in `Packages` files in repositories and elsewhere. 31 | /// 32 | /// Fields annotated as *mandatory* in the Debian Policy Manual have getters that 33 | /// return [Result] and will error if a field is not present. Non-mandatory fields 34 | /// return [Option]. This enforcement can be bypassed by calling 35 | /// [ControlParagraph::field()]. 36 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 37 | pub struct BinaryPackageControlFile<'a> { 38 | paragraph: ControlParagraph<'a>, 39 | } 40 | 41 | impl<'a> Deref for BinaryPackageControlFile<'a> { 42 | type Target = ControlParagraph<'a>; 43 | 44 | fn deref(&self) -> &Self::Target { 45 | &self.paragraph 46 | } 47 | } 48 | 49 | impl<'a> DerefMut for BinaryPackageControlFile<'a> { 50 | fn deref_mut(&mut self) -> &mut Self::Target { 51 | &mut self.paragraph 52 | } 53 | } 54 | 55 | impl<'a> From> for BinaryPackageControlFile<'a> { 56 | fn from(paragraph: ControlParagraph<'a>) -> Self { 57 | Self { paragraph } 58 | } 59 | } 60 | 61 | impl<'a> From> for ControlParagraph<'a> { 62 | fn from(cf: BinaryPackageControlFile<'a>) -> Self { 63 | cf.paragraph 64 | } 65 | } 66 | 67 | impl<'a> BinaryPackageControlFile<'a> { 68 | /// The `Package` field value. 69 | pub fn package(&self) -> Result<&str> { 70 | self.required_field_str("Package") 71 | } 72 | 73 | /// The `Version` field as its original string. 74 | pub fn version_str(&self) -> Result<&str> { 75 | self.required_field_str("Version") 76 | } 77 | 78 | /// The `Version` field parsed into a [PackageVersion]. 79 | pub fn version(&self) -> Result { 80 | PackageVersion::parse(self.version_str()?) 81 | } 82 | 83 | /// The `Architecture` field. 84 | pub fn architecture(&self) -> Result<&str> { 85 | self.required_field_str("Architecture") 86 | } 87 | 88 | /// The `Maintainer` field. 89 | pub fn maintainer(&self) -> Result<&str> { 90 | self.required_field_str("Maintainer") 91 | } 92 | 93 | /// The `Description` field. 94 | pub fn description(&self) -> Result<&str> { 95 | self.required_field_str("Description") 96 | } 97 | 98 | /// The `Source` field. 99 | pub fn source(&self) -> Option<&str> { 100 | self.field_str("Source") 101 | } 102 | 103 | /// The `Section` field. 104 | pub fn section(&self) -> Option<&str> { 105 | self.field_str("Section") 106 | } 107 | 108 | /// The `Priority` field. 109 | pub fn priority(&self) -> Option<&str> { 110 | self.field_str("Priority") 111 | } 112 | 113 | /// The `Essential` field. 114 | pub fn essential(&self) -> Option<&str> { 115 | self.field_str("Essential") 116 | } 117 | 118 | /// The `Homepage` field. 119 | pub fn homepage(&self) -> Option<&str> { 120 | self.field_str("Homepage") 121 | } 122 | 123 | /// The `Installed-Size` field, parsed to a [u64]. 124 | pub fn installed_size(&self) -> Option> { 125 | self.field_u64("Installed-Size") 126 | } 127 | 128 | /// The `Size` field, parsed to a [u64]. 129 | pub fn size(&self) -> Option> { 130 | self.field_u64("Size") 131 | } 132 | 133 | /// The `Built-Using` field. 134 | pub fn built_using(&self) -> Option<&str> { 135 | self.field_str("Built-Using") 136 | } 137 | 138 | /// The `Depends` field, parsed to a [DependencyList]. 139 | pub fn depends(&self) -> Option> { 140 | self.field_dependency_list("Depends") 141 | } 142 | 143 | /// The `Recommends` field, parsed to a [DependencyList]. 144 | pub fn recommends(&self) -> Option> { 145 | self.field_dependency_list("Recommends") 146 | } 147 | 148 | /// The `Suggests` field, parsed to a [DependencyList]. 149 | pub fn suggests(&self) -> Option> { 150 | self.field_dependency_list("Suggests") 151 | } 152 | 153 | /// The `Enhances` field, parsed to a [DependencyList]. 154 | pub fn enhances(&self) -> Option> { 155 | self.field_dependency_list("Enhances") 156 | } 157 | 158 | /// The `Pre-Depends` field, parsed to a [DependencyList]. 159 | pub fn pre_depends(&self) -> Option> { 160 | self.field_dependency_list("Pre-Depends") 161 | } 162 | 163 | /// Obtain parsed values of all fields defining dependencies. 164 | pub fn package_dependency_fields(&self) -> Result { 165 | PackageDependencyFields::from_paragraph(self) 166 | } 167 | } 168 | 169 | impl<'cf, 'a: 'cf> DebPackageReference<'cf> for BinaryPackageControlFile<'a> { 170 | fn deb_size_bytes(&self) -> Result { 171 | self.size() 172 | .ok_or_else(|| DebianError::ControlRequiredFieldMissing("Size".to_string()))? 173 | } 174 | 175 | fn deb_digest(&self, checksum: ChecksumType) -> Result { 176 | let hex_digest = self 177 | .paragraph 178 | .field_str(checksum.field_name()) 179 | .ok_or_else(|| { 180 | DebianError::ControlRequiredFieldMissing(checksum.field_name().to_string()) 181 | })?; 182 | 183 | ContentDigest::from_hex_digest(checksum, hex_digest) 184 | } 185 | 186 | fn deb_filename(&self) -> Result { 187 | let filename = self 188 | .field_str("Filename") 189 | .ok_or_else(|| DebianError::ControlRequiredFieldMissing("Filename".to_string()))?; 190 | 191 | Ok(if let Some((_, s)) = filename.rsplit_once('/') { 192 | s.to_string() 193 | } else { 194 | filename.to_string() 195 | }) 196 | } 197 | 198 | fn control_file_for_packages_index(&self) -> Result> { 199 | Ok(self.clone()) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /debian-packaging/src/binary_package_list.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Interface with a collection of binary package control definitions. */ 6 | 7 | use { 8 | crate::binary_package_control::BinaryPackageControlFile, 9 | std::ops::{Deref, DerefMut}, 10 | }; 11 | 12 | /// Represents a collection of binary package control files. 13 | /// 14 | /// Various operations in Debian packaging operate against a collection of 15 | /// binary package control files. For example, resolving dependencies of a 16 | /// package requires finding packages from an available set. This type facilitates 17 | /// the implementation of said operations. 18 | #[derive(Clone, Debug, Default)] 19 | pub struct BinaryPackageList<'a> { 20 | packages: Vec>, 21 | } 22 | 23 | impl<'a> Deref for BinaryPackageList<'a> { 24 | type Target = Vec>; 25 | 26 | fn deref(&self) -> &Self::Target { 27 | &self.packages 28 | } 29 | } 30 | 31 | impl<'a> DerefMut for BinaryPackageList<'a> { 32 | fn deref_mut(&mut self) -> &mut Self::Target { 33 | &mut self.packages 34 | } 35 | } 36 | 37 | impl<'a> IntoIterator for BinaryPackageList<'a> { 38 | type Item = BinaryPackageControlFile<'a>; 39 | type IntoIter = std::vec::IntoIter; 40 | 41 | fn into_iter(self) -> Self::IntoIter { 42 | self.packages.into_iter() 43 | } 44 | } 45 | 46 | impl<'a> BinaryPackageList<'a> { 47 | /// Find instances of a package within this collection. 48 | pub fn find_packages_with_name( 49 | &self, 50 | package: String, 51 | ) -> impl Iterator> { 52 | self.packages 53 | .iter() 54 | .filter(move |cf| matches!(cf.package(), Ok(name) if name == package)) 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod test { 60 | use { 61 | super::*, 62 | crate::{control::ControlParagraphReader, error::Result}, 63 | indoc::indoc, 64 | std::io::Cursor, 65 | }; 66 | 67 | const FOO_1_2: &str = indoc! {" 68 | Package: foo 69 | Version: 1.2 70 | Installed-Size: 20268 71 | Architecture: amd64 72 | "}; 73 | 74 | const BAR_1_0: &str = indoc! {" 75 | Package: bar 76 | Version: 1.0 77 | Architecture: amd64 78 | Depends: foo (>= 1.2) 79 | "}; 80 | 81 | const BAZ_1_1: &str = indoc! {" 82 | Package: baz 83 | Version: 1.1 84 | Architecture: amd64 85 | Depends: bar (>= 1.0) 86 | "}; 87 | 88 | #[test] 89 | fn find_package() -> Result<()> { 90 | let foo_para = ControlParagraphReader::new(Cursor::new(FOO_1_2.as_bytes())) 91 | .next() 92 | .unwrap()?; 93 | 94 | let bar_para = ControlParagraphReader::new(Cursor::new(BAR_1_0.as_bytes())) 95 | .next() 96 | .unwrap()?; 97 | 98 | let baz_para = ControlParagraphReader::new(Cursor::new(BAZ_1_1.as_bytes())) 99 | .next() 100 | .unwrap()?; 101 | 102 | let mut l = BinaryPackageList::default(); 103 | l.push(BinaryPackageControlFile::from(foo_para)); 104 | l.push(BinaryPackageControlFile::from(bar_para)); 105 | l.push(BinaryPackageControlFile::from(baz_para)); 106 | 107 | assert_eq!(l.find_packages_with_name("other".into()).count(), 0); 108 | 109 | let packages = l.find_packages_with_name("foo".into()).collect::>(); 110 | assert_eq!(packages.len(), 1); 111 | assert_eq!(packages[0].version_str()?, "1.2"); 112 | 113 | let packages = l.find_packages_with_name("bar".into()).collect::>(); 114 | assert_eq!(packages.len(), 1); 115 | 116 | Ok(()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /debian-packaging/src/changelog.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Defines types representing debian/changelog files. 6 | 7 | See 8 | for the specification. 9 | */ 10 | 11 | use { 12 | chrono::{DateTime, Local}, 13 | std::{borrow::Cow, io::Write}, 14 | }; 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct ChangelogEntry<'a> { 18 | pub package: Cow<'a, str>, 19 | pub version: Cow<'a, str>, 20 | pub distributions: Vec>, 21 | pub urgency: Cow<'a, str>, 22 | pub details: Cow<'a, str>, 23 | pub maintainer_name: Cow<'a, str>, 24 | pub maintainer_email: Cow<'a, str>, 25 | pub date: DateTime, 26 | } 27 | 28 | impl<'a> ChangelogEntry<'a> { 29 | /// Serialize the changelog entry to a writer. 30 | /// 31 | /// This incurs multiple `.write()` calls. So a buffered writer is 32 | /// recommended if performance matters. 33 | pub fn write(&self, writer: &mut W) -> std::io::Result<()> { 34 | /* 35 | package (version) distribution(s); urgency=urgency 36 | [optional blank line(s), stripped] 37 | * change details 38 | more change details 39 | [blank line(s), included in output of dpkg-parsechangelog] 40 | * even more change details 41 | [optional blank line(s), stripped] 42 | -- maintainer name [two spaces] date 43 | */ 44 | writer.write_all(self.package.as_bytes())?; 45 | writer.write_all(b" (")?; 46 | writer.write_all(self.version.as_bytes())?; 47 | writer.write_all(b") ")?; 48 | writer.write_all(self.distributions.join(" ").as_bytes())?; 49 | writer.write_all(b"; urgency=")?; 50 | writer.write_all(self.urgency.as_bytes())?; 51 | writer.write_all(b"\n\n")?; 52 | writer.write_all(self.details.as_bytes())?; 53 | writer.write_all(b"\n")?; 54 | writer.write_all(b"-- ")?; 55 | writer.write_all(self.maintainer_name.as_bytes())?; 56 | writer.write_all(b" <")?; 57 | writer.write_all(self.maintainer_email.as_bytes())?; 58 | writer.write_all(b"> ")?; 59 | writer.write_all(self.date.to_rfc2822().as_bytes())?; 60 | writer.write_all(b"\n\n")?; 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | /// Represents a complete `debian/changelog` file. 67 | /// 68 | /// Changelogs are an ordered series of `ChangelogEntry` items. 69 | #[derive(Default)] 70 | pub struct Changelog<'a> { 71 | entries: Vec>, 72 | } 73 | 74 | impl<'a> Changelog<'a> { 75 | /// Add an entry to this changelog. 76 | pub fn add_entry<'b: 'a>(&mut self, entry: ChangelogEntry<'b>) { 77 | self.entries.push(entry) 78 | } 79 | 80 | /// Serialize the changelog to a writer. 81 | /// 82 | /// Use of a buffered writer is encouraged if performance is a concern. 83 | pub fn write(&self, writer: &mut W) -> std::io::Result<()> { 84 | for entry in &self.entries { 85 | entry.write(writer)?; 86 | } 87 | 88 | Ok(()) 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use {super::*, crate::error::Result}; 95 | 96 | #[test] 97 | fn test_write() -> Result<()> { 98 | let mut changelog = Changelog::default(); 99 | changelog.add_entry(ChangelogEntry { 100 | package: "mypackage".into(), 101 | version: "0.1".into(), 102 | distributions: vec!["mydist".into()], 103 | urgency: "low".into(), 104 | details: "details".into(), 105 | maintainer_name: "maintainer".into(), 106 | maintainer_email: "me@example.com".into(), 107 | date: DateTime::from_naive_utc_and_offset( 108 | chrono::DateTime::from_timestamp(1420000000, 0) 109 | .unwrap() 110 | .naive_utc(), 111 | chrono::TimeZone::from_offset(&chrono::FixedOffset::west_opt(3600 * 7).unwrap()), 112 | ), 113 | }); 114 | 115 | let mut buf = vec![]; 116 | changelog.write(&mut buf)?; 117 | 118 | let s = String::from_utf8(buf).unwrap(); 119 | assert_eq!(s, "mypackage (0.1) mydist; urgency=low\n\ndetails\n-- maintainer Tue, 30 Dec 2014 21:26:40 -0700\n\n"); 120 | 121 | Ok(()) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /debian-packaging/src/deb/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Interfaces for .deb package files. 6 | 7 | The .deb file specification lives at . 8 | */ 9 | 10 | use {crate::error::Result, std::io::Read}; 11 | 12 | pub mod builder; 13 | pub mod reader; 14 | 15 | /// Compression format to apply to `.deb` files. 16 | pub enum DebCompression { 17 | /// Do not compress contents of `.deb` files. 18 | Uncompressed, 19 | /// Compress as `.gz` files. 20 | Gzip, 21 | /// Compress as `.xz` files using a specified compression level. 22 | Xz(u32), 23 | /// Compress as `.zst` files using a specified compression level. 24 | Zstandard(i32), 25 | } 26 | 27 | impl DebCompression { 28 | /// Obtain the filename extension for this compression format. 29 | pub fn extension(&self) -> &'static str { 30 | match self { 31 | Self::Uncompressed => "", 32 | Self::Gzip => ".gz", 33 | Self::Xz(_) => ".xz", 34 | Self::Zstandard(_) => ".zst", 35 | } 36 | } 37 | 38 | /// Compress input data from a reader. 39 | pub fn compress(&self, reader: &mut impl Read) -> Result> { 40 | let mut buffer = vec![]; 41 | 42 | match self { 43 | Self::Uncompressed => { 44 | std::io::copy(reader, &mut buffer)?; 45 | } 46 | Self::Gzip => { 47 | let header = libflate::gzip::HeaderBuilder::new().finish(); 48 | 49 | let mut encoder = libflate::gzip::Encoder::with_options( 50 | &mut buffer, 51 | libflate::gzip::EncodeOptions::new().header(header), 52 | )?; 53 | std::io::copy(reader, &mut encoder)?; 54 | encoder.finish().into_result()?; 55 | } 56 | Self::Xz(level) => { 57 | let mut encoder = xz2::write::XzEncoder::new(buffer, *level); 58 | std::io::copy(reader, &mut encoder)?; 59 | buffer = encoder.finish()?; 60 | } 61 | Self::Zstandard(level) => { 62 | let mut encoder = zstd::Encoder::new(buffer, *level)?; 63 | std::io::copy(reader, &mut encoder)?; 64 | buffer = encoder.finish()?; 65 | } 66 | } 67 | 68 | Ok(buffer) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /debian-packaging/src/debian_source_package_list.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! A collection of source control package control files. */ 6 | 7 | use { 8 | crate::debian_source_control::DebianSourceControlFile, 9 | std::ops::{Deref, DerefMut}, 10 | }; 11 | 12 | /// Represents a collection of Debian source control paragraphs. 13 | /// 14 | /// This provides a wrapper around [Vec] for convenience. 15 | /// 16 | /// Note that [DebianSourceControlFile] within this collection may not conform to the 17 | /// strict requirements of Debian source control `.dsc` files. For example, the 18 | /// `Source` field may not be present (try `Package` instead). 19 | #[derive(Default)] 20 | pub struct DebianSourcePackageList<'a> { 21 | packages: Vec>, 22 | } 23 | 24 | impl<'a> Deref for DebianSourcePackageList<'a> { 25 | type Target = Vec>; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.packages 29 | } 30 | } 31 | 32 | impl<'a> DerefMut for DebianSourcePackageList<'a> { 33 | fn deref_mut(&mut self) -> &mut Self::Target { 34 | &mut self.packages 35 | } 36 | } 37 | 38 | impl<'a> IntoIterator for DebianSourcePackageList<'a> { 39 | type Item = DebianSourceControlFile<'a>; 40 | type IntoIter = std::vec::IntoIter; 41 | 42 | fn into_iter(self) -> Self::IntoIter { 43 | self.packages.into_iter() 44 | } 45 | } 46 | 47 | impl<'a> DebianSourcePackageList<'a> { 48 | /// Find source packages having the given name. 49 | /// 50 | /// This patches against the `Package` field in the control files. 51 | pub fn iter_with_package_name( 52 | &self, 53 | package: String, 54 | ) -> impl Iterator> { 55 | self.packages.iter().filter( 56 | move |cf| matches!(cf.required_field_str("Package"), Ok(name) if name == package), 57 | ) 58 | } 59 | 60 | /// Find source packages providing the given binary package. 61 | /// 62 | /// This consults the list of binary packages in the `Binary` field and returns control 63 | /// paragraphs where `package` appears in that list. 64 | pub fn iter_with_binary_package( 65 | &self, 66 | package: String, 67 | ) -> impl Iterator> { 68 | self.packages.iter().filter(move |cf| { 69 | if let Some(mut packages) = cf.binary() { 70 | packages.any(|p| p == package) 71 | } else { 72 | false 73 | } 74 | }) 75 | } 76 | 77 | /// Find source packages providing packages for the given architecture. 78 | /// 79 | /// This consults the list of architectures in the `Architecture` field and returns 80 | /// control paragraphs where `architecture` appears in that list. 81 | pub fn iter_with_architecture( 82 | &self, 83 | architecture: String, 84 | ) -> impl Iterator> { 85 | self.packages.iter().filter(move |cf| { 86 | if let Some(mut architectures) = cf.architecture() { 87 | architectures.any(|a| a == architecture) 88 | } else { 89 | false 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /debian-packaging/src/error.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Error handling. */ 6 | 7 | use {simple_file_manifest::FileManifestError, thiserror::Error}; 8 | 9 | /// Primary crate error type. 10 | #[derive(Debug, Error)] 11 | pub enum DebianError { 12 | #[error("file manifest error: {0}")] 13 | FileManifestError(#[from] FileManifestError), 14 | 15 | #[error("URL error: {0:?}")] 16 | Url(#[from] url::ParseError), 17 | 18 | #[error("PGP error: {0:?}")] 19 | Pgp(#[from] pgp::errors::Error), 20 | 21 | #[error("date parsing error: {0:?}")] 22 | DateParse(#[from] mailparse::MailParseError), 23 | 24 | #[cfg(feature = "http")] 25 | #[error("HTTP error: {0:?}")] 26 | Reqwest(#[from] reqwest::Error), 27 | 28 | #[error("I/O error: {0:?}")] 29 | Io(#[from] std::io::Error), 30 | 31 | #[error("integer parsing error: {0:?}")] 32 | ParseInt(#[from] std::num::ParseIntError), 33 | 34 | #[error("invalid hex string (`{0}`) when parsing content digest: {0:?}")] 35 | ContentDigestBadHex(String, hex::FromHexError), 36 | 37 | #[error("control file parse error: {0}")] 38 | ControlParseError(String), 39 | 40 | #[error("Control file lacks a paragraph")] 41 | ControlFileNoParagraph, 42 | 43 | #[error("Control file not found")] 44 | ControlFileNotFound, 45 | 46 | #[error("cannot convert to simple field value since value contains line breaks")] 47 | ControlSimpleValueNoMultiline, 48 | 49 | #[error("required control paragraph field not found: {0}")] 50 | ControlRequiredFieldMissing(String), 51 | 52 | #[error("control field {0} can not be parsed as an integer: {0:?}")] 53 | ControlFieldIntParse(String, std::num::ParseIntError), 54 | 55 | #[error("failed to parse control field timestamp")] 56 | ControlFieldTimestampParse, 57 | 58 | #[error("missing field {0} in Package-List entry")] 59 | ControlPackageListMissingField(&'static str), 60 | 61 | #[error("expected 1 control paragraph in Debian source control file; got {0}")] 62 | DebianSourceControlFileParagraphMismatch(usize), 63 | 64 | #[error("unknown entry in binary package archive: {0}")] 65 | DebUnknownBinaryPackageEntry(String), 66 | 67 | #[error("unknown compression in deb archive file: {0}")] 68 | DebUnknownCompression(String), 69 | 70 | #[error("do not know how to construct repository reader from URL: {0}")] 71 | RepositoryReaderUnrecognizedUrl(String), 72 | 73 | #[error("do not know how to construct repository writer from URL: {0}")] 74 | RepositoryWriterUnrecognizedUrl(String), 75 | 76 | #[error("release file does not contain supported checksum flavor")] 77 | RepositoryReadReleaseNoKnownChecksum, 78 | 79 | #[error("could not find Contents indices entry in Release file")] 80 | RepositoryReadContentsIndicesEntryNotFound, 81 | 82 | #[error("could not find packages indices entry in Release file")] 83 | RepositoryReadPackagesIndicesEntryNotFound, 84 | 85 | #[error("could not find Sources indices entry in Release file")] 86 | RepositoryReadSourcesIndicesEntryNotFound, 87 | 88 | #[error("could not determine content digest of binary package")] 89 | RepositoryReadCouldNotDeterminePackageDigest, 90 | 91 | #[error("No packages indices for checksum {0}")] 92 | RepositoryNoPackagesIndices(&'static str), 93 | 94 | #[error("repository I/O error on path {0}: {1:?}")] 95 | RepositoryIoPath(String, std::io::Error), 96 | 97 | #[error("attempting to add package to undefined component: {0}")] 98 | RepositoryBuildUnknownComponent(String), 99 | 100 | #[error("attempting to add package to undefined architecture: {0}")] 101 | RepositoryBuildUnknownArchitecture(String), 102 | 103 | #[error("pool layout cannot be changed after content is indexed")] 104 | RepositoryBuildPoolLayoutImmutable, 105 | 106 | #[error(".deb not available: {0}")] 107 | RepositoryBuildDebNotAvailable(&'static str), 108 | 109 | #[error("expected 1 paragraph in control file; got {0}")] 110 | ReleaseControlParagraphMismatch(usize), 111 | 112 | #[error("digest missing from index entry")] 113 | ReleaseMissingDigest, 114 | 115 | #[error("size missing from index entry")] 116 | ReleaseMissingSize, 117 | 118 | #[error("path missing from index entry")] 119 | ReleaseMissingPath, 120 | 121 | #[error("index entry path unexpectedly has spaces: {0}")] 122 | ReleasePathWithSpaces(String), 123 | 124 | #[error("release indices file cannot be converted to the given type")] 125 | ReleaseIndicesEntryWrongType, 126 | 127 | #[error("No PGP signatures found")] 128 | ReleaseNoSignatures, 129 | 130 | #[error("No PGP signatures found from the specified key")] 131 | ReleaseNoSignaturesByKey, 132 | 133 | #[error("indices files not found in Release file")] 134 | ReleaseNoIndicesFiles, 135 | 136 | #[error("failed to parse dependency expression: {0}")] 137 | DependencyParse(String), 138 | 139 | #[error("unknown binary dependency field: {0}")] 140 | UnknownBinaryDependencyField(String), 141 | 142 | #[error("the epoch component has non-digit characters: {0}")] 143 | EpochNonNumeric(String), 144 | 145 | #[error("upstream_version component has illegal character: {0}")] 146 | UpstreamVersionIllegalChar(String), 147 | 148 | #[error("debian_revision component has illegal character: {0}")] 149 | DebianRevisionIllegalChar(String), 150 | 151 | #[error("unknown S3 region: {0}")] 152 | S3BadRegion(String), 153 | 154 | #[error("unknown verify behavior for null:// destination: {0}")] 155 | SinkWriterVerifyBehaviorUnknown(String), 156 | 157 | #[error("{0}")] 158 | Other(String), 159 | } 160 | 161 | /// Result wrapper for this crate. 162 | pub type Result = std::result::Result; 163 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-10-release.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 51 | idjr8CGNeffTyeMJbSniGisGOkhiX9TLbz8ufw== 52 | =a2kx 53 | -----END PGP PUBLIC KEY BLOCK----- 54 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-11-release.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 54 | e18slw8= 55 | =7Dni 56 | -----END PGP PUBLIC KEY BLOCK----- 57 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-8-archive.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 109 | 3rRmtFrJQ50iIcnJX8TjzB9e 110 | =2Gih 111 | -----END PGP PUBLIC KEY BLOCK----- 112 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-8-release.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 60 | 2a65h59CeW4zAYbU/0t47ghTDi/ybqTVbcsp 61 | =kPlg 62 | -----END PGP PUBLIC KEY BLOCK----- 63 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-8-security.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 109 | lc/RtZpF5uUq1yGf/TyOgm9X3tiS+mueTz36 110 | =pvO8 111 | -----END PGP PUBLIC KEY BLOCK----- 112 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-9-archive.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 158 | k5Bh 159 | =3qWJ 160 | -----END PGP PUBLIC KEY BLOCK----- 161 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-9-release.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 50 | HNbs0H9t4g== 51 | =DqiM 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /debian-packaging/src/keys/debian-9-security.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 39 | 9ctfpvsvRgroju2g0jiK4iupBQJZIzGlFwyAATCZEb6pZtBhMFMEVxG05f8VsP2C 40 | AgcAAAoJEO2g0jiK4iupW14P/0NjlEQWA3W7lpzAoWcEgStiCLlk0EP0wMxPM5Vo 41 | 1WrnUemLTfrI4rIs17Ocs6opX9nEYPxfIoVGxqqNqvuj3Hej5JnglPctbJ1+ttNX 42 | fuB6yqsa/wANPOC5qAVy+u4MY1ljyKSTCtc9kwc+K2Vjr/TrRDDq/o2xrmjC4JwP 43 | 3IpPr2dolGsBUQ3+HWAeLgBhqH3e4sYL5DLQgav+2cv39ez9jGp4FMZ0nztky+hi 44 | Q2zHSyYzTzYzPc3775H7LMnH39b64DO0MfREJb+3Wbpj3hCBNn8zdSE807Tx5XeX 45 | dQbIynXafFU5CR3AJbCuzs8RjdMXpeVlvad0OOStbjFDNpGmeKXgbD0A43ddCUj6 46 | GKAlWLX7J7pN2OKkQmewZWgRmNEXIAaorUR3GXClORMzeUTYopQLazHIkXWH1VWO 47 | 4Yqqew7K3LkCF2eMA8wj02fjyMtb+8I+xoj4wEHROSDQGGf90fcHgfoPWZgvjgvp 48 | s2egpO19X8zMTVvZW8VoNDeiaeMSlLLEVBhMHYwiB3MpQYXfnkfLjhKmhvil86Jg 49 | t0F/CTzTRdd8JjtwOsIC6TtYcZPsZGyDFBvx/K8lOcN2t5p5VZMnFQm0aCV2d6r0 50 | 0nD5HexDPBMq3Lvy3vm5zmwJxM072SU69k5qEHCKERYyfyuNrhA4VxOm9UqnDcDI 51 | lwpetFBEZWJpYW4gU2VjdXJpdHkgQXJjaGl2ZSBBdXRvbWF0aWMgU2lnbmluZyBL 52 | ZXkgKDkvc3RyZXRjaCkgPGZ0cG1hc3RlckBkZWJpYW4ub3JnPokCVAQTAQoAPhYh 53 | BG7W9ctfpvsvRgroju2g0jiK4iupBQJZIzDxAhsDBQkPCZwABQsJCAcDBRUKCQgL 54 | BRYCAwEAAh4BAheAAAoJEO2g0jiK4iupWUEP/Az929HCCGTGBZajoEfN2FNqOWZ4 55 | pdKN6a4pPwnY2Cg7FeKqsXcYG2EM+Iuea2MK2IIlA2n5LYr6VIjLWfQJkkRoeZwo 56 | QNTcb7fE5nBF/mUhqYovm4mSbDgmL/c5EVk7iqL7YDUE+fYWyIbGipIPKVHvST1b 57 | 18GHq1aEoURPHgxnqsWapuKlZFNhGQc6ncq5O0bdUthH5jdwu8/qX0iLbtK0MeLR 58 | kQK4Vhw3WkMKRlsTTgo+lw0ubxj6UW8osaDGMY0HxFDPrlQLzdW9vcxLZVbNG0FV 59 | woPymNwnrlC++GMbS8dtv8JlsaRJhsL7fbYX6IQUAC8hqBd7Rpcu9Mx+87z6iUFv 60 | cm0lpiN2fp3OJ7+oacF7yjCfJrDJTXHVn923GLODewx/3g6bfBkRZf3bstg9gl9g 61 | 5mZkCyczYzjnQdRmQWGW//DyhGJ4GyatYfbYU+05uWlZTpoWuvgUpepMV4v4A70a 62 | vjDh+RGiGqJ8rZ6uC6hxTTvMWEQ6p+5IAThMs2Hx9m34o6KXXXl2qHsY7pyGPcPi 63 | qTjoJYOQ1Sx5n2pe6eLAfi1yHhYQ0FNHak/WE3SQ4Og5lT1ZPrv9MFO4KxBhEbSP 64 | B5m1dWn8AkIxsZVsXgBxP9fdsj9KHbkLPzrOZDS1exlWjh4iFFrzzFPTTlCXfaR0 65 | 1e8ossCe5eU4PmsOiQJVBBABCAA/FiEE+/q9tUG13JVb2bpu2xbPW7ElJcQFAlkk 66 | n1shGmh0dHA6Ly9ncGcuZ2FubmVmZi5kZS9wb2xpY3kudHh0AAoJENsWz1uxJSXE 67 | Ol0QAMFZ++JM3Fus0Q95SSZiNlR23653mvIvRJZxc99rOKig78XHsOZDvpRZ7rmm 68 | NdGbo5Wcx/G0gywwqFLKfP7AKEt7/0t4QySQM0sAu/NgLOcJX+lu5rIwUfVu9z1z 69 | 9i7wb+bb+u+QRCJvHyhSFrT0p3PTI/nWIaTdoLpPQSaLzvChW4At2rrNah0IVa0X 70 | L9J5oid3QtdreYbL1jzf4QK5LgfKqnorzf5ugjaeJdM+utsEav3w9jYwSHAegJ4t 71 | YEtM5SS/mCt98ZGbQeKAhMMjqrFS9Oa0wGctayoSdFT9cIeeZW132U3uSnxxPwCJ 72 | nnY5JY3k9AvL6nWaWfTDWZbp4VZSHUXxOyd3xXoYLcTFAKKFNAedDwpcyebPPknr 73 | CrJ2BrHWn4LQak2X0Mo8H3bBMsYMjXtGQK3kNQE0E+tO7ESMQ0H7k30pBPbwyNmJ 74 | 2fiGS/Fc5M432IYQ3m/bid0i0qalrGRNtuZ1+UvyZ8gDfLEgvZ2G+woxX7u2WKYi 75 | pVONFOp62YjLqfORtTuJf8RmyVlADCWr1YQupFyE61c37UYJWZEDA+USO3iN90jo 76 | VZUiPROkeEI1ugVHUZSzux+BzB/lDRlvWEII7W2PzdK0v8WfWzij3mVb+P5GgQYE 77 | cGZPfw//WX968FfTQ4Slq0brvSIJOoh1bdKnqGM/iGgE8laQiQIzBBABCgAdFiEE 78 | gOl28UpQikjpyj/pvDciUsoc+WQFAlkkgbQACgkQvDciUsoc+WQRvA//QhtAhQME 79 | OvgkgKJwFTnfZQhyRld0mcAzwSMHhtrG+20VMbcVfzhniE4NNP544krqcQ7pwXNJ 80 | 9Sf3R2qS4Si2gxy0qNtgg5QUp4pp49Y/SVFZbjTFfEcxHhl6pizL+3D94x6WoE7I 81 | aHUlgLkxWn1QAsxrxmll3pLktdgPqY//jEUDfK2CIs/t+Xe6UiVYUw6l+K8Og3Hn 82 | 898i6vlfKYlB3wtaHdzASh3LQ61Ir1slqi95MyuHleVEynLCQgcycTcTy6tNC43P 83 | aSunlQ54Q1ID95EI75pbv2JR6+54LO+8pCRoy1CY7WmqBChkIv+brGab5tApWLDw 84 | fj5pEFoePVFAzz+sZ3/SzbdYQfMp8UE8B8ac72MRnnGhyH7iJztezc5myqRRDriu 85 | /h1+z4Hw0MO5zFMla1f4XKegqPKYGP5/QwOZlIUV2K2ntzCGDebqpMBispgsDS9E 86 | CWNUzrqfUfTLPIQWqt7FOgeZ56O9fMvKTZq7GQpeHg14ZsBmLq5mypomfB7ky1J1 87 | CLurGScaOUnABZoZ2zSKlIx4ZLT7YB8m4h3aRPnMOWfnTW/evERG7hWriFRDEBy2 88 | T+u7ekyAM9cJDWuhmLegHvJD0FgzhQCRICIXx1CsmGTgJ2QHUKExE4hHwghKSUWr 89 | XZTUbKepuMGXQqyftljQVXoiGUK4O2W0yviJAhwEEAEIAAYFAlkl7NUACgkQEbTl 90 | /xWw/YK8cQ//QVg0ICy5rRlo7ZPHnOFBkMc/F9tNFhvsdogSalJZEVglb7niZRdn 91 | 8MVmO8O8/SaTG2JTw1n1cVRvS7XQkKWY58PXmxXYWxLkv41xJIiRLzE+DuAV7J99 92 | b71N/gRSIkFdTQR1y6qHGt/uLJ2lSZ9gptw/Slx+iE11sxBuv/au1qK6UHKOFkUz 93 | RGEiRMXtTKN6EZeUYM5IzFN/G8j4rIdCgYi5Yo0q5BZ6JrmlPJ7fnW88gQ1jMuuB 94 | pjSMBC4+6MliGsGYTWYERGk5UUdXGIt5DtUy4FdSMSobEOYMPJxX4faHSgInE/Q6 95 | aYDNYzkXCzda/c09aa3oShHWmiQX2cxAfPXl1IficerXXrsHJf769pB8Z3uDbXet 96 | JbI0b/YHLef6TKpxNKqVOxieT5YVFaxBkBsnv8VCmxBleOUa645REhknugoFN1Nd 97 | 75bMRxyedXsFCoJsPLDhQv1pbtyXnhUu6+GescBkplqmYIw2o+fAkdrupAPMSfOq 98 | DSw2gTJL6stXqSNn8g8afGzQ1TcVRhplqYa1RJtSPKvA4FPNNbS1QTIcR6olsA25 99 | iYb/7tNk4EVtQePvUSWfBW43ZLOggMB8CjqxUSSkOjYgecrU+pY8G8mAbzqOexRL 100 | gG4sLSoL0l7NpSxzpnd7+ARJz+qz2hCr+JPJseld6D7MJpyz6Pnz43+JAhwEEwEK 101 | AAYFAlkmhB0ACgkQdjjQRCuQ0BBFww//U2aBxtpskWSW436P4FxoAG0K6SKPiiJh 102 | ZFx9Ox813fXrWKJ5qPV39hT+mW4VhYYepvUgtAw110H7zegHvjfjhYr2nAh4CDsD 103 | uCOqwlOp6RpqPI1zUoUoOVeAaExMg7nPZeQAaqXNtQ1ohGgxBFdeBYRA4ICx2EGy 104 | y+YR9mABZZbhfh+AeDljQlw5bqpxaWNkkiQsysB4SYDdcVeCZ3FMdiCF4MU5ceHG 105 | kFuWQIRLgYL1zNek1WDXRcuyJM5a6YE0sOZ6guNKJ+ZVV/yzFRN4JjTFbWunYqgM 106 | pisNM5i0xce/VMe/y2QqnyTenO6mYRv0qX2Yr0Dag/W+CxqAmvS+17eD3SZzSXqr 107 | DMDZQkdvquyaYjVcqg/ixHRtRTrIJ1gGPjfl9YiOTlGgm3/HiVzgIZPcDdU2KZP1 108 | iHlUhPQnfEHT6sHTrRVdxLWgxQuWgX1CsuPYgSe4Ia9BBQL7xFwCkonx/80fsHeN 109 | 1ryMX6wBZt1dAJkc1pmS5ahzgwlIKWU9fnufsXn7tAQWG9BHtjrXsCBdlKIKNWKW 110 | Un3Vl1oDVaWMMZ9Gu0bkpDv/wA4vHQehtrKe0XfSnU0W7QQifCHVRV5kuLSvVD3I 111 | nMWxZV5uAQ239VCzMrKLyErSyKybMuDos0csSrBvOtCIVLoDvVS6u+uoyc+DsQOm 112 | xawn3wpaElGJAhwEEwEKAAYFAlkmhTMACgkQnW2Pa8hXyQaYcg//d3vMCp9ZXgIk 113 | t1GQTVYQIhDqpR3Bq4RGHswZS3HEVShv+yoFlbciUccp0Q3FFmxwP6HsGMlhVHK9 114 | UG42maJ5yfsewoMnumWFLR2LeimuCxYCzN6C486qPwK5aNpOzhs3nDWaqDq6J1wG 115 | yQWnDGGilDrgSe/NptPTBCAERUwnmbuL4Y3WdrZM0Shgs8JDoMHFOpQiXVGJ5qoh 116 | uQnK1O6fjlqsRAUIu+amfSl21s67OfBFlL20AoITBGLvKBrZvj6NhHHE34Wa6WCp 117 | AQ7QdKprU2CM6VigGdPS2WSEZKY4JdpqwQ1tJV1xDOneraNajDuWMOFNmTfR2Gcw 118 | Xkjf8W3QdsYC6wAJHpjfcdXG/tCfzqJPUmX/2lJrQtPhBNnT2Vle5KD5qrZa8fSz 119 | O3tFtDialAWQju3KbbMpPo4gsYl/bU59kOaS4zUO0l1NGc+3QjZQ+VXsp272Tjhv 120 | qGYAtg1xag/9YvLRWFqyB+KxZ6sjjDzoTZHshY97GWO/0mIawWmhJfTgn8AHZ4Oj 121 | L+IxRrhwiLSk9+RQNNt1JqVgl0A8vPy3lWvDZaw7YmKRGmygvVNncWqWIEltOJ7I 122 | yLvQtd/t2F2ABFehCmz8tF6VFuYdnIW0Vqo9M2aTzVysRqR63dUVtD5Fyb6AIpxW 123 | dcO0xTuCLdcJ/ANzjMRbXkzcGfEAjo25Ag0EWSMw8QEQAOopVbKlcD6pgGqxEbtA 124 | Usl6S70J4YzhycNnG8GYibKNKy30P3a5llk3mCb39s+VbdDN2sGhLniv9uXZnf7q 125 | ZfEPDSNYq+6aeD9XYP4pFuqAE+/5AlYJ7rjNQOteSzNuit61kxSIOKuEQj+Vw04H 126 | VNUbq2YJhtrkX3U774SKBMJMg3GuT7pipNvXBd8w9k7/cfBrEhI6mToFDfz284KK 127 | 8jY7AlgntY98GGXIBgfFLqnWD3YGxq63mzy1yxTTKfzDV0TCdXJDNKjeBe03uki2 128 | 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 158 | TLeFkkM3UnAOQN0p 159 | =1HGQ 160 | -----END PGP PUBLIC KEY BLOCK----- 161 | -------------------------------------------------------------------------------- /debian-packaging/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Debian packaging primitives. 6 | 7 | This crate defines pure Rust implementations of Debian packaging primitives. Debian packaging 8 | (frequently interacted with by tools like `apt` and `apt-get`) provides the basis for 9 | packaging on Debian-flavored Linux distributions like Debian and Ubuntu. 10 | 11 | # Goals 12 | 13 | ## Compliance and Compatibility 14 | 15 | We want this crate to be as-compliant and as-compatible as possible with in-the-wild Debian 16 | packaging deployments so it can be used as a basis to implementing tools which consume and 17 | produce entities that are compatible with the official Debian packaging implementations. 18 | 19 | This crate could be considered an attempt to reimplement aspects of 20 | [apt](https://salsa.debian.org/apt-team/apt) in pure Rust. (The `apt` repository defines 21 | command line tools like `apt-get` as well as libraries like `libapt-pkg` and `libapt-inst`. 22 | This crate is more focused on providing the library-level interfaces. However, a goal is 23 | to have as much code be usable as a library so the surface area of any tools is minimal.) 24 | 25 | ## Determinism and Reproducibility 26 | 27 | To help combat the rise in software supply chain attacks and to make debugging and testing 28 | easier, a goal of this crate is to be as deterministic and reproducible as possible. 29 | 30 | Given the same source code / version of this crate, operations like creating a `.deb` file 31 | or building a repository from indexed `.deb` files should be as byte-for-byte identical as 32 | reasonably achievable. 33 | 34 | ## Performance 35 | 36 | We strive for highly optimal implementations of packaging primitives wherever possible. 37 | 38 | We want to facilitate intensive operations (like reading all packages in a Debian 39 | repository) or publishing to a repository to scale out to as many CPU cores as possible. 40 | 41 | Read operations like parsing control files or version strings should be able to use 42 | 0-copy to avoid excessive memory allocations and copying. 43 | 44 | # A Tour of Functionality 45 | 46 | A `.deb` file defines a Debian package. Readers and writers of `.deb` files exist in the 47 | [deb] module. To read the contents of a `.deb` defining a binary package, use 48 | [deb::reader::BinaryPackageReader]. To create new `.deb` files, use [deb::builder::DebBuilder]. 49 | 50 | A common primitive within Debian packaging is *control files*. These consist of *paragraphs* 51 | of key-value metadata. Low-level control file primitives are defined in the [control] module. 52 | [control::ControlParagraph] defines a paragraph, which consists of [control::ControlField]. 53 | [control::ControlFile] provides an interface for a *control file*, which consists of multiple 54 | paragraphs. [control::ControlParagraphReader] implements a streaming reader of control files 55 | and [control::ControlParagraphAsyncReader] implements an asynchronous streaming reader. 56 | 57 | There are different flavors of *control files* within Debian packaging. 58 | [binary_package_control::BinaryPackageControlFile] defines a *control file* for a binary package. 59 | This type provides helper functions for resolving common fields on binary control files. 60 | [debian_source_control::DebianSourceControlFile] defines a *control file* for a source package, 61 | as expressed in a `.dsc` file. 62 | 63 | There is a meta language for expressing dependencies between Debian packages. The 64 | [dependency] module defines types for parsing and writing this language. e.g. 65 | [dependency::DependencyList] represents a parsed list of dependencies like 66 | `libc6 (>= 2.4), libx11-6`. [dependency::PackageDependencyFields] represents a collection 67 | of control fields that define relationships between packages. 68 | 69 | The [package_version] module implements Debian package version string parsing, 70 | serialization, and comparison. [package_version::PackageVersion] is the main type used for this. 71 | 72 | The [dependency_resolution] module implements functionality related to resolving dependencies. 73 | e.g. [dependency_resolution::DependencyResolver] can be used to index known binary packages 74 | and find direct and transitive dependencies. This could be used as the basis for a package 75 | manager or other tool wishing to walk the dependency tree for a given package. 76 | 77 | The [repository] module provides functionality related to Debian repositories, which are 78 | publications of Debian packages and metadata. The [repository::RepositoryRootReader] trait 79 | provides an interface for reading the root directory of a repository and 80 | [repository::ReleaseReader] provides an interface for reading content from a parsed 81 | `[In]Release` file. The [repository::RepositoryWriter] trait abstracts I/O for writing 82 | to a repository. Repository interaction involves many support primitives. 83 | [repository::release::ReleaseFile] represents an `[In]Release` file. Support for verifying 84 | PGP signatures is provided. [repository::contents::ContentsFile] represents a `Contents` 85 | file. 86 | 87 | Concrete implementations of repository interaction exist. [repository::http::HttpRepositoryClient] 88 | enables reading from an HTTP-hosted repository (e.g. `http://archive.canonical.com/ubuntu`). 89 | [repository::filesystem::FilesystemRepositoryWriter] enables writing repositories to a local 90 | filesystem. 91 | 92 | The [repository::builder] module contains functionality for creating and publishing 93 | Debian repositories. [repository::builder::RepositoryBuilder] is the main type for 94 | publishing Debian repositories. 95 | 96 | The [repository::copier] module contains functionality for copying Debian repositories. 97 | [repository::copier::RepositoryCopier] is the main type for copying Debian repositories. 98 | 99 | The [signing_key] module provides functionality related to PGP signing. 100 | [signing_key::DistroSigningKey] defines PGP public keys for well-known signing keys used by 101 | popular Linux distributions. [signing_key::signing_secret_key_params_builder()] and 102 | [signing_key::create_self_signed_key()] enable easily creating signing keys for Debian 103 | repositories. 104 | 105 | Various other modules provide miscellaneous functionality. [io] defines I/O helpers, including 106 | stream adapters for validating content digests on read and computing content digests on write. 107 | 108 | # Crate Features 109 | 110 | The optional and enabled-by-default `http` feature enables HTTP client support for interacting 111 | with Debian repositories via HTTP. 112 | */ 113 | 114 | pub mod binary_package_control; 115 | pub mod binary_package_list; 116 | pub mod changelog; 117 | pub mod control; 118 | pub mod deb; 119 | pub mod debian_source_control; 120 | pub mod debian_source_package_list; 121 | pub mod dependency; 122 | pub mod dependency_resolution; 123 | pub mod error; 124 | pub mod io; 125 | pub mod package_version; 126 | pub mod repository; 127 | pub mod signing_key; 128 | pub mod source_package_control; 129 | -------------------------------------------------------------------------------- /debian-packaging/src/repository/contents.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! `Contents` index file handling. */ 6 | 7 | use { 8 | crate::error::Result, 9 | futures::{AsyncBufRead, AsyncBufReadExt}, 10 | pin_project::pin_project, 11 | std::{ 12 | collections::{BTreeMap, BTreeSet}, 13 | io::{BufRead, Write}, 14 | }, 15 | }; 16 | 17 | /// Represents a `Contents` file. 18 | /// 19 | /// A `Contents` file maps paths to packages. It facilitates lookups of which paths 20 | /// are in which packages. 21 | /// 22 | /// Internally, paths are stored as [String] because bulk operations against paths 23 | /// can be expensive due to more expensive comparison/equality checks. 24 | #[derive(Clone, Debug, Default)] 25 | pub struct ContentsFile { 26 | /// Mapping of paths to packages they occur in. 27 | paths: BTreeMap>, 28 | /// Mapping of package names to paths they contain. 29 | packages: BTreeMap>, 30 | } 31 | 32 | impl ContentsFile { 33 | fn parse_and_add_line(&mut self, line: &str) -> Result<()> { 34 | // According to https://wiki.debian.org/DebianRepository/Format#A.22Contents.22_indices 35 | // `Contents` files begin with freeform text then have a table of path to package list. 36 | // Invalid lines are ignored. 37 | 38 | let words = line.split_ascii_whitespace().collect::>(); 39 | 40 | if words.len() != 2 { 41 | return Ok(()); 42 | } 43 | 44 | let path = words[0]; 45 | let packages = words[1]; 46 | 47 | for package in packages.split(',') { 48 | self.paths 49 | .entry(path.to_string()) 50 | .or_default() 51 | .insert(package.to_string()); 52 | self.packages 53 | .entry(package.to_string()) 54 | .or_default() 55 | .insert(path.to_string()); 56 | } 57 | 58 | Ok(()) 59 | } 60 | 61 | /// Register a path as belonging to a package. 62 | pub fn add_package_path(&mut self, path: String, package: String) { 63 | self.paths 64 | .entry(path.clone()) 65 | .or_default() 66 | .insert(package.clone()); 67 | self.packages.entry(package).or_default().insert(path); 68 | } 69 | 70 | /// Obtain an iterator of packages having the specified path. 71 | pub fn packages_with_path(&self, path: &str) -> Box + '_> { 72 | if let Some(packages) = self.paths.get(path) { 73 | Box::new(packages.iter().map(|x| x.as_str())) 74 | } else { 75 | Box::new(std::iter::empty()) 76 | } 77 | } 78 | 79 | /// Obtain an iterator of paths in a given package. 80 | pub fn package_paths(&self, package: &str) -> Box + '_> { 81 | if let Some(paths) = self.packages.get(package) { 82 | Box::new(paths.iter().map(|x| x.as_str())) 83 | } else { 84 | Box::new(std::iter::empty()) 85 | } 86 | } 87 | 88 | /// Emit lines constituting this file. 89 | pub fn as_lines(&self) -> impl Iterator + '_ { 90 | self.paths.iter().map(|(path, packages)| { 91 | // BTreeSet doesn't have a .join(). So we need to build a collection that does. 92 | let packages = packages.iter().map(|s| s.as_str()).collect::>(); 93 | 94 | format!("{} {}\n", path, packages.join(",'")) 95 | }) 96 | } 97 | 98 | /// Write the content of this file to a writer. 99 | /// 100 | /// Returns the total number of bytes written. 101 | pub fn write_to(&self, writer: &mut impl Write) -> Result { 102 | let mut bytes_count = 0; 103 | 104 | for line in self.as_lines() { 105 | writer.write_all(line.as_bytes())?; 106 | bytes_count += line.as_bytes().len(); 107 | } 108 | 109 | Ok(bytes_count) 110 | } 111 | } 112 | 113 | #[derive(Clone, Debug)] 114 | pub struct ContentsFileReader { 115 | reader: R, 116 | contents: ContentsFile, 117 | } 118 | 119 | impl ContentsFileReader { 120 | /// Create a new instance bound to a reader. 121 | pub fn new(reader: R) -> Self { 122 | Self { 123 | reader, 124 | contents: ContentsFile::default(), 125 | } 126 | } 127 | 128 | /// Consumes the instance, returning the original reader. 129 | pub fn into_inner(self) -> R { 130 | self.reader 131 | } 132 | 133 | /// Parse the entirety of the source reader. 134 | pub fn read_all(&mut self) -> Result { 135 | let mut bytes_read = 0; 136 | 137 | while let Ok(read_size) = self.read_line() { 138 | if read_size == 0 { 139 | break; 140 | } 141 | 142 | bytes_read += read_size; 143 | } 144 | 145 | Ok(bytes_read) 146 | } 147 | 148 | /// Read and parse a single line from the reader. 149 | pub fn read_line(&mut self) -> Result { 150 | let mut line = String::new(); 151 | let read_size = self.reader.read_line(&mut line)?; 152 | 153 | if read_size != 0 { 154 | self.contents.parse_and_add_line(&line)?; 155 | } 156 | 157 | Ok(read_size) 158 | } 159 | 160 | /// Consume the instance and return the inner [ContentsFile] and the reader. 161 | pub fn consume(self) -> (ContentsFile, R) { 162 | (self.contents, self.reader) 163 | } 164 | } 165 | 166 | #[pin_project] 167 | pub struct ContentsFileAsyncReader { 168 | #[pin] 169 | reader: R, 170 | contents: ContentsFile, 171 | } 172 | 173 | impl ContentsFileAsyncReader 174 | where 175 | R: AsyncBufRead + Unpin, 176 | { 177 | /// Create a new instance bound to a reader. 178 | pub fn new(reader: R) -> Self { 179 | Self { 180 | reader, 181 | contents: ContentsFile::default(), 182 | } 183 | } 184 | 185 | /// Consumes self, returning the inner reader. 186 | pub fn into_inner(self) -> R { 187 | self.reader 188 | } 189 | 190 | /// Parse the entirety of the source reader. 191 | pub async fn read_all(&mut self) -> Result { 192 | let mut bytes_read = 0; 193 | 194 | while let Ok(read_size) = self.read_line().await { 195 | if read_size == 0 { 196 | break; 197 | } 198 | 199 | bytes_read += read_size; 200 | } 201 | 202 | Ok(bytes_read) 203 | } 204 | 205 | /// Read and parse a single line from the reader. 206 | pub async fn read_line(&mut self) -> Result { 207 | let mut line = String::new(); 208 | let read_size = self.reader.read_line(&mut line).await?; 209 | 210 | if read_size != 0 { 211 | self.contents.parse_and_add_line(&line)?; 212 | } 213 | 214 | Ok(read_size) 215 | } 216 | 217 | /// Consume the instance and return the inner [ContentsFile] and source reader. 218 | pub fn consume(self) -> (ContentsFile, R) { 219 | (self.contents, self.reader) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /debian-packaging/src/repository/filesystem.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Filesystem based Debian repositories. */ 6 | 7 | use { 8 | crate::{ 9 | error::{DebianError, Result}, 10 | io::{Compression, ContentDigest, DataResolver, DigestingReader}, 11 | repository::{ 12 | release::ReleaseFile, ReleaseReader, RepositoryPathVerification, 13 | RepositoryPathVerificationState, RepositoryRootReader, RepositoryWrite, 14 | RepositoryWriter, 15 | }, 16 | }, 17 | async_trait::async_trait, 18 | futures::{io::BufReader, AsyncRead, AsyncReadExt}, 19 | std::{ 20 | borrow::Cow, 21 | path::{Path, PathBuf}, 22 | pin::Pin, 23 | }, 24 | url::Url, 25 | }; 26 | 27 | /// A readable interface to a Debian repository backed by a filesystem. 28 | #[derive(Clone, Debug)] 29 | pub struct FilesystemRepositoryReader { 30 | root_dir: PathBuf, 31 | } 32 | 33 | impl FilesystemRepositoryReader { 34 | /// Construct a new instance, bound to the root directory specified. 35 | /// 36 | /// No validation of the passed path is performed. 37 | pub fn new(path: impl AsRef) -> Self { 38 | Self { 39 | root_dir: path.as_ref().to_path_buf(), 40 | } 41 | } 42 | } 43 | 44 | #[async_trait] 45 | impl DataResolver for FilesystemRepositoryReader { 46 | async fn get_path(&self, path: &str) -> Result>> { 47 | let path = self.root_dir.join(path); 48 | 49 | let f = std::fs::File::open(&path) 50 | .map_err(|e| DebianError::RepositoryIoPath(format!("{}", path.display()), e))?; 51 | 52 | Ok(Box::pin(futures::io::AllowStdIo::new(f))) 53 | } 54 | } 55 | 56 | #[async_trait] 57 | impl RepositoryRootReader for FilesystemRepositoryReader { 58 | fn url(&self) -> Result { 59 | Url::from_file_path(&self.root_dir) 60 | .map_err(|_| DebianError::Other("error converting filesystem path to URL".to_string())) 61 | } 62 | 63 | async fn release_reader_with_distribution_path( 64 | &self, 65 | path: &str, 66 | ) -> Result> { 67 | let distribution_path = path.trim_matches('/').to_string(); 68 | let inrelease_path = format!("{}/InRelease", distribution_path); 69 | let release_path = format!("{}/Release", distribution_path); 70 | let distribution_dir = self.root_dir.join(&distribution_path); 71 | 72 | let release = self 73 | .fetch_inrelease_or_release(&inrelease_path, &release_path) 74 | .await?; 75 | 76 | let fetch_compression = Compression::default_preferred_order() 77 | .next() 78 | .expect("iterator should not be empty"); 79 | 80 | Ok(Box::new(FilesystemReleaseClient { 81 | distribution_dir, 82 | relative_path: distribution_path, 83 | release, 84 | fetch_compression, 85 | })) 86 | } 87 | } 88 | 89 | pub struct FilesystemReleaseClient { 90 | distribution_dir: PathBuf, 91 | relative_path: String, 92 | release: ReleaseFile<'static>, 93 | fetch_compression: Compression, 94 | } 95 | 96 | #[async_trait] 97 | impl DataResolver for FilesystemReleaseClient { 98 | async fn get_path(&self, path: &str) -> Result>> { 99 | let path = self.distribution_dir.join(path); 100 | 101 | let f = std::fs::File::open(&path) 102 | .map_err(|e| DebianError::RepositoryIoPath(format!("{}", path.display()), e))?; 103 | 104 | Ok(Box::pin(BufReader::new(futures::io::AllowStdIo::new(f)))) 105 | } 106 | } 107 | 108 | #[async_trait] 109 | impl ReleaseReader for FilesystemReleaseClient { 110 | fn url(&self) -> Result { 111 | Url::from_file_path(&self.distribution_dir) 112 | .map_err(|_| DebianError::Other("error converting filesystem path to URL".to_string())) 113 | } 114 | 115 | fn root_relative_path(&self) -> &str { 116 | &self.relative_path 117 | } 118 | 119 | fn release_file(&self) -> &ReleaseFile<'static> { 120 | &self.release 121 | } 122 | 123 | fn preferred_compression(&self) -> Compression { 124 | self.fetch_compression 125 | } 126 | 127 | fn set_preferred_compression(&mut self, compression: Compression) { 128 | self.fetch_compression = compression; 129 | } 130 | } 131 | 132 | /// A writable Debian repository backed by a filesystem. 133 | pub struct FilesystemRepositoryWriter { 134 | root_dir: PathBuf, 135 | } 136 | 137 | impl FilesystemRepositoryWriter { 138 | /// Construct a new instance, bound to the root directory specified. 139 | /// 140 | /// No validation of the passed path is performed. The directory does not need to exist. 141 | pub fn new(path: impl AsRef) -> Self { 142 | Self { 143 | root_dir: path.as_ref().to_path_buf(), 144 | } 145 | } 146 | } 147 | 148 | #[async_trait] 149 | impl RepositoryWriter for FilesystemRepositoryWriter { 150 | async fn verify_path<'path>( 151 | &self, 152 | path: &'path str, 153 | expected_content: Option<(u64, ContentDigest)>, 154 | ) -> Result> { 155 | let dest_path = self.root_dir.join(path); 156 | 157 | let metadata = match async_std::fs::metadata(&dest_path).await { 158 | Ok(res) => res, 159 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => { 160 | return Ok(RepositoryPathVerification { 161 | path, 162 | state: RepositoryPathVerificationState::Missing, 163 | }); 164 | } 165 | Err(e) => return Err(DebianError::RepositoryIoPath(path.to_string(), e)), 166 | }; 167 | 168 | if metadata.is_file() { 169 | if let Some((expected_size, expected_digest)) = expected_content { 170 | if metadata.len() != expected_size { 171 | Ok(RepositoryPathVerification { 172 | path, 173 | state: RepositoryPathVerificationState::ExistsIntegrityMismatch, 174 | }) 175 | } else { 176 | let f = async_std::fs::File::open(&dest_path) 177 | .await 178 | .map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?; 179 | 180 | let mut remaining = expected_size; 181 | let mut reader = DigestingReader::new(f); 182 | let mut buf = [0u8; 16384]; 183 | 184 | loop { 185 | let size = reader 186 | .read(&mut buf[..]) 187 | .await 188 | .map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))? 189 | as u64; 190 | 191 | if size >= remaining || size == 0 { 192 | break; 193 | } 194 | 195 | remaining -= size; 196 | } 197 | 198 | let digest = reader.finish().1; 199 | 200 | Ok(RepositoryPathVerification { 201 | path, 202 | state: if digest.matches_digest(&expected_digest) { 203 | RepositoryPathVerificationState::ExistsIntegrityVerified 204 | } else { 205 | RepositoryPathVerificationState::ExistsIntegrityMismatch 206 | }, 207 | }) 208 | } 209 | } else { 210 | Ok(RepositoryPathVerification { 211 | path, 212 | state: RepositoryPathVerificationState::ExistsNoIntegrityCheck, 213 | }) 214 | } 215 | } else { 216 | Ok(RepositoryPathVerification { 217 | path, 218 | state: RepositoryPathVerificationState::Missing, 219 | }) 220 | } 221 | } 222 | 223 | async fn write_path<'path, 'reader>( 224 | &self, 225 | path: Cow<'path, str>, 226 | reader: Pin>, 227 | ) -> Result> { 228 | let dest_path = self.root_dir.join(path.as_ref()); 229 | 230 | if let Some(parent) = dest_path.parent() { 231 | std::fs::create_dir_all(parent) 232 | .map_err(|e| DebianError::RepositoryIoPath(format!("{}", parent.display()), e))?; 233 | } 234 | 235 | let fh = std::fs::File::create(&dest_path) 236 | .map_err(|e| DebianError::RepositoryIoPath(format!("{}", dest_path.display()), e))?; 237 | 238 | let mut writer = futures::io::AllowStdIo::new(fh); 239 | 240 | let bytes_written = futures::io::copy(reader, &mut writer) 241 | .await 242 | .map_err(|e| DebianError::RepositoryIoPath(format!("{}", dest_path.display()), e))?; 243 | 244 | Ok(RepositoryWrite { 245 | path, 246 | bytes_written, 247 | }) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /debian-packaging/src/repository/proxy_writer.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! A repository writer that doesn't actually write. */ 6 | 7 | use { 8 | crate::{ 9 | error::{DebianError, Result}, 10 | io::ContentDigest, 11 | repository::{ 12 | RepositoryPathVerification, RepositoryPathVerificationState, RepositoryWrite, 13 | RepositoryWriter, 14 | }, 15 | }, 16 | async_trait::async_trait, 17 | futures::AsyncRead, 18 | std::{borrow::Cow, pin::Pin, sync::Mutex}, 19 | }; 20 | 21 | /// How [RepositoryWriter::verify_path()] should behave for [ProxyWriter] instances. 22 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 23 | pub enum ProxyVerifyBehavior { 24 | /// Always call the inner [RepositoryWriter::verify_path()]. 25 | Proxy, 26 | AlwaysExistsNoIntegrityCheck, 27 | AlwaysExistsIntegrityVerified, 28 | AlwaysExistsIntegrityMismatch, 29 | AlwaysMissing, 30 | } 31 | 32 | /// A [RepositoryWriter] that proxies operations to an inner writer. 33 | /// 34 | /// The behavior of each I/O operation can be configured to facilitate customizing 35 | /// behavior. It also records operations performed. This makes this type useful as part 36 | /// of testing and simulating what would occur. 37 | pub struct ProxyWriter { 38 | inner: W, 39 | verify_behavior: ProxyVerifyBehavior, 40 | /// List of paths that were written. 41 | path_writes: Mutex>, 42 | } 43 | 44 | impl ProxyWriter { 45 | /// Construct a new instance by wrapping an existing writer. 46 | /// 47 | /// The default behavior for path verification is to call the inner writer. 48 | pub fn new(writer: W) -> Self { 49 | Self { 50 | inner: writer, 51 | verify_behavior: ProxyVerifyBehavior::Proxy, 52 | path_writes: Mutex::new(vec![]), 53 | } 54 | } 55 | 56 | /// Return the inner writer, consuming self. 57 | pub fn into_inner(self) -> W { 58 | self.inner 59 | } 60 | 61 | /// Set the behavior for [RepositoryWriter::verify_path()]. 62 | pub fn set_verify_behavior(&mut self, behavior: ProxyVerifyBehavior) { 63 | self.verify_behavior = behavior; 64 | } 65 | } 66 | 67 | #[async_trait] 68 | impl RepositoryWriter for ProxyWriter { 69 | async fn verify_path<'path>( 70 | &self, 71 | path: &'path str, 72 | expected_content: Option<(u64, ContentDigest)>, 73 | ) -> Result> { 74 | match self.verify_behavior { 75 | ProxyVerifyBehavior::Proxy => self.inner.verify_path(path, expected_content).await, 76 | ProxyVerifyBehavior::AlwaysExistsIntegrityVerified => Ok(RepositoryPathVerification { 77 | path, 78 | state: RepositoryPathVerificationState::ExistsIntegrityVerified, 79 | }), 80 | ProxyVerifyBehavior::AlwaysExistsNoIntegrityCheck => Ok(RepositoryPathVerification { 81 | path, 82 | state: RepositoryPathVerificationState::ExistsNoIntegrityCheck, 83 | }), 84 | ProxyVerifyBehavior::AlwaysExistsIntegrityMismatch => Ok(RepositoryPathVerification { 85 | path, 86 | state: RepositoryPathVerificationState::ExistsIntegrityMismatch, 87 | }), 88 | ProxyVerifyBehavior::AlwaysMissing => Ok(RepositoryPathVerification { 89 | path, 90 | state: RepositoryPathVerificationState::Missing, 91 | }), 92 | } 93 | } 94 | 95 | async fn write_path<'path, 'reader>( 96 | &self, 97 | path: Cow<'path, str>, 98 | reader: Pin>, 99 | ) -> Result> { 100 | let res = self.inner.write_path(path.clone(), reader).await?; 101 | 102 | self.path_writes 103 | .lock() 104 | .map_err(|_| { 105 | DebianError::RepositoryIoPath( 106 | path.to_string(), 107 | std::io::Error::new( 108 | std::io::ErrorKind::Other, 109 | "error acquiring write paths mutex", 110 | ), 111 | ) 112 | })? 113 | .push(path.to_string()); 114 | 115 | Ok(res) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /debian-packaging/src/repository/s3.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use { 6 | crate::{ 7 | error::{DebianError, Result}, 8 | io::{ContentDigest, MultiDigester}, 9 | repository::{ 10 | RepositoryPathVerification, RepositoryPathVerificationState, RepositoryWrite, 11 | RepositoryWriter, 12 | }, 13 | }, 14 | async_trait::async_trait, 15 | futures::{AsyncRead, AsyncReadExt as FuturesAsyncReadExt}, 16 | rusoto_core::{ByteStream, Client, Region, RusotoError}, 17 | rusoto_s3::{ 18 | GetBucketLocationRequest, GetObjectError, GetObjectRequest, HeadObjectError, 19 | HeadObjectRequest, PutObjectRequest, S3Client, S3, 20 | }, 21 | std::{borrow::Cow, pin::Pin, str::FromStr}, 22 | tokio::io::AsyncReadExt as TokioAsyncReadExt, 23 | }; 24 | 25 | pub struct S3Writer { 26 | client: S3Client, 27 | bucket: String, 28 | key_prefix: Option, 29 | } 30 | 31 | impl S3Writer { 32 | /// Create a new S3 writer bound to a named bucket with optional key prefix. 33 | /// 34 | /// This will construct a default AWS [Client]. 35 | pub fn new(region: Region, bucket: impl ToString, key_prefix: Option<&str>) -> Self { 36 | Self { 37 | client: S3Client::new(region), 38 | bucket: bucket.to_string(), 39 | key_prefix: key_prefix.map(|x| x.trim_matches('/').to_string()), 40 | } 41 | } 42 | 43 | /// Create a new S3 writer bound to a named bucket, optional key prefix, with an AWS [Client]. 44 | /// 45 | /// This is like [Self::new()] except the caller can pass in the AWS [Client] to use. 46 | pub fn new_with_client( 47 | client: Client, 48 | region: Region, 49 | bucket: impl ToString, 50 | key_prefix: Option<&str>, 51 | ) -> Self { 52 | Self { 53 | client: S3Client::new_with_client(client, region), 54 | bucket: bucket.to_string(), 55 | key_prefix: key_prefix.map(|x| x.trim_matches('/').to_string()), 56 | } 57 | } 58 | 59 | /// Compute the S3 key name given a repository relative path. 60 | pub fn path_to_key(&self, path: &str) -> String { 61 | if let Some(prefix) = &self.key_prefix { 62 | format!("{}/{}", prefix, path.trim_matches('/')) 63 | } else { 64 | path.trim_matches('/').to_string() 65 | } 66 | } 67 | } 68 | 69 | #[async_trait] 70 | impl RepositoryWriter for S3Writer { 71 | async fn verify_path<'path>( 72 | &self, 73 | path: &'path str, 74 | expected_content: Option<(u64, ContentDigest)>, 75 | ) -> Result> { 76 | if let Some((expected_size, expected_digest)) = expected_content { 77 | let req = GetObjectRequest { 78 | bucket: self.bucket.clone(), 79 | key: self.path_to_key(path), 80 | ..Default::default() 81 | }; 82 | 83 | match self.client.get_object(req).await { 84 | Ok(output) => { 85 | // Fast path without having to ready the body. 86 | if let Some(cl) = output.content_length { 87 | if cl as u64 != expected_size { 88 | return Ok(RepositoryPathVerification { 89 | path, 90 | state: RepositoryPathVerificationState::ExistsIntegrityMismatch, 91 | }); 92 | } 93 | } 94 | 95 | if let Some(body) = output.body { 96 | let mut digester = MultiDigester::default(); 97 | 98 | let mut remaining = expected_size; 99 | let mut reader = body.into_async_read(); 100 | let mut buf = [0u8; 16384]; 101 | 102 | loop { 103 | let size = reader 104 | .read(&mut buf[..]) 105 | .await 106 | .map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?; 107 | 108 | digester.update(&buf[0..size]); 109 | 110 | let size = size as u64; 111 | 112 | if size >= remaining || size == 0 { 113 | break; 114 | } 115 | 116 | remaining -= size; 117 | } 118 | 119 | let digests = digester.finish(); 120 | 121 | Ok(RepositoryPathVerification { 122 | path, 123 | state: if !digests.matches_digest(&expected_digest) { 124 | RepositoryPathVerificationState::ExistsIntegrityMismatch 125 | } else { 126 | RepositoryPathVerificationState::ExistsIntegrityVerified 127 | }, 128 | }) 129 | } else { 130 | Ok(RepositoryPathVerification { 131 | path, 132 | state: RepositoryPathVerificationState::Missing, 133 | }) 134 | } 135 | } 136 | Err(RusotoError::Service(GetObjectError::NoSuchKey(_))) => { 137 | Ok(RepositoryPathVerification { 138 | path, 139 | state: RepositoryPathVerificationState::Missing, 140 | }) 141 | } 142 | Err(e) => Err(DebianError::RepositoryIoPath( 143 | path.to_string(), 144 | std::io::Error::new(std::io::ErrorKind::Other, format!("S3 error: {:?}", e)), 145 | )), 146 | } 147 | } else { 148 | let req = HeadObjectRequest { 149 | bucket: self.bucket.clone(), 150 | key: self.path_to_key(path), 151 | ..Default::default() 152 | }; 153 | 154 | match self.client.head_object(req).await { 155 | Ok(_) => Ok(RepositoryPathVerification { 156 | path, 157 | state: RepositoryPathVerificationState::ExistsNoIntegrityCheck, 158 | }), 159 | Err(RusotoError::Service(HeadObjectError::NoSuchKey(_))) => { 160 | Ok(RepositoryPathVerification { 161 | path, 162 | state: RepositoryPathVerificationState::Missing, 163 | }) 164 | } 165 | Err(e) => Err(DebianError::RepositoryIoPath( 166 | path.to_string(), 167 | std::io::Error::new(std::io::ErrorKind::Other, format!("S3 error: {:?}", e)), 168 | )), 169 | } 170 | } 171 | } 172 | 173 | async fn write_path<'path, 'reader>( 174 | &self, 175 | path: Cow<'path, str>, 176 | mut reader: Pin>, 177 | ) -> Result> { 178 | // rusoto wants a Stream. There's no easy way to convert from an AsyncRead to a 179 | // Stream. So we just buffer content locally. 180 | // TODO implement this properly 181 | let mut buf = vec![]; 182 | reader 183 | .read_to_end(&mut buf) 184 | .await 185 | .map_err(|e| DebianError::RepositoryIoPath(path.to_string(), e))?; 186 | 187 | let bytes_written = buf.len() as u64; 188 | let stream = futures::stream::once(async { Ok(bytes::Bytes::from(buf)) }); 189 | 190 | let req = PutObjectRequest { 191 | bucket: self.bucket.clone(), 192 | key: self.path_to_key(path.as_ref()), 193 | body: Some(ByteStream::new(stream)), 194 | ..Default::default() 195 | }; 196 | 197 | match self.client.put_object(req).await { 198 | Ok(_) => Ok(RepositoryWrite { 199 | path, 200 | bytes_written, 201 | }), 202 | Err(e) => Err(DebianError::RepositoryIoPath( 203 | path.to_string(), 204 | std::io::Error::new(std::io::ErrorKind::Other, format!("S3 error: {:?}", e)), 205 | )), 206 | } 207 | } 208 | } 209 | 210 | /// Attempt to resolve the AWS region of an S3 bucket. 211 | pub async fn get_bucket_region(bucket: impl ToString) -> Result { 212 | get_bucket_region_with_client(S3Client::new(Region::UsEast1), bucket).await 213 | } 214 | 215 | /// Attempt to resolve the AWS region of an S3 bucket using a provided [S3Client]. 216 | pub async fn get_bucket_region_with_client( 217 | client: S3Client, 218 | bucket: impl ToString, 219 | ) -> Result { 220 | let req = GetBucketLocationRequest { 221 | bucket: bucket.to_string(), 222 | ..Default::default() 223 | }; 224 | 225 | match client.get_bucket_location(req).await { 226 | Ok(res) => { 227 | if let Some(constraint) = res.location_constraint { 228 | Ok(Region::from_str(&constraint) 229 | .map_err(|_| DebianError::S3BadRegion(constraint))?) 230 | } else { 231 | Ok(Region::UsEast1) 232 | } 233 | } 234 | Err(e) => Err(DebianError::Io(std::io::Error::new( 235 | std::io::ErrorKind::Other, 236 | format!("S3 error: {:?}", e), 237 | ))), 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /debian-packaging/src/repository/sink_writer.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! A special repository writer that writes to a black hole. */ 6 | 7 | use { 8 | crate::{ 9 | error::{DebianError, Result}, 10 | io::ContentDigest, 11 | repository::{ 12 | RepositoryPathVerification, RepositoryPathVerificationState, RepositoryWrite, 13 | RepositoryWriter, 14 | }, 15 | }, 16 | async_trait::async_trait, 17 | futures::AsyncRead, 18 | std::{borrow::Cow, pin::Pin, str::FromStr}, 19 | }; 20 | 21 | /// How [RepositoryWriter::verify_path()] should behave for [SinkWriter] instances. 22 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 23 | pub enum SinkWriterVerifyBehavior { 24 | /// Path exists but an integrity check was not performed. 25 | ExistsNoIntegrityCheck, 26 | /// Path exists and its integrity was verified. 27 | ExistsIntegrityVerified, 28 | /// Path exists but its integrity doesn't match expectations. 29 | ExistsIntegrityMismatch, 30 | /// Path does not exist. 31 | Missing, 32 | } 33 | 34 | impl From for RepositoryPathVerificationState { 35 | fn from(v: SinkWriterVerifyBehavior) -> Self { 36 | match v { 37 | SinkWriterVerifyBehavior::ExistsNoIntegrityCheck => Self::ExistsNoIntegrityCheck, 38 | SinkWriterVerifyBehavior::ExistsIntegrityVerified => Self::ExistsIntegrityVerified, 39 | SinkWriterVerifyBehavior::ExistsIntegrityMismatch => Self::ExistsIntegrityMismatch, 40 | SinkWriterVerifyBehavior::Missing => Self::Missing, 41 | } 42 | } 43 | } 44 | 45 | impl FromStr for SinkWriterVerifyBehavior { 46 | type Err = DebianError; 47 | 48 | fn from_str(s: &str) -> std::result::Result { 49 | match s { 50 | "exists-no-integrity-check" => Ok(Self::ExistsNoIntegrityCheck), 51 | "exists-integrity-verified" => Ok(Self::ExistsIntegrityVerified), 52 | "exists-integrity-mismatch" => Ok(Self::ExistsIntegrityMismatch), 53 | "missing" => Ok(Self::Missing), 54 | _ => Err(DebianError::SinkWriterVerifyBehaviorUnknown(s.to_string())), 55 | } 56 | } 57 | } 58 | 59 | /// A [RepositoryWriter] that writes data to a black hole. 60 | pub struct SinkWriter { 61 | verify_behavior: SinkWriterVerifyBehavior, 62 | } 63 | 64 | impl Default for SinkWriter { 65 | fn default() -> Self { 66 | Self { 67 | verify_behavior: SinkWriterVerifyBehavior::Missing, 68 | } 69 | } 70 | } 71 | 72 | impl SinkWriter { 73 | /// Set the behavior for [RepositoryWriter::verify_path()] on this instance. 74 | pub fn set_verify_behavior(&mut self, behavior: SinkWriterVerifyBehavior) { 75 | self.verify_behavior = behavior; 76 | } 77 | } 78 | 79 | #[async_trait] 80 | impl RepositoryWriter for SinkWriter { 81 | async fn verify_path<'path>( 82 | &self, 83 | path: &'path str, 84 | _expected_content: Option<(u64, ContentDigest)>, 85 | ) -> Result> { 86 | Ok(RepositoryPathVerification { 87 | path, 88 | state: self.verify_behavior.into(), 89 | }) 90 | } 91 | 92 | async fn write_path<'path, 'reader>( 93 | &self, 94 | path: Cow<'path, str>, 95 | reader: Pin>, 96 | ) -> Result> { 97 | let mut writer = futures::io::sink(); 98 | let bytes_written = futures::io::copy(reader, &mut writer).await?; 99 | 100 | Ok(RepositoryWrite { 101 | path, 102 | bytes_written, 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /debian-packaging/src/signing_key.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! PGP signing keys. */ 6 | 7 | use { 8 | pgp::{ 9 | crypto::{hash::HashAlgorithm, sym::SymmetricKeyAlgorithm}, 10 | types::{CompressionAlgorithm, SecretKeyTrait}, 11 | Deserializable, KeyType, SecretKeyParams, SecretKeyParamsBuilder, SignedPublicKey, 12 | SignedSecretKey, 13 | }, 14 | smallvec::smallvec, 15 | std::io::Cursor, 16 | strum::EnumIter, 17 | }; 18 | 19 | /// Release signing key for Debian 8 Jessie. 20 | pub const DEBIAN_8_RELEASE_KEY: &str = include_str!("keys/debian-8-release.asc"); 21 | 22 | /// Archive signing key for Debian 8 Jessie. 23 | pub const DEBIAN_8_ARCHIVE_KEY: &str = include_str!("keys/debian-8-archive.asc"); 24 | 25 | /// Security archive signing key for Debian 8 Jessie. 26 | pub const DEBIAN_8_SECURITY_ARCHIVE_KEY: &str = include_str!("keys/debian-8-security.asc"); 27 | 28 | /// Release signing key for Debian 9 Stretch. 29 | pub const DEBIAN_9_RELEASE_KEY: &str = include_str!("keys/debian-9-release.asc"); 30 | 31 | /// Archive signing key for Debian 9 Stretch. 32 | pub const DEBIAN_9_ARCHIVE_KEY: &str = include_str!("keys/debian-9-archive.asc"); 33 | 34 | /// Security archive signing key for Debian 9 Stretch. 35 | pub const DEBIAN_9_SECURITY_ARCHIVE_KEY: &str = include_str!("keys/debian-9-security.asc"); 36 | 37 | /// Release signing key for Debian 10 Buster. 38 | pub const DEBIAN_10_RELEASE_KEY: &str = include_str!("keys/debian-10-release.asc"); 39 | 40 | /// Archive signing key for Debian 10 Buster. 41 | pub const DEBIAN_10_ARCHIVE_KEY: &str = include_str!("keys/debian-10-archive.asc"); 42 | 43 | /// Security archive signing key for Debian 10 Buster. 44 | pub const DEBIAN_10_SECURITY_ARCHIVE_KEY: &str = include_str!("keys/debian-10-security.asc"); 45 | 46 | /// Release signing key for Debian 11 Bullseye. 47 | pub const DEBIAN_11_RELEASE_KEY: &str = include_str!("keys/debian-11-release.asc"); 48 | 49 | /// Archive signing key for Debian 11 Bullseye. 50 | pub const DEBIAN_11_ARCHIVE_KEY: &str = include_str!("keys/debian-11-archive.asc"); 51 | 52 | /// Security archive signing key for Debian 11 Bullseye. 53 | pub const DEBIAN_11_SECURITY_ARCHIVE_KEY: &str = include_str!("keys/debian-11-security.asc"); 54 | 55 | /// Defines well-known signing keys embedded within this crate. 56 | #[derive(Clone, Copy, Debug, EnumIter)] 57 | pub enum DistroSigningKey { 58 | /// Debian 9 Stretch release/stable key. 59 | Debian8Release, 60 | /// Debian 9 Stretch archive/automatic key. 61 | Debian8Archive, 62 | /// Debian 9 Stretch security archive/automatic key. 63 | Debian8SecurityArchive, 64 | /// Debian 9 Stretch release/stable key. 65 | Debian9Release, 66 | /// Debian 9 Stretch archive/automatic key. 67 | Debian9Archive, 68 | /// Debian 9 Stretch security archive/automatic key. 69 | Debian9SecurityArchive, 70 | /// Debian 10 Buster release/stable key. 71 | Debian10Release, 72 | /// Debian 10 Buster archive/automatic key. 73 | Debian10Archive, 74 | /// Debian 10 Buster security archive/automatic key. 75 | Debian10SecurityArchive, 76 | /// Debian 11 Bullseye release/stable key. 77 | Debian11Release, 78 | /// Debian 11 Bullseye archive/automatic key. 79 | Debian11Archive, 80 | /// Debian 11 Bullseye security archive/automatic key. 81 | Debian11SecurityArchive, 82 | } 83 | 84 | impl DistroSigningKey { 85 | /// Obtain the ASCII armored PGP public key. 86 | pub fn armored_public_key(&self) -> &'static str { 87 | match self { 88 | Self::Debian8Release => DEBIAN_8_RELEASE_KEY, 89 | Self::Debian8Archive => DEBIAN_8_ARCHIVE_KEY, 90 | Self::Debian8SecurityArchive => DEBIAN_8_SECURITY_ARCHIVE_KEY, 91 | Self::Debian9Release => DEBIAN_9_RELEASE_KEY, 92 | Self::Debian9Archive => DEBIAN_9_ARCHIVE_KEY, 93 | Self::Debian9SecurityArchive => DEBIAN_9_SECURITY_ARCHIVE_KEY, 94 | Self::Debian10Release => DEBIAN_10_RELEASE_KEY, 95 | Self::Debian10Archive => DEBIAN_10_ARCHIVE_KEY, 96 | Self::Debian10SecurityArchive => DEBIAN_10_SECURITY_ARCHIVE_KEY, 97 | Self::Debian11Release => DEBIAN_11_RELEASE_KEY, 98 | Self::Debian11Archive => DEBIAN_11_ARCHIVE_KEY, 99 | Self::Debian11SecurityArchive => DEBIAN_11_SECURITY_ARCHIVE_KEY, 100 | } 101 | } 102 | 103 | /// Obtain the parsed PGP public key for this variant. 104 | pub fn public_key(&self) -> SignedPublicKey { 105 | SignedPublicKey::from_armor_single(Cursor::new(self.armored_public_key().as_bytes())) 106 | .expect("built-in signing keys should parse") 107 | .0 108 | } 109 | } 110 | 111 | /// Obtain a [SecretKeyParamsBuilder] defining how to generate a signing key. 112 | /// 113 | /// The returned builder will have defaults appropriate for Debian packaging signing keys. 114 | /// 115 | /// The `primary_user_id` has a format like `Name `. e.g. `John Smith `. 116 | pub fn signing_secret_key_params_builder(primary_user_id: impl ToString) -> SecretKeyParamsBuilder { 117 | let mut key_params = SecretKeyParamsBuilder::default(); 118 | key_params 119 | .key_type(KeyType::Rsa(2048)) 120 | .preferred_symmetric_algorithms(smallvec![SymmetricKeyAlgorithm::AES256]) 121 | .preferred_hash_algorithms(smallvec![ 122 | HashAlgorithm::SHA2_256, 123 | HashAlgorithm::SHA2_384, 124 | HashAlgorithm::SHA2_512 125 | ]) 126 | .preferred_compression_algorithms(smallvec![CompressionAlgorithm::ZLIB]) 127 | .can_sign(true) 128 | .primary_user_id(primary_user_id.to_string()); 129 | 130 | key_params 131 | } 132 | 133 | /// Create a self-signed PGP key pair. 134 | /// 135 | /// This takes [SecretKeyParams] that define the PGP key that will be generated. 136 | /// It is recommended to use [signing_secret_key_params_builder()] to obtain these 137 | /// params. 138 | /// 139 | /// `key_passphrase` defines a function that will return the passphrase used to 140 | /// lock the private key. 141 | /// 142 | /// This returns a [SignedSecretKey] and a [SignedPublicKey] representing the 143 | /// private-public key pair. Each key is self-signed by the just-generated private 144 | /// key. 145 | /// 146 | /// A self-signed PGP key pair may not be appropriate for real-world signing keys 147 | /// on production Debian repositories. PGP best practices often entail: 148 | /// 149 | /// * Use of sub-keys where the sub-key is used for signing and the primary key is a 150 | /// more closely guarded secret and is only used for signing newly-created sub-keys 151 | /// or other keys. 152 | /// * Having a key signed by additional keys to help build a *web of trust*. 153 | /// 154 | /// Users are highly encouraged to research PGP best practices before using the keys 155 | /// produced by this function in a production capacity. 156 | /// 157 | /// ```rust 158 | /// use debian_packaging::signing_key::*; 159 | /// 160 | /// let builder = signing_secret_key_params_builder("someone@example.com"); 161 | /// // This is where you would further customize the key parameters. 162 | /// let params = builder.build().unwrap(); 163 | /// let (private_key, public_key) = create_self_signed_key(params, String::new).unwrap(); 164 | /// 165 | /// // You can ASCII armor the emitted key pair using the `.to_armored_*()` functions. This format 166 | /// // is a common way to store and exchange PGP key pairs. 167 | /// 168 | /// // Produces `-----BEGIN PGP PRIVATE KEY BLOCK----- ...` 169 | /// let private_key_armored = private_key.to_armored_string(Default::default()).unwrap(); 170 | /// // Produces `-----BEGIN PGP PUBLIC KEY BLOCK----- ...` 171 | /// let public_key_armored = public_key.to_armored_string(Default::default()).unwrap(); 172 | /// ``` 173 | pub fn create_self_signed_key( 174 | params: SecretKeyParams, 175 | key_passphrase: PW, 176 | ) -> pgp::errors::Result<(SignedSecretKey, SignedPublicKey)> 177 | where 178 | PW: (FnOnce() -> String) + Clone, 179 | { 180 | let mut rng = rand::thread_rng(); 181 | let secret_key = params.generate(&mut rng)?; 182 | let secret_key_signed = secret_key.sign(&mut rng, key_passphrase.clone())?; 183 | 184 | let public_key = secret_key_signed.public_key(); 185 | let public_key_signed = public_key.sign(&mut rng, &secret_key_signed, key_passphrase)?; 186 | 187 | Ok((secret_key_signed, public_key_signed)) 188 | } 189 | 190 | #[cfg(test)] 191 | mod test { 192 | use {super::*, strum::IntoEnumIterator}; 193 | 194 | #[test] 195 | fn all_distro_signing_keys() { 196 | for key in DistroSigningKey::iter() { 197 | key.public_key(); 198 | } 199 | } 200 | 201 | #[test] 202 | fn key_creation() -> pgp::errors::Result<()> { 203 | let builder = signing_secret_key_params_builder("Me "); 204 | let params = builder.build().unwrap(); 205 | let (private, public) = create_self_signed_key(params, || "passphrase".to_string())?; 206 | 207 | assert!(private 208 | .to_armored_string(Default::default())? 209 | .starts_with("-----BEGIN PGP PRIVATE KEY BLOCK-----")); 210 | assert!(public 211 | .to_armored_string(Default::default())? 212 | .starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----")); 213 | 214 | Ok(()) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /debian-packaging/src/source_package_control.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! Source package control files. */ 6 | 7 | use { 8 | crate::{ 9 | control::{ControlFile, ControlParagraph}, 10 | error::{DebianError, Result}, 11 | }, 12 | std::io::BufRead, 13 | }; 14 | 15 | /// Represents a `debian/control` file. 16 | /// 17 | /// Specified at . 18 | /// 19 | /// The 1st paragraph defines the source package. Subsequent paragraphs define binary packages 20 | /// that the source tree builds. 21 | #[derive(Default)] 22 | pub struct SourcePackageControlFile<'a> { 23 | general: ControlParagraph<'a>, 24 | binaries: Vec>, 25 | } 26 | 27 | impl<'a> SourcePackageControlFile<'a> { 28 | /// Construct an instance from an iterable of [ControlParagraph]. 29 | pub fn from_paragraphs( 30 | mut paragraphs: impl Iterator>, 31 | ) -> Result { 32 | let general = paragraphs.next().ok_or_else(|| { 33 | DebianError::ControlParseError( 34 | "no general paragraph in source control file".to_string(), 35 | ) 36 | })?; 37 | 38 | let binaries = paragraphs.collect::>(); 39 | 40 | Ok(Self { general, binaries }) 41 | } 42 | 43 | /// Construct an instance by parsing a control file from a reader. 44 | pub fn parse_reader(reader: &mut R) -> Result { 45 | let control = ControlFile::parse_reader(reader)?; 46 | 47 | Self::from_paragraphs(control.paragraphs().map(|x| x.to_owned())) 48 | } 49 | 50 | /// Construct an instance by parsing a string. 51 | pub fn parse_str(s: &str) -> Result { 52 | let mut reader = std::io::BufReader::new(s.as_bytes()); 53 | Self::parse_reader(&mut reader) 54 | } 55 | 56 | /// Obtain a handle on the general paragraph. 57 | pub fn general_paragraph(&self) -> &ControlParagraph<'a> { 58 | &self.general 59 | } 60 | 61 | /// Obtain an iterator over paragraphs defining binaries. 62 | pub fn binary_paragraphs(&self) -> impl Iterator> { 63 | self.binaries.iter() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /debian-packaging/src/testdata/libzstd_1.4.8+dfsg-3.dsc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNED MESSAGE----- 2 | Hash: SHA512 3 | 4 | Format: 3.0 (quilt) 5 | Source: libzstd 6 | Binary: libzstd-dev, libzstd1, zstd, libzstd1-udeb 7 | Architecture: any 8 | Version: 1.4.8+dfsg-3 9 | Maintainer: Debian Med Packaging Team 10 | Uploaders: Kevin Murray , Olivier Sallou , Alexandre Mestiashvili 11 | Homepage: https://github.com/facebook/zstd 12 | Standards-Version: 4.6.0 13 | Vcs-Browser: https://salsa.debian.org/med-team/libzstd 14 | Vcs-Git: https://salsa.debian.org/med-team/libzstd.git 15 | Testsuite: autopkgtest 16 | Build-Depends: d-shlibs, debhelper-compat (= 13), help2man, liblz4-dev, liblzma-dev, zlib1g-dev 17 | Package-List: 18 | libzstd-dev deb libdevel optional arch=any 19 | libzstd1 deb libs optional arch=any 20 | libzstd1-udeb udeb debian-installer optional arch=any 21 | zstd deb utils optional arch=any 22 | Checksums-Sha1: 23 | a24e4ccf9fc356aeaaa0783316a26bd65817c354 1331996 libzstd_1.4.8+dfsg.orig.tar.xz 24 | 896a47a2934d0fcf9faa8397d05a12b932697d1f 12184 libzstd_1.4.8+dfsg-3.debian.tar.xz 25 | Checksums-Sha256: 26 | 1e8ce5c4880a6d5bd8d3186e4186607dd19b64fc98a3877fc13aeefd566d67c5 1331996 libzstd_1.4.8+dfsg.orig.tar.xz 27 | fecd87a469d5a07b6deeeef53ed24b2f1a74ee097ce11528fe3b58540f05c147 12184 libzstd_1.4.8+dfsg-3.debian.tar.xz 28 | Files: 29 | 943bed8b8d98a50c8d8a101b12693bb4 1331996 libzstd_1.4.8+dfsg.orig.tar.xz 30 | 4d2692830e1f481ce769e2dd24cbc9db 12184 libzstd_1.4.8+dfsg-3.debian.tar.xz 31 | 32 | -----BEGIN PGP SIGNATURE----- 33 | 34 | iQJFBAEBCgAvFiEE8fAHMgoDVUHwpmPKV4oElNHGRtEFAmFdVM4RHHRpbGxlQGRl 35 | Ymlhbi5vcmcACgkQV4oElNHGRtF0Kg//bLydBw+YSjs1UYLmuL3jWvQqwhEyLwlK 36 | Inao4bAJ13CjPXuALuCOz0XVyF/J480PiLAjqoBlIsm0zBITdaWik0P4ZWh//hnT 37 | oQQEXI3dlHykHVYJ9ZXz9zAHWWjrs8tmTMGj2xQ479fTu1XTGQaWHuej5+TBhvIL 38 | 9S0kjtzGNnjsMPNdydToCkFY51KEBwvcaeVa1wFgrhHgu6SytPCJ27czm/HkkAvu 39 | XFWnikKSMfzeqA+lctwxxRwlEDdCO3JW9jES8418Ucw+I9Lx4FMIJhOLiq12vxzd 40 | LVAN7sWXRm6UjO1deZSuXEG9RBgUGtyzx0HIT01l38dpJ/z23dvl+E6xtoSXumIw 41 | KMMBddplNjcOd+IS011e6jlX9Ht2XdLJQ92Z+FAIvV+DiAX0SVQZuJkxSZ6PsJ+3 42 | 03cEL0FbSLCW9XHcQe3YoyF/rAQhSnrNKkYdkGRYpo13qpWDojY1EVjLoIe2gYv2 43 | asNXQCTmp5TrfkgpkHeB3yXo/CzzecB9Ewfpam0iB7mYTS0siNasK5gboOSz0urp 44 | I9V2pG/UDClKXbmNIC8etScG2ZwH70jQddFcCnReUkGNOdubdPRBlEGahxPgh1aD 45 | rSdD+NE9tV9EqJMAb381iKMBLVsmKB3eonHBMKYIz4VKTeq31vQs2O4ig5en4ktE 46 | FRIa3Zc3eUU= 47 | =ZzNx 48 | -----END PGP SIGNATURE----- 49 | -------------------------------------------------------------------------------- /debian-repo-tool/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `debian-repo-tool` History 2 | 3 | 4 | 5 | ## Unreleased 6 | 7 | Released on ReleaseDate. 8 | 9 | ## 0.9.0 10 | 11 | Released on 2024-11-02. 12 | 13 | ## 0.8.0 14 | 15 | Released on 2023-11-03. 16 | 17 | ## 0.7.0 and Earlier 18 | 19 | * No changelog kept. 20 | -------------------------------------------------------------------------------- /debian-repo-tool/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "debian-repo-tool" 3 | version = "0.9.0" 4 | edition = "2021" 5 | rust-version = "1.75" 6 | authors = ["Gregory Szorc "] 7 | license = "MPL-2.0" 8 | description = "CLI tool to interact with Debian repositories" 9 | keywords = ["debian", "package", "apt", "deb"] 10 | homepage = "https://github.com/indygreg/linux-packaging-rs" 11 | repository = "https://github.com/indygreg/linux-packaging-rs.git" 12 | readme = "README.md" 13 | 14 | [[bin]] 15 | name = "drt" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | clap = "4.5.20" 20 | num_cpus = "1.16.0" 21 | pbr = "1.1.1" 22 | serde_yaml = "0.9.34" 23 | thiserror = "1.0.66" 24 | tokio = { version = "1.41.0", features = ["full"] } 25 | 26 | [dependencies.debian-packaging] 27 | version = "0.18.0" 28 | path = "../debian-packaging" 29 | 30 | [dev-dependencies] 31 | trycmd = "0.15.8" 32 | -------------------------------------------------------------------------------- /debian-repo-tool/README.md: -------------------------------------------------------------------------------- 1 | # debian-repo-tool 2 | 3 | `debian-repo-tool` is a binary crate providing the `drt` command-line 4 | executable. This CLI tool allows interacting with Debian repositories 5 | (published entities that define `.deb` packages and other metadata). 6 | -------------------------------------------------------------------------------- /debian-repo-tool/src/cli.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use { 6 | clap::{value_parser, Arg, ArgAction, ArgMatches, Command}, 7 | debian_packaging::{ 8 | error::DebianError, 9 | repository::{ 10 | copier::{RepositoryCopier, RepositoryCopierConfig}, 11 | PublishEvent, 12 | }, 13 | }, 14 | std::sync::{Arc, Mutex}, 15 | thiserror::Error, 16 | }; 17 | 18 | const URLS_ABOUT: &str = "\ 19 | Repository URLs 20 | 21 | Various commands accept URLs describing the location of a repository. Here is 22 | how they work. 23 | 24 | If a value contains `://`, it will be parsed as a URL. Otherwise it will be 25 | interpreted as a local filesystem path. 26 | 27 | The following URL schemes (the part before the `://` in a URL) are recognized: 28 | 29 | file:// 30 | A local filesystem path. The path component of the URL is interpreted as 31 | a filesystem path. 32 | 33 | http:// and https:// 34 | A HTTP-based repository. 35 | 36 | Read-only (writes not supported) 37 | 38 | null:// 39 | A repository that points to nothing. 40 | 41 | This repository sends all writes to a null / nothing / a black hole. 42 | 43 | By default, `null://` will assume a file does not exist in the destination. 44 | It is possible to override this behavior by specifying one of the following 45 | values: 46 | 47 | null://exists-no-integrity-check 48 | Assumes a file exists without indicating that an integrity check was 49 | performed. 50 | null://exists-integrity-verified 51 | Assumes a file exists and indicates an integrity check was performed. 52 | null://exists-integrity-mismatch 53 | Assumes a file exists and its content does not match what the copier 54 | desires it to be. 55 | 56 | Write-only (reads not supported) 57 | 58 | s3:// 59 | An S3 bucket. 60 | 61 | URLs of the form `s3://bucket` anchor the repository at the root of the S3 62 | bucket. 63 | 64 | URLs of the form `s3://bucket/path` anchor the repository under a key prefix 65 | in the bucket. 66 | 67 | The AWS client will be resolved using configuration files and environment 68 | variables as is typical for AWS clients. For example, it looks in 69 | `~/.aws/config` and in `AWS_*` environment variables. 70 | 71 | Write-only (reads not supported) 72 | 73 | In all cases, the URL should point to the base of the Debian repository. This 74 | is typically a directory containing `dists` and `pool` sub-directories. 75 | "; 76 | 77 | const COPY_REPOSITORY_ABOUT: &str = "\ 78 | Copy a Debian repository. 79 | 80 | Given a source and destination repository and parameters to control what to 81 | copy, this command will ensure the destination repository has a complete 82 | copy of the content in the source repository. 83 | 84 | Repository copying works by reading the `[In]Release` file for a given 85 | distribution, fetching additional indices files (such as `Packages` and 86 | `Sources` files) to find additional content, and bulk copying all found 87 | files to the destination. 88 | 89 | Copying is generally smart about avoiding I/O if possible. For example, 90 | a file in the destination will not be written to if it already exists and 91 | has the appropriate content. 92 | 93 | # YAML Configuration 94 | 95 | A YAML file can be used to specify the configuration of the copy operation(s) 96 | to perform. 97 | 98 | The YAML file consists of 1 or more documents. Each document can have the 99 | following keys: 100 | 101 | source_url (required) (string) 102 | The URL or path of the repository to copy from. 103 | 104 | destination_url (required) (string) 105 | The URL or path of the repository to copy to. 106 | 107 | distributions (optional) (list[string]) 108 | Names of distributions to copy. Distributions must be located in paths 109 | like `dists/`. 110 | 111 | distribution_paths (optional) (list[string]) 112 | Paths of distributions to copy. 113 | 114 | Use this if a distribution isn't in a directory named `dists/`. 115 | 116 | only_components (optional) (list[string]) 117 | Names of components to copy. Common component names include `contrib` and 118 | `main`. 119 | 120 | If not specified, all advertised components are copied. 121 | 122 | binary_packages_copy (optional) (bool) 123 | Whether to copy binary packages. 124 | 125 | binary_packages_only_architectures (optional) (list[string]) 126 | Filter of architectures of binary packages to copy. 127 | 128 | installer_binary_packages_copy (optional) (bool) 129 | Whether to copy installer binary packages (udebs). 130 | 131 | installer_binary_packages_only_architectures (optional) (list[string]) 132 | Filter of architectures of installer binary packages to copy. 133 | 134 | sources_copy (optional) (bool) 135 | Whether to copy source packages. 136 | 137 | # Partial Copying 138 | 139 | By default, a copy operation will copy all content in the specified 140 | distributions. However, it is possible to limit the content that is 141 | copied. 142 | 143 | Our definition of _copy_ preserves the repository indices (the 144 | cryptographically signed documents advertising the repository content). 145 | When performing a partial _copy_, rewriting the indices to advertise a 146 | different set of content would invalidate the existing cryptographic 147 | signature, which is not something we want to allow in _copy_ mode. 148 | 149 | If partial copying is being performed, all indices files are preserved 150 | without modification, therefore preserving their cryptographic signature. 151 | However, these indices may refer to content that doesn't exist in the 152 | destination. This can result in packaging clients encountering missing 153 | files. 154 | "; 155 | 156 | #[derive(Debug, Error)] 157 | pub enum DrtError { 158 | #[error("argument parsing error: {0:?}")] 159 | Clap(#[from] clap::Error), 160 | 161 | #[error("{0:?}")] 162 | Debian(#[from] DebianError), 163 | 164 | #[error("I/O error: {0:?}")] 165 | Io(#[from] std::io::Error), 166 | 167 | #[error("YAML error: {0:?}")] 168 | SerdeYaml(#[from] serde_yaml::Error), 169 | 170 | #[error("invalid sub-command: {0}")] 171 | InvalidSubCommand(String), 172 | } 173 | 174 | pub type Result = std::result::Result; 175 | 176 | fn default_threads_count() -> usize { 177 | num_cpus::get() 178 | } 179 | 180 | pub async fn run_cli() -> Result<()> { 181 | let app = Command::new("Debian Repository Tool") 182 | .version("0.1") 183 | .author("Gregory Szorc ") 184 | .about("Interface with Debian Repositories") 185 | .arg_required_else_help(true); 186 | 187 | let app = app.arg( 188 | Arg::new("max-parallel-io") 189 | .long("max-parallel-io") 190 | .action(ArgAction::Set) 191 | .value_parser(value_parser!(usize)) 192 | .global(true) 193 | .help("Maximum number of parallel I/O operations to perform"), 194 | ); 195 | 196 | let app = app.subcommand( 197 | Command::new("copy-repository") 198 | .about("Copy a Debian repository between locations") 199 | .long_about(COPY_REPOSITORY_ABOUT) 200 | .arg( 201 | Arg::new("yaml-config") 202 | .long("yaml-config") 203 | .action(ArgAction::Set) 204 | .required(true) 205 | .value_parser(value_parser!(std::ffi::OsString)) 206 | .help("Path to a YAML file defining the copy configuration"), 207 | ), 208 | ); 209 | 210 | let mut app = 211 | app.subcommand(Command::new("urls").about("Print documentation about repository URLs")); 212 | 213 | let matches = app.clone().get_matches(); 214 | 215 | match matches.subcommand() { 216 | Some(("copy-repository", args)) => command_copy_repository(args).await, 217 | Some(("urls", _)) => { 218 | println!("{}", URLS_ABOUT); 219 | Ok(()) 220 | } 221 | Some((command, _)) => Err(DrtError::InvalidSubCommand(command.to_string())), 222 | None => { 223 | app.print_help()?; 224 | Ok(()) 225 | } 226 | } 227 | } 228 | 229 | async fn command_copy_repository(args: &ArgMatches) -> Result<()> { 230 | let max_parallel_io = args 231 | .get_one::("max-parallel-io") 232 | .copied() 233 | .unwrap_or_else(default_threads_count); 234 | 235 | let yaml_path = args 236 | .get_one::("yaml-config") 237 | .expect("yaml-config argument is required"); 238 | 239 | let f = std::fs::File::open(yaml_path)?; 240 | let config: RepositoryCopierConfig = serde_yaml::from_reader(f)?; 241 | 242 | let pb = Arc::new(Mutex::new(None)); 243 | 244 | let cb = Box::new(move |event: PublishEvent| match event { 245 | PublishEvent::WriteSequenceBeginWithTotalBytes(total) => { 246 | let mut bar = pbr::ProgressBar::new(total); 247 | bar.set_units(pbr::Units::Bytes); 248 | 249 | pb.lock().unwrap().replace(bar); 250 | } 251 | PublishEvent::WriteSequenceProgressBytes(count) => { 252 | pb.lock() 253 | .unwrap() 254 | .as_mut() 255 | .expect("progress bar should be defined") 256 | .add(count); 257 | } 258 | PublishEvent::WriteSequenceFinished => { 259 | let mut guard = pb.lock().unwrap(); 260 | guard 261 | .as_mut() 262 | .expect("progress bar should be defined") 263 | .finish(); 264 | guard.take(); 265 | } 266 | PublishEvent::CopyPhaseBegin(_) | PublishEvent::CopyPhaseEnd(_) => { 267 | println!("{}", event); 268 | } 269 | _ => {} 270 | }); 271 | 272 | RepositoryCopier::copy_from_config(config, max_parallel_io, &Some(cb)).await?; 273 | 274 | Ok(()) 275 | } 276 | -------------------------------------------------------------------------------- /debian-repo-tool/src/main.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | mod cli; 6 | 7 | #[tokio::main(flavor = "multi_thread")] 8 | async fn main() { 9 | std::process::exit(match cli::run_cli().await { 10 | Ok(_) => 0, 11 | Err(err) => { 12 | eprintln!("{:#?}", err); 13 | 1 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /debian-repo-tool/tests/cli_tests.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #[test] 6 | fn cli_tests() { 7 | let cases = trycmd::TestCases::new(); 8 | 9 | cases.case("tests/cmd/*.trycmd").case("tests/cmd/*.toml"); 10 | 11 | cases.run(); 12 | } 13 | -------------------------------------------------------------------------------- /debian-repo-tool/tests/cmd/help.trycmd: -------------------------------------------------------------------------------- 1 | ``` 2 | $ drt help 3 | Interface with Debian Repositories 4 | 5 | Usage: drt[EXE] [OPTIONS] [COMMAND] 6 | 7 | Commands: 8 | copy-repository Copy a Debian repository between locations 9 | urls Print documentation about repository URLs 10 | help Print this message or the help of the given subcommand(s) 11 | 12 | Options: 13 | --max-parallel-io Maximum number of parallel I/O operations to perform 14 | -h, --help Print help 15 | -V, --version Print version 16 | 17 | ``` 18 | 19 | ``` 20 | $ drt help copy-repository 21 | Copy a Debian repository. 22 | 23 | Given a source and destination repository and parameters to control what to 24 | copy, this command will ensure the destination repository has a complete 25 | copy of the content in the source repository. 26 | 27 | Repository copying works by reading the `[In]Release` file for a given 28 | distribution, fetching additional indices files (such as `Packages` and 29 | `Sources` files) to find additional content, and bulk copying all found 30 | files to the destination. 31 | 32 | Copying is generally smart about avoiding I/O if possible. For example, 33 | a file in the destination will not be written to if it already exists and 34 | has the appropriate content. 35 | 36 | # YAML Configuration 37 | 38 | A YAML file can be used to specify the configuration of the copy operation(s) 39 | to perform. 40 | 41 | The YAML file consists of 1 or more documents. Each document can have the 42 | following keys: 43 | 44 | source_url (required) (string) 45 | The URL or path of the repository to copy from. 46 | 47 | destination_url (required) (string) 48 | The URL or path of the repository to copy to. 49 | 50 | distributions (optional) (list[string]) 51 | Names of distributions to copy. Distributions must be located in paths 52 | like `dists/`. 53 | 54 | distribution_paths (optional) (list[string]) 55 | Paths of distributions to copy. 56 | 57 | Use this if a distribution isn't in a directory named `dists/`. 58 | 59 | only_components (optional) (list[string]) 60 | Names of components to copy. Common component names include `contrib` and 61 | `main`. 62 | 63 | If not specified, all advertised components are copied. 64 | 65 | binary_packages_copy (optional) (bool) 66 | Whether to copy binary packages. 67 | 68 | binary_packages_only_architectures (optional) (list[string]) 69 | Filter of architectures of binary packages to copy. 70 | 71 | installer_binary_packages_copy (optional) (bool) 72 | Whether to copy installer binary packages (udebs). 73 | 74 | installer_binary_packages_only_architectures (optional) (list[string]) 75 | Filter of architectures of installer binary packages to copy. 76 | 77 | sources_copy (optional) (bool) 78 | Whether to copy source packages. 79 | 80 | # Partial Copying 81 | 82 | By default, a copy operation will copy all content in the specified 83 | distributions. However, it is possible to limit the content that is 84 | copied. 85 | 86 | Our definition of _copy_ preserves the repository indices (the 87 | cryptographically signed documents advertising the repository content). 88 | When performing a partial _copy_, rewriting the indices to advertise a 89 | different set of content would invalidate the existing cryptographic 90 | signature, which is not something we want to allow in _copy_ mode. 91 | 92 | If partial copying is being performed, all indices files are preserved 93 | without modification, therefore preserving their cryptographic signature. 94 | However, these indices may refer to content that doesn't exist in the 95 | destination. This can result in packaging clients encountering missing 96 | files. 97 | 98 | 99 | Usage: drt[EXE] copy-repository [OPTIONS] --yaml-config 100 | 101 | Options: 102 | --max-parallel-io 103 | Maximum number of parallel I/O operations to perform 104 | 105 | --yaml-config 106 | Path to a YAML file defining the copy configuration 107 | 108 | -h, --help 109 | Print help (see a summary with '-h') 110 | 111 | ``` 112 | 113 | ``` 114 | $ drt help urls 115 | Print documentation about repository URLs 116 | 117 | Usage: drt[EXE] urls [OPTIONS] 118 | 119 | Options: 120 | --max-parallel-io Maximum number of parallel I/O operations to perform 121 | -h, --help Print help 122 | 123 | ``` 124 | -------------------------------------------------------------------------------- /linux-package-analyzer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `linux-package-analyzer` History 2 | 3 | 4 | 5 | ## Unreleased 6 | 7 | Released on ReleaseDate. 8 | 9 | ## 0.3.0 10 | 11 | Released on 2024-11-02. 12 | 13 | ## 0.2.0 14 | 15 | Released on 2023-11-03. 16 | 17 | ## 0.1.0 18 | 19 | * No changelog kept. 20 | -------------------------------------------------------------------------------- /linux-package-analyzer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "linux-package-analyzer" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.75" 6 | authors = ["Gregory Szorc "] 7 | license = "MPL-2.0" 8 | description = "CLI tool to analyze Linux packages" 9 | keywords = ["deb", "package", "rpm"] 10 | homepage = "https://github.com/indygreg/linux-packaging-rs" 11 | repository = "https://github.com/indygreg/linux-packaging-rs.git" 12 | readme = "README.md" 13 | 14 | [[bin]] 15 | name = "lpa" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | anyhow = "1.0.92" 20 | clap = "4.5.20" 21 | cpio = "0.4.0" 22 | futures = "0.3.31" 23 | futures-util = "0.3.31" 24 | iced-x86 = "1.21.0" 25 | indoc = "2.0.5" 26 | num_cpus = "1.16.0" 27 | object = "0.36.5" 28 | once_cell = "1.20.2" 29 | pbr = "1.1.1" 30 | rusqlite = { version = "0.29.0", features = ["bundled"] } 31 | symbolic-demangle = "12.12.0" 32 | tokio = { version = "1.41.0", features = ["full"] } 33 | url = "2.5.2" 34 | xz2 = { version = "0.1.7", features = ["static"] } 35 | zstd = "0.13.2" 36 | 37 | [dependencies.debian-packaging] 38 | version = "0.18.0" 39 | path = "../debian-packaging" 40 | 41 | [dependencies.rpm-repository] 42 | version = "0.3.0" 43 | path = "../rpm-repository" 44 | 45 | # rpm-rs seems to be unmaintained and its old dependencies are holding us back. 46 | # Disabled until we figure out a path forward. 47 | #[dependencies.rpm-rs] 48 | #git = "https://github.com/indygreg/rpm-rs" 49 | #rev = "d6623c68a85e3a14f4260c2161c348fa697131c0" 50 | 51 | [dev-dependencies] 52 | trycmd = "0.15.8" 53 | -------------------------------------------------------------------------------- /linux-package-analyzer/README.md: -------------------------------------------------------------------------------- 1 | # linux-package-analyzer 2 | 3 | `linux-package-analyzer` is a binary Rust crate providing the `lpa` command-line 4 | executable. This CLI tool facilitates indexing and then inspecting the contents of 5 | Linux package repositories. Both Debian and RPM based repositories are supported. 6 | 7 | Run `lpa help` for more details. 8 | 9 | # Installing 10 | 11 | ``` 12 | # From the latest released version on crates.io: 13 | $ cargo install linux-package-analyzer 14 | 15 | # From the latest commit in the canonical Git repository: 16 | $ cargo install --git https://github.com/indygreg/linux-packaging-rs linux-package-analyzer 17 | 18 | # From the root directory of a Git source checkout: 19 | $ cargo install --path linux-package-analyzer 20 | ``` 21 | 22 | # How It Works 23 | 24 | `lpa` exposes sub-commands for importing the contents of a specified package 25 | repository into a local SQLite database. Essentially, the package lists from 26 | the remote repository are retrieved and referenced packages are downloaded 27 | and their content indexed. The indexed content includes: 28 | 29 | * Files installed by the package 30 | * ELF file content 31 | * File header values 32 | * Section metadata 33 | * Dynamic library dependencies 34 | * Symbols 35 | * x86 instruction counts 36 | 37 | Additional sub-commands exist for performing analysis of the indexed content 38 | within the SQLite databases. However, there is a lot of data in the SQLite 39 | database that is not exposed or queryable via the CLI. 40 | 41 | # Example 42 | 43 | The following command will import all packages from Ubuntu 21.10 Impish for 44 | amd64 into the SQLite database `ubuntu-impish.db`: 45 | 46 | ``` 47 | lpa --db ubuntu-impish.db \ 48 | import-debian-repository \ 49 | --components main,multiverse,restricted,universe \ 50 | --architectures amd64 \ 51 | http://us.archive.ubuntu.com/ubuntu impish 52 | ``` 53 | 54 | This should download ~96 GB of packages (as of January 2022) and create a 55 | ~12 GB SQLite database. 56 | 57 | Once we have a populated database, we can run commands to query its content. 58 | 59 | To see which files import (and presumably call) a specific C function: 60 | 61 | ``` 62 | lpa --db ubuntu-impish.db \ 63 | elf-files-importing-symbol OPENSSL_init_ssl 64 | ``` 65 | 66 | To see what are the most popular ELF section names: 67 | 68 | ``` 69 | lpa --db ubuntu-impish.db elf-section-name-counts 70 | ``` 71 | 72 | Power users may want to write their own queries against the database. To 73 | get started, open the SQLite database and poke around: 74 | 75 | ``` 76 | $ sqlite3 ubuntu-impish.db 77 | SQLite version 3.35.5 2021-04-19 18:32:05 78 | Enter ".help" for usage hints. 79 | 80 | sqlite> .tables 81 | elf_file package_file 82 | elf_file_needed_library symbol_name 83 | elf_file_x86_base_register_count v_elf_needed_library 84 | elf_file_x86_instruction_count v_elf_symbol 85 | elf_file_x86_register_count v_package_elf_file 86 | elf_section v_package_file 87 | elf_symbol v_package_instruction_count 88 | package 89 | 90 | sqlite> select * from v_elf_needed_library where library_name = "libc.so.6" order by package_name asc limit 1; 91 | 0ad|0.0.25b-1|http://us.archive.ubuntu.com/ubuntu/pool/universe/0/0ad/0ad_0.0.25b-1_amd64.deb|usr/games/pyrogenesis|libc.so.6 92 | ``` 93 | 94 | The `v_` prefixed tables are views and conveniently pull in data from 95 | multiple tables. For example, `v_elf_symbol` has all the columns of 96 | `elf_symbol` but also expands the package name, version, file path, etc. 97 | 98 | # Constants and Special Values 99 | 100 | Various ELF data uses constants to define attributes. e.g. `elf_file.machine` 101 | is an integer holding the ELF machine type. A good reference for values of 102 | these constants is 103 | https://docs.rs/object/0.28.2/src/object/elf.rs.html#1-6256. 104 | 105 | `lpa` also exposes various `reference-*` commands for printing known 106 | values. 107 | 108 | # Known Issues 109 | 110 | ## x86 Disassembly Quirks 111 | 112 | On package index/import, an attempt is made to disassemble x86 / x86-64 files so 113 | instruction counts and register usage can be stored in the database. 114 | 115 | We disassemble all sections marked as executable. Instructions in other 116 | sections may not be found (this is hopefully rare). 117 | 118 | We disassemble using the [iced_x86](https://crates.io/crates/iced-x86) Rust crate. 119 | So any limitations in that crate apply to the disassembler. 120 | 121 | We disassemble instructions by iterating over content of the binary section, 122 | attempting to read instructions until end of section. Executable sections can 123 | contain NULL bytes, inline data, and other bytes that may not represent valid 124 | instructions. This will result in many byte sequences decoding to the special 125 | *invalid* instruction. In some cases, a byte sequence may decode to an 126 | instruction even though the underlying data is not an instruction. i.e. there 127 | can be false positives on instruction counts. 128 | 129 | ## Intermittent HTTP Failures on Package Retrieval 130 | 131 | Intermittent HTTP GET failures when importing packages is expected due to 132 | intrinsic network unreliability. This often manifests as an error like the 133 | following: 134 | 135 | ``` 136 | error processing package (ignoring): repository I/O error on path pool/universe/g/gcc-10/gnat-10_10.3.0-11ubuntu1_amd64.deb: Custom { kind: Other, error: "error sending HTTP request: reqwest::Error { kind: Request, url: Url { scheme: \"http\", cannot_be_a_base: false, username: \"\", password: None, host: Some(Domain(\"us.archive.ubuntu.com\")), port: None, path: \"/ubuntu/pool/universe/g/gcc-10/gnat-10_10.3.0-11ubuntu1_amd64.deb\", query: None, fragment: None }, source: hyper::Error(IncompleteMessage) }" } 137 | ``` 138 | 139 | If you see failures like this, simply retry the import operation. Already 140 | imported packages should automatically be skipped. 141 | 142 | ## Package Server Throttling 143 | 144 | `lpa` can issue parallel HTTP requests to retrieve content. By default, it 145 | issues up to as many parallel requests as CPU cores/threads. 146 | 147 | Some package repositories limit the number of simultaneous HTTP 148 | connections/requests by client. If your machine has many CPU cores, you may run 149 | into these limits and get a high volume of HTTP errors when fetching packages. 150 | To mitigate, reduce the number of simultaneous I/O operations via `--threads`. 151 | e.g. `lpa --threads 4 ...` 152 | 153 | ## SQLite Integrity Weakening 154 | 155 | To maximize speed of import operations, SQLite databases have their content 156 | integrity and durability guarantees weakened via `PRAGMA` statements issued 157 | on database open. A process or machine crash during a write operation could 158 | corrupt the SQLite database more easily than it otherwise would. 159 | -------------------------------------------------------------------------------- /linux-package-analyzer/src/main.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use anyhow::Result; 6 | 7 | pub mod binary; 8 | pub mod cli; 9 | pub mod db; 10 | pub mod import; 11 | 12 | #[tokio::main(flavor = "multi_thread")] 13 | async fn main() -> Result<()> { 14 | cli::run().await 15 | } 16 | -------------------------------------------------------------------------------- /linux-package-analyzer/tests/cli_tests.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | #[test] 6 | fn cli_tests() { 7 | let cases = trycmd::TestCases::new(); 8 | 9 | cases.case("tests/cmd/*.trycmd").case("tests/cmd/*.toml"); 10 | 11 | cases.run(); 12 | } 13 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["main"] 2 | push-remote = "origin" 3 | pre-release-commit-message = "workspace: perform releases" 4 | tag-message = "{{crate_name}}: version {{version}}" 5 | tag-name = "{{crate_name}}/{{version}}" 6 | tag = true 7 | enable-features = [] 8 | enable-all-features = false 9 | dependent-version = "fix" 10 | 11 | pre-release-replacements = [ 12 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}"}, 13 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, 14 | {file="CHANGELOG.md", search="", replace="\n\n## Unreleased\n\nReleased on ReleaseDate.", exactly=1}, 15 | ] 16 | -------------------------------------------------------------------------------- /rpm-repository/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `rpm-repository` History 2 | 3 | 4 | 5 | ## Unreleased 6 | 7 | Released on ReleaseDate. 8 | 9 | ## 0.3.0 10 | 11 | Released on 2024-11-02. 12 | 13 | ## 0.2.0 14 | 15 | Released on 2023-11-03. 16 | 17 | ## 0.1.0 18 | 19 | * No changelog kept. 20 | -------------------------------------------------------------------------------- /rpm-repository/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rpm-repository" 3 | version = "0.3.0" 4 | edition = "2021" 5 | rust-version = "1.75" 6 | authors = ["Gregory Szorc "] 7 | license = "MPL-2.0" 8 | description = "RPM repository interaction" 9 | keywords = ["createrepo", "rpm", "packaging", "repomd", "yum"] 10 | homepage = "https://github.com/indygreg/linux-packaging-rs" 11 | repository = "https://github.com/indygreg/linux-packaging-rs.git" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | digest = "0.10.7" 16 | futures = "0.3.31" 17 | hex = "0.4.3" 18 | pin-project = "1.1.7" 19 | serde = { version = "1.0.214", features = ["derive"] } 20 | serde-xml-rs = "0.6.0" 21 | sha-1 = "0.10.1" 22 | sha2 = "0.10.8" 23 | thiserror = "1.0.66" 24 | tokio = { version = "1.41.0", features = ["macros"] } 25 | url = "2.5.2" 26 | 27 | [dependencies.async-compression] 28 | version = "0.4.17" 29 | features = ["futures-io", "gzip", "xz", "zstd"] 30 | 31 | [dependencies.reqwest] 32 | version = "0.12.9" 33 | default-features = false 34 | features = ["rustls-tls", "stream"] 35 | -------------------------------------------------------------------------------- /rpm-repository/README.md: -------------------------------------------------------------------------------- 1 | # RPM Repository 2 | 3 | `rpm-repository` is a library crate implementing functionality related 4 | to RPM / yum based repositories. 5 | 6 | See the crate's documentation for more. 7 | -------------------------------------------------------------------------------- /rpm-repository/src/error.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use thiserror::Error; 6 | 7 | /// Error type for this crate. 8 | #[derive(Debug, Error)] 9 | pub enum RpmRepositoryError { 10 | #[error("URL parse error: {0:?}")] 11 | UrlParse(#[from] url::ParseError), 12 | 13 | #[error("HTTP error: {0:?}")] 14 | Http(#[from] reqwest::Error), 15 | 16 | #[error("XML error: {0:?}")] 17 | Xml(#[from] serde_xml_rs::Error), 18 | 19 | #[error("repository I/O error on path {0}: {1:?}")] 20 | IoPath(String, std::io::Error), 21 | 22 | #[error("invalid hex in content digest: {0}; {1:?}")] 23 | ContentDigestBadHex(String, hex::FromHexError), 24 | 25 | #[error("unknown content digest format: {0}")] 26 | UnknownDigestFormat(String), 27 | 28 | #[error("repository metadata entry not found: {0}")] 29 | MetadataFileNotFound(&'static str), 30 | 31 | #[error("unexpected data path: {0}")] 32 | UnexpectedDataPath(String), 33 | 34 | #[error("content size missing from metadata entry")] 35 | MetadataMissingSize, 36 | } 37 | 38 | /// Result type for this crate. 39 | pub type Result = std::result::Result; 40 | -------------------------------------------------------------------------------- /rpm-repository/src/http.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use { 6 | crate::{ 7 | error::{Result, RpmRepositoryError}, 8 | metadata::repomd::RepoMd, 9 | DataResolver, MetadataReader, RepositoryRootReader, 10 | }, 11 | futures::{AsyncRead, TryStreamExt}, 12 | reqwest::{Client, ClientBuilder, IntoUrl, StatusCode, Url}, 13 | std::{future::Future, pin::Pin}, 14 | }; 15 | 16 | /// Default HTTP user agent string. 17 | pub const USER_AGENT: &str = "rpm-repository Rust crate (https://crates.io/crates/rpm-repository)"; 18 | 19 | async fn fetch_url( 20 | client: &Client, 21 | root_url: &Url, 22 | path: &str, 23 | ) -> Result>> { 24 | let request_url = root_url.join(path)?; 25 | 26 | let res = client.get(request_url.clone()).send().await.map_err(|e| { 27 | RpmRepositoryError::IoPath( 28 | path.to_string(), 29 | std::io::Error::new( 30 | std::io::ErrorKind::Other, 31 | format!("error sending HTTP request: {:?}", e), 32 | ), 33 | ) 34 | })?; 35 | 36 | let res = res.error_for_status().map_err(|e| { 37 | if e.status() == Some(StatusCode::NOT_FOUND) { 38 | RpmRepositoryError::IoPath( 39 | path.to_string(), 40 | std::io::Error::new( 41 | std::io::ErrorKind::NotFound, 42 | format!("HTTP 404 for {}", request_url), 43 | ), 44 | ) 45 | } else { 46 | RpmRepositoryError::IoPath( 47 | path.to_string(), 48 | std::io::Error::new( 49 | std::io::ErrorKind::Other, 50 | format!("bad HTTP status code: {:?}", e), 51 | ), 52 | ) 53 | } 54 | })?; 55 | 56 | Ok(Box::pin( 57 | res.bytes_stream() 58 | .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e))) 59 | .into_async_read(), 60 | )) 61 | } 62 | 63 | /// Client for RPM repositories served via HTTP. 64 | /// 65 | /// Instances are bound to a base URL, which represents the base directory. 66 | #[derive(Debug)] 67 | pub struct HttpRepositoryClient { 68 | /// HTTP client to use. 69 | client: Client, 70 | 71 | /// Base URL for this repository. 72 | root_url: Url, 73 | } 74 | 75 | impl HttpRepositoryClient { 76 | /// Construct an instance bound to the specified URL. 77 | pub fn new(url: impl IntoUrl) -> Result { 78 | let builder = ClientBuilder::new().user_agent(USER_AGENT); 79 | 80 | Self::new_client(builder.build()?, url) 81 | } 82 | 83 | pub fn new_client(client: Client, url: impl IntoUrl) -> Result { 84 | let mut root_url = url.into_url()?; 85 | 86 | // Trailing URLs are significant to the Url type when we .join(). So ensure 87 | // the URL has a trailing path. 88 | if !root_url.path().ends_with('/') { 89 | root_url.set_path(&format!("{}/", root_url.path())); 90 | } 91 | 92 | Ok(Self { client, root_url }) 93 | } 94 | } 95 | 96 | impl DataResolver for HttpRepositoryClient { 97 | #[allow(clippy::type_complexity)] 98 | fn get_path( 99 | &self, 100 | path: String, 101 | ) -> Pin>>> + Send + '_>> { 102 | async fn run( 103 | slf: &HttpRepositoryClient, 104 | path: String, 105 | ) -> Result>> { 106 | fetch_url(&slf.client, &slf.root_url, &path).await 107 | } 108 | 109 | Box::pin(run(self, path)) 110 | } 111 | } 112 | 113 | impl RepositoryRootReader for HttpRepositoryClient { 114 | fn url(&self) -> Result { 115 | Ok(self.root_url.clone()) 116 | } 117 | 118 | #[allow(clippy::type_complexity)] 119 | fn metadata_reader( 120 | &self, 121 | ) -> Pin>> + Send + '_>> { 122 | async fn run(slf: &HttpRepositoryClient) -> Result> { 123 | let relative_path = "repodata".to_string(); 124 | 125 | let root_url = slf.root_url.join(&relative_path)?; 126 | 127 | let repomd = slf 128 | .fetch_repomd(format!("{}/repomd.xml", relative_path)) 129 | .await?; 130 | 131 | Ok(Box::new(HttpMetadataClient { 132 | client: slf.client.clone(), 133 | root_url, 134 | relative_path, 135 | repomd, 136 | })) 137 | } 138 | 139 | Box::pin(run(self)) 140 | } 141 | } 142 | 143 | /// Repository HTTP client bound to a parsed `repomd.xml` file. 144 | pub struct HttpMetadataClient { 145 | client: Client, 146 | root_url: Url, 147 | relative_path: String, 148 | repomd: RepoMd, 149 | } 150 | 151 | impl DataResolver for HttpMetadataClient { 152 | #[allow(clippy::type_complexity)] 153 | fn get_path( 154 | &self, 155 | path: String, 156 | ) -> Pin>>> + Send + '_>> { 157 | async fn run( 158 | slf: &HttpMetadataClient, 159 | path: String, 160 | ) -> Result>> { 161 | fetch_url(&slf.client, &slf.root_url, &path).await 162 | } 163 | 164 | Box::pin(run(self, path)) 165 | } 166 | } 167 | 168 | impl MetadataReader for HttpMetadataClient { 169 | fn url(&self) -> Result { 170 | Ok(self.root_url.clone()) 171 | } 172 | 173 | fn root_relative_path(&self) -> &str { 174 | &self.relative_path 175 | } 176 | 177 | fn repomd(&self) -> &RepoMd { 178 | &self.repomd 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod test { 184 | use super::*; 185 | 186 | const FEDORA_41_URL: &str = 187 | "https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/41/Server/x86_64/os"; 188 | 189 | #[tokio::test] 190 | async fn fedora_41() -> Result<()> { 191 | let root = HttpRepositoryClient::new(FEDORA_41_URL)?; 192 | 193 | let metadata = root.metadata_reader().await?; 194 | 195 | let primary = metadata.primary_packages().await?; 196 | 197 | let zlib = primary 198 | .packages 199 | .iter() 200 | .find(|entry| entry.name == "zstd") 201 | .unwrap(); 202 | 203 | assert_eq!(zlib.package_type, "rpm"); 204 | // This could change if a new version is released. 205 | assert!(zlib.version.version.starts_with("1.5")); 206 | 207 | Ok(()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /rpm-repository/src/io.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | use { 6 | crate::error::{Result, RpmRepositoryError}, 7 | async_compression::futures::bufread::{GzipDecoder, XzDecoder, ZstdDecoder}, 8 | futures::{AsyncBufRead, AsyncRead}, 9 | pin_project::pin_project, 10 | std::{ 11 | fmt::Formatter, 12 | pin::Pin, 13 | task::{Context, Poll}, 14 | }, 15 | }; 16 | 17 | /// Compression format. 18 | pub enum Compression { 19 | /// No compression. 20 | None, 21 | /// Gzip compression. 22 | Gzip, 23 | /// Xz compression. 24 | Xz, 25 | /// Zstd compression. 26 | Zstd, 27 | } 28 | 29 | pub fn read_decompressed<'a>( 30 | stream: impl AsyncBufRead + Send + 'a, 31 | compression: Compression, 32 | ) -> Pin> { 33 | match compression { 34 | Compression::None => Box::pin(stream), 35 | Compression::Gzip => Box::pin(GzipDecoder::new(stream)), 36 | Compression::Xz => Box::pin(XzDecoder::new(stream)), 37 | Compression::Zstd => Box::pin(ZstdDecoder::new(stream)), 38 | } 39 | } 40 | 41 | pub enum DigestFlavor { 42 | Sha1, 43 | Sha256, 44 | } 45 | 46 | /// Represents a content digest. 47 | #[derive(Clone, Eq, PartialEq, PartialOrd)] 48 | pub enum ContentDigest { 49 | /// A SHA-1 digest. 50 | Sha1(Vec), 51 | /// A SHA-256 digest. 52 | Sha256(Vec), 53 | } 54 | 55 | impl std::fmt::Debug for ContentDigest { 56 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 57 | match self { 58 | Self::Sha1(data) => write!(f, "Sha1({})", hex::encode(data)), 59 | Self::Sha256(data) => write!(f, "Sha256({})", hex::encode(data)), 60 | } 61 | } 62 | } 63 | 64 | impl ContentDigest { 65 | /// Create a new SHA-1 instance by parsing a hex digest. 66 | pub fn sha1_hex(digest: &str) -> Result { 67 | Self::from_hex_digest(DigestFlavor::Sha1, digest) 68 | } 69 | 70 | /// Create a new SHA-256 instance by parsing a hex digest. 71 | pub fn sha256_hex(digest: &str) -> Result { 72 | Self::from_hex_digest(DigestFlavor::Sha256, digest) 73 | } 74 | 75 | /// Obtain an instance by parsing a hex string as a [ChecksumType]. 76 | pub fn from_hex_digest(checksum: DigestFlavor, digest: &str) -> Result { 77 | let digest = hex::decode(digest) 78 | .map_err(|e| RpmRepositoryError::ContentDigestBadHex(digest.to_string(), e))?; 79 | 80 | Ok(match checksum { 81 | DigestFlavor::Sha1 => Self::Sha1(digest), 82 | DigestFlavor::Sha256 => Self::Sha256(digest), 83 | }) 84 | } 85 | 86 | /// Create a new hasher matching for the type of this digest. 87 | pub fn new_hasher(&self) -> Box { 88 | match self { 89 | Self::Sha1(_) => Box::::default(), 90 | Self::Sha256(_) => Box::::default(), 91 | } 92 | } 93 | 94 | /// Obtain the digest bytes for this content digest. 95 | pub fn digest_bytes(&self) -> &[u8] { 96 | match self { 97 | Self::Sha1(x) => x, 98 | Self::Sha256(x) => x, 99 | } 100 | } 101 | 102 | /// Obtain the hex encoded content digest. 103 | pub fn digest_hex(&self) -> String { 104 | hex::encode(self.digest_bytes()) 105 | } 106 | 107 | /// Obtain the [ChecksumType] for this digest. 108 | pub fn digest_type(&self) -> DigestFlavor { 109 | match self { 110 | Self::Sha1(_) => DigestFlavor::Sha1, 111 | Self::Sha256(_) => DigestFlavor::Sha256, 112 | } 113 | } 114 | } 115 | 116 | #[pin_project] 117 | pub struct ContentValidatingReader { 118 | hasher: Option>, 119 | expected_size: u64, 120 | expected_digest: ContentDigest, 121 | #[pin] 122 | source: R, 123 | bytes_read: u64, 124 | } 125 | 126 | impl ContentValidatingReader { 127 | pub fn new(source: R, expected_size: u64, expected_digest: ContentDigest) -> Self { 128 | Self { 129 | hasher: Some(expected_digest.new_hasher()), 130 | expected_size, 131 | expected_digest, 132 | source, 133 | bytes_read: 0, 134 | } 135 | } 136 | } 137 | 138 | impl AsyncRead for ContentValidatingReader 139 | where 140 | R: AsyncRead + Unpin, 141 | { 142 | fn poll_read( 143 | self: Pin<&mut Self>, 144 | cx: &mut Context<'_>, 145 | buf: &mut [u8], 146 | ) -> Poll> { 147 | let mut this = self.project(); 148 | 149 | match this.source.as_mut().poll_read(cx, buf) { 150 | Poll::Ready(Ok(size)) => { 151 | if size > 0 { 152 | if let Some(hasher) = this.hasher.as_mut() { 153 | hasher.update(&buf[0..size]); 154 | } else { 155 | panic!("hasher destroyed prematurely"); 156 | } 157 | 158 | *this.bytes_read += size as u64; 159 | } 160 | 161 | match this.bytes_read.cmp(&this.expected_size) { 162 | std::cmp::Ordering::Equal => { 163 | if let Some(hasher) = this.hasher.take() { 164 | let got_digest = hasher.finalize(); 165 | 166 | if got_digest.as_ref() != this.expected_digest.digest_bytes() { 167 | return Poll::Ready(Err(std::io::Error::new( 168 | std::io::ErrorKind::Other, 169 | format!( 170 | "digest mismatch of retrieved content: expected {}, got {}", 171 | this.expected_digest.digest_hex(), 172 | hex::encode(got_digest) 173 | ), 174 | ))); 175 | } 176 | } 177 | } 178 | std::cmp::Ordering::Greater => { 179 | return Poll::Ready(Err(std::io::Error::new( 180 | std::io::ErrorKind::Other, 181 | format!( 182 | "extra bytes read: expected {}; got {}", 183 | this.expected_size, this.bytes_read 184 | ), 185 | ))); 186 | } 187 | std::cmp::Ordering::Less => {} 188 | } 189 | 190 | Poll::Ready(Ok(size)) 191 | } 192 | res => res, 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /rpm-repository/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! RPM repository interaction. 6 | 7 | This crate facilitates interacting with RPM package repositories. 8 | 9 | RPM repositories are defined by a base URL. Under that base URL is typically a 10 | `repodata` directory containing a `repomd.xml` file. This `repomd.xml` file 11 | (represented by [metadata::repomd::RepoMd]) describes other _metadata_ 12 | files constituting the repository. 13 | 14 | Files and data structures in the `repodata` directory are defined in the 15 | [metadata] module tree. 16 | 17 | The [RepositoryRootReader] trait defines a generic read interface bound to a 18 | base URL. The [MetadataReader] trait defines an interface to repository metadata 19 | via a parsed `repomd.xml` file. 20 | 21 | Concrete repository readers exist. [http::HttpRepositoryClient] provides a reader 22 | for repositories accessed via HTTP. 23 | 24 | */ 25 | 26 | pub mod error; 27 | pub mod http; 28 | pub mod io; 29 | pub mod metadata; 30 | 31 | pub use crate::error::{Result, RpmRepositoryError}; 32 | 33 | use { 34 | crate::{ 35 | io::{read_decompressed, Compression, ContentDigest, ContentValidatingReader}, 36 | metadata::{ 37 | primary::Primary, 38 | repomd::{RepoMd, RepoMdData}, 39 | }, 40 | }, 41 | futures::{AsyncRead, AsyncReadExt}, 42 | std::{future::Future, pin::Pin}, 43 | }; 44 | 45 | /// Path based content fetching. 46 | pub trait DataResolver: Sync { 47 | /// Get the content of a relative path as an async reader. 48 | #[allow(clippy::type_complexity)] 49 | fn get_path( 50 | &self, 51 | path: String, 52 | ) -> Pin>>> + Send + '_>>; 53 | 54 | /// Obtain a reader that performs content integrity checking. 55 | /// 56 | /// Because content digests can only be computed once all content is read, the reader 57 | /// emits data as it is streaming but only compares the cryptographic digest once all 58 | /// data has been read. If there is a content digest mismatch, an error will be raised 59 | /// once the final byte is read. 60 | /// 61 | /// Validation only occurs if the stream is read to completion. Failure to read the 62 | /// entire stream could result in reading of unexpected content. 63 | #[allow(clippy::type_complexity)] 64 | fn get_path_with_digest_verification( 65 | &self, 66 | path: String, 67 | expected_size: u64, 68 | expected_digest: ContentDigest, 69 | ) -> Pin>>> + Send + '_>> { 70 | async fn run( 71 | slf: &(impl DataResolver + ?Sized), 72 | path: String, 73 | expected_size: u64, 74 | expected_digest: ContentDigest, 75 | ) -> Result>> { 76 | Ok(Box::pin(ContentValidatingReader::new( 77 | slf.get_path(path).await?, 78 | expected_size, 79 | expected_digest, 80 | ))) 81 | } 82 | 83 | Box::pin(run(self, path, expected_size, expected_digest)) 84 | } 85 | 86 | /// Get the content of a relative path, transparently applying decompression. 87 | #[allow(clippy::type_complexity)] 88 | fn get_path_decompressed( 89 | &self, 90 | path: String, 91 | compression: Compression, 92 | ) -> Pin>>> + Send + '_>> { 93 | async fn run( 94 | slf: &(impl DataResolver + ?Sized), 95 | path: String, 96 | compression: Compression, 97 | ) -> Result>> { 98 | let reader = slf.get_path(path).await?; 99 | 100 | Ok(read_decompressed( 101 | Box::pin(futures::io::BufReader::new(reader)), 102 | compression, 103 | )) 104 | } 105 | 106 | Box::pin(run(self, path, compression)) 107 | } 108 | 109 | /// A combination of both [Self::get_path_decompressed()] and [Self::get_path_with_digest_verification()]. 110 | #[allow(clippy::type_complexity)] 111 | fn get_path_decompressed_with_digest_verification( 112 | &self, 113 | path: String, 114 | compression: Compression, 115 | expected_size: u64, 116 | expected_digest: ContentDigest, 117 | ) -> Pin>>> + Send + '_>> { 118 | async fn run( 119 | slf: &(impl DataResolver + ?Sized), 120 | path: String, 121 | compression: Compression, 122 | expected_size: u64, 123 | expected_digest: ContentDigest, 124 | ) -> Result>> { 125 | let reader = slf 126 | .get_path_with_digest_verification(path, expected_size, expected_digest) 127 | .await?; 128 | 129 | Ok(read_decompressed( 130 | Box::pin(futures::io::BufReader::new(reader)), 131 | compression, 132 | )) 133 | } 134 | 135 | Box::pin(run(self, path, compression, expected_size, expected_digest)) 136 | } 137 | } 138 | 139 | /// A read-only interface for the root of an RPM repository. 140 | pub trait RepositoryRootReader: DataResolver + Sync { 141 | /// Obtain the URL to which this reader is bound. 142 | fn url(&self) -> Result; 143 | 144 | #[allow(clippy::type_complexity)] 145 | fn metadata_reader( 146 | &self, 147 | ) -> Pin>> + Send + '_>>; 148 | 149 | /// Fetch and parse a `repomd.xml` file given the relative path to that file. 150 | fn fetch_repomd( 151 | &self, 152 | path: String, 153 | ) -> Pin> + Send + '_>> { 154 | async fn run(slf: &(impl RepositoryRootReader + ?Sized), path: String) -> Result { 155 | let mut reader = slf.get_path(path.clone()).await?; 156 | 157 | let mut data = vec![]; 158 | reader 159 | .read_to_end(&mut data) 160 | .await 161 | .map_err(|e| RpmRepositoryError::IoPath(path, e))?; 162 | 163 | RepoMd::from_reader(std::io::Cursor::new(data)) 164 | } 165 | 166 | Box::pin(run(self, path)) 167 | } 168 | } 169 | 170 | /// A read-only interface for metadata in an RPM repository. 171 | /// 172 | /// This essentially provides methods for retrieving and parsing content 173 | /// from the `repodata` directory. 174 | pub trait MetadataReader: DataResolver + Sync { 175 | /// Obtain the base URL to which this instance is bound. 176 | fn url(&self) -> Result; 177 | 178 | /// Obtain the path relative to the repository root this instance is bound to. 179 | fn root_relative_path(&self) -> &str; 180 | 181 | /// Obtain the raw parsed `repomd.xml` data structure. 182 | fn repomd(&self) -> &RepoMd; 183 | 184 | #[allow(clippy::type_complexity)] 185 | fn fetch_data_file<'slf>( 186 | &'slf self, 187 | data: &'slf RepoMdData, 188 | ) -> Pin>>> + Send + 'slf>> { 189 | async fn run( 190 | slf: &(impl MetadataReader + ?Sized), 191 | data: &RepoMdData, 192 | ) -> Result>> { 193 | let path = data.location.href.as_str(); 194 | 195 | let expected_size = data.size.ok_or(RpmRepositoryError::MetadataMissingSize)?; 196 | let expected_digest = ContentDigest::try_from(data.checksum.clone())?; 197 | 198 | let compression = match path { 199 | _ if path.ends_with(".gz") => Compression::Gzip, 200 | _ if path.ends_with(".xz") => Compression::Xz, 201 | _ => Compression::None, 202 | }; 203 | 204 | slf.get_path_decompressed_with_digest_verification( 205 | path.to_string(), 206 | compression, 207 | expected_size, 208 | expected_digest, 209 | ) 210 | .await 211 | } 212 | 213 | Box::pin(run(self, data)) 214 | } 215 | 216 | #[allow(clippy::type_complexity)] 217 | fn primary_packages(&self) -> Pin> + Send + '_>> { 218 | async fn run(slf: &(impl MetadataReader + ?Sized)) -> Result { 219 | let primary = slf 220 | .repomd() 221 | .data 222 | .iter() 223 | .find(|entry| entry.data_type == "primary") 224 | .ok_or(RpmRepositoryError::MetadataFileNotFound("primary"))?; 225 | 226 | let mut reader = slf.fetch_data_file(primary).await?; 227 | let mut data = vec![]; 228 | 229 | reader 230 | .read_to_end(&mut data) 231 | .await 232 | .map_err(|e| RpmRepositoryError::IoPath(primary.location.href.clone(), e))?; 233 | 234 | Primary::from_reader(std::io::Cursor::new(data)) 235 | } 236 | 237 | Box::pin(run(self)) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /rpm-repository/src/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! RPM repository metadata XML definitions. 6 | 7 | RPM repositories define metadata via a series of XML files. This module defines these 8 | XML data structures. 9 | */ 10 | 11 | pub mod primary; 12 | pub mod repomd; 13 | -------------------------------------------------------------------------------- /rpm-repository/src/metadata/primary.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! `primary.xml` file format. */ 6 | 7 | use { 8 | crate::{ 9 | error::{Result, RpmRepositoryError}, 10 | io::ContentDigest, 11 | metadata::repomd::Location, 12 | }, 13 | serde::{Deserialize, Serialize}, 14 | std::io::Read, 15 | }; 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct Primary { 19 | /// The number of packages expressed by this document. 20 | #[serde(rename = "packages")] 21 | pub count: usize, 22 | 23 | /// `` elements in this document. 24 | #[serde(rename = "package")] 25 | pub packages: Vec, 26 | } 27 | 28 | impl Primary { 29 | /// Construct an instance by parsing XML from a reader. 30 | pub fn from_reader(reader: impl Read) -> Result { 31 | Ok(serde_xml_rs::from_reader(reader)?) 32 | } 33 | 34 | /// Construct an instance by parsing XML from a string. 35 | pub fn from_xml(s: &str) -> Result { 36 | Ok(serde_xml_rs::from_str(s)?) 37 | } 38 | } 39 | 40 | /// A package as advertised in a `primary.xml` file. 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct Package { 43 | /// The type/flavor of a package. 44 | /// 45 | /// e.g. `rpm`. 46 | #[serde(rename = "type")] 47 | pub package_type: String, 48 | 49 | /// The name of the package. 50 | pub name: String, 51 | 52 | /// The machine architecture the package is targeting. 53 | pub arch: String, 54 | 55 | /// The package version. 56 | pub version: PackageVersion, 57 | 58 | /// Content digest of package file. 59 | pub checksum: Checksum, 60 | 61 | /// A text summary of the package. 62 | pub summary: String, 63 | 64 | /// A longer text description of the package. 65 | pub description: String, 66 | 67 | /// Name of entity that produced the package. 68 | pub packager: Option, 69 | 70 | /// URL where additional package info can be obtained. 71 | pub url: Option, 72 | 73 | /// Time the package was created. 74 | pub time: PackageTime, 75 | 76 | /// Describes sizes affiliated with the package. 77 | pub size: PackageSize, 78 | 79 | /// Where the package can be obtained from. 80 | pub location: Location, 81 | 82 | /// Additional metadata about the package. 83 | pub format: Option, 84 | } 85 | 86 | /// Describes a package version. 87 | #[derive(Clone, Debug, Deserialize, Serialize)] 88 | pub struct PackageVersion { 89 | /// When the version came into existence. 90 | pub epoch: u64, 91 | 92 | /// Version string. 93 | #[serde(rename = "ver")] 94 | pub version: String, 95 | 96 | /// Release string. 97 | #[serde(rename = "rel")] 98 | pub release: String, 99 | } 100 | 101 | /// Describes the content checksum of a package. 102 | #[derive(Clone, Debug, Deserialize, Serialize)] 103 | pub struct Checksum { 104 | /// Digest type. 105 | #[serde(rename = "type")] 106 | pub name: String, 107 | 108 | /// Hex encoded digest value. 109 | #[serde(rename = "$value")] 110 | pub value: String, 111 | 112 | #[serde(rename = "pkgid")] 113 | pub pkg_id: Option, 114 | } 115 | 116 | impl TryFrom for ContentDigest { 117 | type Error = RpmRepositoryError; 118 | 119 | fn try_from(v: Checksum) -> std::result::Result { 120 | match v.name.as_str() { 121 | "sha1" => ContentDigest::sha1_hex(&v.value), 122 | "sha256" => ContentDigest::sha256_hex(&v.value), 123 | name => Err(RpmRepositoryError::UnknownDigestFormat(name.to_string())), 124 | } 125 | } 126 | } 127 | 128 | /// Times associated with a package. 129 | #[derive(Clone, Debug, Deserialize, Serialize)] 130 | pub struct PackageTime { 131 | pub file: u64, 132 | pub build: u64, 133 | } 134 | 135 | /// Sizes associated with a package. 136 | #[derive(Clone, Debug, Deserialize, Serialize)] 137 | pub struct PackageSize { 138 | pub package: u64, 139 | 140 | /// Total size in bytes when installed. 141 | pub installed: u64, 142 | 143 | /// Size in bytes of package archive. 144 | pub archive: u64, 145 | } 146 | 147 | /// Additional metadata about a package. 148 | #[derive(Clone, Debug, Deserialize, Serialize)] 149 | pub struct PackageFormat { 150 | /// The package's license. 151 | pub license: Option, 152 | 153 | /// Vendor of package. 154 | pub vendor: Option, 155 | pub group: Option, 156 | 157 | /// Hostname of machine that built the package. 158 | #[serde(rename = "buildhost")] 159 | pub build_host: Option, 160 | 161 | /// Name of RPM from which this package is derived. 162 | #[serde(rename = "sourcerpm")] 163 | pub source_rpm: Option, 164 | 165 | /// File segment containing the header. 166 | #[serde(rename = "header-range")] 167 | pub header_range: Option, 168 | 169 | /// Packages that this package provides. 170 | pub provides: Option, 171 | 172 | /// Packages that this package obsoletes. 173 | pub obsoletes: Option, 174 | 175 | /// Packages that this package requires. 176 | pub requires: Option, 177 | 178 | /// Packages that conflict with this one. 179 | pub conflicts: Option, 180 | 181 | /// Packages that are suggested when this one is installed. 182 | pub suggests: Option, 183 | 184 | /// Packages that are recommended when this one is installed. 185 | pub recommends: Option, 186 | 187 | /// Packages that this package supplements. 188 | pub supplements: Option, 189 | 190 | /// Files provided by this package. 191 | #[serde(default, rename = "file")] 192 | pub files: Vec, 193 | } 194 | 195 | /// Describes the location of a header in a package. 196 | #[derive(Clone, Debug, Deserialize, Serialize)] 197 | pub struct HeaderRange { 198 | /// Start offset in bytes. 199 | pub start: u64, 200 | 201 | /// End offset in bytes. 202 | pub end: u64, 203 | } 204 | 205 | /// A collection of [PackageEntry]. 206 | #[derive(Clone, Debug, Deserialize, Serialize)] 207 | pub struct Entries { 208 | #[serde(rename = "entry")] 209 | pub entries: Vec, 210 | } 211 | 212 | /// Describes a package relationship. 213 | #[derive(Clone, Debug, Deserialize, Serialize)] 214 | pub struct PackageEntry { 215 | /// Name of package. 216 | pub name: String, 217 | 218 | /// Version comparison flags. 219 | pub flags: Option, 220 | 221 | /// Epoch value. 222 | pub epoch: Option, 223 | 224 | /// Version of package. 225 | #[serde(rename = "ver")] 226 | pub version: Option, 227 | 228 | /// Release of package. 229 | #[serde(rename = "rel")] 230 | pub release: Option, 231 | 232 | /// Whether this is a pre-release. 233 | pub pre: Option, 234 | } 235 | 236 | #[derive(Clone, Debug, Deserialize, Serialize)] 237 | pub struct FileEntry { 238 | /// Type of file. 239 | /// 240 | /// Missing value seems to imply regular file. 241 | #[serde(rename = "type")] 242 | pub file_type: Option, 243 | 244 | #[serde(rename = "$value")] 245 | pub value: String, 246 | } 247 | -------------------------------------------------------------------------------- /rpm-repository/src/metadata/repomd.rs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | /*! `repomd.xml` file format. */ 6 | 7 | use { 8 | crate::{ 9 | error::{Result, RpmRepositoryError}, 10 | io::ContentDigest, 11 | }, 12 | serde::{Deserialize, Serialize}, 13 | std::io::Read, 14 | }; 15 | 16 | /// A `repomd.xml` file. 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct RepoMd { 19 | /// Revision of the repository. 20 | /// 21 | /// Often an integer-like value. 22 | pub revision: String, 23 | /// Describes additional primary data files constituting this repository. 24 | pub data: Vec, 25 | } 26 | 27 | impl RepoMd { 28 | /// Construct an instance by parsing XML from a reader. 29 | pub fn from_reader(reader: impl Read) -> Result { 30 | Ok(serde_xml_rs::from_reader(reader)?) 31 | } 32 | 33 | /// Construct an instance by parsing XML from a string. 34 | pub fn from_xml(s: &str) -> Result { 35 | Ok(serde_xml_rs::from_str(s)?) 36 | } 37 | } 38 | 39 | /// A `` element in a `repomd.xml` file. 40 | #[derive(Clone, Debug, Deserialize, Serialize)] 41 | pub struct RepoMdData { 42 | /// The type of data. 43 | #[serde(rename = "type")] 44 | pub data_type: String, 45 | /// Content checksum of this file. 46 | pub checksum: Checksum, 47 | /// Where the file is located. 48 | pub location: Location, 49 | /// Size in bytes of the file as stored in the repository. 50 | pub size: Option, 51 | /// Time file was created/modified. 52 | pub timestamp: Option, 53 | /// Content checksum of the decoded (often decompressed) file. 54 | #[serde(rename = "open-checksum")] 55 | pub open_checksum: Option, 56 | /// Size in bytes of the decoded (often decompressed) file. 57 | #[serde(rename = "open-size")] 58 | pub open_size: Option, 59 | /// Content checksum of header data. 60 | #[serde(rename = "header-checksum")] 61 | pub header_checksum: Option, 62 | /// Size in bytes of the header. 63 | #[serde(rename = "header-size")] 64 | pub header_size: Option, 65 | } 66 | 67 | /// The content checksum of a `` element. 68 | #[derive(Clone, Debug, Deserialize, Serialize)] 69 | pub struct Checksum { 70 | /// The name of the content digest. 71 | #[serde(rename = "type")] 72 | pub name: String, 73 | /// The hex encoded content digest. 74 | #[serde(rename = "$value")] 75 | pub value: String, 76 | } 77 | 78 | impl TryFrom for ContentDigest { 79 | type Error = RpmRepositoryError; 80 | 81 | fn try_from(v: Checksum) -> std::result::Result { 82 | match v.name.as_str() { 83 | "sha1" => ContentDigest::sha1_hex(&v.value), 84 | "sha256" => ContentDigest::sha256_hex(&v.value), 85 | name => Err(RpmRepositoryError::UnknownDigestFormat(name.to_string())), 86 | } 87 | } 88 | } 89 | 90 | /// The location of a `` element. 91 | #[derive(Clone, Debug, Deserialize, Serialize)] 92 | pub struct Location { 93 | pub href: String, 94 | } 95 | 96 | #[cfg(test)] 97 | mod test { 98 | use super::*; 99 | 100 | const FEDORA_35_REPOMD_XML: &str = include_str!("../testdata/fedora-35-repodata.xml"); 101 | 102 | #[test] 103 | fn fedora_35_parse() -> Result<()> { 104 | RepoMd::from_xml(FEDORA_35_REPOMD_XML)?; 105 | 106 | Ok(()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rpm-repository/src/testdata/fedora-35-repodata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1635225124 4 | 5 | 2311f37b3cdc06f72a535cb394e155c41e683edbeb4adfadc37bbfa090736d20 6 | 2cc3c948f5b5542ea6472d187d53df13a9598a4ca7323850758cd8ed87bc7767 7 | 8 | 1635225121 9 | 529838 10 | 4430568 11 | 12 | 13 | 1c27df5e0ce2b7d8f4d20301dd0c52774e34ca0b3935b0232c799c0012d92dea 14 | f158c554baf9ad0c4a9119f1342a151e6f1d9e995065ef347c19941ec1f5b1ed 15 | 16 | 1635225121 17 | 1278036 18 | 14044647 19 | 20 | 21 | ccf942ff083cfd01aeac3811774aa7060c9eed55a4796547c1e0f127204a2cb6 22 | 7beed2d2bd07cbd8b2b3344a7738ab442a6fc154c2c1941ae32bab0b6d057883 23 | 24 | 1635225121 25 | 504983 26 | 4911790 27 | 28 | 29 | 39d24ea1e128cb95071b36586363230c682584d4ac410330920b9dc2cc291487 30 | 61d268ba791b40580cbcd1739cd6be75f188ce797abcc085c3e010658d5adab0 31 | 32 | 1635225122 33 | 791392 34 | 4296704 35 | 10 36 | 37 | 38 | 6b14ebdbfd00c0765de56f0724fcb5aea9273284382e99442cf2fd3586f0a6e9 39 | 830fbfc293695e9edebe7b2055d5ca209b076b45e69b45de750316a57bb9ab80 40 | 41 | 1635225123 42 | 1216724 43 | 7237632 44 | 10 45 | 46 | 47 | 9e236d3288e09791773e41bd2dfa5933976cca7a95305570bfd975dc5bcc1d3d 48 | bec8e6cf16eec2328b98d8514ae348a395db847ce9a2e164c683cf91911fa5e3 49 | 50 | 1635225122 51 | 379168 52 | 4505600 53 | 10 54 | 55 | 56 | 52749b7d111c47548c5a68c5d6daf2449415b1b55be94f9e84fb0f97452a2e09 57 | 2cc3c948f5b5542ea6472d187d53df13a9598a4ca7323850758cd8ed87bc7767 58 | 4ea6c649505d487a853c08c76809fd7628cb2b841c97bf9dcad36c9c84635da9 59 | 60 | 1635225121 61 | 1059955 62 | 4430568 63 | 20571 64 | 65 | 66 | 69259bbcbef93b2cc7ff36a1d1cfb80fd15c4e62564b7c3904636bd47cd8cfe1 67 | f158c554baf9ad0c4a9119f1342a151e6f1d9e995065ef347c19941ec1f5b1ed 68 | f7ba84d8d4149caf5e355f44388022cce45f5a25f623d6ef31cebc3be4350b41 69 | 70 | 1635225121 71 | 1350659 72 | 14044647 73 | 20640 74 | 75 | 76 | f1ab54978eec08eaa17b9295ea3e4cb13966aa5346ad2f46aa1b75c1af1be92e 77 | 7beed2d2bd07cbd8b2b3344a7738ab442a6fc154c2c1941ae32bab0b6d057883 78 | 8329ea8316f1590970c89aa0fcfb70d668cb67fde413b8e82dfc6f8b3914d06f 79 | 80 | 1635225121 81 | 708993 82 | 4911790 83 | 20564 84 | 85 | 86 | 4fcfe2d7ab9cf7375051a27642d473314cb2b8f2d3d98cd09b9561428203a9e3 87 | 88 | 1635225090 89 | 106236 90 | 91 | 92 | 20c70a35954eef42f88e532ba9cda00b65eb78fea9bd2e2e9681ea5e47016276 93 | 94 | 1635225121 95 | 23368 96 | 97 | 98 | 85221f4de87a3a3e1dffd01990821992fffe7059101fdfeca45b78bc2abcb0e3 99 | 20c70a35954eef42f88e532ba9cda00b65eb78fea9bd2e2e9681ea5e47016276 100 | 63adce3e9909362a0d33edf8985786c7a700817352da6c30a22d6a215078d088 101 | 102 | 1635225124 103 | 29665 104 | 23368 105 | 158 106 | 107 | 108 | -------------------------------------------------------------------------------- /scripts/secure_download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | 6 | # Securely download a file by validating its SHA-256 against expectations. 7 | 8 | import argparse 9 | import gzip 10 | import hashlib 11 | import http.client 12 | import pathlib 13 | import urllib.error 14 | import urllib.request 15 | 16 | 17 | def hash_path(p: pathlib.Path): 18 | h = hashlib.sha256() 19 | 20 | with p.open("rb") as fh: 21 | while True: 22 | chunk = fh.read(65536) 23 | if not chunk: 24 | break 25 | 26 | h.update(chunk) 27 | 28 | return h.hexdigest() 29 | 30 | 31 | class IntegrityError(Exception): 32 | """Represents an integrity error when downloading a URL.""" 33 | 34 | 35 | def secure_download_stream(url, sha256): 36 | """Securely download a URL to a stream of chunks. 37 | 38 | If the integrity of the download fails, an exception is raised. 39 | """ 40 | h = hashlib.sha256() 41 | 42 | with urllib.request.urlopen(url) as fh: 43 | if not url.endswith(".gz") and fh.info().get("Content-Encoding") == "gzip": 44 | fh = gzip.GzipFile(fileobj=fh) 45 | 46 | while True: 47 | chunk = fh.read(65536) 48 | if not chunk: 49 | break 50 | 51 | h.update(chunk) 52 | 53 | yield chunk 54 | 55 | digest = h.hexdigest() 56 | 57 | if digest != sha256: 58 | raise IntegrityError( 59 | "integrity mismatch on %s: wanted sha256=%s; got sha256=%s" 60 | % (url, sha256, digest) 61 | ) 62 | 63 | 64 | def download_to_path(url: str, path: pathlib.Path, sha256: str): 65 | # We download to a temporary file and rename at the end so there's 66 | # no chance of the final file being partially written or containing 67 | # bad data. 68 | print("downloading %s to %s" % (url, path)) 69 | 70 | if path.exists(): 71 | good = True 72 | 73 | if good: 74 | if hash_path(path) != sha256: 75 | print("existing file hash is wrong; removing") 76 | good = False 77 | 78 | if good: 79 | print("%s exists and passes integrity checks" % path) 80 | return 81 | 82 | path.unlink() 83 | 84 | tmp = path.with_name("%s.tmp" % path.name) 85 | 86 | path.parent.mkdir(parents=True, exist_ok=True) 87 | 88 | for _ in range(5): 89 | try: 90 | try: 91 | with tmp.open("wb") as fh: 92 | for chunk in secure_download_stream(url, sha256): 93 | fh.write(chunk) 94 | 95 | break 96 | except IntegrityError: 97 | tmp.unlink() 98 | raise 99 | except http.client.HTTPException as e: 100 | print("HTTP exception; retrying: %s" % e) 101 | except urllib.error.URLError as e: 102 | print("urllib error; retrying: %s" % e) 103 | else: 104 | raise Exception("download failed after multiple retries") 105 | 106 | tmp.rename(path) 107 | print("successfully downloaded %s" % url) 108 | 109 | 110 | def main(): 111 | parser = argparse.ArgumentParser() 112 | parser.add_argument("url", help="URL to download") 113 | parser.add_argument("sha256", help="Expected SHA-256 of downloaded file") 114 | parser.add_argument("dest", help="Destination path to write") 115 | 116 | args = parser.parse_args() 117 | 118 | download_to_path(args.url, pathlib.Path(args.dest), sha256=args.sha256) 119 | 120 | 121 | if __name__ == "__main__": 122 | main() 123 | --------------------------------------------------------------------------------