Two audit fixes plus two consistency cleanups, all in the changed 1.6.5 surface:
- hddvd: the VTI clip-order read used `.ok()`, flattening an unreadable
authored order (a scratched sector under the .vti — the name came from the
directory, so it is never "absent") into "no order" with no diagnostic. The
sibling clip-extent arms log every read failure with its code; this one now
does too, then falls back to the per-clip heuristic exactly as an unauthored
disc would. Behaviour is otherwise unchanged; the loud line is the point.
- extract: the progress sink hardcoded bytes_unreadable_total: 0 and counted
every zero-filled hole as good, so a progress-only consumer saw a holed
extraction climb to a clean 100%. Thread the running unreadable total through
and report the real good/unreadable split. The authoritative ExtractResult was
already truthful; only the live channel lied.
- labels: correct a stale comment that described testlog's old lock-based
capture; it now installs one global subscriber and routes to a thread-local
sink.
- ps: import SYSTEM_HEADER from consts instead of re-declaring 0xBB, matching
its sibling stream-id constants and the file's single-source rule.
Ten lenses over v1.6.4..HEAD, every claim read against the code before
it was believed. Seven confirmed; six are here, one is recorded for the
next round. All of these are the same family — a failure wearing the
shape of success — which is the family that once shipped 9 MB of
ciphertext inside a main-movie m2ts at rc=0.
A clip whose extents cannot be resolved is now accounted for, in both
disc readers. Only `UdfUnrecordedExtent` used to count: every other way
`file_extents` can fail — a scratched sector under the clip's ICB
(DiscRead), an allocation-descriptor chain that never terminated, a file
whose data is embedded rather than extent-mapped — fell through to the
ordinary "file absent" path. On Blu-ray that yielded a title advertising
its full runtime with a clip's bytes silently missing, because the size
and the play-item timing had already counted it. On HD-DVD it was worse:
the clip was never added to `unusable`, so a split feature still composed
from FEATURE_1 alone and offered half a movie as the whole thing. Neither
emitted a single log line. Absence is still benign — a 2D disc has no
.ssif and the extension fallback exists for exactly that.
`Halted` is excluded deliberately, and that exclusion is the whole reason
the first version of this fix was wrong. Cancellation makes EVERY drive
command return `Halted`; classifying it as a disc defect would have
dropped each remaining playlist in turn and handed back a truncated title
list at success — the same defect, wearing a cancel. `parse_playlist`
returns Option and has no channel to propagate a halt, so the existing
behaviour is preserved rather than made worse. Propagating it properly is
next round's work.
Both log sites now emit the error's OWN code instead of a hardcoded 6017.
Accounting a scratched disc (E6000) as an authoring hole would send
anyone triaging it looking for the wrong thing entirely.
AD type 3 is embedded data, not a descriptor list (ECMA-167 4/14.6.8).
`read_icb_extents` lumped it in with the reserved values and decoded the
file's own CONTENT as (length, LBA) pairs, manufacturing extents out of
arbitrary bytes and pointing the reader at unrelated sectors. This same
release already taught `read_directory` to honour type 3; this is the
file half of that decision. It is an error rather than an empty list,
because an empty list reaches the caller as a clip that contributed
nothing while its declared duration still counts it — the silent loss
pointed the other way. A legally zero-length embedded file still returns
an empty list. New code E6018: reusing DiscRead would have mislabelled a
deterministic structural property as transient I/O and fed the retry and
NonTrimmed machinery a byte that will never change.
`file_extents_addressing`, `extents_abs_at` and `AbsExtent` drop to
`pub(crate)`. The first hands back unrecorded extents UNFLAGGED, in a
shape identical to the safe call's return; its doc says callers must use
`file_extents` instead, but a doc comment is not a guard. No dependent
crate references any of the three.
Three tests close gaps the audit found, each proven red before green:
a held AC-3 access unit must not resume as a normal frame after its track
poisons; the PS resume cursor must survive a drain that rebases it (three
separate mutants caught); and AD type 3 must be refused rather than
decoded. The first attempt at the HD-DVD test passed with the fix
reverted, which made it worthless — it needed a VTI fixture before the
composition path ran at all.
Also: four error codes were missing from the uniqueness test that claims
to cover every published code, so a new variant reusing 6014, 6016 or
6017 would have passed it.
Squashed from 12 commits. Every fix was proven red-before-green and killed by a
mutation; the reasoning for each is in the private audit record.
UDF and extents
Honour ICB types rather than assuming a Short AD, so an AD-type-3 directory
is no longer decoded from FID bytes into a silently empty listing. Carry the
ECMA-167 recorded flag through to the resolvers: an allocated-but-never-
written extent used to reach the read plan as ordinary content and splice
undefined sectors into the rip. file_extents now refuses such a file, and
only when the hole actually occupies byte space — a zero-length one displaces
nothing, and refusing on it dropped whole titles off discs that ripped
correctly. Type-2 sparse extents are kept alongside type-1; they were falling
into a catch-all that exited the descriptor loop and returned a truncated
list as complete. merge_ranges no longer claims a sector neither input
covered. A short skip or an over-long AD chain errors instead of truncating.
HD-DVD and Blu-ray scanning
Bound the XPL nesting depth, title count, clips and chapters per title, and
memoize the clip-name fallback probe — four separate amplification axes, each
of which alone left the worst case unbounded. The clip and title caps are 512,
~10x any retail disc, and a test pins the product of cap and probe budget.
The scan is cancellable: it returned Ok with titles carrying no streams when
halted, presenting a cancelled scan as a successful one. A clip dropped for an
unrecorded extent now says so.
Codecs and muxing
Resume a held E-AC-3 access unit rather than rescanning from its first frame,
and drop it on a discontinuity — a stale hold indexed past the end of the new
buffer. Map every ISO 639-1 code instead of collapsing fifteen languages to
und. Correct the DVD palette order. Detect a skip past EOF.
Drive and I/O
Classify dead-bus faults so the wedged-drive path can see them; a catch-all
arm had been flattening the variants before the classifier ran. A prefetch
producer that dies now reports SourceTerminated instead of Ok(0), which the
reader legitimately read as a short read and zero-filled — a whole title
could be fabricated and the pass reported complete.
Also: charge Ok(0) reads to the CSS crack budget, drop the unreachable soft
re-crack, and send disc-derived strings to logs through the debug formatter so
a crafted label cannot paint an operator's terminal.
The remaining triage items after tonight's HIGH fixes: 1,290 lines, almost
all tests. Covers disc/mod.rs's DVD scan path (with real minimal VMG/VTS IFO
fixtures rather than mocks), drive/mod.rs, labels/class_reader.rs and
labels/mod.rs — the two biggest untriaged survivor clusters in the crate —
plus hevc.rs and ps.rs.
One production change, and it is an extraction rather than a behaviour
change: MacScsiTransport::open mapped the shim's negative failure sentinels
to typed errors inline, where nothing could reach it without a real IOKit
FFI call. It is now map_shim_open_error, so the mapping can be pinned. It
matters because collapsing -5 into the DeviceNotFound catch-all turns
"another process holds the drive" into "no such drive", and an operator
chasing the wrong problem is worse than a blunt error.
Gate green on the pinned toolchain including the secrets scanner.
A 12,330-mutant run left 159 survivors across these three files, all from
missing assertions rather than wrong code — every gap here is a test, no
production logic changed.
Two shapes accounted for most of them:
- Buffer-cap constants (MAX_PES_BUFFER_TOTAL, MAX_PS_BUFFER,
MAX_BD_PES_PAYLOAD, PES_BUFFER_INIT_CAP) were only ever read by tests
through their own symbol, so a mutated `*`/`-` in the constant's
definition changes what the symbol itself evaluates to and every
self-referential assertion still passes. Pinned each against a literal
computed independently in the test.
- Several `>`/`==` boundary checks on framing lengths (MPEG-2 pack header,
system header, BD-TS adaptation field) were only ever exercised with
slack in the buffer, never at the exact byte the check exists for.
Added exact-fit cases for the pack header, system header, and
psi_payload_base's AF-consumes-everything boundary.
Real, higher-value gaps closed along the way:
- ts.rs's per-PID discontinuity_flag and the NULL-TS concealment marker
both require adaptation_field_length > 0 before trusting the AF flags
byte; neither branch had a test proving af_len == 0 (no flags byte at
all, ordinary payload underneath) is left alone.
- header_remaining (PES header spillover across TS packets) only had
single-continuation-packet coverage, which can't distinguish `-=` from
`+=`/`*=` because the corrupted value never gets read again. Added a
case spanning two continuations.
- ps.rs's parse_stream_id_extension (used for HD-DVD 0xFD routing) walks
nine optional PES-header/extension fields with a `pos +=` each; only
the PTS/DTS pair had ever been exercised. One test now arms every
field and checks the walk lands on the right byte.
- find_ps_boundary's `sc + 3 >= len` guard had no test at sc == 0 with a
bare 3-byte start code, the case a `+` -> `-` mutation turns into a
debug-mode subtract-overflow panic on ordinary tail-of-buffer input.
- tsmux.rs: an oversized video access unit must go out as a single
unbounded-length PES; the `is_video || small-enough` guard that
enforces this had no test with a video frame actually over the
bounded-PES threshold, so a `||` -> `&&` mutant survived (it would
silently split a keyframe across several look-alike-independent PES
units). Also pinned the PES-length and PTS big-endian encodes at
values above 255 / with bit 29+ set, where a `>>`/`<<` swap first
becomes observable.
Every test above was verified by hand: applied the exact mutation,
confirmed the test fails (or the specific panic fires), then reverted.
Left unclosed, all confirmed equivalent by hand-tracing rather than
just left alone:
- Every `<<8 | byte` PID/length bit-combine (ts.rs pid/PAT/PMT parsing,
ps.rs dvd_audio_pid/hddvd_extended_pid/parse_pts): the two halves
never share a bit, so `|` and `^` produce identical output for every
input - no test can tell them apart.
- ts.rs's `af_len > 183` check in process_packet: fully subsumed by the
`payload_start >= TS_PACKET_BYTES` check three lines later for every
af_len that could trip it.
- ts.rs's out-of-range `pid_index` sentinel (-1 vs 1): unreachable, since
a TS PID is masked to 13 bits (max 8191) and the table is always sized
to at least 8192.
- A cluster of "push an empty slice on an exact boundary" mutants in
tsmux.rs's write_pes_chain (offset < hdr_len, af_bytes stuffing
guards): the guarded write becomes a length-0 write_all, a no-op
either way.
Not reached this pass, for lack of a clean seam within the time
available - ps.rs's extract_packets bounded-PES-length exact-fit
checks (lines 278/282/303, the `sc+6>len` / `sc+6+pes_len>len` /
force-flush cap arithmetic). The first two need a scenario where
"proceed vs. wait one more byte" is observable in the packet list, and
the third only shows up at a start-code offset (sc) that survives to
the moment the cap check runs - in this code path sc is always 0 once
an unbounded PES buffer starts accumulating, since nothing before it
ever drains. Didn't find a construction in the time available; flagged
rather than papered over with a self-referential assert.
Mutation testing over src/mux/. No production change — 49 survivors
killed, all proven red before green.
The MP4 composition-time chain was entirely unconstrained: VideoTiming::ctts,
build_ctts and parse_ctts could each return a constant and the suite
stayed green. Confirmed on HEAD: build_ctts -> vec![] passes all 1,220
mux tests. A demuxed B-frame title presenting in decode order would
have shipped.
The cause is a test whose name asserts coverage its body does not
deliver — stts_and_ctts_expand builds an stts box and never touches
ctts, and write_then_read_round_trip asserts sample sizes and keyframe
flags but not one PTS. Same shape as the set_speed forwarding finding,
different disguise.
mlp_num_substreams / mlp_substr_header_size: every TrueHD fixture in
the crate uses one substream and no extraword, so both could return a
constant and agree with all of them. These position mlp_parity_ok's
window over the AU header, so a constant mis-windows the parity check
on exactly the multi-substream AUs that carry 7.1 and Atmos.
CodecPrivate absent vs empty: mkv.rs writes Some(bytes) verbatim and
omits the element on None (RFC 9559 5.1.4.1.24), so a zero-length Some
emits a track header asserting the config IS empty. Four parsers could
return Some(vec![]) before any frame.
Also: mandatory ISO/IEC 14496-12 boxes (tkhd, vmhd, smhd, dinf, mdhd)
could each build empty; HEVC num_extra_slice_header_bits (H.265 7.3.2.3)
was never non-zero in any fixture, so the slice-type offset skip was
unexercised; chapter names from the disc go straight into
<ChapterString> and the & escape must run first; a stray 0x47 in a
payload must not latch a TS resync.
Documented as equivalent rather than killed: CodecParser::flush and the
three parser flush bodies that differ from the mutant only by a tracing
call, and DropTally::log_summary.
The Program Stream demuxer appended every fed byte and enforced its 4 MiB
cap only inside a branch reached once a start code had been found. Input
containing no start code anywhere therefore hit no cap at all, and since a
whole title is fed through this demuxer, a zero-filled or ciphertext VOB
extent buffered the entire title — up to ~90 GB. When the buffer holds no
start code, only a two-byte `00 00` prefix can begin a PS unit on the next
feed, so that is kept and the rest dropped. The bound is exact rather than
a heuristic: a start code can straddle a feed boundary by at most its
first two bytes, so no real byte is discarded, and a test feeding
`FF FF 00 00` then `01 E0 ...` pins that.
The existing test named for this case fed a real start code first, so the
cap it exercised was the in-PES one. Renamed to say what it covers.
The BD-J label path had a different shape to anything found so far: the
cap is on the COMPRESSED size of a disc file while the allocation scales
with the decompressed size. A `.class` gated only by a path prefix
inflates to the 64 MiB ceiling, yielding ~33M retained strings from `ldc`
operands or ~67M pushes onto a symbolic stack whose depth was unbounded
despite the Code attribute's own `max_stack` being parsed and then
ignored. Bounded both, the stack by `max_stack` itself (JVMS §4.7.3).
The VMG TT_SRPT title count is an untrusted u16 with no de-duplication,
so ~800 KB of crafted IFO re-parsed one PGC 65535 times. Capped at 99, the
DVD-Video maximum, so no conformant disc is clipped.
Every cap carries stated headroom against real media, and each has a test
locking that real media still passes.
I rewrote three of the new assertions before landing them. They compared
the result against the very constant under test — `total <= MAX_TT_SRPT_TITLES`
— which passes vacuously the moment someone raises the constant, the most
likely future regression and the seventh instance of this tautology shape
in this audit. They now assert literals derived from the spec.
The TT_SRPT fixture also had to change: with 65535 identical entries the
de-duplication collapsed them on its own and the cap was never what
bounded the result, so the test passed with the cap removed entirely.
Distinct entries defeat dedup and leave the cap as the only guard;
de-duplication now has its own fixture. Verified by raising each of the
three constants and confirming all three tests fail.
Round-8 findings from the 10-phase release audit:
- detect_disc_format's BDMV fallback passed detect_format's result
through unchanged, so an SD bonus/menu title could tag a BD-tree disc as
DVD (mis-sizing the ECC sweep) — violating its own "never below Blu-ray"
invariant. Clamp anything but UHD up to Blu-ray.
- Track the MPEG-2 GOP byte total incrementally instead of re-summing the
whole gop_buf on every pushed picture (was O(pictures²) on any MPEG-2
disc, not just adversarial input).
- Back the DTS pts_marks deque with a VecDeque so the over-cap prune is an
O(1) pop_front, not an O(n) Vec::remove(0).
- Add a test exercising parse_stream_id_extension's PTS/DTS skip branches
(the real AU-opening 0xFD video PES path) — previously untested.
VC-1 HD-DVDs (e.g. Shaun of the Dead) carry video on MPEG-PS extended
stream id 0xFD, with the real stream selector in stream_id_extension
inside the PES extension. Parse that field so the video routes to a
distinct track (pid 0xFD00|ext) instead of being dropped.
Reframe VC-1 access units in AuAssembler with a dedicated Mode::Vc1:
an AU is delimited by the next frame BDU (0x0D) once a frame has already
been seen, so the sequence (0x0F) and entry-point (0x0E) headers that
precede an I-frame stay attached to the frame they describe. The old
single-start-code split stranded those headers on the prior AU, which
the decoder reported as bits-overconsumption and hard decode failures.
hddvd probe now tracks the video pid it detects and emits VC-1 on 0xFD.
Carry per-picture truth and byte-exact source provenance THROUGH the stream so
the muxer (and the upcoming video index) read MEASURED facts instead of
assuming them. Honest data in, honest data out.
- codec/coding.rs: codec-agnostic PictureInfo (CodingType / FieldOrder + the
accessors field_order/coding_type/nb_fields/progressive/keyframe). Each codec
folds its raw signals in; consumers use only accessors, never branch on codec.
- mpeg2: builds PictureInfo from the picture coding extension and carries it +
SourcePos (source_marks, parallel to pts_marks) on every emitted frame.
- pes / codec::Frame: additive `coding` + `source`, forwarded through the
highway; None for audio/subtitle and the network/stdio deserialize hop.
- mkvstream: DEFER muxer construction until the first coded picture, set the
video track's FieldOrder from the MEASURED value, THEN write the header —
right the first time, no guess, no seek-back. An interlaced track that arrives
with no measured order is LOGGED loudly and left UNDETERMINED, never faked.
- mkv: MkvTrack::video no longer guesses TFF (a bitstream property the scan
cannot know is UNDETERMINED at build). Removed VideoStream::top_field_first
(the dead scan-time guess) crate-wide.
- Tests: parser population (every PictureInfo facet + per-PES source carry) and
mux-stream consumption (measured -> correct; missing -> UNDETERMINED, not
faked). Two obsolete tests updated only after confirming (their own comments)
they existed to enforce the deleted hardcoded-TFF.
- 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.
Test-hardening release, no runtime changes. Adds spec-grounded unit tests
across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD
title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers,
MKV/EBML container output, the mux pipeline, sector prefetch + decrypt
decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each
test is grounded in the format spec or real on-disc behavior and verified to
fail under a targeted source mutation. No behavior changed.
Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant
handling and trailing-partial-unit policy, corrected MPLS mark offset and
added UDF allocation bounds, hardened the mux/codec framing and M2TS paths,
guarded SCSI READ CAPACITY short transfers and unified error mapping, added
overflow guards on untrusted disc input, and made prefetch shutdown
deterministic. Release profile now builds with thin LTO + single codegen unit.
Subtitle/DVD output-corruption + stream-mapping coverage fixes.
1. DVD subtitle/audio track-mapping collision (CRITICAL). The PS path
routed 0xBD private-stream packets to a track via (sub_id & 0x1F)+1,
so VobSub subtitle sub-id 0x20+j aliased audio track j+1: subtitle
PES was fed to the AC-3 parser and the real subtitle track got
nothing. Route by the canonical DVD PID instead via a new
PsPacket::dvd_pid() that mirrors scan_dvd_titles' PID assignment
(video 0xE0, audio 0xBD00+i, subtitle 0x20+j), then look up the
track in pid_to_track. Fixed identically at all three sites
(pipelined_stream consume_ps, disc.rs live feed, disc.rs EOF flush).
Unmappable/unmapped packets now WARN instead of silently dropping.
2. PGS flush() missing. PgsParser inherited the no-op default flush, so
the last subtitle of every PGS track (emitted only when a following
PCS arrives) was dropped at EOF. Implemented flush() to drain the
pending display set (duration_ns: None for the trailing block).
3. DVD VobSub multi-PES SPU not reassembled. A subpicture unit larger
than one PES spans multiple PES (only the head carries a PTS).
DvdSubParser is now stateful: it buffers per sub-stream until the
leading 2-byte SPU_size is satisfied, inherits the head PTS, and
emits one Frame. flush() drains a truncated trailing SPU at EOF.
4. One-table hygiene. scan_streams had a duplicate stream_type->Codec
table that had drifted from Codec::from_coding_type (missing 0x80
LPCM, 0x85 mapped to DTS-HD MA vs HR, etc.). scan_streams now uses
from_coding_type plus a new Codec::kind()/CodecKind category split,
so the two mappings can never diverge. Silent drops in
scan_streams and bluray STN parsing now WARN with PID + type.
Tests: dvd_pid mapping + subtitle/audio collision regression, PGS
final-subtitle flush, VobSub multi-PES reassembly + EOF flush,
scan_streams 0x80 LPCM via from_coding_type.
- Disc::copy() hardcoded batch=64 sectors, exceeding BU40N's 60-sector
hw limit. Now accepts batch_sectors param, defaults to 60.
- IFO PGC: playback time at offset 0x04 not 0x02, cell time at cell+4
- DiscStream: set demuxer from content_format (TS for BD, PS for DVD)
- Flush TS/PS demuxers at EOF to avoid losing last PES frame
- M2tsStream: flush demuxer at EOF
- StdioStream: FMKV metadata header for roundtrip compatibility
Stream trait: read() returns PesFrame, write() accepts PesFrame.
A stream is a stream — you read from it or write to it.
No separate Input/Output traits.
API: libfreemkv::input(url) and libfreemkv::output(url, title, codecs)
Returns Box<dyn Stream>.