1.2.0: mux loss-concealment read path (P3/Edit-2, A2 NULL-TS fill)

Decrypt-verify is a RIP gate, not a MUX gate. On the mux read path an
undecryptable content unit must never abort the mux:

- DecryptingSectorSource gains tolerate_decrypt_loss(): when set, an
  undecryptable in-content unit is tallied, overwritten with valid NULL
  TS packets (PID 0x1FFF) via aacs::fill_null_ts_unit, logged loud with
  its LBA, and the read returns Ok — the stream keeps flowing. The rip
  paths keep the fail-loud DECRYPT_VERIFY_READ decorator (re-read off the
  disc); only the mux opts in.
- Wire it into both mux read paths: the file-backed highway
  (build_iso_pipeline) and the inline DiscStream.
- NULL-TS fill keeps the demuxer byte-synced on the 192-byte stride; the
  lost video/audio PID packets surface as a CC gap the TS assembler
  already drops a partial PES on (the B1 foundation). Ciphertext is never
  passed downstream either way.
- Fix stale resolve_vid_only no-cert test: default is UHD (audit #4).

Tests: conceal-as-NULL-TS, fill well-formedness, fail-loud still holds.
This commit is contained in:
Matthew Jackson
2026-06-28 22:44:19 -07:00
parent a731e7b26b
commit 9a7be7a1a5
6 changed files with 234 additions and 8 deletions
+38
View File
@@ -163,6 +163,44 @@ pub fn aacs_unit_needs_decrypt(unit: &[u8]) -> bool {
aacs_unit_encrypted(unit) && ts_sync_destroyed(unit)
}
/// Overwrite an aligned unit (6144 bytes) IN PLACE with valid NULL MPEG-TS
/// source packets — the [A2] mux loss-concealment fill for a content unit that
/// genuinely would not decrypt.
///
/// Zero-filling such a unit is wrong at the TS layer: a run of `0x00` bytes
/// carries no `0x47` sync, so the demuxer loses packet framing and can mis-parse
/// the *next* unit if a stray `0x47` appears mid-zero. Instead we lay down 32
/// well-formed BD source packets, each a TS null packet (PID `0x1FFF`):
///
/// ```text
/// [4-byte TP_extra_header = 0][47 1F FF 10][184 bytes 0xFF stuffing]
/// ```
///
/// The demuxer stays byte-synced on the 192-byte stride, and because PID
/// `0x1FFF` matches no elementary stream every null packet is silently dropped —
/// so the *video/audio* PID simply loses these packets. That shows up downstream
/// as a continuity-counter gap on the real PID, which the TS assembler already
/// turns into a dropped partial PES (see `mux::ts`), the foundation B1 builds on.
/// This NEVER emits ciphertext and is lossless framing, not fabricated content.
pub fn fill_null_ts_unit(unit: &mut [u8]) {
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
let mut off = 0;
while off + PKT <= unit.len() {
// TP_extra_header (arrival timestamp / copy-control) — zero is fine; the
// demuxer never reads it for a PID it does not track.
unit[off..off + 4].fill(0);
// 188-byte TS null packet: sync, PID 0x1FFF (no PUSI/TEI), payload-only
// with continuity counter 0.
unit[off + 4] = TS_SYNC; // 0x47
unit[off + 5] = 0x1F; // PID high (top 5 bits of 0x1FFF, flags clear)
unit[off + 6] = 0xFF; // PID low
unit[off + 7] = 0x10; // adaptation=01 (payload only), CC=0
// Stuffing: 0xFF is the conventional null-packet payload fill.
unit[off + 8..off + PKT].fill(0xFF);
off += PKT;
}
}
/// Count the MPEG-TS sync bytes (`0x47`) present at the BD-TS packet stride
/// (offset 4 and every 192 bytes after — 4-byte TP_extra_header + 188-byte
/// TS packet). A clear or correctly-decrypted m2ts unit shows ~one per