libfreemkv 0.31.2: comprehensive spec-grounded test suite (~950 tests)
Test-hardening release, no runtime changes. Adds spec-grounded unit tests across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers, MKV/EBML container output, the mux pipeline, sector prefetch + decrypt decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each test is grounded in the format spec or real on-disc behavior and verified to fail under a targeted source mutation. No behavior changed.
This commit is contained in:
+595
@@ -148,3 +148,598 @@ impl Disc {
|
||||
titles
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sector::SectorSource;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// In-memory disc + minimal UDF image (single physical partition,
|
||||
// metadata_start == partition_start). Offsets cited against
|
||||
// udf.rs::read_filesystem / ECMA-167.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const PART_START: u32 = 3000;
|
||||
|
||||
struct MemDisc {
|
||||
sectors: HashMap<u32, [u8; 2048]>,
|
||||
}
|
||||
impl MemDisc {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
sectors: HashMap::new(),
|
||||
}
|
||||
}
|
||||
fn put(&mut self, lba: u32, data: [u8; 2048]) {
|
||||
self.sectors.insert(lba, data);
|
||||
}
|
||||
fn put_bytes(&mut self, lba: u32, bytes: &[u8]) {
|
||||
for (i, chunk) in bytes.chunks(2048).enumerate() {
|
||||
let mut s = [0u8; 2048];
|
||||
s[..chunk.len()].copy_from_slice(chunk);
|
||||
self.put(lba + i as u32, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SectorSource for MemDisc {
|
||||
fn read_sectors(
|
||||
&mut self,
|
||||
lba: u32,
|
||||
count: u16,
|
||||
buf: &mut [u8],
|
||||
_recovery: bool,
|
||||
) -> crate::error::Result<usize> {
|
||||
let need = count as usize * 2048;
|
||||
for i in 0..count as u32 {
|
||||
let off = i as usize * 2048;
|
||||
let s = self.sectors.get(&(lba + i)).copied().unwrap_or([0u8; 2048]);
|
||||
buf[off..off + 2048].copy_from_slice(&s);
|
||||
}
|
||||
Ok(need)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended File Entry ICB (tag 266) with one Short AD. info_length@56,
|
||||
/// l_ea@208, l_ad@212, AD len(4)@216 | lba(4)@220.
|
||||
fn build_file_icb(size: u32, data_lba: u32) -> [u8; 2048] {
|
||||
let mut s = [0u8; 2048];
|
||||
s[0..2].copy_from_slice(&266u16.to_le_bytes());
|
||||
s[56..64].copy_from_slice(&(size as u64).to_le_bytes());
|
||||
s[208..212].copy_from_slice(&0u32.to_le_bytes());
|
||||
s[212..216].copy_from_slice(&8u32.to_le_bytes());
|
||||
s[216..220].copy_from_slice(&(size & 0x3FFF_FFFF).to_le_bytes());
|
||||
s[220..224].copy_from_slice(&data_lba.to_le_bytes());
|
||||
s
|
||||
}
|
||||
|
||||
/// One FID (tag 257). file_chars@18, l_fi@19, ICB LBA@24, l_iu@36,
|
||||
/// name@(38). Name compression-id 8 (ASCII).
|
||||
fn push_fid(buf: &mut Vec<u8>, name: &str, icb_lba: u32, is_dir: bool, is_parent: bool) {
|
||||
let start = buf.len();
|
||||
let name_field: Vec<u8> = if is_parent {
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut v = vec![0x08u8];
|
||||
v.extend_from_slice(name.as_bytes());
|
||||
v
|
||||
};
|
||||
let mut fid = vec![0u8; 38];
|
||||
fid[0..2].copy_from_slice(&257u16.to_le_bytes());
|
||||
let mut fc = 0u8;
|
||||
if is_dir {
|
||||
fc |= 0x02;
|
||||
}
|
||||
if is_parent {
|
||||
fc |= 0x08;
|
||||
}
|
||||
fid[18] = fc;
|
||||
fid[19] = name_field.len() as u8;
|
||||
fid[24..28].copy_from_slice(&icb_lba.to_le_bytes());
|
||||
fid[36..38].copy_from_slice(&0u16.to_le_bytes());
|
||||
buf.extend_from_slice(&fid);
|
||||
buf.extend_from_slice(&name_field);
|
||||
let used = buf.len() - start;
|
||||
buf.resize(start + ((used + 3) & !3), 0);
|
||||
}
|
||||
|
||||
struct FileSpec {
|
||||
name: String,
|
||||
icb_lba: u32,
|
||||
data_lba: u32,
|
||||
contents: Vec<u8>,
|
||||
}
|
||||
|
||||
fn build_udf_skeleton(disc: &mut MemDisc, root_icb_lba: u32) {
|
||||
let mut avdp = [0u8; 2048];
|
||||
avdp[0..2].copy_from_slice(&2u16.to_le_bytes());
|
||||
disc.put(256, avdp);
|
||||
let mut pd = [0u8; 2048];
|
||||
pd[0..2].copy_from_slice(&5u16.to_le_bytes());
|
||||
pd[188..192].copy_from_slice(&PART_START.to_le_bytes());
|
||||
disc.put(32, pd);
|
||||
let mut lvd = [0u8; 2048];
|
||||
lvd[0..2].copy_from_slice(&6u16.to_le_bytes());
|
||||
lvd[268..272].copy_from_slice(&1u32.to_le_bytes());
|
||||
disc.put(33, lvd);
|
||||
let mut td = [0u8; 2048];
|
||||
td[0..2].copy_from_slice(&8u16.to_le_bytes());
|
||||
disc.put(34, td);
|
||||
let mut fsd = [0u8; 2048];
|
||||
fsd[0..2].copy_from_slice(&256u16.to_le_bytes());
|
||||
fsd[404..408].copy_from_slice(&root_icb_lba.to_le_bytes());
|
||||
disc.put(PART_START, fsd);
|
||||
}
|
||||
|
||||
/// Build a UDF tree with a single VIDEO_TS directory holding the given
|
||||
/// files, and return the navigable UdfFs over `disc`.
|
||||
fn build_video_ts_fs(disc: &mut MemDisc, files: &[FileSpec]) -> crate::udf::UdfFs {
|
||||
let mut fids = Vec::new();
|
||||
push_fid(&mut fids, "", 50, true, true);
|
||||
for f in files {
|
||||
push_fid(&mut fids, &f.name, f.icb_lba, false, false);
|
||||
disc.put(
|
||||
PART_START + f.icb_lba,
|
||||
build_file_icb(f.contents.len() as u32, f.data_lba),
|
||||
);
|
||||
disc.put_bytes(PART_START + f.data_lba, &f.contents);
|
||||
}
|
||||
// VIDEO_TS dir ICB + data.
|
||||
disc.put(PART_START + 50, build_file_icb(fids.len() as u32, 51));
|
||||
disc.put_bytes(PART_START + 51, &fids);
|
||||
// Root dir referencing VIDEO_TS.
|
||||
let mut root_fids = Vec::new();
|
||||
push_fid(&mut root_fids, "", 10, true, true);
|
||||
push_fid(&mut root_fids, "VIDEO_TS", 50, true, false);
|
||||
disc.put(PART_START + 10, build_file_icb(root_fids.len() as u32, 11));
|
||||
disc.put_bytes(PART_START + 11, &root_fids);
|
||||
build_udf_skeleton(disc, 10);
|
||||
crate::udf::read_filesystem(disc).expect("fs")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// IFO builders (DVD-Video spec). Offsets cited against ifo.rs.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// VMG (VIDEO_TS.IFO): magic "DVDVIDEO-VMG"@0, TT_SRPT sector ptr@0xC4.
|
||||
/// TT_SRPT lives at tt_srpt_sector*2048: num_titles(u16)@0, then 12-byte
|
||||
/// entries from +8. Each entry: num_chapters(u16)@+2, vts_number@+6,
|
||||
/// vts_title_num@+7.
|
||||
fn build_vmg(
|
||||
titles: &[(
|
||||
u16, /*chapters*/
|
||||
u8, /*vts*/
|
||||
u8, /*vts_title*/
|
||||
)],
|
||||
) -> Vec<u8> {
|
||||
// Put TT_SRPT at sector 1 (offset 2048).
|
||||
let tt_srpt_sector = 1u32;
|
||||
let mut d = vec![0u8; 2 * 2048];
|
||||
d[0..12].copy_from_slice(b"DVDVIDEO-VMG");
|
||||
d[0xC4..0xC8].copy_from_slice(&tt_srpt_sector.to_be_bytes());
|
||||
let base = tt_srpt_sector as usize * 2048;
|
||||
d[base..base + 2].copy_from_slice(&(titles.len() as u16).to_be_bytes());
|
||||
for (i, (chapters, vts, vts_title)) in titles.iter().enumerate() {
|
||||
let e = base + 8 + i * 12;
|
||||
d[e + 2..e + 4].copy_from_slice(&chapters.to_be_bytes());
|
||||
d[e + 6] = *vts;
|
||||
d[e + 7] = *vts_title;
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// Cell playback info entry (24 bytes): BCD time@4..8 (unused here),
|
||||
/// first_sector(u32 BE)@8, last_sector(u32 BE)@20.
|
||||
fn write_cell(buf: &mut [u8], off: usize, first_sector: u32, last_sector: u32) {
|
||||
buf[off + 8..off + 12].copy_from_slice(&first_sector.to_be_bytes());
|
||||
buf[off + 20..off + 24].copy_from_slice(&last_sector.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Build a VTS_XX_0.IFO. Layout per ifo.rs:
|
||||
/// magic "DVDVIDEO-VTS"@0
|
||||
/// vob_start_sector(u32 BE)@0xC0
|
||||
/// VTS_PGCIT sector ptr(u32 BE)@0xCC
|
||||
/// video attr byte@0x200
|
||||
/// num_audio(u16 BE)@0x202, audio blocks (8B) @0x204
|
||||
/// num_subs(u16 BE)@0x254, subtitle blocks (6B) @0x256
|
||||
/// PGCIT (at pgcit_sector*2048): num_pgcs(u16)@0, PGC info entries (8B)
|
||||
/// from +8 with PGC byte offset(u32 BE)@+4.
|
||||
/// PGC: nr_programs@0x02, nr_cells@0x03, BCD time@0x04, pgm_map ptr@0xE6,
|
||||
/// cell_playback ptr@0xE8 (both u16 BE rel to PGC start).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_vts(
|
||||
vob_start: u32,
|
||||
video_b0: u8,
|
||||
audio: &[(
|
||||
u8, /*b0 coding/sr*/
|
||||
u8, /*b1 channels*/
|
||||
[u8; 2], /*lang*/
|
||||
)],
|
||||
subs: &[[u8; 2]],
|
||||
cells: &[(u32, u32)],
|
||||
palette_nonzero: bool,
|
||||
) -> Vec<u8> {
|
||||
// Total file: header sector(s) + PGCIT at sector 2.
|
||||
let pgcit_sector = 2u32;
|
||||
let mut d = vec![0u8; 4 * 2048];
|
||||
d[0..12].copy_from_slice(b"DVDVIDEO-VTS");
|
||||
d[0xC0..0xC4].copy_from_slice(&vob_start.to_be_bytes());
|
||||
d[0xCC..0xD0].copy_from_slice(&pgcit_sector.to_be_bytes());
|
||||
d[0x200] = video_b0;
|
||||
d[0x202..0x204].copy_from_slice(&(audio.len() as u16).to_be_bytes());
|
||||
for (i, (b0, b1, lang)) in audio.iter().enumerate() {
|
||||
let a = 0x204 + i * 8;
|
||||
d[a] = *b0;
|
||||
d[a + 1] = *b1;
|
||||
d[a + 2] = lang[0];
|
||||
d[a + 3] = lang[1];
|
||||
}
|
||||
d[0x254..0x256].copy_from_slice(&(subs.len() as u16).to_be_bytes());
|
||||
for (i, lang) in subs.iter().enumerate() {
|
||||
let s = 0x256 + i * 6;
|
||||
d[s + 2] = lang[0];
|
||||
d[s + 3] = lang[1];
|
||||
}
|
||||
|
||||
// PGCIT: one PGC.
|
||||
let pg = pgcit_sector as usize * 2048;
|
||||
d[pg..pg + 2].copy_from_slice(&1u16.to_be_bytes()); // num_pgcs = 1
|
||||
// PGC info entry 0 at pg+8; PGC byte offset (rel to PGCIT) at +4.
|
||||
let pgc_rel: u32 = 0x100; // PGC body 256 bytes into the PGCIT
|
||||
d[pg + 8 + 4..pg + 8 + 8].copy_from_slice(&pgc_rel.to_be_bytes());
|
||||
let pgc = pg + pgc_rel as usize;
|
||||
// Ensure room for PGC (needs >= 0xEA past pgc, plus cell table).
|
||||
d[pgc + 0x02] = 1; // nr_of_programs
|
||||
d[pgc + 0x03] = cells.len() as u8; // nr_of_cells
|
||||
// BCD playback time 00:00:30:00 → 30 s, frame-rate bits 0b01 (25fps)
|
||||
// not needed; keep simple 30s. BCD: hh,mm,ss,frame|rate.
|
||||
d[pgc + 0x04] = 0x00;
|
||||
d[pgc + 0x05] = 0x00;
|
||||
d[pgc + 0x06] = 0x30; // 30 seconds BCD
|
||||
d[pgc + 0x07] = 0b0100_0000; // rate bits = 01 (25fps); 0 frames
|
||||
// pgm map ptr @0xE6, cell playback ptr @0xE8 (rel to PGC start).
|
||||
let cell_tbl_rel: u16 = 0xF0;
|
||||
let pgm_map_rel: u16 = 0xEC;
|
||||
d[pgc + 0xE6..pgc + 0xE8].copy_from_slice(&pgm_map_rel.to_be_bytes());
|
||||
d[pgc + 0xE8..pgc + 0xEA].copy_from_slice(&cell_tbl_rel.to_be_bytes());
|
||||
// Program map: program 0 → first cell 1.
|
||||
d[pgc + pgm_map_rel as usize] = 1;
|
||||
// Cell playback table.
|
||||
let cell_base = pgc + cell_tbl_rel as usize;
|
||||
for (i, (first, last)) in cells.iter().enumerate() {
|
||||
write_cell(&mut d, cell_base + i * 24, *first, *last);
|
||||
}
|
||||
// Palette at PGC+0xA4: 16 × [pad,Y,Cb,Cr]. Non-zero if requested.
|
||||
if palette_nonzero {
|
||||
d[pgc + 0xA4 + 1] = 0x40; // Y of color 0
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// scan_dvd_titles returns empty when VIDEO_TS.IFO can't be parsed
|
||||
/// (dvd.rs: `parse_vmg(...) Err → return Vec::new()`). Never panics.
|
||||
#[test]
|
||||
fn scan_dvd_titles_no_ifo_is_empty() {
|
||||
let mut disc = MemDisc::new();
|
||||
// VIDEO_TS exists but VIDEO_TS.IFO is missing.
|
||||
let udf = build_video_ts_fs(&mut disc, &[]);
|
||||
assert!(Disc::scan_dvd_titles(&mut disc, &udf).is_empty());
|
||||
}
|
||||
|
||||
/// Single VTS, single title, one cell. Extent absolute LBA =
|
||||
/// vob_start + cell.first_sector (dvd.rs); sector_count = last - first
|
||||
/// + 1 (inclusive range); size_bytes = sectors * 2048 (DVD sector).
|
||||
#[test]
|
||||
fn scan_dvd_titles_single_cell_extent_math() {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(1, 1, 1)]); // 1 chapter, VTS 1, title 1
|
||||
// vob_start 1000; one cell sectors [10..=109] → 100 sectors.
|
||||
let vts = build_vts(
|
||||
1000,
|
||||
0x00, // NTSC, 4:3
|
||||
&[],
|
||||
&[],
|
||||
&[(10, 109)],
|
||||
false,
|
||||
);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts,
|
||||
},
|
||||
],
|
||||
);
|
||||
let titles = Disc::scan_dvd_titles(&mut disc, &udf);
|
||||
assert_eq!(titles.len(), 1);
|
||||
let t = &titles[0];
|
||||
assert_eq!(t.extents.len(), 1);
|
||||
// absolute start = vob_start(1000) + first_sector(10) = 1010.
|
||||
assert_eq!(t.extents[0].start_lba, 1010);
|
||||
// inclusive: 109 - 10 + 1 = 100 sectors.
|
||||
assert_eq!(t.extents[0].sector_count, 100);
|
||||
// DVD sector = 2048 bytes.
|
||||
assert_eq!(t.size_bytes, 100 * 2048);
|
||||
// playlist field format VTS_XX_title.VOB; title_number is 1.
|
||||
assert_eq!(t.playlist, "VTS_01_1.VOB");
|
||||
assert_eq!(t.playlist_id, 1);
|
||||
assert_eq!(t.content_format, ContentFormat::MpegPs);
|
||||
}
|
||||
|
||||
/// Multi-cell title: extents preserve cell order and each maps to its
|
||||
/// own (vob_start + first .. last) range. mux reads cells in order.
|
||||
#[test]
|
||||
fn scan_dvd_titles_multi_cell_extents_in_order() {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(2, 1, 1)]);
|
||||
let vts = build_vts(
|
||||
500,
|
||||
0x00,
|
||||
&[],
|
||||
&[],
|
||||
&[(0, 99), (200, 299)], // two cells
|
||||
false,
|
||||
);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts,
|
||||
},
|
||||
],
|
||||
);
|
||||
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
||||
assert_eq!(t.extents.len(), 2);
|
||||
assert_eq!(t.extents[0].start_lba, 500); // 500 + 0
|
||||
assert_eq!(t.extents[0].sector_count, 100);
|
||||
assert_eq!(t.extents[1].start_lba, 700); // 500 + 200
|
||||
assert_eq!(t.extents[1].sector_count, 100);
|
||||
assert_eq!(t.size_bytes, 200 * 2048);
|
||||
}
|
||||
|
||||
/// PAL video standard (b0 low bits == 1) sets FrameRate::F25; NTSC sets
|
||||
/// F29_97 (dvd.rs match on ts.video.standard). The video PID is the
|
||||
/// fixed DVD MPEG-PS video stream id 0xE0.
|
||||
#[test]
|
||||
fn scan_dvd_titles_pal_frame_rate_and_video_pid() {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(1, 1, 1)]);
|
||||
// video b0 low 2 bits = 1 → PAL.
|
||||
let vts = build_vts(0, 0x01, &[], &[], &[(0, 9)], false);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts,
|
||||
},
|
||||
],
|
||||
);
|
||||
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
||||
let v = t
|
||||
.streams
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
Stream::Video(v) => Some(v),
|
||||
_ => None,
|
||||
})
|
||||
.expect("video stream");
|
||||
assert_eq!(v.pid, 0xE0, "DVD video PID is fixed 0xE0");
|
||||
assert_eq!(v.frame_rate, FrameRate::F25, "PAL → 25 fps");
|
||||
assert_eq!(v.resolution, Resolution::R576i, "PAL → 576i");
|
||||
}
|
||||
|
||||
/// AC-3 audio gets sub_stream_id 0x80 → PID routed via dvd_audio_pid
|
||||
/// (dvd.rs uses `a.sub_stream_id.and_then(dvd_audio_pid)`). A mixed
|
||||
/// AC-3 + DTS title must NOT collide: AC-3 → 0x80 base, DTS → 0x88 base.
|
||||
#[test]
|
||||
fn scan_dvd_titles_mixed_audio_codecs_distinct_pids() {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(1, 1, 1)]);
|
||||
// audio b0: coding_mode is (b0 >> 5) & 7. AC-3 = 0 → b0=0x00.
|
||||
// DTS = 6 → b0 = 6<<5 = 0xC0. b1 channels nibble high.
|
||||
let vts = build_vts(
|
||||
0,
|
||||
0x00,
|
||||
&[(0x00, 0x10, *b"en"), (0xC0, 0x50, *b"fr")], // AC-3 eng, DTS fra
|
||||
&[],
|
||||
&[(0, 9)],
|
||||
false,
|
||||
);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts,
|
||||
},
|
||||
],
|
||||
);
|
||||
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
||||
let audios: Vec<_> = t
|
||||
.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
Stream::Audio(a) => Some(a),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(audios.len(), 2);
|
||||
assert_eq!(audios[0].codec, Codec::Ac3);
|
||||
assert_eq!(audios[0].language, "en");
|
||||
assert_eq!(audios[1].codec, Codec::Dts);
|
||||
// PIDs must differ (no 0xBD00 collision).
|
||||
assert_ne!(
|
||||
audios[0].pid, audios[1].pid,
|
||||
"mixed-codec audio must route to distinct PIDs"
|
||||
);
|
||||
}
|
||||
|
||||
/// Subtitle streams map to Codec::DvdSub with palette codec_data when a
|
||||
/// non-zero palette is present (dvd.rs builds codec_data from
|
||||
/// dvd_title.palette). VobSub sub-id 0x20+i.
|
||||
#[test]
|
||||
fn scan_dvd_titles_subtitle_palette_codec_data() {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(1, 1, 1)]);
|
||||
let vts = build_vts(
|
||||
0,
|
||||
0x00,
|
||||
&[],
|
||||
&[*b"en"],
|
||||
&[(0, 9)],
|
||||
true, // non-zero palette
|
||||
);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts,
|
||||
},
|
||||
],
|
||||
);
|
||||
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
||||
let sub = t
|
||||
.streams
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
Stream::Subtitle(s) => Some(s),
|
||||
_ => None,
|
||||
})
|
||||
.expect("subtitle stream");
|
||||
assert_eq!(sub.codec, Codec::DvdSub);
|
||||
assert_eq!(sub.language, "en");
|
||||
assert!(
|
||||
sub.codec_data.is_some(),
|
||||
"non-zero palette must yield codec_data"
|
||||
);
|
||||
}
|
||||
|
||||
/// Multiple titles in one VTS each become their own DiscTitle with a
|
||||
/// monotonically increasing title_number / playlist_id (dvd.rs
|
||||
/// `title_number += 1` per dvd_title). Both share the VTS streams.
|
||||
#[test]
|
||||
fn scan_dvd_titles_numbering_increments_per_title() {
|
||||
let mut disc = MemDisc::new();
|
||||
// Two titles in VTS 1 (title nums 1 and 2). num_pgcs must cover
|
||||
// pgc_index = vts_title - 1, so we need >=2 PGC entries; our
|
||||
// build_vts only emits 1 PGC. So the second title's PGC index (1)
|
||||
// exceeds num_pgcs (1) and is skipped. To exercise numbering we use
|
||||
// two separate VTS sets instead.
|
||||
let vmg = build_vmg(&[(1, 1, 1), (1, 2, 1)]);
|
||||
let vts1 = build_vts(100, 0x00, &[], &[], &[(0, 9)], false);
|
||||
let vts2 = build_vts(200, 0x00, &[], &[], &[(0, 19)], false);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts1,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_02_0.IFO".into(),
|
||||
icb_lba: 64,
|
||||
data_lba: 7000,
|
||||
contents: vts2,
|
||||
},
|
||||
],
|
||||
);
|
||||
let titles = Disc::scan_dvd_titles(&mut disc, &udf);
|
||||
assert_eq!(titles.len(), 2);
|
||||
// title_number is a running counter across all title sets.
|
||||
assert_eq!(titles[0].playlist_id, 1);
|
||||
assert_eq!(titles[1].playlist_id, 2);
|
||||
assert_eq!(titles[0].playlist, "VTS_01_1.VOB");
|
||||
assert_eq!(titles[1].playlist, "VTS_02_2.VOB");
|
||||
// Distinct vob_start → distinct extents.
|
||||
assert_eq!(titles[0].extents[0].start_lba, 100);
|
||||
assert_eq!(titles[1].extents[0].start_lba, 200);
|
||||
}
|
||||
|
||||
/// chapter_times from the IFO become Chapter entries with ordinal
|
||||
/// names (dvd.rs maps chapter_times → Chapter{time_secs, chapter_name}).
|
||||
#[test]
|
||||
fn scan_dvd_titles_chapters_present() {
|
||||
let mut disc = MemDisc::new();
|
||||
let vmg = build_vmg(&[(1, 1, 1)]);
|
||||
let vts = build_vts(0, 0x00, &[], &[], &[(0, 9)], false);
|
||||
let udf = build_video_ts_fs(
|
||||
&mut disc,
|
||||
&[
|
||||
FileSpec {
|
||||
name: "VIDEO_TS.IFO".into(),
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vmg,
|
||||
},
|
||||
FileSpec {
|
||||
name: "VTS_01_0.IFO".into(),
|
||||
icb_lba: 62,
|
||||
data_lba: 6000,
|
||||
contents: vts,
|
||||
},
|
||||
],
|
||||
);
|
||||
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
||||
// One program in the program map → one chapter time (0.0 for the
|
||||
// first program). Name is the ordinal from chapter_name(0).
|
||||
assert_eq!(t.chapters.len(), 1);
|
||||
assert_eq!(t.chapters[0].name, chapter_name(0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user