bluray: test the extent filter with a multi-extent clip

This commit is contained in:
Matthew Jackson
2026-09-29 19:17:34 -07:00
parent 7c1d2a203b
commit 4c198c7979
2 changed files with 186 additions and 16 deletions
+137 -15
View File
@@ -277,17 +277,7 @@ impl Disc {
// not closed since it's unclear that's always a defect (see hddvd.rs).
if let Some(file_exts) = file_exts {
let span_start = feed_pos;
for (lba, sectors) in file_exts {
if sectors > 0 && lba > 0 {
extents.push(Extent {
start_lba: lba,
sector_count: sectors,
});
feed_pos = feed_pos.saturating_add(
sectors as u64 * crate::consts::SECTOR_BYTES as u64,
);
}
}
push_extents(file_exts, &mut extents, &mut feed_pos);
if feed_pos > span_start {
spans.insert(play_item.clip_id.clone(), (span_start, feed_pos));
}
@@ -521,6 +511,21 @@ impl Disc {
// Decode the five predefined XML entities and numeric char refs in element text, and unwrap a
// CDATA section; unknown or malformed references are kept literally.
/// Append each usable `(lba, sectors)` to `extents`, advancing `feed_pos` by its bytes. An extent
/// with no sectors or at LBA 0 occupies no readable bytes and is skipped without moving `feed_pos`.
fn push_extents(file_exts: Vec<(u32, u32)>, extents: &mut Vec<Extent>, feed_pos: &mut u64) {
for (lba, sectors) in file_exts {
if sectors > 0 && lba > 0 {
extents.push(Extent {
start_lba: lba,
sector_count: sectors,
});
*feed_pos =
feed_pos.saturating_add(sectors as u64 * crate::consts::SECTOR_BYTES as u64);
}
}
}
fn xml_text_decode(raw: &str) -> String {
if let Some(inner) = raw
.strip_prefix("<![CDATA[")
@@ -2786,10 +2791,11 @@ mod tests {
assert_eq!(t.clips[2].feed_span, Some((0, s1)), "repeat reuses span");
}
// A zero-length stream file yields no extent, so the clip has no feed_span and later clips
// start at feed offset 0.
// A clip with no extents gets no feed_span and does not shift later clips' spans. (UDF
// itself yields no entry for a zero-length file, so this is the empty-span guard, not the
// extent filter; the filter is covered by `push_extents_*` below.)
#[test]
fn parse_playlist_empty_extent_is_skipped_without_advancing_feed() {
fn parse_playlist_clip_without_extents_gets_no_feed_span() {
let mut disc = MemDisc::new();
let udf = make_bdmv_fs(
&mut disc,
@@ -2809,11 +2815,127 @@ mod tests {
let t = Disc::parse_playlist(&mut disc, &udf, "00001.mpls", &mpls)
.expect("scan")
.expect("title");
assert_eq!(t.extents.len(), 1, "empty extent must be filtered");
assert_eq!(t.extents.len(), 1);
assert_eq!(t.clips[0].feed_span, None);
assert_eq!(t.clips[1].feed_span, Some((0, 500 * 2048)));
}
// The extent filter: zero-sector and LBA-0 extents are dropped and do not advance feed_pos.
// Each half is pinned separately (a lone `sectors > 0` or lone `lba > 0` must fail).
#[test]
fn push_extents_drops_empty_and_lba_zero_extents_without_advancing() {
let mut extents = Vec::new();
let mut pos = 0u64;
push_extents(
vec![(100, 2), (200, 0), (0, 5), (300, 3)],
&mut extents,
&mut pos,
);
let got: Vec<(u32, u32)> = extents
.iter()
.map(|e| (e.start_lba, e.sector_count))
.collect();
assert_eq!(got, vec![(100, 2), (300, 3)]);
assert_eq!(pos, 5 * 2048);
}
#[test]
fn push_extents_drops_zero_sector_extent_alone() {
let mut extents = Vec::new();
let mut pos = 0u64;
push_extents(vec![(200, 0), (300, 1)], &mut extents, &mut pos);
assert_eq!(extents.len(), 1);
assert_eq!(pos, 2048);
}
#[test]
fn push_extents_drops_lba_zero_extent_alone() {
let mut extents = Vec::new();
let mut pos = 0u64;
push_extents(vec![(0, 4), (300, 1)], &mut extents, &mut pos);
assert_eq!(extents.len(), 1);
assert_eq!(pos, 2048);
}
// A clip whose file has several allocation descriptors: its extents are all kept in
// order, its feed_span covers their sum, and the next clip starts after it. A zero-length
// AD ends the list (ECMA-167 4/12), so a descriptor after it never appears.
#[test]
fn parse_playlist_multi_extent_clip_feed_span_covers_sum() {
let mut disc = MemDisc::new();
let stream = vec![
file_ads(
"00001.m2ts",
100,
&[
(5000, 300 * 2048),
(7000, 200 * 2048),
(0, 0),
(8000, 99 * 2048),
],
true,
),
file("00002.m2ts", 101, 9000, 500 * 2048, true),
];
let clipinf = vec![
file_with("00001.clpi", 102, 6000, build_clpi(4000), false),
file_with("00002.clpi", 103, 6100, build_clpi(2000), false),
];
let mk = |name: &str, icb, dd, files, subdirs| DirSpec {
name: name.to_string(),
icb_lba: icb,
dir_data_lba: dd,
files,
subdirs,
};
let root = mk(
"",
10,
11,
Vec::new(),
vec![mk(
"BDMV",
20,
21,
Vec::new(),
vec![
mk("STREAM", 22, 23, stream, vec![]),
mk("CLIPINF", 24, 25, clipinf, vec![]),
],
)],
);
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
let udf = udf::read_filesystem(&mut disc).expect("fs");
let pi = |c: &[u8; 5], i: u32| PiSpec {
clip_id: *c,
in_time: i * 60 * 45000,
out_time: (i + 1) * 60 * 45000,
};
let mpls = build_mpls(
&[pi(b"00001", 0), pi(b"00002", 1)],
(0, 0, 0, 0, 0, 0, 0, 0),
&[],
&[],
);
let t = Disc::parse_playlist(&mut disc, &udf, "00001.mpls", &mpls)
.expect("scan")
.expect("title");
let got: Vec<(u32, u32)> = t
.extents
.iter()
.map(|e| (e.start_lba, e.sector_count))
.collect();
let ps = udf.partition_start();
assert_eq!(
got,
vec![(ps + 5000, 300), (ps + 7000, 200), (ps + 9000, 500)]
);
let s1 = 500u64 * 2048;
assert_eq!(t.clips[0].feed_span, Some((0, s1)));
assert_eq!(t.clips[1].feed_span, Some((s1, s1 + 500 * 2048)));
}
// Chapter names number the surviving entry marks 1..n, not their original mark index.
#[test]
fn parse_playlist_chapter_names_are_dense_ordinals_after_filter() {
+49 -1
View File
@@ -6026,6 +6026,10 @@ pub(crate) mod fixture {
pub(crate) size: u64,
pub(crate) long_ad: bool,
pub(crate) contents: Vec<u8>,
/// When non-empty, the file's ICB carries exactly these `(partition-relative
/// LBA, byte length)` allocation descriptors (in order) instead of the single
/// `data_lba`/`size` one. A zero length terminates the list, as on a real disc.
pub(crate) ads: Vec<(u32, u32)>,
}
/// A directory node: ICB LBA, FID-list LBA, child files and subdirectories.
@@ -6056,6 +6060,26 @@ pub(crate) mod fixture {
s
}
/// Extended File Entry ICB carrying every `(lba, byte_len)` in `ads` as a recorded AD.
pub(crate) fn build_file_icb_ads(ads: &[(u32, u32)], long_ad: bool) -> [u8; 2048] {
let mut s = [0u8; 2048];
s[0..2].copy_from_slice(&266u16.to_le_bytes());
if long_ad {
s[34..36].copy_from_slice(&1u16.to_le_bytes());
}
let total: u64 = ads.iter().map(|a| a.1 as u64).sum();
s[56..64].copy_from_slice(&total.to_le_bytes());
let ad_size: usize = if long_ad { 16 } else { 8 };
s[212..216].copy_from_slice(&((ads.len() * ad_size) as u32).to_le_bytes());
let mut off = 216;
for &(lba, len) in ads {
s[off..off + 4].copy_from_slice(&(len & 0x3FFF_FFFF).to_le_bytes());
s[off + 4..off + 8].copy_from_slice(&lba.to_le_bytes());
off += ad_size;
}
s
}
fn build_dir_icb(dir_data_lba: u32, dir_data_len: u32) -> [u8; 2048] {
build_file_icb(dir_data_len as u64, dir_data_lba, false)
}
@@ -6099,7 +6123,11 @@ pub(crate) mod fixture {
push_fid(&mut fids, &f.name, f.icb_lba, false, false);
disc.put(
PART_START + f.icb_lba,
build_file_icb(f.size, f.data_lba, f.long_ad),
if f.ads.is_empty() {
build_file_icb(f.size, f.data_lba, f.long_ad)
} else {
build_file_icb_ads(&f.ads, f.long_ad)
},
);
if !f.contents.is_empty() {
disc.put_bytes(PART_START + f.data_lba, &f.contents);
@@ -6159,6 +6187,7 @@ pub(crate) mod fixture {
size,
long_ad,
contents: Vec::new(),
ads: Vec::new(),
}
}
@@ -6176,6 +6205,25 @@ pub(crate) mod fixture {
size: contents.len() as u64,
long_ad,
contents,
ads: Vec::new(),
}
}
/// A file whose ICB lists exactly `ads` as `(partition-relative LBA, byte length)`.
pub(crate) fn file_ads(
name: &str,
icb_lba: u32,
ads: &[(u32, u32)],
long_ad: bool,
) -> FileSpec {
FileSpec {
name: name.to_string(),
icb_lba,
data_lba: 0,
size: 0,
long_ad,
contents: Vec::new(),
ads: ads.to_vec(),
}
}
}