When the drive is in extended-access state (unlocked), retrieve VID via
the per-drive `read_vid_cdb` from the bundled profile instead of the
cert-based AACS REPORT_KEY handshake. Cert handshake remains the
fallback for drives that don't enter extended-access state, or whose
profile lacks the required CDB.
Empirically verified on the BU40N (signature 999ec375) against
Barbie UHD: drive returns 36 bytes from buffer 0x44 at offset
0x10E291, VID at response[4..20]. The 16 bytes match Dune Part Two's
known VID in keydb.cfg byte-for-byte, cross-validating the path
against an independent oracle.
Architectural impact:
- Renames `Drive::is_libredrive_active()` → `Drive::is_unlocked()`.
Internal `Mt1959::libredrive_active` becomes `Mt1959::unlocked`;
the prior `unlocked` (init-success flag) becomes `init_complete`
to avoid the name collision.
- `disc/encrypt.rs::Disc::read_vid` is the single entry point.
When `is_unlocked()` is true, calls `read_vid_oem` (issues the
per-drive CDB, validates the response signature high-3-bytes
`00 22 00`, returns bytes [4..20]). Otherwise delegates to
`read_vid_cert` (the existing AACS REPORT_KEY format 0x80 path).
- `DriveProfile` gains the per-drive CDB templates and identifier
blocks extracted from each per-drive firmware payload — including
`read_vid_cdb`, `read_disc_keys_cdb`, `drive_nominal_speed_cdb`,
`set_speed_max_cdb`, two cache-prime canary CDBs, the buffer-0x45
verify CDB, the firmware-upload CDB, and the unlock probe CDB.
Variants A and B differ in which fields are populated. All optional;
consumers fall back to the cert/handshake path when fields are
absent.
- New error variants `Error::DriveProfileMissing` (E7020) and
`Error::VidCdbUnavailable` (E7021). Both treated as
"OEM unavailable → try cert path" by `read_vid`, not terminal.
Closes the v0.25.x gap where HRL-burned host certs (the public
libaacs leaked cert is on every recent drive's HRL) blocked all
post-handshake VID retrieval. With OEM-driven VID:
- AACS 1.0 BD on supported drives: rips end-to-end with our existing
DKs walking the MKB.
- AACS 2.x UHD: fails honestly at the DK wall (E7018 "No usable DK"
for v77+ MKBs) instead of the misleading E7017 "No Volume ID"
the prior code surfaced. We have VID; we just don't have v77+ DK
material — that gap is a key-acquisition problem, not a code
problem.
Empirically verified on rip1 (BU40N + Barbie UHD, MKB v77,
2026-05-21): error code flipped from E7017 to E7018 as predicted.
The DK wall is now correctly the proximate failure for unrippable
modern UHD discs, instead of the indirect VID-retrieval wall the
v0.25.x cert-only path produced.
Renames and comment scrubs eliminate upstream-RE-vocabulary
references in the public crate per `feedback_no_breadcrumbs.md`.
674 tests pass (565 lib + 109 integration). No tradename leaks in
any modified file.
Three coherent threads landing for v0.25.11:
1. Libredrive raw-read VID path. When Mt1959::do_unlock sees both the
MMkv active-mode marker at [12..16] and the LbDr mode-ID marker at
[16..20], Drive::is_libredrive_active() returns true and
do_handshake skips the AACS cert dance — VID is retrieved via
READ_DISC_STRUCTURE format 0x80 with AGID=0 and bus encryption is
already off. This unblocks UHD ripping on drives whose leaked host
cert is on the AACS HRL.
- platform/mt1959/mod.rs: detection + active flag + 4 unit tests.
- platform/mod.rs: PlatformDriver::is_libredrive_active trait method.
- drive/mod.rs: Drive::is_libredrive_active accessor.
- disc/encrypt.rs: do_handshake branches on the flag; new
read_volume_id_libredrive helper. Return type widened to
(Option<HandshakeResult>, Option<Error>) so callers see which
specific failure happened.
- disc/mod.rs: scan_with plumbs the new tuple through and preserves
handshake errors as disc.aacs_error.
2. Revert v0.25.9 built-in AACS keys + plugin slot. Single source of
AACS truth: keydb.cfg. The compiled-in DKs/PKs were a slim
convenience that didn't move the hard problem (no v77+ DKs) and
added a maintenance surface. Plugin slot was overlapping
functionality with the main keydb.
- Deleted src/aacs/builtin_keys.rs (4 DKs + 3 PKs).
- Removed KeyDb::with_builtins, load_or_builtins, merge_from,
merge_local_plugin, local_plugin_path, internal dedup helpers.
KeyDb::empty kept for unit-test use.
- KeyDb::load reverts to pre-0.25.9 form: read file or return I/O
error; no fallback.
- disc::encrypt::resolve_encryption keydb_path back to required
(&Path), not Option<&Path>.
- disc::scan_with surfaces KeydbLoad { path: "<no keydb in search
paths>" } sentinel when encrypted + no keydb — same sentinel
autorip's message switch already handles.
- CSS player keys in src/css/auth.rs stay compiled in; they're
1999-era public inputs separate from AACS and pre-date the 0.25.9
additions.
3. Walker fix follow-through (libaacs-parity validate_processing_key,
cvalues 0x07-then-0x05 preference, path-2/3/4 short-circuit on
zero VID) + NIST AES-CMAC KAT + VID MAC round-trip / mutation /
zero-rejection tests.
5 new Error variants for finer-grained AACS failure reporting:
AacsHostCertRejected (E7015), AacsLibredriveUnsupported (E7016),
AacsVidUnavailable (E7017), AacsMkUnavailable (E7018),
AacsVukNotInKeydb (E7019). Lets CLIs/UIs render which piece of the
AACS chain failed instead of always saying "no keys."
Fix extract_scsi_context() and Error::scsi_sense() to handle Error::DiscRead
in addition to Error::ScsiError, so is_marginal_read() works for DiscRead
errors and disc::copy() can properly route MEDIUM ERROR as a marginal
(bad sector) instead of bailing.
Closes: (internal)#20260427
bytes_pending was an opaque aggregate of NonTried + NonTrimmed +
NonScraped. UIs that wanted a "will retry in Pass 2-N" bucket were
stuck showing the entire unread disc as Maybe at pct=0.
Adds two granular fields to MapStats:
bytes_nontried — Pass 1 hasn't read these yet
bytes_retryable — NonTrimmed + NonScraped, Pass 2-N will retry
bytes_pending stays for back-compat (= bytes_nontried + bytes_retryable).
Also picks up the cargo fmt --check lint that's been red on main CI
since v0.13.18 (rustfmt fold differences on a few long format-string
layouts; functional no-op).
Through the entire 0.13.x line, every CHECK CONDITION reply from the
drive (the standard way SCSI tells you why a sector failed) was being
collapsed into a synthetic status=0xFF, sense_key=0 transport-wedge
sentinel and the actual sense data was thrown away. Confirmed live on
the BU40N reading Dune 2 on 2026-04-27: drive returned host_status=0,
driver_status=8, status=2, exec_elapsed_ms=1416 on every bad sector
— a clean CHECK CONDITION carrying full sense data — and Disc::copy
was bailing on it as if the bridge had wedged.
Root cause: scsi/linux.rs's wedge check was
`host_status != 0 || driver_status != 0`
SG's DRIVER_SENSE bit (0x08) is set on every CHECK CONDITION reply
just to flag "sense buffer is populated" — it's not a transport
failure on its own. Pre-fix we conflated the two and silently lost
every drive-reported error reason. macOS and Windows backends had
the same shape: they extracted sense_key only, dropping ASC/ASCQ.
API restructure (clean separation):
Error::ScsiError {
opcode: u8,
status: u8, // 0xFF = synthetic transport-failure
sense: Option<ScsiSense>, // None ⇔ no sense delivered
}
pub struct ScsiSense { sense_key: u8, asc: u8, ascq: u8 }
impl ScsiSense {
pub fn is_marginal(&self) -> bool // keys 0/1/3/B
pub fn is_medium_error(&self) -> bool
pub fn is_hardware_error(&self) -> bool
pub fn is_unit_attention(&self) -> bool
pub fn is_data_protect(&self) -> bool
pub fn is_not_ready(&self) -> bool
pub fn is_illegal_request(&self) -> bool
pub fn is_aborted_command(&self) -> bool
}
impl Error {
pub fn scsi_sense(&self) -> Option<&ScsiSense>
pub fn is_scsi_transport_failure(&self) -> bool
pub fn is_marginal_read(&self) -> bool
}
SCSI protocol constants (SCSI_STATUS_*, SENSE_KEY_*) moved from
error.rs to scsi/mod.rs where they belong alongside SCSI_INQUIRY,
SCSI_READ_10, etc. parse_sense replaces parse_sense_key (returns the
full triple, not just the key); inline tests now exercise ASC/ASCQ
extraction at the right offsets for both descriptor (0x72/0x73) and
fixed (0x70/0x71) sense formats.
Disc::copy + Disc::patch sense-aware dispatch:
- marginal sense (MEDIUM ERROR / ABORTED COMMAND / RECOVERED ERROR
/ NO SENSE) → engage hysteresis (Block→Single, bpt=1)
- non-marginal sense (HARDWARE / DATA PROTECT / UNIT ATTENTION /
NOT READY / ILLEGAL REQUEST / transport failure / kernel
IoError) → bail with full sense info preserved; caller (autorip)
surfaces "physical replug" / "drive failing" / "media changed"
Pre-fix: every CHECK CONDITION → 0xFF synthetic → Disc::copy bailed
→ bytes_good froze at the bad zone. The hysteresis from v0.13.22
was correct but never got to run. This release unblocks it.
Disc::patch's wedged_threshold (50 consecutive failures) stays as
defense-in-depth for chains of marginal failures; a single
non-marginal sense now short-circuits it.
New phase=bail trace event records the bail reason with the sense
triple. phase=transport_err remains for genuine bridge wedges /
kernel timeouts; phase=scsi_err carries the parsed sense_key, asc,
ascq for drive-reported errors.
All 350 tests pass. Clippy clean across all targets.
Audit pass against the project docs "no English text in library code" rule.
Found 9 call sites that violated the contract by stuffing English into
io::Error::new(kind, "…") or by abusing Error::DeviceNotFound { path }
as a free-form description field. Each is now a typed Error variant.
New variants and codes: ScsiInterfaceUnavailable (E1004), DeviceLocked
(E1005), IoKitPluginFailed (E1006), UnsupportedPlatform (E2003),
PlatformNotImplemented (E2004), MapfileInvalid (E6011), DiscUrlNotDirect
(E9009).
labels::apply() previously pushed Commentary/Descriptive/Score/IME and
" (Secondary)" English literals into AudioStream.label, leaking into
MKV titles + autorip UI. AudioStream now exposes structured `purpose:
LabelPurpose`, SubtitleStream `qualifier: LabelQualifier`. Callers
translate to localized text. label keeps codec-formatting only.
API hygiene: 11 mux/* modules dropped from `pub` to `pub(crate)` —
their *types* are still re-exported from lib.rs, but the modules were
leaking low-level EBML/TS/network primitives. Stream trait gets a real
rustdoc explaining read-vs-write split. lib.rs grouped re-exports into
documented sections. ScanOptions::with_keydb() removed (one-method-per-
action rule); use struct literal.
Dead-code sweep: removed lookahead.rs (orphan, never declared as mod),
tsreader.rs (TsDemuxReader unused), ebml::{write_int,read_vint,SEEK_*},
ts::{scan_first/last_pts,scan_duration,SCAN_HEAD/TAIL_SIZE,take/set_
remainder}, MkvMuxer codec_private_slots/filled fields and
fill_codec_private method (deferred-codecPrivate path never used since
the v0.10 PES rewrite). cargo clippy --all-targets -D warnings clean.
Tests: new error::tests for variant codes + Display "no English" guard +
io::ErrorKind mapping. 233 lib tests, all green (was 230).
Breaking: ScanOptions::with_keydb removed; mux/* modules pub(crate);
AudioStream and SubtitleStream gained required fields; UnsupportedDrive
{ product_revision: "Renesas not yet implemented" } no longer produced
(use PlatformNotImplemented).
Architecture:
- One stream per format, bidirectional PES (read/write on same type)
- IsoStream merged into DiscStream (one type, any SectorReader)
- Disc::copy() for disc→ISO raw sector dump
- IOStream trait deleted, all byte-level Read/Write removed
- ContentReader/OpenDisc/open_title/open_input/open_output deleted
- CountingStream wrapper for progress tracking
Error codes:
- All io::Error English strings replaced with Error enum variants
- From<Error> for io::Error conversion
- Unused variants removed, new stream/mux variants added
Deleted: mkvout.rs, pesout.rs, isowriter.rs, mkv-muxer-plan.md
Updated: all docs, README stream table, CHANGELOG
238 tests, 0 clippy warnings.
Previously used all-zeros AES key when unit_key_idx was out of range,
producing silently corrupted output. Now returns DecryptFailed error.
Also wired --raw flag through InputOptions → set_raw() on streams.
- Split AacsError { detail } into 13 specific error variants (AacsCertShort,
AacsAgidAlloc, AacsCertRejected, etc.) with unique error codes E7001-E7012
- Split DiscError { detail } into 7 specific variants (DiscRead, MplsParse,
ClpiParse, UdfNotFound, DiscNoTitles, DiscTitleRange, DiscNoExtents)
- Add WriteError (E5001), KeydbLoad (E8005), MuxLookahead (E9000), MuxWrite (E9001)
- Add OpenDisc API for single-call open+scan+rip workflow
- Remove all English text from error Display impl (code-only output)
- Normalize doc comments to use -- instead of em dash for ASCII consistency
- Error codes as public constants (E_DEVICE_NOT_FOUND etc.) — single source of truth
- SCSI opcodes as named constants (SCSI_READ_10, SCSI_REPORT_KEY, etc.)
- AACS key class constant, MKB format constant
- No magic numbers in CDB construction
- docs/disc-to-rip.md — end-to-end flow from disc insert to decrypted content
- Links all module docs together as a starting point
Features:
- Open drive identification via SPC-4 INQUIRY + MMC-6 GET CONFIGURATION
- 141 supported drives with bundled profiles
- MT1959 platform: unlock, calibrate, raw sector reads
- DriveSpeed enum: BD1x-BD12x, DVD1x-DVD16x
- Field names follow SPC-4 §6.4.2 and MMC-6 §5.3.10 standards
- No proprietary fingerprints — open matching by SCSI fields
- Zero config: profiles compiled into binary
Tested on real hardware: HL-DT-ST BD-RE BU40N 1.03