├── .cargo └── config.toml ├── .devcontainer └── devcontainer.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── commits.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── assets └── omnibor-logo.svg ├── dist-workspace.toml ├── omnibor-cli ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── config │ └── omnibor.json ├── src │ ├── app.rs │ ├── cli.rs │ ├── cmd │ │ ├── artifact │ │ │ ├── find.rs │ │ │ ├── id.rs │ │ │ └── mod.rs │ │ ├── debug │ │ │ ├── mod.rs │ │ │ └── paths.rs │ │ ├── manifest │ │ │ ├── create.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── store │ │ │ ├── add.rs │ │ │ ├── log.rs │ │ │ ├── mod.rs │ │ │ └── remove.rs │ ├── config.rs │ ├── error.rs │ ├── fs.rs │ ├── log.rs │ ├── main.rs │ └── print │ │ ├── error.rs │ │ ├── find_file.rs │ │ ├── id_file.rs │ │ ├── mod.rs │ │ └── paths.rs └── tests │ ├── data │ └── main.c │ ├── snapshots │ ├── test__artifact_id_json.snap │ ├── test__artifact_id_plain.snap │ ├── test__artifact_id_short.snap │ ├── test__artifact_no_args.snap │ ├── test__debug_no_args.snap │ ├── test__manifest_no_args.snap │ └── test__no_args.snap │ └── test.rs ├── omnibor ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── benches │ └── benchmark.rs ├── src │ ├── artifact_id │ │ ├── artifact_id.rs │ │ ├── artifact_id_builder.rs │ │ └── mod.rs │ ├── embedding_mode.rs │ ├── error │ │ ├── artifact_id_error.rs │ │ ├── input_manifest_error.rs │ │ └── mod.rs │ ├── ffi │ │ ├── artifact_id.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── status.rs │ │ └── util.rs │ ├── gitoid │ │ ├── gitoid.rs │ │ ├── gitoid_url_parser.rs │ │ ├── internal.rs │ │ └── mod.rs │ ├── hash_algorithm.rs │ ├── hash_provider │ │ ├── boringssl.rs │ │ ├── mod.rs │ │ ├── openssl.rs │ │ └── rustcrypto.rs │ ├── input_manifest │ │ ├── input_manifest.rs │ │ ├── input_manifest_builder.rs │ │ └── mod.rs │ ├── lib.rs │ ├── object_type.rs │ ├── storage │ │ ├── file_system_storage.rs │ │ ├── in_memory_storage.rs │ │ ├── mod.rs │ │ └── test.rs │ ├── test.rs │ └── util │ │ ├── clone_as_boxstr.rs │ │ ├── for_each_buf_fill.rs │ │ ├── mod.rs │ │ ├── pathbuf.rs │ │ ├── sealed.rs │ │ └── stream_len.rs └── test │ └── data │ ├── hello_world.txt │ ├── unix_line.txt │ └── windows_line.txt └── xtask ├── Cargo.toml ├── README.md └── src ├── cli.rs ├── main.rs ├── pipeline.rs └── release.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | 2 | [alias] 3 | xtask = "run --package xtask --" 4 | 5 | [build] 6 | # Needed for using `tokio_console` with the CLI. 7 | rustflags = ["--cfg", "tokio_unstable"] 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnibor-dev-container", 3 | "image": "mcr.microsoft.com/devcontainers/universal:2-linux", 4 | "features": { 5 | "ghcr.io/devcontainers/features/rust:1": { 6 | "version": "1.75.0", 7 | "profile": "default" 8 | } 9 | }, 10 | "postCreateCommand": "cargo install cbindgen" 11 | } 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Windows test file that should _always_ use DOS-style newlines, 2 | # regardless of the current system. Used for tests to validate 3 | # newline normalization is working. 4 | windows_line.txt text eol=crlf 5 | 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | 16 | ## How Has This Been Tested? 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/commits.yml: -------------------------------------------------------------------------------- 1 | name: Commit Checks 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | conventional-commits: 12 | name: Conventional Commits 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 5 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: webiny/action-conventional-commits@v1.3.0 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | # Run on both PRs and pushes to the main branch. 4 | # It may seem redundant to run tests on main, since we disallow pushing directly 5 | # to main and all PRs get tested before merging. 6 | # 7 | # But due to how GitHub Actions isolates caches, we need to run the tests on 8 | # main so that caches are available to new PRs. The caches created when testing 9 | # PR code cannot be re-used outside of testing that PR. 10 | # 11 | # See the GitHub Actions documentation here: 12 | # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache 13 | on: 14 | push: 15 | branches: [main] 16 | paths: 17 | - "gitoid/**" 18 | - "omnibor/**" 19 | - "omnibor-cli/**" 20 | - "xtask/**" 21 | pull_request: 22 | branches: [main] 23 | paths: 24 | - "gitoid/**" 25 | - "omnibor/**" 26 | - "omnibor-cli/**" 27 | - "xtask/**" 28 | 29 | permissions: 30 | contents: read 31 | 32 | env: 33 | RUSTFLAGS: -Dwarnings 34 | CARGO_TERM_COLOR: always 35 | # Necessary for 'cargo-insta' to handle CI behavior correctly. 36 | CI: true 37 | 38 | jobs: 39 | test: 40 | strategy: 41 | matrix: 42 | os: [ubuntu-22.04, windows-2019, macos-13, macos-14] 43 | name: "${{ matrix.os }}" 44 | runs-on: ${{ matrix.os }} 45 | timeout-minutes: 15 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: dtolnay/rust-toolchain@stable 49 | - uses: swatinem/rust-cache@v2 50 | with: 51 | key: ${{ matrix.os }} 52 | - name: Dependency Tree 53 | run: cargo tree 54 | - name: Check 55 | run: cargo check --verbose --workspace 56 | - name: Test 57 | run: cargo test --verbose --workspace 58 | - name: Lint 59 | run: cargo clippy --verbose --workspace 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode/ 3 | Cargo.lock 4 | gitoid.h 5 | gitoid/test/c_test 6 | c-test.exe 7 | c-test 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | gitbom.infra@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We're happy to accept contributions! 4 | 5 | For bug fixes and minor changes to the implementation, feel free to open an issue 6 | in the issue tracker explaining what you'd like to fix, and then open a Pull 7 | Request with the change. 8 | 9 | For larger design changes, you may also want to discuss the changes either in the 10 | issue tracker or in the repository's Discussions page. 11 | 12 | ## Developer Certificate of Origin 13 | 14 | Contributions to this repository are under the [Developer Certificate of Origin][dco] 15 | rules and are indicated by signing off on commits with the `-s`/`--signoff` flag 16 | when committing. This is enforced with a DCO CI job that checks your commits for 17 | signoff. If you forget to do it, the CI job gives instructions on how to fix 18 | your commits to include the signoff. 19 | 20 | [dco]: https://developercertificate.org/ 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | # Overall workspace configuration. 3 | [workspace] 4 | 5 | members = ["omnibor", "omnibor-cli", "xtask"] 6 | resolver = "2" 7 | 8 | # Shared settings across packages in the workspace. 9 | [workspace.package] 10 | 11 | edition = "2021" 12 | license = "Apache-2.0" 13 | license-file = "LICENSE" 14 | homepage = "https://omnibor.io" 15 | 16 | [profile.dev.package] 17 | insta.opt-level = 3 18 | similar.opt-level = 3 19 | 20 | # The profile that 'cargo dist' will build with 21 | [profile.dist] 22 | 23 | inherits = "release" 24 | 25 | lto = "thin" 26 | 27 | 28 | #============================================================================ 29 | # Config for 'cargo release' 30 | #---------------------------------------------------------------------------- 31 | 32 | [workspace.metadata.release] 33 | 34 | # Commit message to use when doing a release. 35 | pre-release-commit-message = "chore: Release {{crate_name}}-v{{version}}" 36 | 37 | # Whether to use a single commit when releasing versions of multiple 38 | # crates in a workspace. 39 | consolidate-commits = false 40 | 41 | 42 | #============================================================================ 43 | # Config for 'git cliff' 44 | #---------------------------------------------------------------------------- 45 | 46 | [workspace.metadata.git-cliff.changelog] 47 | 48 | trim = true 49 | 50 | header = """ 51 | # Changelog\n 52 | All notable changes to this project will be documented in this file. 53 | 54 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 55 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n 56 | """ 57 | 58 | body = """ 59 | {% if version -%} 60 | ## [{{ version | split(pat="-") | last | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 61 | {% else -%} 62 | ## [Unreleased] 63 | {% endif -%} 64 | {% for group, commits in commits | group_by(attribute="group") %} 65 | ### {{ group | upper_first }} 66 | {% for commit in commits %} 67 | - {{ commit.message | upper_first }}\ 68 | {% endfor %} 69 | {% endfor %}\n 70 | """ 71 | 72 | footer = """ 73 | {% for release in releases -%} 74 | {% if release.version -%} 75 | {% if release.previous.version -%} 76 | [{{ release.version | split(pat="-") | last | trim_start_matches(pat="v") }}]: \ 77 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 78 | /compare/{{ release.previous.version }}..{{ release.version }} 79 | {% endif -%} 80 | {% else -%} 81 | [unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\ 82 | /compare/{{ release.previous.version }}..HEAD 83 | {% endif -%} 84 | {% endfor %} 85 | 86 | """ 87 | 88 | [workspace.metadata.git-cliff.git] 89 | 90 | # parse the commits based on https://www.conventionalcommits.org 91 | conventional_commits = true 92 | 93 | # filter out the commits that are not conventional 94 | filter_unconventional = true 95 | 96 | # process each line of a commit as an individual commit 97 | split_commits = false 98 | 99 | # regex for parsing and grouping commits 100 | commit_parsers = [ 101 | { message = "^.*: add", group = "Added" }, 102 | { message = "^.*: support", group = "Added" }, 103 | { message = "^.*: remove", group = "Removed" }, 104 | { message = "^.*: delete", group = "Removed" }, 105 | { message = "^test", group = "Fixed" }, 106 | { message = "^fix", group = "Fixed" }, 107 | { message = "^.*: fix", group = "Fixed" }, 108 | { message = "^.*", group = "Changed" }, 109 | ] 110 | 111 | # protect breaking changes from being skipped due to matching a skipping commit_parser 112 | protect_breaking_commits = false 113 | 114 | # filter out the commits that are not matched by commit parsers 115 | filter_commits = true 116 | 117 | # regex for matching git tags 118 | tag_pattern = "v[0-9].*" 119 | 120 | # regex for skipping tags 121 | skip_tags = "v0.1.0-beta.1" 122 | 123 | # regex for ignoring tags 124 | ignore_tags = "" 125 | 126 | # sort the tags topologically 127 | topo_order = false 128 | 129 | # sort the commits inside sections by oldest/newest order 130 | sort_commits = "oldest" 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | OmniBOR Logo 6 | 7 |
8 | 9 | __Reproducible identifiers & fine-grained build dependency tracking for software artifacts.__ 10 | 11 | [![Website](https://img.shields.io/badge/website-omnibor.io-blue)](https://omnibor.io) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/omnibor/omnibor-rs/blob/main/LICENSE) 12 | 13 |
14 | 15 | ## What is OmniBOR? 16 | 17 | [OmniBOR][omnibor] is a draft specification which defines two key concepts: 18 | 19 | - __Artifact Identifiers__: independently-reproducible identifiers for 20 | software artifacts. 21 | - __Artifact Input Manifests__: record the IDs of every input used in the 22 | build process for an artifact. 23 | 24 | Artifact IDs enable _anyone_ to identify and cross-reference information for 25 | software artifacts without a central authority. Unlike [pURL][purl] or [CPE][cpe], 26 | OmniBOR Artifact IDs don't rely on a third-party, they are _inherent 27 | identifiers_ determined only by an artifact itself. They're based on 28 | [Git's Object IDs (GitOIDs)][gitoid] in both construction and choice of 29 | cryptographic hash functions. 30 | 31 | Artifact Input Manifests allow consumers to reconstruct Artifact Dependency 32 | Graphs that give _fine-grained_ visibility into how artifacts in your 33 | software supply chain were made. With these graphs, consumers could 34 | in the future identify the presence of exact files associated with known 35 | vulnerabilities, side-stepping the complexities of matching version numbers 36 | across platforms and patching practicies. 37 | 38 | [__You can view the OmniBOR specification here.__][omnibor_spec] 39 | 40 | The United States Cybersecurity & Infrastructure Security Agency (CISA), 41 | identified OmniBOR as a major candidate for software identities 42 | in its 2023 report ["Software Identification Ecosystem Option 43 | Analysis."][cisa_report] 44 | 45 | ## What's in this Repository? 46 | 47 | | Crate Name | Type | Purpose | Links | 48 | |:--------------|:----------------------------------------------------------|:------------------------------------------|:----------------------------------------------------------------------------------------------------------------| 49 | | `omnibor` | ![Library](https://img.shields.io/badge/Library-darkblue) | OmniBOR Identifiers and Manifests | [README][omnibor_r] · [Changelog][omnibor_c] · [API Docs][omnibor_d] · [Crate][omnibor_cr] | 50 | | `omnibor-cli` | ![Binary](https://img.shields.io/badge/Binary-darkgreen) | CLI for OmniBOR Identifiers and Manifests | [README][omnibor_cli_r] · [Changelog][omnibor_cli_c] · [Crate][omnibor_cli_cr] | 51 | | `xtask` | ![Binary](https://img.shields.io/badge/Binary-darkgreen) | OmniBOR Rust Workspace Automation | [README][xtask_r] | 52 | 53 | ## Contributing 54 | 55 | __We happily accept contributions to any of the packages in this repository!__ 56 | 57 | All contributed commits _must_ include a Developer Certificate of Origin 58 | sign-off (use the `--signoff` flag when running `git commit`). This is checked 59 | by Continuous Integration tests to make sure you don't miss it! You can 60 | [learn more on the DCO website][dco]. 61 | 62 | Contributors do not sign any Contributor License Agreement. Your contributions 63 | remain owned by you, licensed for use in OmniBOR under the terms of the Apache 64 | 2.0 license. 65 | 66 | Check out the full [Contributing Guide][contributing] to learn more! 67 | 68 | ## Discussions & Support 69 | 70 | If you've encountered [specific bugs][bugs] or have specific 71 | [feature requests][features], we recommend opening issues in the 72 | [issue tracker][issues]! 73 | 74 | However, if you have more open-ended ideas, want to ask questions 75 | about OmniBOR or the OmniBOR Rust implementation, or want to get support 76 | debugging an issue you've encountered, we recommend opening a new 77 | [discussion][discussion]. 78 | 79 | If you believe you've found a security vulnerability, please 80 | [report it to us][vuln]. 81 | 82 | ## Security 83 | 84 | The project maintains an official [Security Policy][security] and accepts 85 | security disclosures through GitHub. 86 | 87 | ## Code of Conduct 88 | 89 | All discussions, issues, pull requests, and other communication spaces 90 | associated with this project require participants abide by the project's 91 | [Code of Conduct][coc] (Contributor Covenant 2.0). 92 | 93 | ## License 94 | 95 | All crates in this repository are Apache 2.0 licensed. You can read the full 96 | license text in the [`LICENSE`][license] file. 97 | 98 | [contributing]: CONTRIBUTING.md 99 | [cbindgen]: https://github.com/eqrion/cbindgen 100 | [cisa_report]: https://www.cisa.gov/sites/default/files/2023-10/Software-Identification-Ecosystem-Option-Analysis-508c.pdf 101 | [cpe]: https://nvd.nist.gov/products/cpe 102 | [gitoid]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects 103 | [license]: https://github.com/omnibor/omnibor-rs/blob/main/LICENSE 104 | [omnibor]: https://omnibor.io 105 | [omnibor_cr]: https://crates.io/crates/omnibor 106 | [omnibor_r]: https://github.com/omnibor/omnibor-rs/blob/main/omnibor/README.md 107 | [omnibor_c]: https://github.com/omnibor/omnibor-rs/blob/main/omnibor/CHANGELOG.md 108 | [omnibor_d]: https://docs.rs/crate/omnibor/latest 109 | [omnibor_cli_r]: https://github.com/omnibor/omnibor-rs/blob/main/omnibor-cli/README.md 110 | [omnibor_cli_c]: https://github.com/omnibor/omnibor-rs/blob/main/omnibor-cli/CHANGELOG.md 111 | [omnibor_cli_cr]: https://crates.io/crates/omnibor-cli 112 | [omnibor_spec]: https://github.com/omnibor/spec 113 | [purl]: https://github.com/package-url/purl-spec 114 | [xtask_r]: https://github.com/omnibor/omnibor-rs/blob/main/xtask/README.md 115 | [dco]: https://developercertificate.org/ 116 | [security]: https://github.com/omnibor/omnibor-rs/blob/main/SECURITY.md 117 | [coc]: https://github.com/omnibor/omnibor-rs/blob/main/CODE_OF_CONDUCT.md 118 | [bugs]: https://github.com/omnibor/omnibor-rs/issues/new?assignees=&labels=&projects=&template=bug_report.md&title= 119 | [features]: https://github.com/omnibor/omnibor-rs/issues/new?assignees=&labels=&projects=&template=feature_request.md&title= 120 | [issues]: https://github.com/omnibor/omnibor-rs/issues 121 | [discussion]: https://github.com/omnibor/omnibor-rs/discussions 122 | [vuln]: https://github.com/omnibor/omnibor-rs/security/advisories/new 123 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The following is the security policy for: 4 | 5 | - The `gitoid` library crate. 6 | - The `omnibor` library crate. 7 | - The `omnibor_cli` binary crate. 8 | 9 | All of which are found in this workspace. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Vulnerabilities can be reported using the "Report a Vulnerability" button under 14 | the security tab of the repository. If a vulnerability is found to be legitimate, 15 | a RustSec advisory will be created. 16 | 17 | Please give us 90 days to respond to a vulnerability disclosure. In general, we 18 | will try to be faster than that to produce fixes and respond publicly to 19 | disclosures. 20 | 21 | If we accept the legitimacy of a vulnerability, please wait for us to have 22 | publcily responded to the vulnerability, including publication of new versions, 23 | yanking of old versions, and public disclosure in the RustSec database, before 24 | publicly disclosing the vulnerability yourself. 25 | 26 | We ask that you _not_ create advisories yourself, but instead submit 27 | vulnerability reports to us first so we can plan a response including 28 | producing any necessary patches, publishing fixed versions, yanking affected 29 | versions, and communicating about the vulnerability to users. 30 | 31 | We consider soundness violations (violations of safe Rust's memory, thread, or 32 | type safety guarantees) to be at least informational vulnerabilities and 33 | will treat them as such. 34 | 35 | RustSec advisories are automatically imported into the GitHub Security Advisory 36 | system, and into the OSV database, so duplicate reports do not need to be made 37 | for those systems. 38 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # CI backends to support 9 | ci = "github" 10 | # The installers to generate for each app 11 | installers = ["shell", "powershell", "homebrew"] 12 | # Target platforms to build apps for (Rust target-triple syntax) 13 | targets = [ 14 | "aarch64-apple-darwin", 15 | "aarch64-unknown-linux-gnu", 16 | "aarch64-pc-windows-msvc", 17 | "x86_64-apple-darwin", 18 | "x86_64-unknown-linux-gnu", 19 | "x86_64-unknown-linux-musl", 20 | "x86_64-pc-windows-msvc", 21 | ] 22 | # Which actions to run on pull requests 23 | pr-run-mode = "plan" 24 | # Whether to install an updater program 25 | install-updater = true 26 | # Whether to enable GitHub Attestations 27 | github-attestations = true 28 | # Path that installers should place binaries in 29 | install-path = ["~/.local/bin", "~/.omnibor/bin"] 30 | 31 | # NOTE: MUST be synced manually with runners in `.github/workflows/hipcheck.yml` 32 | [dist.github-custom-runners] 33 | global = "ubuntu-22.04" 34 | # Ensure Apple Silicon macOS builds run natively rather than cross-compiling 35 | # from x86. Also makes sure our Apple Silicon macOS release builds match the 36 | # runner used for regular CI testing. 37 | aarch64-apple-darwin = "macos-14" 38 | # Update our Ubuntu release runs away from Ubuntu 20.04, which is now being 39 | # sunset by GitHub. They only track the last two LTS Ubuntu releases for free 40 | # runners, and with 24.04 out they're sunsetting 20.04. We're *just* moving to 41 | # 22.04, since releases compiled against 22.04's glibc should be forwards- 42 | # compatible with 24.04, but if we built on 24.04 the glibc *would not* be 43 | # backwards-compatible. 44 | x86_64-unknown-linux-gnu = "ubuntu-22.04" 45 | -------------------------------------------------------------------------------- /omnibor-cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.8.0] - 2025-02-04 9 | 10 | ### Changed 11 | 12 | - Big refactor. (#234) 13 | - Release omnibor-v0.8.0 14 | 15 | ## [0.7.0] - 2025-01-29 16 | 17 | ### Changed 18 | 19 | - Show help when missing args for all cmds. (#209) 20 | - Add snapshot testing for CLI. (#210) 21 | - Introduce PrintSender type (#212) 22 | - Identify files in parallel. (#214) 23 | - Added snapshot tests for `omnibor artifact id` (#213) 24 | - Parallelize 'artifact find' (#216) 25 | - Remove "buffer" tunable. (#217) 26 | - Introduce config for tunables (#218) 27 | - Turn `debug config` into `debug paths` (#219) 28 | - Added color output (#221) 29 | - Improve error reporting. (#222) 30 | - Introduce new "store" subcommand. (#223) 31 | - Make adding manifest to store optional. (#224) 32 | - Change naming of manifest file. (#226) 33 | - Support the --no-out flag on `manifest create` (#227) 34 | - Implement newline normalization. (#228) 35 | - Release omnibor-v0.7.0 36 | 37 | ## [0.7.0] - 2024-09-26 38 | 39 | ### Changed 40 | 41 | - Update omnibor-cli CHANGELOG for 0.7.0 42 | - Release omnibor-cli-v0.7.0 43 | 44 | ## [0.6.0] - 2024-09-26 45 | 46 | ### Changed 47 | 48 | - Update project and crate READMEs (#173) 49 | - Fallback to sync printing if needed. (#178) 50 | - Update `omnibor-cli/README.md` (#179) 51 | - Release omnibor-v0.6.0 52 | 53 | ### Fixed 54 | 55 | - Correct typo in CLI `README.md` 56 | 57 | ## [0.6.0] - 2024-03-08 58 | 59 | ### Changed 60 | 61 | - Split out CLI to its own package. (#171) 62 | - Update `omnibor-cli` crate CHANGELOG.md 63 | - Release omnibor-cli-v0.6.0 64 | 65 | ### Fixed 66 | 67 | - Fix broken version parsing in release (#172) 68 | 69 | [0.8.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.7.0..omnibor-v0.8.0 70 | [0.7.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-cli-v0.7.0..omnibor-v0.7.0 71 | [0.7.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.6.0..omnibor-cli-v0.7.0 72 | [0.6.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-cli-v0.6.0..omnibor-v0.6.0 73 | [0.6.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.5.1..omnibor-cli-v0.6.0 74 | 75 | 76 | -------------------------------------------------------------------------------- /omnibor-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "omnibor-cli" 4 | version = "0.8.0" 5 | 6 | description = "CLI for working with OmniBOR Identifiers and Manifests" 7 | repository = "https://github.com/omnibor/omnibor-rs" 8 | readme = "README.md" 9 | categories = ["cryptography", "development-tools"] 10 | keywords = ["gitbom", "omnibor", "sbom"] 11 | 12 | edition.workspace = true 13 | license.workspace = true 14 | homepage.workspace = true 15 | 16 | # Surprisingly, setting this on the package-specific manifest for 17 | # `omnibor-cli` configures the default-run binary for the entire 18 | # workspace. This... should probably be fixed to be properly set 19 | # on the workspace-root manifest, but for now this works. 20 | default-run = "omnibor" 21 | 22 | # Tell Cargo that the binary name should be "omnibor", 23 | # not "omnibor-cli". Otherwise it'll default to the name of the package. 24 | [[bin]] 25 | 26 | name = "omnibor" 27 | path = "src/main.rs" 28 | 29 | [dependencies] 30 | async-channel = "2.3.1" 31 | 32 | async-walkdir = "1.0.0" 33 | clap = { version = "4.5.1", features = ["derive", "env"] } 34 | clap-verbosity-flag = "2.2.2" 35 | console = "0.15.8" 36 | console-subscriber = "0.4.1" 37 | dirs = "5.0.1" 38 | dyn-clone = "1.0.17" 39 | futures-lite = "2.2.0" 40 | futures-util = "0.3.31" 41 | omnibor = { version = "0.9.0", path = "../omnibor" } 42 | pathbuf = "1.0.0" 43 | serde = { version = "1.0.215", features = ["derive"] } 44 | serde_json = "1.0.114" 45 | thiserror = "2.0.3" 46 | tokio = { version = "1.36.0", features = [ 47 | "fs", 48 | "io-std", 49 | "io-util", 50 | "macros", 51 | "rt", 52 | "rt-multi-thread", 53 | "sync", 54 | "time", 55 | "tracing", 56 | ] } 57 | tracing = "0.1.40" 58 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 59 | url = "2.5.0" 60 | 61 | [dev-dependencies] 62 | insta = { version = "1.41.1", features = ["yaml", "filters"] } 63 | insta-cmd = "0.6.0" 64 | -------------------------------------------------------------------------------- /omnibor-cli/README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | # `omnibor-cli` 5 | 6 |
7 | 8 | __Reproducible identifiers & fine-grained build dependency tracking for software artifacts.__ 9 | 10 | [![Website](https://img.shields.io/badge/website-omnibor.io-blue)](https://omnibor.io) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/omnibor/omnibor-rs/blob/main/LICENSE) 11 | 12 |
13 | 14 | This package defines an [OmniBOR] Command Line Interface, intended to enable 15 | easier generation and handling of OmniBOR identifiers and manifests. 16 | 17 | > [!NOTE] 18 | > This is currently a work-in-progress. Today, the `omnibor` CLI only supports 19 | > working with identifiers, not manifests. 20 | 21 | This command is intended to enable integration with shell scripts and other 22 | programs where the overhead of integrating directly with the `omnibor` crate 23 | through the C-language Foreign Function Interface (FFI) may not be worthwhile, 24 | and where the cost of running a shell to execute this CLI isn't problematic. 25 | 26 | ## Installation 27 | 28 | ### Install from Release (recommended) 29 | 30 | The OmniBOR CLI provides pre-built binaries for the following platforms: 31 | 32 | - Apple Silicon macOS 33 | - Intel macOS 34 | - x64 Windows 35 | - x64 Linux 36 | - x64 MUSL Linux 37 | 38 | For shell (Linux and macOS; requires `tar`, `unzip`, and either `curl` or `wget`): 39 | 40 | ```sh 41 | $ curl --proto '=https' --tlsv1.2 -LsSf "https://github.com/omnibor/omnibor-rs/releases/download/omnibor-cli-v0.6.0/omnibor-cli-installer.sh" | sh 42 | ``` 43 | 44 | For Powershell (Windows; requires `tar`, `Expand-Archive`, and `Net.Webclient`): 45 | 46 | ```powershell 47 | > powershell -c "irm https://github.com/omnibor/omnibor-rs/releases/download/omnibor-cli-v0.6.0/omnibor-cli-installer.ps1 | iex" 48 | ``` 49 | 50 | > [!NOTE] 51 | > Huge shoutout to the folks at [Axo] for making [`cargo-dist`], which makes 52 | > producing these binaries _extremely_ easy. 53 | 54 | ### Install with `cargo-binstall` 55 | 56 | You can also use [`cargo-binstall`] to install the OmniBOR CLI. This requires 57 | both `cargo` and `cargo-binstall` to be installed. 58 | 59 | ```sh 60 | $ cargo binstall omnibor-cli 61 | ``` 62 | 63 | ### Build from source (stable) 64 | 65 | You can build from source using Cargo, which requires a recent-enough 66 | Rust toolchain. We do not commit to a Minimum Supported Rust Version, 67 | and generally track stable currently. 68 | 69 | ```sh 70 | $ cargo install omnibor-cli 71 | ``` 72 | 73 | ### Build from source (unstable) 74 | 75 | Finally, you can build from the latest source in the repository itself. 76 | While we run continuous integration testing and try not to break the 77 | build in the repository, this runs a higher risk of brokenness or 78 | incompleteness of features relative to versions published to [Crates.io]. 79 | 80 | This requires `git` to check out the repository, plus a recent-enough 81 | version of Rust. We do not commit to a Minimum Support Rust Version, 82 | and generally track stable currently. 83 | 84 | ```sh 85 | # Run the following from the root of the repository after checking 86 | # the repository out with `git clone`. 87 | $ cargo install --path omnibor-cli 88 | ``` 89 | 90 | ## Examples 91 | 92 | 93 |
94 | id with Plain Format 95 | 96 | ```sh 97 | $ omnibor id Cargo.toml 98 | # Cargo.toml => gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f 99 | ``` 100 |
101 | 102 | 103 | 104 |
105 | id with JSON Format 106 | 107 | ```sh 108 | $ omnibor id Cargo.toml -f json 109 | # {"id":"gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f","path":"Cargo.toml"} 110 | ``` 111 |
112 | 113 | 114 | 115 |
116 | id with Short Format 117 | 118 | ```sh 119 | $ omnibor id Cargo.toml -f short 120 | # gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f 121 | ``` 122 |
123 | 124 | 125 | 126 |
127 | find with Plain Format 128 | 129 | ```sh 130 | $ omnibor find gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f . 131 | # gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f => ./Cargo.toml 132 | ``` 133 |
134 | 135 | 136 | 137 |
138 | find with JSON Format 139 | 140 | ```sh 141 | $ omnibor find gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f . -f json 142 | # {"id":"gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f","path":"./Cargo.toml"} 143 | ``` 144 |
145 | 146 | 147 | 148 |
149 | find with Short Format 150 | 151 | ```sh 152 | $ omnibor find gitoid:blob:sha256:c54d66281dea2bf213083f9bd3345d89dc6657fa554b1c9ef14cfe4bab14893f . -f short 153 | # ./Cargo.toml 154 | ``` 155 |
156 | 157 | ## Usage 158 | 159 |
160 | omnibor --help 161 | 162 | ``` 163 | Usage: omnibor [OPTIONS] 164 | 165 | Commands: 166 | id For files, prints their Artifact ID. For directories, recursively prints IDs for all files under it 167 | find Find file matching an Artifact ID 168 | help Print this message or the help of the given subcommand(s) 169 | 170 | Options: 171 | -b, --buffer How many print messages to buffer at one time, tunes printing perf 172 | -h, --help Print help 173 | -V, --version Print version 174 | ``` 175 |
176 | 177 |
178 | omnibor id --help 179 | 180 | ``` 181 | For files, prints their Artifact ID. For directories, recursively prints IDs for all files under it 182 | 183 | Usage: omnibor id [OPTIONS] 184 | 185 | Arguments: 186 | Path to identify 187 | 188 | Options: 189 | -f, --format Output format (can be "plain", "short", or "json") [default: plain] 190 | -H, --hash Hash algorithm (can be "sha256") [default: sha256] 191 | -h, --help Print help 192 | ``` 193 |
194 | 195 |
196 | omnibor find --help 197 | 198 | ``` 199 | Find file matching an Artifact ID 200 | 201 | Usage: omnibor find [OPTIONS] 202 | 203 | Arguments: 204 | `gitoid` URL to match 205 | The root path to search under 206 | 207 | Options: 208 | -f, --format Output format (can be "plain", "short", or "json") [default: plain] 209 | -h, --help Print help 210 | ``` 211 |
212 | 213 | ### Output Formats 214 | 215 | Both the `id` and `find` subcommand support a `-f`/`--format` flag which can be 216 | any of the following: 217 | 218 | - `plain` (default): A simple human-readable format which maps between 219 | paths and identifiers, separated by a fat arrow (`=>`). 220 | - `short`: Just prints the thing being searched for (for the `id` command, an 221 | Artifact Identifier, for the `find` command, a filesystem path). 222 | - `json`: Prints a JSON object with `path` and `id` string-type fields. 223 | 224 | The `short` format is recommended for piping or redirecting into other commands. 225 | 226 | The `json` format is recommended for more structured contexts, and can be 227 | passed to `jq` to manipulate. 228 | 229 | ## License 230 | 231 | The OmniBOR CLI source code is licensed under the Apache-2.0 license. 232 | 233 | [OmniBOR]: https://omnibor.io 234 | [release]: https://github.com/omnibor/omnibor-rs/releases 235 | [Axo]: https://axo.dev/ 236 | [`cargo-dist`]: https://github.com/axodotdev/cargo-dist 237 | [`cargo-binstall`]: https://github.com/cargo-bins/cargo-binstall 238 | -------------------------------------------------------------------------------- /omnibor-cli/config/omnibor.json: -------------------------------------------------------------------------------- 1 | { 2 | "perf": { 3 | "print_queue_size": 1, 4 | "work_queue_size": 1, 5 | "num_workers": 1 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /omnibor-cli/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{cli::Args, config::Config, print::PrintSender}; 2 | use std::fmt::Debug; 3 | 4 | pub struct App { 5 | /// The user's command line arguments. 6 | pub args: Args, 7 | 8 | /// Configuration data. 9 | pub config: Config, 10 | 11 | /// Sender for print data. 12 | pub print_tx: PrintSender, 13 | } 14 | 15 | impl Debug for App { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | f.debug_struct("App") 18 | .field("args", &self.args) 19 | .field("config", &self.config) 20 | .field("print_tx", &"") 21 | .finish() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/artifact/find.rs: -------------------------------------------------------------------------------- 1 | //! The `artifact find` command, which finds files by ID. 2 | 3 | use crate::{ 4 | app::App, 5 | cli::{FindArgs, Format, SelectedHash}, 6 | error::{Error, Result}, 7 | fs::*, 8 | print::{find_file::FindFileMsg, PrintSender, PrinterCmd}, 9 | }; 10 | use async_channel::{bounded, Receiver}; 11 | use futures_lite::stream::StreamExt as _; 12 | use futures_util::pin_mut; 13 | use std::path::PathBuf; 14 | use tokio::task::JoinSet; 15 | use tracing::debug; 16 | use url::Url; 17 | 18 | /// Run the `artifact find` subcommand. 19 | pub async fn run(app: &App, args: &FindArgs) -> Result<()> { 20 | let FindArgs { aid, path } = args; 21 | let url = aid.url(); 22 | 23 | let (sender, receiver) = bounded(app.config.perf.work_queue_size()); 24 | 25 | tokio::spawn(walk_target( 26 | sender, 27 | app.print_tx.clone(), 28 | app.args.format(), 29 | path.to_path_buf(), 30 | )); 31 | 32 | let mut join_set = JoinSet::new(); 33 | 34 | let num_workers = app.config.perf.num_workers(); 35 | debug!(num_workers = %num_workers); 36 | 37 | for _ in 0..num_workers { 38 | join_set.spawn(open_and_match_files( 39 | receiver.clone(), 40 | app.print_tx.clone(), 41 | app.args.format(), 42 | url.clone(), 43 | )); 44 | } 45 | 46 | while let Some(result) = join_set.join_next().await { 47 | result.map_err(Error::CouldNotJoinWorker)??; 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | async fn open_and_match_files( 54 | path_rx: Receiver, 55 | tx: PrintSender, 56 | format: Format, 57 | url: Url, 58 | ) -> Result<()> { 59 | pin_mut!(path_rx); 60 | 61 | while let Some(path) = path_rx.next().await { 62 | let mut file = open_async_file(&path).await?; 63 | let file_url = hash_file(SelectedHash::Sha256, &mut file, &path).await?; 64 | 65 | if url == file_url { 66 | tx.send(PrinterCmd::msg( 67 | FindFileMsg { 68 | path: path.to_path_buf(), 69 | id: url.clone(), 70 | }, 71 | format, 72 | )) 73 | .await?; 74 | } 75 | } 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/artifact/id.rs: -------------------------------------------------------------------------------- 1 | //! The `artifact id` command, which identifies files. 2 | 3 | use crate::{app::App, cli::IdArgs, error::Result, fs::*}; 4 | 5 | /// Run the `artifact id` subcommand. 6 | pub async fn run(app: &App, args: &IdArgs) -> Result<()> { 7 | let mut file = open_async_file(&args.path).await?; 8 | 9 | if file_is_dir(&file, &args.path).await? { 10 | id_directory(app, args.hash(), &app.print_tx, &args.path).await?; 11 | } else { 12 | id_file( 13 | &app.print_tx, 14 | &mut file, 15 | &args.path, 16 | app.args.format(), 17 | args.hash(), 18 | ) 19 | .await?; 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/artifact/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod find; 2 | pub mod id; 3 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/debug/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod paths; 2 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/debug/paths.rs: -------------------------------------------------------------------------------- 1 | //! The `debug config` command, which helps debug the CLI configuration. 2 | 3 | use crate::{ 4 | app::App, 5 | cli::DebugPathsArgs, 6 | error::{Error, Result}, 7 | print::{paths::PathsMsg, PrinterCmd}, 8 | }; 9 | use std::{collections::HashMap, ops::Not, path::Path}; 10 | 11 | /// Run the `debug paths` subcommand. 12 | pub async fn run(app: &App, args: &DebugPathsArgs) -> Result<()> { 13 | let root = app.args.dir().ok_or(Error::NoRoot)?.to_path_buf(); 14 | 15 | let mut to_insert: HashMap<&'static str, Option<&Path>> = HashMap::new(); 16 | to_insert.insert("dir", Some(&root)); 17 | to_insert.insert("config", app.args.config()); 18 | 19 | let mut msg = PathsMsg::new(); 20 | 21 | to_insert 22 | .into_iter() 23 | .filter(|(key, _)| { 24 | if args.keys.is_empty().not() { 25 | let key: String = key.to_string(); 26 | args.keys.contains(&key) 27 | } else { 28 | // Keep everything if there's no filter list. 29 | true 30 | } 31 | }) 32 | .for_each(|(key, path)| msg.insert(key, path)); 33 | 34 | app.print_tx 35 | .send(PrinterCmd::msg(msg, app.args.format())) 36 | .await?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/manifest/create.rs: -------------------------------------------------------------------------------- 1 | //! The `manifest create` command, which creates manifests. 2 | 3 | use crate::{ 4 | app::App, 5 | cli::ManifestCreateArgs, 6 | error::{Error, Result}, 7 | }; 8 | use omnibor::{ 9 | hash_algorithm::Sha256, 10 | hash_provider::{HashProvider, RustCrypto}, 11 | storage::{FileSystemStorage, Storage}, 12 | ArtifactId, ArtifactIdBuilder, EmbeddingMode, InputManifestBuilder, 13 | }; 14 | use pathbuf::pathbuf; 15 | use std::{ 16 | env::current_dir, 17 | fs::File, 18 | io::Write, 19 | ops::Not as _, 20 | path::{Path, PathBuf}, 21 | }; 22 | use tracing::info; 23 | 24 | /// Run the `manifest create` subcommand. 25 | pub async fn run(app: &App, args: &ManifestCreateArgs) -> Result<()> { 26 | let root = app.args.dir().ok_or(Error::NoRoot)?; 27 | let storage = 28 | FileSystemStorage::new(RustCrypto::new(), root).map_err(Error::StorageInitFailed)?; 29 | let builder = InputManifestBuilder::::new( 30 | EmbeddingMode::NoEmbed, 31 | storage, 32 | RustCrypto::new(), 33 | ); 34 | create_with_builder(args, builder)?; 35 | Ok(()) 36 | } 37 | 38 | fn create_with_builder( 39 | args: &ManifestCreateArgs, 40 | mut builder: InputManifestBuilder, 41 | ) -> Result<()> 42 | where 43 | P: HashProvider, 44 | S: Storage, 45 | { 46 | for input in &args.inputs { 47 | let aid = input.clone().into_artifact_id().map_err(Error::IdFailed)?; 48 | builder 49 | .add_relation(aid) 50 | .map_err(Error::AddRelationFailed)?; 51 | } 52 | 53 | let linked_manifest = builder 54 | .finish(&args.target) 55 | .map_err(Error::ManifestBuildFailed)?; 56 | 57 | let manifest_aid = ArtifactIdBuilder::with_rustcrypto().identify_manifest(&linked_manifest); 58 | 59 | if args.no_out.not() { 60 | let target_aid = ArtifactIdBuilder::with_rustcrypto() 61 | .identify_path(&args.target) 62 | .map_err(|source| Error::FileFailedToId { 63 | path: args.target.clone(), 64 | source, 65 | })?; 66 | 67 | let path = manifest_file_path(args.output.as_deref(), target_aid)?; 68 | 69 | let mut output_file = match File::create_new(&path) { 70 | Ok(file) => file, 71 | Err(source) => { 72 | let mut existing_file = File::open(&path).unwrap(); 73 | let existing_file_aid = ArtifactIdBuilder::with_rustcrypto() 74 | .identify_file(&mut existing_file) 75 | .unwrap(); 76 | if existing_file_aid == manifest_aid { 77 | info!("matching manifest already found at '{}'", path.display()); 78 | return Ok(()); 79 | } else { 80 | return Err(Error::CantWriteManifest { 81 | path: path.to_path_buf(), 82 | source, 83 | }); 84 | } 85 | } 86 | }; 87 | 88 | output_file 89 | // SAFETY: We just constructed the manifest, so we know it's fine. 90 | .write_all(&linked_manifest.as_bytes()) 91 | .map_err(|source| Error::CantWriteManifest { 92 | path: path.to_path_buf(), 93 | source, 94 | })?; 95 | 96 | info!("wrote manifest '{}' to '{}'", manifest_aid, path.display()); 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | fn manifest_file_path(output: Option<&Path>, target_aid: ArtifactId) -> Result { 103 | let dir = match &output { 104 | Some(dir) => dir.to_path_buf(), 105 | None => match current_dir() { 106 | Ok(dir) => dir, 107 | Err(_) => return Err(Error::NoOutputDir), 108 | }, 109 | }; 110 | 111 | Ok(pathbuf![&dir, &target_aid.as_file_name()]) 112 | } 113 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/manifest/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defines individual subcommands. 2 | 3 | pub mod artifact; 4 | pub mod debug; 5 | pub mod manifest; 6 | pub mod store; 7 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/store/add.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::App, cli::StoreAddArgs, error::Result}; 2 | 3 | /// Run the `store add` subcommand. 4 | pub async fn run(_app: &App, _args: &StoreAddArgs) -> Result<()> { 5 | todo!() 6 | } 7 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/store/log.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::App, cli::StoreLogArgs, error::Result}; 2 | 3 | /// Run the `store log` subcommand. 4 | pub async fn run(_app: &App, _args: &StoreLogArgs) -> Result<()> { 5 | todo!() 6 | } 7 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/store/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add; 2 | pub mod log; 3 | pub mod remove; 4 | -------------------------------------------------------------------------------- /omnibor-cli/src/cmd/store/remove.rs: -------------------------------------------------------------------------------- 1 | use crate::{app::App, cli::StoreRemoveArgs, error::Result}; 2 | 3 | /// Run the `store remove` subcommand. 4 | pub async fn run(_app: &App, _args: &StoreRemoveArgs) -> Result<()> { 5 | todo!() 6 | } 7 | -------------------------------------------------------------------------------- /omnibor-cli/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::DEFAULT_CONFIG, 3 | error::{Error, Result}, 4 | }; 5 | use serde::Deserialize; 6 | use std::{fs::File, io::Read, path::Path}; 7 | use tokio::runtime::Handle; 8 | use tracing::debug; 9 | 10 | #[derive(Debug, Default, Deserialize)] 11 | pub struct Config { 12 | #[serde(default)] 13 | pub perf: PerfConfig, 14 | } 15 | 16 | impl Config { 17 | pub fn init(path: Option<&Path>) -> Result { 18 | let Some(path) = path else { 19 | debug!("no config path provided"); 20 | return Ok(Config::default()); 21 | }; 22 | 23 | let mut file = match File::open(path) { 24 | Ok(file) => file, 25 | Err(error) => { 26 | // If we simply didn't find the default file, that's fine. It's 27 | // allowed to be missing, we just use the default config. 28 | if file_was_not_found(&error) && is_default_path(path) { 29 | return Ok(Config::default()); 30 | } 31 | 32 | match (file_was_not_found(&error), is_default_path(path)) { 33 | // Not found, is default. 34 | (true, true) => return Ok(Config::default()), 35 | // Not found, is not default. 36 | (true, false) => { 37 | return Err(Error::ConfigNotFound { 38 | path: path.to_path_buf(), 39 | source: error, 40 | }); 41 | } 42 | // Found, is default. 43 | (false, true) => { 44 | return Err(Error::ConfigDefaultCouldNotRead { 45 | path: path.to_path_buf(), 46 | source: error, 47 | }) 48 | } 49 | // Found, is not default. 50 | (false, false) => { 51 | return Err(Error::ConfigCouldNotRead { 52 | path: path.to_path_buf(), 53 | source: error, 54 | }) 55 | } 56 | } 57 | } 58 | }; 59 | 60 | let mut config_contents = String::new(); 61 | file.read_to_string(&mut config_contents) 62 | .map_err(|source| Error::ConfigCouldNotRead { 63 | path: path.to_path_buf(), 64 | source, 65 | })?; 66 | 67 | // If the file exists, we can read it, but it's all whitespace, just 68 | // ignore it and use the default. 69 | if config_contents.chars().all(|c| c.is_whitespace()) { 70 | return Ok(Config::default()); 71 | } 72 | 73 | let config = serde_json::from_str(&config_contents).map_err(Error::CantReadConfig)?; 74 | Ok(config) 75 | } 76 | } 77 | 78 | fn file_was_not_found(error: &std::io::Error) -> bool { 79 | matches!(error.kind(), std::io::ErrorKind::NotFound) 80 | } 81 | 82 | fn is_default_path(path: &Path) -> bool { 83 | let Some(Some(default_path)) = DEFAULT_CONFIG.get() else { 84 | return false; 85 | }; 86 | 87 | path == default_path 88 | } 89 | 90 | #[derive(Debug, Default, Deserialize)] 91 | pub struct PerfConfig { 92 | /// The max number of print items that can be held in the print queue. 93 | print_queue_size: PrintQueueSize, 94 | 95 | /// The max number of work items that can be held in the work queue. 96 | work_queue_size: WorkQueueSize, 97 | 98 | /// The number of worker tasks to spawn. 99 | num_workers: NumWorkers, 100 | } 101 | 102 | impl PerfConfig { 103 | pub fn print_queue_size(&self) -> usize { 104 | self.print_queue_size.0 105 | } 106 | 107 | pub fn work_queue_size(&self) -> usize { 108 | self.work_queue_size.0 109 | } 110 | 111 | pub fn num_workers(&self) -> usize { 112 | self.num_workers.0 113 | } 114 | } 115 | 116 | #[derive(Debug, Deserialize)] 117 | #[serde(transparent)] 118 | pub struct PrintQueueSize(usize); 119 | 120 | impl Default for PrintQueueSize { 121 | fn default() -> Self { 122 | PrintQueueSize(100) 123 | } 124 | } 125 | 126 | #[derive(Debug, Deserialize)] 127 | #[serde(transparent)] 128 | pub struct WorkQueueSize(usize); 129 | 130 | impl Default for WorkQueueSize { 131 | fn default() -> Self { 132 | WorkQueueSize(100) 133 | } 134 | } 135 | 136 | #[derive(Debug, Deserialize)] 137 | #[serde(transparent)] 138 | pub struct NumWorkers(usize); 139 | 140 | impl Default for NumWorkers { 141 | fn default() -> Self { 142 | let num = Handle::current().metrics().num_workers() - 1; 143 | NumWorkers(num) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /omnibor-cli/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types. 2 | 3 | use async_channel::SendError; 4 | use omnibor::error::{ArtifactIdError, InputManifestError}; 5 | use serde_json::Error as JsonError; 6 | use std::{io::Error as IoError, path::PathBuf, result::Result as StdResult}; 7 | use tokio::task::JoinError; 8 | 9 | #[derive(Debug, thiserror::Error)] 10 | pub enum Error { 11 | #[error("could not identify '{0}'")] 12 | NotIdentifiable(String), 13 | 14 | #[error("could not find root directory")] 15 | NoRoot, 16 | 17 | #[error("failed to initialize file system storage")] 18 | StorageInitFailed(#[source] InputManifestError), 19 | 20 | #[error("failed to generate Artifact ID")] 21 | IdFailed(#[source] ArtifactIdError), 22 | 23 | #[error("failed to add relation to Input Manifest")] 24 | AddRelationFailed(#[source] InputManifestError), 25 | 26 | #[error("failed to build Input Manifest")] 27 | ManifestBuildFailed(#[source] InputManifestError), 28 | 29 | #[error("failed to write to stdout")] 30 | StdoutWriteFailed(#[source] IoError), 31 | 32 | #[error("failed to write to stderr")] 33 | StderrWriteFailed(#[source] IoError), 34 | 35 | #[error("failed walking under directory '{}'", path.display())] 36 | WalkDirFailed { path: PathBuf, source: IoError }, 37 | 38 | #[error("unable to identify file type for '{}'", path.display())] 39 | UnknownFileType { 40 | path: PathBuf, 41 | #[source] 42 | source: IoError, 43 | }, 44 | 45 | #[error("failed to open file '{}'", path.display())] 46 | FileFailedToOpen { 47 | path: PathBuf, 48 | #[source] 49 | source: IoError, 50 | }, 51 | 52 | #[error("failed to get file metadata '{}'", path.display())] 53 | FileFailedMetadata { 54 | path: PathBuf, 55 | #[source] 56 | source: IoError, 57 | }, 58 | 59 | #[error("failed to make Artifact ID for '{}'", path.display())] 60 | FileFailedToId { 61 | path: PathBuf, 62 | #[source] 63 | source: ArtifactIdError, 64 | }, 65 | 66 | #[error("can't identify directory to write manifest")] 67 | NoOutputDir, 68 | 69 | #[error("can't write manifest to '{}'", path.display())] 70 | CantWriteManifest { path: PathBuf, source: IoError }, 71 | 72 | #[error("work channel closed for sending")] 73 | WorkChannelCloseSend(#[source] SendError), 74 | 75 | #[error("failed to join worker task")] 76 | CouldNotJoinWorker(#[source] JoinError), 77 | 78 | #[error("print channel closed")] 79 | PrintChannelClose, 80 | 81 | #[error("did not find configuration file '{}'", path.display())] 82 | ConfigNotFound { 83 | path: PathBuf, 84 | #[source] 85 | source: IoError, 86 | }, 87 | 88 | #[error("could not read default configuration file '{}'", path.display())] 89 | ConfigDefaultCouldNotRead { 90 | path: PathBuf, 91 | #[source] 92 | source: IoError, 93 | }, 94 | 95 | #[error("could not read configuration file '{}'", path.display())] 96 | ConfigCouldNotRead { 97 | path: PathBuf, 98 | #[source] 99 | source: IoError, 100 | }, 101 | 102 | #[error("can't read configuration file")] 103 | CantReadConfig(#[source] JsonError), 104 | } 105 | 106 | pub type Result = StdResult; 107 | -------------------------------------------------------------------------------- /omnibor-cli/src/fs.rs: -------------------------------------------------------------------------------- 1 | //! File system helper operations. 2 | 3 | use crate::{ 4 | app::App, 5 | cli::{Format, SelectedHash}, 6 | error::{Error, Result}, 7 | print::{error::ErrorMsg, id_file::IdFileMsg, PrintSender, PrinterCmd}, 8 | }; 9 | use async_channel::{bounded, Receiver, Sender as WorkSender}; 10 | use async_walkdir::{DirEntry as AsyncDirEntry, WalkDir}; 11 | use futures_util::{pin_mut, StreamExt}; 12 | use omnibor::{hash_algorithm::Sha256, ArtifactId, ArtifactIdBuilder}; 13 | use std::path::{Path, PathBuf}; 14 | use tokio::{fs::File as AsyncFile, task::JoinSet}; 15 | use tracing::debug; 16 | use url::Url; 17 | 18 | // Identify, recursively, all the files under a directory. 19 | pub async fn id_directory( 20 | app: &App, 21 | hash: SelectedHash, 22 | tx: &PrintSender, 23 | path: &Path, 24 | ) -> Result<()> { 25 | let (sender, receiver) = bounded(app.config.perf.work_queue_size()); 26 | 27 | tokio::spawn(walk_target( 28 | sender, 29 | tx.clone(), 30 | app.args.format(), 31 | path.to_path_buf(), 32 | )); 33 | 34 | let mut join_set = JoinSet::new(); 35 | 36 | // TODO: Make this tunable on the CLI, with the logic here as a fallback. 37 | // Subtract 1, since we've spawned one task separately. 38 | let num_workers = tokio::runtime::Handle::current().metrics().num_workers() - 1; 39 | 40 | debug!(num_workers = %num_workers); 41 | 42 | for _ in 0..num_workers { 43 | join_set.spawn(open_and_id_files( 44 | receiver.clone(), 45 | tx.clone(), 46 | app.args.format(), 47 | hash, 48 | )); 49 | } 50 | 51 | while let Some(result) = join_set.join_next().await { 52 | result.map_err(Error::CouldNotJoinWorker)??; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | /// Walk the target path structure, printing errors and sending discovered 59 | /// paths out to workers. 60 | pub async fn walk_target( 61 | path_sender: WorkSender, 62 | print_tx: PrintSender, 63 | format: Format, 64 | path: PathBuf, 65 | ) -> Result<()> { 66 | let mut entries = WalkDir::new(&path); 67 | 68 | loop { 69 | match entries.next().await { 70 | None => break Ok(()), 71 | Some(Err(source)) => { 72 | print_tx 73 | .send(PrinterCmd::msg( 74 | ErrorMsg::new(Error::WalkDirFailed { 75 | path: path.to_path_buf(), 76 | source, 77 | }), 78 | format, 79 | )) 80 | .await? 81 | } 82 | Some(Ok(entry)) => { 83 | let path = &entry.path(); 84 | 85 | if entry_is_dir(&entry).await? { 86 | continue; 87 | } 88 | 89 | path_sender 90 | .send(path.clone()) 91 | .await 92 | .map_err(Error::WorkChannelCloseSend)?; 93 | } 94 | } 95 | } 96 | } 97 | 98 | /// Listen on the path receiver and identify each file found. 99 | /// 100 | /// The semantics of the channel being used mean each path sent will only 101 | /// be received by one receiver. 102 | async fn open_and_id_files( 103 | path_rx: Receiver, 104 | print_tx: PrintSender, 105 | format: Format, 106 | hash: SelectedHash, 107 | ) -> Result<()> { 108 | pin_mut!(path_rx); 109 | 110 | while let Some(path) = path_rx.next().await { 111 | let mut file = open_async_file(&path).await?; 112 | id_file(&print_tx, &mut file, &path, format, hash).await?; 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | /// Identify a single file. 119 | pub async fn id_file( 120 | tx: &PrintSender, 121 | file: &mut AsyncFile, 122 | path: &Path, 123 | format: Format, 124 | hash: SelectedHash, 125 | ) -> Result<()> { 126 | let url = hash_file(hash, file, path).await?; 127 | 128 | tx.send(PrinterCmd::msg( 129 | IdFileMsg { 130 | path: path.to_path_buf(), 131 | id: url.clone(), 132 | }, 133 | format, 134 | )) 135 | .await?; 136 | 137 | Ok(()) 138 | } 139 | 140 | /// Hash the file and produce a `gitoid`-scheme URL. 141 | pub async fn hash_file(hash: SelectedHash, file: &mut AsyncFile, path: &Path) -> Result { 142 | match hash { 143 | SelectedHash::Sha256 => sha256_id_async_file(file, path).await.map(|id| id.url()), 144 | } 145 | } 146 | 147 | /// Check if the file is for a directory. 148 | pub async fn file_is_dir(file: &AsyncFile, path: &Path) -> Result { 149 | file.metadata() 150 | .await 151 | .map(|meta| meta.is_dir()) 152 | .map_err(|source| Error::FileFailedMetadata { 153 | path: path.to_path_buf(), 154 | source, 155 | }) 156 | } 157 | 158 | /// Check if the entry is for a directory. 159 | pub async fn entry_is_dir(entry: &AsyncDirEntry) -> Result { 160 | entry 161 | .file_type() 162 | .await 163 | .map(|file_type| file_type.is_dir()) 164 | .map_err(|source| Error::UnknownFileType { 165 | path: entry.path(), 166 | source, 167 | }) 168 | } 169 | 170 | /// Open an asynchronous file. 171 | pub async fn open_async_file(path: &Path) -> Result { 172 | AsyncFile::open(path) 173 | .await 174 | .map_err(|source| Error::FileFailedToOpen { 175 | path: path.to_path_buf(), 176 | source, 177 | }) 178 | } 179 | 180 | /// Identify a file using a SHA-256 hash. 181 | pub async fn sha256_id_async_file(file: &mut AsyncFile, path: &Path) -> Result> { 182 | ArtifactIdBuilder::with_rustcrypto() 183 | .identify_async_file(file) 184 | .await 185 | .map_err(|source| Error::FileFailedToId { 186 | path: path.to_path_buf(), 187 | source, 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /omnibor-cli/src/log.rs: -------------------------------------------------------------------------------- 1 | //! Functions for initializing logging. 2 | 3 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 4 | use tracing::Subscriber; 5 | use tracing_subscriber::{ 6 | filter::EnvFilter, layer::SubscriberExt as _, registry::LookupSpan, 7 | util::SubscriberInitExt as _, Layer, 8 | }; 9 | 10 | // The environment variable to use when configuring the log. 11 | const LOG_VAR: &str = "OMNIBOR_LOG"; 12 | 13 | pub fn init_log(verbosity: Verbosity, console: bool) { 14 | let level_filter = adapt_level_filter(verbosity.log_level_filter()); 15 | let filter = EnvFilter::from_env(LOG_VAR).add_directive(level_filter.into()); 16 | let fmt_layer = fmt_layer(filter); 17 | let registry = tracing_subscriber::registry().with(fmt_layer); 18 | 19 | if console { 20 | let console_layer = console_subscriber::spawn(); 21 | registry.with(console_layer).init(); 22 | } else { 23 | registry.init() 24 | } 25 | } 26 | 27 | fn fmt_layer(filter: EnvFilter) -> impl Layer 28 | where 29 | S: Subscriber + for<'span> LookupSpan<'span>, 30 | { 31 | tracing_subscriber::fmt::layer() 32 | .event_format( 33 | tracing_subscriber::fmt::format() 34 | .with_level(true) 35 | .without_time() 36 | .with_target(false) 37 | .with_thread_ids(false) 38 | .with_thread_names(false) 39 | .compact(), 40 | ) 41 | .with_filter(filter) 42 | } 43 | 44 | /// Convert the clap LevelFilter to the tracing LevelFilter. 45 | fn adapt_level_filter( 46 | clap_filter: clap_verbosity_flag::LevelFilter, 47 | ) -> tracing_subscriber::filter::LevelFilter { 48 | match clap_filter { 49 | clap_verbosity_flag::LevelFilter::Off => tracing_subscriber::filter::LevelFilter::OFF, 50 | clap_verbosity_flag::LevelFilter::Error => tracing_subscriber::filter::LevelFilter::ERROR, 51 | clap_verbosity_flag::LevelFilter::Warn => tracing_subscriber::filter::LevelFilter::WARN, 52 | clap_verbosity_flag::LevelFilter::Info => tracing_subscriber::filter::LevelFilter::INFO, 53 | clap_verbosity_flag::LevelFilter::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, 54 | clap_verbosity_flag::LevelFilter::Trace => tracing_subscriber::filter::LevelFilter::TRACE, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /omnibor-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod cli; 3 | mod cmd; 4 | mod config; 5 | mod error; 6 | mod fs; 7 | mod log; 8 | mod print; 9 | 10 | use crate::{ 11 | app::App, 12 | cli::{Args, ArtifactCommand, Command, DebugCommand, ManifestCommand, StoreCommand}, 13 | cmd::{artifact, debug, manifest, store}, 14 | config::Config, 15 | error::Result, 16 | log::init_log, 17 | print::{error::ErrorMsg, Printer, PrinterCmd}, 18 | }; 19 | use clap::Parser as _; 20 | use std::{error::Error as StdError, process::ExitCode}; 21 | use tokio::runtime::Runtime; 22 | use tracing::{error, trace}; 23 | 24 | fn main() -> ExitCode { 25 | let runtime = Runtime::new().expect("runtime construction succeeds"); 26 | runtime.block_on(async { run().await }) 27 | } 28 | 29 | async fn run() -> ExitCode { 30 | let args = Args::parse(); 31 | init_log(args.verbosity(), args.console()); 32 | 33 | let config = match Config::init(args.config()) { 34 | Ok(config) => config, 35 | Err(error) => { 36 | log_error(&error); 37 | return ExitCode::FAILURE; 38 | } 39 | }; 40 | 41 | let printer = Printer::launch(config.perf.print_queue_size()); 42 | 43 | let app = App { 44 | args, 45 | config, 46 | print_tx: printer.tx().clone(), 47 | }; 48 | trace!(app = ?app); 49 | 50 | let exit_code = match run_cmd(&app).await { 51 | Ok(_) => ExitCode::SUCCESS, 52 | Err(e) => { 53 | printer 54 | .send(PrinterCmd::msg(ErrorMsg::new(e), app.args.format())) 55 | .await; 56 | ExitCode::FAILURE 57 | } 58 | }; 59 | 60 | // Ensure we always send the "End" printer command. 61 | app.print_tx.send(PrinterCmd::End).await.unwrap(); 62 | printer.join().await; 63 | exit_code 64 | } 65 | 66 | /// Select and run the chosen command. 67 | async fn run_cmd(app: &App) -> Result<()> { 68 | match app.args.command() { 69 | Command::Artifact(ref args) => match args.command { 70 | ArtifactCommand::Id(ref args) => artifact::id::run(app, args).await, 71 | ArtifactCommand::Find(ref args) => artifact::find::run(app, args).await, 72 | }, 73 | Command::Manifest(ref args) => match args.command { 74 | ManifestCommand::Create(ref args) => manifest::create::run(app, args).await, 75 | }, 76 | Command::Store(ref args) => match args.command { 77 | StoreCommand::Add(ref args) => store::add::run(app, args).await, 78 | StoreCommand::Remove(ref args) => store::remove::run(app, args).await, 79 | StoreCommand::Log(ref args) => store::log::run(app, args).await, 80 | }, 81 | Command::Debug(ref args) => match args.command { 82 | DebugCommand::Paths(ref args) => debug::paths::run(app, args).await, 83 | }, 84 | } 85 | } 86 | 87 | fn log_error(error: &dyn StdError) { 88 | error!("{}", error); 89 | 90 | if let Some(child) = error.source() { 91 | log_error(child); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /omnibor-cli/src/print/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::Error, 3 | print::{CommandOutput, Status}, 4 | }; 5 | use console::Style; 6 | use serde_json::json; 7 | use std::sync::{Arc, Mutex}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct ErrorMsg { 11 | pub error: Arc>, 12 | } 13 | 14 | impl ErrorMsg { 15 | pub fn new(error: Error) -> Self { 16 | ErrorMsg { 17 | error: Arc::new(Mutex::new(error)), 18 | } 19 | } 20 | 21 | fn error_string(&self) -> String { 22 | // SAFETY: This error type should only have a singular owner anyway. 23 | self.error.lock().unwrap().to_string() 24 | } 25 | } 26 | 27 | impl CommandOutput for ErrorMsg { 28 | fn plain_output(&self) -> String { 29 | format!( 30 | "{}: {}", 31 | Style::new().red().apply_to("error"), 32 | self.error_string() 33 | ) 34 | } 35 | 36 | fn short_output(&self) -> String { 37 | self.error_string() 38 | } 39 | 40 | fn json_output(&self) -> serde_json::Value { 41 | json!({"error": self.error_string()}) 42 | } 43 | 44 | fn status(&self) -> Status { 45 | Status::Error 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /omnibor-cli/src/print/find_file.rs: -------------------------------------------------------------------------------- 1 | use crate::print::{CommandOutput, Status}; 2 | use console::Style; 3 | use serde_json::json; 4 | use std::path::PathBuf; 5 | use url::Url; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct FindFileMsg { 9 | pub path: PathBuf, 10 | pub id: Url, 11 | } 12 | 13 | impl FindFileMsg { 14 | fn path_string(&self) -> String { 15 | self.path.display().to_string() 16 | } 17 | 18 | fn id_string(&self) -> String { 19 | self.id.to_string() 20 | } 21 | } 22 | 23 | impl CommandOutput for FindFileMsg { 24 | fn plain_output(&self) -> String { 25 | format!( 26 | "{} {} {}", 27 | Style::new().blue().bold().apply_to(self.id_string()), 28 | Style::new().dim().apply_to("=>"), 29 | self.path_string() 30 | ) 31 | } 32 | 33 | fn short_output(&self) -> String { 34 | self.path_string() 35 | } 36 | 37 | fn json_output(&self) -> serde_json::Value { 38 | json!({"path": self.path_string(), "id": self.id_string()}) 39 | } 40 | 41 | fn status(&self) -> Status { 42 | Status::Success 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /omnibor-cli/src/print/id_file.rs: -------------------------------------------------------------------------------- 1 | use crate::print::{CommandOutput, Status}; 2 | use console::Style; 3 | use serde_json::json; 4 | use std::path::PathBuf; 5 | use url::Url; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct IdFileMsg { 9 | pub path: PathBuf, 10 | pub id: Url, 11 | } 12 | 13 | impl IdFileMsg { 14 | fn path_string(&self) -> String { 15 | self.path.display().to_string() 16 | } 17 | 18 | fn id_string(&self) -> String { 19 | self.id.to_string() 20 | } 21 | } 22 | 23 | impl CommandOutput for IdFileMsg { 24 | fn plain_output(&self) -> String { 25 | format!( 26 | "{} {} {}", 27 | Style::new().blue().bold().apply_to(self.path_string()), 28 | Style::new().dim().apply_to("=>"), 29 | self.id_string() 30 | ) 31 | } 32 | 33 | fn short_output(&self) -> String { 34 | self.id_string() 35 | } 36 | 37 | fn json_output(&self) -> serde_json::Value { 38 | json!({"path": self.path_string(), "id": self.id_string()}) 39 | } 40 | 41 | fn status(&self) -> Status { 42 | Status::Success 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /omnibor-cli/src/print/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defines a simple print queue abstraction. 2 | 3 | pub mod error; 4 | pub mod find_file; 5 | pub mod id_file; 6 | pub mod paths; 7 | 8 | use crate::{ 9 | cli::Format, 10 | error::{Error, Result}, 11 | }; 12 | use dyn_clone::{clone_box, DynClone}; 13 | use error::ErrorMsg; 14 | use serde_json::Value as JsonValue; 15 | use std::{ 16 | fmt::Debug, 17 | future::Future, 18 | io::Write, 19 | ops::{Deref, Not}, 20 | panic, 21 | result::Result as StdResult, 22 | }; 23 | use tokio::{ 24 | sync::mpsc::{self, Sender}, 25 | task::JoinError, 26 | }; 27 | use tracing::{debug, error}; 28 | 29 | /// A handle to assist in interacting with the printer. 30 | pub struct Printer { 31 | /// The transmitter to send message to the task. 32 | tx: PrintSender, 33 | 34 | /// The actual future to be awaited. 35 | task: Box> + Unpin>, 36 | } 37 | 38 | impl Printer { 39 | /// Launch the print queue task, give back sender and future for it. 40 | pub fn launch(buffer_size: usize) -> Printer { 41 | let (tx, mut rx) = mpsc::channel::(buffer_size); 42 | 43 | let printer = tokio::task::spawn_blocking(move || { 44 | while let Some(msg) = rx.blocking_recv() { 45 | debug!(msg = ?msg); 46 | 47 | match msg { 48 | // Closing the stream ensures it still drains if there are messages in flight. 49 | PrinterCmd::End => rx.close(), 50 | PrinterCmd::Message { output, format } => { 51 | let status = output.status(); 52 | let output = output.format(format); 53 | 54 | if let Err(error) = sync_print(status, output.clone()) { 55 | let err_output = ErrorMsg::new(error).format(format); 56 | 57 | if let Err(err) = sync_print(Status::Error, err_output) { 58 | error!(msg = "failed to print sync error message", error = %err); 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }); 65 | 66 | Printer { 67 | tx: PrintSender(tx), 68 | task: Box::new(printer), 69 | } 70 | } 71 | 72 | /// Send a message to the print task. 73 | pub async fn send(&self, cmd: PrinterCmd) { 74 | if let Err(e) = self.tx.send(cmd.clone()).await { 75 | error!(msg = "failed to send printer cmd", cmd = ?cmd, error = %e); 76 | } 77 | } 78 | 79 | /// Wait on the underlying task. 80 | /// 81 | /// This function waits, and then either returns normally or panics. 82 | pub async fn join(self) { 83 | if let Err(error) = self.task.await { 84 | // If the print task panicked, the whole task should panic. 85 | if error.is_panic() { 86 | panic::resume_unwind(error.into_panic()) 87 | } 88 | 89 | if error.is_cancelled() { 90 | error!(msg = "the printer task was cancelled unexpectedly"); 91 | } 92 | } 93 | } 94 | 95 | /// Get a reference to the task transmitter. 96 | pub fn tx(&self) -> &PrintSender { 97 | &self.tx 98 | } 99 | } 100 | 101 | pub struct PrintSender(Sender); 102 | 103 | impl PrintSender { 104 | pub async fn send(&self, value: PrinterCmd) -> Result<()> { 105 | self.0 106 | .send(value) 107 | .await 108 | .map_err(|_| Error::PrintChannelClose) 109 | } 110 | } 111 | 112 | impl Clone for PrintSender { 113 | fn clone(&self) -> Self { 114 | PrintSender(self.0.clone()) 115 | } 116 | } 117 | 118 | /// A print queue message, either an actual message or a signals to end printing. 119 | #[derive(Debug, Clone)] 120 | pub enum PrinterCmd { 121 | /// Shut down the printer task. 122 | End, 123 | 124 | /// Print the following message. 125 | Message { output: Msg, format: Format }, 126 | } 127 | 128 | impl PrinterCmd { 129 | /// Construct a new ID printer command. 130 | pub fn msg(output: C, format: Format) -> Self { 131 | PrinterCmd::Message { 132 | output: Msg::new(output), 133 | format, 134 | } 135 | } 136 | } 137 | 138 | #[derive(Debug)] 139 | pub struct Msg(Box); 140 | 141 | impl Clone for Msg { 142 | fn clone(&self) -> Self { 143 | Msg(clone_box(self.0.deref())) 144 | } 145 | } 146 | 147 | impl Deref for Msg { 148 | type Target = Box; 149 | 150 | fn deref(&self) -> &Self::Target { 151 | &self.0 152 | } 153 | } 154 | 155 | impl Msg { 156 | fn new(output: C) -> Self { 157 | Msg(Box::new(output)) 158 | } 159 | } 160 | 161 | /// Trait representing the different possible outputs that can arise. 162 | pub trait CommandOutput: Debug + DynClone + Send + 'static { 163 | fn plain_output(&self) -> String; 164 | fn short_output(&self) -> String; 165 | fn json_output(&self) -> JsonValue; 166 | fn status(&self) -> Status; 167 | 168 | fn format(&self, format: Format) -> String { 169 | let mut output = match format { 170 | Format::Plain => self.plain_output(), 171 | // SAFETY: serde_json::Value can always be converted to a string. 172 | Format::Json => serde_json::to_string(&self.json_output()).unwrap(), 173 | Format::Short => self.short_output(), 174 | }; 175 | 176 | if output.ends_with('\n').not() { 177 | output.push('\n'); 178 | } 179 | 180 | output 181 | } 182 | } 183 | 184 | /// Print the contents of the message synchronously. 185 | fn sync_print(status: Status, output: String) -> Result<()> { 186 | let bytes = output.as_bytes(); 187 | 188 | match status { 189 | Status::Success => std::io::stdout() 190 | .write_all(bytes) 191 | .map_err(Error::StdoutWriteFailed)?, 192 | Status::Error => std::io::stderr() 193 | .write_all(bytes) 194 | .map_err(Error::StderrWriteFailed)?, 195 | } 196 | 197 | Ok(()) 198 | } 199 | 200 | /// Whether the message is a success or error. 201 | #[derive(Debug, Clone, Copy)] 202 | pub enum Status { 203 | Success, 204 | Error, 205 | } 206 | -------------------------------------------------------------------------------- /omnibor-cli/src/print/paths.rs: -------------------------------------------------------------------------------- 1 | use crate::print::{CommandOutput, Status}; 2 | use console::Style; 3 | use serde_json::json; 4 | use std::{ 5 | collections::BTreeMap, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct PathsMsg { 11 | data: BTreeMap>, 12 | } 13 | 14 | impl PathsMsg { 15 | pub fn new() -> Self { 16 | PathsMsg { 17 | data: BTreeMap::new(), 18 | } 19 | } 20 | 21 | pub fn insert(&mut self, name: &'static str, path: Option<&Path>) { 22 | self.data 23 | .insert(name.to_string(), path.map(ToOwned::to_owned)); 24 | } 25 | } 26 | 27 | fn opt_path(path: &Option) -> String { 28 | path.as_deref() 29 | .map(|path| path.display().to_string()) 30 | .unwrap_or_else(|| String::from("None")) 31 | } 32 | 33 | impl CommandOutput for PathsMsg { 34 | fn plain_output(&self) -> String { 35 | let pad_width = self.data.keys().map(|key| key.len()).max().unwrap_or(10) + 2; 36 | 37 | self.data 38 | .iter() 39 | .fold(String::new(), |mut output, (name, path)| { 40 | output.push_str(&format!( 41 | "{:>width$}: {}\n", 42 | Style::new().blue().bold().apply_to(name), 43 | opt_path(path), 44 | width = pad_width, 45 | )); 46 | output 47 | }) 48 | } 49 | 50 | fn short_output(&self) -> String { 51 | self.data.values().fold(String::new(), |mut output, path| { 52 | output.push_str(&format!("{}\n", opt_path(path))); 53 | output 54 | }) 55 | } 56 | 57 | fn json_output(&self) -> serde_json::Value { 58 | self.data 59 | .iter() 60 | .fold(serde_json::Map::new(), |mut map, (name, path)| { 61 | map.insert(name.to_string(), json!(opt_path(path))); 62 | map 63 | }) 64 | .into() 65 | } 66 | 67 | fn status(&self) -> Status { 68 | Status::Success 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /omnibor-cli/tests/data/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("Hello, world!\n"); 5 | } 6 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__artifact_id_json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: 6 | - artifact 7 | - id 8 | - "--format" 9 | - json 10 | - "--path" 11 | - tests/data/main.c 12 | --- 13 | success: true 14 | exit_code: 0 15 | ----- stdout ----- 16 | {"id":"gitoid:blob:sha256:93561f4501717b4c4a2f3eb5776f03231d32ec2a1f709a611ad3d8dcf931dc1b","path":"tests/data/main.c"} 17 | 18 | ----- stderr ----- 19 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__artifact_id_plain.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: 6 | - artifact 7 | - id 8 | - "--format" 9 | - plain 10 | - "--path" 11 | - tests/data/main.c 12 | --- 13 | success: true 14 | exit_code: 0 15 | ----- stdout ----- 16 | tests/data/main.c => gitoid:blob:sha256:93561f4501717b4c4a2f3eb5776f03231d32ec2a1f709a611ad3d8dcf931dc1b 17 | 18 | ----- stderr ----- 19 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__artifact_id_short.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: 6 | - artifact 7 | - id 8 | - "--format" 9 | - short 10 | - "--path" 11 | - tests/data/main.c 12 | --- 13 | success: true 14 | exit_code: 0 15 | ----- stdout ----- 16 | gitoid:blob:sha256:93561f4501717b4c4a2f3eb5776f03231d32ec2a1f709a611ad3d8dcf931dc1b 17 | 18 | ----- stderr ----- 19 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__artifact_no_args.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: 6 | - artifact 7 | --- 8 | success: false 9 | exit_code: 2 10 | ----- stdout ----- 11 | 12 | ----- stderr ----- 13 | Actions related to Artifact Identifiers 14 | 15 | Usage: omnibor artifact [OPTIONS] 16 | 17 | Commands: 18 | id For files, prints their Artifact ID. For directories, recursively prints IDs for all files under it 19 | find Find file matching an Artifact ID 20 | help Print this message or the help of the given subcommand(s) 21 | 22 | Options: 23 | -f, --format Output format [env: OMNIBOR_FORMAT=] [possible values: plain, short, json] 24 | -d, --dir Directory to store manifests [env: OMNIBOR_DIR=] 25 | -c, --config Path to a configuration file [env: OMNIBOR_CONFIG=] 26 | -D, --debug-console Turn on 'tokio-console' debug integration [env: OMNIBOR_DEBUG_CONSOLE=] 27 | -v, --verbose... Increase logging verbosity 28 | -q, --quiet... Decrease logging verbosity 29 | -h, --help Print help (see more with '--help') 30 | -V, --version Print version 31 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__debug_no_args.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: 6 | - debug 7 | --- 8 | success: false 9 | exit_code: 2 10 | ----- stdout ----- 11 | 12 | ----- stderr ----- 13 | Actions to help debug the OmniBOR CLI 14 | 15 | Usage: omnibor debug [OPTIONS] 16 | 17 | Commands: 18 | paths 19 | help Print this message or the help of the given subcommand(s) 20 | 21 | Options: 22 | -f, --format Output format [env: OMNIBOR_FORMAT=] [possible values: plain, short, json] 23 | -d, --dir Directory to store manifests [env: OMNIBOR_DIR=] 24 | -c, --config Path to a configuration file [env: OMNIBOR_CONFIG=] 25 | -D, --debug-console Turn on 'tokio-console' debug integration [env: OMNIBOR_DEBUG_CONSOLE=] 26 | -v, --verbose... Increase logging verbosity 27 | -q, --quiet... Decrease logging verbosity 28 | -h, --help Print help (see more with '--help') 29 | -V, --version Print version 30 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__manifest_no_args.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: 6 | - manifest 7 | --- 8 | success: false 9 | exit_code: 2 10 | ----- stdout ----- 11 | 12 | ----- stderr ----- 13 | Actions related to Input Manifests 14 | 15 | Usage: omnibor manifest [OPTIONS] 16 | 17 | Commands: 18 | create Create a new manifest and add it to the store 19 | help Print this message or the help of the given subcommand(s) 20 | 21 | Options: 22 | -f, --format Output format [env: OMNIBOR_FORMAT=] [possible values: plain, short, json] 23 | -d, --dir Directory to store manifests [env: OMNIBOR_DIR=] 24 | -c, --config Path to a configuration file [env: OMNIBOR_CONFIG=] 25 | -D, --debug-console Turn on 'tokio-console' debug integration [env: OMNIBOR_DEBUG_CONSOLE=] 26 | -v, --verbose... Increase logging verbosity 27 | -q, --quiet... Decrease logging verbosity 28 | -h, --help Print help (see more with '--help') 29 | -V, --version Print version 30 | -------------------------------------------------------------------------------- /omnibor-cli/tests/snapshots/test__no_args.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: omnibor-cli/tests/test.rs 3 | info: 4 | program: omnibor 5 | args: [] 6 | --- 7 | success: false 8 | exit_code: 2 9 | ----- stdout ----- 10 | 11 | ----- stderr ----- 12 | CLI for working with OmniBOR Identifiers and Manifests 13 | 14 | Usage: omnibor [OPTIONS] 15 | 16 | Commands: 17 | artifact Actions related to Artifact Identifiers 18 | manifest Actions related to Input Manifests 19 | store Actions related to the filesystem store 20 | debug Actions to help debug the OmniBOR CLI 21 | help Print this message or the help of the given subcommand(s) 22 | 23 | Options: 24 | -f, --format Output format [env: OMNIBOR_FORMAT=] [possible values: plain, short, json] 25 | -d, --dir Directory to store manifests [env: OMNIBOR_DIR=] 26 | -c, --config Path to a configuration file [env: OMNIBOR_CONFIG=] 27 | -D, --debug-console Turn on 'tokio-console' debug integration [env: OMNIBOR_DEBUG_CONSOLE=] 28 | -v, --verbose... Increase logging verbosity 29 | -q, --quiet... Decrease logging verbosity 30 | -h, --help Print help (see more with '--help') 31 | -V, --version Print version 32 | -------------------------------------------------------------------------------- /omnibor-cli/tests/test.rs: -------------------------------------------------------------------------------- 1 | use insta::Settings; 2 | use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; 3 | use std::process::Command; 4 | 5 | macro_rules! settings { 6 | ($block:expr) => { 7 | let mut settings = Settings::clone_current(); 8 | settings.add_filter(r#"omnibor(?:\.exe)?"#, "omnibor"); 9 | settings.bind(|| $block); 10 | }; 11 | } 12 | 13 | #[test] 14 | fn no_args() { 15 | settings!({ 16 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor"))); 17 | }); 18 | } 19 | 20 | #[test] 21 | fn artifact_no_args() { 22 | settings!({ 23 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor")).arg("artifact")); 24 | }); 25 | } 26 | 27 | #[test] 28 | fn manifest_no_args() { 29 | settings!({ 30 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor")).arg("manifest")); 31 | }); 32 | } 33 | 34 | #[test] 35 | fn debug_no_args() { 36 | settings!({ 37 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor")).arg("debug")); 38 | }); 39 | } 40 | 41 | #[test] 42 | fn artifact_id_plain() { 43 | settings!({ 44 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor")).args([ 45 | "artifact", 46 | "id", 47 | "--format", 48 | "plain", 49 | "--path", 50 | "tests/data/main.c" 51 | ])) 52 | }); 53 | } 54 | 55 | #[test] 56 | fn artifact_id_short() { 57 | settings!({ 58 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor")).args([ 59 | "artifact", 60 | "id", 61 | "--format", 62 | "short", 63 | "--path", 64 | "tests/data/main.c" 65 | ])) 66 | }); 67 | } 68 | 69 | #[test] 70 | fn artifact_id_json() { 71 | settings!({ 72 | assert_cmd_snapshot!(Command::new(get_cargo_bin("omnibor")).args([ 73 | "artifact", 74 | "id", 75 | "--format", 76 | "json", 77 | "--path", 78 | "tests/data/main.c" 79 | ])) 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /omnibor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.9.0] - 2025-02-06 9 | 10 | ### Changed 11 | 12 | - Brought back benchmarking. (#235) 13 | - Substantial `omnibor` refactor. (#236) 14 | - Make Docs.rs show feature annotations. (#237) 15 | 16 | ## [0.8.0] - 2025-02-04 17 | 18 | ### Changed 19 | 20 | - Big refactor. (#234) 21 | - Update `omnibor` crate CHANGELOG.md 22 | - Release omnibor-v0.8.0 23 | 24 | ## [0.7.0] - 2025-01-29 25 | 26 | ### Changed 27 | 28 | - Update `omnibor` crate CHANGELOG.md 29 | - Release omnibor-v0.7.0 30 | 31 | ## [0.9.0] - 2025-01-29 32 | 33 | ### Changed 34 | 35 | - Make adding manifest to store optional. (#224) 36 | - Change naming of manifest file. (#226) 37 | - Support the --no-out flag on `manifest create` (#227) 38 | - Implement newline normalization. (#228) 39 | - Feature cleanup 40 | - Release gitoid-v0.9.0 41 | 42 | ## [0.6.0] - 2024-09-26 43 | 44 | ### Changed 45 | 46 | - Updated omnibor CHANGELOG for 0.6.0 47 | - Release omnibor-v0.6.0 48 | 49 | ## [0.8.0] - 2024-09-26 50 | 51 | ### Changed 52 | 53 | - Split out CLI to its own package. (#171) 54 | - Update project and crate READMEs (#173) 55 | - Improve `omnibor/README.md` (#177) 56 | - Initial implementation of `InputManifest` (#180) 57 | - Overhaul CI testing. (#192) 58 | - Release gitoid-v0.8.0 59 | 60 | ## [0.5.1] - 2024-03-07 61 | 62 | ### Changed 63 | 64 | - Teach `cargo-dist` to build CLI (#170) 65 | - Update `omnibor` crate CHANGELOG.md 66 | - Release omnibor-v0.5.1 67 | 68 | ## [0.5.0] - 2024-03-07 69 | 70 | ### Changed 71 | 72 | - Add gitoid crate README, update crate desc. (#128) 73 | - Implemented async printing in CLI (#130) 74 | - Add 'cargo xtask' for custom tasks (#131) 75 | - Flush prints before exit, add short format (#133) 76 | - Split up CLI crate into modules. (#135) 77 | - Small fixups in the CLI code. (#136) 78 | - First pass at an OmniBOR package README (#141) 79 | - Add size test for ArtifactId (#142) 80 | - Optionally-implement serde for ArtifactId (#146) 81 | - Introduce `omnibor` FFI. (#160) 82 | - Bump `gitoid`: 0.5.0 -> 0.7.0 (#168) 83 | - Update `omnibor` crate CHANGELOG.md 84 | - Release 85 | 86 | ### Fixed 87 | 88 | - Fix 'find' command short format. (#139) 89 | - Don't stop looking at first 'find' match (#143) 90 | 91 | ## [0.4.0] - 2024-02-22 92 | 93 | ### Added 94 | 95 | - Add `tree` cmd and --format args to CLI (#118) 96 | - Add --hash flag to CLI commands (#119) 97 | 98 | ### Changed 99 | 100 | - Renamed ArtifactId methods and add docs (#115) 101 | - Introduce new OmniBOR CLI. (#117) 102 | - Combine CLI id/tree cmds, add find cmd (#122) 103 | 104 | ### Fixed 105 | 106 | - Fix double-print in tree command. (#120) 107 | 108 | ## [0.3.0] - 2024-02-20 109 | 110 | ### Changed 111 | 112 | - First draft of README rewrite (#88) 113 | - Windows test, FFI test, and commit msg CI (#106) 114 | - Initial full ArtifactId impl (#114) 115 | 116 | ### Fixed 117 | 118 | - Remove unused dependencies. 119 | - Remove unused dependencies. 120 | - Fix broken compilation of `omnibor` crate. 121 | - Missing prior version bump for OmniBOR in main 122 | 123 | [0.9.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.8.0..omnibor-v0.9.0 124 | [0.8.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.7.0..omnibor-v0.8.0 125 | [0.7.0]: https://github.com/omnibor/omnibor-rs/compare/gitoid-v0.9.0..omnibor-v0.7.0 126 | [0.9.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.6.0..gitoid-v0.9.0 127 | [0.6.0]: https://github.com/omnibor/omnibor-rs/compare/gitoid-v0.8.0..omnibor-v0.6.0 128 | [0.8.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.5.1..gitoid-v0.8.0 129 | [0.5.1]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.5.0..omnibor-v0.5.1 130 | [0.5.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.4.0..omnibor-v0.5.0 131 | [0.4.0]: https://github.com/omnibor/omnibor-rs/compare/omnibor-v0.3.0..omnibor-v0.4.0 132 | [0.3.0]: https://github.com/omnibor/omnibor-rs/compare/gitoid-v0.5.0..omnibor-v0.3.0 133 | 134 | 135 | -------------------------------------------------------------------------------- /omnibor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "omnibor" 4 | version = "0.9.0" 5 | 6 | description = "Reproducible software identity and fine-grained build dependency tracking." 7 | repository = "https://github.com/omnibor/omnibor-rs" 8 | readme = "README.md" 9 | categories = ["cryptography", "development-tools"] 10 | keywords = ["gitbom", "omnibor", "sbom"] 11 | 12 | edition.workspace = true 13 | license.workspace = true 14 | homepage.workspace = true 15 | 16 | [lib] 17 | crate-type = [ 18 | # Let the Rust compiler choose a "recommended" lib type for use by Rust code. 19 | "lib", 20 | 21 | # A system-specific static library usable by non-Rust code. 22 | # This is `*.a` on Linux, macOS, and Windows using the MinGW toolchain. 23 | # This is `*.lib` on Windows using the MSVC toolchain. 24 | "staticlib", 25 | 26 | # A system-specific dynamic library usable by non-Rust code. 27 | # This is a `*.so` file on Linux. 28 | # This is a `*.dylib` file on macOS. 29 | # This is a `*.dll` file on Windows. 30 | "cdylib", 31 | ] 32 | 33 | [dependencies] 34 | 35 | # NOTE: Must match the version used in the hash crate. 36 | # 37 | # Technically, we could rely on the re-export from one of those crates, 38 | # but since all the hash crates are optional dependencies our usage code 39 | # within the 'gitoid' crate would be more complex to handle the possibility 40 | # for any/all of them to be missing. It's simpler to just specify it here 41 | # so we know we always get the crate. 42 | digest = { version = "0.10.7", features = ["std"] } 43 | 44 | # Cryptography Providers. 45 | boring = { version = "4.6.0", optional = true } 46 | openssl = { version = "0.10.66", optional = true } 47 | sha2 = { version = "0.10.8", features = ["std"], optional = true } 48 | 49 | # Required crates. 50 | bytecount = { version = "0.6.8", features = ["runtime-dispatch-simd"] } 51 | hex = "0.4.3" 52 | serde = { version = "1.0.197", features = ["derive"] } 53 | thiserror = "2.0.0" 54 | tokio = { version = "1.36.0", features = ["io-util", "fs"] } 55 | url = "2.4.1" 56 | walkdir = { version = "2.5.0" } 57 | 58 | [dev-dependencies] 59 | 60 | anyhow = "1.0.95" 61 | criterion = { version = "0.5.1" } 62 | # NOTE: Match version in "dependencies" 63 | digest = "0.10.7" 64 | serde_test = "1.0.176" 65 | # Need "rt" and "fs" additionally for tests. 66 | tokio = { version = "1.36.0", features = [ 67 | "io-util", 68 | "fs", 69 | "rt", 70 | "rt-multi-thread", 71 | ] } 72 | tokio-test = "0.4.3" 73 | 74 | [features] 75 | 76 | # By default, you get: 77 | # 78 | # - Async support. 79 | # - The 'rustcrypto' backend. 80 | # - Standard library support. 81 | default = ["backend-rustcrypto"] 82 | 83 | # Enable using RustCrypto as a cryptography backend. 84 | backend-rustcrypto = ["dep:sha2"] 85 | 86 | # Enable using BoringSLL as a cryptography backend. 87 | backend-boringssl = ["dep:boring"] 88 | 89 | # Enable using OpenSSL as a cryptography backend. 90 | backend-openssl = ["dep:openssl"] 91 | 92 | [[bench]] 93 | name = "benchmark" 94 | harness = false 95 | 96 | [package.metadata.docs.rs] 97 | 98 | # Whether to pass `--all-features` to Cargo (default: false) 99 | all-features = true 100 | 101 | # Defines the configuration attribute `docsrs` 102 | rustdoc-args = ["--cfg", "docsrs"] 103 | -------------------------------------------------------------------------------- /omnibor/README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | # `omnibor` 7 | 8 |
9 | 10 | __Reproducible identifiers & fine-grained build dependency tracking for software artifacts.__ 11 | 12 | [![Website](https://img.shields.io/badge/website-omnibor.io-blue)](https://omnibor.io) [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/omnibor/omnibor-rs/blob/main/LICENSE) 13 | 14 |
15 | 16 | This is a Rust implementation of the [OmniBOR] specification, which defines 17 | a reproducible identifier scheme and a fine-grained build dependency tracking 18 | mechanism for software artifacts. 19 | 20 | The goal for OmniBOR is to be incorporated into software build tools, linkers, 21 | and more, so software consumers can: 22 | 23 | - Reproducibly identify software artifacts; 24 | - Deeply inspect all the components used to construct a software artifact, 25 | beyond just what packages are in a dependency tree; 26 | - Detect when dependencies change through updated artifact identifiers. 27 | 28 | The last point is key: Artifact ID's incorporate dependency information, 29 | forming a variant of a Merkle Tree! This Directed Acyclic Graph (DAG) of 30 | dependencies cascades up evidence of artifact dependency changes, enabling 31 | consumers of software artifacts to know when changes happen, and what 32 | precisely has changed. 33 | 34 | > [!IMPORTANT] 35 | > The OmniBOR spec, and this Rust package, are still a work-in-progress. 36 | > This also means it's a great time to contribute! 37 | > 38 | > If you want to contribute to the specification instead, check out the 39 | > [OmniBOR spec] repository. 40 | 41 | ## Using 42 | 43 | ### Using from Rust 44 | 45 | Run the following to add the library to your own crate. 46 | 47 | ```sh 48 | $ cargo add omnibor 49 | ``` 50 | 51 | The `omnibor` crate currently exposes the following features: 52 | 53 | | Name | Description | Default? | 54 | |:--------|:------------------------------------------------------------|:---------| 55 | | `serde` | Add support for serializing and deserializing `ArtifactId`s | No | 56 | 57 | To turn on a feature, you can run `cargo add omnibor --features=""`, or 58 | [edit your `Cargo.toml` to activate the feature][features]. 59 | 60 | ### Using from other languages 61 | 62 | The `omnibor` crate is designed to also be used from other programming languages! 63 | 64 | All API's in the crate are exposed over a Foreign Function Interface, usable by 65 | anything that can consume C code. 66 | 67 | The crate is configured to produce files suitable for either static or dynamic linking 68 | with non-Rust code. Additionally, you'll need to use `cbindgen` to produce a header 69 | file which describes the contents of the linkable library. 70 | 71 | ## Testing 72 | 73 | `omnibor` provides a variety of tests, which can be run with `cargo test`. To ensure 74 | serialization and deserialization code are tested as well, run 75 | `cargo test --features="serde"`. 76 | 77 | ## Design 78 | 79 | The OmniBOR Rust implementation is designed with the following goals in mind: 80 | 81 | - __Cross-language readiness__: The OmniBOR Rust implementation should be 82 | built with solid Foreign Function Interface (FFI) support, so it can be 83 | used as the basis for libraries in other languages. 84 | - __Multi-platform__: The OmniBOR Rust implementation should be ready for 85 | use in as many contexts as possible, including embedded environments. This 86 | means supporting use without an allocator to dynamically allocate memory, 87 | and minimizing the size of any types resident in memory. 88 | - __Fast__: The OmniBOR Rust implementation should run as quickly as possible, 89 | and be designed for high-performance use cases like rapid large scale 90 | matching of artifacts to identifiers. 91 | - __Memory efficient__: The OmniBor data is designed to be of minimal size in 92 | memory, with the understanding that real-world uses of `omnibor` are likely 93 | to work with large number of identifiers at a time. For example, the type 94 | `ArtifactId` is exactly 32 bytes, just the number of bytes necessary 95 | to store the SHA-256 hash. 96 | 97 | Usage in `no_std` environments is currently planned but not yet implemented. 98 | 99 | ## Stability Policy 100 | 101 | `omnibor` does intend to follow semantic versioning when publishing new versions. 102 | It is currently pre-`1.0`, which for us means we do not generally aim for 103 | stability of the APIs exposed, as we are still iterating and designing what 104 | we consider to be an ideal API for future stabilization. 105 | 106 | That said, we do not break stability in point releases. 107 | 108 | New version inferences are supported by the use of conventional commits which 109 | mark the introduction of new features or fixing of bugs. When new releases of 110 | `omnibor` are made, `git-cliff` checks our requested version bump against its 111 | determination of the correct version bump based on the `CHANGELOG.md`, and 112 | produces a release error if it believes we're requesting to bump the wrong 113 | version. This helps defend against accidentally-incorrect version bumps which 114 | would violate semantic versioning. 115 | 116 | ### Minimum Supported Rust Version (MSRV) 117 | 118 | This crate does not maintain a Minimum Supported Rust Version, and generally 119 | tracks the latest Rust stable version. 120 | 121 | ## Contributing 122 | 123 | We recommend checking out the full [`CONTRIBUTING.md`] for the OmniBOR Rust 124 | project, which outlines our process. 125 | 126 | ## License 127 | 128 | All of the OmniBOR Rust implementation is Apache 2.0 licensed. Contributions 129 | to `omnibor` are assumed to be made in compliance with the Apache 2.0 license. 130 | 131 | [OmniBOR]: https://omnibor.io 132 | [OmniBOR spec]: https://github.com/omnibor/spec 133 | [features]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features 134 | [`CONTRIBUTING.md`]: https://github.com/omnibor/omnibor-rs/blob/main/CONTRIBUTING.md 135 | -------------------------------------------------------------------------------- /omnibor/benches/benchmark.rs: -------------------------------------------------------------------------------- 1 | //! Benchmarks comparing cryptography backends. 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use omnibor::ArtifactIdBuilder; 5 | 6 | #[cfg(not(any( 7 | feature = "backend-rustcrypto", 8 | feature = "backend-boringssl", 9 | feature = "backend-openssl" 10 | )))] 11 | compile_error!( 12 | r#"At least one cryptography backend must be active: "# 13 | r#""backend-rustcrypto", "backend-boringssl", "backend-openssl""# 14 | ); 15 | 16 | /*=============================================================================================== 17 | * BENCHMARK FUNCTIONS 18 | * 19 | * Define the benchmark functions based on the selected features. 20 | */ 21 | 22 | #[cfg(feature = "backend-rustcrypto")] 23 | fn bench_rustcrypto_sha256_small(c: &mut Criterion) { 24 | let name = "OmniBOR RustCrypto SHA-256 11B"; 25 | let input = b"hello world"; 26 | c.bench_function(name, |b| { 27 | b.iter(|| { 28 | let _ = ArtifactIdBuilder::with_rustcrypto().identify_bytes(black_box(input)); 29 | }) 30 | }); 31 | } 32 | 33 | #[cfg(feature = "backend-boringssl")] 34 | fn bench_boring_sha256_small(c: &mut Criterion) { 35 | let name = "OmniBOR BoringSSL SHA-256 11B"; 36 | let input = b"hello world"; 37 | c.bench_function(name, |b| { 38 | b.iter(|| { 39 | let _ = ArtifactIdBuilder::with_boringssl().identify_bytes(black_box(input)); 40 | }) 41 | }); 42 | } 43 | 44 | #[cfg(feature = "backend-openssl")] 45 | fn bench_openssl_sha256_small(c: &mut Criterion) { 46 | let name = "OmniBOR OpenSSL SHA-256 11B"; 47 | let input = b"hello world"; 48 | c.bench_function(name, |b| { 49 | b.iter(|| { 50 | let _ = ArtifactIdBuilder::with_openssl().identify_bytes(black_box(input)); 51 | }) 52 | }); 53 | } 54 | 55 | #[cfg(feature = "backend-rustcrypto")] 56 | fn bench_rustcrypto_sha256_large(c: &mut Criterion) { 57 | let name = "OmniBOR RustCrypto SHA-256 100MB"; 58 | let input = &[0; 1024 * 1024 * 100]; // 100 MB 59 | c.bench_function(name, |b| { 60 | b.iter(|| { 61 | let _ = ArtifactIdBuilder::with_rustcrypto().identify_bytes(black_box(input)); 62 | }) 63 | }); 64 | } 65 | 66 | #[cfg(feature = "backend-boringssl")] 67 | fn bench_boring_sha256_large(c: &mut Criterion) { 68 | let name = "OmniBOR BoringSSL SHA-256 100MB"; 69 | let input = &[0; 1024 * 1024 * 100]; // 100 MB 70 | c.bench_function(name, |b| { 71 | b.iter(|| { 72 | let _ = ArtifactIdBuilder::with_boringssl().identify_bytes(black_box(input)); 73 | }) 74 | }); 75 | } 76 | 77 | #[cfg(feature = "backend-openssl")] 78 | fn bench_openssl_sha256_large(c: &mut Criterion) { 79 | let name = "OmniBOR OpenSSL SHA-256 100MB"; 80 | let input = &[0; 1024 * 1024 * 100]; // 100 MB 81 | c.bench_function(name, |b| { 82 | b.iter(|| { 83 | let _ = ArtifactIdBuilder::with_openssl().identify_bytes(black_box(input)); 84 | }) 85 | }); 86 | } 87 | 88 | /*=============================================================================================== 89 | * BENCHMARK GROUPS 90 | * 91 | * Define the benchmark groups based on the selected features. 92 | */ 93 | 94 | #[cfg(feature = "backend-rustcrypto")] 95 | criterion_group!( 96 | name = rustcrypto_benches; 97 | config = Criterion::default(); 98 | targets = bench_rustcrypto_sha256_small, bench_rustcrypto_sha256_large 99 | ); 100 | 101 | #[cfg(feature = "backend-boringssl")] 102 | criterion_group!( 103 | name = boringssl_benches; 104 | config = Criterion::default(); 105 | targets = bench_boring_sha256_small, bench_boring_sha256_large 106 | ); 107 | 108 | #[cfg(feature = "backend-openssl")] 109 | criterion_group!( 110 | name = openssl_benches; 111 | config = Criterion::default(); 112 | targets = bench_openssl_sha256_small, bench_openssl_sha256_large 113 | ); 114 | 115 | /*=============================================================================================== 116 | * MAIN FUNCTION 117 | * 118 | * Use conditional compilation to select the main function, incorporating the defined benchmark 119 | * groups based on the selected features. 120 | */ 121 | 122 | #[cfg(all( 123 | feature = "backend-rustcrypto", 124 | feature = "backend-boringssl", 125 | feature = "backend-openssl" 126 | ))] 127 | criterion_main!(rustcrypto_benches, boringssl_benches, openssl_benches); 128 | 129 | #[cfg(all( 130 | feature = "backend-rustcrypto", 131 | feature = "backend-boringssl", 132 | not(feature = "backend-openssl") 133 | ))] 134 | criterion_main!(rustcrypto_benches, boringssl_benches); 135 | 136 | #[cfg(all( 137 | not(feature = "backend-rustcrypto"), 138 | feature = "backend-boringssl", 139 | feature = "backend-openssl" 140 | ))] 141 | criterion_main!(boringssl_benches, openssl_benches); 142 | 143 | #[cfg(all( 144 | feature = "backend-rustcrypto", 145 | not(feature = "backend-boringssl"), 146 | feature = "backend-openssl" 147 | ))] 148 | criterion_main!(rustcrypto_benches, openssl_benches); 149 | 150 | #[cfg(all( 151 | feature = "backend-rustcrypto", 152 | not(feature = "backend-boringssl"), 153 | not(feature = "backend-openssl"), 154 | ))] 155 | criterion_main!(rustcrypto_benches); 156 | 157 | #[cfg(all( 158 | not(feature = "backend-rustcrypto"), 159 | feature = "backend-boringssl", 160 | not(feature = "backend-openssl") 161 | ))] 162 | criterion_main!(boringssl_benches); 163 | 164 | #[cfg(all( 165 | not(feature = "backend-rustcrypto"), 166 | not(feature = "backend-boringssl"), 167 | feature = "backend-openssl" 168 | ))] 169 | criterion_main!(openssl_benches); 170 | -------------------------------------------------------------------------------- /omnibor/src/artifact_id/artifact_id_builder.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | artifact_id::ArtifactId, 4 | error::ArtifactIdError, 5 | gitoid::internal::{gitoid_from_async_reader, gitoid_from_buffer, gitoid_from_reader}, 6 | hash_algorithm::{HashAlgorithm, Sha256}, 7 | hash_provider::HashProvider, 8 | input_manifest::InputManifest, 9 | object_type::Blob, 10 | util::clone_as_boxstr::CloneAsBoxstr, 11 | }, 12 | std::{ 13 | fs::File, 14 | io::{Read, Seek}, 15 | marker::PhantomData, 16 | path::Path, 17 | }, 18 | tokio::{ 19 | fs::File as AsyncFile, 20 | io::{AsyncRead, AsyncSeek}, 21 | }, 22 | }; 23 | 24 | /// A builder for [`ArtifactId`]s. 25 | pub struct ArtifactIdBuilder> { 26 | _hash_algorithm: PhantomData, 27 | provider: P, 28 | } 29 | 30 | #[cfg(feature = "backend-rustcrypto")] 31 | impl ArtifactIdBuilder { 32 | /// Create a new [`ArtifactIdBuilder`] with `RustCrypto` as the [`HashProvider`]. 33 | pub fn with_rustcrypto() -> Self { 34 | Self { 35 | _hash_algorithm: PhantomData, 36 | provider: crate::hash_provider::RustCrypto::new(), 37 | } 38 | } 39 | } 40 | 41 | #[cfg(feature = "backend-boringssl")] 42 | impl ArtifactIdBuilder { 43 | /// Create a new [`ArtifactIdBuilder`] with `BoringSsl` as the [`HashProvider`]. 44 | pub fn with_boringssl() -> Self { 45 | Self { 46 | _hash_algorithm: PhantomData, 47 | provider: crate::hash_provider::BoringSsl::new(), 48 | } 49 | } 50 | } 51 | 52 | #[cfg(feature = "backend-openssl")] 53 | impl ArtifactIdBuilder { 54 | /// Create a new [`ArtifactIdBuilder`] with `OpenSsl` as the [`HashProvider`]. 55 | pub fn with_openssl() -> Self { 56 | Self { 57 | _hash_algorithm: PhantomData, 58 | provider: crate::hash_provider::OpenSsl::new(), 59 | } 60 | } 61 | } 62 | 63 | impl> ArtifactIdBuilder { 64 | /// Create a new [`ArtifactIdBuilder`] with the given [`HashProvider`]. 65 | pub fn with_provider(provider: P) -> Self { 66 | Self { 67 | _hash_algorithm: PhantomData, 68 | provider, 69 | } 70 | } 71 | 72 | /// Create an [`ArtifactId`] for the given bytes. 73 | pub fn identify_bytes(&self, bytes: &[u8]) -> ArtifactId { 74 | // PANIC SAFETY: We're reading from an in-memory buffer, so no IO errors can arise. 75 | let gitoid = gitoid_from_buffer::(self.provider.digester(), bytes).unwrap(); 76 | ArtifactId::from_gitoid(gitoid) 77 | } 78 | 79 | /// Create an [`ArtifactId`] for the given string. 80 | pub fn identify_string(&self, s: &str) -> ArtifactId { 81 | self.identify_bytes(s.as_bytes()) 82 | } 83 | 84 | /// Create an [`ArtifactId`] for the given file. 85 | pub fn identify_file(&self, file: &mut File) -> Result, ArtifactIdError> { 86 | self.identify_reader(file) 87 | } 88 | 89 | /// Create an [`ArtifactId`] for the file at the given path. 90 | pub fn identify_path(&self, path: &Path) -> Result, ArtifactIdError> { 91 | let mut file = 92 | File::open(path).map_err(|source| ArtifactIdError::FailedToOpenFileForId { 93 | path: path.clone_as_boxstr(), 94 | source: Box::new(source), 95 | })?; 96 | self.identify_file(&mut file) 97 | } 98 | 99 | /// Create an [`ArtifactId`] for the given arbitrary seekable reader. 100 | pub fn identify_reader( 101 | &self, 102 | reader: R, 103 | ) -> Result, ArtifactIdError> { 104 | let gitoid = gitoid_from_reader::(self.provider.digester(), reader)?; 105 | Ok(ArtifactId::from_gitoid(gitoid)) 106 | } 107 | 108 | /// Create an [`ArtifactId`] for the given file, asynchronously. 109 | pub async fn identify_async_file( 110 | &self, 111 | file: &mut AsyncFile, 112 | ) -> Result, ArtifactIdError> { 113 | self.identify_async_reader(file).await 114 | } 115 | 116 | /// Create an [`ArtifactId`] for the file at the given path, asynchronously. 117 | pub async fn identify_async_path(&self, path: &Path) -> Result, ArtifactIdError> { 118 | let mut file = AsyncFile::open(path).await.map_err(|source| { 119 | ArtifactIdError::FailedToOpenFileForId { 120 | path: path.clone_as_boxstr(), 121 | source: Box::new(source), 122 | } 123 | })?; 124 | self.identify_async_file(&mut file).await 125 | } 126 | 127 | /// Create an [`ArtifactId`] for the given arbitrary seekable reader, asynchronously. 128 | pub async fn identify_async_reader( 129 | &self, 130 | reader: R, 131 | ) -> Result, ArtifactIdError> { 132 | let gitoid = 133 | gitoid_from_async_reader::(self.provider.digester(), reader).await?; 134 | Ok(ArtifactId::from_gitoid(gitoid)) 135 | } 136 | 137 | /// Create an [`ArtifactId`] for the given [`InputManifest`] 138 | pub fn identify_manifest(&self, manifest: &InputManifest) -> ArtifactId { 139 | self.identify_bytes(&manifest.as_bytes()[..]) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /omnibor/src/artifact_id/mod.rs: -------------------------------------------------------------------------------- 1 | //! Reproducible artifact identifier. 2 | 3 | pub(crate) mod artifact_id; 4 | pub(crate) mod artifact_id_builder; 5 | 6 | pub use crate::artifact_id::artifact_id::ArtifactId; 7 | pub use crate::artifact_id::artifact_id_builder::ArtifactIdBuilder; 8 | -------------------------------------------------------------------------------- /omnibor/src/embedding_mode.rs: -------------------------------------------------------------------------------- 1 | //! Control whether an [`InputManifest`](crate::InputManifest)'s [`ArtifactId`](crate::ArtifactId) is stored in an artifact. 2 | 3 | /// Indicate whether to embed the identifier for an input manifest in an artifact. 4 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 5 | pub enum EmbeddingMode { 6 | /// Embed the identifier for the input manifest. 7 | Embed, 8 | /// Do not embed the identifier for the input manifest. 9 | NoEmbed, 10 | } 11 | -------------------------------------------------------------------------------- /omnibor/src/error/artifact_id_error.rs: -------------------------------------------------------------------------------- 1 | use { 2 | hex::FromHexError as HexError, 3 | std::{ 4 | fmt::{Display, Formatter, Result as FmtResult}, 5 | io::{Error as IoError, SeekFrom}, 6 | }, 7 | url::ParseError as UrlError, 8 | }; 9 | 10 | #[cfg(doc)] 11 | use crate::{artifact_id::ArtifactId, input_manifest::InputManifest}; 12 | 13 | /// An error arising from Artifact ID operations. 14 | #[derive(Debug, thiserror::Error)] 15 | #[non_exhaustive] 16 | pub enum ArtifactIdError { 17 | /// Failed to open file for identification. 18 | #[error("failed to open file for identification '{path}'")] 19 | FailedToOpenFileForId { 20 | /// The path of the file we failed to open. 21 | path: Box, 22 | /// The underlying IO error. 23 | #[source] 24 | source: Box, 25 | }, 26 | 27 | /// Failed to asynchronously read the reader. 28 | #[error("failed to asynchronously read the reader")] 29 | FailedRead(#[source] Box), 30 | 31 | /// Failed to reset reader back to the start. 32 | #[error("failed to reset reader to '{}'", SeekFromDisplay(.0))] 33 | FailedSeek(SeekFrom, #[source] Box), 34 | 35 | /// Failed to check reader position. 36 | #[error("failed to check reader position")] 37 | FailedCheckReaderPos(#[source] Box), 38 | 39 | /// Invalid scheme in URL. 40 | #[error("invalid scheme in URL '{0}'")] 41 | InvalidScheme(Box), 42 | 43 | /// Missing object type in URL. 44 | #[error("missing object type in URL '{0}'")] 45 | MissingObjectType(Box), 46 | 47 | /// Missing hash algorithm in URL. 48 | #[error("missing hash algorithm in URL '{0}'")] 49 | MissingHashAlgorithm(Box), 50 | 51 | /// Missing hash in URL. 52 | #[error("missing hash in URL '{0}'")] 53 | MissingHash(Box), 54 | 55 | /// Mismatched object type. 56 | #[error("mismatched object type; expected '{expected}', got '{got}'")] 57 | MismatchedObjectType { 58 | /// The expected object type. 59 | expected: Box, 60 | /// The received object type. 61 | got: Box, 62 | }, 63 | 64 | /// Mismatched hash algorithm. 65 | #[error("mismatched hash algorithm; expected '{expected}', got '{got}'")] 66 | MismatchedHashAlgorithm { 67 | /// The expected hash algorithm. 68 | expected: Box, 69 | /// The received hash algorithm. 70 | got: Box, 71 | }, 72 | 73 | /// Invalid hex string. 74 | #[error("invalid hex string '{0}'")] 75 | InvalidHex(Box, #[source] Box), 76 | 77 | /// URL for Artifact ID is not a valid URL 78 | #[error("URL for Artifact ID is not a valid URL '{0}'")] 79 | FailedToParseUrl(Box, #[source] Box), 80 | } 81 | 82 | /// Helper struct to implement `Display` from `SeekFrom`. 83 | struct SeekFromDisplay<'s>(&'s SeekFrom); 84 | 85 | impl Display for SeekFromDisplay<'_> { 86 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 87 | match self.0 { 88 | SeekFrom::Start(pos) => write!(f, "{} bytes from start", pos), 89 | SeekFrom::End(pos) => write!(f, "{} bytes from end", pos), 90 | SeekFrom::Current(pos) => write!(f, "{} bytes from current position", pos), 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /omnibor/src/error/input_manifest_error.rs: -------------------------------------------------------------------------------- 1 | use {crate::error::ArtifactIdError, std::io::Error as IoError}; 2 | 3 | #[cfg(doc)] 4 | use crate::{artifact_id::ArtifactId, input_manifest::InputManifest}; 5 | 6 | /// An error arising from Input Manifest operations. 7 | #[derive(Debug, thiserror::Error)] 8 | #[non_exhaustive] 9 | pub enum InputManifestError { 10 | /// Input manifest missing header line. 11 | #[error("input manifest missing header line")] 12 | ManifestMissingHeader, 13 | 14 | /// Missing 'gitoid' in manifest header. 15 | #[error("missing 'gitoid' in manifest header")] 16 | MissingGitOidInHeader, 17 | 18 | /// Missing 'blob' in manifest header. 19 | #[error("missing object type 'blob' in manifest header")] 20 | MissingObjectTypeInHeader, 21 | 22 | /// Missing one or more header parts. 23 | #[error("missing one or more header parts")] 24 | MissingHeaderParts, 25 | 26 | /// Missing bom indicator in relation. 27 | #[error("missing bom indicator in relation")] 28 | MissingBomIndicatorInRelation, 29 | 30 | /// Missing one or more relation parts. 31 | #[error("missing one or more relation parts")] 32 | MissingRelationParts, 33 | 34 | /// Wrong hash algorithm. 35 | #[error("wrong hash algorithm; expected '{expected}', got '{got}'")] 36 | WrongHashAlgorithm { 37 | /// The expected hash algorithm. 38 | expected: Box, 39 | /// The hash algorithm encountered. 40 | got: Box, 41 | }, 42 | 43 | /// Unknown file type for manifest ID embedding. 44 | #[error("unknown file type for manifest ID embedding")] 45 | UnknownEmbeddingTarget, 46 | 47 | /// Failed to read input manifest file. 48 | #[error("failed to read input manifest file")] 49 | FailedManifestRead(#[source] Box), 50 | 51 | /// Failed to read the target artifact during input manifest creation. 52 | #[error("failed to read the target artifact during input manifest creation")] 53 | FailedTargetArtifactRead(#[source] Box), 54 | 55 | /// An error arising from an Artifact ID problem. 56 | #[error(transparent)] 57 | ArtifactIdError(#[from] ArtifactIdError), 58 | 59 | /// No storage root found. 60 | #[error("no storage root found; provide one or set the 'OMNIBOR_DIR' environment variable")] 61 | NoStorageRoot, 62 | 63 | /// Can't access storage root. 64 | #[error("unable to access file system storage root '{0}'; please check permissions")] 65 | CantAccessRoot(Box, #[source] Box), 66 | 67 | /// Object store is not a directory. 68 | #[error("object store is not a directory; '{0}'")] 69 | ObjectStoreNotDir(Box), 70 | 71 | /// Object store path is not valid. 72 | #[error("not a valid object store path; '{0}'")] 73 | InvalidObjectStorePath(Box), 74 | 75 | /// Object store is not empty. 76 | #[error("object store is not empty; '{0}'")] 77 | ObjectStoreDirNotEmpty(Box), 78 | 79 | /// Can't create object store. 80 | #[error("can't create object store '{0}'")] 81 | CantCreateObjectStoreDir(Box, #[source] Box), 82 | 83 | /// Can't write manifest directory. 84 | #[error("can't write manifest directory '{0}'")] 85 | CantWriteManifestDir(Box, #[source] Box), 86 | 87 | /// Can't open target index file. 88 | #[error("can't open target index file '{0}'")] 89 | CantOpenTargetIndex(Box, #[source] Box), 90 | 91 | /// Can't create target index file. 92 | #[error("can't create target index file '{0}'")] 93 | CantCreateTargetIndex(Box, #[source] Box), 94 | 95 | /// Can't open target index temp file during an upsert. 96 | #[error("can't open target index temp file for upsert '{0}'")] 97 | CantOpenTargetIndexTemp(Box, #[source] Box), 98 | 99 | /// Can't write to target index temp file for upsert. 100 | #[error("can't write to target index temp file for upsert '{0}'")] 101 | CantWriteTargetIndexTemp(Box, #[source] Box), 102 | 103 | /// Can't delete target index temp file during an upsert. 104 | #[error("can't delete target index temp file for upsert '{0}'")] 105 | CantDeleteTargetIndexTemp(Box, #[source] Box), 106 | 107 | /// Can't replace target index with temp file. 108 | #[error("can't replace target index '{index}' with temp file '{temp}'")] 109 | CantReplaceTargetIndexWithTemp { 110 | /// The path to the target index temp file. 111 | temp: Box, 112 | /// The path to the target index file. 113 | index: Box, 114 | /// The underlying IO error. 115 | #[source] 116 | source: Box, 117 | }, 118 | 119 | /// Can't write manifest file. 120 | #[error("can't write manifest file '{0}'")] 121 | CantWriteManifest(Box, #[source] Box), 122 | 123 | /// Target index entry is malformed. 124 | #[error("target index entry '{line_no}' is malformed")] 125 | TargetIndexMalformedEntry { 126 | /// The line of the malformed entry. 127 | line_no: usize, 128 | }, 129 | 130 | /// Can't read entry of the target index file. 131 | #[error("can't read entry '{line_no}' of the target index file")] 132 | CantReadTargetIndexLine { 133 | /// The line of the entry we can't read. 134 | line_no: usize, 135 | /// The underlying IO error. 136 | #[source] 137 | source: Box, 138 | }, 139 | 140 | /// An Artifact ID is missing from target index upsert. 141 | #[error("missing manifest_aid or target_aid from target index upsert operation")] 142 | InvalidTargetIndexUpsert, 143 | 144 | /// Failed to clean up storage root. 145 | #[error("failed to clean up storage root '{0}'")] 146 | FailedStorageCleanup(Box, #[source] Box), 147 | } 148 | -------------------------------------------------------------------------------- /omnibor/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | //! [`ArtifactIdError`] and [`InputManifestError`] types. 2 | 3 | pub(crate) mod artifact_id_error; 4 | pub(crate) mod input_manifest_error; 5 | 6 | pub use crate::error::artifact_id_error::ArtifactIdError; 7 | pub use crate::error::input_manifest_error::InputManifestError; 8 | -------------------------------------------------------------------------------- /omnibor/src/ffi/error.rs: -------------------------------------------------------------------------------- 1 | //! Errors arising from FFI code. 2 | //! 3 | //! This module contains four related parts: 4 | //! 5 | //! - A thread-local-storage-allocated value containing any error messages 6 | //! set by errors in FFI code, along with a getter and setter function. 7 | //! - A mechanism for catching panics and recording error messages into that 8 | //! thread-local storage. 9 | //! - An error type (plus trait impls) specific to FFI code. 10 | //! - An "error message" type to assist capturing messages from panics. 11 | //! 12 | //! Together, these provide a consistent mechanism for collecting and reporting 13 | //! errors to users of the `ArtifactId` FFI. 14 | 15 | use { 16 | crate::error::ArtifactIdError, 17 | core::{ 18 | any::Any, 19 | cell::RefCell, 20 | fmt::{Display, Formatter, Result as FmtResult}, 21 | panic::UnwindSafe, 22 | str::Utf8Error, 23 | }, 24 | std::{error::Error as StdError, ffi::NulError, panic::catch_unwind}, 25 | url::ParseError as UrlError, 26 | }; 27 | 28 | thread_local! { 29 | // The last error to have been reported by the FFI code. 30 | /// cbindgen:ignore 31 | #[doc(hidden)] 32 | static LAST_ERROR: RefCell> = const { RefCell::new(None) }; 33 | } 34 | 35 | /// Update the last error with a new error message. 36 | #[inline] 37 | pub(crate) fn set_error_msg(e: String) { 38 | LAST_ERROR.with(|last| { 39 | *last.borrow_mut() = Some(e); 40 | }); 41 | } 42 | 43 | /// Get the last error message if there is one. 44 | #[inline] 45 | pub(crate) fn get_error_msg() -> Option { 46 | LAST_ERROR.with(|last| last.borrow_mut().take()) 47 | } 48 | 49 | /// Convenient panic-catching and reporting. 50 | /// 51 | /// This wraps `std::panic::catch_unwind`, but enables you to write functions 52 | /// which return `Result` and have those errors correctly 53 | /// reported out. 54 | pub(crate) fn catch_panic(f: F) -> Option 55 | where 56 | F: FnOnce() -> Result + UnwindSafe, 57 | { 58 | // The return type is Result, AnyError> 59 | let result = catch_unwind(f); 60 | 61 | match result { 62 | Ok(Ok(value)) => Some(value), 63 | Ok(Err(err)) => { 64 | // We have our `Error` type. 65 | set_error_msg(match err.source() { 66 | None => err.to_string(), 67 | Some(source_err) => { 68 | format!("{}: {}", err, source_err) 69 | } 70 | }); 71 | None 72 | } 73 | Err(err) => { 74 | // We have a `Box` 75 | set_error_msg(ErrorMsg::from(err).to_string()); 76 | None 77 | } 78 | } 79 | } 80 | 81 | /// An Error arising from FFI code. 82 | #[derive(Debug)] 83 | pub(crate) enum Error { 84 | ContentPtrIsNull, 85 | StringPtrIsNull, 86 | ArtifactIdPtrIsNull, 87 | Utf8UnexpectedEnd, 88 | Utf8InvalidByte(usize, usize), 89 | NotValidUrl(UrlError), 90 | NotArtifactIdUrl(ArtifactIdError), 91 | StringHadInteriorNul(usize), 92 | } 93 | 94 | impl Display for Error { 95 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 96 | match self { 97 | Error::ContentPtrIsNull => write!(f, "data pointer is null"), 98 | Error::StringPtrIsNull => write!(f, "string pointer is null"), 99 | Error::ArtifactIdPtrIsNull => write!(f, "ArtifactId pointer is null"), 100 | Error::Utf8UnexpectedEnd => write!(f, "UTF-8 byte sequence ended unexpectedly"), 101 | Error::Utf8InvalidByte(start, len) => write!( 102 | f, 103 | "invalid {}-byte UTF-8 sequence, starting at byte {}", 104 | len, start 105 | ), 106 | Error::NotValidUrl(_) => write!(f, "string is not a valid URL"), 107 | Error::NotArtifactIdUrl(_) => write!(f, "string is not a valid ArtifactId URL"), 108 | Error::StringHadInteriorNul(loc) => { 109 | write!(f, "string had interior NUL at byte {}", loc) 110 | } 111 | } 112 | } 113 | } 114 | 115 | impl StdError for Error { 116 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 117 | match self { 118 | Error::NotValidUrl(e) => Some(e), 119 | Error::NotArtifactIdUrl(e) => Some(e), 120 | _ => None, 121 | } 122 | } 123 | } 124 | 125 | impl From for Error { 126 | fn from(utf8_error: Utf8Error) -> Error { 127 | match utf8_error.error_len() { 128 | None => Error::Utf8UnexpectedEnd, 129 | Some(len) => Error::Utf8InvalidByte(utf8_error.valid_up_to(), len), 130 | } 131 | } 132 | } 133 | 134 | impl From for Error { 135 | fn from(url_error: UrlError) -> Error { 136 | Error::NotValidUrl(url_error) 137 | } 138 | } 139 | 140 | impl From for Error { 141 | fn from(artifact_id_error: ArtifactIdError) -> Error { 142 | Error::NotArtifactIdUrl(artifact_id_error) 143 | } 144 | } 145 | 146 | impl From for Error { 147 | fn from(nul_error: NulError) -> Error { 148 | Error::StringHadInteriorNul(nul_error.nul_position()) 149 | } 150 | } 151 | 152 | /// An error message arising from a panic. 153 | /// 154 | /// This is part of the implement of the LAST_ERROR mechanism, which takes any `AnyError`, 155 | /// attempts to extract an `ErrorMsg` out of it, and then stores the resulting string 156 | /// (from the `ToString` impl implies by `Display`) as the LAST_ERROR message. 157 | #[derive(Debug)] 158 | enum ErrorMsg { 159 | /// A successfully-extracted message. 160 | Known(String), 161 | 162 | /// Could not extract a message, so the error is unknown. 163 | Unknown, 164 | } 165 | 166 | impl Display for ErrorMsg { 167 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 168 | match self { 169 | ErrorMsg::Known(s) => write!(f, "{}", s), 170 | ErrorMsg::Unknown => write!(f, "an unknown error occured"), 171 | } 172 | } 173 | } 174 | 175 | impl From> for ErrorMsg { 176 | fn from(other: Box) -> ErrorMsg { 177 | if let Some(s) = other.downcast_ref::() { 178 | ErrorMsg::Known(s.clone()) 179 | } else if let Some(s) = other.downcast_ref::<&str>() { 180 | ErrorMsg::Known(s.to_string()) 181 | } else { 182 | ErrorMsg::Unknown 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /omnibor/src/ffi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Foreign Function Interface (FFI) to use `omnibor` from other languages. 2 | 3 | mod artifact_id; 4 | pub(crate) mod error; 5 | pub(crate) mod status; 6 | pub(crate) mod util; 7 | 8 | // Re-export 9 | pub use crate::ffi::artifact_id::*; 10 | -------------------------------------------------------------------------------- /omnibor/src/ffi/status.rs: -------------------------------------------------------------------------------- 1 | //! Status codes returned for functions which signal errors with `c_int`. 2 | 3 | /// Status codes for functions returning `c_int` to signal errors. 4 | pub(crate) enum Status { 5 | /// Unknown error that shouldn't happen. 6 | UnexpectedError = -1, 7 | /// The buffer passed in is null. 8 | BufferIsNull = -2, 9 | /// The buffer passed in is too small to put data into. 10 | BufferTooSmall = -3, 11 | /// Writing to the provided buffer failed. 12 | BufferWriteFailed = -4, 13 | /// Input pointer is invalid. 14 | InvalidPtr = -5, 15 | } 16 | -------------------------------------------------------------------------------- /omnibor/src/ffi/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for the FFI code. 2 | 3 | use { 4 | crate::ffi::{error::Error, status::Status}, 5 | std::{ 6 | ffi::{c_int, CString}, 7 | io::Write as _, 8 | }, 9 | }; 10 | 11 | /// Write a string slice to a C buffer. 12 | /// 13 | /// This performs a write, including the null terminator and performing zeroization of any 14 | /// excess in the destination buffer. 15 | pub(crate) fn write_to_c_buf(src: &str, mut dst: &mut [u8]) -> c_int { 16 | // Ensure the string has the null terminator. 17 | let src = match CString::new(src.as_bytes()) { 18 | Ok(s) => s, 19 | Err(_) => return Status::UnexpectedError as c_int, 20 | }; 21 | let src = src.as_bytes_with_nul(); 22 | 23 | // Make sure the destination buffer is big enough. 24 | if dst.len() < src.len() { 25 | return Status::BufferTooSmall as c_int; 26 | } 27 | 28 | // Write the buffer. 29 | match dst.write_all(src) { 30 | Ok(()) => 0, 31 | Err(_) => Status::BufferWriteFailed as c_int, 32 | } 33 | } 34 | 35 | /// Check if a pointer is null, and if it is return the given error. 36 | pub(crate) fn check_null(ptr: *const T, error: Error) -> Result<(), Error> { 37 | if ptr.is_null() { 38 | return Err(error); 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /omnibor/src/gitoid/gitoid.rs: -------------------------------------------------------------------------------- 1 | //! A gitoid representing a single artifact. 2 | 3 | use { 4 | crate::{ 5 | error::ArtifactIdError, gitoid::gitoid_url_parser::GitOidUrlParser, 6 | hash_algorithm::HashAlgorithm, object_type::ObjectType, 7 | util::clone_as_boxstr::CloneAsBoxstr, 8 | }, 9 | serde::{ 10 | de::{Deserializer, Error as DeserializeError, Visitor}, 11 | Deserialize, Serialize, Serializer, 12 | }, 13 | std::{ 14 | cmp::Ordering, 15 | fmt::{Debug, Display, Formatter, Result as FmtResult}, 16 | hash::{Hash, Hasher}, 17 | marker::PhantomData, 18 | result::Result as StdResult, 19 | str::FromStr, 20 | }, 21 | url::Url, 22 | }; 23 | 24 | /// A struct that computes [gitoids][g] based on the selected algorithm 25 | /// 26 | /// [g]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects 27 | #[repr(C)] 28 | pub struct GitOid 29 | where 30 | H: HashAlgorithm, 31 | O: ObjectType, 32 | { 33 | #[doc(hidden)] 34 | pub(crate) _phantom: PhantomData, 35 | 36 | #[doc(hidden)] 37 | pub(crate) value: ::Array, 38 | } 39 | 40 | pub(crate) const GITOID_URL_SCHEME: &str = "gitoid"; 41 | 42 | impl GitOid 43 | where 44 | H: HashAlgorithm, 45 | O: ObjectType, 46 | { 47 | /// Construct a new `GitOid` from a `Url`. 48 | pub fn try_from_url(url: Url) -> Result, ArtifactIdError> { 49 | GitOid::try_from(url) 50 | } 51 | 52 | /// Get a URL for the current `GitOid`. 53 | pub fn url(&self) -> Url { 54 | // PANIC SAFETY: We know that this is a valid URL; 55 | // our `Display` impl is the URL representation. 56 | Url::parse(&self.to_string()).unwrap() 57 | } 58 | 59 | /// Get the underlying bytes of the hash. 60 | pub fn as_bytes(&self) -> &[u8] { 61 | &self.value[..] 62 | } 63 | 64 | /// Convert the hash to a hexadecimal string. 65 | pub fn as_hex(&self) -> String { 66 | hex::encode(self.as_bytes()) 67 | } 68 | 69 | /// Get the hash algorithm used for the `GitOid`. 70 | pub const fn hash_algorithm(&self) -> &'static str { 71 | H::NAME 72 | } 73 | 74 | /// Get the object type of the `GitOid`. 75 | pub const fn object_type(&self) -> &'static str { 76 | O::NAME 77 | } 78 | 79 | /// Get the length of the hash in bytes. 80 | pub fn hash_len(&self) -> usize { 81 | self.value.len() 82 | } 83 | } 84 | 85 | impl FromStr for GitOid 86 | where 87 | H: HashAlgorithm, 88 | O: ObjectType, 89 | { 90 | type Err = ArtifactIdError; 91 | 92 | fn from_str(s: &str) -> Result, ArtifactIdError> { 93 | let url = Url::parse(s).map_err(|source| { 94 | ArtifactIdError::FailedToParseUrl(s.clone_as_boxstr(), Box::new(source)) 95 | })?; 96 | GitOid::try_from_url(url) 97 | } 98 | } 99 | 100 | impl Clone for GitOid 101 | where 102 | H: HashAlgorithm, 103 | O: ObjectType, 104 | { 105 | fn clone(&self) -> Self { 106 | *self 107 | } 108 | } 109 | 110 | impl Copy for GitOid 111 | where 112 | H: HashAlgorithm, 113 | O: ObjectType, 114 | { 115 | } 116 | 117 | impl PartialEq> for GitOid 118 | where 119 | H: HashAlgorithm, 120 | O: ObjectType, 121 | { 122 | fn eq(&self, other: &GitOid) -> bool { 123 | self.value == other.value 124 | } 125 | } 126 | 127 | impl Eq for GitOid 128 | where 129 | H: HashAlgorithm, 130 | O: ObjectType, 131 | { 132 | } 133 | 134 | impl PartialOrd> for GitOid 135 | where 136 | H: HashAlgorithm, 137 | O: ObjectType, 138 | { 139 | fn partial_cmp(&self, other: &Self) -> Option { 140 | Some(self.cmp(other)) 141 | } 142 | } 143 | 144 | impl Ord for GitOid 145 | where 146 | H: HashAlgorithm, 147 | O: ObjectType, 148 | { 149 | fn cmp(&self, other: &Self) -> Ordering { 150 | self.value.cmp(&other.value) 151 | } 152 | } 153 | 154 | impl Hash for GitOid 155 | where 156 | H: HashAlgorithm, 157 | O: ObjectType, 158 | { 159 | fn hash

(&self, state: &mut H2) 160 | where 161 | H2: Hasher, 162 | { 163 | self.value.hash(state); 164 | } 165 | } 166 | 167 | impl Debug for GitOid 168 | where 169 | H: HashAlgorithm, 170 | O: ObjectType, 171 | { 172 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 173 | f.debug_struct("GitOid") 174 | .field("object_type", &O::NAME) 175 | .field("hash_algorithm", &H::NAME) 176 | .field("value", &self.value) 177 | .finish() 178 | } 179 | } 180 | 181 | impl Display for GitOid 182 | where 183 | H: HashAlgorithm, 184 | O: ObjectType, 185 | { 186 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 187 | write!( 188 | f, 189 | "{}:{}:{}:{}", 190 | GITOID_URL_SCHEME, 191 | O::NAME, 192 | H::NAME, 193 | self.as_hex() 194 | ) 195 | } 196 | } 197 | 198 | impl Serialize for GitOid 199 | where 200 | H: HashAlgorithm, 201 | O: ObjectType, 202 | { 203 | fn serialize(&self, serializer: S) -> StdResult 204 | where 205 | S: Serializer, 206 | { 207 | // Serialize self as the URL string. 208 | let self_as_url_str = self.url().to_string(); 209 | serializer.serialize_str(&self_as_url_str) 210 | } 211 | } 212 | 213 | impl<'de, H, O> Deserialize<'de> for GitOid 214 | where 215 | H: HashAlgorithm, 216 | O: ObjectType, 217 | { 218 | fn deserialize(deserializer: D) -> StdResult 219 | where 220 | D: Deserializer<'de>, 221 | { 222 | // Deserialize self from the URL string. 223 | struct GitOidVisitor(PhantomData, PhantomData); 224 | 225 | impl Visitor<'_> for GitOidVisitor { 226 | type Value = GitOid; 227 | 228 | fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { 229 | formatter.write_str("a gitoid-scheme URL") 230 | } 231 | 232 | fn visit_str(self, value: &str) -> StdResult 233 | where 234 | E: DeserializeError, 235 | { 236 | let url = Url::parse(value).map_err(E::custom)?; 237 | let id = GitOid::try_from(url).map_err(E::custom)?; 238 | Ok(id) 239 | } 240 | } 241 | 242 | deserializer.deserialize_str(GitOidVisitor(PhantomData, PhantomData)) 243 | } 244 | } 245 | 246 | impl TryFrom for GitOid 247 | where 248 | H: HashAlgorithm, 249 | O: ObjectType, 250 | { 251 | type Error = ArtifactIdError; 252 | 253 | fn try_from(url: Url) -> Result, ArtifactIdError> { 254 | GitOidUrlParser::new(&url).parse() 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /omnibor/src/gitoid/gitoid_url_parser.rs: -------------------------------------------------------------------------------- 1 | //! A gitoid representing a single artifact. 2 | 3 | use { 4 | crate::{ 5 | error::ArtifactIdError, 6 | gitoid::{gitoid::GITOID_URL_SCHEME, GitOid}, 7 | hash_algorithm::HashAlgorithm, 8 | object_type::ObjectType, 9 | util::clone_as_boxstr::CloneAsBoxstr, 10 | }, 11 | std::{marker::PhantomData, ops::Not as _, str::Split}, 12 | url::Url, 13 | }; 14 | 15 | pub(crate) struct GitOidUrlParser<'u, H, O> 16 | where 17 | H: HashAlgorithm, 18 | O: ObjectType, 19 | { 20 | url: &'u Url, 21 | 22 | segments: Split<'u, char>, 23 | 24 | #[doc(hidden)] 25 | _hash_algorithm: PhantomData, 26 | 27 | #[doc(hidden)] 28 | _object_type: PhantomData, 29 | } 30 | 31 | fn some_if_not_empty(s: &str) -> Option<&str> { 32 | s.is_empty().not().then_some(s) 33 | } 34 | 35 | impl<'u, H, O> GitOidUrlParser<'u, H, O> 36 | where 37 | H: HashAlgorithm, 38 | O: ObjectType, 39 | { 40 | pub(crate) fn new(url: &'u Url) -> GitOidUrlParser<'u, H, O> { 41 | GitOidUrlParser { 42 | url, 43 | segments: url.path().split(':'), 44 | _hash_algorithm: PhantomData, 45 | _object_type: PhantomData, 46 | } 47 | } 48 | 49 | pub(crate) fn parse(&mut self) -> Result, ArtifactIdError> { 50 | self.validate_url_scheme() 51 | .and_then(|_| self.validate_object_type()) 52 | .and_then(|_| self.validate_hash_algorithm()) 53 | .and_then(|_| self.parse_hash()) 54 | .map(|value| GitOid { 55 | _phantom: PhantomData, 56 | value, 57 | }) 58 | } 59 | 60 | fn validate_url_scheme(&self) -> Result<(), ArtifactIdError> { 61 | if self.url.scheme() != GITOID_URL_SCHEME { 62 | return Err(ArtifactIdError::InvalidScheme(self.url.clone_as_boxstr())); 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | fn validate_object_type(&mut self) -> Result<(), ArtifactIdError> { 69 | let object_type = self 70 | .segments 71 | .next() 72 | .and_then(some_if_not_empty) 73 | .ok_or_else(|| ArtifactIdError::MissingObjectType(self.url.clone_as_boxstr()))?; 74 | 75 | if object_type != O::NAME { 76 | return Err(ArtifactIdError::MismatchedObjectType { 77 | expected: O::NAME.clone_as_boxstr(), 78 | got: object_type.clone_as_boxstr(), 79 | }); 80 | } 81 | 82 | Ok(()) 83 | } 84 | 85 | fn validate_hash_algorithm(&mut self) -> Result<(), ArtifactIdError> { 86 | let hash_algorithm = self 87 | .segments 88 | .next() 89 | .and_then(some_if_not_empty) 90 | .ok_or_else(|| ArtifactIdError::MissingHashAlgorithm(self.url.clone_as_boxstr()))?; 91 | 92 | if hash_algorithm != H::NAME { 93 | return Err(ArtifactIdError::MismatchedHashAlgorithm { 94 | expected: H::NAME.clone_as_boxstr(), 95 | got: hash_algorithm.clone_as_boxstr(), 96 | }); 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | fn parse_hash(&mut self) -> Result { 103 | let hex_str = self 104 | .segments 105 | .next() 106 | .and_then(some_if_not_empty) 107 | .ok_or_else(|| ArtifactIdError::MissingHash(self.url.clone_as_boxstr()))?; 108 | 109 | let decoded = hex::decode(hex_str).map_err(|source| { 110 | ArtifactIdError::InvalidHex(hex_str.clone_as_boxstr(), Box::new(source)) 111 | })?; 112 | 113 | let value = ::Array::from_iter(decoded); 114 | 115 | Ok(value) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /omnibor/src/gitoid/internal.rs: -------------------------------------------------------------------------------- 1 | //! A gitoid representing a single artifact. 2 | 3 | use crate::{ 4 | error::ArtifactIdError, 5 | gitoid::GitOid, 6 | hash_algorithm::HashAlgorithm, 7 | object_type::ObjectType, 8 | util::{ 9 | for_each_buf_fill::ForEachBufFill as _, 10 | stream_len::{async_stream_len, stream_len}, 11 | }, 12 | }; 13 | use digest::Digest; 14 | use std::{ 15 | io::{BufReader, Read, Seek, SeekFrom}, 16 | marker::PhantomData, 17 | }; 18 | use tokio::io::{ 19 | AsyncBufReadExt as _, AsyncRead, AsyncSeek, AsyncSeekExt as _, BufReader as AsyncBufReader, 20 | }; 21 | 22 | /// Generate a GitOid from data in a buffer of bytes. 23 | /// 24 | /// If data is small enough to fit in memory, then generating a GitOid for it 25 | /// this way should be much faster, as it doesn't require seeking. 26 | pub(crate) fn gitoid_from_buffer( 27 | mut digester: impl Digest, 28 | buffer: &[u8], 29 | ) -> Result, ArtifactIdError> 30 | where 31 | H: HashAlgorithm, 32 | O: ObjectType, 33 | { 34 | let hashed_len = buffer.len() - num_carriage_returns_in_buffer(buffer); 35 | digest_gitoid_header(&mut digester, O::NAME, hashed_len); 36 | digest_with_normalized_newlines(&mut digester, buffer); 37 | let hash = digester.finalize(); 38 | Ok(GitOid { 39 | _phantom: PhantomData, 40 | value: ::Array::from_iter(hash), 41 | }) 42 | } 43 | 44 | /// Generate a GitOid by reading from an arbitrary reader. 45 | pub(crate) fn gitoid_from_reader( 46 | mut digester: impl Digest, 47 | mut reader: R, 48 | ) -> Result, ArtifactIdError> 49 | where 50 | H: HashAlgorithm, 51 | O: ObjectType, 52 | R: Read + Seek, 53 | { 54 | let expected_len = stream_len(&mut reader)? as usize; 55 | let (num_carriage_returns, reader) = num_carriage_returns_in_reader(reader)?; 56 | let hashed_len = expected_len - num_carriage_returns; 57 | 58 | digest_gitoid_header(&mut digester, O::NAME, hashed_len); 59 | let _ = BufReader::new(reader) 60 | .for_each_buf_fill(|b| digest_with_normalized_newlines(&mut digester, b))?; 61 | 62 | let hash = digester.finalize(); 63 | 64 | Ok(GitOid { 65 | _phantom: PhantomData, 66 | value: ::Array::from_iter(hash), 67 | }) 68 | } 69 | 70 | /// Async version of `gitoid_from_reader`. 71 | pub(crate) async fn gitoid_from_async_reader( 72 | mut digester: impl Digest, 73 | mut reader: R, 74 | ) -> Result, ArtifactIdError> 75 | where 76 | H: HashAlgorithm, 77 | O: ObjectType, 78 | R: AsyncRead + AsyncSeek + Unpin, 79 | { 80 | let expected_len = async_stream_len(&mut reader).await? as usize; 81 | 82 | let (num_carriage_returns, reader) = num_carriage_returns_in_async_reader(reader).await?; 83 | let hashed_len = expected_len - num_carriage_returns; 84 | 85 | digest_gitoid_header(&mut digester, O::NAME, hashed_len); 86 | 87 | let mut reader = AsyncBufReader::new(reader); 88 | 89 | loop { 90 | let buffer = reader 91 | .fill_buf() 92 | .await 93 | .map_err(|source| ArtifactIdError::FailedRead(Box::new(source)))?; 94 | let amount_read = buffer.len(); 95 | 96 | if amount_read == 0 { 97 | break; 98 | } 99 | 100 | digest_with_normalized_newlines(&mut digester, buffer); 101 | 102 | reader.consume(amount_read); 103 | } 104 | 105 | let hash = digester.finalize(); 106 | 107 | Ok(GitOid { 108 | _phantom: PhantomData, 109 | value: ::Array::from_iter(hash), 110 | }) 111 | } 112 | 113 | /// Digest the "header" required for a GitOID. 114 | #[inline] 115 | fn digest_gitoid_header(digester: &mut impl Digest, object_type: &str, object_len: usize) { 116 | digester.update(object_type.as_bytes()); 117 | digester.update(b" "); 118 | digester.update(object_len.to_string().as_bytes()); 119 | digester.update(b"\0"); 120 | } 121 | 122 | /// Update a hash digest with data in a buffer, normalizing newlines. 123 | #[inline] 124 | fn digest_with_normalized_newlines(digester: &mut impl Digest, buf: &[u8]) { 125 | for chunk in buf.chunk_by(|char1, _| *char1 != b'\r') { 126 | let chunk = match chunk.last() { 127 | // Omit the carriage return at the end of the chunk. 128 | Some(b'\r') => &chunk[0..(chunk.len() - 1)], 129 | _ => chunk, 130 | }; 131 | 132 | digester.update(chunk) 133 | } 134 | } 135 | 136 | /// Count carriage returns in an in-memory buffer. 137 | #[inline(always)] 138 | fn num_carriage_returns_in_buffer(buffer: &[u8]) -> usize { 139 | bytecount::count(buffer, b'\r') 140 | } 141 | 142 | /// Read a seek-able stream and reset to the beginning when done. 143 | fn read_and_reset(reader: R, f: F) -> Result<(usize, R), ArtifactIdError> 144 | where 145 | R: Read + Seek, 146 | F: Fn(R) -> Result<(usize, R), ArtifactIdError>, 147 | { 148 | let (data, mut reader) = f(reader)?; 149 | reader 150 | .seek(SeekFrom::Start(0)) 151 | .map_err(|source| ArtifactIdError::FailedSeek(SeekFrom::Start(0), Box::new(source)))?; 152 | Ok((data, reader)) 153 | } 154 | 155 | /// Count carriage returns in a reader. 156 | fn num_carriage_returns_in_reader(reader: R) -> Result<(usize, R), ArtifactIdError> 157 | where 158 | R: Read + Seek, 159 | { 160 | read_and_reset(reader, |reader| { 161 | let mut buf_reader = BufReader::new(reader); 162 | let mut total_dos_newlines = 0; 163 | 164 | buf_reader.for_each_buf_fill(|buf| { 165 | // The number of separators is the number of chunks minus one. 166 | total_dos_newlines += buf.chunk_by(|char1, _| *char1 != b'\r').count() - 1 167 | })?; 168 | 169 | Ok((total_dos_newlines, buf_reader.into_inner())) 170 | }) 171 | } 172 | 173 | /// Count carriage returns in a reader. 174 | async fn num_carriage_returns_in_async_reader(reader: R) -> Result<(usize, R), ArtifactIdError> 175 | where 176 | R: AsyncRead + AsyncSeek + Unpin, 177 | { 178 | let mut reader = AsyncBufReader::new(reader); 179 | let mut total_dos_newlines = 0; 180 | 181 | loop { 182 | let buffer = reader 183 | .fill_buf() 184 | .await 185 | .map_err(|source| ArtifactIdError::FailedRead(Box::new(source)))?; 186 | let amount_read = buffer.len(); 187 | 188 | if amount_read == 0 { 189 | break; 190 | } 191 | 192 | total_dos_newlines += buffer.chunk_by(|char1, _| *char1 != b'\r').count() - 1; 193 | 194 | reader.consume(amount_read); 195 | } 196 | 197 | let (data, mut reader) = (total_dos_newlines, reader.into_inner()); 198 | reader 199 | .seek(SeekFrom::Start(0)) 200 | .await 201 | .map_err(|source| ArtifactIdError::FailedSeek(SeekFrom::Start(0), Box::new(source)))?; 202 | Ok((data, reader)) 203 | } 204 | -------------------------------------------------------------------------------- /omnibor/src/gitoid/mod.rs: -------------------------------------------------------------------------------- 1 | //! A content-addressable identity for software artifacts. 2 | //! 3 | //! ## What are GitOIDs? 4 | //! 5 | //! Git Object Identifiers ([GitOIDs][gitoid]) are a mechanism for 6 | //! identifying artifacts in a manner which is independently reproducible 7 | //! because it relies only on the contents of the artifact itself. 8 | //! 9 | //! The GitOID scheme comes from the Git version control system, which uses 10 | //! this mechanism to identify commits, tags, files (called "blobs"), and 11 | //! directories (called "trees"). 12 | //! 13 | //! This implementation of GitOIDs is produced by the [OmniBOR][omnibor] 14 | //! working group, which uses GitOIDs as the basis for OmniBOR Artifact 15 | //! Identifiers. 16 | //! 17 | //! ### GitOID URL Scheme 18 | //! 19 | //! `gitoid` is also an IANA-registered URL scheme, meaning that GitOIDs 20 | //! are represented and shared as URLs. A `gitoid` URL looks like: 21 | //! 22 | //! ```text 23 | //! gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03 24 | //! ``` 25 | //! 26 | //! This scheme starts with "`gitoid`", followed by the object type 27 | //! ("`blob`" in this case), the hash algorithm ("`sha256`"), and the 28 | //! hash produced by the GitOID hash construction. Each of these parts is 29 | //! separated by a colon. 30 | //! 31 | //! ### GitOID Hash Construction 32 | //! 33 | //! GitOID hashes are made by hashing a prefix string containing the object 34 | //! type and the size of the object being hashed in bytes, followed by a null 35 | //! terminator, and then hashing the object itself. So GitOID hashes do not 36 | //! match the result of only hashing the object. 37 | //! 38 | //! ### GitOID Object Types 39 | //! 40 | //! The valid object types for a GitOID are: 41 | //! 42 | //! - `blob` 43 | //! - `tree` 44 | //! - `commit` 45 | //! - `tag` 46 | //! 47 | //! Currently, this crate implements convenient handling of `blob` objects, 48 | //! but does not handle ensuring the proper formatting of `tree`, `commit`, 49 | //! or `tag` objects to match the Git implementation. 50 | //! 51 | //! ### GitOID Hash Algorithms 52 | //! 53 | //! The valid hash algorithms are: 54 | //! 55 | //! - `sha1` 56 | //! - `sha1dc` 57 | //! - `sha256` 58 | //! 59 | //! `sha1dc` is actually Git's default algorithm, and is equivalent to `sha1` 60 | //! in _most_ cases. Where it differs is when the hasher detects what it 61 | //! believes to be an attempt to generate a purposeful SHA-1 collision, 62 | //! in which case it modifies the hash process to produce a different output 63 | //! and avoid the malicious collision. 64 | //! 65 | //! Git does this under the hood, but does not clearly distinguish to end 66 | //! users that the underlying hashing algorithm isn't equivalent to SHA-1. 67 | //! This is fine for Git, where the specific hash used is an implementation 68 | //! detail and only matters within a single repository, but for the OmniBOR 69 | //! working group it's important to distinguish whether plain SHA-1 or 70 | //! SHA-1DC is being used, so it's distinguished in the code for this crate. 71 | //! 72 | //! This means for compatibility with Git that SHA-1DC should be used. 73 | //! 74 | //! ## Why Care About GitOIDs? 75 | //! 76 | //! GitOIDs provide a convenient mechanism to establish artifact identity and 77 | //! validate artifact integrity (this artifact hasn't been modified) and 78 | //! agreement (I have the same artifact you have). The fact that they're based 79 | //! only on the type of object ("`blob`", usually) and the artifact itself 80 | //! means they can be derived independently, enabling distributed artifact 81 | //! identification that avoids a central decider. 82 | //! 83 | //! Alternative identity schemes, like Package URLs (purls) or Common Platform 84 | //! Enumerations (CPEs) rely on central authorities to produce identifiers or 85 | //! define the taxonomy in which identifiers are produced. 86 | //! 87 | //! ## Using this Crate 88 | //! 89 | //! The central type of this crate is [`GitOid`], which is generic over both 90 | //! the hash algorithm used and the object type being identified. These are 91 | //! defined by the [`HashAlgorithm`] and [`ObjectType`] traits. 92 | //! 93 | //! ## Example 94 | //! 95 | //! ```text 96 | //! # use gitoid::{Sha256, Blob}; 97 | //! type GitOid = gitoid::GitOid; 98 | //! 99 | //! let gitoid = GitOid::from_str("hello, world"); 100 | //! println!("gitoid: {}", gitoid); 101 | //! ``` 102 | //! 103 | //! [gitoid]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects 104 | //! [omnibor]: https://omnibor.io 105 | 106 | mod gitoid; 107 | mod gitoid_url_parser; 108 | pub(crate) mod internal; 109 | 110 | pub use crate::gitoid::gitoid::GitOid; 111 | -------------------------------------------------------------------------------- /omnibor/src/hash_algorithm.rs: -------------------------------------------------------------------------------- 1 | //! Hash algorithms supported for Artifact IDs. 2 | //! 3 | //! __See [Hash Algorithms and Hash Providers][idx] documentation for more info.__ 4 | //! 5 | //! [idx]: crate#hash-algorithms-and-hash-providers 6 | 7 | use { 8 | crate::util::sealed::Sealed, 9 | digest::{ 10 | consts::U32, 11 | generic_array::{sequence::GenericSequence, GenericArray}, 12 | }, 13 | std::{fmt::Debug, ops::Deref}, 14 | }; 15 | 16 | #[cfg(doc)] 17 | use crate::ArtifactId; 18 | 19 | /// Marker trait for hash algorithms supported for constructing [`ArtifactId`]s. 20 | /// 21 | /// This trait is sealed, meaning it can only be implemented within the 22 | /// `omnibor` crate. 23 | pub trait HashAlgorithm: Sealed { 24 | /// The name of the hash algorithm, to be written in the GitOid string. 25 | #[doc(hidden)] 26 | const NAME: &'static str; 27 | 28 | /// The array type generated by the hash. 29 | #[doc(hidden)] 30 | type Array: GenericSequence 31 | + FromIterator 32 | + Deref 33 | + PartialEq 34 | + Eq 35 | + Clone 36 | + Copy 37 | + Debug; 38 | } 39 | 40 | /// Use SHA-256 as the hash algorithm. 41 | pub struct Sha256 { 42 | #[doc(hidden)] 43 | _private: (), 44 | } 45 | 46 | impl Sealed for Sha256 {} 47 | 48 | impl HashAlgorithm for Sha256 { 49 | const NAME: &'static str = "sha256"; 50 | 51 | // A SHA-256 hash is 32 bytes long. 52 | type Array = GenericArray; 53 | } 54 | -------------------------------------------------------------------------------- /omnibor/src/hash_provider/boringssl.rs: -------------------------------------------------------------------------------- 1 | //! BoringSSL-based cryptography backend. 2 | 3 | #![allow(clippy::new_without_default)] 4 | 5 | use { 6 | super::HashProvider, 7 | crate::hash_algorithm::Sha256, 8 | boring::sha, 9 | digest::{consts::U32, FixedOutput, HashMarker, Output, OutputSizeUser, Update}, 10 | }; 11 | 12 | /// Use the BoringSSL hash implementation. 13 | #[cfg_attr(docsrs, doc(cfg(feature = "backend-boringssl")))] 14 | #[derive(Clone, Copy)] 15 | pub struct BoringSsl { 16 | #[doc(hidden)] 17 | _phantom: (), 18 | } 19 | 20 | impl BoringSsl { 21 | /// Construct a new `BoringSsl` provider. 22 | pub fn new() -> Self { 23 | BoringSsl { _phantom: () } 24 | } 25 | } 26 | 27 | impl HashProvider for BoringSsl { 28 | type Digester = Sha256Digester; 29 | 30 | fn digester(&self) -> Self::Digester { 31 | Sha256Digester::default() 32 | } 33 | } 34 | 35 | /// `boringssl` SHA-256 implementing the `Digest` trait. 36 | #[doc(hidden)] 37 | pub struct Sha256Digester { 38 | hash: sha::Sha256, 39 | } 40 | 41 | impl Update for Sha256Digester { 42 | fn update(&mut self, data: &[u8]) { 43 | self.hash.update(data); 44 | } 45 | } 46 | 47 | impl OutputSizeUser for Sha256Digester { 48 | type OutputSize = U32; 49 | } 50 | 51 | impl FixedOutput for Sha256Digester { 52 | fn finalize_into(self, out: &mut Output) { 53 | out.copy_from_slice(self.hash.finish().as_slice()); 54 | } 55 | 56 | fn finalize_fixed(self) -> Output { 57 | let mut out = Output::::default(); 58 | out.copy_from_slice(self.hash.finish().as_slice()); 59 | out 60 | } 61 | } 62 | 63 | impl HashMarker for Sha256Digester {} 64 | 65 | impl Default for Sha256Digester { 66 | fn default() -> Self { 67 | Self { 68 | hash: sha::Sha256::new(), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /omnibor/src/hash_provider/mod.rs: -------------------------------------------------------------------------------- 1 | //! Cryptography libraries providing hash function implementations. 2 | //! 3 | //! __See [Hash Algorithms and Hash Providers][idx] documentation for more info.__ 4 | //! 5 | //! [idx]: crate#hash-algorithms-and-hash-providers 6 | 7 | #[cfg(doc)] 8 | use crate::artifact_id::ArtifactId; 9 | use crate::hash_algorithm::HashAlgorithm; 10 | use digest::Digest; 11 | 12 | #[cfg(feature = "backend-boringssl")] 13 | mod boringssl; 14 | #[cfg(feature = "backend-boringssl")] 15 | pub use crate::hash_provider::boringssl::BoringSsl; 16 | 17 | #[cfg(feature = "backend-openssl")] 18 | mod openssl; 19 | #[cfg(feature = "backend-openssl")] 20 | pub use crate::hash_provider::openssl::OpenSsl; 21 | 22 | #[cfg(feature = "backend-rustcrypto")] 23 | mod rustcrypto; 24 | #[cfg(feature = "backend-rustcrypto")] 25 | pub use crate::hash_provider::rustcrypto::RustCrypto; 26 | 27 | /// A cryptography library for producing [`ArtifactId`]s with SHA-256. 28 | pub trait HashProvider: Copy { 29 | /// The type used to produce the SHA-256 digest. 30 | type Digester: Digest; 31 | 32 | /// Get the SHA-256 digester. 33 | fn digester(&self) -> Self::Digester; 34 | } 35 | -------------------------------------------------------------------------------- /omnibor/src/hash_provider/openssl.rs: -------------------------------------------------------------------------------- 1 | //! OpenSSL-based cryptography backend. 2 | 3 | #![allow(clippy::new_without_default)] 4 | 5 | use { 6 | super::HashProvider, 7 | crate::hash_algorithm::Sha256, 8 | digest::{consts::U32, FixedOutput, HashMarker, Output, OutputSizeUser, Update}, 9 | openssl::sha, 10 | }; 11 | 12 | /// Use the OpenSSL hash implementation. 13 | #[cfg_attr(docsrs, doc(cfg(feature = "backend-openssl")))] 14 | #[derive(Clone, Copy)] 15 | pub struct OpenSsl { 16 | #[doc(hidden)] 17 | _phantom: (), 18 | } 19 | 20 | impl OpenSsl { 21 | /// Construct a new `OpenSsl` provider. 22 | pub fn new() -> Self { 23 | OpenSsl { _phantom: () } 24 | } 25 | } 26 | 27 | impl HashProvider for OpenSsl { 28 | type Digester = Sha256Digester; 29 | 30 | fn digester(&self) -> Self::Digester { 31 | Sha256Digester::default() 32 | } 33 | } 34 | 35 | /// `openssl` SHA-256 implementing the `Digest` trait. 36 | #[doc(hidden)] 37 | pub struct Sha256Digester { 38 | hash: sha::Sha256, 39 | } 40 | 41 | impl Update for Sha256Digester { 42 | fn update(&mut self, data: &[u8]) { 43 | self.hash.update(data); 44 | } 45 | } 46 | 47 | impl OutputSizeUser for Sha256Digester { 48 | type OutputSize = U32; 49 | } 50 | 51 | impl FixedOutput for Sha256Digester { 52 | fn finalize_into(self, out: &mut Output) { 53 | out.copy_from_slice(self.hash.finish().as_slice()); 54 | } 55 | 56 | fn finalize_fixed(self) -> Output { 57 | let mut out = Output::::default(); 58 | out.copy_from_slice(self.hash.finish().as_slice()); 59 | out 60 | } 61 | } 62 | 63 | impl HashMarker for Sha256Digester {} 64 | 65 | impl Default for Sha256Digester { 66 | fn default() -> Self { 67 | Self { 68 | hash: sha::Sha256::new(), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /omnibor/src/hash_provider/rustcrypto.rs: -------------------------------------------------------------------------------- 1 | //! RustCrypto-based cryptography backend. 2 | 3 | #![allow(clippy::derivable_impls)] 4 | #![allow(clippy::new_without_default)] 5 | 6 | use { 7 | super::HashProvider, 8 | crate::hash_algorithm::Sha256, 9 | digest::{consts::U32, FixedOutput, HashMarker, Output, OutputSizeUser, Update}, 10 | sha2 as sha, 11 | }; 12 | 13 | /// Use the RustCrypto hash implementation. 14 | #[cfg_attr(docsrs, doc(cfg(feature = "backend-rustcrypto")))] 15 | #[derive(Clone, Copy)] 16 | pub struct RustCrypto { 17 | #[doc(hidden)] 18 | _phantom: (), 19 | } 20 | 21 | impl RustCrypto { 22 | /// Construct a new `RustCrypto` provider. 23 | pub fn new() -> Self { 24 | RustCrypto { _phantom: () } 25 | } 26 | } 27 | 28 | impl HashProvider for RustCrypto { 29 | type Digester = Sha256Digester; 30 | 31 | fn digester(&self) -> Self::Digester { 32 | Sha256Digester::default() 33 | } 34 | } 35 | 36 | /// `rustcrypto` SHA-256 implementing the `Digest` trait. 37 | /// 38 | /// This just wraps the internal type that already implements `Digest` for 39 | /// consistency with the other cryptography providers. 40 | #[doc(hidden)] 41 | pub struct Sha256Digester { 42 | hash: sha::Sha256, 43 | } 44 | 45 | impl Update for Sha256Digester { 46 | fn update(&mut self, data: &[u8]) { 47 | Update::update(&mut self.hash, data); 48 | } 49 | } 50 | 51 | impl OutputSizeUser for Sha256Digester { 52 | type OutputSize = U32; 53 | } 54 | 55 | impl FixedOutput for Sha256Digester { 56 | fn finalize_into(self, out: &mut Output) { 57 | FixedOutput::finalize_into(self.hash, out) 58 | } 59 | 60 | fn finalize_fixed(self) -> Output { 61 | self.hash.finalize_fixed() 62 | } 63 | } 64 | 65 | impl HashMarker for Sha256Digester {} 66 | 67 | impl Default for Sha256Digester { 68 | fn default() -> Self { 69 | Self { 70 | hash: sha::Sha256::default(), 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /omnibor/src/input_manifest/input_manifest.rs: -------------------------------------------------------------------------------- 1 | //! [`InputManifest`] type that represents build inputs for an artifact. 2 | 3 | use { 4 | crate::{ 5 | artifact_id::ArtifactId, 6 | error::InputManifestError, 7 | hash_algorithm::HashAlgorithm, 8 | object_type::{Blob, ObjectType}, 9 | util::clone_as_boxstr::CloneAsBoxstr, 10 | }, 11 | std::{ 12 | cmp::Ordering, 13 | fmt::{Debug, Formatter, Result as FmtResult}, 14 | fs::File, 15 | io::{BufRead, BufReader, Write}, 16 | path::Path, 17 | str::FromStr, 18 | }, 19 | }; 20 | 21 | /// A manifest describing the inputs used to build an artifact. 22 | /// 23 | /// The manifest is constructed with a specific target artifact in mind. 24 | /// The rest of the manifest then describes relations to other artifacts. 25 | /// Each relation can be thought of as describing edges between nodes 26 | /// in an Artifact Dependency Graph. 27 | /// 28 | /// Each relation encodes a kind which describes how the other artifact 29 | /// relates to the target artifact. 30 | /// 31 | /// Relations may additionally refer to the [`InputManifest`] of the 32 | /// related artifact. 33 | #[derive(PartialEq, Eq)] 34 | pub struct InputManifest { 35 | /// The artifact the manifest is describing. 36 | /// 37 | /// A manifest without this is "detached" because we don't know 38 | /// what artifact it's describing. 39 | target: Option>, 40 | 41 | /// The relations recorded in the manifest. 42 | relations: Vec>, 43 | } 44 | 45 | impl InputManifest { 46 | pub(crate) fn with_relations( 47 | relations: impl Iterator>, 48 | ) -> Self { 49 | InputManifest { 50 | target: None, 51 | relations: relations.collect(), 52 | } 53 | } 54 | 55 | /// Get the ID of the artifact this manifest is describing. 56 | /// 57 | /// If the manifest does not have a target, it is a "detached" manifest. 58 | /// 59 | /// Detached manifests may still be usable if the target artifact was 60 | /// created in embedding mode, in which case it will carry the [`ArtifactId`] 61 | /// of its own input manifest in its contents. 62 | #[inline] 63 | pub fn target(&self) -> Option> { 64 | self.target 65 | } 66 | 67 | /// Identify if the manifest is a "detached" manifest. 68 | /// 69 | /// "Detached" manifests are ones without a target [`ArtifactId`]. 70 | pub fn is_detached(&self) -> bool { 71 | self.target.is_none() 72 | } 73 | 74 | /// Set a new target. 75 | pub(crate) fn set_target(&mut self, target: Option>) -> &mut Self { 76 | self.target = target; 77 | self 78 | } 79 | 80 | /// Get the relations inside an [`InputManifest`]. 81 | #[inline] 82 | pub fn relations(&self) -> &[InputManifestRelation] { 83 | &self.relations[..] 84 | } 85 | 86 | /// Construct an [`InputManifest`] from a file at a specified path. 87 | pub fn from_path(path: &Path) -> Result { 88 | let file = BufReader::new( 89 | File::open(path) 90 | .map_err(|source| InputManifestError::FailedManifestRead(Box::new(source)))?, 91 | ); 92 | let mut lines = file.lines(); 93 | 94 | let first_line = lines 95 | .next() 96 | .ok_or(InputManifestError::ManifestMissingHeader)? 97 | .map_err(|source| InputManifestError::FailedManifestRead(Box::new(source)))?; 98 | 99 | let parts = first_line.split(':').collect::>(); 100 | 101 | if parts.len() != 3 { 102 | return Err(InputManifestError::MissingHeaderParts); 103 | } 104 | 105 | // Panic Safety: we've already checked the length. 106 | let (gitoid, blob, hash_algorithm) = (parts[0], parts[1], parts[2]); 107 | 108 | if gitoid != "gitoid" { 109 | return Err(InputManifestError::MissingGitOidInHeader); 110 | } 111 | 112 | if blob != "blob" { 113 | return Err(InputManifestError::MissingObjectTypeInHeader); 114 | } 115 | 116 | if hash_algorithm != H::NAME { 117 | return Err(InputManifestError::WrongHashAlgorithm { 118 | expected: H::NAME.clone_as_boxstr(), 119 | got: hash_algorithm.clone_as_boxstr(), 120 | }); 121 | } 122 | 123 | let mut relations = Vec::new(); 124 | for line in lines { 125 | let line = 126 | line.map_err(|source| InputManifestError::FailedManifestRead(Box::new(source)))?; 127 | let relation = parse_relation::(&line)?; 128 | relations.push(relation); 129 | } 130 | 131 | Ok(InputManifest { 132 | target: None, 133 | relations, 134 | }) 135 | } 136 | 137 | /// Write the manifest out at the given path. 138 | pub fn as_bytes(&self) -> Vec { 139 | let mut bytes = vec![]; 140 | 141 | // Per the spec, this prefix is present to substantially shorten 142 | // the metadata info that would otherwise be attached to all IDs in 143 | // a manifest if they were written in full form. Instead, only the 144 | // hex-encoded hashes are recorded elsewhere, because all the metadata 145 | // is identical in a manifest and only recorded once at the beginning. 146 | let _ = writeln!(bytes, "gitoid:{}:{}", Blob::NAME, H::NAME); 147 | 148 | for relation in &self.relations { 149 | let aid = relation.artifact; 150 | 151 | let _ = write!(bytes, "{}", aid.as_hex()); 152 | 153 | if let Some(mid) = relation.manifest { 154 | let _ = write!(bytes, " manifest {}", mid.as_hex()); 155 | } 156 | 157 | let _ = writeln!(bytes); 158 | } 159 | 160 | bytes 161 | } 162 | } 163 | 164 | impl Debug for InputManifest { 165 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 166 | f.debug_struct("InputManifest") 167 | .field("target", &self.target) 168 | .field("relations", &self.relations) 169 | .finish() 170 | } 171 | } 172 | 173 | impl Clone for InputManifest { 174 | fn clone(&self) -> Self { 175 | InputManifest { 176 | target: self.target, 177 | relations: self.relations.clone(), 178 | } 179 | } 180 | } 181 | 182 | /// Parse a single relation line. 183 | fn parse_relation( 184 | input: &str, 185 | ) -> Result, InputManifestError> { 186 | let parts = input.split(' ').collect::>(); 187 | 188 | if parts.len() < 2 { 189 | return Err(InputManifestError::MissingRelationParts); 190 | } 191 | 192 | // Panic Safety: we've already checked the length. 193 | let (aid_hex, manifest_indicator, manifest_aid_hex) = (parts[0], parts.get(1), parts.get(2)); 194 | 195 | let artifact = 196 | ArtifactId::::from_str(&format!("gitoid:{}:{}:{}", Blob::NAME, H::NAME, aid_hex))?; 197 | 198 | let manifest = match (manifest_indicator, manifest_aid_hex) { 199 | (None, None) | (Some(_), None) | (None, Some(_)) => None, 200 | (Some(manifest_indicator), Some(manifest_aid_hex)) => { 201 | if *manifest_indicator != "manifest" { 202 | return Err(InputManifestError::MissingBomIndicatorInRelation); 203 | } 204 | 205 | let gitoid_url = &format!("gitoid:{}:{}:{}", Blob::NAME, H::NAME, manifest_aid_hex); 206 | 207 | ArtifactId::::from_str(gitoid_url).ok() 208 | } 209 | }; 210 | 211 | Ok(InputManifestRelation { artifact, manifest }) 212 | } 213 | 214 | /// A single row in an [`InputManifest`]. 215 | #[derive(Copy)] 216 | pub struct InputManifestRelation { 217 | /// The ID of the artifact itself. 218 | artifact: ArtifactId, 219 | 220 | /// The ID of the manifest, if a manifest is present. 221 | manifest: Option>, 222 | } 223 | 224 | // We implement this ourselves instead of deriving it because 225 | // the auto-derive logic will only conditionally derive it based 226 | // on whether the `H` type parameter implements `Debug`, which 227 | // isn't actually relevant in this case because we don't _really_ 228 | // store a value of type-`H`, we just use it for type-level 229 | // programming. 230 | impl Debug for InputManifestRelation { 231 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 232 | f.debug_struct("Relation") 233 | .field("artifact", &self.artifact) 234 | .field("manifest", &self.manifest) 235 | .finish() 236 | } 237 | } 238 | 239 | impl Clone for InputManifestRelation { 240 | fn clone(&self) -> Self { 241 | InputManifestRelation { 242 | artifact: self.artifact, 243 | manifest: self.manifest, 244 | } 245 | } 246 | } 247 | 248 | impl PartialEq for InputManifestRelation { 249 | fn eq(&self, other: &Self) -> bool { 250 | self.artifact.eq(&other.artifact) && self.manifest.eq(&other.manifest) 251 | } 252 | } 253 | 254 | impl Eq for InputManifestRelation {} 255 | 256 | impl PartialOrd for InputManifestRelation { 257 | fn partial_cmp(&self, other: &Self) -> Option { 258 | Some(self.cmp(other)) 259 | } 260 | } 261 | 262 | impl Ord for InputManifestRelation { 263 | fn cmp(&self, other: &Self) -> Ordering { 264 | self.artifact.cmp(&other.artifact) 265 | } 266 | } 267 | 268 | impl InputManifestRelation { 269 | pub(crate) fn new( 270 | artifact: ArtifactId, 271 | manifest: Option>, 272 | ) -> InputManifestRelation { 273 | InputManifestRelation { artifact, manifest } 274 | } 275 | 276 | /// Get the ID of the artifact. 277 | #[inline] 278 | pub fn artifact(&self) -> ArtifactId { 279 | self.artifact 280 | } 281 | 282 | /// Get the manifest ID, if present. 283 | #[inline] 284 | pub fn manifest(&self) -> Option> { 285 | self.manifest 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /omnibor/src/input_manifest/input_manifest_builder.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | artifact_id::{ArtifactId, ArtifactIdBuilder}, 4 | embedding_mode::EmbeddingMode, 5 | error::InputManifestError, 6 | hash_algorithm::HashAlgorithm, 7 | hash_provider::HashProvider, 8 | input_manifest::{InputManifest, InputManifestRelation}, 9 | storage::Storage, 10 | }, 11 | std::{ 12 | collections::BTreeSet, 13 | fmt::{Debug, Formatter, Result as FmtResult}, 14 | fs::{File, OpenOptions}, 15 | path::Path, 16 | }, 17 | }; 18 | 19 | /// A builder for [`InputManifest`]s. 20 | pub struct InputManifestBuilder, S: Storage> { 21 | /// The relations to be written to a new manifest by this transaction. 22 | relations: BTreeSet>, 23 | 24 | /// Indicates whether manifests should be embedded in the artifact or not. 25 | mode: EmbeddingMode, 26 | 27 | /// The cryptography library providing the hash implementation. 28 | hash_provider: P, 29 | 30 | /// The storage system used to store manifests. 31 | storage: S, 32 | } 33 | 34 | impl, S: Storage> Debug for InputManifestBuilder { 35 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 36 | f.debug_struct("InputManifestBuilder") 37 | .field("mode", &self.mode) 38 | .field("relations", &self.relations) 39 | .finish_non_exhaustive() 40 | } 41 | } 42 | 43 | impl, S: Storage> InputManifestBuilder { 44 | /// Construct a new [`InputManifestBuilder`]. 45 | pub fn new(mode: EmbeddingMode, storage: S, hash_provider: P) -> Self { 46 | Self { 47 | relations: BTreeSet::new(), 48 | mode, 49 | storage, 50 | hash_provider, 51 | } 52 | } 53 | 54 | /// Add a relation to an artifact to the transaction. 55 | /// 56 | /// If an Input Manifest for the given `ArtifactId` is found in the storage 57 | /// this builder is using, then this relation will also include the 58 | /// `ArtifactId` of that Input Manifest. 59 | pub fn add_relation( 60 | &mut self, 61 | artifact: ArtifactId, 62 | ) -> Result<&mut Self, InputManifestError> { 63 | let manifest = self.storage.get_manifest_id_for_artifact(artifact)?; 64 | self.relations 65 | .insert(InputManifestRelation::new(artifact, manifest)); 66 | Ok(self) 67 | } 68 | 69 | /// Finish building the manifest, updating the artifact if embedding is on. 70 | pub fn finish(&mut self, target: &Path) -> Result, InputManifestError> { 71 | let builder = ArtifactIdBuilder::with_provider(self.hash_provider); 72 | 73 | // Construct a new input manifest. 74 | let mut manifest = InputManifest::with_relations(self.relations.iter().cloned()); 75 | 76 | // Write the manifest to storage. 77 | let manifest_aid = self.storage.write_manifest(&manifest)?; 78 | 79 | // Get the ArtifactID of the target, possibly embedding the 80 | // manifest ArtifactID into the target first. 81 | let target_aid = match self.mode { 82 | EmbeddingMode::Embed => { 83 | let mut file = OpenOptions::new() 84 | .read(true) 85 | .write(true) 86 | .open(target) 87 | .map_err(|source| { 88 | InputManifestError::FailedTargetArtifactRead(Box::new(source)) 89 | })?; 90 | embed_manifest_in_target(target, &mut file, manifest_aid)?; 91 | builder.identify_file(&mut file)? 92 | } 93 | EmbeddingMode::NoEmbed => { 94 | let mut file = File::open(target).map_err(|source| { 95 | InputManifestError::FailedTargetArtifactRead(Box::new(source)) 96 | })?; 97 | builder.identify_file(&mut file)? 98 | } 99 | }; 100 | 101 | // Update the manifest in storage with the target ArtifactID. 102 | self.storage 103 | .update_target_for_manifest(manifest_aid, target_aid)?; 104 | 105 | // Update the manifest in memory with the target ArtifactID. 106 | manifest.set_target(Some(target_aid)); 107 | 108 | // Clear out the set of relations so you can reuse the builder. 109 | self.relations.clear(); 110 | 111 | Ok(manifest) 112 | } 113 | 114 | /// Access the underlying storage for the builder. 115 | pub fn storage(&self) -> &S { 116 | &self.storage 117 | } 118 | } 119 | 120 | /// Embed the manifest's [`ArtifactId`] into the target file. 121 | fn embed_manifest_in_target( 122 | path: &Path, 123 | file: &mut File, 124 | manifest_aid: ArtifactId, 125 | ) -> Result, InputManifestError> { 126 | match TargetType::infer(path, file) { 127 | TargetType::KnownBinaryType(BinaryType::ElfFile) => { 128 | embed_in_elf_file(path, file, manifest_aid) 129 | } 130 | TargetType::KnownTextType(TextType::PrefixComments { prefix }) => { 131 | embed_in_text_file_with_prefix_comment(path, file, manifest_aid, &prefix) 132 | } 133 | TargetType::KnownTextType(TextType::WrappedComments { prefix, suffix }) => { 134 | embed_in_text_file_with_wrapped_comment(path, file, manifest_aid, &prefix, &suffix) 135 | } 136 | TargetType::Unknown => Err(InputManifestError::UnknownEmbeddingTarget), 137 | } 138 | } 139 | 140 | fn embed_in_elf_file( 141 | _path: &Path, 142 | _file: &mut File, 143 | _manifest_aid: ArtifactId, 144 | ) -> Result, InputManifestError> { 145 | todo!("embedding mode for ELF files is not yet implemented") 146 | } 147 | 148 | fn embed_in_text_file_with_prefix_comment( 149 | _path: &Path, 150 | _file: &mut File, 151 | _manifest_aid: ArtifactId, 152 | _prefix: &str, 153 | ) -> Result, InputManifestError> { 154 | todo!("embedding mode for text files is not yet implemented") 155 | } 156 | 157 | fn embed_in_text_file_with_wrapped_comment( 158 | _path: &Path, 159 | _file: &mut File, 160 | _manifest_aid: ArtifactId, 161 | _prefix: &str, 162 | _suffix: &str, 163 | ) -> Result, InputManifestError> { 164 | todo!("embedding mode for text files is not yet implemented") 165 | } 166 | 167 | #[allow(unused)] 168 | #[derive(Debug)] 169 | enum TargetType { 170 | KnownBinaryType(BinaryType), 171 | KnownTextType(TextType), 172 | Unknown, 173 | } 174 | 175 | impl TargetType { 176 | fn infer(_path: &Path, _file: &File) -> Self { 177 | todo!("inferring target file type is not yet implemented") 178 | } 179 | } 180 | 181 | #[allow(unused)] 182 | #[derive(Debug)] 183 | enum BinaryType { 184 | ElfFile, 185 | } 186 | 187 | #[allow(unused)] 188 | #[derive(Debug)] 189 | enum TextType { 190 | PrefixComments { prefix: String }, 191 | WrappedComments { prefix: String, suffix: String }, 192 | } 193 | 194 | #[cfg(test)] 195 | mod tests { 196 | use { 197 | super::*, 198 | crate::{ 199 | embedding_mode::EmbeddingMode, 200 | hash_algorithm::Sha256, 201 | pathbuf, 202 | storage::{FileSystemStorage, InMemoryStorage}, 203 | }, 204 | }; 205 | 206 | #[cfg(feature = "backend-rustcrypto")] 207 | /// A basic builder test that creates a single manifest and validates it. 208 | fn basic_builder_test(storage: impl Storage) { 209 | use crate::hash_provider::RustCrypto; 210 | 211 | let builder = ArtifactIdBuilder::with_rustcrypto(); 212 | 213 | let target = pathbuf![ 214 | env!("CARGO_MANIFEST_DIR"), 215 | "test", 216 | "data", 217 | "hello_world.txt" 218 | ]; 219 | 220 | let first_input_aid = builder.identify_string("test_1"); 221 | let second_input_aid = builder.identify_string("test_2"); 222 | 223 | let manifest = InputManifestBuilder::::new( 224 | EmbeddingMode::NoEmbed, 225 | storage, 226 | RustCrypto::new(), 227 | ) 228 | .add_relation(first_input_aid) 229 | .unwrap() 230 | .add_relation(second_input_aid) 231 | .unwrap() 232 | .finish(&target) 233 | .unwrap(); 234 | 235 | // Check the first relation in the manifest. 236 | let first_relation = &manifest.relations()[0]; 237 | assert_eq!( 238 | first_relation.artifact().as_hex(), 239 | second_input_aid.as_hex() 240 | ); 241 | 242 | // Check the second relation in the manifest. 243 | let second_relation = &manifest.relations()[1]; 244 | assert_eq!( 245 | second_relation.artifact().as_hex(), 246 | first_input_aid.as_hex() 247 | ); 248 | } 249 | 250 | #[cfg(feature = "backend-rustcrypto")] 251 | #[test] 252 | fn in_memory_builder_works() { 253 | use crate::hash_provider::RustCrypto; 254 | 255 | let storage = InMemoryStorage::new(RustCrypto::new()); 256 | basic_builder_test(storage); 257 | } 258 | 259 | #[cfg(feature = "backend-rustcrypto")] 260 | #[test] 261 | fn file_system_builder_works() { 262 | use crate::hash_provider::RustCrypto; 263 | 264 | let storage_root = pathbuf![env!("CARGO_MANIFEST_DIR"), "test", "fs_storage"]; 265 | let mut storage = FileSystemStorage::new(RustCrypto::new(), &storage_root).unwrap(); 266 | basic_builder_test(&mut storage); 267 | storage.cleanup().unwrap(); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /omnibor/src/input_manifest/mod.rs: -------------------------------------------------------------------------------- 1 | //! Record of software build inputs by Artifact ID. 2 | 3 | pub mod input_manifest; 4 | pub mod input_manifest_builder; 5 | 6 | pub use input_manifest::InputManifest; 7 | pub use input_manifest::InputManifestRelation; 8 | pub use input_manifest_builder::InputManifestBuilder; 9 | -------------------------------------------------------------------------------- /omnibor/src/object_type.rs: -------------------------------------------------------------------------------- 1 | //! The types of objects for which a `GitOid` can be made. 2 | 3 | use crate::util::sealed::Sealed; 4 | 5 | #[cfg(doc)] 6 | use crate::gitoid::GitOid; 7 | 8 | /// Object types usable to construct a [`GitOid`]. 9 | /// 10 | /// This is a sealed trait to ensure it's only used for hash 11 | /// algorithms which are actually supported by Git. 12 | /// 13 | /// For more information on sealed traits, read Predrag 14 | /// Gruevski's ["A Definitive Guide to Sealed Traits in Rust"][1]. 15 | /// 16 | /// [1]: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ 17 | pub trait ObjectType: Sealed { 18 | #[doc(hidden)] 19 | const NAME: &'static str; 20 | } 21 | 22 | /// A Blob GitOid object. 23 | pub struct Blob { 24 | #[doc(hidden)] 25 | _private: (), 26 | } 27 | 28 | impl Sealed for Blob {} 29 | 30 | impl ObjectType for Blob { 31 | const NAME: &'static str = "blob"; 32 | } 33 | -------------------------------------------------------------------------------- /omnibor/src/storage/in_memory_storage.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | artifact_id::{ArtifactId, ArtifactIdBuilder}, 4 | error::InputManifestError, 5 | hash_algorithm::{HashAlgorithm, Sha256}, 6 | hash_provider::HashProvider, 7 | input_manifest::InputManifest, 8 | storage::Storage, 9 | }, 10 | std::fmt::Debug, 11 | }; 12 | 13 | /// In-memory storage for [`InputManifest`]s. 14 | /// 15 | /// Note that this "storage" doesn't persist anything. We use it for testing, and it 16 | /// may be useful in other applications where you only care about producing and using 17 | /// manifests in the short-term, and not in persisting them to a disk or some other 18 | /// durable location. 19 | #[derive(Debug)] 20 | pub struct InMemoryStorage> { 21 | /// The cryptography library providing a hash implementation. 22 | hash_provider: P, 23 | 24 | /// Stored SHA-256 [`InputManifest`]s. 25 | sha256_manifests: Vec>, 26 | } 27 | 28 | impl> InMemoryStorage

{ 29 | /// Construct a new `InMemoryStorage` instance. 30 | pub fn new(hash_provider: P) -> Self { 31 | Self { 32 | hash_provider, 33 | sha256_manifests: Vec::new(), 34 | } 35 | } 36 | 37 | /// Find the manifest entry that matches the target [`ArtifactId`] 38 | fn match_by_target_aid( 39 | &self, 40 | target_aid: ArtifactId, 41 | ) -> Option<&ManifestEntry> { 42 | self.sha256_manifests 43 | .iter() 44 | .find(|entry| entry.manifest.target() == Some(target_aid)) 45 | } 46 | } 47 | 48 | impl> Storage for InMemoryStorage

{ 49 | fn has_manifest_for_artifact(&self, target_aid: ArtifactId) -> bool { 50 | self.match_by_target_aid(target_aid).is_some() 51 | } 52 | 53 | fn get_manifest_for_artifact( 54 | &self, 55 | target_aid: ArtifactId, 56 | ) -> Result>, InputManifestError> { 57 | Ok(self 58 | .match_by_target_aid(target_aid) 59 | .map(|entry| entry.manifest.clone())) 60 | } 61 | 62 | fn get_manifest_id_for_artifact( 63 | &self, 64 | target_aid: ArtifactId, 65 | ) -> Result>, InputManifestError> { 66 | Ok(self 67 | .match_by_target_aid(target_aid) 68 | .and_then(|entry| entry.manifest.target())) 69 | } 70 | 71 | fn write_manifest( 72 | &mut self, 73 | manifest: &InputManifest, 74 | ) -> Result, InputManifestError> { 75 | let builder = ArtifactIdBuilder::with_provider(self.hash_provider); 76 | let manifest_aid = builder.identify_manifest(manifest); 77 | 78 | self.sha256_manifests.push(ManifestEntry { 79 | manifest_aid, 80 | manifest: manifest.clone(), 81 | }); 82 | 83 | Ok(manifest_aid) 84 | } 85 | 86 | fn update_target_for_manifest( 87 | &mut self, 88 | manifest_aid: ArtifactId, 89 | target_aid: ArtifactId, 90 | ) -> Result<(), InputManifestError> { 91 | self.sha256_manifests 92 | .iter_mut() 93 | .find(|entry| entry.manifest_aid == manifest_aid) 94 | .map(|entry| entry.manifest.set_target(Some(target_aid))); 95 | 96 | Ok(()) 97 | } 98 | 99 | fn get_manifests(&self) -> Result>, InputManifestError> { 100 | Ok(self 101 | .sha256_manifests 102 | .iter() 103 | .map(|entry| entry.manifest.clone()) 104 | .collect()) 105 | } 106 | } 107 | 108 | /// An entry in the in-memory manifest storage. 109 | struct ManifestEntry { 110 | /// The [`ArtifactId`] of the manifest. 111 | manifest_aid: ArtifactId, 112 | 113 | /// The manifest itself. 114 | manifest: InputManifest, 115 | } 116 | 117 | impl Debug for ManifestEntry { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | f.debug_struct("ManifestEntry") 120 | .field("manifest_aid", &self.manifest_aid) 121 | .field("manifest", &self.manifest) 122 | .finish() 123 | } 124 | } 125 | 126 | impl Clone for ManifestEntry { 127 | fn clone(&self) -> Self { 128 | ManifestEntry { 129 | manifest_aid: self.manifest_aid, 130 | manifest: self.manifest.clone(), 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /omnibor/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! How manifests are stored and accessed. 2 | //! 3 | //! __See [Storing Input Manifests][idx] documentation for more info.__ 4 | //! 5 | //! [idx]: crate#storing-input-manifests 6 | 7 | pub(crate) mod file_system_storage; 8 | pub(crate) mod in_memory_storage; 9 | #[cfg(test)] 10 | mod test; 11 | 12 | pub use crate::storage::file_system_storage::FileSystemStorage; 13 | pub use crate::storage::in_memory_storage::InMemoryStorage; 14 | 15 | use crate::{ 16 | artifact_id::ArtifactId, error::InputManifestError, hash_algorithm::HashAlgorithm, 17 | input_manifest::InputManifest, 18 | }; 19 | 20 | /// Represents the interface for storing and querying manifests. 21 | pub trait Storage { 22 | /// Check if we have the manifest for a specific artifact. 23 | fn has_manifest_for_artifact(&self, target_aid: ArtifactId) -> bool; 24 | 25 | /// Get the manifest for a specific artifact. 26 | fn get_manifest_for_artifact( 27 | &self, 28 | target_aid: ArtifactId, 29 | ) -> Result>, InputManifestError>; 30 | 31 | /// Get the ID of the manifest for the artifact. 32 | fn get_manifest_id_for_artifact( 33 | &self, 34 | target_aid: ArtifactId, 35 | ) -> Result>, InputManifestError>; 36 | 37 | /// Write a manifest to the storage. 38 | fn write_manifest( 39 | &mut self, 40 | manifest: &InputManifest, 41 | ) -> Result, InputManifestError>; 42 | 43 | /// Update the manifest file to reflect the target ID. 44 | fn update_target_for_manifest( 45 | &mut self, 46 | manifest_aid: ArtifactId, 47 | target_aid: ArtifactId, 48 | ) -> Result<(), InputManifestError>; 49 | 50 | /// Get all manifests from the storage. 51 | fn get_manifests(&self) -> Result>, InputManifestError>; 52 | } 53 | 54 | impl> Storage for &mut S { 55 | fn has_manifest_for_artifact(&self, target_aid: ArtifactId) -> bool { 56 | (**self).has_manifest_for_artifact(target_aid) 57 | } 58 | 59 | fn get_manifest_for_artifact( 60 | &self, 61 | target_aid: ArtifactId, 62 | ) -> Result>, InputManifestError> { 63 | (**self).get_manifest_for_artifact(target_aid) 64 | } 65 | 66 | fn get_manifest_id_for_artifact( 67 | &self, 68 | target_aid: ArtifactId, 69 | ) -> Result>, InputManifestError> { 70 | (**self).get_manifest_id_for_artifact(target_aid) 71 | } 72 | 73 | fn write_manifest( 74 | &mut self, 75 | manifest: &InputManifest, 76 | ) -> Result, InputManifestError> { 77 | (**self).write_manifest(manifest) 78 | } 79 | 80 | fn update_target_for_manifest( 81 | &mut self, 82 | manifest_aid: ArtifactId, 83 | target_aid: ArtifactId, 84 | ) -> Result<(), InputManifestError> { 85 | (**self).update_target_for_manifest(manifest_aid, target_aid) 86 | } 87 | 88 | fn get_manifests(&self) -> Result>, InputManifestError> { 89 | (**self).get_manifests() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /omnibor/src/storage/test.rs: -------------------------------------------------------------------------------- 1 | use super::FileSystemStorage; 2 | use crate::{artifact_id::ArtifactId, hash_algorithm::Sha256, pathbuf}; 3 | use std::str::FromStr; 4 | 5 | #[cfg(feature = "backend-rustcrypto")] 6 | #[test] 7 | fn correct_aid_storage_path() { 8 | use crate::hash_provider::RustCrypto; 9 | 10 | let root = pathbuf![env!("CARGO_MANIFEST_DIR"), "test", "fs_storage"]; 11 | let storage = FileSystemStorage::new(RustCrypto::new(), &root).unwrap(); 12 | 13 | let aid = ArtifactId::::from_str( 14 | "gitoid:blob:sha256:9d09789f20162dca6d80d2d884f46af22c824f6409d4f447332d079a2d1e364f", 15 | ) 16 | .unwrap(); 17 | 18 | let path = storage.manifest_path(aid); 19 | let path = path.strip_prefix(&root).unwrap(); 20 | let expected = pathbuf![ 21 | "manifests", 22 | "gitoid_blob_sha256", 23 | "9d", 24 | "09789f20162dca6d80d2d884f46af22c824f6409d4f447332d079a2d1e364f" 25 | ]; 26 | 27 | assert_eq!(path, expected); 28 | } 29 | -------------------------------------------------------------------------------- /omnibor/src/test.rs: -------------------------------------------------------------------------------- 1 | //! Tests against the OmniBOR crate as a whole. 2 | 3 | use { 4 | crate::{ 5 | artifact_id::{ArtifactId, ArtifactIdBuilder}, 6 | hash_algorithm::Sha256, 7 | }, 8 | anyhow::Result, 9 | serde_test::{assert_tokens, Token}, 10 | std::fs::File, 11 | tokio::{fs::File as AsyncFile, runtime::Runtime}, 12 | url::Url, 13 | }; 14 | 15 | /// SHA-256 hash of a file containing "hello world" 16 | /// 17 | /// Taken from a Git repo as ground truth. 18 | const ARTIFACT_ID_HELLO_WORLD_SHA256: &str = 19 | "gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03"; 20 | 21 | /// ArtifactID should be exactly 32 bytes, the size of the buffer. 22 | #[test] 23 | fn artifact_id_sha256_size() { 24 | assert_eq!(size_of::>(), 32); 25 | } 26 | 27 | #[test] 28 | fn generate_sha256_artifact_id_from_bytes() { 29 | let input = b"hello world"; 30 | let result = ArtifactIdBuilder::with_rustcrypto().identify_bytes(input); 31 | 32 | assert_eq!(result.to_string(), ARTIFACT_ID_HELLO_WORLD_SHA256); 33 | } 34 | 35 | #[test] 36 | fn generate_sha256_artifact_id_from_buffer() -> Result<()> { 37 | let mut file = File::open("test/data/hello_world.txt")?; 38 | let result = ArtifactIdBuilder::with_rustcrypto().identify_file(&mut file)?; 39 | 40 | assert_eq!(result.to_string(), ARTIFACT_ID_HELLO_WORLD_SHA256); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn generate_sha256_artifact_id_from_async_buffer() -> Result<()> { 47 | let runtime = Runtime::new()?; 48 | runtime.block_on(async { 49 | let mut file = AsyncFile::open("test/data/hello_world.txt").await?; 50 | let result = ArtifactIdBuilder::with_rustcrypto() 51 | .identify_async_file(&mut file) 52 | .await?; 53 | 54 | assert_eq!(result.to_string(), ARTIFACT_ID_HELLO_WORLD_SHA256); 55 | 56 | Ok(()) 57 | }) 58 | } 59 | 60 | #[test] 61 | fn newline_normalization_from_file() -> Result<()> { 62 | let mut unix_file = File::open("test/data/unix_line.txt")?; 63 | let mut windows_file = File::open("test/data/windows_line.txt")?; 64 | 65 | let builder = ArtifactIdBuilder::with_rustcrypto(); 66 | 67 | let unix_artifact_id = builder.identify_file(&mut unix_file)?; 68 | let windows_artifact_id = builder.identify_file(&mut windows_file)?; 69 | 70 | assert_eq!( 71 | unix_artifact_id.to_string(), 72 | windows_artifact_id.to_string() 73 | ); 74 | 75 | Ok(()) 76 | } 77 | 78 | #[test] 79 | fn newline_normalization_from_async_file() -> Result<()> { 80 | let runtime = Runtime::new()?; 81 | runtime.block_on(async { 82 | let mut unix_file = AsyncFile::open("test/data/unix_line.txt").await?; 83 | let mut windows_file = AsyncFile::open("test/data/windows_line.txt").await?; 84 | 85 | let builder = ArtifactIdBuilder::with_rustcrypto(); 86 | 87 | let unix_artifact_id = builder.identify_async_file(&mut unix_file).await?; 88 | let windows_artifact_id = builder.identify_async_file(&mut windows_file).await?; 89 | 90 | assert_eq!( 91 | unix_artifact_id.to_string(), 92 | windows_artifact_id.to_string() 93 | ); 94 | 95 | Ok(()) 96 | }) 97 | } 98 | 99 | #[test] 100 | fn newline_normalization_in_memory() -> Result<()> { 101 | let with_crlf = b"some\r\nstring\r\n"; 102 | let wout_crlf = b"some\nstring\n"; 103 | 104 | let builder = ArtifactIdBuilder::with_rustcrypto(); 105 | 106 | let with_crlf_artifact_id = builder.identify_bytes(&with_crlf[..]); 107 | let wout_crlf_artifact_id = builder.identify_bytes(&wout_crlf[..]); 108 | 109 | assert_eq!( 110 | with_crlf_artifact_id.to_string(), 111 | wout_crlf_artifact_id.to_string() 112 | ); 113 | 114 | Ok(()) 115 | } 116 | 117 | #[test] 118 | fn validate_uri() -> Result<()> { 119 | let content = b"hello world"; 120 | let artifact_id = ArtifactIdBuilder::with_rustcrypto().identify_bytes(content); 121 | 122 | assert_eq!( 123 | artifact_id.url().to_string(), 124 | ARTIFACT_ID_HELLO_WORLD_SHA256 125 | ); 126 | 127 | Ok(()) 128 | } 129 | 130 | #[test] 131 | #[should_panic] 132 | fn try_from_url_bad_scheme() { 133 | let url = Url::parse("gitiod:blob:sha1:95d09f2b10159347eece71399a7e2e907ea3df4f").unwrap(); 134 | ArtifactId::::try_from_url(url).unwrap(); 135 | } 136 | 137 | #[test] 138 | #[should_panic] 139 | fn try_from_url_missing_object_type() { 140 | let url = Url::parse("gitoid:").unwrap(); 141 | ArtifactId::::try_from_url(url).unwrap(); 142 | } 143 | 144 | #[test] 145 | #[should_panic] 146 | fn try_from_url_bad_object_type() { 147 | let url = Url::parse("gitoid:whatever").unwrap(); 148 | ArtifactId::::try_from_url(url).unwrap(); 149 | } 150 | 151 | #[test] 152 | #[should_panic] 153 | fn try_from_url_missing_hash_algorithm() { 154 | let url = Url::parse("gitoid:blob:").unwrap(); 155 | ArtifactId::::try_from_url(url).unwrap(); 156 | } 157 | 158 | #[test] 159 | #[should_panic] 160 | fn try_from_url_bad_hash_algorithm() { 161 | let url = Url::parse("gitoid:blob:sha10000").unwrap(); 162 | ArtifactId::::try_from_url(url).unwrap(); 163 | } 164 | 165 | #[test] 166 | #[should_panic] 167 | fn try_from_url_missing_hash() { 168 | let url = Url::parse("gitoid:blob:sha256:").unwrap(); 169 | ArtifactId::::try_from_url(url).unwrap(); 170 | } 171 | 172 | #[test] 173 | fn try_url_roundtrip() { 174 | let url = Url::parse(ARTIFACT_ID_HELLO_WORLD_SHA256).unwrap(); 175 | let artifact_id = ArtifactId::::try_from_url(url.clone()).unwrap(); 176 | let output = artifact_id.url(); 177 | assert_eq!(url, output); 178 | } 179 | 180 | #[test] 181 | fn valid_artifact_id_ser_de() { 182 | let id = ArtifactIdBuilder::with_rustcrypto().identify_string("hello world"); 183 | assert_tokens(&id, &[Token::Str(ARTIFACT_ID_HELLO_WORLD_SHA256)]); 184 | } 185 | -------------------------------------------------------------------------------- /omnibor/src/util/clone_as_boxstr.rs: -------------------------------------------------------------------------------- 1 | use { 2 | std::path::{Path, PathBuf}, 3 | url::Url, 4 | }; 5 | 6 | pub trait CloneAsBoxstr { 7 | fn clone_as_boxstr(self) -> Box; 8 | } 9 | 10 | impl CloneAsBoxstr for &str { 11 | fn clone_as_boxstr(self) -> Box { 12 | self.to_owned().into_boxed_str() 13 | } 14 | } 15 | 16 | impl CloneAsBoxstr for &String { 17 | fn clone_as_boxstr(self) -> Box { 18 | self.clone().into_boxed_str() 19 | } 20 | } 21 | 22 | impl CloneAsBoxstr for &Path { 23 | fn clone_as_boxstr(self) -> Box { 24 | self.display().to_string().into_boxed_str() 25 | } 26 | } 27 | 28 | impl CloneAsBoxstr for &PathBuf { 29 | fn clone_as_boxstr(self) -> Box { 30 | self.display().to_string().into_boxed_str() 31 | } 32 | } 33 | 34 | impl CloneAsBoxstr for &Url { 35 | fn clone_as_boxstr(self) -> Box { 36 | self.clone().to_string().into_boxed_str() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /omnibor/src/util/for_each_buf_fill.rs: -------------------------------------------------------------------------------- 1 | use {crate::error::ArtifactIdError, std::io::BufRead}; 2 | 3 | /// Helper extension trait to give a convenient way to iterate over 4 | /// chunks sized to the size of the internal buffer of the reader. 5 | pub(crate) trait ForEachBufFill: BufRead { 6 | /// Takes a function to apply to each buffer fill, and returns if any 7 | /// errors arose along with the number of bytes read in total. 8 | fn for_each_buf_fill(&mut self, f: impl FnMut(&[u8])) -> Result; 9 | } 10 | 11 | impl ForEachBufFill for R { 12 | fn for_each_buf_fill(&mut self, mut f: impl FnMut(&[u8])) -> Result { 13 | let mut total_read = 0; 14 | 15 | loop { 16 | let buffer = self 17 | .fill_buf() 18 | .map_err(|source| ArtifactIdError::FailedRead(Box::new(source)))?; 19 | let amount_read = buffer.len(); 20 | 21 | if amount_read == 0 { 22 | break; 23 | } 24 | 25 | f(buffer); 26 | 27 | self.consume(amount_read); 28 | total_read += amount_read; 29 | } 30 | 31 | Ok(total_read) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /omnibor/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clone_as_boxstr; 2 | pub mod for_each_buf_fill; 3 | pub mod pathbuf; 4 | pub mod sealed; 5 | pub mod stream_len; 6 | -------------------------------------------------------------------------------- /omnibor/src/util/pathbuf.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | #[doc(hidden)] 3 | macro_rules! pathbuf { 4 | ( $( $part:expr ),* ) => {{ 5 | use std::path::PathBuf; 6 | 7 | let mut temp = PathBuf::new(); 8 | 9 | $( 10 | temp.push($part); 11 | )* 12 | 13 | temp 14 | }}; 15 | 16 | ($( $part:expr, )*) => ($crate::pathbuf![$($part),*]) 17 | } 18 | -------------------------------------------------------------------------------- /omnibor/src/util/sealed.rs: -------------------------------------------------------------------------------- 1 | pub trait Sealed {} 2 | -------------------------------------------------------------------------------- /omnibor/src/util/stream_len.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::error::ArtifactIdError, 3 | std::io::{Seek, SeekFrom}, 4 | tokio::io::{AsyncSeek, AsyncSeekExt as _}, 5 | }; 6 | 7 | // Adapted from the Rust standard library's unstable implementation 8 | // of `Seek::stream_len`. 9 | // 10 | // TODO(abrinker): Remove this when `Seek::stream_len` is stabilized. 11 | // 12 | // License reproduction: 13 | // 14 | // Permission is hereby granted, free of charge, to any 15 | // person obtaining a copy of this software and associated 16 | // documentation files (the "Software"), to deal in the 17 | // Software without restriction, including without 18 | // limitation the rights to use, copy, modify, merge, 19 | // publish, distribute, sublicense, and/or sell copies of 20 | // the Software, and to permit persons to whom the Software 21 | // is furnished to do so, subject to the following 22 | // conditions: 23 | // 24 | // The above copyright notice and this permission notice 25 | // shall be included in all copies or substantial portions 26 | // of the Software. 27 | // 28 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 29 | // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 30 | // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 31 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 32 | // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 33 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 34 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 35 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 36 | // DEALINGS IN THE SOFTWARE. 37 | pub(crate) fn stream_len(mut stream: R) -> Result 38 | where 39 | R: Seek, 40 | { 41 | let old_pos = stream 42 | .stream_position() 43 | .map_err(|source| ArtifactIdError::FailedCheckReaderPos(Box::new(source)))?; 44 | 45 | let len = stream 46 | .seek(SeekFrom::End(0)) 47 | .map_err(|source| ArtifactIdError::FailedSeek(SeekFrom::End(0), Box::new(source)))?; 48 | 49 | // Avoid seeking a third time when we were already at the end of the 50 | // stream. The branch is usually way cheaper than a seek operation. 51 | if old_pos != len { 52 | stream.seek(SeekFrom::Start(old_pos)).map_err(|source| { 53 | ArtifactIdError::FailedSeek(SeekFrom::Start(old_pos), Box::new(source)) 54 | })?; 55 | } 56 | 57 | Ok(len) 58 | } 59 | 60 | /// An async equivalent of `stream_len`. 61 | pub(crate) async fn async_stream_len(mut stream: R) -> Result 62 | where 63 | R: AsyncSeek + Unpin, 64 | { 65 | let old_pos = stream 66 | .stream_position() 67 | .await 68 | .map_err(|source| ArtifactIdError::FailedCheckReaderPos(Box::new(source)))?; 69 | 70 | let len = stream 71 | .seek(SeekFrom::End(0)) 72 | .await 73 | .map_err(|source| ArtifactIdError::FailedSeek(SeekFrom::End(0), Box::new(source)))?; 74 | 75 | // Avoid seeking a third time when we were already at the end of the 76 | // stream. The branch is usually way cheaper than a seek operation. 77 | if old_pos != len { 78 | stream 79 | .seek(SeekFrom::Start(old_pos)) 80 | .await 81 | .map_err(|source| { 82 | ArtifactIdError::FailedSeek(SeekFrom::Start(old_pos), Box::new(source)) 83 | })?; 84 | } 85 | 86 | Ok(len) 87 | } 88 | -------------------------------------------------------------------------------- /omnibor/test/data/hello_world.txt: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /omnibor/test/data/unix_line.txt: -------------------------------------------------------------------------------- 1 | hello 2 | world 3 | -------------------------------------------------------------------------------- /omnibor/test/data/windows_line.txt: -------------------------------------------------------------------------------- 1 | hello 2 | world 3 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | description = "Helper tasks for the omnibor project workspace" 4 | version = "0.1.0" 5 | publish = false 6 | readme = "README.md" 7 | 8 | homepage.workspace = true 9 | license.workspace = true 10 | edition.workspace = true 11 | 12 | [dependencies] 13 | anyhow = "1.0.80" 14 | cargo_metadata = "0.18.1" 15 | clap = { version = "4.5.1", features = ["derive"] } 16 | derive_more = { version = "1.0.0", features = ["display"] } 17 | env_logger = "0.11.2" 18 | log = "0.4.20" 19 | pathbuf = "1.0.0" 20 | semver = "1.0.22" 21 | serde = { version = "1.0.197", features = ["derive"] } 22 | toml = "0.8.10" 23 | which = "6.0.0" 24 | xshell = "0.2.7" 25 | -------------------------------------------------------------------------------- /xtask/README.md: -------------------------------------------------------------------------------- 1 | # `xtask` 2 | 3 | This is the `xtask` package for the OmniBOR Rust project. This implements 4 | commonly-used project-wide commands for convenience. 5 | 6 | ## Design Goals 7 | 8 | This crate has a few key design goals: 9 | 10 | - __Fast compilation__: This tool will get recompiled whenever changes are 11 | made to it, and we want to empower contributors to the OmniBOR project to 12 | make changes to `xtask` when they encounter a new task for the project that 13 | they want to automate. To make this editing appealing, the write-edit-run 14 | loop needs to be fast, which means fast compilation. 15 | - __Minimal dependencies__: Related to the above, the `xtask` crate should 16 | have a minimal number of dependencies, and where possible those dependencies 17 | should be configured with the minimum number of features. 18 | - __Easy to use__: The commands exposed by `xtask` should have as simple an 19 | interface, and be as automatic, as possible. Fewer flags, fewer required 20 | arguments, etc. 21 | -------------------------------------------------------------------------------- /xtask/src/cli.rs: -------------------------------------------------------------------------------- 1 | //! The `cargo xtask` Command Line Interface (CLI). 2 | 3 | use clap::{arg, Parser as _}; 4 | 5 | /// Define the CLI and parse arguments from the command line. 6 | pub fn args() -> Cli { 7 | Cli::parse() 8 | } 9 | 10 | #[derive(Debug, clap::Parser)] 11 | pub struct Cli { 12 | #[clap(subcommand)] 13 | pub subcommand: Subcommand, 14 | } 15 | 16 | #[derive(Debug, Clone, clap::Subcommand)] 17 | pub enum Subcommand { 18 | /// Release a new version of a crate. 19 | /// 20 | /// Runs the following steps: 21 | /// 22 | /// (1) Verifies external tool dependencies are installed, 23 | /// (2) Verifies that Git worktree is ready (unless `--allow-dirty`), 24 | /// (3) Verifies you're on the `main` branch, 25 | /// (4) Verifies that `git-cliff` agrees about the version bump, 26 | /// (5) Generates the `CHANGELOG.md`, 27 | /// (6) Commits the `CHANGELOG.md`, 28 | /// (7) Runs a dry-run `cargo release` (unless `--execute`). 29 | /// 30 | /// Unless `--execute`, all steps will be rolled back after completion 31 | /// of the pipeline. All previously-executed steps will also be rolled back 32 | /// if a prior step fails. 33 | /// 34 | /// Note that this *does not* account for: 35 | /// 36 | /// (1) Running more than one instance of this command at the same time, 37 | /// (2) Running other programs which may interfere with relevant state (like 38 | /// Git repo state) at the same time, 39 | /// (3) Terminating the program prematurely, causing rollback to fail. 40 | /// 41 | /// It is your responsibility to cleanup manually if any of the above 42 | /// situations arise. 43 | Release(ReleaseArgs), 44 | } 45 | 46 | #[derive(Debug, Clone, clap::Args)] 47 | pub struct ReleaseArgs { 48 | /// The crate to release. 49 | #[arg(short = 'c', long = "crate", value_name = "CRATE")] 50 | pub krate: Crate, 51 | 52 | /// The version to bump. 53 | #[arg(short = 'b', long = "bump")] 54 | pub bump: Bump, 55 | 56 | /// Not a dry-run, actually execute the release. 57 | #[arg(short = 'x', long = "execute")] 58 | pub execute: bool, 59 | 60 | /// Allow Git worktree to be dirty. 61 | #[arg(short = 'd', long = "allow-dirty")] 62 | pub allow_dirty: bool, 63 | } 64 | 65 | /// The crate to release; can be "gitoid" or "omnibor" 66 | #[derive(Debug, Clone, Copy, derive_more::Display, clap::ValueEnum)] 67 | pub enum Crate { 68 | /// The `gitoid` crate, found in the `gitoid` folder. 69 | #[display("gitoid")] 70 | Gitoid, 71 | 72 | /// The `omnibor` crate, found in the `omnibor` folder. 73 | #[display("omnibor")] 74 | Omnibor, 75 | 76 | /// The `omnibor-cli` crate, found in the `omnibor-cli` folder. 77 | #[display("omnibor-cli")] 78 | OmniborCli, 79 | } 80 | 81 | /// The version to bump; can be "major", "minor", or "patch" 82 | #[derive(Debug, Clone, Copy, PartialEq, Eq, derive_more::Display, clap::ValueEnum)] 83 | pub enum Bump { 84 | /// Bump the major version. 85 | #[display("major")] 86 | Major, 87 | 88 | /// Bump the minor version. 89 | #[display("minor")] 90 | Minor, 91 | 92 | /// Bump the patch version. 93 | #[display("patch")] 94 | Patch, 95 | } 96 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A Task Runner for the OmniBOR Rust workspace. 2 | 3 | mod cli; 4 | mod pipeline; 5 | mod release; 6 | 7 | use env_logger::{Builder as LoggerBuilder, Env}; 8 | use std::process::ExitCode; 9 | 10 | fn main() -> ExitCode { 11 | LoggerBuilder::from_env(Env::default().default_filter_or("info")) 12 | .format_timestamp(None) 13 | .format_module_path(false) 14 | .format_target(false) 15 | .init(); 16 | 17 | let args = cli::args(); 18 | 19 | let res = match args.subcommand { 20 | cli::Subcommand::Release(args) => release::run(&args), 21 | }; 22 | 23 | if let Err(err) = res { 24 | log::error!("{}", err); 25 | 26 | // We skip the first error in the chain because it's the 27 | // exact error we've just printed. 28 | for err in err.chain().skip(1) { 29 | log::error!("\tcaused by: {}", err); 30 | } 31 | 32 | return ExitCode::FAILURE; 33 | } 34 | 35 | ExitCode::SUCCESS 36 | } 37 | -------------------------------------------------------------------------------- /xtask/src/pipeline.rs: -------------------------------------------------------------------------------- 1 | //! Generic mechanisms for running sequences of steps with rollback. 2 | 3 | use anyhow::{anyhow, bail, Error, Result}; 4 | use std::error::Error as StdError; 5 | use std::fmt::{Display, Formatter, Result as FmtResult}; 6 | use std::iter::Iterator; 7 | use std::ops::Not as _; 8 | use std::result::Result as StdResult; 9 | 10 | /// A pipeline of steps to execute and optionally rollback. 11 | /// 12 | /// The goal of the `Pipeline` type is to enable running a series of 13 | /// steps which may fail, and to correctly handling rolling them back 14 | /// if they _do_ fail. It's essentially a wrapper around an iterator 15 | /// of steps along with some minimal configuration for how execution 16 | /// should be done. 17 | pub struct Pipeline 18 | where 19 | It: Iterator, 20 | { 21 | /// The steps to execute. 22 | steps: It, 23 | 24 | /// Whether rollback should be forced even if all steps succeed. 25 | force_rollback: bool, 26 | } 27 | 28 | impl Pipeline 29 | where 30 | It: Iterator, 31 | { 32 | /// Construct a new pipeline. 33 | pub fn new(steps: I) -> Self 34 | where 35 | I: IntoIterator, 36 | { 37 | Pipeline { 38 | steps: steps.into_iter(), 39 | force_rollback: false, 40 | } 41 | } 42 | 43 | /// Force rollback at the end of the pipeline, regardless of outcome. 44 | pub fn plan_forced_rollback(&mut self) { 45 | self.force_rollback = true; 46 | } 47 | 48 | /// Run the pipeline. 49 | pub fn run(self) -> Result<()> { 50 | let mut forward_err = None; 51 | let mut completed_steps = Vec::new(); 52 | 53 | // Run the steps forward. 54 | for mut step in self.steps { 55 | if let Err(forward) = forward(step.as_mut()) { 56 | forward_err = Some(forward); 57 | completed_steps.push(step); 58 | break; 59 | } 60 | 61 | // We expect steps beginning with "check-" don't mutate the 62 | // environment and therefore don't need to be rolled back. 63 | if step.name().starts_with("check-").not() { 64 | completed_steps.push(step); 65 | } 66 | } 67 | 68 | // If we're forcing rollback or forward had an error, initiate rollback. 69 | if self.force_rollback || forward_err.is_some() { 70 | let forward_err = forward_err.unwrap_or_else(StepError::forced_rollback); 71 | 72 | for mut step in completed_steps.into_iter().rev() { 73 | if let Err(backward_err) = backward(step.as_mut()) { 74 | bail!(PipelineError::rollback(forward_err, backward_err)); 75 | } 76 | } 77 | 78 | bail!(PipelineError::forward(forward_err)); 79 | } 80 | 81 | Ok(()) 82 | } 83 | } 84 | 85 | /// A Boxed [`Step`]` trait object. 86 | pub type DynStep = Box; 87 | 88 | /// Move a step to the heap and get an owning fat pointer to the trait object. 89 | #[macro_export] 90 | macro_rules! step { 91 | ( $step:expr ) => {{ 92 | Box::new($step) as Box 93 | }}; 94 | } 95 | 96 | /// Construct a pipeline of steps each implementing the `Step` trait. 97 | #[macro_export] 98 | macro_rules! pipeline { 99 | ( $($step:expr),* ) => {{ 100 | Pipeline::new([ 101 | $( 102 | $crate::step!($step) 103 | ),* 104 | ]) 105 | }}; 106 | } 107 | 108 | /// A pipeline step which mutates the environment and can be undone. 109 | pub trait Step { 110 | /// The name of the step, to report to the user. 111 | /// 112 | /// # Note 113 | /// 114 | /// This should _always_ return a consistent name for the step, 115 | /// not based on any logic related to the arguments passed to the 116 | /// program. 117 | /// 118 | /// This is a method, not an associated function, to ensure that 119 | /// the [`Step`] trait is object-safe. The `pipeline::run` function 120 | /// runs steps through an iterator of `Step` trait objects, so this 121 | /// is a requirement of the design. 122 | fn name(&self) -> &'static str; 123 | 124 | /// Do the step. 125 | /// 126 | /// Steps are expected to clean up after themselves for the forward 127 | /// direction if they fail after partial completion. The `undo` is 128 | /// only for undoing a completely successful forward step if a later 129 | /// step fails. 130 | fn run(&mut self) -> Result<()>; 131 | 132 | /// Undo the step. 133 | /// 134 | /// This is run automatically by the pipelining system if there's 135 | /// a need to rollback the pipeline because a later step failed. 136 | /// 137 | /// This is to ensure that any pipeline of operations operates 138 | /// a single cohesive whole, either _all_ completing or _none_ 139 | /// visibly completing by the end. 140 | /// 141 | /// Note that this trait does _not_ ensure graceful shutdown if 142 | /// you cancel an operation with a kill signal before the `undo` 143 | /// operation can complete. 144 | fn undo(&mut self) -> Result<()> { 145 | Ok(()) 146 | } 147 | } 148 | 149 | /// Helper function to run a step forward and convert the error to [`StepError`] 150 | fn forward(step: &mut dyn Step) -> StdResult<(), StepError> { 151 | log::info!("running step '{}'", step.name()); 152 | 153 | step.run().map_err(|error| StepError { 154 | name: step.name(), 155 | error, 156 | }) 157 | } 158 | 159 | /// Helper function to run a step backward and convert the error to [`StepError`] 160 | fn backward(step: &mut dyn Step) -> StdResult<(), StepError> { 161 | log::info!("rolling back step '{}'", step.name()); 162 | 163 | step.undo().map_err(|error| StepError { 164 | name: step.name(), 165 | error, 166 | }) 167 | } 168 | 169 | /// An error from running a pipeline of steps. 170 | #[derive(Debug)] 171 | enum PipelineError { 172 | /// An error arose during forward execution. 173 | Forward { 174 | /// The error produced by the offending step. 175 | forward: StepError, 176 | }, 177 | /// An error arose during forward execution and also during rollback. 178 | Rollback { 179 | /// The name of the forward step that errored. 180 | forward_name: &'static str, 181 | 182 | /// The name of the backward step that errored. 183 | backward_name: &'static str, 184 | 185 | /// A combination of the backward and forward error types. 186 | rollback: Error, 187 | }, 188 | } 189 | 190 | impl PipelineError { 191 | /// Construct a forward error. 192 | fn forward(forward: StepError) -> Self { 193 | PipelineError::Forward { forward } 194 | } 195 | 196 | /// Construct a rollback error. 197 | fn rollback(forward: StepError, backward: StepError) -> Self { 198 | let forward_name = forward.name; 199 | let backward_name = backward.name; 200 | let rollback = Error::new(backward).context(forward); 201 | 202 | PipelineError::Rollback { 203 | forward_name, 204 | backward_name, 205 | rollback, 206 | } 207 | } 208 | } 209 | 210 | impl Display for PipelineError { 211 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 212 | match self { 213 | PipelineError::Forward { forward } => { 214 | write!(f, "{}, but rollback was successful", forward) 215 | } 216 | PipelineError::Rollback { 217 | forward_name, 218 | backward_name, 219 | .. 220 | } => write!( 221 | f, 222 | "step '{}' failed and step '{}' failed to rollback", 223 | forward_name, backward_name 224 | ), 225 | } 226 | } 227 | } 228 | 229 | impl StdError for PipelineError { 230 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 231 | match self { 232 | PipelineError::Forward { forward } => Some(forward), 233 | PipelineError::Rollback { rollback, .. } => Some(rollback.as_ref()), 234 | } 235 | } 236 | } 237 | 238 | /// An error from a single pipeline step. 239 | #[derive(Debug)] 240 | struct StepError { 241 | /// The name of the step that errored. 242 | name: &'static str, 243 | 244 | /// The error the step produced. 245 | error: Error, 246 | } 247 | 248 | impl StepError { 249 | /// A dummy error for when forced rollback is requested. 250 | fn forced_rollback() -> Self { 251 | StepError { 252 | name: "forced-rollback", 253 | error: anyhow!("forced rollback"), 254 | } 255 | } 256 | } 257 | 258 | impl Display for StepError { 259 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 260 | write!(f, "step '{}' failed", self.name) 261 | } 262 | } 263 | 264 | impl StdError for StepError { 265 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 266 | Some(self.error.as_ref()) 267 | } 268 | } 269 | --------------------------------------------------------------------------------