mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
dvd: the unit walk stops on Stop; duration and chapter names follow the chapter marks
The interleaved-unit walk polls halt before each read, passes a drive's Halted up instead of taking it for an unreadable chain, gives the cell back whole on a short read rather than reparsing the last pack, and cannot overflow on a cell spanning every sector. A title's duration no longer subtracts the dropped leading angle pieces: the PGC time never counted them, and the cell-sum fallback now counts an angle block once, as the chapter marks do. Text-data chapter names are placed by the part-of-title table on the program each chapter starts at; names that cannot be placed one per program are dropped. A text-data name is looked for within its longest accepted length, not to the end of the table.
This commit is contained in:
+299
-36
@@ -24,43 +24,80 @@ const DSI_NEXT_ILVU_SA: usize = DSI_DATA + 32 + 6;
|
||||
// SML_PBI's "no next unit": 0xFFFFFFFF by the book, and discs also write 0x7FFFFFFF.
|
||||
const NO_NEXT_ILVU: u32 = 0x7FFF_FFFF;
|
||||
|
||||
// The sector ranges an interleaved cell plays: its own interleaved units, found by following
|
||||
// each unit's navigation pack. The chain runs per VOB, past the cell, so it is clipped to the
|
||||
// cell. `None` when a pack does not read or parse, which leaves the cell's range to the caller.
|
||||
// An interleaved cell's own units, following each unit's nav pack, clipped to the cell (at most
|
||||
// one read per cell sector, `halt` polled before each). `Ok(None)` when a pack does not read
|
||||
// whole or parse, which leaves the caller the cell's range; a Stop is `Err(Halted)`.
|
||||
fn interleaved_units(
|
||||
reader: &mut dyn SectorSource,
|
||||
vob_start: u32,
|
||||
cell: &ifo::DvdCell,
|
||||
) -> Option<Vec<Extent>> {
|
||||
let mut buf = vec![0u8; 2048];
|
||||
halt: Option<&crate::halt::Halt>,
|
||||
) -> Result<Option<Vec<Extent>>> {
|
||||
let mut buf = vec![0u8; crate::consts::SECTOR_BYTES];
|
||||
let mut at = cell.first_sector;
|
||||
let mut units = Vec::new();
|
||||
while at <= cell.last_sector {
|
||||
reader
|
||||
.read_sectors(vob_start.checked_add(at)?, 1, &mut buf, false)
|
||||
.ok()?;
|
||||
if halt.is_some_and(|h| h.is_cancelled()) {
|
||||
return Err(Error::Halted);
|
||||
}
|
||||
let Some(lba) = vob_start.checked_add(at) else {
|
||||
return Ok(None);
|
||||
};
|
||||
match reader.read_sectors(lba, 1, &mut buf, false) {
|
||||
Ok(n) if n >= buf.len() => {}
|
||||
Err(Error::Halted) => return Err(Error::Halted),
|
||||
Ok(_) | Err(_) => return Ok(None),
|
||||
}
|
||||
let is_nav = buf[..4] == [0, 0, 1, 0xBA]
|
||||
&& buf[0x400..0x404] == [0, 0, 1, 0xBF]
|
||||
&& buf[0x406] == 0x01;
|
||||
if !is_nav {
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
let word = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]);
|
||||
let (end_rel, next_rel) = (word(DSI_ILVU_EA), word(DSI_NEXT_ILVU_SA));
|
||||
if end_rel == 0 {
|
||||
return None;
|
||||
}
|
||||
let end = at.checked_add(end_rel)?.min(cell.last_sector);
|
||||
let end = at.saturating_add(end_rel).min(cell.last_sector);
|
||||
let Some(sector_count) = (end - at).checked_add(1).filter(|_| end_rel != 0) else {
|
||||
return Ok(None);
|
||||
};
|
||||
units.push(Extent {
|
||||
start_lba: vob_start.checked_add(at)?,
|
||||
sector_count: end - at + 1,
|
||||
start_lba: lba,
|
||||
sector_count,
|
||||
});
|
||||
if next_rel >= NO_NEXT_ILVU || next_rel == 0 {
|
||||
break;
|
||||
}
|
||||
at = at.checked_add(next_rel)?;
|
||||
let Some(next) = at.checked_add(next_rel) else {
|
||||
break;
|
||||
};
|
||||
at = next;
|
||||
}
|
||||
(!units.is_empty()).then_some(units)
|
||||
Ok((!units.is_empty()).then_some(units))
|
||||
}
|
||||
|
||||
// Chapter (part of title) names moved onto the `programs` program marks: each part names the
|
||||
// program it starts at. `None` when that is not one name per program at most (a part in
|
||||
// another PGC, two parts on one program, a part past the marks).
|
||||
fn program_names(
|
||||
names: &[Option<String>],
|
||||
title: &ifo::DvdTitle,
|
||||
programs: usize,
|
||||
) -> Option<Vec<Option<String>>> {
|
||||
let Some(parts) = title.ptt_programs.as_deref() else {
|
||||
// No part table: parts are taken as programs 1, 2, ...
|
||||
return Some(names.to_vec());
|
||||
};
|
||||
if parts.len() != names.len() {
|
||||
return None;
|
||||
}
|
||||
let mut out: Vec<Option<Option<String>>> = vec![None; programs];
|
||||
for (&pgn, name) in parts.iter().zip(names) {
|
||||
let slot = out.get_mut(usize::from(pgn).checked_sub(1)?)?;
|
||||
if slot.replace(name.clone()).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(out.into_iter().map(Option::flatten).collect())
|
||||
}
|
||||
|
||||
impl Disc {
|
||||
@@ -209,10 +246,7 @@ impl Disc {
|
||||
extents.push(whole);
|
||||
continue;
|
||||
}
|
||||
if halt.is_some_and(|h| h.is_cancelled()) {
|
||||
return Err(Error::Halted);
|
||||
}
|
||||
match interleaved_units(reader, ts.vob_start_sector, cell) {
|
||||
match interleaved_units(reader, ts.vob_start_sector, cell, halt)? {
|
||||
Some(units) => extents.extend(units),
|
||||
None => {
|
||||
tracing::warn!(
|
||||
@@ -295,10 +329,12 @@ impl Disc {
|
||||
.collect();
|
||||
let in_head = shifted.iter().filter(|&&t| t <= 0.0).count();
|
||||
let skipped = in_head.saturating_sub(1);
|
||||
// Disc text names index the chapter before the head was dropped.
|
||||
// Disc text names each chapter (part of title); a mark is a program, indexed
|
||||
// from before the head was dropped.
|
||||
let names = dvd_info
|
||||
.chapter_names
|
||||
.get(&(ts.vts_number, dvd_title.vts_title_num));
|
||||
.get(&(ts.vts_number, dvd_title.vts_title_num))
|
||||
.and_then(|n| program_names(n, dvd_title, shifted.len()));
|
||||
let chapters: Vec<Chapter> = shifted
|
||||
.into_iter()
|
||||
.skip(skipped)
|
||||
@@ -306,6 +342,7 @@ impl Disc {
|
||||
.map(|(i, time_secs)| Chapter {
|
||||
time_secs,
|
||||
name: names
|
||||
.as_ref()
|
||||
.and_then(|n| n.get(skipped + i).cloned().flatten())
|
||||
.unwrap_or_else(|| chapter_name(i)),
|
||||
})
|
||||
@@ -314,7 +351,8 @@ impl Disc {
|
||||
titles.push(DiscTitle {
|
||||
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
|
||||
playlist_id: title_number,
|
||||
duration_secs: (dvd_title.duration_secs - dropped_secs).max(0.0),
|
||||
// The dropped head is angle pieces, which the title time already leaves out.
|
||||
duration_secs: dvd_title.duration_secs,
|
||||
size_bytes,
|
||||
clips: Vec::new(),
|
||||
streams,
|
||||
@@ -2101,7 +2139,9 @@ mod tests {
|
||||
nav_pack(&mut disc, vob + 100, 9, 30); // unit 100..=109, next at 130
|
||||
nav_pack(&mut disc, vob + 130, 4, 20); // unit 130..=134, next at 150
|
||||
nav_pack(&mut disc, vob + 150, 9, end_marker); // unit 150..=159, last
|
||||
let units = super::interleaved_units(&mut disc, vob, &icell(100, 159)).unwrap();
|
||||
let units = super::interleaved_units(&mut disc, vob, &icell(100, 159), None)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let spans: Vec<(u32, u32)> = units
|
||||
.iter()
|
||||
.map(|e| (e.start_lba, e.sector_count))
|
||||
@@ -2116,9 +2156,102 @@ mod tests {
|
||||
fn an_interleaved_cell_unit_walk_stays_inside_the_cell() {
|
||||
let mut disc = MemDisc::new();
|
||||
nav_pack(&mut disc, 100, 50, 0x7FFF_FFFF);
|
||||
let units = super::interleaved_units(&mut disc, 0, &icell(100, 120)).unwrap();
|
||||
let units = super::interleaved_units(&mut disc, 0, &icell(100, 120), None)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!((units[0].start_lba, units[0].sector_count), (100, 21));
|
||||
assert!(super::interleaved_units(&mut disc, 0, &icell(500, 600)).is_none());
|
||||
assert!(
|
||||
super::interleaved_units(&mut disc, 0, &icell(500, 600), None)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
// A source that reads `inner`, cancels `halt` after `cancel_after` reads, and answers a
|
||||
// read at or past `short_from` with Ok(0) and the buffer untouched.
|
||||
struct WalkReader<'a> {
|
||||
inner: &'a mut MemDisc,
|
||||
halt: crate::halt::Halt,
|
||||
cancel_after: u32,
|
||||
short_from: u32,
|
||||
reads: u32,
|
||||
}
|
||||
impl SectorSource for WalkReader<'_> {
|
||||
fn read_sectors(
|
||||
&mut self,
|
||||
lba: u32,
|
||||
count: u16,
|
||||
buf: &mut [u8],
|
||||
recovery: bool,
|
||||
) -> crate::error::Result<usize> {
|
||||
self.reads += 1;
|
||||
if self.reads >= self.cancel_after {
|
||||
self.halt.cancel();
|
||||
}
|
||||
if lba >= self.short_from {
|
||||
return Ok(0);
|
||||
}
|
||||
self.inner.read_sectors(lba, count, buf, recovery)
|
||||
}
|
||||
}
|
||||
|
||||
fn three_unit_chain() -> MemDisc {
|
||||
let mut disc = MemDisc::new();
|
||||
nav_pack(&mut disc, 100, 9, 30);
|
||||
nav_pack(&mut disc, 130, 4, 20);
|
||||
nav_pack(&mut disc, 150, 9, 0xFFFF_FFFF);
|
||||
disc
|
||||
}
|
||||
|
||||
// A Stop during the unit walk ends it as Halted, whether raised between reads or by the
|
||||
// drive's own read; neither is mistaken for an unreadable chain.
|
||||
#[test]
|
||||
fn an_interleaved_unit_walk_stops_on_halt() {
|
||||
let mut disc = three_unit_chain();
|
||||
let halt = crate::halt::Halt::new();
|
||||
let mut reader = WalkReader {
|
||||
inner: &mut disc,
|
||||
halt: halt.clone(),
|
||||
cancel_after: 1,
|
||||
short_from: u32::MAX,
|
||||
reads: 0,
|
||||
};
|
||||
let walk = super::interleaved_units(&mut reader, 0, &icell(100, 159), Some(&halt));
|
||||
assert!(matches!(walk, Err(Error::Halted)), "{walk:?}");
|
||||
assert_eq!(reader.reads, 1, "no read after the Stop");
|
||||
|
||||
let mut disc = three_unit_chain();
|
||||
let mut drive = HaltingReader {
|
||||
inner: &mut disc,
|
||||
halt_at: 130,
|
||||
};
|
||||
let walk = super::interleaved_units(&mut drive, 0, &icell(100, 159), None);
|
||||
assert!(matches!(walk, Err(Error::Halted)), "{walk:?}");
|
||||
}
|
||||
|
||||
// A read that returns short leaves the last pack in the buffer: the walk gives the cell
|
||||
// back whole rather than reparse it as the next unit.
|
||||
#[test]
|
||||
fn a_short_read_in_the_unit_walk_is_unreadable() {
|
||||
let mut disc = three_unit_chain();
|
||||
let mut reader = WalkReader {
|
||||
inner: &mut disc,
|
||||
halt: crate::halt::Halt::new(),
|
||||
cancel_after: u32::MAX,
|
||||
short_from: 130,
|
||||
reads: 0,
|
||||
};
|
||||
let walk = super::interleaved_units(&mut reader, 0, &icell(100, 159), None);
|
||||
assert!(matches!(walk, Ok(None)), "{walk:?}");
|
||||
}
|
||||
|
||||
// A cell spanning the whole sector space whose first unit claims all of it: no overflow.
|
||||
#[test]
|
||||
fn an_interleaved_unit_spanning_every_sector_does_not_overflow() {
|
||||
let mut disc = MemDisc::new();
|
||||
nav_pack(&mut disc, 0, u32::MAX, 0xFFFF_FFFF);
|
||||
let walk = super::interleaved_units(&mut disc, 0, &icell(0, u32::MAX), None);
|
||||
assert!(matches!(walk, Ok(None)), "{walk:?}");
|
||||
}
|
||||
|
||||
/// A chapter named in the VMG text data reaches Chapter.name in place of
|
||||
@@ -2193,7 +2326,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// A leading interleaved-angle sub-block cell (category 0x90) must be dropped from muxed
|
||||
// extents; chapters shift earlier by its duration.
|
||||
// extents; chapter marks never counted its time.
|
||||
#[test]
|
||||
fn scan_dvd_titles_drops_leading_scene_index_cell() {
|
||||
let mut disc = MemDisc::new();
|
||||
@@ -2236,18 +2369,17 @@ mod tests {
|
||||
// First extent starts at the feature cell (vob 1000 + 100), not at 1000+0.
|
||||
assert_eq!(t.extents[0].start_lba, 9000 + 1000 + 100); // ifo_lba + vtstt + first
|
||||
assert_eq!(t.extents[1].start_lba, 9000 + 1000 + 300);
|
||||
// Chapter times shift earlier by the dropped 5s. Program 0 was at the
|
||||
// dropped head (clamped to 0); program 1 was at cell 3 =
|
||||
// dur(cell0)+dur(cell1) = 5 + 59 = 64s, now 59s after the 5s shift.
|
||||
// The 0x90 head cell is an angle piece, which chapter timing gives no time: program 0
|
||||
// starts at 0 and program 1 at cell 3 = dur(cell1) = 59s.
|
||||
assert_eq!(t.chapters.len(), 2);
|
||||
assert!(
|
||||
(t.chapters[0].time_secs - 0.0).abs() < 0.01,
|
||||
"ch0 clamped to 0, got {}",
|
||||
"ch0 at 0, got {}",
|
||||
t.chapters[0].time_secs
|
||||
);
|
||||
assert!(
|
||||
(t.chapters[1].time_secs - 59.0).abs() < 0.01,
|
||||
"ch1 shifted by dropped 5s → 59s, got {}",
|
||||
"ch1 at 59s, got {}",
|
||||
t.chapters[1].time_secs
|
||||
);
|
||||
}
|
||||
@@ -2335,9 +2467,22 @@ mod tests {
|
||||
}
|
||||
|
||||
fn scan_cells(cells: &[(u32, u32, u8, u8)], programs: &[u8], nchap: u16) -> DiscTitle {
|
||||
scan_vts(build_vts_cells(1000, 0x00, cells, programs), nchap)
|
||||
}
|
||||
|
||||
fn scan_vts(vts: Vec<u8>, nchap: u16) -> DiscTitle {
|
||||
scan_vts_named(vts, nchap, &[])
|
||||
}
|
||||
|
||||
// `scan_vts` with VMG text data naming the title's chapters `names` (none: no text data).
|
||||
fn scan_vts_named(vts: Vec<u8>, nchap: u16, names: &[&[u8]]) -> DiscTitle {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(nchap, 1, 1)]);
|
||||
let vts = build_vts_cells(1000, 0x00, cells, programs);
|
||||
let mut vmg = build_vmg(&[(nchap, 1, 1)]);
|
||||
if !names.is_empty() {
|
||||
let mut items: Vec<(u8, &[u8])> = vec![(0x02, b"feature")];
|
||||
items.extend(names.iter().map(|n| (0x04, *n)));
|
||||
vmg = ifo::with_txtdt(vmg, &ifo::build_txtdt_mg(0x11, &items));
|
||||
}
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
@@ -2361,7 +2506,124 @@ mod tests {
|
||||
.remove(0)
|
||||
}
|
||||
|
||||
// Dropped head time comes off the duration, and head chapters collapse to one mark at 0.
|
||||
// Adds a VTS_PTT_SRPT to a `build_vts_cells` IFO: title 1's chapters, (PGCN, PGN) each.
|
||||
fn with_ptt(mut vts: Vec<u8>, parts: &[(u16, u16)]) -> Vec<u8> {
|
||||
let at = 3 * 2048;
|
||||
vts[0xC8..0xCC].copy_from_slice(&3u32.to_be_bytes());
|
||||
vts[at..at + 2].copy_from_slice(&1u16.to_be_bytes());
|
||||
let last = (12 + parts.len() * 4 - 1) as u32;
|
||||
vts[at + 4..at + 8].copy_from_slice(&last.to_be_bytes());
|
||||
vts[at + 8..at + 12].copy_from_slice(&12u32.to_be_bytes());
|
||||
for (i, (pgcn, pgn)) in parts.iter().enumerate() {
|
||||
let e = at + 12 + i * 4;
|
||||
vts[e..e + 2].copy_from_slice(&pgcn.to_be_bytes());
|
||||
vts[e + 2..e + 4].copy_from_slice(&pgn.to_be_bytes());
|
||||
}
|
||||
vts
|
||||
}
|
||||
|
||||
fn names(t: &DiscTitle) -> Vec<&str> {
|
||||
t.chapters.iter().map(|c| c.name.as_str()).collect()
|
||||
}
|
||||
|
||||
const FOUR_CELLS: [(u32, u32, u8, u8); 4] = [
|
||||
(0, 99, 0x00, 0x10),
|
||||
(100, 199, 0x00, 0x10),
|
||||
(200, 299, 0x00, 0x10),
|
||||
(300, 399, 0x00, 0x10),
|
||||
];
|
||||
|
||||
// Chapter names follow the part-of-title table: two named chapters starting at programs
|
||||
// 1 and 3 name those marks, and the marks between keep their ordinals.
|
||||
#[test]
|
||||
fn scan_dvd_titles_names_the_program_each_chapter_starts_at() {
|
||||
let vts = with_ptt(
|
||||
build_vts_cells(1000, 0x00, &FOUR_CELLS, &[1, 2, 3, 4]),
|
||||
&[(1, 1), (1, 3)],
|
||||
);
|
||||
let t = scan_vts_named(vts, 2, &[b"Opening", b"Finale"]);
|
||||
assert_eq!(names(&t), ["Opening", "2", "Finale", "4"]);
|
||||
}
|
||||
|
||||
// A chapter in another PGC, or two on one program, cannot be placed: no names.
|
||||
#[test]
|
||||
fn scan_dvd_titles_drops_names_it_cannot_place() {
|
||||
for parts in [[(1, 1), (2, 1)], [(1, 2), (1, 2)]] {
|
||||
let vts = with_ptt(
|
||||
build_vts_cells(1000, 0x00, &FOUR_CELLS, &[1, 2, 3, 4]),
|
||||
&parts,
|
||||
);
|
||||
let t = scan_vts_named(vts, 2, &[b"Opening", b"Finale"]);
|
||||
assert_eq!(names(&t), ["1", "2", "3", "4"], "{parts:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// Names index the marks from before the head collapsed: the first kept mark is program 3.
|
||||
#[test]
|
||||
fn scan_dvd_titles_names_skip_the_collapsed_head() {
|
||||
let cells = [
|
||||
(0, 9, 0x90, 0x05),
|
||||
(10, 19, 0x90, 0x05),
|
||||
(100, 199, 0x00, 0x20),
|
||||
(300, 399, 0x00, 0x20),
|
||||
];
|
||||
let parts = [(1, 1), (1, 2), (1, 3), (1, 4)];
|
||||
let vts = with_ptt(build_vts_cells(1000, 0x00, &cells, &[1, 2, 3, 4]), &parts);
|
||||
let t = scan_vts_named(vts, 4, &[b"Logo", b"Index", b"Main", b"End"]);
|
||||
assert_eq!(names(&t), ["Main", "End"]);
|
||||
}
|
||||
|
||||
// Sets the PGC playback time (BCD seconds) of a `build_vts_cells` IFO.
|
||||
fn with_pgc_secs(mut vts: Vec<u8>, bcd_secs: u8) -> Vec<u8> {
|
||||
vts[2 * 2048 + 0x100 + 6] = bcd_secs;
|
||||
vts
|
||||
}
|
||||
|
||||
// The PGC's own playback time counts an angle block once: the dropped leading angle
|
||||
// pieces are not in it, so nothing comes off it, and the last chapter mark stays inside.
|
||||
#[test]
|
||||
fn scan_dvd_titles_pgc_time_is_not_cut_by_the_dropped_head() {
|
||||
let cells = [
|
||||
(0, 9, 0x90, 0x05),
|
||||
(10, 19, 0x90, 0x05),
|
||||
(100, 199, 0x00, 0x20),
|
||||
(300, 399, 0x00, 0x20),
|
||||
];
|
||||
let vts = with_pgc_secs(build_vts_cells(1000, 0x00, &cells, &[3, 4]), 0x40);
|
||||
let t = scan_vts(vts, 2);
|
||||
assert!(
|
||||
(t.duration_secs - 40.0).abs() < 0.01,
|
||||
"got {}",
|
||||
t.duration_secs
|
||||
);
|
||||
assert!((t.chapters[1].time_secs - 20.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
// With no PGC time, the duration summed from cells leaves out an angle block's other
|
||||
// angles wherever the block sits, as the chapter marks do.
|
||||
#[test]
|
||||
fn scan_dvd_titles_cell_sum_counts_an_angle_block_once() {
|
||||
let t = scan_cells(
|
||||
&[
|
||||
(0, 99, 0x00, 0x20),
|
||||
(100, 199, 0x50, 0x10),
|
||||
(200, 299, 0x90, 0x10),
|
||||
(300, 399, 0xD0, 0x10),
|
||||
(400, 499, 0x00, 0x20),
|
||||
],
|
||||
&[1, 5],
|
||||
2,
|
||||
);
|
||||
assert!(
|
||||
(t.duration_secs - 50.0).abs() < 0.01,
|
||||
"got {}",
|
||||
t.duration_secs
|
||||
);
|
||||
assert!((t.chapters[1].time_secs - 30.0).abs() < 0.01);
|
||||
assert_eq!(t.extents.len(), 3, "one angle of the block is read");
|
||||
}
|
||||
|
||||
// The dropped head adds no time to the duration, and head chapters collapse to one mark at 0.
|
||||
#[test]
|
||||
fn scan_dvd_titles_dropped_head_adjusts_duration_and_chapters() {
|
||||
let t = scan_cells(
|
||||
@@ -2434,6 +2696,7 @@ mod tests {
|
||||
spst_ctl: [0; 32],
|
||||
vts_title_num,
|
||||
pgcs,
|
||||
ptt_programs: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+93
-31
@@ -70,6 +70,9 @@ pub struct DvdTitle {
|
||||
/// Distinct PGCs this title's part-of-title entries point at. Only the first is read,
|
||||
/// so a title with more is missing the rest.
|
||||
pub pgcs: usize,
|
||||
/// The program (PGN) each chapter (part of title) starts at, from VTS_PTT_SRPT. `None`
|
||||
/// when the table does not hold the title; empty when a chapter lies in another PGC.
|
||||
pub ptt_programs: Option<Vec<u16>>,
|
||||
}
|
||||
|
||||
/// A cell — contiguous sector range within a VOB.
|
||||
@@ -651,7 +654,9 @@ fn txtdt_titles(vmg: &[u8]) -> Option<Vec<Vec<Option<String>>>> {
|
||||
// One tab- or NUL-terminated name. Unknown charsets pass only plain ASCII; empty,
|
||||
// overlong or control-bearing names are `None` so the caller keeps its fallback.
|
||||
fn txtdt_name(data: &[u8], at: usize, charset: u8) -> Option<String> {
|
||||
// Never look past the longest name: thousands of items can share one unterminated run.
|
||||
let raw = data.get(at..)?;
|
||||
let raw = &raw[..raw.len().min(TXTDT_MAX_NAME + 1)];
|
||||
let raw = &raw[..raw
|
||||
.iter()
|
||||
.position(|&b| b == b'\t' || b == 0)
|
||||
@@ -1019,38 +1024,46 @@ fn ptt_srpt_pgc_index(data: &[u8], ptt_offset: usize, ttn: u8) -> Option<usize>
|
||||
pgcn.checked_sub(1)
|
||||
}
|
||||
|
||||
// Title `ttn`'s part-of-title entries, (PGCN, PGN) each, in chapter order; `None` when the
|
||||
// table is absent or does not hold the title.
|
||||
fn ptt_srpt_parts(data: &[u8], ptt_offset: usize, ttn: u8) -> Option<Vec<(u16, u16)>> {
|
||||
if ptt_offset == 0 || ttn == 0 {
|
||||
return None;
|
||||
}
|
||||
let count = be_u16(data, ptt_offset).ok()? as usize;
|
||||
let last_byte = be_u32(data, ptt_offset + 4).ok()? as usize;
|
||||
if ttn as usize > count {
|
||||
return None;
|
||||
}
|
||||
let at = |t: usize| {
|
||||
be_u32(data, ptt_offset + 8 + (t - 1) * 4)
|
||||
.ok()
|
||||
.map(|r| r as usize)
|
||||
};
|
||||
let start = at(ttn as usize)?;
|
||||
let end = if (ttn as usize) < count {
|
||||
at(ttn as usize + 1)?
|
||||
} else {
|
||||
last_byte.checked_add(1)?
|
||||
};
|
||||
let mut parts = Vec::new();
|
||||
let mut rel = start;
|
||||
while rel.checked_add(4)? <= end {
|
||||
let entry = ptt_offset.checked_add(rel)?;
|
||||
parts.push((be_u16(data, entry).ok()?, be_u16(data, entry + 2).ok()?));
|
||||
rel += 4;
|
||||
}
|
||||
Some(parts)
|
||||
}
|
||||
|
||||
// Distinct PGCs title `ttn`'s part-of-title entries point at. A title split across
|
||||
// PGCs (one per chapter) is read from its first PGC only; the caller warns on > 1.
|
||||
fn ptt_srpt_pgc_count(data: &[u8], ptt_offset: usize, ttn: u8) -> usize {
|
||||
let entries = || -> Option<Vec<u16>> {
|
||||
let count = be_u16(data, ptt_offset).ok()? as usize;
|
||||
let last_byte = be_u32(data, ptt_offset + 4).ok()? as usize;
|
||||
if ttn as usize > count {
|
||||
return None;
|
||||
}
|
||||
let at = |t: usize| {
|
||||
be_u32(data, ptt_offset + 8 + (t - 1) * 4)
|
||||
.ok()
|
||||
.map(|r| r as usize)
|
||||
};
|
||||
let start = at(ttn as usize)?;
|
||||
let end = if (ttn as usize) < count {
|
||||
at(ttn as usize + 1)?
|
||||
} else {
|
||||
last_byte.checked_add(1)?
|
||||
};
|
||||
let mut pgcns = Vec::new();
|
||||
let mut rel = start;
|
||||
while rel.checked_add(4)? <= end {
|
||||
pgcns.push(be_u16(data, ptt_offset.checked_add(rel)?).ok()?);
|
||||
rel += 4;
|
||||
}
|
||||
Some(pgcns)
|
||||
};
|
||||
if ptt_offset == 0 || ttn == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut pgcns = entries().unwrap_or_default();
|
||||
let mut pgcns: Vec<u16> = ptt_srpt_parts(data, ptt_offset, ttn)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(pgcn, _)| pgcn)
|
||||
.collect();
|
||||
pgcns.sort_unstable();
|
||||
pgcns.dedup();
|
||||
pgcns.len()
|
||||
@@ -1098,6 +1111,14 @@ pub(crate) fn parse_pgcit(
|
||||
|
||||
// Warned once per disc by the DVD scan, which sees every title set.
|
||||
let pgcs = ptt_srpt_pgc_count(data, ptt_offset, vts_title_num);
|
||||
// The program each chapter starts at, when every chapter lies in this PGC.
|
||||
let ptt_programs = ptt_srpt_parts(data, ptt_offset, vts_title_num).map(|parts| match parts
|
||||
.iter()
|
||||
.all(|&(pgcn, _)| usize::from(pgcn) == pgc_index + 1)
|
||||
{
|
||||
true => parts.into_iter().map(|(_, pgn)| pgn).collect(),
|
||||
false => Vec::new(),
|
||||
});
|
||||
|
||||
let entry_offset = entries_start + pgc_index * 8;
|
||||
if entry_offset + 8 > data.len() {
|
||||
@@ -1124,6 +1145,7 @@ pub(crate) fn parse_pgcit(
|
||||
// dropped sibling PGC (position in `titles` is not vts_title_num).
|
||||
title.vts_title_num = vts_title_num;
|
||||
title.pgcs = pgcs;
|
||||
title.ptt_programs = ptt_programs;
|
||||
titles.push(title);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1205,9 +1227,15 @@ pub(crate) fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result
|
||||
);
|
||||
}
|
||||
|
||||
// Recalculate duration from cell times if PGC-level time is zero.
|
||||
// Recalculate duration from cell times if PGC-level time is zero. Like the PGC time and
|
||||
// the chapter marks, it counts an angle block once: its other angles' cells add nothing
|
||||
// (unless every cell is one, which `leading_secondary_cells` also keeps whole).
|
||||
let duration_secs = if duration_secs == 0.0 && !cells.is_empty() {
|
||||
cells.iter().map(|c| c.duration_secs).sum()
|
||||
let primary = |c: &&DvdCell| !CellCategory::decode(c.category).is_secondary_block_piece();
|
||||
match cells.iter().any(|c| primary(&c)) {
|
||||
true => cells.iter().filter(primary).map(|c| c.duration_secs).sum(),
|
||||
false => cells.iter().map(|c| c.duration_secs).sum(),
|
||||
}
|
||||
} else {
|
||||
duration_secs
|
||||
};
|
||||
@@ -1310,6 +1338,7 @@ pub(crate) fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result
|
||||
// Set by the caller (parse_pgcit) which knows the TT_SRPT title number.
|
||||
vts_title_num: 0,
|
||||
pgcs: 0,
|
||||
ptt_programs: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2307,6 +2336,7 @@ mod tests {
|
||||
spst_ctl: [0; 32],
|
||||
vts_title_num: 0,
|
||||
pgcs: 1,
|
||||
ptt_programs: None,
|
||||
};
|
||||
assert_eq!(t.feature_start_cell(), 0);
|
||||
assert_eq!(t.feature_cells().len(), 3);
|
||||
@@ -2332,6 +2362,7 @@ mod tests {
|
||||
spst_ctl: [0; 32],
|
||||
vts_title_num: 0,
|
||||
pgcs: 1,
|
||||
ptt_programs: None,
|
||||
};
|
||||
assert_eq!(t.feature_start_cell(), 2);
|
||||
let fc = t.feature_cells();
|
||||
@@ -2353,6 +2384,7 @@ mod tests {
|
||||
spst_ctl: [0; 32],
|
||||
vts_title_num: 0,
|
||||
pgcs: 1,
|
||||
ptt_programs: None,
|
||||
};
|
||||
assert_eq!(t.feature_start_cell(), 0);
|
||||
assert_eq!(t.feature_cells().len(), 2);
|
||||
@@ -2371,6 +2403,7 @@ mod tests {
|
||||
spst_ctl: [0; 32],
|
||||
vts_title_num: 0,
|
||||
pgcs: 1,
|
||||
ptt_programs: None,
|
||||
};
|
||||
assert_eq!(t.feature_start_cell(), 0);
|
||||
assert!(t.feature_cells().is_empty());
|
||||
@@ -2979,6 +3012,35 @@ mod tests {
|
||||
assert_eq!(map[&(1, 1)], names(&[None, Some("ascii")]));
|
||||
}
|
||||
|
||||
/// Every item of a full table pointing into one long unterminated run (the item table
|
||||
/// itself, crafted free of TAB and NUL) costs one bounded look, not a scan to the end.
|
||||
#[test]
|
||||
fn txtdt_unterminated_names_are_read_bounded() {
|
||||
let items = usize::from(u16::MAX / 2);
|
||||
let base = TXTDT_MG_HDR + 8 + TXTDT_LU_ITEMS;
|
||||
let mut d = vec![0xFFu8; base + 4 + items * 8 + 1024 * 1024];
|
||||
d[..TXTDT_MG_HDR + 8].fill(0x01);
|
||||
d[TXTDT_MG_HDR + 4..TXTDT_MG_HDR + 8]
|
||||
.copy_from_slice(&((TXTDT_MG_HDR + 8) as u32).to_be_bytes());
|
||||
let last = (d.len() - 1) as u32;
|
||||
d[0x10..0x14].copy_from_slice(&last.to_be_bytes());
|
||||
for i in 0..items {
|
||||
let item = base + 4 + i * 8;
|
||||
d[item] = if i == 0 { TX_TITLE } else { TX_CHAPTER };
|
||||
d[item + 6..item + 8].copy_from_slice(&0x0404u16.to_be_bytes());
|
||||
}
|
||||
let vmg = with_txtdt(vmg_with_tt_srpt(1, &[(2, 1, 1)]), &d);
|
||||
let started = std::time::Instant::now();
|
||||
let titles = txtdt_titles(&vmg).expect("table");
|
||||
assert_eq!(titles[0].len(), items - 1);
|
||||
assert!(
|
||||
titles[0].iter().all(Option::is_none),
|
||||
"overlong names are no names"
|
||||
);
|
||||
let took = started.elapsed();
|
||||
assert!(took < std::time::Duration::from_secs(5), "took {took:?}");
|
||||
}
|
||||
|
||||
/// No text data, bad pointers, bad lengths and truncation yield no names and
|
||||
/// never read out of bounds or panic.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user