Dependency currency wave: skia-safe 0.99, the RustCrypto digest-0.11 family, jni 0.22 and ten more — plus the notices they invalidated #193

Merged
enricobuehler merged 26 commits from worktree-dep-currency-wave into main 2026-08-13 12:41:57 +00:00
Owner

Thirteen dependency migrations the aws-lc-rs PR (#192) deliberately left out, plus the notices
regeneration they all needed. Version-currency work — but two of them turned out not to be.

The two that are not currency

aes 0.8 → 0.9 removes a footgun that was costing Android in the field. RustCrypto's aes 0.8
only enables ARMv8 hardware AES behind --cfg aes_armv8, which .cargo/config.toml sets for
aarch64 — but a RUSTFLAGS environment variable overrides config rustflags entirely, and
cargo-ndk sets RUSTFLAGS internally.
So every Android arm64-v8a build has been running
software AES on the per-packet decrypt path. Measured on an M3 Ultra, crypto/open_in_place,
1408-byte MTU shard, AES-128-GCM, single core:

config throughput
aes 0.8 + both cfgs 2.19 GiB/s
aes 0.8, cfgs stripped (what Android shipped) 225 MiB/s
aes 0.9 + both cfgs 5.2766 GiB/s
aes 0.9, no cfgs 5.2766 GiB/s

0.9 detects at runtime with no flag, so the cfgs are now deleted and the failure mode is gone by
construction rather than by remembering. Sealed packets are byte-identical across the bump for
both AES-128-GCM and ChaCha20-Poly1305 over three sequence numbers, so this is not a wire change.

⚠️ One caveat stated plainly: the recorded 2026-07-14 baseline says 0.8-with-cfgs ≈ 790 MiB/s, but
this box measures 2.19 GiB/s for that exact configuration. The software figure reproduces closely
(225 vs ~240 MiB/s), so the harness and machine agree on that path — the gap is specific to the
accelerated one, most likely polyval/ghash patch-level movement since. It does not change the
decision in either direction, but it is unexplained.

The client licence notices were wrong, not merely stale. All four per-client
THIRD-PARTY-NOTICES.txt still attributed ring and named no aws-lc-rs at all — they were
never regenerated after #192 removed ring from the tree. Every client's shipped Acknowledgements
screen has been crediting a crypto library it does not carry while omitting the one it does.

Everything in the wave

Migration Notes
skia-safe 0.87 → 0.99 Twelve milestones; only three reach us — SkPath's mutating API deleted (0.91), gradient_shader deprecated (0.93, a hard break because our clippy gate is -D warnings), vk::BackendContext::new deprecated (0.98)
RustCrypto wave aes 0.9, aes-gcm 0.11, cbc 0.2, hmac 0.13, sha2 0.11, chacha20poly1305 0.11 — all six together; they share crypto-common/digest traits and a partial bump fights itself
jni 0.21 → 0.22 Removes 11 crates: rustls-platform-verifier already required 0.22, so the client was compiling two complete jni copies into one .so
x509-parser 0.16 → 0.18 Clears thiserror 1.0 from the host graph entirely
wasapi 0.23 → 0.24 Every changed .rs line is a comment; the only upstream removal was never called here
windows-service 0.7 → 0.8.1 Kills the last windows-sys 0.52. 0.8.0 only reaches 0.59 — the patch version is load-bearing
x11rb 0.13 → 0.14 No-libxcb property preserved and verified
reis 0.6 → 0.7, xkbcommon 0.8 → 0.9 Currency; no source changes
mdns-sd 0.21 + if-addrs 0.15 One commit — separately, the duplicate stays. Fully cleared
rand 0.8 → 0.9 (host), libloading 0.9, base64 0.23, rcgen 0.14, criterion 0.8, android_logger 0.15 Currency

Deliberately not taken: pipewire 0.10 (hard-blocked — ashpd 0.13.x requires ^0.9 and
pipewire-sys's links key forbids coexistence), a blanket [patch.crates-io] windows, num-derive
inside vendor/usbip-sim, and the exact rusty_enet / ash pins.

Corrections to the original plan, measured rather than assumed

  • rand does not clear the duplicates in a shipping gamestream buildrsa → num-bigint-dig
    still pulls rand 0.8, and crypto-common holds rand_core 0.6. Only the native-only host sheds them.
  • base64 0.22 survives via rcgen 0.14 → pem 3.0.6. Upstream's move, not ours.
  • jni cleared windows-sys 0.45 but not thiserror 1.0 or the jni-sys split — vendored ndk 0.9.0
    holds both, and jni-sys 0.3.1 is itself a facade over 0.4.1.
  • rsa cannot join the wave: it is built on digest 0.10 and its 0.10 line is release-candidate
    only, which is wrong under the Moonlight pairing ceremony. It keeps features = ["sha2", "getrandom"]sha2 because rsa type parameters must speak rsa's own digest generation, and
    getrandom because RsaPrivateKey::new is bound on rand_core 0.6, which rand 0.9 no longer satisfies.

Two judgement calls worth review

  • base64 declared default-features = false. 0.23 added a default-ON simd-unsafe feature
    (hand-written AVX2/NEON). Accepting defaults would have unified hand-written unsafe SIMD into every
    artifact as a side effect of a currency bump. Turning it on is a perf decision deserving a measurement.
  • gen-third-party-notices.sh no longer prefers cargo about for the root file. cargo-about only
    sees cargo dependencies, so it silently drops every vendored tree — pyrowave, the Granite subset,
    volk, Vulkan-Headers, Font Awesome brand icons, Simple Icons — which ship inside first-party crates
    under their own licences. Measured: 7,274 lines / ~514 crates with zero mentions of volk,
    Vulkan-Headers or Font Awesome, against the python generator's 17,324 / 575 with all of them. Merely
    having cargo-about on $PATH was enough to degrade the file. cargo-about remains what the CI licence
    gate runs — that asks a different question and writes to /dev/null.

Also included

Three tests on the GameStream cert path (gamestream/cert.rs), covering what #192 changed but could
not verify: the fresh-install RSA-2048 identity under rcgen-on-aws-lc-rs, a Moonlight-shaped TLS 1.2
mutual handshake, and X25519MLKEM768 pinned by name so a provider or feature regression that
silently drops ML-KEM to a classical curve fails in CI rather than in the field.

Plus two stale comments corrected: gamestream/cert.rs and identity.rs both still explained
themselves in terms of "rcgen's ring backend", which #192 made false.

Verified

Each bump was verified alone by its author; the integration below is the first time all thirteen
compiled together.

Linux — x86_64-unknown-linux-gnu, overall RC=0. cargo metadata --locked; punktfunk-core;
punktfunk-host --bins; host clippy in both shapes (with and without --all-targets — CI lints
without it, so a cfg(test)-only import passes locally and fails the shipping build); pf-capture;
pf-inject; pf-vdisplay; pf-client-core; pf-console-ui; the native-only host
(--no-default-features --features pyrowave); cargo fmt --all --check. cargo tree -i ring empty.

Windows — x86_64-pc-windows-msvc. cargo metadata --locked, host check, host clippy both
shapes, tray check + clippy, pf-client-core clippy, pf-console-ui check — all RC=0. Host tests:
484 passed, 1 failed.

That one failure is mgmt::tests::display_monitors_answers_even_with_no_compositor, and it is
pre-existing rather than assumed to be: reverting wasapi to 0.23 on the box reproduces it
byte-identically. An ssh session is not the console session, so SESSIONNAME is empty and monitor
enumeration returns empty while the compositor still resolves as windows.

The Windows host check matters beyond the tally — it is what type-checks
windows/install.rs::random_password, the one rand 0.9 call site no Linux box compiles.

skia is never built from source. Taken from build-script output on both targets, not from the
release page:

TRYING TO DOWNLOAD AND INSTALL SKIA BINARIES:
  0.99.0/…-x86_64-pc-windows-msvc-jpegd-jpege-pdf-textlayout-vulkan
DOWNLOAD AND INSTALL SUCCEEDED

Note the asset name moved: jpeg joined skia-safe's default feature set between 0.87 and 0.99, so
the JPEG codecs now come along. That is a latent fix — pf-console-ui/src/screens/library.rs feeds
host poster art to Image::from_encoded, and JPEG posters previously fell out as
"undecodable poster".

Duplicates, measured on the integrated tree. windows-sys on windows-msvc went
0.45, 0.52, 0.59, 0.610.59, 0.61: jni 0.22 removed the 0.45 and windows-service 0.8.1 the
0.52, a compounding neither bump reaches alone. if-addrs is single-version. On Linux the only
remaining duplicate is rand (0.8.7 / 0.9.5 / 0.10.2), which is expected — rsa → num-bigint-dig
holds 0.8.

Not verified

  • No GPU runtime. skia's BackendContextBuilder change is on the Vulkan init path; the
    pf-console-ui tests rasterize to CPU surfaces. A live --browse session on a GPU box is still owed.
  • No APK and no xcframework — the Android side was verified as a linked .so for both ABIs plus
    the API-28 import check, not a Gradle build.
  • The full clients/windows leg (sdl3 + the pinned windows-rs git rev) was not built.
Thirteen dependency migrations the aws-lc-rs PR (#192) deliberately left out, plus the notices regeneration they all needed. Version-currency work — but two of them turned out not to be. ## The two that are not currency **`aes` 0.8 → 0.9 removes a footgun that was costing Android in the field.** RustCrypto's `aes` 0.8 only enables ARMv8 hardware AES behind `--cfg aes_armv8`, which `.cargo/config.toml` sets for aarch64 — but **a `RUSTFLAGS` environment variable overrides config rustflags entirely, and `cargo-ndk` sets `RUSTFLAGS` internally.** So every Android arm64-v8a build has been running *software* AES on the per-packet decrypt path. Measured on an M3 Ultra, `crypto/open_in_place`, 1408-byte MTU shard, AES-128-GCM, single core: | config | throughput | |---|---| | aes 0.8 + both cfgs | 2.19 GiB/s | | aes 0.8, cfgs stripped (what Android shipped) | **225 MiB/s** | | aes 0.9 + both cfgs | 5.2766 GiB/s | | **aes 0.9, no cfgs** | **5.2766 GiB/s** | 0.9 detects at runtime with no flag, so the cfgs are now deleted and the failure mode is gone by construction rather than by remembering. Sealed packets are **byte-identical** across the bump for both AES-128-GCM and ChaCha20-Poly1305 over three sequence numbers, so this is not a wire change. ⚠️ One caveat stated plainly: the recorded 2026-07-14 baseline says 0.8-with-cfgs ≈ 790 MiB/s, but this box measures 2.19 GiB/s for that exact configuration. The *software* figure reproduces closely (225 vs ~240 MiB/s), so the harness and machine agree on that path — the gap is specific to the accelerated one, most likely polyval/ghash patch-level movement since. It does not change the decision in either direction, but it is unexplained. **The client licence notices were wrong, not merely stale.** All four per-client `THIRD-PARTY-NOTICES.txt` still attributed **`ring`** and named **no aws-lc-rs at all** — they were never regenerated after #192 removed ring from the tree. Every client's shipped Acknowledgements screen has been crediting a crypto library it does not carry while omitting the one it does. ## Everything in the wave | Migration | Notes | |---|---| | skia-safe 0.87 → 0.99 | Twelve milestones; only three reach us — `SkPath`'s mutating API deleted (0.91), `gradient_shader` deprecated (0.93, a hard break because our clippy gate is `-D warnings`), `vk::BackendContext::new` deprecated (0.98) | | RustCrypto wave | aes 0.9, aes-gcm 0.11, cbc 0.2, hmac 0.13, sha2 0.11, chacha20poly1305 0.11 — all six together; they share `crypto-common`/`digest` traits and a partial bump fights itself | | jni 0.21 → 0.22 | Removes **11 crates**: `rustls-platform-verifier` already required 0.22, so the client was compiling two complete jni copies into one `.so` | | x509-parser 0.16 → 0.18 | Clears thiserror 1.0 from the **host** graph entirely | | wasapi 0.23 → 0.24 | Every changed `.rs` line is a comment; the only upstream removal was never called here | | windows-service 0.7 → **0.8.1** | Kills the last windows-sys 0.52. 0.8.0 only reaches 0.59 — the patch version is load-bearing | | x11rb 0.13 → 0.14 | No-libxcb property preserved and verified | | reis 0.6 → 0.7, xkbcommon 0.8 → 0.9 | Currency; no source changes | | mdns-sd 0.21 + if-addrs 0.15 | One commit — separately, the duplicate stays. Fully cleared | | rand 0.8 → 0.9 (host), libloading 0.9, base64 0.23, rcgen 0.14, criterion 0.8, android_logger 0.15 | Currency | Deliberately **not** taken: pipewire 0.10 (hard-blocked — ashpd 0.13.x requires `^0.9` and pipewire-sys's `links` key forbids coexistence), a blanket `[patch.crates-io] windows`, num-derive inside `vendor/usbip-sim`, and the exact `rusty_enet` / `ash` pins. ## Corrections to the original plan, measured rather than assumed - **rand does not clear the duplicates in a shipping gamestream build** — `rsa → num-bigint-dig` still pulls rand 0.8, and `crypto-common` holds rand_core 0.6. Only the native-only host sheds them. - **base64 0.22 survives** via `rcgen 0.14 → pem 3.0.6`. Upstream's move, not ours. - **jni cleared windows-sys 0.45 but not thiserror 1.0 or the jni-sys split** — vendored `ndk` 0.9.0 holds both, and `jni-sys` 0.3.1 is itself a facade over 0.4.1. - **`rsa` cannot join the wave**: it is built on `digest` 0.10 and its 0.10 line is release-candidate only, which is wrong under the Moonlight pairing ceremony. It keeps `features = ["sha2", "getrandom"]` — `sha2` because rsa *type parameters* must speak rsa's own digest generation, and `getrandom` because `RsaPrivateKey::new` is bound on rand_core 0.6, which rand 0.9 no longer satisfies. ## Two judgement calls worth review - **base64 declared `default-features = false`.** 0.23 added a default-ON `simd-unsafe` feature (hand-written AVX2/NEON). Accepting defaults would have unified hand-written unsafe SIMD into every artifact as a side effect of a currency bump. Turning it on is a perf decision deserving a measurement. - **`gen-third-party-notices.sh` no longer prefers `cargo about` for the root file.** cargo-about only sees *cargo* dependencies, so it silently drops every vendored tree — pyrowave, the Granite subset, volk, Vulkan-Headers, Font Awesome brand icons, Simple Icons — which ship inside first-party crates under their own licences. Measured: 7,274 lines / ~514 crates with zero mentions of volk, Vulkan-Headers or Font Awesome, against the python generator's 17,324 / 575 with all of them. Merely having cargo-about on `$PATH` was enough to degrade the file. cargo-about remains what the CI licence *gate* runs — that asks a different question and writes to `/dev/null`. ## Also included Three tests on the GameStream cert path (`gamestream/cert.rs`), covering what #192 changed but could not verify: the fresh-install RSA-2048 identity under rcgen-on-aws-lc-rs, a Moonlight-shaped TLS 1.2 mutual handshake, and **X25519MLKEM768 pinned by name** so a provider or feature regression that silently drops ML-KEM to a classical curve fails in CI rather than in the field. Plus two stale comments corrected: `gamestream/cert.rs` and `identity.rs` both still explained themselves in terms of "rcgen's ring backend", which #192 made false. ## Verified Each bump was verified alone by its author; the integration below is the first time all thirteen compiled together. **Linux — `x86_64-unknown-linux-gnu`, overall RC=0.** `cargo metadata --locked`; punktfunk-core; punktfunk-host `--bins`; host clippy in **both** shapes (with and without `--all-targets` — CI lints without it, so a `cfg(test)`-only import passes locally and fails the shipping build); pf-capture; pf-inject; pf-vdisplay; pf-client-core; pf-console-ui; the native-only host (`--no-default-features --features pyrowave`); `cargo fmt --all --check`. `cargo tree -i ring` empty. **Windows — `x86_64-pc-windows-msvc`.** `cargo metadata --locked`, host check, host clippy both shapes, tray check + clippy, pf-client-core clippy, pf-console-ui check — all RC=0. Host tests: **484 passed, 1 failed.** That one failure is `mgmt::tests::display_monitors_answers_even_with_no_compositor`, and it is pre-existing rather than assumed to be: reverting wasapi to 0.23 on the box reproduces it byte-identically. An ssh session is not the console session, so `SESSIONNAME` is empty and monitor enumeration returns empty while the compositor still resolves as `windows`. The Windows host check matters beyond the tally — it is what type-checks `windows/install.rs::random_password`, the one `rand` 0.9 call site no Linux box compiles. **skia is never built from source.** Taken from build-script output on both targets, not from the release page: ``` TRYING TO DOWNLOAD AND INSTALL SKIA BINARIES: 0.99.0/…-x86_64-pc-windows-msvc-jpegd-jpege-pdf-textlayout-vulkan DOWNLOAD AND INSTALL SUCCEEDED ``` Note the asset name moved: `jpeg` joined skia-safe's *default* feature set between 0.87 and 0.99, so the JPEG codecs now come along. That is a latent fix — `pf-console-ui/src/screens/library.rs` feeds host poster art to `Image::from_encoded`, and JPEG posters previously fell out as `"undecodable poster"`. **Duplicates, measured on the integrated tree.** `windows-sys` on windows-msvc went `0.45, 0.52, 0.59, 0.61` → **`0.59, 0.61`**: jni 0.22 removed the 0.45 and windows-service 0.8.1 the 0.52, a compounding neither bump reaches alone. `if-addrs` is single-version. On Linux the only remaining duplicate is `rand` (0.8.7 / 0.9.5 / 0.10.2), which is expected — `rsa → num-bigint-dig` holds 0.8. ## Not verified - **No GPU runtime.** skia's `BackendContextBuilder` change is on the Vulkan init path; the pf-console-ui tests rasterize to CPU surfaces. A live `--browse` session on a GPU box is still owed. - **No APK and no xcframework** — the Android side was verified as a linked `.so` for both ABIs plus the API-28 import check, not a Gradle build. - The full `clients/windows` leg (sdl3 + the pinned windows-rs git rev) was not built.
enricobuehler added 26 commits 2026-08-13 12:41:11 +00:00
PR #192 moved the rustls backend to aws-lc-rs and merged before CI reported.
Two of the things it changed here shipped with no assertion behind them.

`generate()` mints the RSA-2048 host identity and runs ONLY when no cert
exists, so no upgraded box ever re-executes it — a fresh install is the
only thing that would have found a regression. It was reached by other
tests via `ServerIdentity::ephemeral()`, but purely as a fixture: nothing
checked that what came back was still RSA-2048, which is the one property
Moonlight requires. The handshake behaviour had no coverage at all, and
the GameStream TLS path is the single place a legacy peer meets the new
backend.

Three tests:

  - generate_mints_a_loadable_rsa2048_identity — the fresh-install path,
    asserting the cert signature is 256 bytes (RSA-2048) rather than
    depending on an `rsa` accessor that could change shape.
  - moonlight_shaped_peer_completes_a_tls12_mutual_handshake — a peer that
    pins out of band, as Moonlight does, presenting an RSA-2048 client cert
    against the real `tls::server_config`.
  - tls13_negotiates_the_post_quantum_group — pins X25519MLKEM768 by name,
    so a provider or feature regression that silently drops ML-KEM back to
    a classical curve fails here instead of in the field.

Also corrects the comment on `generate()`. It opened by asserting the
workspace is ring-only because aws-lc-sys breaks Windows CI, and explained
that rcgen's *ring* backend is what loads the RSA key. Both are now false:
rcgen is on aws_lc_rs and loads and self-signs the key fine — verified, not
assumed. rcgen still cannot GENERATE an RSA key on either backend, which is
the part of the comment that remains true and load-bearing.

Verified on Linux (Ubuntu 26.04, x86_64): 3/3 pass, clippy clean both with
and without --all-targets (host CI lints without it), and the native-only
`--no-default-features --features pyrowave` build still checks clean — the
whole module is gamestream-gated, so it compiles out there.
Version currency for the crate behind the host's WASAPI loopback capture and
virtual mic, and the Windows client's render/capture path. No behavior change.

The 0.23 -> 0.24 API delta is almost entirely additive (device-change
notification callbacks, `AudioMeterInformation`, `HardwareSupport`, `DeviceState:
Clone + Copy`). The single removal is `AudioClient::get_bufferframecount`,
deprecated since 0.17 in favour of `get_buffer_size` — this tree never called it,
so no call site moved.

0.24 also fixes upstream the dangling-`PCWSTR` bug this tree routes around in
five places: `DeviceEnumerator::get_device` used to build its argument as
`PCWSTR::from_raw(HSTRING::from(id).as_ptr())`, dropping the `HSTRING` at the end
of that statement so `GetDevice` read freed memory. Those five comments asserted
the bug in the PRESENT tense and are now wrong, so they are corrected here rather
than left to mislead. The workarounds themselves STAY: `open_wasapi_device` is
still the one resolution path whose errors name the endpoint id and whose
`IMMDevice` `probe_activation` needs, and `device_by_id` additionally filters to
ACTIVE endpoints (`EnumAudioEndpoints(dir, DEVICE_STATE_ACTIVE)`), which the
crate's `get_device` does not. Removing them is a behavior change, not currency.

⚠ This does NOT collapse the duplicate windows-rs. wasapi 0.24 still depends on
`windows ^0.62` / `windows-core ^0.62` exactly as 0.23 did, so the crates.io
`windows 0.62.2` still sits alongside the pinned git copy that `clients/windows`
uses. That duplicate costs build time and binary size, not correctness, and the
blanket `[patch.crates-io] windows` that would collapse it stays ruled out — the
pinned rev uses header-named features while a dozen manifests still use the old
`Win32_*` namespace features.
Version currency for the Android client's only `log` backend. No code change: 0.15 is
almost entirely an internal refactor (the single `lib.rs` split into `config`/`arrays`/
`id`/`platform_log_writer`/`tests`), and the surface this crate uses — `init_once`,
`Config::default`, `with_max_level`, `with_tag` — is untouched. The lockfile delta is the
version and checksum alone: no dependency was added, removed or re-resolved, and the
third-party crate count stays at 582.

The one thing 0.15 adds that we must NOT take is recorded next to the dependency: the new
opt-in `android-api-30` feature filters levels through `__android_log_is_loggable_len` so
logcat's `setprop log.tag.*` overrides are honoured, but it HARD-LINKS that API-30 symbol.
Against our minSdk-28 floor that is a `System.loadLibrary` failure on Android 9/10 — the
identical shape of the ndk 0.9.0 `AMediaCodec_setOnFrameRenderedCallback` break the manifest
already warns about a few lines further down. Default features keep it off; the comment
explains why so nobody "completes" the upgrade by enabling it.

Verified with cargo-ndk (NDK 30.0.14904198), which is the only way to exercise this crate at
all — `android_logger` sits behind `cfg(target_os = "android")`, so the host workspace build
never compiles it:

  cargo ndk -t arm64-v8a   check -p punktfunk-client-android   → ok
  cargo ndk -t armeabi-v7a check -p punktfunk-client-android   → ok

Checked, not built: these are `cargo check` runs, not a linked `.so` and not an APK.
Version currency for the SCM plumbing behind `punktfunk-host service` (the
dispatcher, control handler and ServiceManager install) and the tray's
unprivileged QUERY_STATUS probe. No code changed in either crate.

The payoff is dependency unification, not the API. `windows-service 0.7` was the
ONLY crate in the workspace still pulling `windows-sys 0.52`, so it alone kept a
fourth windows-sys major compiling. It resolves to 0.8.1, which moves to
`windows-sys 0.61` — a version the tree already builds — and the duplicate
disappears:

  cargo tree -d --target x86_64-pc-windows-msvc | grep '^windows-sys v'
    before: 0.45.0, 0.52.0, 0.59.0, 0.61.2
    after:  0.45.0,         0.59.0, 0.61.2

Note 0.8.0 would NOT have been enough — it lands on windows-sys 0.59. 0.8.1 is
the release that reaches 0.61, hence the `"0.8"` caret plus the comment pinning
the reasoning to the manifest.

The 0.7 -> 0.8 delta is tiny and touches nothing this tree calls: `ServiceAccess`
gains READ_CONTROL / WRITE_DAC / WRITE_OWNER (additive), and `Service::raw_handle`
changes return type from `Security::SC_HANDLE` to `Services::SC_HANDLE` as a
consequence of the windows-sys bump — we never call it. `ScHandle` is crate-private
upstream. No enum gained variants, and the service control handler's match already
ends in a `_ =>` arm, so the `#[non_exhaustive]` types stay safe.

What remains duplicated (deliberately out of scope here): windows-sys 0.45 via
`jni`, and 0.59 via `punktfunk-core` + `if-addrs`.
These six share the `crypto-common` and `digest` traits, so they move as ONE change — a
partial bump leaves crates on incompatible trait generations that cannot interoperate.

The point is to delete a footgun, not for version hygiene. `aes` 0.8 only enabled ARMv8
hardware AES on aarch64 behind `--cfg aes_armv8`, and `polyval` 0.6 gated its PMULL GHASH
path behind `--cfg polyval_armv8`. A RUSTFLAGS env var OVERRIDES config rustflags
ENTIRELY, so any aarch64 lane that sets its own (cargo-ndk does this internally for every
Android build) silently dropped both and fell back to software AES on the per-packet
decrypt path. `aes` 0.9 runtime-detects via `cpufeatures` on aarch64 and `polyval` 0.7
selects its armv8 intrinsics backend by target_arch, so neither cfg exists any more.

API changes this generation forces:
  - `AeadInPlace` -> `AeadInOut`; `{encrypt,decrypt}_in_place_detached` ->
    `{encrypt,decrypt}_inout_detached` taking an `InOutBuf`.
  - `generic-array` -> `hybrid-array`: `Array::from_slice` is deprecated in favour of the
    infallible `&[u8; N] -> &Array<u8, UN>` reference cast, or `TryFrom` for runtime slices.
  - `Mac::new_from_slice` moved to `KeyInit::new_from_slice`.
  - `BlockEncrypt`/`BlockDecrypt` -> `BlockCipherEncrypt`/`BlockCipherDecrypt`;
    `BlockEncryptMut` -> `BlockModeEncrypt`; `encrypt_padded_vec_mut` -> `encrypt_padded_vec`.

`rsa` 0.9 is the one crate that cannot come along: it is built on `digest` 0.10 and its
0.10 line is still release-candidate only, which is not something the Moonlight pairing
ceremony should ride. Its `sha2` feature re-exports the digest its own traits speak, so the
three sites where a digest appears as an `rsa` TYPE PARAMETER (cert.rs, pairing.rs, tls.rs)
now take `rsa::sha2::Sha256` explicitly; everything else in the crate is on sha2 0.11.

The GameStream wire formats are untouched — AES-128-ECB no-padding, the CBC audio path, and
the GCM control-stream seal all keep their exact byte behaviour; only the type plumbing moved.
Twelve skia-safe releases (0.88 … 0.99), carrying Skia milestones 140 through
150, every one of them breaking under 0.x semver. Only three of those changes
actually reach this crate — the Vulkan surface/backend-texture path, the
textlayout/paragraph typography and RuntimeEffect all came through untouched:

* m143 (skia-safe 0.91) DELETED SkPath's mutating API. `Path::new()` followed by
  `move_to`/`line_to`/`arc_to`/`close` no longer compiles at all — geometry is
  built through `PathBuilder` now and frozen with `snapshot()`/`detach()`. That
  is the entire error list: 34 E0599s over eight call sites (the hint-bar
  triangles and the PlayStation triangle in `glyphs.rs`, the chevron / space /
  backspace / check icons in `widgets.rs`, the padlock shackle in
  `screens/home.rs`). Each becomes a `PathBuilder` detached at the draw call, so
  the path is still built and thrown away once per draw exactly as before.

* 0.93 deprecated the `gradient_shader` module in favour of `gradient`. Only a
  warning, but the Format/clippy gate runs `-D warnings`, so it is a hard break
  for us. The three gradients — the panel stroke in `theme.rs`, the
  connect-overlay vignette in `shell/overlays.rs`, the host monogram in
  `screens/home.rs` — now build a `gradient::Gradient` from
  `gradient::Colors::new_evenly_spaced` plus `Interpolation::default()`. That
  default (unpremul interpolation, destination colour space, shorter hue) is
  what the old `flags: None` argument mapped to, so the pixels do not move. The
  new API takes `Color4f` directly, which drops the `.to_color()` 8-bit
  round-trip the old signature forced.

* 0.98 deprecated `vk::BackendContext::new` in favour of a builder (upstream
  #1292). `skia_overlay.rs` now calls
  `BackendContext::new_builder(...)` + `build()`, passing `max_api_version:
  None` so Skia keeps deriving its cap from `vkEnumerateInstanceVersion()` —
  bit-for-bit what `new()` passed. `build()` is the unsafe half, so the SAFETY
  proof moved down onto it.

`ash` is untouched and stays on the workspace's exact `=0.38.0+1.3.281` pin:
skia-safe lists ash only as a DEV dependency, so the bump cannot reach it.

The prebuilt-binary assumption still holds — verified from the build log, not
from the release page: skia-bindings printed `DOWNLOAD AND INSTALL SUCCEEDED`
for
`skia-binaries-a25a0fdb7d90429aa2d1-x86_64-unknown-linux-gnu-jpegd-jpege-pdf-textlayout-vulkan`,
so no CI leg compiles Skia from source. The asset name DID change: `jpeg` joined
skia-safe's default feature set between 0.87 and 0.99, so `jpegd-jpege` is now
in the name. We take defaults, so the JPEG codecs came along — which is a fix in
disguise, since `screens/library.rs` hands host poster art straight to
`Image::from_encoded`, and JPEG posters used to fall out as "undecodable". The
Cargo.toml comment now records the verified asset names and the silent-source-
build trap for the next bump.

Verified on 192.168.1.21, x86_64-unknown-linux-gnu, toolchain 1.96.0:
  cargo build  -p pf-console-ui                              exit 0
  cargo clippy -p pf-console-ui --all-targets -- -D warnings  exit 0
  cargo test   -p pf-console-ui                              82 passed, 1 ignored
  cargo fmt --all --check                                    exit 0
aes 0.9 runtime-detects the ARMv8-Crypto backend on aarch64 via `cpufeatures` and polyval 0.7
picks its armv8 PMULL intrinsics by target_arch, so neither cfg exists any more — passing them
is inert. That retires a real footgun rather than tidying a file: a RUSTFLAGS env var overrides
config rustflags ENTIRELY, so every aarch64 lane that set its own (cargo-ndk does so internally
for every Android arm64-v8a build) silently dropped both and ran SOFTWARE AES on the per-packet
decrypt path.

Measured before deleting, `crypto/open_in_place` (1408-byte MTU shard, AES-128-GCM, single core,
Mac15,14 M3 Ultra, four runs back to back under identical background load):

    aes 0.8 + both cfgs      2.19 GiB/s
    aes 0.8, cfgs stripped     225 MiB/s   ~10x cliff — reproduces the recorded ~240 MiB/s
    aes 0.9 + both cfgs      5.28 GiB/s
    aes 0.9, cfgs stripped   5.28 GiB/s    identical to 4 s.f.

The ChaCha20-Poly1305 series of the same bench was the control and moved 0.07% across the cfg
toggle at both versions, so the toggle demonstrably reached only the AES path. A final run with
the flags actually deleted (not merely RUSTFLAGS-overridden) reproduced 5.29 GiB/s.

.cargo/config.toml is kept as a tombstone carrying that table so the flags are not reintroduced.
The two CI comments that warned about losing these cfgs to a RUSTFLAGS override are updated —
mold in ci/cargo-config-mold.toml is now the only thing such an override can cost.
Version currency for the three crates that speak core X11: pf-capture's XFixes
cursor source, pf-vdisplay's gamescope splash client, and pf-client-core's
gamescope overlay watcher. Nothing outside the workspace pulls x11rb, so all
three move together and no two versions coexist in the lock.

**The no-libxcb property survives, which is the whole reason those manifests
say `default-features = false`.** 0.14 declares no `default` feature either, so
that flag is still the belt to the braces; `libc` and `as-raw-xcb-connection`
are still optional and still reachable only through `allow-unsafe-code`, and
`dl-libxcb` still requires it too. `cargo tree -e features -i x11rb -p
pf-capture` resolves to exactly `xfixes` -> `render` + `shape` and nothing else,
and neither `as-raw-xcb-connection` nor any other libxcb-linking crate appears
in Cargo.lock. `RustConnection` remains the only connection type, so no host or
client package gains a C dependency.

**One upstream behaviour change does land**, and it is why the `SessionBind` doc
in gamescope.rs moved. 0.14 removed the abstract-unix-socket attempt from
`rust_connection::stream` -- `ConnectAddress::Socket` is now documented as
"Connect to this Unix socket by path" -- so `@/tmp/.X11-unix/X<n>` is no longer
tried ahead of the filesystem path. That doc asserted the ATTACH route's XFixes
cursor reader reached the display over the abstract socket; it now records what
is actually load-bearing. On ATTACH the session belongs to
`gamescope-session-plus`, we arm no bind, its `/tmp` is the real one, and
`punktfunk-host.service` sets no `PrivateTmp` (nor does the NixOS host unit), so
`/tmp/.X11-unix/X<n>` is exactly where `DISPLAY` says it is. The two conditions
that would have needed the abstract fallback still cannot coincide: the bind
only arms for a resolved `punktfunk-gamescope`, whose patch level 2+ makes
`SessionPlan::gamescope_cursor` false and the reader is never spawned. The
splash client is gamescope's own nested child, inside the namespace, and reads
the bound directory directly. If those ever do have to coexist the reader logs
and retries forever and the stream runs without a composited pointer -- the doc
now says so instead of promising a fallback that no longer exists.

The rest of the 0.13.2 -> 0.14.0 delta is inert here: `AtomEnum::CUT_BUFFE_Rn`
was respelled `CUT_BUFFERn` (unused), the optional `raw-window-handle` went
0.5 -> 0.6 and `libloading`'s range widened (both features off), and the MSRV
moved 1.64 -> 1.68 against a 1.96 toolchain.

Verified on .25 (Ubuntu 26.04, `CARGO_BUILD_JOBS=2`), all rc=0:

    cargo build  -p pf-capture -p pf-client-core -p pf-vdisplay --locked
    cargo clippy -p pf-capture -p pf-client-core -p pf-vdisplay --locked -- -D warnings
    cargo clippy -p pf-capture -p pf-client-core -p pf-vdisplay --all-targets --locked -- -D warnings
    cargo test   -p pf-capture  --locked        68 passed, 0 failed
    cargo test   -p pf-vdisplay --locked       210 passed, 0 failed, 3 ignored
    cargo check  -p punktfunk-host --locked
    cargo fmt --all --check

Clippy is run BOTH ways on purpose: host CI lints without `--all-targets`, so a
`#[cfg(test)]`-only import would pass the local run and fail the shipping build.
`cargo tree -i ring` stays empty.
Version currency, but a real API migration rather than a version-number edit — and the
payoff is bigger than "one crate is newer", because jni 0.22 was ALREADY in this .so:
`rustls-platform-verifier` (via quinn-proto, for Android cert verification) depends on it,
so pinning 0.21 here compiled two complete jni copies into one library. Matching the
version collapses them.

Eliminated, measured as the delta in THIRD-PARTY-NOTICES.txt (582 → 571 crates, nothing
added): jni 0.21.1, its `cesu8`, and — because jni 0.21 was the SOLE consumer of
windows-sys 0.45.0, the oldest crate in the tree — that whole windows-rs 0.42 generation:
windows-sys 0.45.0, windows-targets 0.42.2 and its seven per-arch import libraries. Eleven
crates, carried for a `cfg(windows)` dependency of an Android-only bridge.

NOT eliminated, contrary to what the sweep expected — recorded in the manifest so the next
person does not re-derive it. thiserror 1.0 and the jni-sys 0.3/0.4 split both survive,
because jni was never their only source:

  thiserror 1.0.69  ← vendor/ndk 0.9.0 (+ asn1-rs/x509-parser, host side)
  jni-sys 0.3.1     ← vendor/ndk 0.9.0 AND crates.io ndk-sys 0.6

jni's share of both is gone; the remainder is the ndk stack. jni-sys 0.3.1 is itself a
facade crate over 0.4.1, so the split cannot close until ndk and ndk-sys move, and `ndk` is
vendored for a one-line visibility patch — changing its dependency versions would mean
rewriting the vendored snapshot instead of reading it as a diff against upstream. Left
alone deliberately.

The migration itself, across 66 native methods in 10 files:

  * `JNIEnv` split into `EnvUnowned` (the FFI-safe native-method argument) and `Env` (where
    the JNI calls live). The 41 methods that never touched the environment are a type
    rename; the 22 that do now acquire an `Env` inside `EnvUnowned::with_env` and map the
    outcome with an `ErrorPolicy`.
  * `LogErrorAndDefault` everywhere, chosen to PRESERVE behaviour: the old code swallowed
    JNI errors and returned a default, and this logs and returns the same default. The
    throwing policy would have been a behaviour change (new Java exceptions out of methods
    that previously failed quietly), which is not what a currency bump should do.
  * `with_env` also catches panics, which is exactly what the crate's own `jni_guard` did,
    so the guard is folded into it where the two would have nested. It stays on the methods
    that take no `Env`. ONE exception, marked at the call site: `nativeNextHidout` returns
    -1 as its failure sentinel, and every error policy resolves to `T::default()` — which
    for `jint` is 0, a *valid* byte count. That method keeps `jni_guard(-1, …)` outside and
    uses `with_env_no_catch` inside so the sentinel survives the panic path unchanged.
  * `jboolean` is `bool` in jni-sys 0.4, not `u8` — a type change, not a rename, and the
    reason for most of the mechanical diff (`down != 0` → `down`, `return 0` → `false`).
  * `Env::get_string` is deprecated in favour of `JString::try_to_string`, and CI runs
    clippy with `-D warnings`, so the call sites moved rather than being left to warn.
    Likewise `set_/get_*_array_region` → `JPrimitiveArray::set_region`/`get_region`.
  * `Env::get_native_interface()` is now `Env::get_raw()` — the raw pointer handed to
    `ndk::NativeWindow::from_surface`. The `as *mut _` cast next to it was already commented
    as bridging jni-sys skew between `jni` and `ndk`; that skew is now real (0.4 vs 0.3)
    rather than hypothetical, so the comment says so.
  * Return types moved from raw `jni::sys::jstring`/`jdoubleArray`/`jintArray` to
    `JString`/`JDoubleArray`/`JIntArray`, because `resolve()` requires `T: Default` and raw
    pointers have no `Default`. All three are `#[repr(transparent)]` over the same
    `jobject`, so the exported ABI is unchanged and `Default` IS the null reference the old
    code returned explicitly.

Kotlin and Gradle needed NO changes, checked rather than assumed: every affected
`external fun` in NativeBridge.kt already declares `Boolean` / `String?` / `IntArray?` /
`DoubleArray?`, which is what these signatures still present to the JVM, and the Gradle
side only shells out to cargo-ndk without naming a jni version.

Verified on NDK 30.0.14904198, both shipping ABIs, using the environment
clients/android/kit/build.gradle.kts hands cargo-ndk:

  cargo clippy -p punktfunk-client-android --all-targets -- -D warnings   → ok (host)
  cargo test   -p punktfunk-client-android                                → 20 passed
  cargo ndk -t arm64-v8a   --platform 28 clippy … -- -D warnings          → ok
  cargo ndk -t armeabi-v7a --platform 28 clippy … -- -D warnings          → ok
  cargo ndk -t arm64-v8a -t armeabi-v7a --platform 28 build               → both .so LINKED
  scripts/ci/check-android-jni-imports.sh … 28    → 2 ABI(s) clean at the API-28 floor

That last one matters for this change specifically: a cdylib links with dangling undefined
symbols, so the floor check is the only thing that would catch jni 0.22 hard-importing an
NDK entry point above minSdk 28 — the shape of the 0.9.0 `System.loadLibrary` regression.
It is clean.

`cargo tree -i ring` stays empty on host and on aarch64-linux-android.
Version currency for the libei path. pf-inject is the only consumer -- reis is a
pure-Rust implementation of the EI wire protocol, so this links nothing new and
still needs no libei on the host. The release tracks libei protocol 1.5.0 ->
1.6.0.

**Nothing in our API surface moved.** `ei::Context::new`, `handshake_tokio`,
`reis::tokio::EiConvertEventStream` and `reis::event::{Device, DeviceCapability,
EiEvent, Region}` all keep their shape; `Region`'s six fields are byte-identical.
The two enums grew variants for libei 1.6's `ei_text` (`EiEvent::TextKeysym` /
`TextUtf8`, `DeviceCapability::Text`), which compiles because `handle_ei`'s match
ends in `_ => {}` and the capability set we bind is written out explicitly. The
handshake now ADVERTISES `ei_text` as a supported interface, so a 1.6 EIS may
offer one; we never bind it and never emit on it.

**Behaviour we inherit, all of it upstream bug fixes:**

- Frames now commit per device. 0.6 held one global pending queue, so an
  `ei_device.frame` from device A committed device B's timestamped events under
  A's timestamp. Inert for us -- we are an EI *sender*, and the events we
  receive are device lifecycle plus `KeyboardModifiers`, none of which are the
  timestamped kind that queue -- but strictly more correct.
- `Device::interface()` now forgets an interface when the server sends its
  `destroyed` event, and `DeviceRemoved` un-registers the device's interfaces
  from the converter's reverse map. Our `slot.interface::<ei::Button>()` and
  friends therefore stop handing back a proxy for a torn-down interface instead
  of emitting into a dead object.
- `Device`, `Seat` and `Object` now hash consistently with their `PartialEq`.
  Both were already `Arc::ptr_eq`; `Hash` used the protocol object id, which is
  a broken pair. We keep devices in a `Vec` and compare with `==`/`!=`, so this
  changes nothing here.
- The wire backend rejects a header length above libei's 1 MiB `max_msglen`
  BEFORE waiting for that many bytes, so a malformed length no longer parks the
  reader until the connection dies.

**The host graph loses the `futures` facade crate.** reis's `tokio` feature
depended on `futures`; 0.7 depends on `futures-util` directly, which pf-inject
already declares. `cargo tree --target all -i futures` now names only relm4 (the
GTK Linux client), and `futures` + `futures-executor` are gone from
punktfunk-host's Linux tree (`futures-task` stays, under futures-util). Lockfile
delta is one package: `reis` 0.6.1 -> 0.7.1 with `futures` -> `futures-util` in
its dep list; the workspace package SET is unchanged, since relm4 and glib still
need those crates for the GTK client.

Verified on .25 (Ubuntu 26.04, `CARGO_BUILD_JOBS=2`), all rc=0:

    cargo build  -p pf-inject --locked
    cargo clippy -p pf-inject --locked -- -D warnings
    cargo clippy -p pf-inject --all-targets --locked -- -D warnings
    cargo test   -p pf-inject --locked    127 passed, 0 failed, 8 ignored
                                          + motion_contract: 7 passed, 0 failed
    cargo check  -p punktfunk-host --locked
    cargo clippy -p punktfunk-host --locked -- -D warnings
    cargo fmt --all --check

Clippy is run BOTH ways because host CI lints without `--all-targets`.
`cargo tree -i ring` stays empty.
Version currency for the virtual-keyboard keymap in pf-inject's wlroots path
(`inject/linux/wlr.rs`), the crate's only consumer.

Additive on the Rust side: one new wrapper, `Keymap::key_get_mods_for_level`,
and the `extern` declaration behind it. Nothing we call changed shape -- we use
`Context::new`, `Keymap::new_from_names`, `get_as_string`, `State::new`,
`Keycode::new`, `KeyDirection` and the `serialize_mods`/`serialize_layout` pair,
all untouched. The feature set is unchanged too: `default = ["wayland"]` in both
releases, so `x11` -- the feature that would pull `as-raw-xcb-connection` -- stays
off and this links only `libxkbcommon`, exactly as before.

The one thing worth writing down is the new symbol's floor. On .25's
libxkbcommon 1.13.1 it reads
`xkb_keymap_key_get_mods_for_level@@V_1.0.0`, so the declaration wants
libxkbcommon >= 1.0.0 (2020) if it is ever referenced. Nothing in the workspace
calls the wrapper, so no relocation for it reaches our binaries -- and every ship
target clears 1.0 by years regardless (Ubuntu 22.04 is 1.4, Debian 12 is 1.5,
Debian 13 is 1.7). pf-inject's test binary, which unlike the rlib actually links,
builds and runs clean.

Two internal fixes come along, both in constructors we do not use:
`new_from_string` passes the Rust string's pointer + length to
`xkb_keymap_new_from_buffer` instead of round-tripping through a `CString`
(whose `unwrap()` panicked on an interior NUL), and `new_from_fd` passes the
full mapped `size` rather than `size - 1`.

Verified on .25 (Ubuntu 26.04, `CARGO_BUILD_JOBS=2`), all rc=0:

    cargo build  -p pf-inject --locked
    cargo clippy -p pf-inject --locked -- -D warnings
    cargo clippy -p pf-inject --all-targets --locked -- -D warnings
    cargo test   -p pf-inject --locked    127 passed, 0 failed, 8 ignored
                                          + motion_contract: 7 passed, 0 failed
    cargo check  -p punktfunk-host --locked
    cargo clippy -p punktfunk-host --locked -- -D warnings
    cargo fmt --all --check

Clippy is run BOTH ways because host CI lints without `--all-targets`.
`cargo tree -i ring` stays empty.
x509-parser 0.16 pinned the old asn1-rs 0.6 / der-parser 9 / oid-registry 0.7
chain, and every one of those still depended on thiserror 1.0. It was the ONLY
thing doing so — so the host compiled two thiserror majors (and two
thiserror-impl proc macros) for one crate's error types. 0.18 moves the chain to
asn1-rs 0.7 + thiserror 2, which is the same major the rest of the tree already
uses, and the 1.0 half disappears — for the host, on every target and including
dev-dependencies:

    $ cargo tree -p punktfunk-host --target all -e normal,build,dev -i thiserror@1
    error: package ID specification `thiserror@1` did not match any packages

Scope that claim honestly: this clears the HOST graph, not the workspace.
thiserror 1.0 is still built, reached by `jni` 0.21 AND by the vendored
`ndk` 0.9.0, both under punktfunk-client-android. That is a different graph
and a different bump.

No source change was needed. The one API shift that could have reached us —
asn1-rs 0.7 making `BitString::data` a `Cow<[u8]>` instead of a `&[u8]` — lands
on `x509.signature_value.data.to_vec()` in gamestream/cert.rs and
gamestream/pairing.rs, and `to_vec()` goes through Deref either way. Both are
Moonlight pairing-hash inputs, so they are covered by the gamestream::cert
tests rather than taken on faith. nom 7 and nom 8 were already both in the lock;
this adds no new duplicate.

Verified on CachyOS (rustc 1.96.0):
  cargo clippy -p punktfunk-host --locked --all-targets -- -D warnings   OK
  cargo clippy -p punktfunk-host --locked -- -D warnings                 OK  (shipping build: no --all-targets)
  cargo clippy -p punktfunk-host --no-default-features --features pyrowave --all-targets --locked -- -D warnings   OK
  cargo test -p punktfunk-host --bins --locked      501 passed, 0 failed, 2 ignored
  cargo test ... gamestream::cert                   3 passed
  cargo tree -i ring                                nothing to print
punktfunk-core and pf-client-core were already on 0.9; the host sat on 0.8 by
drift, not by decision, so every build compiled two rand majors to satisfy six
call sites. Mechanical at five of them (`thread_rng()` -> `rng()`,
`gen_range` -> `random_range`); the sixth is the interesting one.

`RsaPrivateKey::new` is bounded on rand_core **0.6**'s `CryptoRngCore`. rand
0.9's `ThreadRng` implements rand_core 0.9's traits — same names, different
crate — so the RSA-2048 keygen in gamestream/cert.rs stopped satisfying the
bound the moment the version moved. It now draws from `rsa::rand_core::OsRng`:
rsa's own re-export, which is by construction the traits rsa compiled against,
so the two rand_core majors never have to meet in our source. That needs
`features = ["getrandom"]` on rsa (not one of its defaults) — and it must be
declared HERE rather than left to feature unification, because dropping our own
rand 0.8 also dropped the `std` feature that used to switch `rand_core/getrandom`
on as a side effect.

What this does and does not clear. The GAMESTREAM host keeps rand 0.8 +
rand_chacha 0.3 — `rsa` drags them in through `num-bigint-dig`, which is not
ours to move:

    $ cargo tree -i rand@0.8.7
    rand v0.8.7
    └── num-bigint-dig v0.8.6
        └── rsa v0.9.10
            └── punktfunk-host

The NATIVE-ONLY host (--no-default-features, no rsa) now sheds both entirely —
`cargo tree -p punktfunk-host --no-default-features --features pyrowave -i
rand@0.8.7` and the same for rand_chacha@0.3.1 both report no match. rand_core
0.6 stays in every flavour regardless: `crypto-common` (under aes-gcm) needs it,
which no rand bump can change.

`windows/install.rs`'s `random_password` is the one site a Linux box cannot
type-check; the edit there is the identical `thread_rng()` -> `rng()` the five
compiled sites took.

Verified on CachyOS (rustc 1.96.0):
  cargo clippy -p punktfunk-host --locked --all-targets -- -D warnings   OK
  cargo clippy -p punktfunk-host --locked -- -D warnings                 OK  (shipping build: no --all-targets)
  cargo clippy -p punktfunk-host --no-default-features --features pyrowave --all-targets --locked -- -D warnings   OK
  cargo test -p punktfunk-host --bins --locked      501 passed, 0 failed, 2 ignored
  cargo test ... gamestream::cert                   3 passed (incl. the fresh-install RSA-2048 keygen)
  cargo fmt --all --check                           clean
ureq 3 already pulls 0.23, so the tree compiled both minors purely because our
two direct declarations named the older one. The API we use — `Engine`,
`engine::general_purpose::STANDARD`, `encode`/`decode` — is unchanged in 0.23;
no source edits.

Both are declared `default-features = false, features = ["std"]` rather than
taking the defaults. 0.23 added `simd-unsafe` (hand-written AVX2/NEON engines)
as a DEFAULT-ON feature, and ureq declares base64 with default features off, so
today that code is not in the tree. Accepting the defaults here would unify the
feature on and quietly add an unsafe SIMD engine to every artifact as a side
effect of a currency bump. Whether to enable it is a perf question deserving a
measurement and its own commit; this one changes versions, not exposure. `std`
covers every call site (encode to `String`, decode to `Vec`).

base64 0.22 does NOT leave the tree: `rcgen` -> `pem` 3.0.6 is now its sole
remaining consumer, and it stays one after the rcgen 0.14 bump later in this
batch — 0.14 still depends on `pem` "3.0.2", which resolves to the same 3.0.6,
which still uses base64 0.22. Clearing that last copy is upstream's move (a
`pem` release on 0.23), not ours.

Verified on CachyOS (rustc 1.96.0):
  cargo clippy -p punktfunk-host -p pf-update-check --locked --all-targets -- -D warnings   OK
  cargo clippy -p punktfunk-host -p pf-update-check --locked -- -D warnings                 OK  (shipping build)
  cargo test -p punktfunk-host --bins --locked    501 passed, 0 failed, 2 ignored
  cargo test -p pf-update-check --locked          32 passed, 0 failed
  cargo fmt --all --check                         clean
All five declarations move together (pf-encode twice — Linux and Windows —
plus pf-client-core, pf-zerocopy and punktfunk-host), because a split would
have compiled two copies of a crate whose whole job is holding a process-wide
dlopen handle.

No source changes. 0.9 replaces the concrete parameter types with sealed traits
— `Library::new(impl AsFilename)` and `Library::get(impl AsSymbolName)` — and
both cover what our 16 call sites already pass: `&str` literals for the sonames
(`libnvidia-encode.so.1`, `libva.so.2`, `libnvidia-ml.so.1`, `libcuda.so.1`) and
`&[u8; N]` NUL-terminated byte literals for the symbols, which 0.9 implements
explicitly alongside `&[u8]`. MSRV rises to 1.88; the workspace pins 1.96.

libloading 0.8 does not leave the lock, and shouldn't: what remains is
`clang-sys` under `bindgen`, reached only as a BUILD-dependency of
ffmpeg-sys-next / libspa-sys / pyrowave-sys. That copy runs at build time and is
linked into nothing we ship.

Verified on CachyOS (rustc 1.96.0):
  cargo clippy --workspace --all-targets --locked -- -D warnings                 OK
  cargo clippy -p pf-encode --all-targets --locked --features nvenc,vulkan-encode,pyrowave -- -D warnings   OK
      (the only leg that compiles enc/linux/nvenc_cuda.rs, where the `lib.get(b"…\0")` calls live)
  cargo clippy -p punktfunk-host -p pf-encode -p pf-zerocopy -p pf-client-core --locked -- -D warnings   OK  (shipping build)
  cargo test -p punktfunk-host --bins --locked    501 passed, 0 failed, 2 ignored
  cargo test -p pf-encode --locked                33 passed, 5 ignored
  cargo test -p pf-zerocopy --locked              40 passed
  cargo fmt --all --check                         clean
One commit because splitting them accomplishes nothing: mdns-sd 0.20 ALREADY
depends on if-addrs 0.15, so while our own five crates declared 0.13 the tree
carried both copies no matter which of the two moved first. Moving them together
is what collapses it:

    $ cargo tree -d | grep '^if-addrs'
    (no output)

Neither needed a source change. mdns-sd 0.21's public API is purely additive
over 0.20.3 — the sole new item is `ServiceDaemon::set_max_packet_size`, and
`ServiceInfo`'s surface is byte-identical — so `ServiceDaemon`/`ServiceInfo`/
`ServiceEvent`/`ResolvedService` behave as before at all six call sites
(host discovery + gamestream mdns, pf-client-core, and the Android, Windows and
probe clients). if-addrs 0.15 keeps 0.13's `Interface`/`IfAddr` shape, and we
only ever read those.

The one real change is a FEATURE, not a version. if-addrs has `link-local`, and
mdns-sd declares if-addrs with it on. Once our crates share that single copy,
unification turns it on for our calls too — meaning `get_if_addrs()` now also
reports fe80:: interfaces (and, on Windows, 169.254.x.x). Rather than inherit
that silently, punktfunk-core and punktfunk-host now NAME the feature. Two
reasons: it is what every real build gets anyway, so a standalone `cargo test -p
punktfunk-core` should not enumerate a different set of NICs than the host does;
and for the consumer here — Wake-on-LAN — it is the behaviour we want, since a
NIC is wake-capable whether or not it currently holds a routable address.

Verified on CachyOS (rustc 1.96.0):
  cargo clippy --workspace --all-targets --locked -- -D warnings   OK
  cargo test --workspace --locked                                  OK, 0 failed
  cargo test -p punktfunk-host --bins --locked      501 passed, 0 failed, 2 ignored
  cargo test ... gamestream::cert                   3 passed
  cargo fmt --all --check                           clean

(One `cargo test --workspace` attempt failed with E0463 "can't find crate for
pf_frame" in a doc-test. That is the target dir having only clippy's .rmeta for
a crate a doc-test wants to LINK, not anything in this change; a plain re-run
after cargo test built the rlibs was green.)
Both declarations keep `default-features = false, features = ["aws_lc_rs",
"pem"]`, which stays load-bearing in 0.14: `ring` is still in rcgen's DEFAULT
feature set, so dropping `default-features = false` would drag the backend this
tree deliberately left back in. Verified after the bump — `cargo tree -i ring`
finds nothing on x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc,
aarch64-apple-darwin or aarch64-linux-android.

One breaking change reached us. `CertifiedKey`'s key field was renamed
`key_pair` -> `signing_key` (the struct is now generic, `CertifiedKey<S:
SigningKey>`), which hits the two `generate_simple_self_signed` callers in
core's quic/endpoint.rs — the dev/test server endpoint and `generate_identity`.
Both are a field rename; `KeyPair` still provides `serialize_der`/`serialize_pem`.

Everything the GAMESTREAM identity depends on survives unchanged, which is what
this bump's precondition was about: `KeyPair::from_pkcs8_pem_and_sign_algo`,
`PKCS_RSA_SHA256`, `CertificateParams::new`, `date_time_ymd`, `self_signed`,
`Certificate::pem`. The split in gamestream/cert.rs is therefore untouched — the
RSA-2048 key is still minted by the `rsa` crate and handed to rcgen only to load
and self-sign, because rcgen cannot generate an RSA key on the backend we use.
That path is asserted, not assumed:

  cargo test -p punktfunk-host --bins --locked gamestream::cert    3 passed, 0 failed
    generate_mints_a_loadable_rsa2048_identity          ok   (fresh-install keygen)
    moonlight_shaped_peer_completes_a_tls12_mutual_handshake   ok
    tls13_negotiates_the_post_quantum_group             ok   (X25519MLKEM768 by name)

Verified on CachyOS (rustc 1.96.0):
  cargo clippy --workspace --all-targets --locked -- -D warnings        OK
  cargo clippy -p punktfunk-host -p punktfunk-core --locked -- -D warnings   OK  (shipping build)
  cargo test --workspace --locked      78 test binaries, all ok, 0 failed
  cargo fmt --all --check              clean
Dev-dependency of punktfunk-core only — it ships in nothing. `default-features =
false, features = ["cargo_bench_support"]` carries over unchanged; that feature
still exists in 0.8 and still keeps plotters/rayon out of a headless CI run.

One source change, and it is a lint issue rather than an API one.
`criterion::black_box` survives in 0.8 but is `#[deprecated]` — it now just
forwards to `std::hint::black_box` — and benches ARE compiled by
`cargo clippy --workspace --all-targets -- -D warnings`, so keeping the criterion
import would have turned a deprecation warning into a failed lint gate. The
bench imports the std one directly.

What CI actually consumes from criterion is the on-disk result layout, so that
was checked rather than assumed: 0.8 still writes
`target/criterion/<group>/<id>/new/estimates.json`, and the key
scripts/bench/compare.py reads — `median.point_estimate` — is still there:

  $ cargo bench -p punktfunk-core --bench pipeline -- --warm-up-time 1 --measurement-time 3
  ... 12/12 benchmarks reported, e.g. pipeline/gf16/1048576  thrpt: [537 MiB/s 540 MiB/s 542 MiB/s]
  $ find target/criterion -name estimates.json | wc -l
  24
  $ python3 -c 'import json; print(json.load(open(".../crypto/seal/new/estimates.json"))["median"]["point_estimate"])'
  817.96

Verified on CachyOS (rustc 1.96.0):
  cargo clippy -p punktfunk-core --all-targets --locked -- -D warnings   OK  (this is what compiles the bench)
  cargo clippy --workspace --all-targets --locked -- -D warnings         OK
  cargo bench -p punktfunk-core --bench pipeline --locked -- --test      12/12 Success
  cargo bench -p punktfunk-core --bench pipeline --locked -- --warm-up-time 1 --measurement-time 3   OK  (CI's exact line)
  cargo test -p punktfunk-core --locked   210 + 8 + 1 passed, 0 failed
  cargo fmt --all --check                 clean
# Conflicts:
#	Cargo.lock
#	crates/punktfunk-host/Cargo.toml
Covers all five generated files, not just the root one: the four per-client
copies are scoped to the binaries their package installs, so they move
independently of the workspace-wide file.

Root: 571 -> 575 crates, reflecting this wave (skia-safe 0.99, the RustCrypto
digest-0.11 family, jni 0.22, x11rb 0.14, reis 0.7, xkbcommon 0.9, wasapi 0.24,
windows-service 0.8.1, x509-parser 0.18, rand 0.9, base64 0.23, libloading 0.9,
mdns-sd 0.21 + if-addrs 0.15, rcgen 0.14, criterion 0.8, android_logger 0.15).

The per-client diffs are much larger than the wave alone explains, because they
were never regenerated after #192: all four still attributed `ring` and named no
aws-lc-rs at all. Since #192 removed ring from the tree entirely, the shipped
Acknowledgements screens have been crediting a crypto library the clients do not
carry while omitting the one they do. They now catch up on both changes at once.
(`ring` still appears via the generator's deliberate `--all-features`
over-approximation, which sees quinn-proto's wasm-only edge; that is by design —
listing an unlinked crate is untidy, omitting a linked one is the failure the
file exists to prevent.)

Also stops gen-third-party-notices.sh preferring `cargo about` for the root file.
That preference was silently destructive: cargo-about only sees CARGO
dependencies, so it drops every VENDORED_TREES entry -- pyrowave, the Granite
subset, volk, Vulkan-Headers, the Font Awesome brand icons, Simple Icons -- which
are third-party sources shipped inside first-party crates under their own
licences. Measured today: cargo-about emitted 7,274 lines / ~514 crates with zero
mentions of volk, Vulkan-Headers or Font Awesome, against the python generator's
17,324 / 575 with all of them. Merely having cargo-about on PATH was enough to
degrade the file, so anyone regenerating after this commit would have undone it.
cargo-about remains what the CI licence gate runs -- that job asks a different
question (is every licence in the about.toml allowlist) and writes to /dev/null.

Both licence-gate legs pass: `cargo about generate about.hbs --fail` and the
drivers-workspace leg, RC=0.
docs(host): the P-256 identity comment still named ring's backend
ci / docs-site (pull_request) Successful in 1m25s
apple / swift (pull_request) Successful in 2m15s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 2m33s
ci / web (pull_request) Successful in 3m26s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 6m20s
android / android (pull_request) Successful in 7m34s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 9m4s
ci / rust-arm64 (pull_request) Successful in 16m13s
nix / flake (pull_request) Successful in 16m36s
ci / rust (pull_request) Successful in 47m49s
4903c9d3b5
#192 moved rcgen to aws-lc-rs and removed ring from the tree, but this comment
still explained the P-256 path in terms of "rcgen's ring backend". It also
cross-references gamestream::cert's note, which this branch already corrected —
so leaving it made the two contradict each other.

The substance is unchanged and still load-bearing: rcgen generates EC keys
directly, while RSA has to be generated by the `rsa` crate and handed to rcgen
to self-sign, because no rcgen backend will generate an RSA key.
enricobuehler merged commit 14425716e5 into main 2026-08-13 12:41:57 +00:00
enricobuehler deleted branch worktree-dep-currency-wave 2026-08-13 12:42:00 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#193