mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Merge branch 'codeaudit/tg-busy-2' into codeaudit/land
This commit is contained in:
@@ -4155,6 +4155,12 @@ mod tests {
|
||||
assert!(d.contains("redacted"), "AacsState missing marker: {d}");
|
||||
|
||||
for k in [
|
||||
Key::Device(vec![crate::aacs::types::DeviceKey {
|
||||
key: [0xD5; 16],
|
||||
node: 0,
|
||||
uv: 0,
|
||||
u_mask_shift: 0,
|
||||
}]),
|
||||
Key::Unit(vec![(1, [0xD5; 16])]),
|
||||
Key::Volume([0xD5; 16]),
|
||||
Key::Processing(vec![[0xD5; 16]]),
|
||||
@@ -7097,6 +7103,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Many ranges: sorted largest-first, capped at 50 with the overflow counted.
|
||||
// Input is ascending, so a flipped sort keeps the smallest and reports its gap.
|
||||
// bps = 2048 B/s, so range i (i+1 sectors) lasts (i+1) * 1000 ms.
|
||||
#[test]
|
||||
fn locate_ranges_sorts_largest_first_and_truncates_at_fifty() {
|
||||
let mut title = title_with_size(204_800, vec![]);
|
||||
title.duration_secs = 100.0;
|
||||
let raw: Vec<(u64, u64)> = (0..53u64)
|
||||
.map(|i| (i * 100 * 2048, (i + 1) * 2048))
|
||||
.collect();
|
||||
let r = locate_ranges(&raw, &title);
|
||||
assert_eq!(r.num_ranges, 53);
|
||||
assert_eq!(r.truncated, 3);
|
||||
assert_eq!(r.ranges.len(), 50);
|
||||
assert_eq!(r.largest_gap_ms, 53_000.0);
|
||||
assert_eq!(r.ranges[0].count, 53);
|
||||
assert_eq!(r.ranges[49].count, 4, "the three smallest were dropped");
|
||||
assert!(
|
||||
r.ranges
|
||||
.windows(2)
|
||||
.all(|w| w[0].duration_ms > w[1].duration_ms)
|
||||
);
|
||||
}
|
||||
|
||||
// bps computed exactly 0.0: duration_ms/main_at_risk_ms must stay 0.0,
|
||||
// never inf/NaN. Kills `bps > 0.0` flipped to `>=` at both sites (the
|
||||
// boundary exactly zero would wrongly take the division branch).
|
||||
|
||||
@@ -2114,6 +2114,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// A truncated run may still assert "not forced" (positive evidence: a non-forced set was
|
||||
// seen) when the shape gate allows demotion, but never a forced verdict (an absence claim).
|
||||
#[test]
|
||||
fn a_truncated_run_asserts_only_a_demotable_not_forced_verdict() {
|
||||
let full = TrackEvidence {
|
||||
observed: true,
|
||||
non_forced: true,
|
||||
displays: 20,
|
||||
..Default::default()
|
||||
};
|
||||
let flagged = TrackEvidence {
|
||||
observed: true,
|
||||
forced_seen: true,
|
||||
displays: 20,
|
||||
..Default::default()
|
||||
};
|
||||
let ev = HashMap::from([(0x1200u16, full), (0x1201u16, flagged)]);
|
||||
assert_eq!(verdicts(&ev, false), HashMap::from([(0x1200u16, false)]));
|
||||
}
|
||||
|
||||
// Coverage is what makes a memo replayable: an entry from a thin sample must not answer a
|
||||
// question needing a thorough one.
|
||||
#[test]
|
||||
|
||||
+25
-2
@@ -1772,10 +1772,26 @@ fn resolve_retains_no_source_and_nothing_asks_after() {
|
||||
let mut buf = vec![0u8; 30 * 2048];
|
||||
w.read_sectors(b, 30, &mut buf, true).unwrap();
|
||||
}
|
||||
let _ = crate::mux::mux_with_keys(
|
||||
// The mux needs a stream to run at all; with none it returns E6009 before reading.
|
||||
let mut title = fx.disc.titles[0].clone();
|
||||
title
|
||||
.streams
|
||||
.push(crate::disc::Stream::Video(crate::disc::VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: crate::disc::Codec::Mpeg2,
|
||||
resolution: crate::disc::Resolution::R1080p,
|
||||
frame_rate: crate::disc::FrameRate::F23_976,
|
||||
hdr: crate::disc::HdrFormat::Sdr,
|
||||
color_space: crate::disc::ColorSpace::Bt709,
|
||||
display_aspect: None,
|
||||
secondary: false,
|
||||
label: String::new(),
|
||||
measured_cicp: None,
|
||||
}));
|
||||
let r = crate::mux::mux_with_keys(
|
||||
crate::mux::MuxSource::Live {
|
||||
reader: Box::new(src.clone()),
|
||||
title: fx.disc.titles[0].clone(),
|
||||
title,
|
||||
format: ContentFormat::BdTs,
|
||||
},
|
||||
Some(&set),
|
||||
@@ -1787,6 +1803,13 @@ fn resolve_retains_no_source_and_nothing_asks_after() {
|
||||
&Halt::new(),
|
||||
Arc::new(crate::mux::driver::NoopEvents),
|
||||
);
|
||||
// The fixture TS carries no muxable frames, so a mux that read every unit through the
|
||||
// held keys ends E6008 (MkvInvalid); E6009 would mean it never read.
|
||||
let e = r.expect_err("no muxable frames");
|
||||
assert_eq!(
|
||||
crate::error::error_code(&e),
|
||||
Some(crate::error::E_MKV_INVALID)
|
||||
);
|
||||
assert_eq!(Arc::strong_count(&f), 1);
|
||||
assert_eq!(calls.len(), asked, "nothing asks after resolve");
|
||||
}
|
||||
|
||||
@@ -376,6 +376,83 @@ mod tests {
|
||||
assert_eq!(set, None);
|
||||
}
|
||||
|
||||
// A UDF with `/BDMV/META/DL/` holding `files` (name, contents), or no DL dir when `dl` is false.
|
||||
fn disc_with_dl(files: &[(&str, Vec<u8>)], dl: bool) -> (crate::udf::fixture::MemDisc, UdfFs) {
|
||||
use crate::udf::fixture::*;
|
||||
let dl_files = files
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (n, c))| file_with(n, 30 + i as u32, 100 + 2_000 * i as u32, c.clone(), true))
|
||||
.collect();
|
||||
let dir = |name: &str, icb, data, files, subdirs| DirSpec {
|
||||
name: name.into(),
|
||||
icb_lba: icb,
|
||||
dir_data_lba: data,
|
||||
files,
|
||||
subdirs,
|
||||
};
|
||||
let leaf = if dl {
|
||||
dir("DL", 16, 17, dl_files, vec![])
|
||||
} else {
|
||||
dir("OTHER", 16, 17, dl_files, vec![])
|
||||
};
|
||||
let meta = dir("META", 14, 15, vec![], vec![leaf]);
|
||||
let bdmv = dir("BDMV", 12, 13, vec![], vec![meta]);
|
||||
let root = dir("", 10, 11, vec![], vec![bdmv]);
|
||||
let mut disc = MemDisc::new();
|
||||
build_udf_skeleton(&mut disc, 10);
|
||||
lay_dir(&mut disc, &root);
|
||||
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
|
||||
(disc, udf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_aggregates_languages_and_first_disc_set_wins() {
|
||||
let xml = |name: &str, set: u32| {
|
||||
format!(
|
||||
"<d><di:name>{name}</di:name><di:setNumber>{set}</di:setNumber>\
|
||||
<di:numSets>2</di:numSets></d>"
|
||||
)
|
||||
.into_bytes()
|
||||
};
|
||||
let mut fra = xml("Titre", 2);
|
||||
fra.extend_from_slice(b"<di:description>Suite.</di:description>");
|
||||
let mut big = xml("Too Big", 1);
|
||||
big.resize(MAX_BDMT_BYTES as usize + 1, b' ');
|
||||
let (mut disc, udf) = disc_with_dl(
|
||||
&[
|
||||
("bdmt_eng.xml", xml("Title", 1)),
|
||||
("bdmt_fra.xml", fra),
|
||||
("bdmt_deu.xml", big),
|
||||
("bdmt_xx.xml", xml("Bad Lang", 1)),
|
||||
("notes.xml", xml("Not BDMT", 1)),
|
||||
],
|
||||
true,
|
||||
);
|
||||
assert!(detect(&udf));
|
||||
let meta = parse(&mut disc, &udf).expect("titles found");
|
||||
let titles: Vec<_> = meta
|
||||
.titles
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
assert_eq!(titles, [("eng", "Title"), ("fra", "Titre")]);
|
||||
assert_eq!(meta.descriptions.len(), 1);
|
||||
assert_eq!(meta.descriptions["fra"], "Suite.");
|
||||
assert_eq!(meta.disc_number, Some((1, 2)), "the first file's set wins");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_and_detect_are_none_without_bdmt_files() {
|
||||
let (mut disc, udf) = disc_with_dl(&[("bdmt_eng.xml", b"<d/>".to_vec())], false);
|
||||
assert!(!detect(&udf));
|
||||
assert!(parse(&mut disc, &udf).is_none());
|
||||
// A DL dir with only a title-less bdmt file: detected, but nothing to parse.
|
||||
let (mut disc, udf) = disc_with_dl(&[("bdmt_eng.xml", b"<d/>".to_vec())], true);
|
||||
assert!(detect(&udf));
|
||||
assert!(parse(&mut disc, &udf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_languages_keyed_correctly() {
|
||||
// Drive parse_bdmt_xml from two synthetic XML blobs and aggregate
|
||||
|
||||
@@ -809,12 +809,9 @@ mod tests {
|
||||
/// Mutation: skip lowercase normalization → "ENG" stays "ENG" in the label.
|
||||
#[test]
|
||||
fn normalize_language_lowercases_and_trims() {
|
||||
assert_eq!(
|
||||
// trim + lowercase, mirroring production normalize_language
|
||||
super::super::mpls_universal::language_display_name(
|
||||
&" ENG ".trim().to_ascii_lowercase(),
|
||||
),
|
||||
"English"
|
||||
);
|
||||
assert_eq!(normalize_language(" ENG "), "eng");
|
||||
assert_eq!(normalize_language(" "), "");
|
||||
// An unknown code keeps its trimmed lowercase form.
|
||||
assert_eq!(normalize_language(" XYZ "), "xyz");
|
||||
}
|
||||
}
|
||||
|
||||
+47
-4
@@ -762,8 +762,8 @@ mod tests {
|
||||
assert_eq!(codec("dts"), "dts");
|
||||
}
|
||||
|
||||
/// Spec: all 36 bare-lang entries must resolve correctly.
|
||||
/// Mutation: swap two entries in BARE_LANGS → wrong code returned.
|
||||
/// Spec: every BARE_LANGS entry must resolve to its code.
|
||||
/// Mutation: swap two entries' codes in BARE_LANGS -> wrong code returned.
|
||||
#[test]
|
||||
fn lang_bare_all_entries_spot_check() {
|
||||
let cases = [
|
||||
@@ -774,17 +774,48 @@ mod tests {
|
||||
("Italian", "ita"),
|
||||
("Japanese", "jpn"),
|
||||
("Chinese", "zho"),
|
||||
("Korean", "kor"),
|
||||
("Mandarin", "zho"),
|
||||
("Cantonese", "zho"),
|
||||
("Portuguese", "por"),
|
||||
("Polish", "pol"),
|
||||
("Czech", "ces"),
|
||||
("Hungarian", "hun"),
|
||||
("Dutch", "nld"),
|
||||
("Korean", "kor"),
|
||||
("Arabic", "ara"),
|
||||
("Russian", "rus"),
|
||||
("Hindi", "hin"),
|
||||
("Turkish", "tur"),
|
||||
("Thai", "tha"),
|
||||
("Swedish", "swe"),
|
||||
("Norwegian", "nor"),
|
||||
("Danish", "dan"),
|
||||
("Finnish", "fin"),
|
||||
("Hebrew", "heb"),
|
||||
("Russian", "rus"),
|
||||
("Greek", "ell"),
|
||||
("Vietnamese", "vie"),
|
||||
("Indonesian", "ind"),
|
||||
("Malay", "msa"),
|
||||
("Ukrainian", "ukr"),
|
||||
("Romanian", "ron"),
|
||||
("Bulgarian", "bul"),
|
||||
("Croatian", "hrv"),
|
||||
("Serbian", "srp"),
|
||||
("Slovak", "slk"),
|
||||
("Slovenian", "slv"),
|
||||
("Estonian", "est"),
|
||||
("Latvian", "lav"),
|
||||
("Lithuanian", "lit"),
|
||||
("Icelandic", "isl"),
|
||||
("Basque", "eus"),
|
||||
("Catalan", "cat"),
|
||||
("Galician", "glg"),
|
||||
];
|
||||
assert_eq!(
|
||||
cases.len(),
|
||||
BARE_LANGS.len(),
|
||||
"table and cases must stay in step"
|
||||
);
|
||||
for (name, code) in cases {
|
||||
let r = lang(name).unwrap_or_else(|| panic!("lang({:?}) must be Some", name));
|
||||
assert_eq!(r.code, code, "wrong code for {}", name);
|
||||
@@ -792,6 +823,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Needles embedded in longer words must not match in qualifier/purpose/lang.
|
||||
/// Mutation: has_word -> contains() in any of the three -> a false hit.
|
||||
#[test]
|
||||
fn embedded_needles_do_not_match_word_classifiers() {
|
||||
assert_eq!(qualifier("xsdhx"), LabelQualifier::None);
|
||||
assert_eq!(qualifier("uncaptions"), LabelQualifier::None);
|
||||
assert_eq!(purpose("uncommentary"), LabelPurpose::Normal);
|
||||
assert_eq!(purpose("scorecard"), LabelPurpose::Normal);
|
||||
assert_eq!(lang("englishman"), None);
|
||||
assert_eq!(lang("thaiwan"), None);
|
||||
}
|
||||
|
||||
/// Word boundary: "sdh" inside "lambdash" must not match.
|
||||
/// Mutation: use `contains("sdh")` → "lambdash" falsely triggers SDH.
|
||||
#[test]
|
||||
|
||||
@@ -1476,6 +1476,17 @@ mod tests {
|
||||
compositions
|
||||
}
|
||||
|
||||
// A visible PCS with no duration has no known wipe time: neither write nor finish adds a clear.
|
||||
#[test]
|
||||
fn pgs_sup_visible_pcs_without_duration_gets_no_clear_at_finish() {
|
||||
let mut writer = PgsSupWriter::default();
|
||||
let mut bytes = Vec::new();
|
||||
let f = sup_frame(1_000_000_000, None, true);
|
||||
writer.write_frame(&mut bytes, &f, f.pts).unwrap();
|
||||
writer.finish(&mut bytes).unwrap();
|
||||
assert_eq!(sup_compositions(&bytes), [(90_000, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pgs_sup_preserves_original_clear_without_a_duplicate_or_later_clear() {
|
||||
let mut writer = PgsSupWriter::default();
|
||||
|
||||
+23
-1
@@ -1319,6 +1319,27 @@ mod tests {
|
||||
.count()
|
||||
}
|
||||
|
||||
/// The declared-only warning check must cover finish() too; captures are thread-local, so the
|
||||
/// sink is finished on the capturing thread instead of the consumer thread.
|
||||
#[test]
|
||||
fn declared_only_mp2_extension_gives_no_warning_at_finish() {
|
||||
let title = mp2_ext_title();
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let d = dir.path().display();
|
||||
for url in [
|
||||
format!("mkv://{d}/o.mkv"),
|
||||
format!("m2ts://{d}/o.m2ts"),
|
||||
format!("demux://{d}/demux"),
|
||||
format!("audio://{d}/audio"),
|
||||
] {
|
||||
let mut sink = crate::mux::resolve::output(&url, &title, None).expect("sink opens");
|
||||
let (res, ev) = crate::testlog::capture(|| sink.finish());
|
||||
let _ = res;
|
||||
assert_eq!(mp2_extension_warnings(&ev), 0, "{url}: declared only");
|
||||
assert!(sink.undelivered_streams().is_empty(), "{url}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard (mpg design §7): every sink but mpg/network/stdio lists an MPEG-2 multichannel
|
||||
/// extension track as excluded once its packets arrive, so lost surround is never silent;
|
||||
/// IFO coding mode 3 alone (no `0xD0|n` packet) gives no warning and no note.
|
||||
@@ -2181,7 +2202,8 @@ mod tests {
|
||||
assert!(mkv.set_codec_private(i + 1, cp).expect("patch"));
|
||||
assert!(!mkv.set_codec_private(i + 1, cp).expect("second patch"));
|
||||
}
|
||||
assert!(!mkv.set_codec_private(1, &[0; 14]).expect("too big"));
|
||||
// Track 4 still holds its reserve, so this reaches the size guard.
|
||||
assert!(!mkv.set_codec_private(4, &[0; 14]).expect("too big"));
|
||||
mkv.write(&frame(1)).expect("audio frame");
|
||||
mkv.finish().expect("finish");
|
||||
let back = crate::mux::mkvstream::MkvStream::open(std::fs::File::open(&path).unwrap())
|
||||
|
||||
@@ -1240,6 +1240,22 @@ mod tests {
|
||||
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_size_truncated_inside_the_vint_errors_at_every_width() {
|
||||
// A width-w size VINT needs w bytes; supply one fewer, never a decoded size.
|
||||
for w in 2..=8usize {
|
||||
let mut bytes = vec![0u8; w - 1];
|
||||
bytes[0] = 0x80 >> (w - 1);
|
||||
let e = read_size(&mut Cursor::new(&bytes)).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof, "width {w}");
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, SIMPLE_BLOCK).unwrap();
|
||||
buf.push(0x40);
|
||||
let e = read_element_header(&mut Cursor::new(&buf)).unwrap_err();
|
||||
assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
|
||||
// write_size — every declared width-boundary, asserting the exact VINT bytes.
|
||||
// Width W encodes 7*W payload bits; the highest value of each width is reserved.
|
||||
|
||||
|
||||
+30
-1
@@ -5550,6 +5550,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Several EBML size deltas in a row, mixed signs: each is relative to the PREVIOUS size,
|
||||
// and a delta that drives a size below zero rejects the whole lace.
|
||||
#[test]
|
||||
fn an_ebml_lace_accumulates_signed_deltas_and_rejects_negative_sizes() {
|
||||
// Sizes 3, +2, -3, +2 = 3, 5, 2, 4; the last frame is the remainder (6).
|
||||
// 1-octet svint: 0x80 | (delta + 63).
|
||||
let mut body = vec![0x04, 0x83, 0xC1, 0xBC, 0xC1];
|
||||
let sizes = [3usize, 5, 2, 4, 6];
|
||||
for (i, n) in sizes.iter().enumerate() {
|
||||
body.extend(std::iter::repeat_n(i as u8 + 1, *n));
|
||||
}
|
||||
let frames = split_lacing(LACING_EBML, &body).expect("well-formed lace");
|
||||
assert_eq!(frames.len(), 5);
|
||||
for (i, (f, n)) in frames.iter().zip(sizes).enumerate() {
|
||||
assert_eq!(*f, vec![i as u8 + 1; n].as_slice(), "frame {i}");
|
||||
}
|
||||
// 3 then -4 = -1: negative size.
|
||||
assert_eq!(
|
||||
split_lacing(LACING_EBML, &[0x02, 0x83, 0xBB, 0, 0, 0]),
|
||||
None
|
||||
);
|
||||
// The delta VINT is missing.
|
||||
assert_eq!(split_lacing(LACING_EBML, &[0x02, 0x83]), None);
|
||||
}
|
||||
|
||||
// The shortest usable (Simple)Block is 4 bytes (track VINT + rel-ts + flags,
|
||||
// empty payload) — exactly the boundary of the short-block guards, where an
|
||||
// off-by-one drops a legal frame or indexes past the buffer end.
|
||||
@@ -5925,7 +5950,11 @@ mod tests {
|
||||
ebml::write_binary(&mut group, ebml::DISCARD_PADDING, &[0; 9]).unwrap();
|
||||
ebml::write_binary(&mut cluster, ebml::BLOCK_GROUP, &group).unwrap();
|
||||
let bytes = mkv_with_tracks_and_cluster(&[TrackSpec::new(1, 2)], &cluster);
|
||||
assert!(MkvStream::open(Cursor::new(bytes)).unwrap().read().is_err());
|
||||
let e = MkvStream::open(Cursor::new(bytes))
|
||||
.unwrap()
|
||||
.read()
|
||||
.unwrap_err();
|
||||
assert!(is_mkv_source_invalid(&e), "{e:?}");
|
||||
}
|
||||
|
||||
fn av_frame(track: usize, pts: i64, keyframe: bool, data: Vec<u8>) -> crate::pes::PesFrame {
|
||||
|
||||
@@ -1124,8 +1124,10 @@ mod tests {
|
||||
.unwrap();
|
||||
tx.send(DemuxBatch::Eof).unwrap();
|
||||
let mut out = Vec::new();
|
||||
let mut pts = Vec::new();
|
||||
while let Some(f) = stream.read().unwrap() {
|
||||
out.push(f.data);
|
||||
pts.push(f.pts);
|
||||
}
|
||||
assert_eq!(
|
||||
out,
|
||||
@@ -1135,6 +1137,8 @@ mod tests {
|
||||
au[4].clone()
|
||||
]
|
||||
);
|
||||
// A merged pair keeps its FIRST field's PTS; only the PTS-less frame reads 0.
|
||||
assert_eq!(pts, [9_000, 12_003, 0]);
|
||||
}
|
||||
|
||||
// Single-video-stream title on `codec` + CountingParser; feeds three 0xE0
|
||||
|
||||
@@ -497,6 +497,16 @@ impl ScsiTransport for SptiTransport {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// A drive root like `D:\` must open the volume, not the root directory.
|
||||
#[test]
|
||||
fn normalize_device_path_maps_drive_forms_to_the_volume_path() {
|
||||
assert_eq!(normalize_device_path("D:"), r"\\.\D:");
|
||||
assert_eq!(normalize_device_path(r"D:\"), r"\\.\D:");
|
||||
assert_eq!(normalize_device_path(r"\\.\D:"), r"\\.\D:");
|
||||
assert_eq!(normalize_device_path(r"\\.\CdRom0"), r"\\.\CdRom0");
|
||||
assert_eq!(normalize_device_path("CdRom0"), r"\\.\CdRom0");
|
||||
}
|
||||
|
||||
// Regression guard: `StorageAdapterDescriptor` must match `STORAGE_ADAPTER_DESCRIPTOR`
|
||||
// (winioctl.h) field-for-field.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user