diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..4009e1f --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,126 @@ +name: Rust CI + +on: + push: + branches: [main] + paths: + - 'rust/**' + - '.github/workflows/rust-ci.yml' + pull_request: + paths: + - 'rust/**' + - '.github/workflows/rust-ci.yml' + workflow_dispatch: + +concurrency: + group: rust-ci-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + +defaults: + run: + working-directory: rust + +jobs: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Kept in sync with the platforms `ort` supports; see rust/README.md. + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + + # The ONNX model files are not in the repository; the tests load them + # from `rust/models`. Cache them so we don't re-download ~230 MB per run. + - name: Cache models + id: cache-models + uses: actions/cache@v4 + with: + path: rust/models + key: trustmark-models-${{ hashFiles('rust/crates/xtask/src/main.rs') }} + + - name: Fetch models + if: steps.cache-models.outputs.cache-hit != 'true' + run: cargo xtask fetch-models + + - name: Build + run: cargo build --workspace --all-targets + + - name: Test + run: cargo test --workspace + + - name: Doc tests + run: cargo test --doc + + lint: + name: Format and clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + + - name: cargo fmt + run: cargo fmt --all -- --check + + - name: cargo clippy + run: cargo clippy --workspace --all-targets --all-features + + msrv: + name: Minimum supported Rust version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Read rust-version from Cargo.toml + id: msrv + run: | + msrv=$(grep -m1 '^rust-version' Cargo.toml | cut -d'"' -f2) + echo "Detected MSRV: $msrv" + echo "version=$msrv" >> "$GITHUB_OUTPUT" + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ steps.msrv.outputs.version }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + + - name: Check against MSRV + run: cargo check --workspace --all-targets + + package: + name: Verify publishable package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + + # Catches problems that only show up in the packaged crate, such as files + # excluded by .gitignore that the build actually needs. + - name: cargo publish --dry-run + run: cargo publish --dry-run --locked diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml new file mode 100644 index 0000000..2f9a857 --- /dev/null +++ b/.github/workflows/rust-release.yml @@ -0,0 +1,122 @@ +name: Rust release + +# Publishes the `trustmark` crate to crates.io. +# +# Trigger by pushing a tag of the form `rust-v` (for example +# `rust-v0.3.0`), where `` matches the `version` field in +# `rust/Cargo.toml`. The tag prefix keeps Rust releases distinct from the +# Python package releases that share this repository. +# +# You can also run it manually from the Actions tab; use `dry_run: true` to +# rehearse everything except the actual `cargo publish`. +# +# Authentication uses crates.io Trusted Publishing (OIDC) — no API token is +# stored in this repository. See rust/RELEASING.md for the one-time setup. + +on: + push: + tags: + - 'rust-v*' + workflow_dispatch: + inputs: + dry_run: + description: 'Run every step but skip the actual publish' + type: boolean + default: true + +concurrency: + group: rust-release + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + +defaults: + run: + working-directory: rust + +jobs: + release: + name: Publish to crates.io + runs-on: ubuntu-latest + environment: crates-io + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust + + - name: Determine version + id: version + run: | + set -euo pipefail + manifest_version=$(cargo metadata --no-deps --format-version 1 \ + | python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"] == "trustmark"))') + echo "Manifest version: $manifest_version" + + if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + tag_version="${GITHUB_REF_NAME#rust-v}" + if [ "$tag_version" != "$manifest_version" ]; then + echo "::error::Tag $GITHUB_REF_NAME implies version $tag_version but rust/Cargo.toml says $manifest_version." + exit 1 + fi + fi + + echo "version=$manifest_version" >> "$GITHUB_OUTPUT" + + - name: Check version is not already published + run: | + set -euo pipefail + version="${{ steps.version.outputs.version }}" + published=$(curl -sSf -H 'User-Agent: adobe/trustmark release workflow' \ + "https://crates.io/api/v1/crates/trustmark" \ + | python3 -c 'import json,sys; print("\n".join(v["num"] for v in json.load(sys.stdin)["versions"]))') + if echo "$published" | grep -qx "$version"; then + echo "::error::trustmark $version is already published on crates.io. Bump the version in rust/Cargo.toml." + exit 1 + fi + echo "trustmark $version is not yet published." + + - name: Cache models + id: cache-models + uses: actions/cache@v4 + with: + path: rust/models + key: trustmark-models-${{ hashFiles('rust/crates/xtask/src/main.rs') }} + + - name: Fetch models + if: steps.cache-models.outputs.cache-hit != 'true' + run: cargo xtask fetch-models + + - name: Test + run: cargo test --workspace --locked + + - name: Package + run: cargo publish --dry-run --locked + + - name: Authenticate to crates.io + if: inputs.dry_run != true + uses: rust-lang/crates-io-auth-action@v1 + id: auth + + - name: Publish + if: inputs.dry_run != true + run: cargo publish --locked + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + + - name: Create GitHub release + if: github.ref_type == 'tag' && inputs.dry_run != true + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + --repo "$GITHUB_REPOSITORY" \ + --title "Rust crate ${{ steps.version.outputs.version }}" \ + --notes "The \`trustmark\` crate ${{ steps.version.outputs.version }} is published at https://crates.io/crates/trustmark/${{ steps.version.outputs.version }}." diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md new file mode 100644 index 0000000..b1fba55 --- /dev/null +++ b/rust/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to the `trustmark` Rust crate are documented here. This +crate is versioned independently of the Python package in this repository. + +Entries for releases before 0.3.0 were reconstructed from the commit history, +since those versions were published manually and without tags. + +## 0.3.0 + +- Upgrade `ort` to 2.0.0-rc.12. This is the fix for 0.2.2 no longer being + buildable: the prebuilt ONNX Runtime binaries that `ort-sys` 2.0.0-rc.8 + downloads at build time are no longer hosted. +- Upgrade `ndarray` to 0.17. +- `encode` and `decode` now take `&self`, so a loaded model can be shared + between callers. Encoding and decoding use separate sessions and can run + concurrently; inference on a single session is serialized. +- Raise the minimum supported Rust version to 1.88. +- Add release automation and this changelog; see [`RELEASING.md`](RELEASING.md). + +## 0.2.2 + +- Update the model download URL used by `cargo xtask fetch-models`. +- Document platform support. + +## 0.2.1 + +- Fix an integer truncation bug. +- Fix the documentation build. + +## 0.2.0 + +- Switch image resizing from `image` to `fast_image_resize` for better + performance. +- Support a user-specified JPEG output quality. +- Save JPEG output as RGB instead of RGBA. +- Fix resizing when the target is larger than the source image. + +## 0.1.0 + +- Initial release: encoding and decoding of TrustMark watermarks in binary mode + for all variants, with the same error correction levels as the Python + implementation. diff --git a/rust/README.md b/rust/README.md index 29dd49e..8412abb 100644 --- a/rust/README.md +++ b/rust/README.md @@ -92,3 +92,14 @@ To run the Python benchmarks, run the following from the workspace root: ``` benches/load.sh && benches/encode.sh ``` + +## Releasing + +Changes to this crate are validated by the [Rust CI](../.github/workflows/rust-ci.yml) +workflow, and releases to crates.io are performed by the +[Rust release](../.github/workflows/rust-release.yml) workflow when a +`rust-v*` tag is pushed. + +See [RELEASING.md](RELEASING.md) for the full process, including the manual +fallback and the setup required on crates.io. Notable changes are recorded in +[CHANGELOG.md](CHANGELOG.md). diff --git a/rust/RELEASING.md b/rust/RELEASING.md new file mode 100644 index 0000000..d8e62e0 --- /dev/null +++ b/rust/RELEASING.md @@ -0,0 +1,113 @@ +# Releasing the `trustmark` crate + +The Rust crate in this directory is published to +[crates.io](https://crates.io/crates/trustmark) independently of the Python +package at the repository root. This document describes how that happens. + +## What gets published + +Only the root package of the `rust/` workspace — `trustmark` — is published. +The workspace members under `rust/crates/` (`trustmark-cli` and `xtask`) are +marked `publish = false` and are intentionally not released to crates.io. + +The ONNX model files are **not** part of the published crate. Consumers fetch +them separately (see [`README.md`](README.md)); `rust/models/` is gitignored +apart from a `.gitkeep`. + +## Automated release (preferred) + +1. Update `version` in `rust/Cargo.toml`. +2. Run `cargo check` (or any cargo command) so `rust/Cargo.lock` picks up the + new version, and add a section to [`CHANGELOG.md`](CHANGELOG.md). +3. Land those changes on `main` through a pull request. The + [Rust CI](../.github/workflows/rust-ci.yml) workflow must be green. +4. Tag the merge commit and push the tag: + + ```sh + git tag rust-v0.3.0 + git push origin rust-v0.3.0 + ``` + + The `rust-v` prefix is required; it keeps crate releases distinguishable + from Python package tags in the same repository. + +Pushing the tag runs [Rust release](../.github/workflows/rust-release.yml), +which verifies that the tag matches `rust/Cargo.toml`, confirms the version +isn't already on crates.io, runs the tests, packages the crate, publishes it, +and opens a GitHub release. + +To rehearse without publishing, run the workflow manually from the Actions tab +with `dry_run` left enabled. + +### One-time crates.io setup + +The release workflow authenticates with +[Trusted Publishing](https://crates.io/docs/trusted-publishing), so no API +token is stored in this repository. An owner of the `trustmark` crate must +configure it once, under **Settings → Trusted Publishing** on the crate page: + +| Field | Value | +| -------------- | -------------------- | +| Repository | `adobe/trustmark` | +| Workflow file | `rust-release.yml` | +| Environment | `crates-io` | + +If you would rather use a long-lived token, add it as a repository secret +named `CARGO_REGISTRY_TOKEN` and replace the "Authenticate to crates.io" step +in the release workflow with that secret. Trusted Publishing is preferred +because the credential is short-lived and scoped to this one workflow. + +## Manual release + +Occasionally you may need to publish from a workstation — for example, when +restoring a crate whose automation is not yet in place. + +Prerequisites: a crates.io account that is an owner of the `trustmark` crate, +and `cargo login` already run (or `CARGO_REGISTRY_TOKEN` exported). + +From this directory (`rust/`): + +```sh +# 1. Confirm the version you are about to publish. +grep -m1 '^version' Cargo.toml + +# 2. Fetch the models, which the tests need. +cargo xtask fetch-models + +# 3. Validate. +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features +cargo test --workspace --locked +cargo publish --dry-run --locked + +# 4. Publish. +cargo publish --locked + +# 5. Record the release in git so the history is not lost. +git tag rust-v$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) +git push origin --tags +``` + +Always push the tag. Releases 0.1.0 through 0.2.2 were published by hand +without tags, which made it hard to tell what source a given crates.io version +corresponded to. + +## Dependency risk: `ort` release candidates + +The crate depends on [`ort`](https://ort.pyke.io) with an exact version pin +(`=2.0.0-rc.N`). `ort-sys` downloads prebuilt ONNX Runtime binaries from a +pyke-hosted CDN at build time, and **those binaries are removed for older +release candidates**. When that happens, an already-published version of +`trustmark` stops building even though nothing on crates.io was yanked. + +This is exactly what happened to `trustmark` 0.2.2, which pins +`ort =2.0.0-rc.8`. The fix is to upgrade the pin and publish a new version; +there is no way to repair a published version in place. + +Practical guidance: + +- Treat an `ort` release-candidate upgrade as release-worthy on its own. +- When `ort` publishes a new rc, upgrade reasonably promptly rather than + waiting for an unrelated feature to justify a release. +- Keep the [platform support section](README.md#platform-support) of the + README in sync with whichever rc is pinned. diff --git a/rust/src/bits/bch.rs b/rust/src/bits/bch.rs index 673eec5..0aa9871 100644 --- a/rust/src/bits/bch.rs +++ b/rust/src/bits/bch.rs @@ -414,7 +414,7 @@ pub(super) fn bch_decode(ecc_state: &mut EccState, data: &mut [u8], recvecc: &[u // recvecc=recvecc+bytes([0]*(4-leftdata)) // w=self.load4bytes(recvecc) // eccbuf.append(w) - recvecc.extend(std::iter::repeat(0).take(4 - leftdata)); + recvecc.extend(std::iter::repeat_n(0, 4 - leftdata)); let w = u32::from_be_bytes(recvecc.clone().try_into().unwrap()); eccbuf.push(w); }