test: constrain the AACS key-map gap fill, the PSI walk, and MP4 field offsets
Third pass over src/mux/. 40 survivors killed, no production change. resolve.rs — the deleted-statement cluster is now fully constrained. All 14 deletable statements probed; 9 were already caught, 5 survived: c.sort_unstable() in fill_base_key_gaps. Every existing case handed it cuts already in LBA order, but IndividualSegment.tbl is a record list. Verified on HEAD: deleting the sort passes all 54 resolve tests. The mutant lays a base-key fill straight over a forensic segment. last_idx = idx (FMTS gap fill) and last_idx = hit (multi-CPS cache hit). An extent with nothing to sample must inherit its neighbour's CPS unit; the mutants fall back to the first unit's key. Exactly the shape this file's own comments name — wrong key, no error, lost_bytes == 0. Both check_halt()? polls in probe_fmts_index_keys. These cannot be killed by outcome, since a later poll returns Halted too. The tests count reads instead, which is what the don't-hammer-a-struggling- drive rule actually says: after a Stop the drive is asked for zero content sectors. The four unresolved += 1 arms each got a test, and deleting each fails exactly one — one-to-one, so no fixture passes for the wrong reason. A control test pins that the baseline table resolves, so an expect_err cannot succeed for an unrelated reason. ts.rs::scan_streams was never entered. Six killed, two of which return wrong answers that look right: reading the PAT/PMT CRC as a table entry invents a stream on PID 546 out of CRC bytes, and dropping the ES_info_length skip decodes a descriptor as an entry and loses the one after it. Every existing PMT fixture declares ES_info_length = 0; a real BD PMT carries a registration descriptor on essentially every entry. Also ISO/IEC 13818-1 2.4.4.3 program_number == 0 is the network PID, not a program. mp4/read.rs — 13. Height read as the width beside it; channelcount; the 4-byte base-128 descriptor varint (every existing esds fixture uses a single byte); all three optional ES_Descriptor fields, whose loss is silent (an AAC track just loses its CodecPrivate); first-vs-last media edit, which is A/V desync of the difference; and the version-1 mvhd timescale offset, emitted by any writer whose duration exceeds 32 bits. dts.rs — 7, from a real cargo-mutants run over the file rather than guesswork. Including a buffer that IS the syncword, which is the state a sync split across PES packets lands in the moment its last byte arrives. Equivalents proven by application, not argued: the sample_encrypted_units guard pair is mutually redundant by construction (total*p/9 < total for p <= 8), so either alone is equivalent and both together are not; the PMT section_len guard is dead code where its PAT twin panics; three of the seven EXSS_HEADER_MIN_BYTES arithmetic mutants still sum to 10.
This commit is contained in:
+247
@@ -2598,4 +2598,251 @@ mod tests {
|
||||
"recovered PES carries only the post-reset ES bytes"
|
||||
);
|
||||
}
|
||||
|
||||
// ── PAT / PMT section-walk boundaries (ISO/IEC 13818-1 §2.4.4.3, §2.4.4.8) ──
|
||||
|
||||
/// A PAT TS packet with an arbitrary program loop and arbitrary CRC bytes.
|
||||
/// `entries` is `(program_number, pid)`; `crc` is the 4 bytes that follow the
|
||||
/// loop, which the scanner never validates and must never PARSE either.
|
||||
fn pat_packet_entries(entries: &[(u16, u16)], crc: [u8; 4]) -> Vec<u8> {
|
||||
let mut body = [0xFFu8; 184];
|
||||
body[0] = 0x00; // pointer_field
|
||||
let s = 1;
|
||||
body[s] = 0x00; // table_id = PAT
|
||||
// tsid(2) + version(1) + section_number(1) + last_section(1)
|
||||
// + 4 per program entry + 4-byte CRC.
|
||||
let section_length = 5 + entries.len() * 4 + 4;
|
||||
body[s + 1] = 0xB0 | (((section_length >> 8) as u8) & 0x0F);
|
||||
body[s + 2] = (section_length & 0xFF) as u8;
|
||||
body[s + 3] = 0x00; // tsid hi
|
||||
body[s + 4] = 0x01; // tsid lo
|
||||
body[s + 5] = 0xC1; // version / current_next
|
||||
body[s + 6] = 0x00; // section_number
|
||||
body[s + 7] = 0x00; // last_section_number
|
||||
let mut p = s + 8;
|
||||
for &(prog, pid) in entries {
|
||||
body[p] = (prog >> 8) as u8;
|
||||
body[p + 1] = (prog & 0xFF) as u8;
|
||||
body[p + 2] = 0xE0 | (((pid >> 8) as u8) & 0x1F);
|
||||
body[p + 3] = (pid & 0xFF) as u8;
|
||||
p += 4;
|
||||
}
|
||||
body[p..p + 4].copy_from_slice(&crc);
|
||||
bdts_packet(body, 0, true)
|
||||
}
|
||||
|
||||
/// A PMT TS packet whose ES entries may carry descriptors, and whose section
|
||||
/// may declare `stuffing` extra bytes between the last entry and the CRC.
|
||||
/// `entries` is `(stream_type, es_pid, es_info bytes)`.
|
||||
fn pmt_packet_desc(
|
||||
pmt_pid: u16,
|
||||
entries: &[(u8, u16, Vec<u8>)],
|
||||
stuffing: &[u8],
|
||||
crc: [u8; 4],
|
||||
) -> Vec<u8> {
|
||||
let mut body = [0xFFu8; 184];
|
||||
body[0] = 0x00; // pointer_field
|
||||
let s = 1;
|
||||
body[s] = 0x02; // table_id = PMT
|
||||
let entries_len: usize = entries.iter().map(|(_, _, d)| 5 + d.len()).sum();
|
||||
let section_length = 9 + entries_len + stuffing.len() + 4;
|
||||
body[s + 1] = 0xB0 | (((section_length >> 8) as u8) & 0x0F);
|
||||
body[s + 2] = (section_length & 0xFF) as u8;
|
||||
body[s + 3] = 0x00; // program_number hi
|
||||
body[s + 4] = 0x01; // program_number lo
|
||||
body[s + 5] = 0xC1;
|
||||
body[s + 6] = 0x00;
|
||||
body[s + 7] = 0x00;
|
||||
body[s + 8] = 0xE0; // PCR PID
|
||||
body[s + 9] = 0x00;
|
||||
body[s + 10] = 0xF0; // program_info_length = 0
|
||||
body[s + 11] = 0x00;
|
||||
let mut p = s + 12;
|
||||
for (stype, es_pid, desc) in entries {
|
||||
body[p] = *stype;
|
||||
body[p + 1] = 0xE0 | (((es_pid >> 8) as u8) & 0x1F);
|
||||
body[p + 2] = (es_pid & 0xFF) as u8;
|
||||
body[p + 3] = 0xF0 | (((desc.len() >> 8) as u8) & 0x0F);
|
||||
body[p + 4] = (desc.len() & 0xFF) as u8;
|
||||
body[p + 5..p + 5 + desc.len()].copy_from_slice(desc);
|
||||
p += 5 + desc.len();
|
||||
}
|
||||
body[p..p + stuffing.len()].copy_from_slice(stuffing);
|
||||
p += stuffing.len();
|
||||
body[p..p + 4].copy_from_slice(&crc);
|
||||
bdts_packet(body, pmt_pid, true)
|
||||
}
|
||||
|
||||
fn stream_pids(streams: &[crate::disc::Stream]) -> Vec<u16> {
|
||||
use crate::disc::Stream;
|
||||
streams
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Stream::Video(v) => v.pid,
|
||||
Stream::Audio(a) => a.pid,
|
||||
Stream::Subtitle(t) => t.pid,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// ISO/IEC 13818-1 §2.4.4.3: a PAT entry with `program_number == 0` carries the
|
||||
/// **network PID**, not a program's PMT PID. Taking it would point pass 2 at the
|
||||
/// NIT, where there is no `table_id 0x02` — so the title's whole stream list
|
||||
/// comes back empty and the disc looks like it has no streams.
|
||||
///
|
||||
/// Two entries with DISTINCT PIDs, the NIT first, so the test also pins the loop
|
||||
/// STRIDE: a 4-byte program entry. A stride that is not 4 lands mid-entry on the
|
||||
/// second one and decodes a PID belonging to nothing.
|
||||
#[test]
|
||||
fn scan_streams_skips_the_pat_network_entry_and_takes_the_real_program() {
|
||||
let pmt_pid = 0x0100u16;
|
||||
let nit_pid = 0x0010u16; // the customary network PID
|
||||
let pat = pat_packet_entries(&[(0, nit_pid), (1, pmt_pid)], [0xFF; 4]);
|
||||
let mut data = pat.clone();
|
||||
data.extend(pmt_packet(pmt_pid, &[(0x1B, 0x1011)]));
|
||||
data.extend(pat); // follower, for the resync corroboration
|
||||
let streams =
|
||||
scan_streams(&data).expect("the program-1 entry must be the one that is used");
|
||||
assert_eq!(
|
||||
stream_pids(&streams),
|
||||
vec![0x1011],
|
||||
"the PMT found through program 1's PID"
|
||||
);
|
||||
|
||||
// And a PAT that carries ONLY a network entry declares no program at all.
|
||||
let nit_only = pat_packet_entries(&[(0, nit_pid)], [0xFF; 4]);
|
||||
let mut data = nit_only.clone();
|
||||
data.extend(pmt_packet(pmt_pid, &[(0x1B, 0x1011)]));
|
||||
data.extend(nit_only);
|
||||
assert!(
|
||||
scan_streams(&data).is_none(),
|
||||
"a network entry is not a program — there is nothing to scan"
|
||||
);
|
||||
}
|
||||
|
||||
/// The PAT program loop stops before the 4-byte `CRC_32`
|
||||
/// (ISO/IEC 13818-1 Table 2-30): `section_length` counts the CRC, the loop
|
||||
/// must not. The scanner never validates the CRC, so those 4 bytes are
|
||||
/// effectively arbitrary — here they are the bit pattern of a valid-looking
|
||||
/// `program_number 1 → pmt_pid` entry.
|
||||
///
|
||||
/// Reading them as a program is not a crash, it is a WRONG ANSWER that looks
|
||||
/// right: the scan returns a full stream list for a PAT that declares no
|
||||
/// program at all. So the assertion is that the streams are NOT found.
|
||||
#[test]
|
||||
fn scan_streams_does_not_read_the_pat_crc_as_a_program_entry() {
|
||||
let pmt_pid = 0x0100u16;
|
||||
// A CRC whose bytes spell "program 1 → 0x0100".
|
||||
let crc = [
|
||||
0x00,
|
||||
0x01,
|
||||
0xE0 | ((pmt_pid >> 8) as u8 & 0x1F),
|
||||
pmt_pid as u8,
|
||||
];
|
||||
let pat = pat_packet_entries(&[(0, 0x0010)], crc);
|
||||
let mut data = pat.clone();
|
||||
data.extend(pmt_packet(pmt_pid, &[(0x1B, 0x1011)]));
|
||||
data.extend(pat);
|
||||
assert!(
|
||||
scan_streams(&data).is_none(),
|
||||
"the CRC_32 is not a program entry, however much it looks like one"
|
||||
);
|
||||
}
|
||||
|
||||
/// ISO/IEC 13818-1 §2.4.4.8: each ES entry is followed by `ES_info_length`
|
||||
/// bytes of descriptors, and the next entry starts after them. Every existing
|
||||
/// PMT fixture declares `ES_info_length = 0`, so the skip is unconstrained by
|
||||
/// them — while a real BD PMT carries a registration descriptor on essentially
|
||||
/// every entry.
|
||||
///
|
||||
/// Failing to skip them does not fail loudly: the descriptor bytes are decoded
|
||||
/// as an ES entry, so the scan reports streams on PIDs that carry nothing and
|
||||
/// silently loses the entries that follow. Here the first entry's descriptor is
|
||||
/// a real `registration_descriptor` for 'HDMV'.
|
||||
#[test]
|
||||
fn scan_streams_steps_over_es_descriptors_to_reach_the_next_entry() {
|
||||
let pmt_pid = 0x0100u16;
|
||||
let hdmv = vec![0x05, 0x04, b'H', b'D', b'M', b'V'];
|
||||
let pmt = pmt_packet_desc(
|
||||
pmt_pid,
|
||||
&[
|
||||
(0x1B, 0x1011, hdmv), // H.264 video, with a descriptor
|
||||
(0x81, 0x1100, Vec::new()), // AC-3 audio, without
|
||||
],
|
||||
&[],
|
||||
[0xFF; 4],
|
||||
);
|
||||
let mut data = pat_packet(pmt_pid);
|
||||
data.extend(pmt);
|
||||
data.extend(pat_packet(pmt_pid));
|
||||
let streams = scan_streams(&data).expect("both entries parse");
|
||||
assert_eq!(
|
||||
stream_pids(&streams),
|
||||
vec![0x1011, 0x1100],
|
||||
"the descriptor bytes are skipped, not decoded as an entry, and the \
|
||||
entry after them is still reached"
|
||||
);
|
||||
assert!(
|
||||
matches!(streams[0], crate::disc::Stream::Video(_)),
|
||||
"0x1B is video"
|
||||
);
|
||||
assert!(
|
||||
matches!(streams[1], crate::disc::Stream::Audio(_)),
|
||||
"0x81 is audio"
|
||||
);
|
||||
}
|
||||
|
||||
/// The PMT ES loop stops before the `CRC_32` for the same reason the PAT loop
|
||||
/// does. It only shows when the declared section is longer than the entries —
|
||||
/// a padded PMT — because otherwise the loop runs out of room on its own and
|
||||
/// the CRC is never in reach.
|
||||
///
|
||||
/// Here two stuffing bytes sit between the last entry and the CRC, and the
|
||||
/// first of them is `0x1B`. Parsing into the CRC therefore invents a SECOND
|
||||
/// video stream, on a PID assembled from CRC bytes — a stream the mux would go
|
||||
/// on to demux, finding nothing.
|
||||
#[test]
|
||||
fn scan_streams_does_not_read_the_pmt_crc_as_an_es_entry() {
|
||||
let pmt_pid = 0x0100u16;
|
||||
let pmt = pmt_packet_desc(
|
||||
pmt_pid,
|
||||
&[(0x1B, 0x1011, Vec::new())],
|
||||
&[0x1B, 0xE2], // padding: a stream_type byte and a PID high byte
|
||||
[0x22, 0xF0, 0x00, 0x99],
|
||||
);
|
||||
let mut data = pat_packet(pmt_pid);
|
||||
data.extend(pmt);
|
||||
data.extend(pat_packet(pmt_pid));
|
||||
let streams = scan_streams(&data).expect("the real entry parses");
|
||||
assert_eq!(
|
||||
stream_pids(&streams),
|
||||
vec![0x1011],
|
||||
"only the declared ES entry — the CRC_32 is not an entry"
|
||||
);
|
||||
}
|
||||
|
||||
/// `section_length` counts the bytes AFTER the length field including the
|
||||
/// 4-byte CRC, so the program loop's end is `3 + section_length - 4`. A section
|
||||
/// declaring less than 4 underflows that subtraction — a debug panic, and in
|
||||
/// release a wrapped bound clamped to the whole buffer. The guard turns a
|
||||
/// malformed PAT into an ordinary "no program here" instead.
|
||||
///
|
||||
/// Reachable exactly as written: `collect_psi_section` truncates the section to
|
||||
/// `3 + section_length`, so a declared 0 yields the 3-byte header alone.
|
||||
#[test]
|
||||
fn scan_streams_pat_section_length_below_the_crc_size_is_rejected_not_underflowed() {
|
||||
let mut body = [0xFFu8; 184];
|
||||
body[0] = 0x00; // pointer_field
|
||||
body[1] = 0x00; // table_id = PAT
|
||||
body[2] = 0xB0; // section_syntax + reserved, length high nibble = 0
|
||||
body[3] = 0x00; // section_length = 0 — shorter than its own CRC
|
||||
let pat = bdts_packet(body, 0, true);
|
||||
let mut data = pat.clone();
|
||||
data.extend(pmt_packet(0x0100, &[(0x1B, 0x1011)]));
|
||||
data.extend(pat);
|
||||
assert!(
|
||||
scan_streams(&data).is_none(),
|
||||
"an undersized PAT section declares no program"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user