The AACS unit-alignment gate measured `lba % 3` against absolute disc LBA 0,
but aligned units are anchored at each clip's encrypted-region start. A clip
whose start_lba is not 3-aligned had its readable units wrongly rejected with
"Decryption failed" (the big-title-only failure on some Blu-rays). One
canonical clip-anchored helper (`aacs::is_unit_aligned`) is now the single
source of truth for the decrypt-on-read gate; both mux read paths set the
per-extent `unit_base = start_lba` via a new `SectorSource::set_unit_base`.
Also moves key *mechanism* into the library: the encrypted sample reader
(`read_encrypted_units`) and the candidate-key resolution loop
(`resolve_and_apply`) now live here, so a key source is purely a lookup.
Regression test covers a clip based at a non-3-aligned LBA.
Single-pass disc->MKV has no Pass N, so its read bottom-out now issues one
bounded recovery read (recovery=true, ~60s ECC) before skipping or aborting,
matching the multipass patch. Fixes a transient/marginal sector surfaced as a
read failure direct-to-MKV while multipass recovered it. One read, not a loop
(hard rule #2); recovered data is used so no bogus-status hole reopens.
- CSS: unlock scrambled-sector reads on enforcing drives via bus-auth
only; classify sense 6F/03 as CSS-locked; early-bail on a fully locked
scan; gate the AACS handshake off DVD discs.
- DVD first-play menu no longer prepended to the feature: read the title
VOBS base from vtstt_vobs (0xC4), not the menu VOBS vtsm_vobs (0xC0).
- Interlaced field-duration (DefaultDecodedFieldDuration) written as a
direct TrackEntry child rather than inside Video, so Windows reports
the correct frame rate.
- Audio channel count read from the AC-3 bitstream; FieldOrder set to
TFF; per-track BPS tags.
- Structured disc diagnostics at --log-level 3; reduced per-operation
log spam.
Interlacing is detected upstream (PAL DVD -> R576i) but was dropped in
two places: the video label hardcoded a 'p' suffix, and the muxer never
wrote any scan-type flag, so MediaInfo inferred progressive and reported
576p for a 576i source.
- Add Resolution::is_interlaced() for the R*i variants.
- generate_video_label now branches i/p for the heights that can be
interlaced (1080, 576, 480) instead of always emitting 'p'.
- MkvTrack carries interlaced + field_order; the video serializer emits
FlagInterlaced (0x9A; 1=interlaced, 2=progressive) and, for interlaced
content, FieldOrder (0x9D) - bottom-field-first for PAL 576i,
top-field-first otherwise. Adds the EBML constants.
The VTS_AST_ATR byte 1 stores (channels - 1) in the low 3 bits, but we
were reading the high nibble. A normal 2.0 AC-3 track has 0x1 in the low
nibble and 0x0 in the high nibble, so we computed 0+1=1 and mislabeled
every stereo track as mono in the muxed Channels element.
Read the low 3 bits instead, and correct the two audio_attr tests that
had encoded (channels - 1) in the high nibble to match the buggy reader.
Drop rx and recycle_tx (now Option fields) before joining the producer
thread in BytePrefetcher::Drop. Without this, a non-EOF source fills the
depth-2 forward channel then spins in send_timeout(POLL_INTERVAL) forever
because rx is never drained; join() deadlocks. Adds a regression test
(drop_endless_prefetcher_joins_cleanly) that directly exercises the path.
When a keyframe AU carried an unchanged seq_header (stripped) but a
redefined entry_point (appended), the old append-then-reassert path
produced [entry_point, seq_header] — entry_point before seq_header,
violating SMPTE 421M which requires seq+entry before every RAP.
Replace the single shared prefix Vec + reassert_active() with per-type
temporaries (redefined_seq / redefined_ep) collected during the scan,
then assembled in canonical seq-then-entry order at keyframe time.
Non-keyframes still emit only genuine redefinitions, also seq-before-ep.
Removes the now-unused reassert_active() helper. Adds a regression test
covering the seq-unchanged / entry-redefined trigger case.
Comments at lines ~63 and ~102 misidentified 0x01 (first-item/seamless) as the
non-seamless trigger and labelled 0x05/0x06 as seamless — inverted vs the BD-ROM
spec and mpls.rs (which documents 1=seamless, 5/6=non-seamless). Corrected all
affected doc blocks; no logic change.
The comment at line 404 claimed "every non-Finished range" but the
immediately-following ranges_with call lists only NonTrimmed,
NonScraped, and Unreadable — deliberately omitting NonTried.
Update the comment to accurately reflect the actual status list and
explain that NonTried is excluded because it is handled by a preceding
sweep pass, not by patch.
- keydb.rs: separate default_path()/no_home_dir() doc blocks; correct the
false XDG lock-step claim (Linux write path uses $HOME, ignores
XDG_CONFIG_HOME; read-side search also checks XDG_CONFIG_HOME).
- io/pipeline.rs: use Release/Acquire on the abandoned flag so a leaked
consumer reliably skips close() on weak memory models (ARM64/POWER),
not just x86 TSO.
- mux/disc.rs: cache the decrypt-loss Arc at construction; lost_bytes()
no longer clones an Arc per frame on the mux hot path.
- disc/dvd.rs: assert display_aspect mapping for both 16:9 (PAL test) and
4:3 (NTSC test).
- mux/resolve.rs: extract css_error_aborts() helper and unit-test the
scrambled-but-uncracked CSS guard (Fix 6) incl. the --raw exemption.
- aacs/keys.rs: add unit tests for mkb_type_raw/mkb_type/mkb_is_uhd and
MkbType (Category C 2.0 UHD, prerecorded 1.0, no-0x10-record None).
- release.yml: publish job needs [verify, test] so a failing test suite
blocks crates.io publication.
Fix three DVD video-attribute bugs surfaced by a PAL disc detected as
NTSC:
- PAL/NTSC: parse video_format from VTS_V_ATR bits 5-4, not bits 1-0
(the old mask read permitted_df, so PAL 576i/25fps was mis-detected
as NTSC 480i/29.97). Named consts replace the magic bit positions.
- Anamorphic aspect: write MKV DisplayWidth/Height from the disc's
display_aspect (16:9 720x576 -> 1024x576) instead of square pixels,
so 16:9 DVDs no longer render as 4:3.
- Colour: stamp SD colorimetry (PAL=BT.470BG, NTSC=SMPTE-170M) instead
of BT.709 (HD).
Adds VideoStream.display_aspect (threaded through every muxer) plus
TvSystem/DvdAspect/ColorSpace plumbing, with regression tests. Removes
the deprecated Disc mux set_halt bridge (use with_halt).
Add an io::fsync module with a per-OS split (posix/windows) mirroring the
writeback_file convention, replacing two duplicated dir-fsync copies:
- dir(): POSIX directory fsync; a no-op on Windows, where std cannot open
a directory as a File and the failed open logged a spurious warning on
every mapfile write.
- file_durable(): opens the target read+write before sync_all so the flush
succeeds on Windows, where FlushFileBuffers rejects a read-only handle
with ERROR_ACCESS_DENIED.
Point the mapfile writer at the shared dir() helper.
Audited every #[repr(C)] struct and IOCTL/flag constant in scsi/windows.rs
against the authoritative Windows SDK headers (ntddscsi.h, winioctl.h,
devioctl.h, winnt.h, fileapi.h). All correct except the already-reverted
ScsiPassThroughDirect packing. Add the missing regression guards:
- StoragePropertyQuery layout (STORAGE_PROPERTY_QUERY: 0/4/8, size 12).
- IOCTL/flag constants, with IOCTLs asserted against an independent CTL_CODE
re-derivation (not a tautological literal) so a mistyped code is caught.
Validated compiling via cargo xwin check --target x86_64-pc-windows-msvc.
rc.4 added #[repr(C, packed(4))] to ScsiPassThroughDirect on the false premise
that ntddscsi.h wraps SCSI_PASS_THROUGH_DIRECT in #pragma pack(push, 4). It does
NOT — verified against the Windows SDK ntddscsi.h: the struct has no pragma pack
and uses natural alignment. On 64-bit Windows (LLP64) that puts DataBuffer at
offset 24 and the struct at 56 bytes, which bare #[repr(C)] produces and which
DeviceIoControl expects.
packed(4) instead imposed offset 20 / 48 bytes — the layout of the SDK's
SEPARATE 32-bit thunk struct SCSI_PASS_THROUGH_DIRECT32 (VOID* POINTER_32). Using
that on a 64-bit host malformed every IOCTL_SCSI_PASS_THROUGH_DIRECT, so the
INQUIRY in drive enumeration failed and autorip/CLI reported zero drives
('RC4 no longer detects my drive'). rc.3.1 (bare repr(C)) worked for the same
users; this restores that layout.
Replace the tautological packed-layout test (which asserted the same wrong
offsets the struct produced) with one cross-checked against the SDK header:
DataBuffer@24, SenseInfoOffset@32, Cdb@36, size 56. Verified compiling via
cargo xwin check for x86_64-pc-windows-msvc.
Expose the MKB Type field (record 0x10) as a typed MkbType enum with mkb_type()
/ mkb_type_raw() / mkb_is_uhd() helpers, so callers can distinguish AACS 1.0
(Blu-ray) from AACS 2.0/2.1 (UHD) discs without poking raw bytes.
When a scrambled AACS unit fails to decrypt under every available key
(a missing/wrong CPS sub-key, or a marginal unit that fails the TS-sync
verify), decrypt_sectors restored the original encrypted bytes and
returned Ok with no signal. Those still-encrypted bytes flowed to the TS
assembler, which silently dropped the non-syncing packets with no loss
counter. The only loss accounting was DiscStream's read-error zero-fill
path, so mux reported lost_video_secs=0 for decrypt-dropped content and
the abort gate accepted the rip even under abort_on_lost_secs=0. A rip
missing real video/audio segments was published as a perfect success.
decrypt_sectors now returns the number of bytes in scrambled units that
no key could decrypt. DecryptingSectorSource accumulates that into a
shared counter exposed via decrypt_loss(); both mux pipelines fold it
into lost_bytes() — the inline DiscStream path directly, and the
file-backed highway via PipelinedPesStream sharing the producer's
counter. Restore-to-original is unchanged, so clear nav-files are never
corrupted; metadata-probe callers that don't read the counter are
unaffected. Adds regression tests at the decrypt and decorator layers.
The doc claimed Error::KeydbInvalid for non-UTF-8 input, but the code
returns Error::KeydbParse (KeydbInvalid is reserved for the size-cap
violation). Correct the doc to match behavior and add a regression test
asserting non-UTF-8 yields KeydbParse.
In http_get, when the server closes the TCP connection before the HTTP
header block completes (n == 0 on the byte-by-byte header read), or sends
a header block exceeding 64 KiB, the code returned KeydbParse (E8004).
Both are connection/protocol-level faults from the server, not parse
failures of keydb content — the keydb bytes were never received. Return
KeydbConnect (E8000) instead, which already covers TCP-level exchange
failures. A CLI user hitting a transient drop or a redirect target that
immediately closes now sees the correct 'server hung up' diagnostic
rather than 'the downloaded file was malformed'.
Add a regression test that stands up a loopback listener which accepts
then drops the connection before headers, asserting KeydbConnect.
ntddscsi.h wraps SCSI_PASS_THROUGH_DIRECT in #pragma pack(push, 4),
forcing the PVOID DataBuffer field to 4-byte alignment even on 64-bit
hosts. The Rust struct used bare #[repr(C)], so the compiler applied
natural 8-byte pointer alignment and inserted 4 padding bytes after
TimeOutValue. That shifted DataBuffer to offset 24 (SDK: 20),
SenseInfoOffset to 32 (28), and Cdb to 36 (32), and grew the struct to
56 bytes (48). DeviceIoControl reads at the SDK offsets, so every SPTI
ioctl on 64-bit Windows either got rejected or interpreted garbage as
the CDB and DataBuffer pointer.
Add #[repr(C, packed(4))] to ScsiPassThroughDirect and the companion
SptwbDirect (so offset_of!(SptwbDirect, sense) stays correct for
SenseInfoOffset), plus a layout regression test asserting DataBuffer at
offset 20 and a 48-byte struct size.
The prefetch producer thread fires a BytesRead event after every
batch it reads, carrying a cumulative byte count. Nothing asserted
this callback actually fired, so a consumer that passed None for the
event_fn would silently get no progress events. Add a fixture-based
regression test that drives a finite extent through new_with_events,
captures the events, and asserts the cumulative count is
non-decreasing and reaches the full extent size at EOF.
This locks the contract autorip's mux progress bar and soft-stall
watchdog depend on.
A SEND KEY / REPORT KEY step in the bus-auth handshake mapped every
SCSI error to a cert/key-specific code (AacsCertRejected, AacsCertRead,
AacsKeyRead, AacsKeyRejected, etc.) via map_err(|_| ...). That discarded
the underlying SCSI error, so a transport-layer wedge (bridge crash / USB
disconnect) mid-handshake was reported as 'drive rejected your host cert',
sending operators down a keydb/host-cert dead end for what is really a
replug/power-cycle situation.
Add a handshake_err() helper that keeps the original error when it is a
transport failure (is_scsi_transport_failure) and only substitutes the
handshake-specific code for genuine SCSI rejections. Apply it at every
SEND KEY / REPORT KEY / REPORT DISC STRUCTURE step in both the AACS 1.0
and AACS 2.0 paths. Add a regression test covering both branches.
read_udf treated a READ CAPACITY SCSI failure as a 0-sector disc via
unwrap_or(0) with no diagnostic. capacity=0 then skews the layer
heuristic (always reports 1 layer, even for dual-layer discs) and the
canonical title-ordering sort, with nothing in /api/state or info to
indicate the command actually failed. Emit a tracing::warn carrying the
original error at the fallback site so a transient capacity failure is
visible. Recovery behavior is unchanged: 0 is still used as the
fallback.
default_path returned Error::KeydbParse (E8004, rendered as 'failed to
parse the keydb file') when HOME/USERPROFILE was unset. That misreports
an environment failure — a process with no home directory, typically a
stripped container or CI config — as a corrupt keydb file the code never
read. Map it to an IoError(NotFound) in the I/O category instead, so no
display path blames the keydb. Add a regression test.