From 9e3b532a7e72e60ce5072a2849b2a089e3300a0a Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Tue, 11 Aug 2026 22:57:13 +0000 Subject: [PATCH] Harden dependency and supply-chain policy (#100) --- .gitea/workflows/release-matrix.yml | 2 +- .gitea/workflows/supply-chain.yml | 70 ++ Cargo.lock | 29 +- Cargo.toml | 1 + RUSTREWRITE.md | 25 +- ci/dependency-policy.json | 63 + ci/release-matrix.json | 18 +- crates/libremetaverse-openjpeg/Cargo.toml | 5 + crates/libremetaverse-opus/Cargo.toml | 28 + crates/libremetaverse-opus/build.rs | 16 + crates/libremetaverse-opus/src/lib.rs | 313 +++++ crates/libremetaverse-voice-webrtc/Cargo.toml | 2 +- .../libremetaverse-voice-webrtc/src/native.rs | 13 +- deny.toml | 72 ++ docs/dependency-policy.md | 81 ++ docs/release-ci-matrix.md | 5 + docs/webrtc.md | 4 +- tests/api-compile/Cargo.lock | 1015 ++++++++++++++++- tools/ci-matrix/src/dependency.rs | 375 ++++++ tools/ci-matrix/src/lib.rs | 4 + tools/ci-matrix/src/main.rs | 23 +- 21 files changed, 2086 insertions(+), 78 deletions(-) create mode 100644 .gitea/workflows/supply-chain.yml create mode 100644 ci/dependency-policy.json create mode 100644 crates/libremetaverse-opus/Cargo.toml create mode 100644 crates/libremetaverse-opus/build.rs create mode 100644 crates/libremetaverse-opus/src/lib.rs create mode 100644 deny.toml create mode 100644 docs/dependency-policy.md create mode 100644 tools/ci-matrix/src/dependency.rs diff --git a/.gitea/workflows/release-matrix.yml b/.gitea/workflows/release-matrix.yml index b989090..dc6cb28 100644 --- a/.gitea/workflows/release-matrix.yml +++ b/.gitea/workflows/release-matrix.yml @@ -58,7 +58,7 @@ jobs: - profile: linux-msrv-portable toolchain: 1.96.0 target: x86_64-unknown-linux-gnu - native_dependencies: false + native_dependencies: true - profile: linux-stable-default toolchain: stable target: x86_64-unknown-linux-gnu diff --git a/.gitea/workflows/supply-chain.yml b/.gitea/workflows/supply-chain.yml new file mode 100644 index 0000000..c46c1b6 --- /dev/null +++ b/.gitea/workflows/supply-chain.yml @@ -0,0 +1,70 @@ +name: Dependency and supply-chain audit + +on: + push: + paths: + - ".gitea/workflows/supply-chain.yml" + - "ci/dependency-policy.json" + - "deny.toml" + - "tools/ci-matrix/**" + - "docs/dependency-policy.md" + - "RUSTREWRITE.md" + - "Cargo.toml" + - "Cargo.lock" + - "crates/**/Cargo.toml" + - "programs/Cargo.toml" + - "tests/**/Cargo.toml" + pull_request: + paths: + - ".gitea/workflows/supply-chain.yml" + - "ci/dependency-policy.json" + - "deny.toml" + - "tools/ci-matrix/**" + - "docs/dependency-policy.md" + - "RUSTREWRITE.md" + - "Cargo.toml" + - "Cargo.lock" + - "crates/**/Cargo.toml" + - "programs/Cargo.toml" + - "tests/**/Cargo.toml" + workflow_dispatch: + +env: + CARGO_BUILD_JOBS: 1 + CARGO_INCREMENTAL: 0 + CARGO_PROFILE_DEV_DEBUG: 0 + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cache immutable Cargo downloads and advisory database + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ~/.cargo/advisory-dbs + key: supply-chain-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'deny.toml') }} + - uses: dtolnay/rust-toolchain@stable + - name: Install pinned audit tools + run: | + cargo install --locked cargo-deny --version 0.20.2 + cargo install --locked cargo-machete --version 0.9.2 + - name: Record and validate the reviewed graph + run: | + mkdir -p artifacts + cargo tree --locked --workspace --all-features --target all --duplicates > artifacts/dependency-duplicates.txt + cargo run --locked -p metacrate-ci-matrix -- dependency-audit --evidence artifacts/dependency-audit.json + - name: Reject advisories, licenses, duplicates, and sources outside policy + run: cargo deny check advisories licenses bans sources --hide-inclusion-graph + - name: Reject unused direct dependencies + run: cargo machete --with-metadata + - name: Upload dependency evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: supply-chain-evidence + path: artifacts/ + if-no-files-found: error diff --git a/Cargo.lock b/Cargo.lock index 6756500..9dc6639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,25 +194,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "audiopus" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3743519567e9135cf6f9f1a509851cb0c8e4cb9d66feb286668afb1923bec458" -dependencies = [ - "audiopus_sys", -] - -[[package]] -name = "audiopus_sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927791de46f70facea982dbfaf19719a41ce6064443403be631a85de6a58fff9" -dependencies = [ - "log", - "pkg-config", -] - [[package]] name = "autocfg" version = "1.5.1" @@ -1596,6 +1577,14 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libremetaverse-opus" +version = "0.0.1" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "libremetaverse-prim-mesher" version = "0.0.1" @@ -1701,10 +1690,10 @@ dependencies = [ name = "libremetaverse-voice-webrtc" version = "0.0.1" dependencies = [ - "audiopus", "cpal", "hound", "libremetaverse", + "libremetaverse-opus", "libremetaverse-structured-data", "libremetaverse-types", "serde", diff --git a/Cargo.toml b/Cargo.toml index 51a2aa8..1cfc299 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/libremetaverse-rlv", "crates/libremetaverse-utilities", "crates/libremetaverse-voice-vivox", + "crates/libremetaverse-opus", "crates/libremetaverse-voice-webrtc", "programs", "tests/compat", diff --git a/RUSTREWRITE.md b/RUSTREWRITE.md index 3e06508..0af13c7 100644 --- a/RUSTREWRITE.md +++ b/RUSTREWRITE.md @@ -417,7 +417,7 @@ MSRV, features, and licenses at adoption time. | SkiaSharp 4.150.1 | [`skia-safe` 0.99.0](https://crates.io/crates/skia-safe/0.99.0) | Adopted behind the opt-in `skia` feature. Target-specific official binary-cache feature sets provide BMP/GIF/ICO/JPEG/PNG/WBMP/WebP decoding on macOS, Linux, and Windows; checked owned buffers keep Skia types out of the core image API. | | Pfim 0.11.4 | [`image` 0.25.10](https://crates.io/crates/image/0.25.10), [`ddsfile` 0.6.0](https://crates.io/crates/ddsfile/0.6.0) | `image` covers TGA and common DDS decoding; `ddsfile` exposes DDS container details. Golden files decide whether both are needed. | | OggVorbisEncoder 1.2.2 | [`vorbis_rs` 0.5.6](https://crates.io/crates/vorbis_rs/0.5.6) | BSD-3-Clause, MSRV 1.82, backed by C libraries. Feature-gate native audio encoding. | -| SIPSorcery 8.0.23 | [`webrtc` 0.20.0](https://crates.io/crates/webrtc/0.20.0), [`cpal` 0.18.1](https://crates.io/crates/cpal/0.18.1), [`opus` 0.3.1](https://crates.io/crates/opus/0.3.1) | Validate SDP, ICE, data-channel framing, audio formats, device hotplug, and native libopus deployment separately. Do not claim parity from successful compilation. | +| SIPSorcery 8.0.23 | [`str0m` 0.22.0](https://crates.io/crates/str0m/0.22.0), [`cpal` 0.18.1](https://crates.io/crates/cpal/0.18.1), system libopus 1.3+ through `libremetaverse-opus` | The private safe adapter avoids the unmaintained `audiopus_sys` binding. Validate SDP, ICE, data-channel framing, audio formats, device hotplug, and native libopus deployment separately. Do not claim parity from successful compilation. | | LSL generated parser | [`lalrpop` 0.23.1](https://crates.io/crates/lalrpop/0.23.1) | Candidate only. Preserve grammar conflicts, recovery, token positions, and diagnostics before replacing the generated parser. | | NUnit/Moq | built-in test harness, [`mockall` 0.15.0](https://crates.io/crates/mockall/0.15.0), [`proptest` 1.11.0](https://crates.io/crates/proptest/1.11.0) | Prefer fakes and deterministic protocol fixtures; use mocks only for interaction contracts. Add properties after direct parity cases exist. | | NUnit benchmarks | [`criterion` 0.8.2](https://crates.io/crates/criterion/0.8.2) | Port benchmark-category methods to `benches/`; never make timing thresholds correctness tests. | @@ -429,6 +429,29 @@ not select a crate merely because NuGet used one. In particular, begin with `Mutex>` rather than a concurrent-map dependency and add a pool only after allocations appear in a profile. +### 4.1 Adopted direct dependency inventory + +The candidates above are not the release inventory. The machine-checked +inventory is [`ci/dependency-policy.json`](ci/dependency-policy.json); each +locked direct version must appear there with its implemented purpose, +maintenance state, transitive-cost classification, and native-code status. +The current adopted set is: + +| Responsibility | Adopted direct crates | +|---|---| +| Async and HTTP | `tokio`, `futures-channel`, `futures-util`, `reqwest` | +| Wire data and parsing | `base64`, `serde`, `serde_json`, `roxmltree`, `regex`, `unicode-general-category` | +| Identity, hashing, and platform facts | `uuid`, `getrandom`, `md-5`, `sha1`, `sha2`, `mac_address2`, `os_info` | +| Archives, images, and audio | `flate2`, `tar`, `bcdec_rs`, `skia-safe`, `hound`, `vorbis_rs`, `str0m`, `cpal` | +| Code generation and program CLI | `syn`, `prettyplease`, `clap` | +| Build and compatibility-test support | `pkg-config`, `vcpkg`, `stats_alloc` | + +System libopus is accessed only through the private `libremetaverse-opus` +safe adapter; it replaces the unmaintained `audiopus`/`audiopus_sys` stack. +The adapter uses `pkg-config` on Linux, macOS, and Windows GNU, and `vcpkg` on +Windows MSVC. The dependency audit fails closed for any new direct crate, +resolved version, duplicate-version set, wildcard, Git source, or registry. + ## 5. Module-by-module implementation guidance ### 5.1 `libremetaverse-types` diff --git a/ci/dependency-policy.json b/ci/dependency-policy.json new file mode 100644 index 0000000..27331c8 --- /dev/null +++ b/ci/dependency-policy.json @@ -0,0 +1,63 @@ +{ + "schema": 1, + "msrv": "1.96.0", + "current": "stable", + "reviewed_on": "2026-08-11", + "review_by": "2026-11-11", + "direct": [ + { "name": "base64", "versions": ["0.22.1"], "purpose": "LLSD, login, asset, and protocol base64 encoding", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`base64`" }, + { "name": "bcdec_rs", "versions": ["0.2.0"], "purpose": "Pure Rust BC6H and BC7 texture block decoding", "maintenance": "stable", "transitive_cost": "low", "native": false, "rewrite_anchor": "`bcdec_rs`" }, + { "name": "clap", "versions": ["4.6.6"], "purpose": "Typed command-line parsing for shipped diagnostic programs", "maintenance": "active", "transitive_cost": "medium", "native": false, "rewrite_anchor": "`clap`" }, + { "name": "cpal", "versions": ["0.18.1"], "purpose": "Opt-in cross-platform physical audio device access", "maintenance": "monitored-native", "transitive_cost": "high", "native": true, "rewrite_anchor": "`cpal`" }, + { "name": "flate2", "versions": ["1.1.9"], "purpose": "Bounded gzip and deflate asset/archive decoding", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`flate2`" }, + { "name": "futures-channel", "versions": ["0.3.33"], "purpose": "One-shot compatibility callbacks without another runtime", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`futures-channel`" }, + { "name": "futures-util", "versions": ["0.3.33"], "purpose": "Stream adaptation for HTTP capability bodies", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`futures-util`" }, + { "name": "getrandom", "versions": ["0.4.3"], "purpose": "Operating-system entropy for protocol identifiers", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`getrandom`" }, + { "name": "hound", "versions": ["3.5.1"], "purpose": "Bounded PCM WAV parsing for WebRTC playback", "maintenance": "stable", "transitive_cost": "low", "native": false, "rewrite_anchor": "`hound`" }, + { "name": "mac_address2", "versions": ["2.0.2"], "purpose": "Cross-platform machine identity compatibility input", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`mac_address2`" }, + { "name": "md-5", "versions": ["0.10.6"], "purpose": "Legacy protocol checksum compatibility", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`md-5`" }, + { "name": "os_info", "versions": ["3.15.0"], "purpose": "Cross-platform login platform reporting", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`os_info`" }, + { "name": "pkg-config", "versions": ["0.3.33"], "purpose": "Unix and macOS native library discovery in reviewed adapters", "maintenance": "stable", "transitive_cost": "low", "native": false, "rewrite_anchor": "`pkg-config`" }, + { "name": "prettyplease", "versions": ["0.2.37"], "purpose": "Deterministic formatting of generated Rust sources", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`prettyplease`" }, + { "name": "regex", "versions": ["1.13.1"], "purpose": "LSL tokenization and bounded program input parsing", "maintenance": "active", "transitive_cost": "medium", "native": false, "rewrite_anchor": "`regex`" }, + { "name": "reqwest", "versions": ["0.13.4"], "purpose": "Rustls-backed login and capability HTTP transport", "maintenance": "active", "transitive_cost": "high", "native": false, "rewrite_anchor": "`reqwest`" }, + { "name": "roxmltree", "versions": ["0.21.1"], "purpose": "Read-only bounded LLSD and protocol XML parsing", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`roxmltree`" }, + { "name": "serde", "versions": ["1.0.229"], "purpose": "Typed JSON wire models and checked evidence manifests", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`serde`" }, + { "name": "serde_json", "versions": ["1.0.151"], "purpose": "JSON protocol, fixture, and audit evidence handling", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`serde_json`" }, + { "name": "sha1", "versions": ["0.10.7"], "purpose": "Legacy protocol hash compatibility", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`sha1`" }, + { "name": "sha2", "versions": ["0.11.0"], "purpose": "Manifest integrity and protocol hashing", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`sha2`" }, + { "name": "skia-safe", "versions": ["0.99.0"], "purpose": "Opt-in cross-platform Skia image decoding", "maintenance": "monitored-native", "transitive_cost": "high", "native": true, "rewrite_anchor": "`skia-safe`" }, + { "name": "stats_alloc", "versions": ["0.1.10"], "purpose": "Allocation-budget compatibility tests", "maintenance": "stable", "transitive_cost": "low", "native": false, "rewrite_anchor": "`stats_alloc`" }, + { "name": "str0m", "versions": ["0.22.0"], "purpose": "Native Rust ICE, DTLS, SRTP, RTP, and SCTP WebRTC transport", "maintenance": "active", "transitive_cost": "high", "native": false, "rewrite_anchor": "`str0m`" }, + { "name": "syn", "versions": ["2.0.119"], "purpose": "Syntax validation for generated Rust sources", "maintenance": "active", "transitive_cost": "medium", "native": false, "rewrite_anchor": "`syn`" }, + { "name": "tar", "versions": ["0.4.46"], "purpose": "Bounded OAR and asset archive traversal", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`tar`" }, + { "name": "tokio", "versions": ["1.53.1"], "purpose": "Shared asynchronous networking, timers, channels, and tasks", "maintenance": "active", "transitive_cost": "medium", "native": false, "rewrite_anchor": "`tokio`" }, + { "name": "unicode-general-category", "versions": ["1.1.0"], "purpose": "Unicode category matching in the LSL lexer", "maintenance": "stable", "transitive_cost": "low", "native": false, "rewrite_anchor": "`unicode-general-category`" }, + { "name": "uuid", "versions": ["1.24.0"], "purpose": "Random UUID generation behind protocol-compatible wrappers", "maintenance": "active", "transitive_cost": "low", "native": false, "rewrite_anchor": "`uuid`" }, + { "name": "vcpkg", "versions": ["0.2.15"], "purpose": "Windows MSVC native library discovery in reviewed adapters", "maintenance": "stable", "transitive_cost": "low", "native": false, "rewrite_anchor": "`vcpkg`" }, + { "name": "vorbis_rs", "versions": ["0.5.6"], "purpose": "Opt-in Ogg Vorbis asset encoding", "maintenance": "monitored-native", "transitive_cost": "medium", "native": true, "rewrite_anchor": "`vorbis_rs`" } + ], + "approved_duplicates": [ + { "name": "block-buffer", "versions": ["0.10.4", "0.12.1"], "reason": "RustCrypto digest 0.10 and 0.11 consumers have not unified their block-buffer major version." }, + { "name": "cfg_aliases", "versions": ["0.1.1", "0.2.2"], "reason": "The target-specific native stacks use different nix-generation build helpers." }, + { "name": "chacha20", "versions": ["0.9.1", "0.10.1"], "reason": "The WebRTC crypto and current random-number stacks require different RustCrypto generations." }, + { "name": "const-oid", "versions": ["0.9.6", "0.10.2"], "reason": "DTLS certificate dependencies and the newer SHA-2 stack use different const-oid generations." }, + { "name": "cpufeatures", "versions": ["0.2.17", "0.3.0"], "reason": "RustCrypto digest 0.10 and 0.11 families have not unified their CPU feature helper." }, + { "name": "crypto-common", "versions": ["0.1.7", "0.2.2"], "reason": "Legacy protocol hashes and current SHA-2 intentionally span RustCrypto digest generations." }, + { "name": "digest", "versions": ["0.10.7", "0.11.3"], "reason": "MD5, SHA-1, and WebRTC use digest 0.10 while manifest SHA-2 uses the audited 0.11 API." }, + { "name": "getrandom", "versions": ["0.2.17", "0.3.4", "0.4.3"], "reason": "WebRTC crypto, transitive rand, and direct UUID entropy consumers span three compatible APIs." }, + { "name": "jni-sys", "versions": ["0.3.1", "0.4.1"], "reason": "CPAL Android NDK support and rustls Android certificate verification require different JNI bindings." }, + { "name": "nix", "versions": ["0.28.0", "0.31.3"], "reason": "Machine-address and operating-system reporting crates have not unified their Unix API generation." }, + { "name": "nom", "versions": ["7.1.3", "8.0.0"], "reason": "Skia bindgen tooling remains on nom 7 while the native WebRTC stack uses nom 8." }, + { "name": "r-efi", "versions": ["5.3.0", "6.0.0"], "reason": "The three resolved getrandom generations require two WASI EFI interface generations." }, + { "name": "rand", "versions": ["0.9.5", "0.10.2"], "reason": "WebRTC protocol crates use rand 0.9 while target-specific dependencies have adopted rand 0.10." }, + { "name": "rand_core", "versions": ["0.6.4", "0.9.5", "0.10.1"], "reason": "DTLS elliptic curves and the two resolved rand generations require distinct rand_core APIs." }, + { "name": "sha2", "versions": ["0.10.9", "0.11.0"], "reason": "WebRTC crypto still requires SHA-2 0.10 while MetaCrate integrity code directly uses 0.11." }, + { "name": "shlex", "versions": ["1.3.0", "2.0.1"], "reason": "Skia bindgen and current native C compilation helpers use different shell-token parsers." }, + { "name": "syn", "versions": ["2.0.119", "3.0.3"], "reason": "MetaCrate code generation uses syn 2 while current derive macro dependencies use syn 3." }, + { "name": "thiserror", "versions": ["1.0.69", "2.0.20"], "reason": "Machine-address compatibility remains on thiserror 1 while WebRTC and Vorbis use version 2." }, + { "name": "thiserror-impl", "versions": ["1.0.69", "2.0.20"], "reason": "The approved thiserror runtime duplication necessarily carries matching derive versions." }, + { "name": "untrusted", "versions": ["0.7.1", "0.9.0"], "reason": "Target-specific certificate backends and rustls webpki require different untrusted APIs." }, + { "name": "windows-sys", "versions": ["0.52.0", "0.61.2"], "reason": "Machine-address compatibility uses 0.52 while current Tokio, CLI, and archive stacks use 0.61." } + ] +} diff --git a/ci/release-matrix.json b/ci/release-matrix.json index 31a9ce5..9450166 100644 --- a/ci/release-matrix.json +++ b/ci/release-matrix.json @@ -14,9 +14,9 @@ "purpose": "Opt-in Skia image formats using the target-specific binary cache or source build" }, { - "id": "audiopus-0.2", - "version": "0.2", - "purpose": "Native Opus codec used by WebRTC voice" + "id": "libopus-1.3", + "version": ">=1.3", + "purpose": "System Opus codec used through the reviewed safe WebRTC adapter" }, { "id": "alsa-1.2", @@ -43,11 +43,15 @@ "toolchain": "1.96.0", "target": "x86_64-unknown-linux-gnu", "feature_sets": ["no-default-features"], - "prerequisites": [], + "prerequisites": ["libopus-1.3"], "commands": [ { "label": "Compile the portable public crates on the declared MSRV", "args": ["check", "--locked", "-j", "1", "--all-targets", "--no-default-features", "-p", "libremetaverse-types", "-p", "libremetaverse-structured-data", "-p", "libremetaverse-imaging", "-p", "libremetaverse-imaging-skia", "-p", "libremetaverse-prim-mesher", "-p", "libremetaverse-lsl-tools", "-p", "libremetaverse-rlv", "-p", "metacrate-ci-matrix"] + }, + { + "label": "Compile the safe native Opus boundary and WebRTC voice on the declared MSRV", + "args": ["check", "--locked", "-j", "1", "--all-targets", "--no-default-features", "-p", "libremetaverse-opus", "-p", "libremetaverse-voice-webrtc"] } ] }, @@ -59,7 +63,7 @@ "toolchain": "stable", "target": "x86_64-unknown-linux-gnu", "feature_sets": ["default", "tests", "examples", "docs"], - "prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "audiopus-0.2"], + "prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "libopus-1.3"], "commands": [ { "label": "Compile every default workspace target", @@ -103,7 +107,7 @@ "toolchain": "stable", "target": "x86_64-unknown-linux-gnu", "feature_sets": ["all-features", "dds-bc67", "jpeg2000", "skia", "vorbis", "real-audio", "tests"], - "prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "audiopus-0.2", "alsa-1.2", "vorbis-rs-0.5.6"], + "prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "libopus-1.3", "alsa-1.2", "vorbis-rs-0.5.6"], "commands": [ { "label": "Compile core with only pure Rust BC6H and BC7", @@ -139,7 +143,7 @@ "toolchain": "stable", "target": "x86_64-unknown-linux-gnu", "feature_sets": ["default", "tests", "examples", "docs"], - "prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "audiopus-0.2"], + "prerequisites": ["openjpeg-2.5.4", "skia-safe-0.99.0", "libopus-1.3"], "commands": [ { "label": "Run all default documentation tests", diff --git a/crates/libremetaverse-openjpeg/Cargo.toml b/crates/libremetaverse-openjpeg/Cargo.toml index 85fc7b7..b4679b6 100644 --- a/crates/libremetaverse-openjpeg/Cargo.toml +++ b/crates/libremetaverse-openjpeg/Cargo.toml @@ -12,6 +12,11 @@ build = "build.rs" pkg-config = "0.3" vcpkg = "0.2" +# cargo-machete cannot associate target-conditional build.rs references with +# build dependencies. Both crates are invoked directly in build.rs. +[package.metadata.cargo-machete] +ignored = ["pkg-config", "vcpkg"] + # Unsafe code is permitted only in this workspace member: it is the reviewed # native ABI boundary. All callers use its safe, owned Rust API. [lints.rust] diff --git a/crates/libremetaverse-opus/Cargo.toml b/crates/libremetaverse-opus/Cargo.toml new file mode 100644 index 0000000..960223b --- /dev/null +++ b/crates/libremetaverse-opus/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "libremetaverse-opus" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Safe system-libopus adapter for MetaCrate" +publish = false +build = "build.rs" + +[build-dependencies] +pkg-config = "0.3" +vcpkg = "0.2" + +# cargo-machete cannot associate target-conditional build.rs references with +# build dependencies. Both crates are invoked directly in build.rs. +[package.metadata.cargo-machete] +ignored = ["pkg-config", "vcpkg"] + +# Unsafe code is permitted only inside this reviewed native ABI boundary. +# Every consumer receives an owned, validated safe Rust API. +[lints.rust] +unsafe_code = "allow" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } diff --git a/crates/libremetaverse-opus/build.rs b/crates/libremetaverse-opus/build.rs new file mode 100644 index 0000000..8e22e07 --- /dev/null +++ b/crates/libremetaverse-opus/build.rs @@ -0,0 +1,16 @@ +use std::env; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + let target = env::var("TARGET").expect("Cargo provides TARGET"); + if target.contains("msvc") { + vcpkg::Config::new() + .find_package("opus") + .expect("WebRTC voice requires libopus 1.3 or newer from vcpkg"); + } else { + pkg_config::Config::new() + .atleast_version("1.3") + .probe("opus") + .expect("WebRTC voice requires pkg-config and libopus 1.3 or newer"); + } +} diff --git a/crates/libremetaverse-opus/src/lib.rs b/crates/libremetaverse-opus/src/lib.rs new file mode 100644 index 0000000..4df07be --- /dev/null +++ b/crates/libremetaverse-opus/src/lib.rs @@ -0,0 +1,313 @@ +//! Safe, narrowly scoped access to the system `libopus` codec. +//! +//! The foreign ABI, raw handles, and error pointers are contained in this +//! crate. Callers can move an encoder or decoder between threads, but all +//! codec operations require exclusive access because libopus mutates them. + +#![allow(unsafe_code)] + +use std::ffi::{CStr, c_char, c_int}; +use std::fmt; +use std::ptr::NonNull; + +const OPUS_OK: c_int = 0; +const OPUS_BAD_ARG: c_int = -1; +const OPUS_ALLOC_FAIL: c_int = -7; +const OPUS_APPLICATION_VOIP: c_int = 2_048; +const MAX_PACKET_BYTES: usize = 1_275; + +#[repr(C)] +struct OpusEncoder { + _private: [u8; 0], +} + +#[repr(C)] +struct OpusDecoder { + _private: [u8; 0], +} + +#[link(name = "opus")] +unsafe extern "C" { + fn opus_encoder_create( + sample_rate: c_int, + channels: c_int, + application: c_int, + error: *mut c_int, + ) -> *mut OpusEncoder; + fn opus_encoder_destroy(encoder: *mut OpusEncoder); + fn opus_encode( + encoder: *mut OpusEncoder, + pcm: *const i16, + frame_size: c_int, + output: *mut u8, + max_output_bytes: c_int, + ) -> c_int; + fn opus_decoder_create( + sample_rate: c_int, + channels: c_int, + error: *mut c_int, + ) -> *mut OpusDecoder; + fn opus_decoder_destroy(decoder: *mut OpusDecoder); + fn opus_decode( + decoder: *mut OpusDecoder, + packet: *const u8, + packet_bytes: c_int, + pcm: *mut i16, + frame_size: c_int, + decode_fec: c_int, + ) -> c_int; + fn opus_strerror(error: c_int) -> *const c_char; +} + +/// Channel layouts accepted by the Opus multirate API. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(i32)] +pub enum Channels { + Mono = 1, + Stereo = 2, +} + +impl Channels { + const fn count(self) -> usize { + self as usize + } +} + +/// Stable libopus error code returned by a codec operation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Error(c_int); + +impl Error { + #[must_use] + pub const fn code(self) -> i32 { + self.0 + } +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = unsafe { + let pointer = opus_strerror(self.0); + (!pointer.is_null()).then(|| CStr::from_ptr(pointer).to_string_lossy()) + }; + match message { + Some(message) => write!(formatter, "libopus error {}: {message}", self.0), + None => write!(formatter, "libopus error {}", self.0), + } + } +} + +impl std::error::Error for Error {} + +/// Owned Opus `VoIP` encoder. +pub struct Encoder { + raw: NonNull, + channels: Channels, + sample_rate: usize, +} + +// libopus codec handles have no thread affinity. Exclusive method access +// prevents concurrent calls after the owned handle is moved to another thread. +unsafe impl Send for Encoder {} + +impl Encoder { + /// Creates a `VoIP` encoder for a supported Opus sample rate. + /// + /// # Errors + /// + /// Returns the native libopus error when the configuration is unsupported + /// or the codec cannot be allocated. + pub fn voip(sample_rate: u32, channels: Channels) -> Result { + let sample_rate = validate_sample_rate(sample_rate)?; + let sample_rate_usize = usize::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))?; + let mut error = OPUS_OK; + let raw = unsafe { + opus_encoder_create( + sample_rate, + channels as c_int, + OPUS_APPLICATION_VOIP, + &raw mut error, + ) + }; + let raw = NonNull::new(raw).ok_or(Error(if error == OPUS_OK { + OPUS_ALLOC_FAIL + } else { + error + }))?; + if error != OPUS_OK { + unsafe { opus_encoder_destroy(raw.as_ptr()) }; + return Err(Error(error)); + } + Ok(Self { + raw, + channels, + sample_rate: sample_rate_usize, + }) + } + + /// Encodes one complete Opus frame into a caller-owned packet buffer. + /// + /// # Errors + /// + /// Rejects malformed channel/frame lengths and oversized output buffers, + /// and returns any native codec failure. + pub fn encode(&mut self, pcm: &[i16], output: &mut [u8]) -> Result { + let channels = self.channels.count(); + if !pcm.len().is_multiple_of(channels) || output.is_empty() { + return Err(Error(OPUS_BAD_ARG)); + } + let frame_size = pcm.len() / channels; + let base = self.sample_rate / 400; + if ![base, base * 2, base * 4, base * 8, base * 16, base * 24].contains(&frame_size) { + return Err(Error(OPUS_BAD_ARG)); + } + let result = unsafe { + opus_encode( + self.raw.as_ptr(), + pcm.as_ptr(), + c_int::try_from(frame_size).map_err(|_| Error(OPUS_BAD_ARG))?, + output.as_mut_ptr(), + c_int::try_from(output.len()).map_err(|_| Error(OPUS_BAD_ARG))?, + ) + }; + result_to_size(result) + } +} + +impl Drop for Encoder { + fn drop(&mut self) { + unsafe { opus_encoder_destroy(self.raw.as_ptr()) }; + } +} + +/// Owned Opus decoder. +pub struct Decoder { + raw: NonNull, + channels: Channels, + sample_rate: usize, +} + +// See the Encoder safety argument above. +unsafe impl Send for Decoder {} + +impl Decoder { + /// Creates a decoder for a supported Opus sample rate. + /// + /// # Errors + /// + /// Returns the native libopus error when the configuration is unsupported + /// or the codec cannot be allocated. + pub fn new(sample_rate: u32, channels: Channels) -> Result { + let sample_rate = validate_sample_rate(sample_rate)?; + let sample_rate_usize = usize::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG))?; + let mut error = OPUS_OK; + let raw = unsafe { opus_decoder_create(sample_rate, channels as c_int, &raw mut error) }; + let raw = NonNull::new(raw).ok_or(Error(if error == OPUS_OK { + OPUS_ALLOC_FAIL + } else { + error + }))?; + if error != OPUS_OK { + unsafe { opus_decoder_destroy(raw.as_ptr()) }; + return Err(Error(error)); + } + Ok(Self { + raw, + channels, + sample_rate: sample_rate_usize, + }) + } + + /// Decodes one Opus packet, or performs packet-loss concealment for `None`. + /// + /// # Errors + /// + /// Rejects invalid output alignment/length and packets too large for the + /// libopus single-stream format, and returns any native codec failure. + pub fn decode( + &mut self, + packet: Option<&[u8]>, + pcm: &mut [i16], + decode_fec: bool, + ) -> Result { + let channels = self.channels.count(); + if !pcm.len().is_multiple_of(channels) + || pcm.is_empty() + || pcm.len() / channels > self.sample_rate * 120 / 1_000 + || packet.is_some_and(|value| value.is_empty() || value.len() > MAX_PACKET_BYTES) + { + return Err(Error(OPUS_BAD_ARG)); + } + let frame_size = pcm.len() / channels; + let (packet_pointer, packet_bytes) = packet.map_or((std::ptr::null(), 0), |value| { + ( + value.as_ptr(), + c_int::try_from(value.len()).unwrap_or(c_int::MAX), + ) + }); + let result = unsafe { + opus_decode( + self.raw.as_ptr(), + packet_pointer, + packet_bytes, + pcm.as_mut_ptr(), + c_int::try_from(frame_size).map_err(|_| Error(OPUS_BAD_ARG))?, + c_int::from(decode_fec), + ) + }; + result_to_size(result) + } +} + +impl Drop for Decoder { + fn drop(&mut self) { + unsafe { opus_decoder_destroy(self.raw.as_ptr()) }; + } +} + +fn validate_sample_rate(sample_rate: u32) -> Result { + if matches!(sample_rate, 8_000 | 12_000 | 16_000 | 24_000 | 48_000) { + c_int::try_from(sample_rate).map_err(|_| Error(OPUS_BAD_ARG)) + } else { + Err(Error(OPUS_BAD_ARG)) + } +} + +fn result_to_size(result: c_int) -> Result { + usize::try_from(result).map_err(|_| Error(result)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn system_libopus_encodes_and_decodes_a_real_frame() { + let mut encoder = Encoder::voip(48_000, Channels::Mono).unwrap(); + let mut decoder = Decoder::new(48_000, Channels::Mono).unwrap(); + let pcm = (0..960) + .map(|index| if index % 40 < 20 { 8_000 } else { -8_000 }) + .collect::>(); + let mut packet = [0_u8; MAX_PACKET_BYTES]; + let packet_bytes = encoder.encode(&pcm, &mut packet).unwrap(); + assert!(packet_bytes > 0); + + let mut output_pcm = [0_i16; 960]; + assert_eq!( + decoder + .decode(Some(&packet[..packet_bytes]), &mut output_pcm, false) + .unwrap(), + 960 + ); + assert!(output_pcm.iter().any(|sample| *sample != 0)); + } + + #[test] + fn safe_boundary_rejects_invalid_shapes() { + assert!(Encoder::voip(44_100, Channels::Mono).is_err()); + let mut encoder = Encoder::voip(48_000, Channels::Mono).unwrap(); + assert!(encoder.encode(&[0; 100], &mut [0; 100]).is_err()); + let mut decoder = Decoder::new(48_000, Channels::Stereo).unwrap(); + assert!(decoder.decode(Some(&[1]), &mut [0; 3], false).is_err()); + } +} diff --git a/crates/libremetaverse-voice-webrtc/Cargo.toml b/crates/libremetaverse-voice-webrtc/Cargo.toml index 2093097..ad82089 100644 --- a/crates/libremetaverse-voice-webrtc/Cargo.toml +++ b/crates/libremetaverse-voice-webrtc/Cargo.toml @@ -8,9 +8,9 @@ repository.workspace = true description = "WebRTC voice shims for the MetaCrate LibreMetaverse rewrite" [dependencies] -audiopus = "0.2" hound = "3.5" libremetaverse = { path = "../libremetaverse" } +libremetaverse-opus = { path = "../libremetaverse-opus" } libremetaverse-structured-data = { path = "../libremetaverse-structured-data" } libremetaverse-types = { path = "../libremetaverse-types" } serde = { version = "1", features = ["derive"] } diff --git a/crates/libremetaverse-voice-webrtc/src/native.rs b/crates/libremetaverse-voice-webrtc/src/native.rs index f1f49dc..5f9773a 100644 --- a/crates/libremetaverse-voice-webrtc/src/native.rs +++ b/crates/libremetaverse-voice-webrtc/src/native.rs @@ -7,8 +7,7 @@ #![allow(clippy::too_many_arguments)] // The RTC loop receives independent owned I/O resources. #![allow(clippy::too_many_lines)] // Linear loops preserve the WebRTC mutation/drain ordering. -use audiopus::coder::{Decoder, Encoder}; -use audiopus::{Application, Channels, SampleRate}; +use libremetaverse_opus::{Channels, Decoder, Encoder}; use libremetaverse_types::UUID; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value, json}; @@ -262,7 +261,7 @@ pub struct OpusFrame(pub Vec); /// Encodes PCM into WebRTC Opus frames using native libopus. pub fn encode_pcm_48k_mono(samples: &[i16]) -> Result, WebRtcError> { - let encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip) + let mut encoder = Encoder::voip(SAMPLE_RATE, Channels::Mono) .map_err(|error| WebRtcError::Audio(error.to_string()))?; let mut padded = samples.to_vec(); let remainder = padded.len() % FRAME_SAMPLES; @@ -783,8 +782,8 @@ async fn run_client( ) { let mut buffer = vec![0_u8; 65_536]; let mut playback: Option = None; - let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).ok(); - let encoder = Encoder::new(SampleRate::Hz48000, Channels::Mono, Application::Voip).ok(); + let mut decoder = Decoder::new(SAMPLE_RATE, Channels::Mono).ok(); + let mut encoder = Encoder::voip(SAMPLE_RATE, Channels::Mono).ok(); let mut captured_elapsed = Duration::ZERO; let mut running = true; while running { @@ -833,7 +832,7 @@ async fn run_client( None => running = false, }, captured = capture.recv() => { - if let (Some(pcm), Some(codec)) = (captured, encoder.as_ref()) + if let (Some(pcm), Some(codec)) = (captured, encoder.as_mut()) && pcm.len() == FRAME_SAMPLES { let mut packet = vec![0_u8; 4_000]; @@ -1731,7 +1730,7 @@ mod tests { fn wav_is_resampled_and_encoded_as_valid_opus() { let frames = encode_wav_bytes(&wav_fixture()).unwrap(); assert_eq!(frames.len(), 5); - let mut decoder = Decoder::new(SampleRate::Hz48000, Channels::Mono).unwrap(); + let mut decoder = Decoder::new(SAMPLE_RATE, Channels::Mono).unwrap(); let mut samples = vec![0_i16; FRAME_SAMPLES * 6]; assert_eq!( decoder diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..7247156 --- /dev/null +++ b/deny.toml @@ -0,0 +1,72 @@ +[graph] +targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-gnu", + "x86_64-apple-darwin", +] +all-features = true + +[advisories] +ignore = [] + +[licenses] +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MIT", + "Unicode-3.0", + "Zlib", +] +confidence-threshold = 0.93 +exceptions = [] + +[licenses.private] +ignore = false + +[bans] +multiple-versions = "deny" +# Path-only workspace dependencies resolve as `*` in Cargo metadata. The Rust +# dependency-policy audit below rejects wildcard registry dependencies while +# permitting those local, unpublished path edges. +wildcards = "allow" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" +allow = [] +allow-workspace = true +deny = [] +skip = [ + { crate = "block-buffer@0.10.4", reason = "Approved RustCrypto digest 0.10 compatibility line" }, + { crate = "cfg_aliases@0.1.1", reason = "Approved older nix build-helper line" }, + { crate = "const-oid@0.9.6", reason = "Approved DTLS certificate compatibility line" }, + { crate = "cpufeatures@0.2.17", reason = "Approved RustCrypto digest 0.10 compatibility line" }, + { crate = "crypto-common@0.1.7", reason = "Approved RustCrypto digest 0.10 compatibility line" }, + { crate = "digest@0.10.7", reason = "Approved legacy hash and WebRTC compatibility line" }, + { crate = "getrandom@0.2.17", reason = "Approved elliptic-curve entropy compatibility line" }, + { crate = "getrandom@0.3.4", reason = "Approved rand 0.9 entropy compatibility line" }, + { crate = "nix@0.28.0", reason = "Approved machine-address Unix compatibility line" }, + { crate = "nom@7.1.3", reason = "Approved Skia bindgen parser compatibility line" }, + { crate = "rand_core@0.6.4", reason = "Approved elliptic-curve random compatibility line" }, + { crate = "rand_core@0.9.5", reason = "Approved rand 0.9 compatibility line" }, + { crate = "sha2@0.10.9", reason = "Approved WebRTC RustCrypto compatibility line" }, + { crate = "shlex@1.3.0", reason = "Approved Skia bindgen shell parser compatibility line" }, + { crate = "syn@2.0.119", reason = "Approved MetaCrate code generator syntax line" }, + { crate = "thiserror@1.0.69", reason = "Approved machine-address error compatibility line" }, + { crate = "thiserror-impl@1.0.69", reason = "Approved machine-address derive compatibility line" }, + { crate = "windows-sys@0.52.0", reason = "Approved machine-address Windows compatibility line" }, +] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] + +[sources.allow-org] +github = [] +gitlab = [] +bitbucket = [] diff --git a/docs/dependency-policy.md b/docs/dependency-policy.md new file mode 100644 index 0000000..ea8720c --- /dev/null +++ b/docs/dependency-policy.md @@ -0,0 +1,81 @@ +# Dependency and supply-chain policy + +MetaCrate treats the locked dependency graph as reviewed release input. The +canonical direct-dependency and duplicate-version inventory is +[`ci/dependency-policy.json`](../ci/dependency-policy.json), and the native Rust +`metacrate-ci-matrix dependency-audit` command compares that inventory with +`cargo metadata --locked --all-features`. + +The audit fails when a direct crate or resolved direct version is added or +removed without review, duplicate versions drift, a registry dependency uses a +wildcard, a package comes from Git or an unapproved registry, rationale is +missing, or the dependency lacks an implemented responsibility in +[`RUSTREWRITE.md`](../RUSTREWRITE.md). Its JSON evidence records every direct +consumer and dependency scope, all approved duplicate sets, native direct +dependencies, review dates, and the resolved external package count. + +The policy intentionally distinguishes Rust 1.96.0, the minimum supported Rust +version, from current stable Rust used for development and release validation. +The release matrix checks both. Raising the MSRV requires an explicit policy, +matrix, documentation, and changelog review; ordinary dependency updates may +not raise it accidentally. + +## Automated gates + +The Ubuntu-only `supply-chain` Gitea workflow installs pinned versions of +`cargo-deny` and `cargo-machete`, then runs: + +```sh +cargo run --locked -p metacrate-ci-matrix -- \ + dependency-audit --evidence artifacts/dependency-audit.json +cargo deny check advisories licenses bans sources --hide-inclusion-graph +cargo machete --with-metadata +``` + +`cargo-deny` accepts only crates.io sources and the reviewed permissive license +set in `deny.toml`. Advisory exceptions and license exceptions are empty. +Duplicate versions are denied unless one exact version is listed with a reason; +the Rust audit independently verifies the complete exact duplicate set across +the lockfile, including target-specific packages. `cargo-machete` rejects +unused direct dependencies. The only metadata exclusions are `pkg-config` and +`vcpkg` in the two native adapter crates because their target-conditional use +is directly visible in each `build.rs`. + +## Native and platform requirements + +All native boundaries have Linux, Windows, and macOS strategies: + +| Boundary | Linux and macOS | Windows | Feature scope | +|---|---|---|---| +| libopus 1.3+ | `pkg-config` package `opus` | vcpkg `opus` for MSVC; `pkg-config` for GNU | WebRTC voice crate | +| OpenJPEG 2.5.4+ | `pkg-config` package `libopenjp2` | vcpkg `openjpeg` for MSVC; `pkg-config` for GNU | opt-in `jpeg2000` | +| Skia 0.99.0 | target-specific official binary cache or source build | same target-specific strategy | opt-in `skia` | +| Vorbis 0.5.6 stack | `vorbis_rs` builds its reviewed C codec stack | same crate strategy | opt-in `vorbis` | +| Physical audio | ALSA development files on Linux; CoreAudio is system-provided on macOS | WASAPI is system-provided | opt-in `real-audio` | + +`pkg-config` and `vcpkg` only discover libraries; they are not runtime +dependencies. `libremetaverse-opus` and `libremetaverse-openjpeg` are the only +workspace crates allowed to contain unsafe ABI calls. They expose owned, +validated safe Rust APIs and require exclusive mutable access to native codec +state. No macOS-only API is used without Linux and Windows equivalents. + +## Review and update cadence + +The graph is reviewed at least quarterly using `reviewed_on` and `review_by`. +RustSec advisories are reviewed immediately. For every update: + +1. identify the implemented caller and confirm the dependency remains needed; +2. inspect release notes, maintenance state, license, MSRV, enabled features, + native code, and target-specific build behavior; +3. update one crate deliberately with `cargo update -p NAME --precise VERSION`; +4. rerun the dependency audit, `cargo-deny`, `cargo-machete`, and the affected + release-matrix profiles from clean target directories; +5. update the exact policy versions and duplicate reasons only after reviewing + the resulting transitive graph. + +High-cost or native dependencies require isolated feature testing and all- +features unification. A successful compile does not replace the real codec, +secure WebRTC loopback, device, or live-grid gates applicable to that boundary. +The abandoned `audiopus`/`audiopus_sys` stack is prohibited by absence from the +direct inventory and by RustSec; MetaCrate instead binds the maintained system +libopus ABI in its private adapter. diff --git a/docs/release-ci-matrix.md b/docs/release-ci-matrix.md index 8a88ea5..44d42b9 100644 --- a/docs/release-ci-matrix.md +++ b/docs/release-ci-matrix.md @@ -42,6 +42,11 @@ The checked profiles cover: - portable Windows GNU and macOS cross-target compilation; - exact OpenJPEG, Skia, Opus, ALSA, and Vorbis prerequisite declarations. +Dependency purpose, maintenance, license, advisory, source, and duplicate +review is the separate supply-chain gate documented in +[`dependency-policy.md`](dependency-policy.md). Changes to manifests or the +lockfile trigger both gates. + ## Clean-build evidence Each profile uses `target/ci/` and refuses to start if that directory diff --git a/docs/webrtc.md b/docs/webrtc.md index 4ee3687..be87fac 100644 --- a/docs/webrtc.md +++ b/docs/webrtc.md @@ -42,8 +42,8 @@ time. Typical packages are: - Ubuntu/Debian: `libopus-dev` (and `pkg-config`). - Fedora: `opus-devel`. -- Windows MSVC: `audiopus` supplies supported prebuilt Opus libraries; a custom - libopus can be selected with `OPUS_LIB_DIR`/`LIBOPUS_LIB_DIR`. +- Windows MSVC: install `opus` through vcpkg and set `VCPKG_ROOT`; the adapter + uses vcpkg's target-aware library discovery. Windows GNU uses `pkg-config`. - macOS: install `opus` with the system package manager when it is not already discoverable by `pkg-config`. diff --git a/tests/api-compile/Cargo.lock b/tests/api-compile/Cargo.lock index d007e24..1ddf8aa 100644 --- a/tests/api-compile/Cargo.lock +++ b/tests/api-compile/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -27,13 +62,48 @@ dependencies = [ ] [[package]] -name = "aotuv_lancer_vorbis_sys" -version = "0.1.6" +name = "arrayvec" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc4fd1a61860d2f1198b60bedd30910eaffa978f1ee6214dfb24ac70d589225" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ - "cc", - "ogg_next_sys", + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -55,6 +125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -71,18 +142,39 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bcdec_rs" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.1" @@ -140,6 +232,18 @@ dependencies = [ "shlex", ] +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -158,6 +262,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.1" @@ -166,7 +281,31 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", ] [[package]] @@ -188,6 +327,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -228,6 +373,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -237,6 +397,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -244,6 +416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -256,6 +429,91 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -263,7 +521,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -273,10 +533,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.2", ] +[[package]] +name = "dimpl" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6aa42b0c64c3e5311a2afad224b32db1ee129d21c63daaaf8ea747b846cdbc" +dependencies = [ + "aes", + "aes-gcm", + "arrayvec", + "aws-lc-rs", + "ccm", + "chacha20 0.9.1", + "chacha20poly1305", + "der", + "ecdsa", + "generic-array", + "hkdf", + "hmac", + "log", + "nom 8.0.0", + "once_cell", + "p256", + "p384", + "pkcs8", + "rand 0.9.5", + "rand_core 0.6.4", + "rcgen", + "sec1", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "time", + "x25519-dalek", + "x509-cert", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -304,6 +601,41 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "errno" version = "0.3.14" @@ -314,6 +646,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.29" @@ -330,6 +684,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -423,6 +783,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -438,6 +799,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -447,11 +820,56 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "rand_core", + "r-efi 6.0.0", + "rand_core 0.10.1", "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "http" version = "1.5.0" @@ -661,12 +1079,34 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +[[package]] +name = "is" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08f9118d003d441f79c1070e84d0a2a89f35721ca8ae0cd5e1f9026dc4a2517" +dependencies = [ + "crc", + "serde", + "str0m-proto", + "subtle", + "tracing", +] + [[package]] name = "itoa" version = "1.0.18" @@ -743,6 +1183,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.189" @@ -768,14 +1214,12 @@ dependencies = [ "serde_json", "tar", "tokio", - "vorbis_rs", ] [[package]] name = "libremetaverse-imaging" version = "0.0.1" dependencies = [ - "libremetaverse-openjpeg", "libremetaverse-types", ] @@ -797,7 +1241,7 @@ dependencies = [ ] [[package]] -name = "libremetaverse-openjpeg" +name = "libremetaverse-opus" version = "0.0.1" dependencies = [ "pkg-config", @@ -860,7 +1304,7 @@ dependencies = [ "getrandom 0.4.3", "md-5", "sha1", - "sha2", + "sha2 0.11.0", "uuid", ] @@ -880,15 +1324,23 @@ dependencies = [ "libremetaverse", "libremetaverse-structured-data", "libremetaverse-types", + "roxmltree", + "tokio", ] [[package]] name = "libremetaverse-voice-webrtc" version = "0.0.1" dependencies = [ + "hound", "libremetaverse", + "libremetaverse-opus", "libremetaverse-structured-data", "libremetaverse-types", + "serde", + "serde_json", + "str0m", + "tokio", ] [[package]] @@ -971,6 +1423,12 @@ dependencies = [ "libremetaverse-voice-webrtc", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1017,6 +1475,59 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objc2" version = "0.6.4" @@ -1177,12 +1688,12 @@ dependencies = [ ] [[package]] -name = "ogg_next_sys" -version = "0.1.5" +name = "oid-registry" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2d7a48e247c2bb07e633aefb65a38648ea58c7eedd4e4408a5861721ab049b" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "cc", + "asn1-rs", ] [[package]] @@ -1191,6 +1702,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -1212,6 +1729,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1224,12 +1774,45 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1239,6 +1822,30 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1278,7 +1885,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", @@ -1314,21 +1921,65 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20", + "chacha20 0.10.1", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -1343,7 +1994,20 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rcgen" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" +dependencies = [ + "aws-lc-rs", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", ] [[package]] @@ -1413,6 +2077,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -1423,7 +2097,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1451,6 +2125,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1536,7 +2219,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1563,6 +2246,35 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sctp-proto" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f895c3c33ae20283f9129bd09db2ca69138798b372ef2b98bd2946d23ea4819b" +dependencies = [ + "bytes", + "crc", + "log", + "rand 0.9.5", + "rustc-hash", + "slab", + "thiserror 2.0.20", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -1599,6 +2311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -1645,6 +2358,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.11.0" @@ -1662,6 +2386,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -1706,12 +2440,75 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "str0m" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4656b60e74d1a0cf7c407b0d992c5bc141c3e75193f4b1399817d54a180505f3" +dependencies = [ + "arrayvec", + "base64ct", + "combine", + "dimpl", + "fastrand", + "is", + "sctp-proto", + "serde", + "str0m-proto", + "str0m-rust-crypto", + "subtle", + "time", + "tracing", +] + +[[package]] +name = "str0m-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f3d99f2cf6c76a502e45feb896ea71e54787ede8744bcdb215acfbfcb905dd" +dependencies = [ + "base64ct", + "dimpl", + "fastrand", + "serde", + "subtle", + "time", +] + +[[package]] +name = "str0m-rust-crypto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947c2417bf43e47504911f92f47c22b6125a57d162e1661f1c77b78ebb3aa9d3" +dependencies = [ + "aes", + "aes-gcm", + "ctr", + "dimpl", + "hmac", + "p256", + "sha1", + "sha2 0.10.9", + "str0m-proto", + "time", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1811,6 +2608,36 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -1937,9 +2764,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1973,6 +2812,22 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -2020,20 +2875,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vorbis_rs" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c5da94d280f7a27e8c937e9b73df2da3e23a2583f48471fd8fb4c72f9c1933" -dependencies = [ - "aotuv_lancer_vorbis_sys", - "errno", - "getrandom 0.4.3", - "ogg_next_sys", - "thiserror 2.0.20", - "tinyvec", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -2059,6 +2900,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -2259,12 +3109,59 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -2275,6 +3172,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -2298,6 +3205,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -2324,6 +3251,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" diff --git a/tools/ci-matrix/src/dependency.rs b/tools/ci-matrix/src/dependency.rs new file mode 100644 index 0000000..c85a7da --- /dev/null +++ b/tools/ci-matrix/src/dependency.rs @@ -0,0 +1,375 @@ +use super::{MatrixError, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::{self, OpenOptions}; +use std::io::Write as _; +use std::path::Path; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +const POLICY_PATH: &str = "ci/dependency-policy.json"; +const REWRITE_PATH: &str = "RUSTREWRITE.md"; + +#[derive(Debug, Deserialize)] +struct DependencyPolicy { + schema: u32, + msrv: String, + current: String, + reviewed_on: String, + review_by: String, + direct: Vec, + approved_duplicates: Vec, +} + +#[derive(Debug, Deserialize)] +struct DirectPolicy { + name: String, + versions: Vec, + purpose: String, + maintenance: String, + transitive_cost: String, + native: bool, + rewrite_anchor: String, +} + +#[derive(Debug, Deserialize)] +struct DuplicatePolicy { + name: String, + versions: Vec, + reason: String, +} + +#[derive(Debug, Default, Serialize)] +struct ObservedDirect { + versions: BTreeSet, + consumers: BTreeSet, + scopes: BTreeSet, +} + +struct ObservedGraph { + direct: BTreeMap, + resolved_external_packages: usize, + duplicates: BTreeMap>, +} + +#[derive(Debug, Serialize)] +struct DependencyEvidence<'a> { + schema: u32, + msrv: &'a str, + current: &'a str, + reviewed_on: &'a str, + review_by: &'a str, + recorded_unix_seconds: u64, + direct_dependencies: &'a BTreeMap, + direct_dependency_count: usize, + resolved_external_package_count: usize, + approved_duplicates: &'a BTreeMap>, + native_dependencies: Vec<&'a str>, + status: &'static str, +} + +/// Audits the complete resolved dependency graph against its reviewed policy. +/// +/// # Errors +/// +/// Returns an error for unreviewed direct crates or versions, stale duplicate +/// approvals, wildcard or non-registry dependencies, incomplete rationale, or +/// a metadata/evidence I/O failure. +pub fn audit_dependencies(root: &Path, evidence: &Path) -> Result<()> { + if evidence.exists() { + return Err(MatrixError::new(format!( + "{} already exists; preserve or remove it before rerunning the audit", + evidence.display() + ))); + } + let policy: DependencyPolicy = serde_json::from_slice(&fs::read(root.join(POLICY_PATH))?)?; + validate_policy(&policy, &fs::read_to_string(root.join(REWRITE_PATH))?)?; + let metadata = cargo_metadata(root)?; + let observed = observe(&metadata)?; + compare_direct(&policy.direct, &observed.direct)?; + compare_duplicates(&policy.approved_duplicates, &observed.duplicates)?; + + let native_dependencies = policy + .direct + .iter() + .filter(|dependency| dependency.native) + .map(|dependency| dependency.name.as_str()) + .collect(); + let evidence_record = DependencyEvidence { + schema: 1, + msrv: &policy.msrv, + current: &policy.current, + reviewed_on: &policy.reviewed_on, + review_by: &policy.review_by, + recorded_unix_seconds: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| MatrixError::new("system clock predates Unix epoch"))? + .as_secs(), + direct_dependencies: &observed.direct, + direct_dependency_count: observed.direct.len(), + resolved_external_package_count: observed.resolved_external_packages, + approved_duplicates: &observed.duplicates, + native_dependencies, + status: "ok", + }; + if let Some(parent) = evidence.parent() { + fs::create_dir_all(parent)?; + } + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(evidence)?; + serde_json::to_writer_pretty(&mut file, &evidence_record)?; + file.write_all(b"\n")?; + file.sync_all()?; + Ok(()) +} + +fn validate_policy(policy: &DependencyPolicy, rewrite: &str) -> Result<()> { + if policy.schema != 1 || policy.msrv != "1.96.0" || policy.current != "stable" { + return Err(MatrixError::new( + "dependency policy must use schema 1, MSRV 1.96.0, and current stable", + )); + } + if !date_shape(&policy.reviewed_on) || !date_shape(&policy.review_by) { + return Err(MatrixError::new( + "dependency review dates must use YYYY-MM-DD", + )); + } + let mut names = BTreeSet::new(); + for dependency in &policy.direct { + if dependency.name.trim().is_empty() + || !names.insert(dependency.name.as_str()) + || dependency.versions.is_empty() + || dependency.purpose.trim().is_empty() + || dependency.rewrite_anchor.trim().is_empty() + || !rewrite.contains(&dependency.rewrite_anchor) + || !matches!( + dependency.maintenance.as_str(), + "active" | "stable" | "monitored-native" + ) + || !matches!( + dependency.transitive_cost.as_str(), + "low" | "medium" | "high" + ) + { + return Err(MatrixError::new(format!( + "dependency {} has incomplete or invalid review metadata", + dependency.name + ))); + } + unique_versions(&dependency.name, &dependency.versions)?; + } + let mut duplicate_names = BTreeSet::new(); + for duplicate in &policy.approved_duplicates { + if duplicate.name.trim().is_empty() + || !duplicate_names.insert(duplicate.name.as_str()) + || duplicate.versions.len() < 2 + || duplicate.reason.trim().len() < 20 + { + return Err(MatrixError::new(format!( + "duplicate {} needs unique versions and a substantive reason", + duplicate.name + ))); + } + unique_versions(&duplicate.name, &duplicate.versions)?; + } + Ok(()) +} + +fn date_shape(value: &str) -> bool { + value.len() == 10 + && value.as_bytes()[4] == b'-' + && value.as_bytes()[7] == b'-' + && value + .bytes() + .enumerate() + .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit()) +} + +fn unique_versions(name: &str, versions: &[String]) -> Result<()> { + let unique = versions.iter().map(String::as_str).collect::>(); + if unique.len() != versions.len() || unique.iter().any(|version| version.trim().is_empty()) { + return Err(MatrixError::new(format!( + "dependency {name} versions must be nonempty and unique" + ))); + } + Ok(()) +} + +fn cargo_metadata(root: &Path) -> Result { + let output = Command::new(super::cargo_program()) + .args([ + "metadata", + "--locked", + "--all-features", + "--format-version", + "1", + ]) + .current_dir(root) + .output()?; + if !output.status.success() { + return Err(MatrixError::new(format!( + "cargo metadata failed during dependency audit: {}", + String::from_utf8_lossy(&output.stderr).trim() + ))); + } + Ok(serde_json::from_slice(&output.stdout)?) +} + +fn observe(metadata: &Value) -> Result { + let packages = metadata["packages"] + .as_array() + .ok_or_else(|| MatrixError::new("cargo metadata has no package array"))?; + let mut package_by_id = BTreeMap::new(); + let mut all_external = BTreeMap::>::new(); + for package in packages { + let id = string(package, "id")?; + let name = string(package, "name")?; + let version = string(package, "version")?; + let source = package["source"].as_str(); + if let Some(source) = source { + if !source.starts_with("registry+https://github.com/rust-lang/crates.io-index") { + return Err(MatrixError::new(format!( + "package {name} {version} uses unapproved source {source}" + ))); + } + all_external + .entry(name.to_owned()) + .or_default() + .insert(version.to_owned()); + } + if package["dependencies"] + .as_array() + .is_some_and(|dependencies| { + dependencies.iter().any(|dependency| { + dependency["source"].is_string() && dependency["req"].as_str() == Some("*") + }) + }) + { + return Err(MatrixError::new(format!( + "package {name} contains a wildcard dependency" + ))); + } + package_by_id.insert( + id.to_owned(), + (name.to_owned(), version.to_owned(), source.is_some()), + ); + } + + let nodes = metadata["resolve"]["nodes"] + .as_array() + .ok_or_else(|| MatrixError::new("cargo metadata has no resolve nodes"))?; + let mut direct = BTreeMap::::new(); + for node in nodes { + let id = string(node, "id")?; + let Some((consumer, _, false)) = package_by_id.get(id) else { + continue; + }; + let dependencies = node["deps"] + .as_array() + .ok_or_else(|| MatrixError::new("cargo metadata node has no dependencies"))?; + for dependency in dependencies { + let package_id = string(dependency, "pkg")?; + let Some((name, version, true)) = package_by_id.get(package_id) else { + continue; + }; + let observed = direct.entry(name.clone()).or_default(); + observed.versions.insert(version.clone()); + observed.consumers.insert(consumer.clone()); + for kind in dependency["dep_kinds"].as_array().into_iter().flatten() { + observed + .scopes + .insert(kind["kind"].as_str().unwrap_or("normal").to_owned()); + } + } + } + let duplicates = all_external + .iter() + .filter(|(_, versions)| versions.len() > 1) + .map(|(name, versions)| (name.clone(), versions.clone())) + .collect(); + Ok(ObservedGraph { + direct, + resolved_external_packages: all_external.values().map(BTreeSet::len).sum(), + duplicates, + }) +} + +fn string<'a>(value: &'a Value, key: &str) -> Result<&'a str> { + value[key] + .as_str() + .ok_or_else(|| MatrixError::new(format!("cargo metadata field {key} is not a string"))) +} + +fn compare_direct( + policy: &[DirectPolicy], + observed: &BTreeMap, +) -> Result<()> { + let expected = policy + .iter() + .map(|dependency| { + ( + dependency.name.as_str(), + dependency.versions.iter().cloned().collect::>(), + ) + }) + .collect::>(); + let expected_names = expected.keys().copied().collect::>(); + let observed_names = observed.keys().map(String::as_str).collect::>(); + if expected_names != observed_names { + return Err(MatrixError::new(format!( + "direct dependency policy mismatch; expected {expected_names:?}, observed {observed_names:?}" + ))); + } + for (name, versions) in expected { + if observed[name].versions != versions { + return Err(MatrixError::new(format!( + "direct dependency {name} version mismatch; expected {versions:?}, observed {:?}", + observed[name].versions + ))); + } + } + Ok(()) +} + +fn compare_duplicates( + policy: &[DuplicatePolicy], + observed: &BTreeMap>, +) -> Result<()> { + let expected = policy + .iter() + .map(|duplicate| { + ( + duplicate.name.clone(), + duplicate.versions.iter().cloned().collect::>(), + ) + }) + .collect::>(); + if expected != *observed { + return Err(MatrixError::new(format!( + "resolved duplicate policy mismatch; expected {expected:?}, observed {observed:?}" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checked_in_dependency_policy_matches_the_locked_graph() { + let root = super::super::workspace_root(Path::new(env!("CARGO_MANIFEST_DIR"))).unwrap(); + let evidence = std::env::temp_dir().join(format!( + "metacrate-dependency-audit-{}.json", + std::process::id() + )); + let _ = fs::remove_file(&evidence); + audit_dependencies(&root, &evidence).unwrap(); + let contents = fs::read_to_string(&evidence).unwrap(); + assert!(contents.contains("\"status\": \"ok\"")); + fs::remove_file(evidence).unwrap(); + } +} diff --git a/tools/ci-matrix/src/lib.rs b/tools/ci-matrix/src/lib.rs index 642ae8e..64ccb1b 100644 --- a/tools/ci-matrix/src/lib.rs +++ b/tools/ci-matrix/src/lib.rs @@ -11,6 +11,10 @@ use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; +mod dependency; + +pub use dependency::audit_dependencies; + pub const MATRIX_PATH: &str = "ci/release-matrix.json"; const WORKFLOW_PATH: &str = ".gitea/workflows/release-matrix.yml"; const REQUIRED_PROFILES: [&str; 7] = [ diff --git a/tools/ci-matrix/src/main.rs b/tools/ci-matrix/src/main.rs index e79cfa6..a09bc96 100644 --- a/tools/ci-matrix/src/main.rs +++ b/tools/ci-matrix/src/main.rs @@ -1,4 +1,4 @@ -use metacrate_ci_matrix::{audit, load, run, workspace_root}; +use metacrate_ci_matrix::{audit, audit_dependencies, load, run, workspace_root}; use std::path::{Path, PathBuf}; fn main() { @@ -32,7 +32,26 @@ fn execute() -> Result<(), Box> { run(&root, &matrix, &profile, &evidence)?; println!("release CI profile {profile}: ok ({})", evidence.display()); } - _ => return Err("usage: ci-matrix audit | run PROFILE --evidence FILE".into()), + Some("dependency-audit") => { + let flag = arguments + .next() + .ok_or("dependency-audit requires --evidence FILE")?; + let evidence = arguments + .next() + .ok_or("dependency-audit requires --evidence FILE")?; + if flag != "--evidence" || arguments.next().is_some() { + return Err("usage: ci-matrix dependency-audit --evidence FILE".into()); + } + let evidence = absolute_or_rooted(&root, &evidence); + audit_dependencies(&root, &evidence)?; + println!("dependency policy: ok ({})", evidence.display()); + } + _ => { + return Err( + "usage: ci-matrix audit | run PROFILE --evidence FILE | dependency-audit --evidence FILE" + .into(), + ); + } } Ok(()) }