dev -> qa -> main, across every public repo.
dev is where work lands and is meant to be pushed to often, so ci.yml stays
the fast answer: fmt, clippy, unit tests, leak-guard. qa is the release
candidate, and qa.yml is the claim that a commit is production worth —
release-profile tests on all three platforms, and the Linux cross-target
clippy that the local precommit gate has always run but CI never did.
Release profile matters as its own gate: overflow checks are off, debug_assert
is compiled out, and inlining changes what the optimiser can prove. A test
that only passes in debug never guarded the binary anyone ships.
qa.yml checks siblings out at qa rather than dev. This is not a monorepo, and
a qa run resolving its dependencies from unreleased dev tips would go green on
a combination that is not the one shipping — the exact mismatch the branch
model exists to prevent. ci.yml now tracks whichever branch triggered it for
the same reason, since it also fires on qa.
The consequence is that repos move to qa together, in dependency order. qa is
backfilled from main so an unchanged crate still presents working code to the
crates built on it.
freemkv-unlock had no automated checks at all: no lint, no tests, no
build on any OS but the one a developer happened to be sitting at. It is
the base of the dependency graph — libfreemkv path-deps it, bdemu
path-deps it with the emulation feature, and the rest pull it in
transitively — so a break here takes all seven other repos down, and
until now the first machine to notice would have been the release
workflow.
Full OS matrix, matching libfreemkv's: lint and test on Linux, plus
macOS and Windows building the tests (not `cargo check`, which skips
codegen and never compiles test-only code).
The `emulation` feature gets its own test run. It is off by default and
it is the face bdemu builds against, so nothing was compiling it.
Also adds the leak-guard workflow the other public repos already carry.
The Windows UI needs current winsafe, whose real minimum is 1.89 (its manifest
under-declares 1.87 while it uses NonNull::from_ref). Rather than stop at the
minimum, this goes to current stable and fixes what that costs.
The counter-intuitive result: 1.97 is CHEAPER than 1.89. libfreemkv had 54
clippy errors at 1.89 and 6 at 1.97, because clippy tightened the noisy
collapsible_if lint in between. Stopping at the minimum would have been the most
expensive choice available.
Roughly 47 lints across the eight repos, the large majority auto-fixed:
libfreemkv 6, freemkv-engine 14, bdemu 8, freemkv-keysources 7, autorip 6,
freemkv-unlock 3, freemkv-i18n 3. The hand-fixed ones are a descending sort to
sort_by_key(Reverse), four manual checked-division sites, a loop counter replaced
by enumerate, and a loop whose first let-else became a while-let.
Worth recording for whoever bumps next: clippy is MSRV-AWARE. Those 54 lints only
appear once the crate DECLARES 1.89 or later, because let-chains become
available. A bare `cargo +1.89 clippy` against a manifest still pinned at 1.87
reports clean and is meaningless — gate with the real precommit script, which is
also the only thing that covers build scripts.
The pin still sits below the Mac default, so it keeps doing its job: catching
lint drift locally before CI sees it.
The Windows UI needs winsafe, whose current release requires rustc 1.87. The
alternative was pinning winsafe back to an older release, which would bake a
stale API surface into a brand-new UI permanently to dodge one minor version.
The pin's purpose is to sit BELOW the Mac default so clippy drift is caught
locally before CI, not to stay on 1.86 specifically, so 1.87 preserves the
discipline exactly.
Verified before moving anything, not after: `cargo +1.87 clippy -- -D warnings`
and `cargo +1.87 fmt --check` are clean across all eight repos, and the full
precommit gate (fmt + clippy + tests) passes on libfreemkv, autorip, bdemu,
freemkv-engine and freemkv-keysources. Zero new lints, zero formatting drift.
The enforcement contact was an address on a domain used for internal
infrastructure, which does not belong in a public repository. GitHub needs no
mailbox to exist and is reachable for anyone who can read this file.
Bump to 1.6.0 (workspace sync, no functional change). Fix the README
example that called the removed matches()/unlock() API — rewrite to the
real unlock_features()/unlock_bus() contract (+ ld/README).
The unlocker performs the DVD bus-auth that clears the drive's
scrambled-read barrier, letting a protected DVD be read at all. That is
a property of the DVD medium, not of the CSS scheme — the descramble key
is recovered keylessly downstream — so naming it "CSS" conflated the
read-enable with the encryption. Rename the struct to DvdUnlocker, its
registry name to "DVD", and the tests to match. Bump 1.5.2.
Replace the single matches()+unlock() contract with two capability methods — unlock_features (drive riplock/speed/OEM VID at drive-prep) and unlock_bus (AACS/CSS bus-encryption removal for the mounted disc) — each defaulting to NotApplicable so an unlocker implements only what it does.
Add the renesis module: the Renesas-platform unlocker (Pioneer + HL-DT-ST Renesas), detected via the READ_BUFFER 0x02/0xF1 identity probe (ASCII "SAT" marker). Features only; the cert handles the bus.
Add product_id to DriveId. Bump to 1.2.3.
Each unlocker now carries its own stable name() (the single source; apps
never hardcode it) and a report-only applies_to() — phase-independent
'would this apply to this drive+disc', distinct from the dispatch-gated
matches(). LibreDrive overrides applies_to to report on drive identity
regardless of disc kind (its matches() is gated to the Unknown-kind init
phase). Enables a registry-driven unlocker matrix.
Reframe: an unlocker provides drive-features (speed/riplock — a property
of the DRIVE, any disc) AND bus removal (AACS decrypt+VID / CSS), kept
separate so a disc can take one without the other. A CSS DVD wants a
matched drive's speed but must NOT take its firmware bus-unlock (breaks
stock CSS).
Add Unlocker::apply_drive_features (default no-op, self-gating). LibreDrive
implements it with STOCK MMC commands only — SET STREAMING (0xB6), the
modern riplock lift many slot-loading BD combos honor when they ignore the
legacy SET CD SPEED (0xBB), then SET CD SPEED as fallback — so it is safe
on a non-unlocked drive. New build_set_streaming() + tests.
Expose the LibreDrive profile catalog as a curated public API on `ld`:
`ld::profiles()` returns the `Profiles` object (with `.get(drive_id)`) and
`ld::profile(filter)` finds one — the catalog of supported drives, queried
without unlocking. The unlock mechanism (firmware blobs, upload sequence, CDB
wire format) stays private; `LibreDrive` is pub(crate), reached only via
`all_unlockers()`.
The unlock-handshake wire format the bdemu test-emulator needs to impersonate a
drive (`UNLOCK_MARKER`, `is_unlock_read_buffer`) is behind a non-default
`emulation` feature; the `cdb` module compiles only under it. Rename the
`ScsiError` error variant to `Scsi` (matches css), gate the test-only
`load_bundled` and `SCSI_STATUS_CHECK_CONDITION`. Clippy-clean in both feature
configs; 86/89 tests pass.
Restore ld's drive-prep match guard so the firmware unlocker fires exactly once
(at drive-prep, kind==Unknown) and never re-fires during the content-keyed
Aacs/Css dispatch — the consumer (libfreemkv) dispatches at three points
(init/cert/css) with the appropriate kind. Add firmware_unlocker_name(drive_id)
for "is this drive supported?" drive-info display without unlocking. 89 tests.
Stage 3: the CSS bus-authentication (the challenge-response that sets the
drive's ASF=1 to unlock scrambled-sector reads, plus crypt_key + the CSS
tables) moves out of libfreemkv into the self-contained src/css module, with
its own error type. CssUnlocker impls crate::Unlocker — matches DiscKind::Css,
self-guards against a non-DVD profile, runs the bus-auth, and returns an empty
Unlocked (CSS yields no VID/bus key; the keyless Stevenson descramble stays in
libfreemkv). The keyless descramble (lfsr/stevenson) is NOT moved — that's
content decryption, the consumer's job.
all_unlockers() now returns [LibreDrive, AacsCert, CssUnlocker] — the entire
unlock layer is built. libfreemkv rewires onto it in stage 4. 89 tests pass.
Stage 2: the AACS host-certificate handshake (the AKE + bus-key derivation +
P-160/P-256 EC crypto, ~2050 lines) moves out of libfreemkv into the
self-contained src/aacs module, with its own error type (the Aacs* failure
points + structured ScsiError) and an aes_ecb_decrypt helper. AacsCert impls
crate::Unlocker — matches DiscKind::Aacs, runs run_cert_handshake against the
host certs the consumer passes via UnlockCtx, and returns Unlocked { vid,
bus_key }. collect_host_certs stays in libfreemkv (it reads keysources). The
SCSI contract gains ScsiSense + the AACS/REPORT-KEY opcodes. libfreemkv is
untouched (still green); it rewires onto this in stage 4.
72 tests pass (the handshake brought its full EC-crypto test suite).
Per the seam audit: keep /src purely the generic contract and move everything
unlocker-specific into its module.
- ld's error type moves src/error.rs -> src/ld/error.rs; it owns the conversions
From<scsi::ScsiError> (transport fault in) and From<ld::Error> for UnlockError
(outcome out). src/scsi.rs keeps only the generic transport contract + a
generic ScsiError; the trait no longer references any concrete error.
- Remove set_max_read_speed from the Unlocker trait — raising read speed is
drive tuning, NOT bus removal. libfreemkv will issue a generic SET CD SPEED
itself at drive-open (tracked).
29 tests pass.
Replace the ld/aacs/css member-crate workspace with a single freemkv-unlock
crate: the generic Unlocker contract + SCSI transport contract at the crate
root (lib.rs, scsi.rs, error.rs), and the firmware unlocker as the self-
contained src/ld module. The contract is repo-agnostic raw types (DriveId,
HostCert, DiscKind, Unlocked, UnlockError) with NO libfreemkv dependency, so
libfreemkv will depend on this crate (one-way, no cycle) and dispatch via
all_unlockers(). ld now impl crate::Unlocker, takes its own DriveId (4 raw
fields, no INQUIRY parsing), and returns a raw Option<[u8;16]> VID. The old
aacs/css plugin wrappers are removed; their crypto moves in from libfreemkv in
the next stages. 31 tests pass.
The AACS host-certificate bus-auth unlocker as a first-class external plugin,
peer to freemkv-unlock-ld and freemkv-unlock-css. AacsUnlocker impls
libfreemkv::Unlocker — matches DiscKind::Aacs, self-guards against the hardware
(refuses NotApplicable without issuing a handshake CDB if the drive reports a
non-Blu-ray profile), gathers host certs from the scan options, and runs the
libfreemkv cert-handshake primitive to learn the Volume ID + bus key. libfreemkv
keeps the handshake + AACS content decryption; this crate owns the unlocker.
Additive: a consuming binary opts in with one register_unlocker(...) line.
The CSS (DVD-Video) bus-auth unlocker as a first-class external plugin, peer
to freemkv-unlock-ld. CssUnlocker impls libfreemkv::Unlocker — matches
DiscKind::Css, self-guards against the hardware (refuses NotApplicable without
issuing a CSS CDB if the drive reports a non-DVD profile), then runs the
libfreemkv bus-auth primitive (unlock_css_reads). libfreemkv keeps the bus-auth
+ keyless descramble; this crate owns the unlocker impl. Additive: a consuming
binary opts in with one register_unlocker(CssUnlocker::new()) line; the in-tree
built-in stays until clients are rewired.
LibreDrive::matches now requires ctx.kind == DiscKind::Unknown (the drive-prep
dispatch) in addition to a bundled-profile hit. This keeps the firmware unlock
from firing during the later content-keyed (Aacs/Css) dispatch on a profiled
drive — the registry is walked at one spot per phase and matches() is the only
filter.
libfreemkv's Unlocker trait now hands matches()/unlock()/set_max_read_speed()
an UnlockCtx instead of a bare DriveId. The firmware unlocker keys off
ctx.drive_id (disc kind is irrelevant to it).
libfreemkv's Unlocker::unlock now returns Unlocked { vid, read_data_key }
rather than a bare Vid. The firmware route serves clear content, so it
reports { vid: Some(oem_vid), read_data_key: None }.