use crate::error::{Error, Result};
pub const DVD_SECTOR: usize = 2048;
pub const VMG_MAGIC: &[u8; 12] = b"DVDVIDEO-VMG";
pub const VTS_MAGIC: &[u8; 12] = b"DVDVIDEO-VTS";
fn read_u16(buf: &[u8], off: usize) -> Result<u16> {
let slice = buf
.get(off..off + 2)
.ok_or(Error::InvalidUdf("ifo: u16 read past end"))?;
Ok(u16::from_be_bytes([slice[0], slice[1]]))
}
fn read_u32(buf: &[u8], off: usize) -> Result<u32> {
let slice = buf
.get(off..off + 4)
.ok_or(Error::InvalidUdf("ifo: u32 read past end"))?;
Ok(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
}
fn read_u8(buf: &[u8], off: usize) -> Result<u8> {
buf.get(off)
.copied()
.ok_or(Error::InvalidUdf("ifo: u8 read past end"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PgcTime {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
pub frame_rate: FrameRate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameRate {
Illegal,
Pal25,
Reserved,
Ntsc30,
}
impl PgcTime {
pub fn from_bytes(bytes: [u8; 4]) -> Self {
fn bcd(b: u8) -> u8 {
((b >> 4) & 0x0F) * 10 + (b & 0x0F)
}
let hours = bcd(bytes[0]);
let minutes = bcd(bytes[1]);
let seconds = bcd(bytes[2]);
let frame_rate = match (bytes[3] >> 6) & 0x03 {
0b00 => FrameRate::Illegal,
0b01 => FrameRate::Pal25,
0b10 => FrameRate::Reserved,
0b11 => FrameRate::Ntsc30,
_ => unreachable!(),
};
let f_lo = bytes[3] & 0x0F;
let f_hi = (bytes[3] >> 4) & 0x03;
let frames = f_hi * 10 + f_lo;
Self {
hours,
minutes,
seconds,
frames,
frame_rate,
}
}
pub fn total_seconds(self) -> u32 {
u32::from(self.hours) * 3600 + u32::from(self.minutes) * 60 + u32::from(self.seconds)
}
}
#[derive(Debug, Clone)]
pub struct VmgIfo {
pub last_sector_vmg_set: u32,
pub last_sector_ifo: u32,
pub version: u16,
pub vmg_category: u32,
pub number_of_volumes: u16,
pub volume_number: u16,
pub side_id: u8,
pub number_of_title_sets: u16,
pub provider_id: String,
pub vmgi_mat_end: u32,
pub fp_pgc_addr: u32,
pub menu_vob_sector: u32,
pub tt_srpt_sector: u32,
pub vmgm_pgci_ut_sector: u32,
pub ptl_mait_sector: u32,
pub vts_atrt_sector: u32,
pub txtdt_mg_sector: u32,
pub vmgm_c_adt_sector: u32,
pub vmgm_vobu_admap_sector: u32,
}
impl VmgIfo {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 0x200 {
return Err(Error::InvalidUdf("VMGI_MAT: buffer shorter than 0x200"));
}
if &buf[0..12] != VMG_MAGIC {
return Err(Error::InvalidUdf("VMGI_MAT: bad magic"));
}
let last_sector_vmg_set = read_u32(buf, 0x000C)?;
let last_sector_ifo = read_u32(buf, 0x001C)?;
let version = read_u16(buf, 0x0020)?;
let vmg_category = read_u32(buf, 0x0022)?;
let number_of_volumes = read_u16(buf, 0x0026)?;
let volume_number = read_u16(buf, 0x0028)?;
let side_id = read_u8(buf, 0x002A)?;
let number_of_title_sets = read_u16(buf, 0x003E)?;
let provider_id_raw = &buf[0x0040..0x0060];
let provider_id = decode_ascii_trim(provider_id_raw);
let vmgi_mat_end = read_u32(buf, 0x0080)?;
let fp_pgc_addr = read_u32(buf, 0x0084)?;
let menu_vob_sector = read_u32(buf, 0x00C0)?;
let tt_srpt_sector = read_u32(buf, 0x00C4)?;
let vmgm_pgci_ut_sector = read_u32(buf, 0x00C8)?;
let ptl_mait_sector = read_u32(buf, 0x00CC)?;
let vts_atrt_sector = read_u32(buf, 0x00D0)?;
let txtdt_mg_sector = read_u32(buf, 0x00D4)?;
let vmgm_c_adt_sector = read_u32(buf, 0x00D8)?;
let vmgm_vobu_admap_sector = read_u32(buf, 0x00DC)?;
Ok(Self {
last_sector_vmg_set,
last_sector_ifo,
version,
vmg_category,
number_of_volumes,
volume_number,
side_id,
number_of_title_sets,
provider_id,
vmgi_mat_end,
fp_pgc_addr,
menu_vob_sector,
tt_srpt_sector,
vmgm_pgci_ut_sector,
ptl_mait_sector,
vts_atrt_sector,
txtdt_mg_sector,
vmgm_c_adt_sector,
vmgm_vobu_admap_sector,
})
}
}
fn decode_ascii_trim(buf: &[u8]) -> String {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end]).trim_end().to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DvdTitleEntry {
pub title_type: u8,
pub angle_count: u8,
pub chapter_count: u16,
pub parental_mask: u16,
pub vts_number: u8,
pub vts_title_number: u8,
pub vts_start_sector: u32,
}
#[derive(Debug, Clone)]
pub struct TtSrpt {
pub title_count: u16,
pub end_address: u32,
pub entries: Vec<DvdTitleEntry>,
}
impl TtSrpt {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("TT_SRPT: shorter than 8-byte header"));
}
let title_count = read_u16(buf, 0)?;
let end_address = read_u32(buf, 4)?;
let needed = 8usize.saturating_add(usize::from(title_count) * 12);
if buf.len() < needed {
return Err(Error::InvalidUdf(
"TT_SRPT: buffer shorter than title_count*12",
));
}
let mut entries = Vec::with_capacity(usize::from(title_count));
for i in 0..usize::from(title_count) {
let base = 8 + i * 12;
entries.push(DvdTitleEntry {
title_type: read_u8(buf, base)?,
angle_count: read_u8(buf, base + 1)?,
chapter_count: read_u16(buf, base + 2)?,
parental_mask: read_u16(buf, base + 4)?,
vts_number: read_u8(buf, base + 6)?,
vts_title_number: read_u8(buf, base + 7)?,
vts_start_sector: read_u32(buf, base + 8)?,
});
}
Ok(Self {
title_count,
end_address,
entries,
})
}
}
#[derive(Debug, Clone)]
pub struct VtsiMat {
pub last_sector_title_set: u32,
pub last_sector_ifo: u32,
pub version: u16,
pub vts_category: u32,
pub vtsi_mat_end: u32,
pub menu_vob_sector: u32,
pub title_vob_sector: u32,
pub vts_ptt_srpt_sector: u32,
pub vts_pgci_sector: u32,
pub vtsm_pgci_ut_sector: u32,
pub vts_tmapti_sector: u32,
pub vtsm_c_adt_sector: u32,
pub vtsm_vobu_admap_sector: u32,
pub vts_c_adt_sector: u32,
pub vts_vobu_admap_sector: u32,
}
impl VtsiMat {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 0x200 {
return Err(Error::InvalidUdf("VTSI_MAT: buffer shorter than 0x200"));
}
if &buf[0..12] != VTS_MAGIC {
return Err(Error::InvalidUdf("VTSI_MAT: bad magic"));
}
Ok(Self {
last_sector_title_set: read_u32(buf, 0x000C)?,
last_sector_ifo: read_u32(buf, 0x001C)?,
version: read_u16(buf, 0x0020)?,
vts_category: read_u32(buf, 0x0022)?,
vtsi_mat_end: read_u32(buf, 0x0080)?,
menu_vob_sector: read_u32(buf, 0x00C0)?,
title_vob_sector: read_u32(buf, 0x00C4)?,
vts_ptt_srpt_sector: read_u32(buf, 0x00C8)?,
vts_pgci_sector: read_u32(buf, 0x00CC)?,
vtsm_pgci_ut_sector: read_u32(buf, 0x00D0)?,
vts_tmapti_sector: read_u32(buf, 0x00D4)?,
vtsm_c_adt_sector: read_u32(buf, 0x00D8)?,
vtsm_vobu_admap_sector: read_u32(buf, 0x00DC)?,
vts_c_adt_sector: read_u32(buf, 0x00E0)?,
vts_vobu_admap_sector: read_u32(buf, 0x00E4)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ptt {
pub pgcn: u16,
pub pgn: u16,
}
#[derive(Debug, Clone)]
pub struct PttTitle {
pub chapters: Vec<Ptt>,
}
#[derive(Debug, Clone)]
pub struct VtsPttSrpt {
pub title_count: u16,
pub end_address: u32,
pub titles: Vec<PttTitle>,
}
impl VtsPttSrpt {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf(
"VTS_PTT_SRPT: shorter than 8-byte header",
));
}
let title_count = read_u16(buf, 0)?;
let end_address = read_u32(buf, 4)?;
let nt = usize::from(title_count);
let offsets_end = 8usize.saturating_add(nt * 4);
if buf.len() < offsets_end {
return Err(Error::InvalidUdf(
"VTS_PTT_SRPT: offset list past end of buffer",
));
}
let mut offsets = Vec::with_capacity(nt);
for i in 0..nt {
offsets.push(read_u32(buf, 8 + i * 4)? as usize);
}
let mut titles = Vec::with_capacity(nt);
for i in 0..nt {
let start = offsets[i];
let end_excl = if i + 1 < nt {
offsets[i + 1]
} else {
(end_address as usize).saturating_add(1)
};
if end_excl < start {
return Err(Error::InvalidUdf(
"VTS_PTT_SRPT: title offsets not monotonic",
));
}
let span = end_excl - start;
if span % 4 != 0 {
return Err(Error::InvalidUdf(
"VTS_PTT_SRPT: title span not a multiple of 4",
));
}
let n_ptt = span / 4;
if buf.len() < start + n_ptt * 4 {
return Err(Error::InvalidUdf(
"VTS_PTT_SRPT: title body past end of buffer",
));
}
let mut chapters = Vec::with_capacity(n_ptt);
for j in 0..n_ptt {
let off = start + j * 4;
chapters.push(Ptt {
pgcn: read_u16(buf, off)?,
pgn: read_u16(buf, off + 2)?,
});
}
titles.push(PttTitle { chapters });
}
Ok(Self {
title_count,
end_address,
titles,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellPlaybackInfo {
pub category_byte0: u8,
pub restricted: bool,
pub still_time: u8,
pub cell_command: u8,
pub playback_time: PgcTime,
pub first_vobu_start_sector: u32,
pub first_ilvu_end_sector: u32,
pub last_vobu_start_sector: u32,
pub last_vobu_end_sector: u32,
}
impl CellPlaybackInfo {
fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 24 {
return Err(Error::InvalidUdf("C_PBI entry shorter than 24 bytes"));
}
let category_byte0 = read_u8(buf, 0)?;
let restricted = (read_u8(buf, 1)? & 0x80) != 0;
let still_time = read_u8(buf, 2)?;
let cell_command = read_u8(buf, 3)?;
let mut t = [0u8; 4];
t.copy_from_slice(&buf[4..8]);
let playback_time = PgcTime::from_bytes(t);
let first_vobu_start_sector = read_u32(buf, 8)?;
let first_ilvu_end_sector = read_u32(buf, 12)?;
let last_vobu_start_sector = read_u32(buf, 16)?;
let last_vobu_end_sector = read_u32(buf, 20)?;
Ok(Self {
category_byte0,
restricted,
still_time,
cell_command,
playback_time,
first_vobu_start_sector,
first_ilvu_end_sector,
last_vobu_start_sector,
last_vobu_end_sector,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellPositionInfo {
pub vob_id: u16,
pub cell_id: u8,
}
impl CellPositionInfo {
fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 4 {
return Err(Error::InvalidUdf("C_POS entry shorter than 4 bytes"));
}
let vob_id = read_u16(buf, 0)?;
let cell_id = read_u8(buf, 3)?;
Ok(Self { vob_id, cell_id })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PaletteEntry {
pub y: u8,
pub cr: u8,
pub cb: u8,
}
impl PaletteEntry {
fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 4 {
return Err(Error::InvalidUdf("PGC palette entry shorter than 4 bytes"));
}
Ok(Self {
y: buf[1],
cr: buf[2],
cb: buf[3],
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NavCommand {
pub bytes: [u8; 8],
}
impl NavCommand {
fn parse(buf: &[u8]) -> Result<Self> {
let slice = buf
.get(0..8)
.ok_or(Error::InvalidUdf("PGC command shorter than 8 bytes"))?;
let mut bytes = [0u8; 8];
bytes.copy_from_slice(slice);
Ok(Self { bytes })
}
pub fn command_type(&self) -> u8 {
self.bytes[0] >> 5
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PgcCommandTable {
pub pre: Vec<NavCommand>,
pub post: Vec<NavCommand>,
pub cell: Vec<NavCommand>,
pub end_address: u16,
}
impl PgcCommandTable {
fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("PGC command table shorter than header"));
}
let pre_count = read_u16(buf, 0)?;
let post_count = read_u16(buf, 2)?;
let cell_count = read_u16(buf, 4)?;
let end_address = read_u16(buf, 6)?;
let total = usize::from(pre_count) + usize::from(post_count) + usize::from(cell_count);
if total > 128 {
return Err(Error::InvalidUdf("PGC command table claims > 128 commands"));
}
let read_list = |start: usize, count: u16| -> Result<Vec<NavCommand>> {
let mut out = Vec::with_capacity(usize::from(count));
for i in 0..usize::from(count) {
let off = start + i * 8;
let word = buf
.get(off..off + 8)
.ok_or(Error::InvalidUdf("PGC command table list past end"))?;
out.push(NavCommand::parse(word)?);
}
Ok(out)
};
let pre_start = 8usize;
let post_start = pre_start + usize::from(pre_count) * 8;
let cell_start = post_start + usize::from(post_count) * 8;
let pre = read_list(pre_start, pre_count)?;
let post = read_list(post_start, post_count)?;
let cell = read_list(cell_start, cell_count)?;
Ok(Self {
pre,
post,
cell,
end_address,
})
}
}
#[derive(Debug, Clone)]
pub struct Pgc {
pub number_of_programs: u8,
pub number_of_cells: u8,
pub playback_time: PgcTime,
pub prohibited_user_ops: u32,
pub next_pgcn: u16,
pub prev_pgcn: u16,
pub goup_pgcn: u16,
pub still_time: u8,
pub playback_mode: u8,
pub palette: [PaletteEntry; 16],
pub offset_commands: u16,
pub offset_program_map: u16,
pub offset_cell_playback: u16,
pub offset_cell_position: u16,
pub program_map: Vec<u8>,
pub cells: Vec<CellPlaybackInfo>,
pub cell_positions: Vec<CellPositionInfo>,
pub commands: Option<PgcCommandTable>,
}
impl Pgc {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 0xEC {
return Err(Error::InvalidUdf("PGC: buffer shorter than header"));
}
let number_of_programs = read_u8(buf, 0x0002)?;
let number_of_cells = read_u8(buf, 0x0003)?;
let mut t = [0u8; 4];
t.copy_from_slice(&buf[0x0004..0x0008]);
let playback_time = PgcTime::from_bytes(t);
let prohibited_user_ops = read_u32(buf, 0x0008)?;
let next_pgcn = read_u16(buf, 0x009C)?;
let prev_pgcn = read_u16(buf, 0x009E)?;
let goup_pgcn = read_u16(buf, 0x00A0)?;
let still_time = read_u8(buf, 0x00A2)?;
let playback_mode = read_u8(buf, 0x00A3)?;
let mut palette = [PaletteEntry::default(); 16];
for (i, slot) in palette.iter_mut().enumerate() {
let base = 0x00A4 + i * 4;
*slot = PaletteEntry::parse(&buf[base..base + 4])?;
}
let offset_commands = read_u16(buf, 0x00E4)?;
let offset_program_map = read_u16(buf, 0x00E6)?;
let offset_cell_playback = read_u16(buf, 0x00E8)?;
let offset_cell_position = read_u16(buf, 0x00EA)?;
let mut program_map = Vec::with_capacity(usize::from(number_of_programs));
if offset_program_map != 0 {
let base = usize::from(offset_program_map);
for i in 0..usize::from(number_of_programs) {
program_map.push(read_u8(buf, base + i)?);
}
}
let mut cells = Vec::with_capacity(usize::from(number_of_cells));
if offset_cell_playback != 0 {
let base = usize::from(offset_cell_playback);
for i in 0..usize::from(number_of_cells) {
let entry = &buf
.get(base + i * 24..base + (i + 1) * 24)
.ok_or(Error::InvalidUdf("PGC: C_PBI past end of buffer"))?;
cells.push(CellPlaybackInfo::parse(entry)?);
}
}
let mut cell_positions = Vec::with_capacity(usize::from(number_of_cells));
if offset_cell_position != 0 {
let base = usize::from(offset_cell_position);
for i in 0..usize::from(number_of_cells) {
let entry = &buf
.get(base + i * 4..base + (i + 1) * 4)
.ok_or(Error::InvalidUdf("PGC: C_POS past end of buffer"))?;
cell_positions.push(CellPositionInfo::parse(entry)?);
}
}
let commands = if offset_commands != 0 {
let base = usize::from(offset_commands);
let tbl = buf
.get(base..)
.ok_or(Error::InvalidUdf("PGC: command table past end of buffer"))?;
Some(PgcCommandTable::parse(tbl)?)
} else {
None
};
Ok(Self {
number_of_programs,
number_of_cells,
playback_time,
prohibited_user_ops,
next_pgcn,
prev_pgcn,
goup_pgcn,
still_time,
playback_mode,
palette,
offset_commands,
offset_program_map,
offset_cell_playback,
offset_cell_position,
program_map,
cells,
cell_positions,
commands,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PgciSrp {
pub category: u32,
pub offset: u32,
}
#[derive(Debug, Clone)]
pub struct Pgci {
pub number_of_pgcs: u16,
pub end_address: u32,
pub srp: Vec<PgciSrp>,
pub pgcs: Vec<Pgc>,
}
impl Pgci {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("PGCI: shorter than 8-byte header"));
}
let number_of_pgcs = read_u16(buf, 0)?;
let end_address = read_u32(buf, 4)?;
let n = usize::from(number_of_pgcs);
let srp_end = 8usize.saturating_add(n * 8);
if buf.len() < srp_end {
return Err(Error::InvalidUdf("PGCI: SRP list past end of buffer"));
}
let mut srp = Vec::with_capacity(n);
for i in 0..n {
let base = 8 + i * 8;
srp.push(PgciSrp {
category: read_u32(buf, base)?,
offset: read_u32(buf, base + 4)?,
});
}
let mut pgcs = Vec::with_capacity(n);
for entry in &srp {
let off = entry.offset as usize;
if off == 0 || off >= buf.len() {
return Err(Error::InvalidUdf("PGCI: PGC offset out of range"));
}
let pgc_buf = &buf[off..];
pgcs.push(Pgc::parse(pgc_buf)?);
}
Ok(Self {
number_of_pgcs,
end_address,
srp,
pgcs,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellAddrEntry {
pub vob_id: u16,
pub cell_id: u8,
pub start_sector: u32,
pub end_sector: u32,
}
#[derive(Debug, Clone)]
pub struct VtsCAdt {
pub number_of_vob_ids: u16,
pub end_address: u32,
pub entries: Vec<CellAddrEntry>,
}
impl VtsCAdt {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("C_ADT: shorter than 8-byte header"));
}
let number_of_vob_ids = read_u16(buf, 0)?;
let end_address = read_u32(buf, 4)?;
let body_bytes = (end_address as usize).saturating_add(1).saturating_sub(8);
if body_bytes % 12 != 0 {
return Err(Error::InvalidUdf(
"C_ADT: end_address implies non-12-byte entry size",
));
}
let n = body_bytes / 12;
let needed = 8 + n * 12;
if buf.len() < needed {
return Err(Error::InvalidUdf("C_ADT: buffer shorter than entry table"));
}
let mut entries = Vec::with_capacity(n);
for i in 0..n {
let base = 8 + i * 12;
entries.push(CellAddrEntry {
vob_id: read_u16(buf, base)?,
cell_id: read_u8(buf, base + 2)?,
start_sector: read_u32(buf, base + 4)?,
end_sector: read_u32(buf, base + 8)?,
});
}
Ok(Self {
number_of_vob_ids,
end_address,
entries,
})
}
pub fn lookup(&self, vob_id: u16, cell_id: u8) -> Option<(u32, u32)> {
self.entries
.iter()
.find(|e| e.vob_id == vob_id && e.cell_id == cell_id)
.map(|e| (e.start_sector, e.end_sector))
}
}
#[derive(Debug, Clone)]
pub struct DvdChapter {
pub number: u16,
pub pgcn: u16,
pub pgn: u16,
pub start_cell: u8,
pub end_cell: u8,
pub playback_time: PgcTime,
}
#[derive(Debug, Clone)]
pub struct DvdTitle {
pub number: u8,
pub angle_count: u8,
pub chapter_count: u16,
pub chapters: Vec<DvdChapter>,
}
#[derive(Debug, Clone)]
pub struct VtsIfo {
pub vts_number: u8,
pub title_count: u8,
pub titles: Vec<DvdTitle>,
pub pgcs: Vec<Pgc>,
pub cell_adt: VtsCAdt,
pub mat: VtsiMat,
}
impl VtsIfo {
pub fn parse(buf: &[u8], vts_number: u8) -> Result<Self> {
let mat = VtsiMat::parse(buf)?;
let ptt_off = (mat.vts_ptt_srpt_sector as usize)
.checked_mul(DVD_SECTOR)
.ok_or(Error::InvalidUdf("VTSI: PTT sector overflow"))?;
let ptt_buf = buf
.get(ptt_off..)
.ok_or(Error::InvalidUdf("VTSI: PTT sector past end"))?;
let ptt_srpt = VtsPttSrpt::parse(ptt_buf)?;
let pgci_off = (mat.vts_pgci_sector as usize)
.checked_mul(DVD_SECTOR)
.ok_or(Error::InvalidUdf("VTSI: PGCI sector overflow"))?;
let pgci_buf = buf
.get(pgci_off..)
.ok_or(Error::InvalidUdf("VTSI: PGCI sector past end"))?;
let pgci = Pgci::parse(pgci_buf)?;
let cadt_off = (mat.vts_c_adt_sector as usize)
.checked_mul(DVD_SECTOR)
.ok_or(Error::InvalidUdf("VTSI: C_ADT sector overflow"))?;
let cadt_buf = buf
.get(cadt_off..)
.ok_or(Error::InvalidUdf("VTSI: C_ADT sector past end"))?;
let cell_adt = VtsCAdt::parse(cadt_buf)?;
let title_count_u8 = u8::try_from(ptt_srpt.title_count.min(255))
.map_err(|_| Error::InvalidUdf("VTSI: title count > 255"))?;
let mut titles = Vec::with_capacity(usize::from(title_count_u8));
for (i, ptt_title) in ptt_srpt.titles.iter().enumerate() {
let title_number = (i as u8).saturating_add(1);
let mut chapters = Vec::with_capacity(ptt_title.chapters.len());
for (ch_i, ptt) in ptt_title.chapters.iter().enumerate() {
let pgc = pgci
.pgcs
.get(usize::from(ptt.pgcn.saturating_sub(1)))
.ok_or(Error::InvalidUdf(
"VTSI: PTT references PGCN past end of PGCI",
))?;
let pgn_idx = usize::from(ptt.pgn.saturating_sub(1));
let start_cell = *pgc
.program_map
.get(pgn_idx)
.ok_or(Error::InvalidUdf("VTSI: PTT PGN past program_map"))?;
let next_in_same_pgc = ptt_title.chapters.get(ch_i + 1).and_then(|next_ptt| {
if next_ptt.pgcn == ptt.pgcn {
pgc.program_map
.get(usize::from(next_ptt.pgn.saturating_sub(1)))
.copied()
.map(|next_start| next_start.saturating_sub(1))
} else {
None
}
});
let end_cell = next_in_same_pgc.unwrap_or(pgc.number_of_cells);
chapters.push(DvdChapter {
number: (ch_i as u16).saturating_add(1),
pgcn: ptt.pgcn,
pgn: ptt.pgn,
start_cell,
end_cell,
playback_time: pgc.playback_time,
});
}
let chapter_count = chapters.len() as u16;
titles.push(DvdTitle {
number: title_number,
angle_count: 1,
chapter_count,
chapters,
});
}
Ok(Self {
vts_number,
title_count: title_count_u8,
titles,
pgcs: pgci.pgcs,
cell_adt,
mat,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pgc_time_decode_ntsc_30() {
let t = PgcTime::from_bytes([0x01, 0x23, 0x45, 0xE0]);
assert_eq!(t.hours, 1);
assert_eq!(t.minutes, 23);
assert_eq!(t.seconds, 45);
assert_eq!(t.frames, 20);
assert_eq!(t.frame_rate, FrameRate::Ntsc30);
assert_eq!(t.total_seconds(), 3600 + 23 * 60 + 45);
}
#[test]
fn pgc_time_decode_pal_25() {
let t = PgcTime::from_bytes([0x00, 0x00, 0x01, 0x40]);
assert_eq!(t.frame_rate, FrameRate::Pal25);
assert_eq!(t.frames, 0);
assert_eq!(t.total_seconds(), 1);
}
fn build_vmg_mat() -> Vec<u8> {
let mut b = vec![0u8; 0x200];
b[0..12].copy_from_slice(VMG_MAGIC);
b[0x000C..0x0010].copy_from_slice(&1000u32.to_be_bytes());
b[0x001C..0x0020].copy_from_slice(&4u32.to_be_bytes());
b[0x0020..0x0022].copy_from_slice(&0x0011u16.to_be_bytes());
b[0x0022..0x0026].copy_from_slice(&0x00FF_0000u32.to_be_bytes());
b[0x0026..0x0028].copy_from_slice(&1u16.to_be_bytes());
b[0x0028..0x002A].copy_from_slice(&1u16.to_be_bytes());
b[0x002A] = 0;
b[0x003E..0x0040].copy_from_slice(&2u16.to_be_bytes());
let pid = b"OXIDEAV-TEST";
b[0x0040..0x0040 + pid.len()].copy_from_slice(pid);
b[0x0080..0x0084].copy_from_slice(&0x01FFu32.to_be_bytes());
b[0x0084..0x0088].copy_from_slice(&0u32.to_be_bytes());
b[0x00C4..0x00C8].copy_from_slice(&1u32.to_be_bytes());
b[0x00D0..0x00D4].copy_from_slice(&2u32.to_be_bytes());
b
}
#[test]
fn vmgi_mat_parse_roundtrip() {
let buf = build_vmg_mat();
let vmg = VmgIfo::parse(&buf).unwrap();
assert_eq!(vmg.last_sector_vmg_set, 1000);
assert_eq!(vmg.last_sector_ifo, 4);
assert_eq!(vmg.version, 0x0011);
assert_eq!(vmg.number_of_volumes, 1);
assert_eq!(vmg.volume_number, 1);
assert_eq!(vmg.side_id, 0);
assert_eq!(vmg.number_of_title_sets, 2);
assert_eq!(vmg.provider_id, "OXIDEAV-TEST");
assert_eq!(vmg.tt_srpt_sector, 1);
assert_eq!(vmg.vts_atrt_sector, 2);
assert_eq!(vmg.menu_vob_sector, 0);
}
#[test]
fn vmgi_mat_rejects_bad_magic() {
let mut buf = build_vmg_mat();
buf[0..12].copy_from_slice(b"DVDVIDEO-BAD");
let err = VmgIfo::parse(&buf).unwrap_err();
match err {
Error::InvalidUdf(_) => {}
other => panic!("expected InvalidUdf, got {other:?}"),
}
}
fn build_vtsi_mat(
ptt_srpt_sector: u32,
pgci_sector: u32,
c_adt_sector: u32,
title_vob_sector: u32,
) -> Vec<u8> {
let mut b = vec![0u8; 0x200];
b[0..12].copy_from_slice(VTS_MAGIC);
b[0x000C..0x0010].copy_from_slice(&100_000u32.to_be_bytes());
b[0x001C..0x0020].copy_from_slice(&15u32.to_be_bytes());
b[0x0020..0x0022].copy_from_slice(&0x0011u16.to_be_bytes());
b[0x0080..0x0084].copy_from_slice(&0x01FFu32.to_be_bytes());
b[0x00C4..0x00C8].copy_from_slice(&title_vob_sector.to_be_bytes());
b[0x00C8..0x00CC].copy_from_slice(&ptt_srpt_sector.to_be_bytes());
b[0x00CC..0x00D0].copy_from_slice(&pgci_sector.to_be_bytes());
b[0x00E0..0x00E4].copy_from_slice(&c_adt_sector.to_be_bytes());
b
}
#[test]
fn vtsi_mat_parse_roundtrip() {
let buf = build_vtsi_mat(1, 2, 3, 42);
let mat = VtsiMat::parse(&buf).unwrap();
assert_eq!(mat.last_sector_title_set, 100_000);
assert_eq!(mat.last_sector_ifo, 15);
assert_eq!(mat.version, 0x0011);
assert_eq!(mat.title_vob_sector, 42);
assert_eq!(mat.vts_ptt_srpt_sector, 1);
assert_eq!(mat.vts_pgci_sector, 2);
assert_eq!(mat.vts_c_adt_sector, 3);
}
fn build_tt_srpt(entries: &[(u8, u8, u16, u8, u8, u32)]) -> Vec<u8> {
let n = entries.len();
let len = 8 + n * 12;
let mut b = vec![0u8; len];
b[0..2].copy_from_slice(&(n as u16).to_be_bytes());
let end_addr = (len - 1) as u32;
b[4..8].copy_from_slice(&end_addr.to_be_bytes());
for (i, e) in entries.iter().enumerate() {
let base = 8 + i * 12;
b[base] = e.0; b[base + 1] = e.1; b[base + 2..base + 4].copy_from_slice(&e.2.to_be_bytes()); b[base + 4..base + 6].copy_from_slice(&0u16.to_be_bytes()); b[base + 6] = e.3; b[base + 7] = e.4; b[base + 8..base + 12].copy_from_slice(&e.5.to_be_bytes()); let _ = e; }
b
}
#[test]
fn tt_srpt_parses_titles() {
let entries = [
(0x3F, 1u8, 15u16, 1u8, 1u8, 0x0000_0500u32),
(0x3F, 1u8, 4u16, 1u8, 2u8, 0x0000_0500u32),
(0x3F, 2u8, 1u16, 2u8, 1u8, 0x0000_8000u32),
];
let buf = build_tt_srpt(&entries);
let srpt = TtSrpt::parse(&buf).unwrap();
assert_eq!(srpt.title_count, 3);
assert_eq!(srpt.end_address, (8 + 3 * 12 - 1) as u32);
assert_eq!(srpt.entries[0].chapter_count, 15);
assert_eq!(srpt.entries[1].vts_title_number, 2);
assert_eq!(srpt.entries[2].vts_number, 2);
assert_eq!(srpt.entries[2].angle_count, 2);
assert_eq!(srpt.entries[2].vts_start_sector, 0x0000_8000);
}
fn build_c_adt(rows: &[(u16, u8, u32, u32)]) -> Vec<u8> {
let n = rows.len();
let len = 8 + n * 12;
let mut b = vec![0u8; len];
let distinct = {
let mut v: Vec<u16> = rows.iter().map(|r| r.0).collect();
v.sort();
v.dedup();
v.len() as u16
};
b[0..2].copy_from_slice(&distinct.to_be_bytes());
let end_addr = (len - 1) as u32;
b[4..8].copy_from_slice(&end_addr.to_be_bytes());
for (i, r) in rows.iter().enumerate() {
let base = 8 + i * 12;
b[base..base + 2].copy_from_slice(&r.0.to_be_bytes());
b[base + 2] = r.1;
b[base + 4..base + 8].copy_from_slice(&r.2.to_be_bytes());
b[base + 8..base + 12].copy_from_slice(&r.3.to_be_bytes());
}
b
}
#[test]
fn c_adt_parses_four_rows() {
let rows = [
(1u16, 1u8, 100u32, 199u32),
(1u16, 2u8, 200u32, 299u32),
(1u16, 3u8, 300u32, 399u32),
(2u16, 1u8, 1000u32, 1999u32),
];
let buf = build_c_adt(&rows);
let adt = VtsCAdt::parse(&buf).unwrap();
assert_eq!(adt.number_of_vob_ids, 2);
assert_eq!(adt.entries.len(), 4);
assert_eq!(adt.lookup(1, 2), Some((200, 299)));
assert_eq!(adt.lookup(2, 1), Some((1000, 1999)));
assert_eq!(adt.lookup(3, 1), None);
}
fn build_pgc_with_cells(cells: &[CellPlaybackInfo], positions: &[CellPositionInfo]) -> Vec<u8> {
assert_eq!(cells.len(), positions.len());
let n = cells.len();
let header_size = 0xEC;
let prog_count = 1u8;
let prog_map_size = (usize::from(prog_count) + 1) & !1; let pre_n = 1usize; let post_n = 1usize;
let cmd_cell_n = 2usize;
let cmd_table_size = 8 + (pre_n + post_n + cmd_cell_n) * 8;
let cpbi_size = n * 24;
let cpos_size = n * 4;
let mut b = vec![0u8; header_size + cmd_table_size + prog_map_size + cpbi_size + cpos_size];
b[0x0002] = prog_count;
b[0x0003] = n as u8;
b[0x0004..0x0008].copy_from_slice(&[0x00, 0x05, 0x00, 0xE0]);
for i in 0..16usize {
let base = 0x00A4 + i * 4;
b[base] = 0x00; b[base + 1] = 0x10 + i as u8; b[base + 2] = 0x80; b[base + 3] = 0x80; }
let off_cmd = header_size as u16; let off_pmap = (header_size + cmd_table_size) as u16;
let off_cpbi = (header_size + cmd_table_size + prog_map_size) as u16;
let off_cpos = (header_size + cmd_table_size + prog_map_size + cpbi_size) as u16;
b[0x00E4..0x00E6].copy_from_slice(&off_cmd.to_be_bytes());
b[0x00E6..0x00E8].copy_from_slice(&off_pmap.to_be_bytes());
b[0x00E8..0x00EA].copy_from_slice(&off_cpbi.to_be_bytes());
b[0x00EA..0x00EC].copy_from_slice(&off_cpos.to_be_bytes());
let ct = header_size;
b[ct..ct + 2].copy_from_slice(&(pre_n as u16).to_be_bytes());
b[ct + 2..ct + 4].copy_from_slice(&(post_n as u16).to_be_bytes());
b[ct + 4..ct + 6].copy_from_slice(&(cmd_cell_n as u16).to_be_bytes());
b[ct + 6..ct + 8].copy_from_slice(&((cmd_table_size - 1) as u16).to_be_bytes());
let mut w = ct + 8;
b[w] = 0xA0; b[w + 7] = 0x01;
w += 8;
b[w] = 0xB0; b[w + 7] = 0x02;
w += 8;
b[w] = 0xC0; b[w + 7] = 0x03;
w += 8;
b[w] = 0xC1; b[w + 7] = 0x04;
b[off_pmap as usize] = 1;
for (i, c) in cells.iter().enumerate() {
let base = off_cpbi as usize + i * 24;
b[base] = c.category_byte0;
b[base + 1] = if c.restricted { 0x80 } else { 0 };
b[base + 2] = c.still_time;
b[base + 3] = c.cell_command;
b[base + 4] = 0x00;
b[base + 5] = 0x01;
b[base + 6] = 0x00;
b[base + 7] = 0xE0;
b[base + 8..base + 12].copy_from_slice(&c.first_vobu_start_sector.to_be_bytes());
b[base + 12..base + 16].copy_from_slice(&c.first_ilvu_end_sector.to_be_bytes());
b[base + 16..base + 20].copy_from_slice(&c.last_vobu_start_sector.to_be_bytes());
b[base + 20..base + 24].copy_from_slice(&c.last_vobu_end_sector.to_be_bytes());
}
for (i, p) in positions.iter().enumerate() {
let base = off_cpos as usize + i * 4;
b[base..base + 2].copy_from_slice(&p.vob_id.to_be_bytes());
b[base + 3] = p.cell_id;
}
b
}
fn make_cell(start: u32, end: u32) -> CellPlaybackInfo {
CellPlaybackInfo {
category_byte0: 0,
restricted: false,
still_time: 0,
cell_command: 0,
playback_time: PgcTime::from_bytes([0, 1, 0, 0xE0]),
first_vobu_start_sector: start,
first_ilvu_end_sector: start + 5,
last_vobu_start_sector: end - 5,
last_vobu_end_sector: end,
}
}
#[test]
fn pgci_parses_one_pgc_with_three_cells() {
let cells = [
make_cell(1000, 1999),
make_cell(2000, 2999),
make_cell(3000, 3999),
];
let positions = [
CellPositionInfo {
vob_id: 1,
cell_id: 1,
},
CellPositionInfo {
vob_id: 1,
cell_id: 2,
},
CellPositionInfo {
vob_id: 1,
cell_id: 3,
},
];
let pgc_blob = build_pgc_with_cells(&cells, &positions);
let srp_size = 8usize;
let body_off = 8 + srp_size; let total = body_off + pgc_blob.len();
let mut b = vec![0u8; total];
b[0..2].copy_from_slice(&1u16.to_be_bytes());
b[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
b[8..12].copy_from_slice(&0u32.to_be_bytes());
b[12..16].copy_from_slice(&(body_off as u32).to_be_bytes());
b[body_off..body_off + pgc_blob.len()].copy_from_slice(&pgc_blob);
let pgci = Pgci::parse(&b).unwrap();
assert_eq!(pgci.number_of_pgcs, 1);
assert_eq!(pgci.pgcs.len(), 1);
let pgc = &pgci.pgcs[0];
assert_eq!(pgc.number_of_programs, 1);
assert_eq!(pgc.number_of_cells, 3);
assert_eq!(pgc.cells.len(), 3);
assert_eq!(pgc.cell_positions.len(), 3);
assert_eq!(pgc.cells[0].first_vobu_start_sector, 1000);
assert_eq!(pgc.cells[2].last_vobu_end_sector, 3999);
assert_eq!(pgc.cell_positions[1].cell_id, 2);
assert_eq!(pgc.playback_time.frame_rate, FrameRate::Ntsc30);
assert_eq!(
pgc.palette[0],
PaletteEntry {
y: 0x10,
cr: 0x80,
cb: 0x80
}
);
assert_eq!(
pgc.palette[15],
PaletteEntry {
y: 0x1F,
cr: 0x80,
cb: 0x80
}
);
let cmds = pgc.commands.as_ref().expect("command table present");
assert_eq!(cmds.pre.len(), 1);
assert_eq!(cmds.post.len(), 1);
assert_eq!(cmds.cell.len(), 2);
assert_eq!(cmds.pre[0].bytes[0], 0xA0);
assert_eq!(cmds.pre[0].bytes[7], 0x01);
assert_eq!(cmds.pre[0].command_type(), 0b101);
assert_eq!(cmds.post[0].bytes[0], 0xB0);
assert_eq!(cmds.post[0].bytes[7], 0x02);
assert_eq!(cmds.cell[0].bytes[7], 0x03);
assert_eq!(cmds.cell[1].bytes[7], 0x04);
}
#[test]
fn palette_entry_skips_reserved_byte() {
let e = PaletteEntry::parse(&[0xFF, 0x42, 0x10, 0xC0]).unwrap();
assert_eq!(
e,
PaletteEntry {
y: 0x42,
cr: 0x10,
cb: 0xC0
}
);
assert!(PaletteEntry::parse(&[0x00, 0x01, 0x02]).is_err());
}
#[test]
fn command_table_carves_three_lists() {
let pre = 2u16;
let post = 1u16;
let cell = 1u16;
let total = (pre + post + cell) as usize;
let size = 8 + total * 8;
let mut b = vec![0u8; size];
b[0..2].copy_from_slice(&pre.to_be_bytes());
b[2..4].copy_from_slice(&post.to_be_bytes());
b[4..6].copy_from_slice(&cell.to_be_bytes());
b[6..8].copy_from_slice(&((size - 1) as u16).to_be_bytes());
for i in 0..total {
b[8 + i * 8 + 7] = (i + 1) as u8;
}
let t = PgcCommandTable::parse(&b).unwrap();
assert_eq!(t.pre.len(), 2);
assert_eq!(t.post.len(), 1);
assert_eq!(t.cell.len(), 1);
assert_eq!(t.end_address, (size - 1) as u16);
assert_eq!(t.pre[0].bytes[7], 1);
assert_eq!(t.pre[1].bytes[7], 2);
assert_eq!(t.post[0].bytes[7], 3);
assert_eq!(t.cell[0].bytes[7], 4);
}
#[test]
fn command_table_rejects_overlong_count() {
let mut b = vec![0u8; 8];
b[0..2].copy_from_slice(&129u16.to_be_bytes());
assert!(PgcCommandTable::parse(&b).is_err());
}
#[test]
fn command_table_rejects_truncated_list() {
let mut b = vec![0u8; 8 + 8];
b[0..2].copy_from_slice(&2u16.to_be_bytes());
assert!(PgcCommandTable::parse(&b).is_err());
}
#[test]
fn pgc_without_command_table_yields_none() {
let mut b = vec![0u8; 0xEC];
b[0x0002] = 0; b[0x0003] = 0; b[0x0004..0x0008].copy_from_slice(&[0x00, 0x00, 0x00, 0xC0]); let pgc = Pgc::parse(&b).unwrap();
assert!(pgc.commands.is_none());
assert_eq!(pgc.palette[7], PaletteEntry::default());
}
#[test]
fn ptt_srpt_walks_two_titles_five_chapters() {
let n_titles = 2usize;
let n_chaps = 5usize;
let offsets_size = n_titles * 4;
let header_size = 8 + offsets_size;
let title_body_size = n_chaps * 4;
let total = header_size + n_titles * title_body_size;
let mut b = vec![0u8; total];
b[0..2].copy_from_slice(&(n_titles as u16).to_be_bytes());
b[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
for ti in 0..n_titles {
let off = (header_size + ti * title_body_size) as u32;
b[8 + ti * 4..8 + ti * 4 + 4].copy_from_slice(&off.to_be_bytes());
}
for ti in 0..n_titles {
for ci in 0..n_chaps {
let base = header_size + ti * title_body_size + ci * 4;
let pgcn = (ti + 1) as u16;
let pgn = (ci + 1) as u16;
b[base..base + 2].copy_from_slice(&pgcn.to_be_bytes());
b[base + 2..base + 4].copy_from_slice(&pgn.to_be_bytes());
}
}
let srpt = VtsPttSrpt::parse(&b).unwrap();
assert_eq!(srpt.title_count, 2);
assert_eq!(srpt.titles.len(), 2);
for ti in 0..n_titles {
assert_eq!(srpt.titles[ti].chapters.len(), 5);
assert_eq!(srpt.titles[ti].chapters[0].pgcn, (ti + 1) as u16);
assert_eq!(srpt.titles[ti].chapters[0].pgn, 1);
assert_eq!(srpt.titles[ti].chapters[4].pgn, 5);
}
}
fn make_composite_vts() -> Vec<u8> {
let mut img = vec![0u8; DVD_SECTOR * 4];
let mat = build_vtsi_mat(1, 2, 3, 100);
img[0..mat.len()].copy_from_slice(&mat);
let cells: Vec<CellPlaybackInfo> = (0..5)
.map(|i| make_cell(1000 + i * 1000, 1999 + i * 1000))
.collect();
let positions: Vec<CellPositionInfo> = (0..5)
.map(|i| CellPositionInfo {
vob_id: 1,
cell_id: (i + 1) as u8,
})
.collect();
let header_size = 0xEC;
let prog_count = 3u8;
let prog_map_size = (usize::from(prog_count) + 1) & !1; let cpbi_size = 5 * 24;
let cpos_size = 5 * 4;
let pgc_blob_len = header_size + prog_map_size + cpbi_size + cpos_size;
let mut pgc_blob = vec![0u8; pgc_blob_len];
pgc_blob[0x0002] = prog_count;
pgc_blob[0x0003] = 5; pgc_blob[0x0004..0x0008].copy_from_slice(&[0x00, 0x15, 0x00, 0xE0]);
let off_pmap = header_size as u16;
let off_cpbi = (header_size + prog_map_size) as u16;
let off_cpos = (header_size + prog_map_size + cpbi_size) as u16;
pgc_blob[0x00E6..0x00E8].copy_from_slice(&off_pmap.to_be_bytes());
pgc_blob[0x00E8..0x00EA].copy_from_slice(&off_cpbi.to_be_bytes());
pgc_blob[0x00EA..0x00EC].copy_from_slice(&off_cpos.to_be_bytes());
pgc_blob[header_size] = 1; pgc_blob[header_size + 1] = 3; pgc_blob[header_size + 2] = 5; for (i, c) in cells.iter().enumerate() {
let base = header_size + prog_map_size + i * 24;
pgc_blob[base + 4..base + 8].copy_from_slice(&[0, 1, 0, 0xE0]);
pgc_blob[base + 8..base + 12].copy_from_slice(&c.first_vobu_start_sector.to_be_bytes());
pgc_blob[base + 12..base + 16].copy_from_slice(&c.first_ilvu_end_sector.to_be_bytes());
pgc_blob[base + 16..base + 20].copy_from_slice(&c.last_vobu_start_sector.to_be_bytes());
pgc_blob[base + 20..base + 24].copy_from_slice(&c.last_vobu_end_sector.to_be_bytes());
}
for (i, p) in positions.iter().enumerate() {
let base = header_size + prog_map_size + cpbi_size + i * 4;
pgc_blob[base..base + 2].copy_from_slice(&p.vob_id.to_be_bytes());
pgc_blob[base + 3] = p.cell_id;
}
let srp_size = 8usize;
let body_off = 8 + srp_size;
let pgci_total = body_off + pgc_blob.len();
let mut pgci = vec![0u8; pgci_total];
pgci[0..2].copy_from_slice(&1u16.to_be_bytes());
pgci[4..8].copy_from_slice(&((pgci_total - 1) as u32).to_be_bytes());
pgci[12..16].copy_from_slice(&(body_off as u32).to_be_bytes());
pgci[body_off..body_off + pgc_blob.len()].copy_from_slice(&pgc_blob);
img[2 * DVD_SECTOR..2 * DVD_SECTOR + pgci.len()].copy_from_slice(&pgci);
let n_titles = 1usize;
let n_chaps = 3usize;
let header_sz = 8 + n_titles * 4;
let title_body = n_chaps * 4;
let total = header_sz + title_body;
let mut ptt = vec![0u8; total];
ptt[0..2].copy_from_slice(&(n_titles as u16).to_be_bytes());
ptt[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
ptt[8..12].copy_from_slice(&(header_sz as u32).to_be_bytes());
for ci in 0..n_chaps {
let base = header_sz + ci * 4;
ptt[base..base + 2].copy_from_slice(&1u16.to_be_bytes()); ptt[base + 2..base + 4].copy_from_slice(&((ci + 1) as u16).to_be_bytes());
}
img[DVD_SECTOR..DVD_SECTOR + ptt.len()].copy_from_slice(&ptt);
let cadt_rows: Vec<(u16, u8, u32, u32)> = (0..5)
.map(|i| {
(
1u16,
(i + 1) as u8,
1000 + i as u32 * 1000,
1999 + i as u32 * 1000,
)
})
.collect();
let cadt = build_c_adt(&cadt_rows);
img[3 * DVD_SECTOR..3 * DVD_SECTOR + cadt.len()].copy_from_slice(&cadt);
img
}
#[test]
fn composite_vts_roundtrip() {
let img = make_composite_vts();
let vts = VtsIfo::parse(&img, 1).unwrap();
assert_eq!(vts.vts_number, 1);
assert_eq!(vts.title_count, 1);
assert_eq!(vts.pgcs.len(), 1);
assert_eq!(vts.pgcs[0].number_of_cells, 5);
assert_eq!(vts.pgcs[0].number_of_programs, 3);
let t = &vts.titles[0];
assert_eq!(t.chapter_count, 3);
assert_eq!(t.chapters[0].start_cell, 1);
assert_eq!(t.chapters[0].end_cell, 2);
assert_eq!(t.chapters[1].start_cell, 3);
assert_eq!(t.chapters[1].end_cell, 4);
assert_eq!(t.chapters[2].start_cell, 5);
assert_eq!(t.chapters[2].end_cell, 5);
assert_eq!(vts.cell_adt.lookup(1, 1), Some((1000, 1999)));
assert_eq!(vts.cell_adt.lookup(1, 5), Some((5000, 5999)));
}
}