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)
}
pub fn to_nanoseconds(self) -> u64 {
let secs = u64::from(self.total_seconds());
let secs_ns = secs.saturating_mul(1_000_000_000);
let frames_ns = match self.frame_rate {
FrameRate::Ntsc30 => u64::from(self.frames).saturating_mul(1_000_000_000) / 30,
FrameRate::Pal25 => u64::from(self.frames).saturating_mul(1_000_000_000) / 25,
FrameRate::Illegal | FrameRate::Reserved => 0,
};
secs_ns.saturating_add(frames_ns)
}
}
#[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,
pub menu_attributes: MenuAttributes,
}
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)?;
let menu_attributes = parse_menu_attribute_block(buf, 0x0100)?;
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,
menu_attributes,
})
}
}
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,
}
impl DvdTitleEntry {
#[inline]
pub fn uop_mask(&self) -> crate::uops::UopMask {
crate::uops::title_type_uop_mask(self.title_type)
}
#[inline]
pub fn is_user_op_allowed(&self, op: crate::uops::UserOp) -> bool {
self.uop_mask().is_allowed(op)
}
}
#[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, Copy, PartialEq, Eq)]
pub enum VideoCodingMode {
Mpeg1,
Mpeg2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoStandard {
Ntsc,
Pal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoAspectRatio {
Ratio4x3,
Ratio16x9,
Reserved(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoResolution {
FullD1,
ThreeQuarterD1,
HalfD1,
Sif,
Reserved(u8),
}
impl VideoResolution {
pub fn dimensions(self, standard: VideoStandard) -> Option<(u16, u16)> {
let h = match standard {
VideoStandard::Ntsc => 480,
VideoStandard::Pal => 576,
};
let w = match self {
VideoResolution::FullD1 => 720,
VideoResolution::ThreeQuarterD1 => 704,
VideoResolution::HalfD1 => 352,
VideoResolution::Sif => 352,
VideoResolution::Reserved(_) => return None,
};
let h = if matches!(self, VideoResolution::Sif) && matches!(standard, VideoStandard::Ntsc) {
240
} else if matches!(self, VideoResolution::Sif) && matches!(standard, VideoStandard::Pal) {
288
} else {
h
};
Some((w, h))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VideoAttributes {
pub raw: [u8; 2],
pub coding_mode: VideoCodingMode,
pub standard: VideoStandard,
pub aspect_ratio: VideoAspectRatio,
pub pan_scan_disallowed: bool,
pub letterbox_disallowed: bool,
pub line21_field1_cc: bool,
pub line21_field2_cc: bool,
pub resolution_code: u8,
pub resolution: VideoResolution,
pub letterboxed_source: bool,
pub film_source_pal: bool,
}
impl VideoAttributes {
pub fn parse(buf: &[u8; 2]) -> Self {
let b0 = buf[0];
let b1 = buf[1];
let coding_mode = match (b0 >> 6) & 0b11 {
0 => VideoCodingMode::Mpeg1,
_ => VideoCodingMode::Mpeg2,
};
let standard = match (b0 >> 4) & 0b11 {
0 => VideoStandard::Ntsc,
_ => VideoStandard::Pal,
};
let aspect_ratio = match (b0 >> 2) & 0b11 {
0 => VideoAspectRatio::Ratio4x3,
3 => VideoAspectRatio::Ratio16x9,
x => VideoAspectRatio::Reserved(x),
};
let resolution_code = (b1 >> 3) & 0b111;
let resolution = match resolution_code {
0 => VideoResolution::FullD1,
1 => VideoResolution::ThreeQuarterD1,
2 => VideoResolution::HalfD1,
3 => VideoResolution::Sif,
x => VideoResolution::Reserved(x),
};
Self {
raw: *buf,
coding_mode,
standard,
aspect_ratio,
pan_scan_disallowed: (b0 & 0b0000_0010) != 0,
letterbox_disallowed: (b0 & 0b0000_0001) != 0,
line21_field1_cc: (b1 & 0b1000_0000) != 0,
line21_field2_cc: (b1 & 0b0100_0000) != 0,
resolution_code,
resolution,
letterboxed_source: (b1 & 0b0000_0100) != 0,
film_source_pal: (b1 & 0b0000_0001) != 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioCodingMode {
Ac3,
Mpeg1,
Mpeg2Ext,
Lpcm,
Dts,
Reserved(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioApplicationMode {
Unspecified,
Karaoke,
Surround,
Reserved(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioQuantizationDrc {
Lpcm16,
Lpcm20,
Lpcm24,
NoDrc,
Drc,
Raw(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioLanguageType {
Unspecified,
Iso639,
Reserved(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AudioAttributes {
pub raw: [u8; 8],
pub coding_mode: AudioCodingMode,
pub multichannel_extension_present: bool,
pub language_type: AudioLanguageType,
pub application_mode: AudioApplicationMode,
pub quantization: AudioQuantizationDrc,
pub sample_rate_code: u8,
pub channel_count: u8,
pub language_code: [u8; 2],
pub code_extension: u8,
pub application_info: u8,
}
impl AudioAttributes {
pub fn parse(buf: &[u8; 8]) -> Self {
let b0 = buf[0];
let b1 = buf[1];
let coding_mode_raw = (b0 >> 5) & 0b111;
let coding_mode = match coding_mode_raw {
0 => AudioCodingMode::Ac3,
2 => AudioCodingMode::Mpeg1,
3 => AudioCodingMode::Mpeg2Ext,
4 => AudioCodingMode::Lpcm,
6 => AudioCodingMode::Dts,
x => AudioCodingMode::Reserved(x),
};
let language_type = match (b0 >> 2) & 0b11 {
0 => AudioLanguageType::Unspecified,
1 => AudioLanguageType::Iso639,
x => AudioLanguageType::Reserved(x),
};
let application_mode = match b0 & 0b11 {
0 => AudioApplicationMode::Unspecified,
1 => AudioApplicationMode::Karaoke,
2 => AudioApplicationMode::Surround,
x => AudioApplicationMode::Reserved(x),
};
let quant_raw = (b1 >> 6) & 0b11;
let quantization = match coding_mode {
AudioCodingMode::Lpcm => match quant_raw {
0 => AudioQuantizationDrc::Lpcm16,
1 => AudioQuantizationDrc::Lpcm20,
2 => AudioQuantizationDrc::Lpcm24,
_ => AudioQuantizationDrc::Raw(quant_raw),
},
AudioCodingMode::Mpeg1 | AudioCodingMode::Mpeg2Ext => match quant_raw {
0 => AudioQuantizationDrc::NoDrc,
1 => AudioQuantizationDrc::Drc,
_ => AudioQuantizationDrc::Raw(quant_raw),
},
_ => AudioQuantizationDrc::Raw(quant_raw),
};
Self {
raw: *buf,
coding_mode,
multichannel_extension_present: (b0 & 0b0001_0000) != 0,
language_type,
application_mode,
quantization,
sample_rate_code: (b1 >> 4) & 0b11,
channel_count: (b1 & 0b0000_0111).saturating_add(1),
language_code: [buf[2], buf[3]],
code_extension: buf[5],
application_info: buf[7],
}
}
pub fn sample_rate_hz(self) -> Option<u32> {
match self.sample_rate_code {
0 => Some(48_000),
_ => None,
}
}
pub fn dolby_surround_suitable(self) -> bool {
matches!(self.application_mode, AudioApplicationMode::Surround)
&& (self.application_info & 0b0000_1000) != 0
}
pub fn karaoke_channel_assignment(self) -> Option<u8> {
if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
Some((self.application_info >> 4) & 0b0000_0111)
} else {
None
}
}
pub fn karaoke_version(self) -> Option<u8> {
if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
Some((self.application_info >> 2) & 0b11)
} else {
None
}
}
pub fn karaoke_mc_intro_present(self) -> Option<bool> {
if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
Some((self.application_info & 0b10) != 0)
} else {
None
}
}
pub fn karaoke_duet(self) -> Option<bool> {
if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
Some((self.application_info & 0b01) != 0)
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubpictureCodingMode {
Rle2Bit,
Reserved(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubpictureLanguageType {
Unspecified,
Iso639,
Reserved(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubpictureAttributes {
pub raw: [u8; 6],
pub coding_mode: SubpictureCodingMode,
pub language_type: SubpictureLanguageType,
pub language_code: [u8; 2],
pub code_extension: u8,
}
impl SubpictureAttributes {
pub fn parse(buf: &[u8; 6]) -> Self {
let b0 = buf[0];
let coding_mode = match (b0 >> 5) & 0b111 {
0 => SubpictureCodingMode::Rle2Bit,
x => SubpictureCodingMode::Reserved(x),
};
let language_type = match b0 & 0b11 {
0 => SubpictureLanguageType::Unspecified,
1 => SubpictureLanguageType::Iso639,
x => SubpictureLanguageType::Reserved(x),
};
Self {
raw: *buf,
coding_mode,
language_type,
language_code: [buf[2], buf[3]],
code_extension: buf[5],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct McExtensionEntry {
pub raw: [u8; 8],
pub ach0_guide_melody: bool,
pub ach1_guide_melody: bool,
pub ach2_guide_vocal_1: bool,
pub ach2_guide_vocal_2: bool,
pub ach2_guide_melody_1: bool,
pub ach2_guide_melody_2: bool,
pub ach3_guide_vocal_1: bool,
pub ach3_guide_vocal_2: bool,
pub ach3_guide_melody_a: bool,
pub ach3_sound_effect_a: bool,
pub ach4_guide_vocal_1: bool,
pub ach4_guide_vocal_2: bool,
pub ach4_guide_melody_b: bool,
pub ach4_sound_effect_b: bool,
}
impl McExtensionEntry {
pub fn parse(buf: &[u8; 8]) -> Self {
let b0 = buf[0];
let b1 = buf[1];
let b2 = buf[2];
let b3 = buf[3];
let b4 = buf[4];
Self {
raw: *buf,
ach0_guide_melody: (b0 & 0b0000_0001) != 0,
ach1_guide_melody: (b1 & 0b0000_0001) != 0,
ach2_guide_vocal_1: (b2 & 0b0000_1000) != 0,
ach2_guide_vocal_2: (b2 & 0b0000_0100) != 0,
ach2_guide_melody_1: (b2 & 0b0000_0010) != 0,
ach2_guide_melody_2: (b2 & 0b0000_0001) != 0,
ach3_guide_vocal_1: (b3 & 0b0000_1000) != 0,
ach3_guide_vocal_2: (b3 & 0b0000_0100) != 0,
ach3_guide_melody_a: (b3 & 0b0000_0010) != 0,
ach3_sound_effect_a: (b3 & 0b0000_0001) != 0,
ach4_guide_vocal_1: (b4 & 0b0000_1000) != 0,
ach4_guide_vocal_2: (b4 & 0b0000_0100) != 0,
ach4_guide_melody_b: (b4 & 0b0000_0010) != 0,
ach4_sound_effect_b: (b4 & 0b0000_0001) != 0,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MenuAttributes {
pub video: Option<VideoAttributes>,
pub audio_streams: Vec<AudioAttributes>,
pub subpicture_streams: Vec<SubpictureAttributes>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TitleAttributes {
pub video: Option<VideoAttributes>,
pub audio_streams: Vec<AudioAttributes>,
pub subpicture_streams: Vec<SubpictureAttributes>,
pub multichannel_extension: Vec<McExtensionEntry>,
}
#[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,
pub menu_attributes: MenuAttributes,
pub title_attributes: TitleAttributes,
}
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"));
}
let menu_attributes = parse_menu_attribute_block(buf, 0x0100)?;
let title_attributes = parse_title_attribute_block(buf)?;
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)?,
menu_attributes,
title_attributes,
})
}
}
fn parse_menu_attribute_block(buf: &[u8], block_off: usize) -> Result<MenuAttributes> {
if buf.len() < block_off + 0x04 {
return Ok(MenuAttributes::default());
}
let video = read_video_attr(buf, block_off)?;
let audio_count = read_u16(buf, block_off + 0x02)? as usize;
let mut audio_streams = Vec::new();
if buf.len() >= block_off + 0x04 + 8 * 8 {
for i in 0..audio_count.min(8) {
audio_streams.push(read_audio_attr(buf, block_off + 0x04 + i * 8)?);
}
}
let subp_count_off = block_off + 0x54;
let mut subpicture_streams = Vec::new();
if buf.len() >= subp_count_off + 2 + 6 {
let subp_count = read_u16(buf, subp_count_off)? as usize;
if subp_count >= 1 {
subpicture_streams.push(read_subp_attr(buf, subp_count_off + 2)?);
}
}
Ok(MenuAttributes {
video: Some(video),
audio_streams,
subpicture_streams,
})
}
fn parse_title_attribute_block(buf: &[u8]) -> Result<TitleAttributes> {
if buf.len() < 0x0204 {
return Ok(TitleAttributes::default());
}
let video = read_video_attr(buf, 0x0200)?;
let audio_count = read_u16(buf, 0x0202)? as usize;
let mut audio_streams = Vec::new();
if buf.len() >= 0x0204 + 8 * 8 {
for i in 0..audio_count.min(8) {
audio_streams.push(read_audio_attr(buf, 0x0204 + i * 8)?);
}
}
let mut subpicture_streams = Vec::new();
if buf.len() >= 0x0256 + 32 * 6 {
let subp_count = read_u16(buf, 0x0254)? as usize;
for i in 0..subp_count.min(32) {
subpicture_streams.push(read_subp_attr(buf, 0x0256 + i * 6)?);
}
}
let mut multichannel_extension = Vec::new();
if buf.len() >= 0x03D8 {
for i in 0..24 {
let off = 0x0318 + i * 8;
let slice: [u8; 8] = buf[off..off + 8]
.try_into()
.map_err(|_| Error::InvalidUdf("VTSI_MAT: MC ext slice"))?;
multichannel_extension.push(McExtensionEntry::parse(&slice));
}
}
Ok(TitleAttributes {
video: Some(video),
audio_streams,
subpicture_streams,
multichannel_extension,
})
}
fn read_video_attr(buf: &[u8], off: usize) -> Result<VideoAttributes> {
let slice = buf
.get(off..off + 2)
.ok_or(Error::InvalidUdf("ifo: video attr read past end"))?;
Ok(VideoAttributes::parse(&[slice[0], slice[1]]))
}
fn read_audio_attr(buf: &[u8], off: usize) -> Result<AudioAttributes> {
let slice = buf
.get(off..off + 8)
.ok_or(Error::InvalidUdf("ifo: audio attr read past end"))?;
let arr: [u8; 8] = slice
.try_into()
.map_err(|_| Error::InvalidUdf("ifo: audio attr slice"))?;
Ok(AudioAttributes::parse(&arr))
}
fn read_subp_attr(buf: &[u8], off: usize) -> Result<SubpictureAttributes> {
let slice = buf
.get(off..off + 6)
.ok_or(Error::InvalidUdf("ifo: subp attr read past end"))?;
let arr: [u8; 6] = slice
.try_into()
.map_err(|_| Error::InvalidUdf("ifo: subp attr slice"))?;
Ok(SubpictureAttributes::parse(&arr))
}
#[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
}
#[inline]
pub fn decode_instruction(&self) -> crate::nav::NavInstruction {
self.decode()
}
}
#[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,
})
}
pub fn pre_instructions(&self) -> impl Iterator<Item = crate::nav::NavInstruction> + '_ {
self.pre.iter().map(NavCommand::decode_instruction)
}
pub fn post_instructions(&self) -> impl Iterator<Item = crate::nav::NavInstruction> + '_ {
self.post.iter().map(NavCommand::decode_instruction)
}
pub fn cell_instructions(&self) -> impl Iterator<Item = crate::nav::NavInstruction> + '_ {
self.cell.iter().map(NavCommand::decode_instruction)
}
pub fn cell_instruction(&self, index_1based: u16) -> Option<crate::nav::NavInstruction> {
if index_1based == 0 {
return None;
}
let idx = usize::from(index_1based - 1);
self.cell.get(idx).map(NavCommand::decode_instruction)
}
}
#[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,
})
}
#[inline]
pub fn uop_mask(&self) -> crate::uops::UopMask {
crate::uops::UopMask::from_bits(self.prohibited_user_ops)
}
#[inline]
pub fn is_user_op_allowed(&self, op: crate::uops::UserOp) -> bool {
self.uop_mask().is_allowed(op)
}
}
#[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,
})
}
}
pub mod menu_existence {
pub const ROOT: u8 = 0x80;
pub const SUBPICTURE: u8 = 0x40;
pub const AUDIO: u8 = 0x20;
pub const ANGLE: u8 = 0x10;
pub const PTT: u8 = 0x08;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuType {
Title,
Root,
Subpicture,
Audio,
Angle,
Ptt,
Unknown(u8),
}
impl MenuType {
pub fn from_nibble(n: u8) -> Self {
match n & 0x0F {
2 => MenuType::Title,
3 => MenuType::Root,
4 => MenuType::Subpicture,
5 => MenuType::Audio,
6 => MenuType::Angle,
7 => MenuType::Ptt,
other => MenuType::Unknown(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PgciLuSrp {
pub category: u32,
pub offset: u32,
}
impl PgciLuSrp {
pub fn is_entry_pgc(&self) -> bool {
(self.category >> 24) & 0x80 != 0
}
pub fn menu_type(&self) -> MenuType {
MenuType::from_nibble((self.category >> 24) as u8)
}
pub fn parental_mask(&self) -> u16 {
(self.category & 0xFFFF) as u16
}
}
#[derive(Debug, Clone)]
pub struct PgciLu {
pub number_of_pgcs: u16,
pub end_address: u32,
pub srp: Vec<PgciLuSrp>,
pub pgcs: Vec<Pgc>,
}
impl PgciLu {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("PGCI_LU: 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
.checked_add(
n.checked_mul(8)
.ok_or(Error::InvalidUdf("PGCI_LU: SRP list × 8 overflow"))?,
)
.ok_or(Error::InvalidUdf("PGCI_LU: SRP list size overflow"))?;
if buf.len() < srp_end {
return Err(Error::InvalidUdf("PGCI_LU: 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(PgciLuSrp {
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_LU: PGC offset out of range"));
}
pgcs.push(Pgc::parse(&buf[off..])?);
}
Ok(Self {
number_of_pgcs,
end_address,
srp,
pgcs,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PgciUtSrp {
pub language_code: u16,
pub language_code_ext: u8,
pub menu_existence: u8,
pub offset: u32,
}
impl PgciUtSrp {
pub fn has_root_menu(&self) -> bool {
self.menu_existence & menu_existence::ROOT != 0
}
pub fn has_subpicture_menu(&self) -> bool {
self.menu_existence & menu_existence::SUBPICTURE != 0
}
pub fn has_audio_menu(&self) -> bool {
self.menu_existence & menu_existence::AUDIO != 0
}
pub fn has_angle_menu(&self) -> bool {
self.menu_existence & menu_existence::ANGLE != 0
}
pub fn has_ptt_menu(&self) -> bool {
self.menu_existence & menu_existence::PTT != 0
}
}
#[derive(Debug, Clone)]
pub struct PgciUt {
pub number_of_language_units: u16,
pub end_address: u32,
pub srp: Vec<PgciUtSrp>,
pub language_units: Vec<PgciLu>,
}
impl PgciUt {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("PGCI_UT: shorter than 8-byte header"));
}
let number_of_language_units = read_u16(buf, 0)?;
let end_address = read_u32(buf, 4)?;
let n = usize::from(number_of_language_units);
let srp_end = 8usize
.checked_add(
n.checked_mul(8)
.ok_or(Error::InvalidUdf("PGCI_UT: SRP list × 8 overflow"))?,
)
.ok_or(Error::InvalidUdf("PGCI_UT: SRP list size overflow"))?;
if buf.len() < srp_end {
return Err(Error::InvalidUdf("PGCI_UT: 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(PgciUtSrp {
language_code: read_u16(buf, base)?,
language_code_ext: buf[base + 2],
menu_existence: buf[base + 3],
offset: read_u32(buf, base + 4)?,
});
}
let mut language_units = 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_UT: LU offset out of range"));
}
language_units.push(PgciLu::parse(&buf[off..])?);
}
Ok(Self {
number_of_language_units,
end_address,
srp,
language_units,
})
}
pub fn language_unit(&self, language_code: u16) -> Option<&PgciLu> {
self.srp
.iter()
.position(|s| s.language_code == language_code)
.and_then(|i| self.language_units.get(i))
}
}
#[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 VobuAdmap {
pub end_address: u32,
pub entries: Vec<u32>,
}
impl VobuAdmap {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 4 {
return Err(Error::InvalidUdf("VOBU_ADMAP: shorter than 4-byte header"));
}
let end_address = read_u32(buf, 0)?;
let body_bytes = (end_address as usize).saturating_add(1).saturating_sub(4);
if body_bytes % 4 != 0 {
return Err(Error::InvalidUdf(
"VOBU_ADMAP: end_address implies non-4-byte entry size",
));
}
let n = body_bytes / 4;
let needed = 4 + n * 4;
if buf.len() < needed {
return Err(Error::InvalidUdf(
"VOBU_ADMAP: buffer shorter than entry table",
));
}
let mut entries = Vec::with_capacity(n);
for i in 0..n {
entries.push(read_u32(buf, 4 + i * 4)?);
}
Ok(Self {
end_address,
entries,
})
}
#[inline]
pub fn vobu_count(&self) -> usize {
self.entries.len()
}
pub fn vobu_start_sector(&self, vobu_number: u32) -> Option<u32> {
if vobu_number == 0 {
return None;
}
self.entries.get((vobu_number - 1) as usize).copied()
}
pub fn vobu_containing(&self, sector: u32) -> Option<u32> {
if self.entries.is_empty() {
return None;
}
let idx = self.entries.partition_point(|&v| v <= sector);
if idx == 0 {
None
} else {
Some(idx as u32)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TmapEntry {
pub sector: u32,
pub discontinuous: bool,
}
impl TmapEntry {
pub const DISCONTINUITY_BIT: u32 = 1 << 31;
pub const SECTOR_MASK: u32 = 0x7FFF_FFFF;
fn from_raw(raw: u32) -> Self {
Self {
sector: raw & Self::SECTOR_MASK,
discontinuous: (raw & Self::DISCONTINUITY_BIT) != 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VtsTmap {
pub time_unit: u8,
pub entries: Vec<TmapEntry>,
}
impl VtsTmap {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 4 {
return Err(Error::InvalidUdf("VTS_TMAP: shorter than 4-byte header"));
}
let time_unit = read_u8(buf, 0)?;
let number_of_entries = read_u16(buf, 2)?;
let n = usize::from(number_of_entries);
let needed = 4usize.saturating_add(n * 4);
if buf.len() < needed {
return Err(Error::InvalidUdf(
"VTS_TMAP: buffer shorter than entry table",
));
}
let mut entries = Vec::with_capacity(n);
for i in 0..n {
let raw = read_u32(buf, 4 + i * 4)?;
entries.push(TmapEntry::from_raw(raw));
}
Ok(Self { time_unit, entries })
}
pub fn sector_at(&self, seconds: u32) -> Option<u32> {
if self.entries.is_empty() || self.time_unit == 0 {
return None;
}
let step = u32::from(self.time_unit);
let idx_zero_based = (seconds / step) as usize;
let idx = idx_zero_based.min(self.entries.len() - 1);
Some(self.entries[idx].sector)
}
pub fn total_seconds(&self) -> u32 {
u32::from(self.time_unit) * self.entries.len() as u32
}
}
#[derive(Debug, Clone)]
pub struct VtsTmapti {
pub number_of_pgcs: u16,
pub end_address: u32,
pub maps: Vec<VtsTmap>,
}
impl VtsTmapti {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("VTS_TMAPTI: 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 offsets_end = 8usize.saturating_add(n * 4);
if buf.len() < offsets_end {
return Err(Error::InvalidUdf(
"VTS_TMAPTI: offset list past end of buffer",
));
}
let mut offsets = Vec::with_capacity(n);
for i in 0..n {
offsets.push(read_u32(buf, 8 + i * 4)? as usize);
}
let mut maps = Vec::with_capacity(n);
for off in &offsets {
let tmap_buf = buf.get(*off..).ok_or(Error::InvalidUdf(
"VTS_TMAPTI: VTS_TMAP offset past end of buffer",
))?;
maps.push(VtsTmap::parse(tmap_buf)?);
}
Ok(Self {
number_of_pgcs,
end_address,
maps,
})
}
pub fn get(&self, pgcn: u16) -> Option<&VtsTmap> {
if pgcn == 0 {
return None;
}
self.maps.get((pgcn - 1) as usize)
}
}
#[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 vobu_admap: Option<VobuAdmap>,
pub time_map: Option<VtsTmapti>,
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 vobu_admap = if mat.vts_vobu_admap_sector != 0 {
let off = (mat.vts_vobu_admap_sector as usize)
.checked_mul(DVD_SECTOR)
.ok_or(Error::InvalidUdf("VTSI: VOBU_ADMAP sector overflow"))?;
let body = buf
.get(off..)
.ok_or(Error::InvalidUdf("VTSI: VOBU_ADMAP sector past end"))?;
Some(VobuAdmap::parse(body)?)
} else {
None
};
let time_map = if mat.vts_tmapti_sector != 0 {
let off = (mat.vts_tmapti_sector as usize)
.checked_mul(DVD_SECTOR)
.ok_or(Error::InvalidUdf("VTSI: TMAPTI sector overflow"))?;
let body = buf
.get(off..)
.ok_or(Error::InvalidUdf("VTSI: TMAPTI sector past end"))?;
Some(VtsTmapti::parse(body)?)
} else {
None
};
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,
vobu_admap,
time_map,
mat,
})
}
pub fn vobu_sector_at_pgc_time(&self, pgcn: u16, seconds: u32) -> Option<u32> {
self.time_map
.as_ref()
.and_then(|t| t.get(pgcn))
.and_then(|m| m.sector_at(seconds))
}
}
#[derive(Debug, Clone)]
pub struct VmgVtsAtrtEntry {
pub vts_number: u8,
pub vts_category: u32,
pub attributes_blob: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct VmgVtsAtrt {
pub number_of_title_sets: u16,
pub end_address: u32,
pub entries: Vec<VmgVtsAtrtEntry>,
}
impl VmgVtsAtrt {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("VMG_VTS_ATRT: header < 8 bytes"));
}
let number_of_title_sets = read_u16(buf, 0)?;
let end_address = read_u32(buf, 4)?;
let count = usize::from(number_of_title_sets);
let offset_table_len = count.checked_mul(4).ok_or(Error::InvalidUdf(
"VMG_VTS_ATRT: title-set count × 4 overflow",
))?;
let header_len = 8usize
.checked_add(offset_table_len)
.ok_or(Error::InvalidUdf("VMG_VTS_ATRT: header length overflow"))?;
if buf.len() < header_len {
return Err(Error::InvalidUdf("VMG_VTS_ATRT: offset table past end"));
}
let mut offsets = Vec::with_capacity(count);
for i in 0..count {
offsets.push(read_u32(buf, 8 + i * 4)?);
}
let mut entries = Vec::with_capacity(count);
for (i, &off) in offsets.iter().enumerate() {
let entry_start = off as usize;
let entry_hdr = buf
.get(entry_start..entry_start + 8)
.ok_or(Error::InvalidUdf("VMG_VTS_ATRT: entry header past buffer"))?;
let entry_ea_rel =
u32::from_be_bytes([entry_hdr[0], entry_hdr[1], entry_hdr[2], entry_hdr[3]])
as usize;
let vts_category =
u32::from_be_bytes([entry_hdr[4], entry_hdr[5], entry_hdr[6], entry_hdr[7]]);
let entry_total = entry_ea_rel
.checked_add(1)
.ok_or(Error::InvalidUdf("VMG_VTS_ATRT: entry EA overflow"))?;
if entry_total < 8 {
return Err(Error::InvalidUdf(
"VMG_VTS_ATRT: entry length shorter than 8-byte header",
));
}
let next_start = offsets
.get(i + 1)
.map(|&n| n as usize)
.unwrap_or((end_address as usize).saturating_add(1));
if entry_start.saturating_add(entry_total) > next_start {
return Err(Error::InvalidUdf(
"VMG_VTS_ATRT: entry EA overlaps next entry",
));
}
let blob_end = entry_start
.checked_add(entry_total)
.ok_or(Error::InvalidUdf("VMG_VTS_ATRT: entry end overflow"))?;
let blob = buf
.get(entry_start + 8..blob_end)
.ok_or(Error::InvalidUdf("VMG_VTS_ATRT: blob past buffer"))?
.to_vec();
entries.push(VmgVtsAtrtEntry {
vts_number: (i as u8).saturating_add(1),
vts_category,
attributes_blob: blob,
});
}
Ok(Self {
number_of_title_sets,
end_address,
entries,
})
}
pub fn entry(&self, vts_number: u8) -> Option<&VmgVtsAtrtEntry> {
if vts_number == 0 {
return None;
}
self.entries.get(usize::from(vts_number - 1))
}
}
#[derive(Debug, Clone)]
pub struct PtlMait {
pub country_code: u16,
pub masks: [Vec<u16>; 8],
}
impl PtlMait {
pub fn mask(&self, parental_level: u8, title_set: u8) -> Option<u16> {
if !(1..=8).contains(&parental_level) {
return None;
}
let level_idx = usize::from(parental_level - 1);
self.masks[level_idx].get(usize::from(title_set)).copied()
}
}
#[derive(Debug, Clone)]
pub struct VmgPtlMait {
pub number_of_countries: u16,
pub number_of_title_sets: u16,
pub end_address: u32,
pub entries: Vec<PtlMait>,
}
impl VmgPtlMait {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(Error::InvalidUdf("VMG_PTL_MAIT: header < 8 bytes"));
}
let number_of_countries = read_u16(buf, 0)?;
let number_of_title_sets = read_u16(buf, 2)?;
let end_address = read_u32(buf, 4)?;
let nts = usize::from(number_of_title_sets);
let masks_per_level = nts
.checked_add(1)
.ok_or(Error::InvalidUdf("VMG_PTL_MAIT: nts + 1 overflow"))?;
let level_block_bytes = masks_per_level
.checked_mul(2)
.ok_or(Error::InvalidUdf("VMG_PTL_MAIT: level block size overflow"))?;
let body_bytes = level_block_bytes
.checked_mul(8)
.ok_or(Error::InvalidUdf("VMG_PTL_MAIT: body size overflow"))?;
let count = usize::from(number_of_countries);
let header_len = 8usize
.checked_add(
count
.checked_mul(8)
.ok_or(Error::InvalidUdf("VMG_PTL_MAIT: country list × 8 overflow"))?,
)
.ok_or(Error::InvalidUdf("VMG_PTL_MAIT: header length overflow"))?;
if buf.len() < header_len {
return Err(Error::InvalidUdf("VMG_PTL_MAIT: country list past end"));
}
let mut entries = Vec::with_capacity(count);
for i in 0..count {
let entry_base = 8 + i * 8;
let country_code = read_u16(buf, entry_base)?;
let body_offset = usize::from(read_u16(buf, entry_base + 4)?);
let body_end = body_offset
.checked_add(body_bytes)
.ok_or(Error::InvalidUdf("VMG_PTL_MAIT: country body end overflow"))?;
if body_end > buf.len() {
return Err(Error::InvalidUdf(
"VMG_PTL_MAIT: country body past buffer end",
));
}
let mut masks: [Vec<u16>; 8] = Default::default();
for level_storage_idx in 0..8 {
let level_value = 8 - level_storage_idx; let block_start = body_offset + level_storage_idx * level_block_bytes;
let mut row = Vec::with_capacity(masks_per_level);
for slot in 0..masks_per_level {
row.push(read_u16(buf, block_start + slot * 2)?);
}
masks[level_value - 1] = row;
}
entries.push(PtlMait {
country_code,
masks,
});
}
Ok(Self {
number_of_countries,
number_of_title_sets,
end_address,
entries,
})
}
pub fn country(&self, country_code: u16) -> Option<&PtlMait> {
self.entries.iter().find(|e| e.country_code == country_code)
}
}
#[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);
}
#[test]
fn pgc_time_to_ns_ntsc_30_integer_seconds() {
let t = PgcTime::from_bytes([0x00, 0x00, 0x01, 0xC0]);
assert_eq!(t.frame_rate, FrameRate::Ntsc30);
assert_eq!(t.to_nanoseconds(), 1_000_000_000);
}
#[test]
fn pgc_time_to_ns_ntsc_30_half_second() {
let t = PgcTime::from_bytes([0x00, 0x00, 0x01, 0xD5]);
assert_eq!(t.frame_rate, FrameRate::Ntsc30);
assert_eq!(t.frames, 15);
assert_eq!(t.to_nanoseconds(), 1_500_000_000);
}
#[test]
fn pgc_time_to_ns_pal_25_frame_period() {
let t = PgcTime::from_bytes([0x00, 0x00, 0x00, 0x41]);
assert_eq!(t.frame_rate, FrameRate::Pal25);
assert_eq!(t.frames, 1);
assert_eq!(t.to_nanoseconds(), 40_000_000);
}
#[test]
fn pgc_time_to_ns_illegal_rate_drops_frames() {
let t = PgcTime::from_bytes([0x00, 0x00, 0x02, 0x07]);
assert_eq!(t.frame_rate, FrameRate::Illegal);
assert_eq!(t.frames, 7);
assert_eq!(t.to_nanoseconds(), 2_000_000_000);
}
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());
}
fn synth_command_table() -> PgcCommandTable {
let pre = 1u16;
let post = 1u16;
let cell = 3u16;
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());
let post_off = 8 + 8;
b[post_off] = 0b0011_0000;
b[post_off + 1] = 0x01;
let cell0 = post_off + 8;
b[cell0] = 0b0011_0000;
b[cell0 + 1] = 0x01;
let cell1 = cell0 + 8;
b[cell1] = 0x00;
b[cell1 + 1] = 0x02;
PgcCommandTable::parse(&b).unwrap()
}
#[test]
fn pgc_cmd_table_typed_iterators_walk_all_three_lists() {
let t = synth_command_table();
let pre: Vec<_> = t.pre_instructions().collect();
let post: Vec<_> = t.post_instructions().collect();
let cell: Vec<_> = t.cell_instructions().collect();
assert_eq!(pre.len(), 1);
assert_eq!(post.len(), 1);
assert_eq!(cell.len(), 3);
assert!(matches!(pre[0], crate::nav::NavInstruction::Nop));
assert!(matches!(post[0], crate::nav::NavInstruction::Exit));
assert!(matches!(cell[0], crate::nav::NavInstruction::Exit));
assert!(matches!(cell[1], crate::nav::NavInstruction::Break));
assert!(matches!(cell[2], crate::nav::NavInstruction::Nop));
}
#[test]
fn pgc_cmd_table_cell_instruction_uses_one_based_index() {
let t = synth_command_table();
assert!(t.cell_instruction(0).is_none());
assert!(matches!(
t.cell_instruction(1),
Some(crate::nav::NavInstruction::Exit)
));
assert!(matches!(
t.cell_instruction(2),
Some(crate::nav::NavInstruction::Break)
));
assert!(matches!(
t.cell_instruction(3),
Some(crate::nav::NavInstruction::Nop)
));
assert!(t.cell_instruction(4).is_none());
assert!(t.cell_instruction(u16::MAX).is_none());
}
#[test]
fn nav_command_decode_instruction_matches_nav_decode() {
let mut bytes = [0u8; 8];
bytes[0] = 0b0011_0000; bytes[1] = 0x01; let raw = NavCommand { bytes };
assert_eq!(raw.decode_instruction(), raw.decode());
assert!(matches!(
raw.decode_instruction(),
crate::nav::NavInstruction::Exit
));
}
#[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)));
assert!(vts.vobu_admap.is_none());
assert!(vts.time_map.is_none());
}
fn build_vobu_admap(entries: &[u32]) -> Vec<u8> {
let n = entries.len();
let len = 4 + n * 4;
let mut b = vec![0u8; len];
let end_addr = (len - 1) as u32;
b[0..4].copy_from_slice(&end_addr.to_be_bytes());
for (i, s) in entries.iter().enumerate() {
b[4 + i * 4..4 + (i + 1) * 4].copy_from_slice(&s.to_be_bytes());
}
b
}
#[test]
fn vobu_admap_parses_three_vobus() {
let entries = [0u32, 200u32, 450u32];
let buf = build_vobu_admap(&entries);
let map = VobuAdmap::parse(&buf).unwrap();
assert_eq!(map.vobu_count(), 3);
assert_eq!(map.entries, entries);
assert_eq!(map.end_address, (buf.len() - 1) as u32);
assert_eq!(map.vobu_start_sector(1), Some(0));
assert_eq!(map.vobu_start_sector(2), Some(200));
assert_eq!(map.vobu_start_sector(3), Some(450));
assert_eq!(map.vobu_start_sector(4), None);
assert_eq!(map.vobu_start_sector(0), None);
}
#[test]
fn vobu_admap_partition_locates_containing_vobu() {
let buf = build_vobu_admap(&[0, 100, 300, 600]);
let map = VobuAdmap::parse(&buf).unwrap();
assert_eq!(map.vobu_containing(0), Some(1));
assert_eq!(map.vobu_containing(99), Some(1));
assert_eq!(map.vobu_containing(100), Some(2));
assert_eq!(map.vobu_containing(299), Some(2));
assert_eq!(map.vobu_containing(300), Some(3));
assert_eq!(map.vobu_containing(599), Some(3));
assert_eq!(map.vobu_containing(600), Some(4));
assert_eq!(map.vobu_containing(1_000_000), Some(4));
}
#[test]
fn vobu_admap_first_entry_above_zero_returns_none_for_pre_sector() {
let buf = build_vobu_admap(&[100, 200, 300]);
let map = VobuAdmap::parse(&buf).unwrap();
assert_eq!(map.vobu_containing(0), None);
assert_eq!(map.vobu_containing(99), None);
assert_eq!(map.vobu_containing(100), Some(1));
}
#[test]
fn vobu_admap_empty_map_lookups_return_none() {
let mut b = vec![0u8; 4];
b[0..4].copy_from_slice(&3u32.to_be_bytes());
let map = VobuAdmap::parse(&b).unwrap();
assert_eq!(map.vobu_count(), 0);
assert_eq!(map.vobu_start_sector(1), None);
assert_eq!(map.vobu_containing(0), None);
}
#[test]
fn vobu_admap_rejects_non_multiple_end_address() {
let mut b = vec![0u8; 8];
b[0..4].copy_from_slice(&5u32.to_be_bytes());
assert!(VobuAdmap::parse(&b).is_err());
}
#[test]
fn vobu_admap_rejects_truncated_buffer() {
let mut b = vec![0u8; 4 + 4];
b[0..4].copy_from_slice(&11u32.to_be_bytes());
assert!(VobuAdmap::parse(&b).is_err());
}
fn build_tmap(time_unit: u8, entries: &[(u32, bool)]) -> Vec<u8> {
let n = entries.len();
let len = 4 + n * 4;
let mut b = vec![0u8; len];
b[0] = time_unit;
b[2..4].copy_from_slice(&(n as u16).to_be_bytes());
for (i, (sector, disc)) in entries.iter().enumerate() {
let mut raw = *sector & TmapEntry::SECTOR_MASK;
if *disc {
raw |= TmapEntry::DISCONTINUITY_BIT;
}
b[4 + i * 4..4 + (i + 1) * 4].copy_from_slice(&raw.to_be_bytes());
}
b
}
#[test]
fn tmap_decodes_entries_and_discontinuity() {
let buf = build_tmap(4, &[(100, false), (250, true), (400, false)]);
let map = VtsTmap::parse(&buf).unwrap();
assert_eq!(map.time_unit, 4);
assert_eq!(map.entries.len(), 3);
assert_eq!(
map.entries[0],
TmapEntry {
sector: 100,
discontinuous: false
}
);
assert_eq!(
map.entries[1],
TmapEntry {
sector: 250,
discontinuous: true
}
);
assert_eq!(map.total_seconds(), 12);
}
#[test]
fn tmap_sector_at_brackets_seconds_per_time_unit() {
let buf = build_tmap(5, &[(10, false), (20, false), (30, false)]);
let map = VtsTmap::parse(&buf).unwrap();
assert_eq!(map.sector_at(0), Some(10));
assert_eq!(map.sector_at(4), Some(10));
assert_eq!(map.sector_at(5), Some(20));
assert_eq!(map.sector_at(9), Some(20));
assert_eq!(map.sector_at(10), Some(30));
assert_eq!(map.sector_at(14), Some(30));
assert_eq!(map.sector_at(15), Some(30));
assert_eq!(map.sector_at(1_000_000), Some(30));
}
#[test]
fn tmap_empty_map_yields_no_sector() {
let buf = build_tmap(2, &[]);
let map = VtsTmap::parse(&buf).unwrap();
assert_eq!(map.time_unit, 2);
assert!(map.entries.is_empty());
assert_eq!(map.sector_at(0), None);
assert_eq!(map.sector_at(60), None);
assert_eq!(map.total_seconds(), 0);
}
#[test]
fn tmap_zero_time_unit_yields_no_sector() {
let buf = build_tmap(0, &[(1, false), (2, false)]);
let map = VtsTmap::parse(&buf).unwrap();
assert_eq!(map.sector_at(0), None);
}
#[test]
fn tmap_rejects_truncated_buffer() {
let mut b = vec![0u8; 4 + 4];
b[0] = 1;
b[2..4].copy_from_slice(&2u16.to_be_bytes());
assert!(VtsTmap::parse(&b).is_err());
}
fn build_tmapti(maps: &[Vec<u8>]) -> Vec<u8> {
let n = maps.len();
let offsets_size = n * 4;
let header_size = 8 + offsets_size;
let body_size: usize = maps.iter().map(|m| m.len()).sum();
let total = header_size + body_size;
let mut b = vec![0u8; total];
b[0..2].copy_from_slice(&(n as u16).to_be_bytes());
b[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
let mut cursor = header_size;
for (i, m) in maps.iter().enumerate() {
let off = cursor as u32;
b[8 + i * 4..8 + (i + 1) * 4].copy_from_slice(&off.to_be_bytes());
b[cursor..cursor + m.len()].copy_from_slice(m);
cursor += m.len();
}
b
}
#[test]
fn tmapti_walks_two_pgc_maps() {
let map_a = build_tmap(2, &[(0, false), (50, false), (100, false)]);
let map_b = build_tmap(3, &[(200, false), (400, true)]);
let buf = build_tmapti(&[map_a, map_b]);
let table = VtsTmapti::parse(&buf).unwrap();
assert_eq!(table.number_of_pgcs, 2);
assert_eq!(table.maps.len(), 2);
let m1 = table.get(1).unwrap();
assert_eq!(m1.time_unit, 2);
assert_eq!(m1.entries.len(), 3);
let m2 = table.get(2).unwrap();
assert_eq!(m2.time_unit, 3);
assert!(m2.entries[1].discontinuous);
assert_eq!(table.get(0), None);
assert_eq!(table.get(3), None);
}
#[test]
fn tmapti_carries_empty_map_per_spec_invariant() {
let empty = build_tmap(0, &[]);
let buf = build_tmapti(&[empty]);
let table = VtsTmapti::parse(&buf).unwrap();
assert_eq!(table.number_of_pgcs, 1);
let m = table.get(1).unwrap();
assert!(m.entries.is_empty());
assert_eq!(m.sector_at(0), None);
}
#[test]
fn tmapti_rejects_short_offset_list() {
let mut b = vec![0u8; 8];
b[0..2].copy_from_slice(&3u16.to_be_bytes());
assert!(VtsTmapti::parse(&b).is_err());
}
fn make_composite_vts_with_admap_and_tmap() -> Vec<u8> {
let mut img = vec![0u8; DVD_SECTOR * 6];
let mat = build_vtsi_mat(1, 2, 3, 100);
img[0..mat.len()].copy_from_slice(&mat);
img[0x00D4..0x00D8].copy_from_slice(&5u32.to_be_bytes()); img[0x00E4..0x00E8].copy_from_slice(&4u32.to_be_bytes());
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);
let admap = build_vobu_admap(&[0, 250, 600, 1100]);
img[4 * DVD_SECTOR..4 * DVD_SECTOR + admap.len()].copy_from_slice(&admap);
let tmap = build_tmap(4, &[(0, false), (250, false), (600, false)]);
let tmapti = build_tmapti(&[tmap]);
img[5 * DVD_SECTOR..5 * DVD_SECTOR + tmapti.len()].copy_from_slice(&tmapti);
img
}
#[test]
fn composite_vts_materialises_admap_and_tmap() {
let img = make_composite_vts_with_admap_and_tmap();
let vts = VtsIfo::parse(&img, 1).unwrap();
let admap = vts.vobu_admap.as_ref().expect("VOBU_ADMAP materialised");
assert_eq!(admap.vobu_count(), 4);
assert_eq!(admap.vobu_start_sector(1), Some(0));
assert_eq!(admap.vobu_start_sector(4), Some(1100));
assert_eq!(admap.vobu_containing(700), Some(3));
let tmapti = vts.time_map.as_ref().expect("VTS_TMAPTI materialised");
assert_eq!(tmapti.number_of_pgcs, 1);
let pgc_map = tmapti.get(1).unwrap();
assert_eq!(pgc_map.time_unit, 4);
assert_eq!(pgc_map.entries.len(), 3);
assert_eq!(vts.vobu_sector_at_pgc_time(1, 5), Some(250));
assert_eq!(vts.vobu_sector_at_pgc_time(1, 0), Some(0));
assert_eq!(vts.vobu_sector_at_pgc_time(1, 9), Some(600));
assert_eq!(vts.vobu_sector_at_pgc_time(2, 0), None);
}
#[allow(clippy::too_many_arguments)]
fn pack_audio_attr(
coding: u8,
mc_ext: bool,
lang_type: u8,
app_mode: u8,
quant: u8,
sample_rate: u8,
chans_minus_one: u8,
lang: [u8; 2],
code_ext: u8,
app_info: u8,
) -> [u8; 8] {
let mut b = [0u8; 8];
b[0] = ((coding & 0b111) << 5)
| (if mc_ext { 0b1_0000 } else { 0 })
| ((lang_type & 0b11) << 2)
| (app_mode & 0b11);
b[1] = ((quant & 0b11) << 6) | ((sample_rate & 0b11) << 4) | (chans_minus_one & 0b111);
b[2] = lang[0];
b[3] = lang[1];
b[5] = code_ext;
b[7] = app_info;
b
}
#[allow(clippy::too_many_arguments)]
fn pack_video_attr(
coding: u8,
standard: u8,
aspect: u8,
pan_scan_disallowed: bool,
letterbox_disallowed: bool,
cc1: bool,
cc2: bool,
resolution: u8,
letterbox_src: bool,
film_pal: bool,
) -> [u8; 2] {
let mut b = [0u8; 2];
b[0] = ((coding & 0b11) << 6)
| ((standard & 0b11) << 4)
| ((aspect & 0b11) << 2)
| (if pan_scan_disallowed { 0b10 } else { 0 })
| (if letterbox_disallowed { 0b01 } else { 0 });
b[1] = (if cc1 { 0b1000_0000 } else { 0 })
| (if cc2 { 0b0100_0000 } else { 0 })
| ((resolution & 0b111) << 3)
| (if letterbox_src { 0b0000_0100 } else { 0 })
| (if film_pal { 0b0000_0001 } else { 0 });
b
}
fn pack_subp_attr(coding: u8, lang_type: u8, lang: [u8; 2], code_ext: u8) -> [u8; 6] {
let mut b = [0u8; 6];
b[0] = ((coding & 0b111) << 5) | (lang_type & 0b11);
b[2] = lang[0];
b[3] = lang[1];
b[5] = code_ext;
b
}
#[test]
fn video_attributes_mpeg2_pal_16x9_full_d1() {
let raw = pack_video_attr(1, 1, 3, false, false, false, false, 0, false, true);
let v = VideoAttributes::parse(&raw);
assert_eq!(v.coding_mode, VideoCodingMode::Mpeg2);
assert_eq!(v.standard, VideoStandard::Pal);
assert_eq!(v.aspect_ratio, VideoAspectRatio::Ratio16x9);
assert_eq!(v.resolution, VideoResolution::FullD1);
assert_eq!(
v.resolution.dimensions(VideoStandard::Pal),
Some((720, 576))
);
assert!(v.film_source_pal);
assert!(!v.line21_field1_cc);
}
#[test]
fn video_attributes_mpeg2_ntsc_4x3_sif_with_cc() {
let raw = pack_video_attr(1, 0, 0, true, false, true, true, 3, true, false);
let v = VideoAttributes::parse(&raw);
assert_eq!(v.coding_mode, VideoCodingMode::Mpeg2);
assert_eq!(v.standard, VideoStandard::Ntsc);
assert_eq!(v.aspect_ratio, VideoAspectRatio::Ratio4x3);
assert_eq!(v.resolution, VideoResolution::Sif);
assert_eq!(
v.resolution.dimensions(VideoStandard::Ntsc),
Some((352, 240))
);
assert!(v.pan_scan_disallowed);
assert!(v.line21_field1_cc);
assert!(v.line21_field2_cc);
assert!(v.letterboxed_source);
}
#[test]
fn video_attributes_reserved_aspect_and_resolution() {
let raw = pack_video_attr(1, 0, 1, false, false, false, false, 5, false, false);
let v = VideoAttributes::parse(&raw);
assert_eq!(v.aspect_ratio, VideoAspectRatio::Reserved(1));
assert_eq!(v.resolution, VideoResolution::Reserved(5));
assert_eq!(v.resolution.dimensions(VideoStandard::Ntsc), None);
}
#[test]
fn audio_attributes_ac3_stereo_english() {
let raw = pack_audio_attr(0, false, 1, 0, 0, 0, 1, *b"en", 0, 0);
let a = AudioAttributes::parse(&raw);
assert_eq!(a.coding_mode, AudioCodingMode::Ac3);
assert!(!a.multichannel_extension_present);
assert_eq!(a.language_type, AudioLanguageType::Iso639);
assert_eq!(a.application_mode, AudioApplicationMode::Unspecified);
assert_eq!(a.channel_count, 2);
assert_eq!(a.sample_rate_hz(), Some(48_000));
assert_eq!(&a.language_code, b"en");
assert!(!a.dolby_surround_suitable());
}
#[test]
fn audio_attributes_lpcm_24bit_six_channel() {
let raw = pack_audio_attr(4, false, 0, 0, 2, 0, 5, [0, 0], 0, 0);
let a = AudioAttributes::parse(&raw);
assert_eq!(a.coding_mode, AudioCodingMode::Lpcm);
assert_eq!(a.quantization, AudioQuantizationDrc::Lpcm24);
assert_eq!(a.channel_count, 6);
}
#[test]
fn audio_attributes_mpeg2_drc_flag() {
let raw = pack_audio_attr(3, false, 0, 0, 1, 0, 1, [0, 0], 0, 0);
let a = AudioAttributes::parse(&raw);
assert_eq!(a.coding_mode, AudioCodingMode::Mpeg2Ext);
assert_eq!(a.quantization, AudioQuantizationDrc::Drc);
}
#[test]
fn audio_attributes_surround_dolby_suitable_bit() {
let raw = pack_audio_attr(0, false, 0, 2, 0, 0, 1, [0, 0], 0, 0b0000_1000);
let a = AudioAttributes::parse(&raw);
assert_eq!(a.application_mode, AudioApplicationMode::Surround);
assert!(a.dolby_surround_suitable());
}
#[test]
fn audio_attributes_karaoke_channel_assignment_3_0() {
let raw = pack_audio_attr(0, true, 0, 1, 0, 0, 2, [0, 0], 0, 0b0011_0010);
let a = AudioAttributes::parse(&raw);
assert_eq!(a.application_mode, AudioApplicationMode::Karaoke);
assert!(a.multichannel_extension_present);
assert_eq!(a.karaoke_channel_assignment(), Some(3));
assert_eq!(a.karaoke_mc_intro_present(), Some(true));
assert_eq!(a.karaoke_duet(), Some(false));
}
#[test]
fn subpicture_attributes_2bit_rle_japanese() {
let raw = pack_subp_attr(0, 1, *b"ja", 0);
let s = SubpictureAttributes::parse(&raw);
assert_eq!(s.coding_mode, SubpictureCodingMode::Rle2Bit);
assert_eq!(s.language_type, SubpictureLanguageType::Iso639);
assert_eq!(&s.language_code, b"ja");
}
#[test]
fn mc_extension_entry_decodes_per_channel_flags() {
let raw = [
0b0000_0001, 0b0000_0000,
0b0000_1010, 0b0000_0101, 0b0000_1001, 0,
0,
0,
];
let m = McExtensionEntry::parse(&raw);
assert!(m.ach0_guide_melody);
assert!(!m.ach1_guide_melody);
assert!(m.ach2_guide_vocal_1);
assert!(m.ach2_guide_melody_1);
assert!(m.ach3_guide_vocal_2);
assert!(m.ach3_sound_effect_a);
assert!(m.ach4_guide_vocal_1);
assert!(m.ach4_sound_effect_b);
assert!(!m.ach4_guide_melody_b);
}
fn build_vtsi_mat_with_attrs() -> Vec<u8> {
let mut b = vec![0u8; 0x03D8];
b[0..12].copy_from_slice(VTS_MAGIC);
b[0x0080..0x0084].copy_from_slice(&(0x03D7u32).to_be_bytes());
let v_menu = pack_video_attr(1, 1, 0, false, false, false, false, 0, false, false);
b[0x0100..0x0102].copy_from_slice(&v_menu);
b[0x0102..0x0104].copy_from_slice(&1u16.to_be_bytes());
let a_menu = pack_audio_attr(2, false, 1, 0, 0, 0, 1, *b"en", 1, 0);
b[0x0104..0x010C].copy_from_slice(&a_menu);
b[0x0154..0x0156].copy_from_slice(&1u16.to_be_bytes());
let s_menu = pack_subp_attr(0, 1, *b"ja", 0);
b[0x0156..0x015C].copy_from_slice(&s_menu);
let v_title = pack_video_attr(1, 0, 3, false, false, false, false, 0, false, false);
b[0x0200..0x0202].copy_from_slice(&v_title);
b[0x0202..0x0204].copy_from_slice(&2u16.to_be_bytes());
let a0 = pack_audio_attr(0, false, 1, 0, 0, 0, 5, *b"en", 1, 0);
let a1 = pack_audio_attr(0, false, 1, 0, 0, 0, 1, *b"fr", 1, 0);
b[0x0204..0x020C].copy_from_slice(&a0);
b[0x020C..0x0214].copy_from_slice(&a1);
b[0x0254..0x0256].copy_from_slice(&2u16.to_be_bytes());
let s0 = pack_subp_attr(0, 1, *b"en", 0);
let s1 = pack_subp_attr(0, 1, *b"de", 0);
b[0x0256..0x025C].copy_from_slice(&s0);
b[0x025C..0x0262].copy_from_slice(&s1);
b
}
#[test]
fn vtsi_mat_decodes_menu_and_title_attribute_blocks() {
let buf = build_vtsi_mat_with_attrs();
let mat = VtsiMat::parse(&buf).unwrap();
let menu_v = mat.menu_attributes.video.unwrap();
assert_eq!(menu_v.standard, VideoStandard::Pal);
assert_eq!(menu_v.aspect_ratio, VideoAspectRatio::Ratio4x3);
assert_eq!(mat.menu_attributes.audio_streams.len(), 1);
assert_eq!(
mat.menu_attributes.audio_streams[0].coding_mode,
AudioCodingMode::Mpeg1
);
assert_eq!(mat.menu_attributes.subpicture_streams.len(), 1);
assert_eq!(
&mat.menu_attributes.subpicture_streams[0].language_code,
b"ja"
);
let title_v = mat.title_attributes.video.unwrap();
assert_eq!(title_v.standard, VideoStandard::Ntsc);
assert_eq!(title_v.aspect_ratio, VideoAspectRatio::Ratio16x9);
assert_eq!(mat.title_attributes.audio_streams.len(), 2);
assert_eq!(mat.title_attributes.audio_streams[0].channel_count, 6);
assert_eq!(&mat.title_attributes.audio_streams[1].language_code, b"fr");
assert_eq!(mat.title_attributes.subpicture_streams.len(), 2);
assert_eq!(
&mat.title_attributes.subpicture_streams[1].language_code,
b"de"
);
assert_eq!(mat.title_attributes.multichannel_extension.len(), 24);
assert!(!mat.title_attributes.multichannel_extension[0].ach0_guide_melody);
}
#[test]
fn vtsi_mat_short_buffer_leaves_attributes_partial() {
let buf = build_vtsi_mat(1, 2, 3, 42);
let mat = VtsiMat::parse(&buf).unwrap();
let menu_v = mat.menu_attributes.video.unwrap();
assert_eq!(menu_v.coding_mode, VideoCodingMode::Mpeg1);
assert!(mat.title_attributes.video.is_none());
assert!(mat.title_attributes.audio_streams.is_empty());
assert!(mat.title_attributes.multichannel_extension.is_empty());
}
fn build_vts_atrt(entries: &[(u32, Vec<u8>)]) -> Vec<u8> {
let header = 8 + 4 * entries.len();
let mut offsets = Vec::with_capacity(entries.len());
let mut bodies = Vec::with_capacity(entries.len());
let mut cursor = header;
for (cat, blob) in entries {
offsets.push(cursor as u32);
let entry_total = 8 + blob.len();
let entry_ea = (entry_total - 1) as u32;
let mut e = Vec::with_capacity(entry_total);
e.extend_from_slice(&entry_ea.to_be_bytes());
e.extend_from_slice(&cat.to_be_bytes());
e.extend_from_slice(blob);
bodies.push(e);
cursor += entry_total;
}
let total = cursor;
let mut out = vec![0u8; total];
out[0..2].copy_from_slice(&(entries.len() as u16).to_be_bytes());
out[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
for (i, off) in offsets.iter().enumerate() {
out[8 + i * 4..8 + (i + 1) * 4].copy_from_slice(&off.to_be_bytes());
}
for (i, body) in bodies.iter().enumerate() {
let start = offsets[i] as usize;
out[start..start + body.len()].copy_from_slice(body);
}
out
}
#[test]
fn vts_atrt_walks_two_entries() {
let blob_a = (0..0x300u32).map(|i| (i & 0xFF) as u8).collect::<Vec<_>>();
let blob_b = vec![0x55u8; 0x300];
let buf = build_vts_atrt(&[(0, blob_a.clone()), (1, blob_b.clone())]);
let atrt = VmgVtsAtrt::parse(&buf).unwrap();
assert_eq!(atrt.number_of_title_sets, 2);
assert_eq!(atrt.entries.len(), 2);
let e1 = atrt.entry(1).unwrap();
assert_eq!(e1.vts_number, 1);
assert_eq!(e1.vts_category, 0);
assert_eq!(e1.attributes_blob, blob_a);
let e2 = atrt.entry(2).unwrap();
assert_eq!(e2.vts_number, 2);
assert_eq!(e2.vts_category, 1); assert_eq!(e2.attributes_blob, blob_b);
assert!(atrt.entry(0).is_none());
assert!(atrt.entry(3).is_none());
}
#[test]
fn vts_atrt_rejects_short_header() {
let buf = vec![0u8; 4];
assert!(VmgVtsAtrt::parse(&buf).is_err());
}
#[test]
fn vts_atrt_rejects_offset_list_past_end() {
let mut buf = vec![0u8; 8];
buf[0..2].copy_from_slice(&5u16.to_be_bytes()); assert!(VmgVtsAtrt::parse(&buf).is_err());
}
#[test]
fn vts_atrt_rejects_entry_ea_overlapping_next_entry() {
let blob = vec![0xAAu8; 0x100];
let mut buf = build_vts_atrt(&[(0, blob.clone()), (0, blob.clone())]);
let e1_off = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
buf[e1_off..e1_off + 4].copy_from_slice(&0xFFFFu32.to_be_bytes());
assert!(VmgVtsAtrt::parse(&buf).is_err());
}
fn build_ptl_mait(
country_codes: &[u16],
nts: u16,
masks_per_country: &[[Vec<u16>; 8]],
) -> Vec<u8> {
assert_eq!(country_codes.len(), masks_per_country.len());
let header_len = 8 + 8 * country_codes.len();
let level_block_bytes = (usize::from(nts) + 1) * 2;
let body_bytes = level_block_bytes * 8;
let total = header_len + body_bytes * country_codes.len();
let mut buf = vec![0u8; total];
buf[0..2].copy_from_slice(&(country_codes.len() as u16).to_be_bytes());
buf[2..4].copy_from_slice(&nts.to_be_bytes());
buf[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
for (i, (&cc, masks)) in country_codes
.iter()
.zip(masks_per_country.iter())
.enumerate()
{
let body_offset = header_len + i * body_bytes;
let entry_base = 8 + i * 8;
buf[entry_base..entry_base + 2].copy_from_slice(&cc.to_be_bytes());
buf[entry_base + 4..entry_base + 6]
.copy_from_slice(&(body_offset as u16).to_be_bytes());
for storage_idx in 0..8usize {
let level_value = 8 - storage_idx; let row = &masks[level_value - 1]; assert_eq!(row.len(), usize::from(nts) + 1);
let block_start = body_offset + storage_idx * level_block_bytes;
for (slot, &m) in row.iter().enumerate() {
buf[block_start + slot * 2..block_start + slot * 2 + 2]
.copy_from_slice(&m.to_be_bytes());
}
}
}
buf
}
#[test]
fn ptl_mait_walks_two_countries() {
let mut country_a_masks: [Vec<u16>; 8] = Default::default();
let mut country_b_masks: [Vec<u16>; 8] = Default::default();
for lvl_idx in 0..8 {
let lvl = (lvl_idx + 1) as u16;
country_a_masks[lvl_idx] = vec![lvl, lvl + 0x10, lvl + 0x20];
country_b_masks[lvl_idx] =
vec![0xF000 | lvl, 0xF000 | (lvl + 0x10), 0xF000 | (lvl + 0x20)];
}
let buf = build_ptl_mait(
&[0x7553, 0x6A70],
2,
&[country_a_masks.clone(), country_b_masks.clone()],
);
let pm = VmgPtlMait::parse(&buf).unwrap();
assert_eq!(pm.number_of_countries, 2);
assert_eq!(pm.number_of_title_sets, 2);
assert_eq!(pm.entries.len(), 2);
let us = pm.country(0x7553).expect("US country present");
assert_eq!(us.mask(1, 0), Some(1));
assert_eq!(us.mask(1, 1), Some(0x11));
assert_eq!(us.mask(1, 2), Some(0x21));
assert_eq!(us.mask(8, 2), Some(0x28));
let jp = pm.country(0x6A70).unwrap();
assert_eq!(jp.mask(1, 0), Some(0xF001));
assert_eq!(jp.mask(8, 2), Some(0xF028));
assert!(pm.country(0x4242).is_none());
assert_eq!(us.mask(0, 0), None);
assert_eq!(us.mask(9, 0), None);
assert_eq!(us.mask(1, 3), None);
}
#[test]
fn ptl_mait_zero_countries_decodes_empty() {
let buf = build_ptl_mait(&[], 2, &[]);
let pm = VmgPtlMait::parse(&buf).unwrap();
assert_eq!(pm.number_of_countries, 0);
assert!(pm.entries.is_empty());
}
#[test]
fn ptl_mait_rejects_short_header() {
let buf = vec![0u8; 4];
assert!(VmgPtlMait::parse(&buf).is_err());
}
#[test]
fn ptl_mait_rejects_country_list_past_end() {
let mut buf = vec![0u8; 8];
buf[0..2].copy_from_slice(&5u16.to_be_bytes()); buf[2..4].copy_from_slice(&2u16.to_be_bytes());
assert!(VmgPtlMait::parse(&buf).is_err());
}
#[test]
fn ptl_mait_rejects_body_offset_past_buffer() {
let mut buf = vec![0u8; 16];
buf[0..2].copy_from_slice(&1u16.to_be_bytes());
buf[2..4].copy_from_slice(&2u16.to_be_bytes());
buf[4..8].copy_from_slice(&63u32.to_be_bytes());
buf[8..10].copy_from_slice(&0x7553u16.to_be_bytes());
buf[12..14].copy_from_slice(&16u16.to_be_bytes()); assert!(VmgPtlMait::parse(&buf).is_err());
}
fn empty_pgc_body() -> Vec<u8> {
vec![0u8; 0xEC]
}
fn build_pgci_ut(units: &[(u16, u8, u8, Vec<u32>)]) -> Vec<u8> {
let header_len = 8 + 8 * units.len();
let lu_lens: Vec<usize> = units
.iter()
.map(|(_, _, _, pgcs)| 8 + pgcs.len() * 8 + pgcs.len() * 0xEC)
.collect();
let total: usize = header_len + lu_lens.iter().sum::<usize>();
let mut buf = vec![0u8; total];
buf[0..2].copy_from_slice(&(units.len() as u16).to_be_bytes());
buf[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
let mut lu_offset = header_len;
for (i, ((lang, ext, exist, pgcs), &lu_len)) in units.iter().zip(lu_lens.iter()).enumerate()
{
let srp_base = 8 + i * 8;
buf[srp_base..srp_base + 2].copy_from_slice(&lang.to_be_bytes());
buf[srp_base + 2] = *ext;
buf[srp_base + 3] = *exist;
buf[srp_base + 4..srp_base + 8].copy_from_slice(&(lu_offset as u32).to_be_bytes());
buf[lu_offset..lu_offset + 2].copy_from_slice(&(pgcs.len() as u16).to_be_bytes());
buf[lu_offset + 4..lu_offset + 8].copy_from_slice(&((lu_len - 1) as u32).to_be_bytes());
let inner_srp_end = 8 + pgcs.len() * 8;
for (j, &cat) in pgcs.iter().enumerate() {
let inner_base = lu_offset + 8 + j * 8;
buf[inner_base..inner_base + 4].copy_from_slice(&cat.to_be_bytes());
let pgc_offset_in_lu = (inner_srp_end + j * 0xEC) as u32;
buf[inner_base + 4..inner_base + 8]
.copy_from_slice(&pgc_offset_in_lu.to_be_bytes());
let pgc_global = lu_offset + pgc_offset_in_lu as usize;
let empty = empty_pgc_body();
buf[pgc_global..pgc_global + 0xEC].copy_from_slice(&empty);
}
lu_offset += lu_len;
}
buf
}
#[test]
fn pgci_ut_walks_two_language_units() {
let units = vec![
(
0x656E, 0,
menu_existence::ROOT | menu_existence::AUDIO,
vec![0x83_00_00_00, 0x00_00_00_00],
),
(
0x6A70, 0,
menu_existence::ROOT,
vec![0x82_00_00_00, 0x00_00_00_00],
),
];
let buf = build_pgci_ut(&units);
let table = PgciUt::parse(&buf).unwrap();
assert_eq!(table.number_of_language_units, 2);
assert_eq!(table.srp.len(), 2);
assert_eq!(table.language_units.len(), 2);
let en_srp = &table.srp[0];
assert_eq!(en_srp.language_code, 0x656E);
assert!(en_srp.has_root_menu());
assert!(en_srp.has_audio_menu());
assert!(!en_srp.has_subpicture_menu());
assert!(!en_srp.has_angle_menu());
assert!(!en_srp.has_ptt_menu());
let en_lu = &table.language_units[0];
assert_eq!(en_lu.number_of_pgcs, 2);
assert_eq!(en_lu.srp.len(), 2);
assert_eq!(en_lu.pgcs.len(), 2);
assert!(en_lu.srp[0].is_entry_pgc());
assert_eq!(en_lu.srp[0].menu_type(), MenuType::Root);
assert!(!en_lu.srp[1].is_entry_pgc());
let jp_srp = &table.srp[1];
assert_eq!(jp_srp.language_code, 0x6A70);
let jp_lu = &table.language_units[1];
assert_eq!(jp_lu.srp[0].menu_type(), MenuType::Title);
assert_eq!(
table.language_unit(0x656E).map(|lu| lu.number_of_pgcs),
Some(2)
);
assert!(table.language_unit(0x4242).is_none());
}
#[test]
fn pgci_ut_zero_language_units_decodes_empty() {
let buf = build_pgci_ut(&[]);
let table = PgciUt::parse(&buf).unwrap();
assert_eq!(table.number_of_language_units, 0);
assert!(table.srp.is_empty());
assert!(table.language_units.is_empty());
}
#[test]
fn pgci_ut_rejects_short_header() {
let buf = vec![0u8; 4];
assert!(PgciUt::parse(&buf).is_err());
}
#[test]
fn pgci_ut_rejects_srp_list_past_buffer() {
let mut buf = vec![0u8; 8];
buf[0..2].copy_from_slice(&5u16.to_be_bytes());
assert!(PgciUt::parse(&buf).is_err());
}
#[test]
fn pgci_ut_rejects_lu_offset_zero() {
let mut buf = vec![0u8; 16];
buf[0..2].copy_from_slice(&1u16.to_be_bytes());
buf[8..10].copy_from_slice(&0x656Eu16.to_be_bytes());
buf[11] = menu_existence::ROOT;
assert!(PgciUt::parse(&buf).is_err());
}
#[test]
fn pgci_ut_rejects_lu_offset_past_buffer() {
let mut buf = vec![0u8; 16];
buf[0..2].copy_from_slice(&1u16.to_be_bytes());
buf[8..10].copy_from_slice(&0x656Eu16.to_be_bytes());
buf[11] = menu_existence::ROOT;
buf[12..16].copy_from_slice(&64u32.to_be_bytes()); assert!(PgciUt::parse(&buf).is_err());
}
#[test]
fn pgci_lu_rejects_pgc_offset_past_buffer() {
let mut buf = vec![0u8; 16];
buf[0..2].copy_from_slice(&1u16.to_be_bytes());
buf[12..16].copy_from_slice(&512u32.to_be_bytes());
assert!(PgciLu::parse(&buf).is_err());
}
#[test]
fn pgci_lu_srp_decodes_parental_mask() {
let units = vec![(0x656E, 0, menu_existence::ROOT, vec![0x83_00_00_FF_u32])];
let buf = build_pgci_ut(&units);
let table = PgciUt::parse(&buf).unwrap();
let lu = &table.language_units[0];
assert!(lu.srp[0].is_entry_pgc());
assert_eq!(lu.srp[0].menu_type(), MenuType::Root);
assert_eq!(lu.srp[0].parental_mask(), 0x00FF);
}
#[test]
fn menu_type_unknown_for_undefined_nibble() {
assert_eq!(MenuType::from_nibble(1), MenuType::Unknown(1));
assert_eq!(MenuType::from_nibble(0), MenuType::Unknown(0));
assert_eq!(MenuType::from_nibble(0xF3), MenuType::Root);
}
}