test: constrain MP4 composition timing, MLP substream directory, and codec-private absence
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.
This commit is contained in:
@@ -1413,6 +1413,49 @@ mod tests {
|
||||
assert!(ogm.contains("CHAPTER02NAME=2"));
|
||||
}
|
||||
|
||||
/// Chapter names originate on the disc, which is untrusted input, and the
|
||||
/// sink drops them straight into an XML document (`<ChapterString>`). Escaping
|
||||
/// is what keeps a hostile or merely odd name from terminating the element and
|
||||
/// injecting markup — the document is character data, so XML 1.0 §2.4 requires
|
||||
/// `&` and `<`, and escaping `>` as well keeps a `]]>` sequence safe too.
|
||||
///
|
||||
/// The order matters as much as the set: `&` must be replaced FIRST, otherwise
|
||||
/// the ampersands introduced by the `<`/`>` replacements get escaped a second
|
||||
/// time and `<` renders as the literal text `<` instead of a `<`.
|
||||
#[test]
|
||||
fn chapter_names_are_xml_escaped_so_a_disc_cannot_inject_markup() {
|
||||
let chaps = vec![Chapter {
|
||||
time_secs: 0.0,
|
||||
name: "</ChapterString><Injected/> Tom & Jerry <3 >:(".to_string(),
|
||||
}];
|
||||
let xml = chapters_xml(&chaps);
|
||||
|
||||
assert!(
|
||||
xml.contains(
|
||||
"<ChapterString></ChapterString><Injected/> \
|
||||
Tom & Jerry <3 >:(</ChapterString>"
|
||||
),
|
||||
"every metacharacter escaped, and `&` escaped first so nothing is \
|
||||
double-escaped; got:\n{xml}"
|
||||
);
|
||||
// The injected element must not survive as markup anywhere in the file.
|
||||
assert!(
|
||||
!xml.contains("<Injected/>"),
|
||||
"a chapter name must not be able to open a new element"
|
||||
);
|
||||
// Exactly one ChapterString element pair — the name did not close it early.
|
||||
assert_eq!(xml.matches("<ChapterString>").count(), 1);
|
||||
assert_eq!(xml.matches("</ChapterString>").count(), 1);
|
||||
|
||||
// A name with no metacharacters passes through byte-identical: escaping
|
||||
// must not rewrite ordinary text.
|
||||
let plain = chapters_xml(&[Chapter {
|
||||
time_secs: 0.0,
|
||||
name: "Opening Credits".to_string(),
|
||||
}]);
|
||||
assert!(plain.contains("<ChapterString>Opening Credits</ChapterString>"));
|
||||
}
|
||||
|
||||
// ── Timeline continuity ──────────────────────────────────────────────────
|
||||
//
|
||||
// The corrector itself is tested verbatim in `crate::mux::timeline`. Here we
|
||||
|
||||
Reference in New Issue
Block a user