Fix audit findings: DTS AMODE bound, key-fetch negative memoization, PGS probe coverage

- dts: accept all 16 legal AMODE channel-arrangement codes (0-15), not just
  0-9. Per ETSI TS 102 114 the 6-bit AMODE field has 16 defined arrangements;
  only 16-63 are reserved. ffmpeg's ff_dca_channels[16] confirms 10-15 are
  decodable 6/7/8-channel layouts. The old bound of 10 dropped spec-legal
  multichannel core frames as undecodable, silencing recoverable audio. Add a
  regression test (literal 0..16 range) that fails if the bound reverts to 10.

- keysource: only memoize a NEGATIVE (empty) key-fetch result when every source
  genuinely ran and none held the key — never when a source Err'd (network down,
  unreachable). A transient outage was being cached as a permanent "no key" for
  the fingerprint, permanently dropping a unit that could be recovered once the
  source came back. Thread an `errored` flag out of the drivers and gate the
  cache insert on it. Tests cover both the recover-after-outage case and that a
  genuine absence is still memoized.

- pgs_forced_probe: add happy-path coverage feeding real synthetic BD-TS PGS
  display sets through the full demux -> parse -> observe -> apply path, both a
  forced verdict landing and a non-forced verdict clearing a vendor flag.

- mp4: correct fit_report doc (audio carried is AC-3/E-AC-3 AND DTS/DTS-HD).

- scan_iso test: add independent fixture expectations (volume id) so the parity
  test is no longer purely tautological against a re-run of the same composition.
This commit is contained in:
Matthew Jackson
2026-07-24 08:32:37 -07:00
parent c00384d4df
commit 661ab138c6
5 changed files with 360 additions and 27 deletions
+130
View File
@@ -182,6 +182,136 @@ mod tests {
assert!(s.forced, "no content observed → vendor forced preserved");
}
/// A reader that serves a fixed BD-TS byte stream once (across sequential
/// `read_sectors` calls), then EOF — so the probe's demux→parse→observe→apply
/// path runs on real synthetic PGS content.
struct TsReader {
data: Vec<u8>,
pos: usize,
}
impl SectorSource for TsReader {
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
buf: &mut [u8],
_recovery: bool,
) -> crate::error::Result<usize> {
if self.pos >= self.data.len() {
return Ok(0);
}
let n = buf.len().min(self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
fn capacity_sectors(&self) -> u32 {
self.data.len().div_ceil(SECTOR_BYTES) as u32
}
}
// PGS PCS layout (matches the private constants in mux::codec::pgs): a
// display-set frame begins with a PCS (segment type 0x16); byte 13 is
// number_of_composition_objects; byte 17 is the first object's flags, whose
// 0x40 bit is forced_on_flag.
const PCS_SEG: u8 = 0x16;
const PCS_NUM_OBJECTS_OFF: usize = 13;
const PCS_FLAGS_OFF: usize = 17;
const PCS_FORCED_FLAG: u8 = 0x40;
/// One PGS display-set elementary payload with a single composition object;
/// `forced` sets forced_on_flag.
fn pcs_display(forced: bool) -> Vec<u8> {
let mut d = vec![0u8; 18];
d[0] = PCS_SEG;
d[PCS_NUM_OBJECTS_OFF] = 1;
d[PCS_FLAGS_OFF] = if forced { PCS_FORCED_FLAG } else { 0 };
d
}
/// Wrap an elementary payload in one 192-byte BD-TS PES packet (PUSI, PTS
/// present) on `pid`. `cc` is the 4-bit continuity counter.
fn bd_pes_packet(pid: u16, cc: u8, es: &[u8]) -> Vec<u8> {
let mut pkt = vec![0u8; 192];
// pkt[0..4] = TP_extra_header (zeros). TS packet starts at pkt[4].
pkt[4] = 0x47; // sync
pkt[5] = 0x40 | ((pid >> 8) & 0x1F) as u8; // PUSI + PID high 5 bits
pkt[6] = (pid & 0xFF) as u8; // PID low 8 bits
pkt[7] = 0x10 | (cc & 0x0F); // adaptation=payload-only + continuity counter
// PES header (at ts payload = pkt[8..]): 00 00 01 stream_id len flags.
let p = 8;
pkt[p] = 0x00;
pkt[p + 1] = 0x00;
pkt[p + 2] = 0x01;
pkt[p + 3] = 0xBD; // private_stream_1 (carries the standard PES extension)
pkt[p + 4] = 0x00; // PES packet length hi (0 = unbounded; ignored by demux)
pkt[p + 5] = 0x00; // PES packet length lo
pkt[p + 6] = 0x80; // flags1 ('10' marker)
pkt[p + 7] = 0x80; // flags2 → PTS present
pkt[p + 8] = 0x05; // PES_header_data_length = 5 (one PTS)
// 5-byte PTS with the mandatory marker bits (bytes 0,2,4 low bit = 1).
pkt[p + 9] = 0x21;
pkt[p + 10] = 0x00;
pkt[p + 11] = 0x01;
pkt[p + 12] = 0x00;
pkt[p + 13] = 0x01;
let es_off = p + 14; // ES data follows the 14-byte PES header
let n = es.len().min(192 - es_off);
pkt[es_off..es_off + n].copy_from_slice(&es[..n]);
pkt
}
/// Two BD-TS PES on `pid`: the FIRST carries `es` (the observed display set);
/// the second (a fresh PUSI) exists only to flush the first PES out of the
/// demuxer — the probe never calls `TsDemuxer::flush`, so an open PES stays
/// buffered until the next PES start arrives.
fn ts_stream(pid: u16, es: &[u8]) -> Vec<u8> {
let mut s = bd_pes_packet(pid, 0, es);
s.extend_from_slice(&bd_pes_packet(pid, 1, &pcs_display(false)));
s
}
#[test]
fn forced_display_sets_apply_forced_verdict() {
// Feed REAL synthetic PGS bytes through the full demux→parse→observe→apply
// path: a forced display set must flip a vendor-not-forced PGS track to
// forced. Mutation guard: inverting ForcedTracker::is_forced flips this.
let pid = 0x1200u16;
let mut reader = TsReader {
data: ts_stream(pid, &pcs_display(true)),
pos: 0,
};
let mut title = pgs_title(pid, false); // vendor label says NOT forced
probe_and_set_forced(&mut reader, &mut title);
let Stream::Subtitle(s) = &title.streams[0] else {
panic!()
};
assert!(
s.forced,
"an all-forced PGS track → forced verdict applied onto the stream"
);
}
#[test]
fn nonforced_display_sets_clear_forced_verdict() {
// A non-forced display set observed on the wire overrides a vendor-forced
// label → the track settles as not-forced.
let pid = 0x1200u16;
let mut reader = TsReader {
data: ts_stream(pid, &pcs_display(false)),
pos: 0,
};
let mut title = pgs_title(pid, true); // vendor label says forced
probe_and_set_forced(&mut reader, &mut title);
let Stream::Subtitle(s) = &title.streams[0] else {
panic!()
};
assert!(
!s.forced,
"a non-forced display set observed → forced verdict cleared"
);
}
#[test]
fn no_pgs_streams_is_noop() {
// A title with no PGS subtitle streams is a no-op (the reader is never