1use crate::error::{Error, Result};
44
45pub const DVD_SECTOR: usize = 2048;
47
48pub const VMG_MAGIC: &[u8; 12] = b"DVDVIDEO-VMG";
50
51pub const VTS_MAGIC: &[u8; 12] = b"DVDVIDEO-VTS";
53
54fn read_u16(buf: &[u8], off: usize) -> Result<u16> {
59 let slice = buf
60 .get(off..off + 2)
61 .ok_or(Error::InvalidUdf("ifo: u16 read past end"))?;
62 Ok(u16::from_be_bytes([slice[0], slice[1]]))
63}
64
65fn read_u32(buf: &[u8], off: usize) -> Result<u32> {
66 let slice = buf
67 .get(off..off + 4)
68 .ok_or(Error::InvalidUdf("ifo: u32 read past end"))?;
69 Ok(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
70}
71
72fn read_u8(buf: &[u8], off: usize) -> Result<u8> {
73 buf.get(off)
74 .copied()
75 .ok_or(Error::InvalidUdf("ifo: u8 read past end"))
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct PgcTime {
92 pub hours: u8,
93 pub minutes: u8,
94 pub seconds: u8,
95 pub frames: u8,
96 pub frame_rate: FrameRate,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum FrameRate {
102 Illegal,
104 Pal25,
106 Reserved,
108 Ntsc30,
110}
111
112impl PgcTime {
113 pub fn from_bytes(bytes: [u8; 4]) -> Self {
115 fn bcd(b: u8) -> u8 {
116 ((b >> 4) & 0x0F) * 10 + (b & 0x0F)
117 }
118 let hours = bcd(bytes[0]);
119 let minutes = bcd(bytes[1]);
120 let seconds = bcd(bytes[2]);
121 let frame_rate = match (bytes[3] >> 6) & 0x03 {
126 0b00 => FrameRate::Illegal,
127 0b01 => FrameRate::Pal25,
128 0b10 => FrameRate::Reserved,
129 0b11 => FrameRate::Ntsc30,
130 _ => unreachable!(),
131 };
132 let f_lo = bytes[3] & 0x0F;
133 let f_hi = (bytes[3] >> 4) & 0x03;
134 let frames = f_hi * 10 + f_lo;
135 Self {
136 hours,
137 minutes,
138 seconds,
139 frames,
140 frame_rate,
141 }
142 }
143
144 pub fn total_seconds(self) -> u32 {
148 u32::from(self.hours) * 3600 + u32::from(self.minutes) * 60 + u32::from(self.seconds)
149 }
150
151 pub fn to_nanoseconds(self) -> u64 {
169 let secs = u64::from(self.total_seconds());
170 let secs_ns = secs.saturating_mul(1_000_000_000);
171 let frames_ns = match self.frame_rate {
172 FrameRate::Ntsc30 => u64::from(self.frames).saturating_mul(1_000_000_000) / 30,
173 FrameRate::Pal25 => u64::from(self.frames).saturating_mul(1_000_000_000) / 25,
174 FrameRate::Illegal | FrameRate::Reserved => 0,
175 };
176 secs_ns.saturating_add(frames_ns)
177 }
178}
179
180#[derive(Debug, Clone)]
190pub struct VmgIfo {
191 pub last_sector_vmg_set: u32,
193 pub last_sector_ifo: u32,
195 pub version: u16,
199 pub vmg_category: u32,
201 pub number_of_volumes: u16,
204 pub volume_number: u16,
206 pub side_id: u8,
208 pub number_of_title_sets: u16,
210 pub provider_id: String,
212 pub vmgi_mat_end: u32,
214 pub fp_pgc_addr: u32,
216 pub menu_vob_sector: u32,
218 pub tt_srpt_sector: u32,
221 pub vmgm_pgci_ut_sector: u32,
223 pub ptl_mait_sector: u32,
225 pub vts_atrt_sector: u32,
227 pub txtdt_mg_sector: u32,
229 pub vmgm_c_adt_sector: u32,
231 pub vmgm_vobu_admap_sector: u32,
233 pub menu_attributes: MenuAttributes,
237}
238
239impl VmgIfo {
240 pub fn parse(buf: &[u8]) -> Result<Self> {
243 if buf.len() < 0x200 {
244 return Err(Error::InvalidUdf("VMGI_MAT: buffer shorter than 0x200"));
245 }
246 if &buf[0..12] != VMG_MAGIC {
247 return Err(Error::InvalidUdf("VMGI_MAT: bad magic"));
248 }
249 let last_sector_vmg_set = read_u32(buf, 0x000C)?;
250 let last_sector_ifo = read_u32(buf, 0x001C)?;
251 let version = read_u16(buf, 0x0020)?;
252 let vmg_category = read_u32(buf, 0x0022)?;
253 let number_of_volumes = read_u16(buf, 0x0026)?;
254 let volume_number = read_u16(buf, 0x0028)?;
255 let side_id = read_u8(buf, 0x002A)?;
256 let number_of_title_sets = read_u16(buf, 0x003E)?;
257 let provider_id_raw = &buf[0x0040..0x0060];
258 let provider_id = decode_ascii_trim(provider_id_raw);
259 let vmgi_mat_end = read_u32(buf, 0x0080)?;
260 let fp_pgc_addr = read_u32(buf, 0x0084)?;
261 let menu_vob_sector = read_u32(buf, 0x00C0)?;
262 let tt_srpt_sector = read_u32(buf, 0x00C4)?;
263 let vmgm_pgci_ut_sector = read_u32(buf, 0x00C8)?;
264 let ptl_mait_sector = read_u32(buf, 0x00CC)?;
265 let vts_atrt_sector = read_u32(buf, 0x00D0)?;
266 let txtdt_mg_sector = read_u32(buf, 0x00D4)?;
267 let vmgm_c_adt_sector = read_u32(buf, 0x00D8)?;
268 let vmgm_vobu_admap_sector = read_u32(buf, 0x00DC)?;
269 let menu_attributes = parse_menu_attribute_block(buf, 0x0100)?;
270
271 Ok(Self {
272 last_sector_vmg_set,
273 last_sector_ifo,
274 version,
275 vmg_category,
276 number_of_volumes,
277 volume_number,
278 side_id,
279 number_of_title_sets,
280 provider_id,
281 vmgi_mat_end,
282 fp_pgc_addr,
283 menu_vob_sector,
284 tt_srpt_sector,
285 vmgm_pgci_ut_sector,
286 ptl_mait_sector,
287 vts_atrt_sector,
288 txtdt_mg_sector,
289 vmgm_c_adt_sector,
290 vmgm_vobu_admap_sector,
291 menu_attributes,
292 })
293 }
294}
295
296fn decode_ascii_trim(buf: &[u8]) -> String {
297 let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
298 String::from_utf8_lossy(&buf[..end]).trim_end().to_string()
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub struct DvdTitleEntry {
312 pub title_type: u8,
314 pub angle_count: u8,
316 pub chapter_count: u16,
318 pub parental_mask: u16,
320 pub vts_number: u8,
322 pub vts_title_number: u8,
324 pub vts_start_sector: u32,
326}
327
328impl DvdTitleEntry {
329 #[inline]
339 pub fn uop_mask(&self) -> crate::uops::UopMask {
340 crate::uops::title_type_uop_mask(self.title_type)
341 }
342
343 #[inline]
351 pub fn is_user_op_allowed(&self, op: crate::uops::UserOp) -> bool {
352 self.uop_mask().is_allowed(op)
353 }
354}
355
356#[derive(Debug, Clone)]
358pub struct TtSrpt {
359 pub title_count: u16,
361 pub end_address: u32,
363 pub entries: Vec<DvdTitleEntry>,
365}
366
367impl TtSrpt {
368 pub fn parse(buf: &[u8]) -> Result<Self> {
371 if buf.len() < 8 {
372 return Err(Error::InvalidUdf("TT_SRPT: shorter than 8-byte header"));
373 }
374 let title_count = read_u16(buf, 0)?;
375 let end_address = read_u32(buf, 4)?;
376 let needed = 8usize.saturating_add(usize::from(title_count) * 12);
377 if buf.len() < needed {
378 return Err(Error::InvalidUdf(
379 "TT_SRPT: buffer shorter than title_count*12",
380 ));
381 }
382 let mut entries = Vec::with_capacity(usize::from(title_count));
383 for i in 0..usize::from(title_count) {
384 let base = 8 + i * 12;
385 entries.push(DvdTitleEntry {
386 title_type: read_u8(buf, base)?,
387 angle_count: read_u8(buf, base + 1)?,
388 chapter_count: read_u16(buf, base + 2)?,
389 parental_mask: read_u16(buf, base + 4)?,
390 vts_number: read_u8(buf, base + 6)?,
391 vts_title_number: read_u8(buf, base + 7)?,
392 vts_start_sector: read_u32(buf, base + 8)?,
393 });
394 }
395 Ok(Self {
396 title_count,
397 end_address,
398 entries,
399 })
400 }
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419pub enum VideoCodingMode {
420 Mpeg1,
421 Mpeg2,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426pub enum VideoStandard {
427 Ntsc,
428 Pal,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
435pub enum VideoAspectRatio {
436 Ratio4x3,
438 Ratio16x9,
440 Reserved(u8),
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum VideoResolution {
450 FullD1,
452 ThreeQuarterD1,
454 HalfD1,
456 Sif,
458 Reserved(u8),
460}
461
462impl VideoResolution {
463 pub fn dimensions(self, standard: VideoStandard) -> Option<(u16, u16)> {
468 let h = match standard {
469 VideoStandard::Ntsc => 480,
470 VideoStandard::Pal => 576,
471 };
472 let w = match self {
473 VideoResolution::FullD1 => 720,
474 VideoResolution::ThreeQuarterD1 => 704,
475 VideoResolution::HalfD1 => 352,
476 VideoResolution::Sif => 352,
477 VideoResolution::Reserved(_) => return None,
478 };
479 let h = if matches!(self, VideoResolution::Sif) && matches!(standard, VideoStandard::Ntsc) {
480 240
481 } else if matches!(self, VideoResolution::Sif) && matches!(standard, VideoStandard::Pal) {
482 288
483 } else {
484 h
485 };
486 Some((w, h))
487 }
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
510pub struct VideoAttributes {
511 pub raw: [u8; 2],
513 pub coding_mode: VideoCodingMode,
514 pub standard: VideoStandard,
515 pub aspect_ratio: VideoAspectRatio,
516 pub pan_scan_disallowed: bool,
519 pub letterbox_disallowed: bool,
522 pub line21_field1_cc: bool,
525 pub line21_field2_cc: bool,
527 pub resolution_code: u8,
530 pub resolution: VideoResolution,
531 pub letterboxed_source: bool,
534 pub film_source_pal: bool,
538}
539
540impl VideoAttributes {
541 pub fn parse(buf: &[u8; 2]) -> Self {
543 let b0 = buf[0];
544 let b1 = buf[1];
545 let coding_mode = match (b0 >> 6) & 0b11 {
546 0 => VideoCodingMode::Mpeg1,
547 _ => VideoCodingMode::Mpeg2,
548 };
549 let standard = match (b0 >> 4) & 0b11 {
550 0 => VideoStandard::Ntsc,
551 _ => VideoStandard::Pal,
552 };
553 let aspect_ratio = match (b0 >> 2) & 0b11 {
554 0 => VideoAspectRatio::Ratio4x3,
555 3 => VideoAspectRatio::Ratio16x9,
556 x => VideoAspectRatio::Reserved(x),
557 };
558 let resolution_code = (b1 >> 3) & 0b111;
559 let resolution = match resolution_code {
560 0 => VideoResolution::FullD1,
561 1 => VideoResolution::ThreeQuarterD1,
562 2 => VideoResolution::HalfD1,
563 3 => VideoResolution::Sif,
564 x => VideoResolution::Reserved(x),
565 };
566 Self {
567 raw: *buf,
568 coding_mode,
569 standard,
570 aspect_ratio,
571 pan_scan_disallowed: (b0 & 0b0000_0010) != 0,
572 letterbox_disallowed: (b0 & 0b0000_0001) != 0,
573 line21_field1_cc: (b1 & 0b1000_0000) != 0,
574 line21_field2_cc: (b1 & 0b0100_0000) != 0,
575 resolution_code,
576 resolution,
577 letterboxed_source: (b1 & 0b0000_0100) != 0,
578 film_source_pal: (b1 & 0b0000_0001) != 0,
579 }
580 }
581}
582
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587pub enum AudioCodingMode {
588 Ac3,
589 Mpeg1,
590 Mpeg2Ext,
591 Lpcm,
592 Dts,
593 Reserved(u8),
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600pub enum AudioApplicationMode {
601 Unspecified,
602 Karaoke,
603 Surround,
604 Reserved(u8),
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612pub enum AudioQuantizationDrc {
613 Lpcm16,
615 Lpcm20,
617 Lpcm24,
619 NoDrc,
621 Drc,
623 Raw(u8),
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631pub enum AudioLanguageType {
632 Unspecified,
633 Iso639,
634 Reserved(u8),
635}
636
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
661pub struct AudioAttributes {
662 pub raw: [u8; 8],
663 pub coding_mode: AudioCodingMode,
664 pub multichannel_extension_present: bool,
667 pub language_type: AudioLanguageType,
668 pub application_mode: AudioApplicationMode,
669 pub quantization: AudioQuantizationDrc,
670 pub sample_rate_code: u8,
673 pub channel_count: u8,
675 pub language_code: [u8; 2],
678 pub code_extension: u8,
679 pub application_info: u8,
683}
684
685impl AudioAttributes {
686 pub fn parse(buf: &[u8; 8]) -> Self {
688 let b0 = buf[0];
689 let b1 = buf[1];
690 let coding_mode_raw = (b0 >> 5) & 0b111;
691 let coding_mode = match coding_mode_raw {
692 0 => AudioCodingMode::Ac3,
693 2 => AudioCodingMode::Mpeg1,
694 3 => AudioCodingMode::Mpeg2Ext,
695 4 => AudioCodingMode::Lpcm,
696 6 => AudioCodingMode::Dts,
697 x => AudioCodingMode::Reserved(x),
698 };
699 let language_type = match (b0 >> 2) & 0b11 {
700 0 => AudioLanguageType::Unspecified,
701 1 => AudioLanguageType::Iso639,
702 x => AudioLanguageType::Reserved(x),
703 };
704 let application_mode = match b0 & 0b11 {
705 0 => AudioApplicationMode::Unspecified,
706 1 => AudioApplicationMode::Karaoke,
707 2 => AudioApplicationMode::Surround,
708 x => AudioApplicationMode::Reserved(x),
709 };
710 let quant_raw = (b1 >> 6) & 0b11;
711 let quantization = match coding_mode {
712 AudioCodingMode::Lpcm => match quant_raw {
713 0 => AudioQuantizationDrc::Lpcm16,
714 1 => AudioQuantizationDrc::Lpcm20,
715 2 => AudioQuantizationDrc::Lpcm24,
716 _ => AudioQuantizationDrc::Raw(quant_raw),
717 },
718 AudioCodingMode::Mpeg1 | AudioCodingMode::Mpeg2Ext => match quant_raw {
719 0 => AudioQuantizationDrc::NoDrc,
720 1 => AudioQuantizationDrc::Drc,
721 _ => AudioQuantizationDrc::Raw(quant_raw),
722 },
723 _ => AudioQuantizationDrc::Raw(quant_raw),
724 };
725 Self {
726 raw: *buf,
727 coding_mode,
728 multichannel_extension_present: (b0 & 0b0001_0000) != 0,
729 language_type,
730 application_mode,
731 quantization,
732 sample_rate_code: (b1 >> 4) & 0b11,
733 channel_count: (b1 & 0b0000_0111).saturating_add(1),
734 language_code: [buf[2], buf[3]],
735 code_extension: buf[5],
736 application_info: buf[7],
737 }
738 }
739
740 pub fn sample_rate_hz(self) -> Option<u32> {
744 match self.sample_rate_code {
745 0 => Some(48_000),
746 _ => None,
747 }
748 }
749
750 pub fn dolby_surround_suitable(self) -> bool {
754 matches!(self.application_mode, AudioApplicationMode::Surround)
755 && (self.application_info & 0b0000_1000) != 0
756 }
757
758 pub fn karaoke_channel_assignment(self) -> Option<u8> {
764 if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
765 Some((self.application_info >> 4) & 0b0000_0111)
766 } else {
767 None
768 }
769 }
770
771 pub fn karaoke_version(self) -> Option<u8> {
773 if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
774 Some((self.application_info >> 2) & 0b11)
775 } else {
776 None
777 }
778 }
779
780 pub fn karaoke_mc_intro_present(self) -> Option<bool> {
782 if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
783 Some((self.application_info & 0b10) != 0)
784 } else {
785 None
786 }
787 }
788
789 pub fn karaoke_duet(self) -> Option<bool> {
791 if matches!(self.application_mode, AudioApplicationMode::Karaoke) {
792 Some((self.application_info & 0b01) != 0)
793 } else {
794 None
795 }
796 }
797}
798
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
802pub enum SubpictureCodingMode {
803 Rle2Bit,
804 Reserved(u8),
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
809pub enum SubpictureLanguageType {
810 Unspecified,
811 Iso639,
812 Reserved(u8),
813}
814
815#[derive(Debug, Clone, Copy, PartialEq, Eq)]
830pub struct SubpictureAttributes {
831 pub raw: [u8; 6],
832 pub coding_mode: SubpictureCodingMode,
833 pub language_type: SubpictureLanguageType,
834 pub language_code: [u8; 2],
835 pub code_extension: u8,
836}
837
838impl SubpictureAttributes {
839 pub fn parse(buf: &[u8; 6]) -> Self {
841 let b0 = buf[0];
842 let coding_mode = match (b0 >> 5) & 0b111 {
843 0 => SubpictureCodingMode::Rle2Bit,
844 x => SubpictureCodingMode::Reserved(x),
845 };
846 let language_type = match b0 & 0b11 {
847 0 => SubpictureLanguageType::Unspecified,
848 1 => SubpictureLanguageType::Iso639,
849 x => SubpictureLanguageType::Reserved(x),
850 };
851 Self {
852 raw: *buf,
853 coding_mode,
854 language_type,
855 language_code: [buf[2], buf[3]],
856 code_extension: buf[5],
857 }
858 }
859}
860
861#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
866pub struct McExtensionEntry {
867 pub raw: [u8; 8],
868 pub ach0_guide_melody: bool,
871 pub ach1_guide_melody: bool,
873 pub ach2_guide_vocal_1: bool,
875 pub ach2_guide_vocal_2: bool,
877 pub ach2_guide_melody_1: bool,
879 pub ach2_guide_melody_2: bool,
881 pub ach3_guide_vocal_1: bool,
883 pub ach3_guide_vocal_2: bool,
885 pub ach3_guide_melody_a: bool,
887 pub ach3_sound_effect_a: bool,
889 pub ach4_guide_vocal_1: bool,
891 pub ach4_guide_vocal_2: bool,
893 pub ach4_guide_melody_b: bool,
895 pub ach4_sound_effect_b: bool,
897}
898
899impl McExtensionEntry {
900 pub fn parse(buf: &[u8; 8]) -> Self {
905 let b0 = buf[0];
906 let b1 = buf[1];
907 let b2 = buf[2];
908 let b3 = buf[3];
909 let b4 = buf[4];
910 Self {
911 raw: *buf,
912 ach0_guide_melody: (b0 & 0b0000_0001) != 0,
913 ach1_guide_melody: (b1 & 0b0000_0001) != 0,
914 ach2_guide_vocal_1: (b2 & 0b0000_1000) != 0,
915 ach2_guide_vocal_2: (b2 & 0b0000_0100) != 0,
916 ach2_guide_melody_1: (b2 & 0b0000_0010) != 0,
917 ach2_guide_melody_2: (b2 & 0b0000_0001) != 0,
918 ach3_guide_vocal_1: (b3 & 0b0000_1000) != 0,
919 ach3_guide_vocal_2: (b3 & 0b0000_0100) != 0,
920 ach3_guide_melody_a: (b3 & 0b0000_0010) != 0,
921 ach3_sound_effect_a: (b3 & 0b0000_0001) != 0,
922 ach4_guide_vocal_1: (b4 & 0b0000_1000) != 0,
923 ach4_guide_vocal_2: (b4 & 0b0000_0100) != 0,
924 ach4_guide_melody_b: (b4 & 0b0000_0010) != 0,
925 ach4_sound_effect_b: (b4 & 0b0000_0001) != 0,
926 }
927 }
928}
929
930#[derive(Debug, Clone, Default, PartialEq, Eq)]
943pub struct MenuAttributes {
944 pub video: Option<VideoAttributes>,
945 pub audio_streams: Vec<AudioAttributes>,
946 pub subpicture_streams: Vec<SubpictureAttributes>,
947}
948
949#[derive(Debug, Clone, Default, PartialEq, Eq)]
954pub struct TitleAttributes {
955 pub video: Option<VideoAttributes>,
956 pub audio_streams: Vec<AudioAttributes>,
957 pub subpicture_streams: Vec<SubpictureAttributes>,
958 pub multichannel_extension: Vec<McExtensionEntry>,
963}
964
965#[derive(Debug, Clone)]
971pub struct VtsiMat {
972 pub last_sector_title_set: u32,
974 pub last_sector_ifo: u32,
976 pub version: u16,
978 pub vts_category: u32,
980 pub vtsi_mat_end: u32,
982 pub menu_vob_sector: u32,
984 pub title_vob_sector: u32,
986 pub vts_ptt_srpt_sector: u32,
988 pub vts_pgci_sector: u32,
990 pub vtsm_pgci_ut_sector: u32,
992 pub vts_tmapti_sector: u32,
994 pub vtsm_c_adt_sector: u32,
996 pub vtsm_vobu_admap_sector: u32,
998 pub vts_c_adt_sector: u32,
1000 pub vts_vobu_admap_sector: u32,
1002 pub menu_attributes: MenuAttributes,
1007 pub title_attributes: TitleAttributes,
1011}
1012
1013impl VtsiMat {
1014 pub fn parse(buf: &[u8]) -> Result<Self> {
1021 if buf.len() < 0x200 {
1022 return Err(Error::InvalidUdf("VTSI_MAT: buffer shorter than 0x200"));
1023 }
1024 if &buf[0..12] != VTS_MAGIC {
1025 return Err(Error::InvalidUdf("VTSI_MAT: bad magic"));
1026 }
1027 let menu_attributes = parse_menu_attribute_block(buf, 0x0100)?;
1028 let title_attributes = parse_title_attribute_block(buf)?;
1029 Ok(Self {
1030 last_sector_title_set: read_u32(buf, 0x000C)?,
1031 last_sector_ifo: read_u32(buf, 0x001C)?,
1032 version: read_u16(buf, 0x0020)?,
1033 vts_category: read_u32(buf, 0x0022)?,
1034 vtsi_mat_end: read_u32(buf, 0x0080)?,
1035 menu_vob_sector: read_u32(buf, 0x00C0)?,
1036 title_vob_sector: read_u32(buf, 0x00C4)?,
1037 vts_ptt_srpt_sector: read_u32(buf, 0x00C8)?,
1038 vts_pgci_sector: read_u32(buf, 0x00CC)?,
1039 vtsm_pgci_ut_sector: read_u32(buf, 0x00D0)?,
1040 vts_tmapti_sector: read_u32(buf, 0x00D4)?,
1041 vtsm_c_adt_sector: read_u32(buf, 0x00D8)?,
1042 vtsm_vobu_admap_sector: read_u32(buf, 0x00DC)?,
1043 vts_c_adt_sector: read_u32(buf, 0x00E0)?,
1044 vts_vobu_admap_sector: read_u32(buf, 0x00E4)?,
1045 menu_attributes,
1046 title_attributes,
1047 })
1048 }
1049}
1050
1051fn parse_menu_attribute_block(buf: &[u8], block_off: usize) -> Result<MenuAttributes> {
1065 if buf.len() < block_off + 0x04 {
1067 return Ok(MenuAttributes::default());
1068 }
1069 let video = read_video_attr(buf, block_off)?;
1070 let audio_count = read_u16(buf, block_off + 0x02)? as usize;
1071 let mut audio_streams = Vec::new();
1072 if buf.len() >= block_off + 0x04 + 8 * 8 {
1073 for i in 0..audio_count.min(8) {
1074 audio_streams.push(read_audio_attr(buf, block_off + 0x04 + i * 8)?);
1075 }
1076 }
1077 let subp_count_off = block_off + 0x54;
1078 let mut subpicture_streams = Vec::new();
1079 if buf.len() >= subp_count_off + 2 + 6 {
1080 let subp_count = read_u16(buf, subp_count_off)? as usize;
1081 if subp_count >= 1 {
1082 subpicture_streams.push(read_subp_attr(buf, subp_count_off + 2)?);
1083 }
1084 }
1085 Ok(MenuAttributes {
1086 video: Some(video),
1087 audio_streams,
1088 subpicture_streams,
1089 })
1090}
1091
1092fn parse_title_attribute_block(buf: &[u8]) -> Result<TitleAttributes> {
1107 if buf.len() < 0x0204 {
1108 return Ok(TitleAttributes::default());
1109 }
1110 let video = read_video_attr(buf, 0x0200)?;
1111 let audio_count = read_u16(buf, 0x0202)? as usize;
1112 let mut audio_streams = Vec::new();
1113 if buf.len() >= 0x0204 + 8 * 8 {
1114 for i in 0..audio_count.min(8) {
1115 audio_streams.push(read_audio_attr(buf, 0x0204 + i * 8)?);
1116 }
1117 }
1118 let mut subpicture_streams = Vec::new();
1119 if buf.len() >= 0x0256 + 32 * 6 {
1120 let subp_count = read_u16(buf, 0x0254)? as usize;
1121 for i in 0..subp_count.min(32) {
1122 subpicture_streams.push(read_subp_attr(buf, 0x0256 + i * 6)?);
1123 }
1124 }
1125 let mut multichannel_extension = Vec::new();
1133 if buf.len() >= 0x03D8 {
1134 for i in 0..24 {
1135 let off = 0x0318 + i * 8;
1136 let slice: [u8; 8] = buf[off..off + 8]
1137 .try_into()
1138 .map_err(|_| Error::InvalidUdf("VTSI_MAT: MC ext slice"))?;
1139 multichannel_extension.push(McExtensionEntry::parse(&slice));
1140 }
1141 }
1142 Ok(TitleAttributes {
1143 video: Some(video),
1144 audio_streams,
1145 subpicture_streams,
1146 multichannel_extension,
1147 })
1148}
1149
1150fn read_video_attr(buf: &[u8], off: usize) -> Result<VideoAttributes> {
1151 let slice = buf
1152 .get(off..off + 2)
1153 .ok_or(Error::InvalidUdf("ifo: video attr read past end"))?;
1154 Ok(VideoAttributes::parse(&[slice[0], slice[1]]))
1155}
1156
1157fn read_audio_attr(buf: &[u8], off: usize) -> Result<AudioAttributes> {
1158 let slice = buf
1159 .get(off..off + 8)
1160 .ok_or(Error::InvalidUdf("ifo: audio attr read past end"))?;
1161 let arr: [u8; 8] = slice
1162 .try_into()
1163 .map_err(|_| Error::InvalidUdf("ifo: audio attr slice"))?;
1164 Ok(AudioAttributes::parse(&arr))
1165}
1166
1167fn read_subp_attr(buf: &[u8], off: usize) -> Result<SubpictureAttributes> {
1168 let slice = buf
1169 .get(off..off + 6)
1170 .ok_or(Error::InvalidUdf("ifo: subp attr read past end"))?;
1171 let arr: [u8; 6] = slice
1172 .try_into()
1173 .map_err(|_| Error::InvalidUdf("ifo: subp attr slice"))?;
1174 Ok(SubpictureAttributes::parse(&arr))
1175}
1176
1177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1183pub struct Ptt {
1184 pub pgcn: u16,
1186 pub pgn: u16,
1188}
1189
1190#[derive(Debug, Clone)]
1193pub struct PttTitle {
1194 pub chapters: Vec<Ptt>,
1195}
1196
1197#[derive(Debug, Clone)]
1200pub struct VtsPttSrpt {
1201 pub title_count: u16,
1202 pub end_address: u32,
1203 pub titles: Vec<PttTitle>,
1204}
1205
1206impl VtsPttSrpt {
1207 pub fn parse(buf: &[u8]) -> Result<Self> {
1226 if buf.len() < 8 {
1227 return Err(Error::InvalidUdf(
1228 "VTS_PTT_SRPT: shorter than 8-byte header",
1229 ));
1230 }
1231 let title_count = read_u16(buf, 0)?;
1232 let end_address = read_u32(buf, 4)?;
1233 let nt = usize::from(title_count);
1234 let offsets_end = 8usize.saturating_add(nt * 4);
1235 if buf.len() < offsets_end {
1236 return Err(Error::InvalidUdf(
1237 "VTS_PTT_SRPT: offset list past end of buffer",
1238 ));
1239 }
1240 let mut offsets = Vec::with_capacity(nt);
1241 for i in 0..nt {
1242 offsets.push(read_u32(buf, 8 + i * 4)? as usize);
1243 }
1244 let mut titles = Vec::with_capacity(nt);
1245 for i in 0..nt {
1246 let start = offsets[i];
1247 let end_excl = if i + 1 < nt {
1250 offsets[i + 1]
1251 } else {
1252 (end_address as usize).saturating_add(1)
1253 };
1254 if end_excl < start {
1255 return Err(Error::InvalidUdf(
1256 "VTS_PTT_SRPT: title offsets not monotonic",
1257 ));
1258 }
1259 let span = end_excl - start;
1260 if span % 4 != 0 {
1261 return Err(Error::InvalidUdf(
1262 "VTS_PTT_SRPT: title span not a multiple of 4",
1263 ));
1264 }
1265 let n_ptt = span / 4;
1266 if buf.len() < start + n_ptt * 4 {
1267 return Err(Error::InvalidUdf(
1268 "VTS_PTT_SRPT: title body past end of buffer",
1269 ));
1270 }
1271 let mut chapters = Vec::with_capacity(n_ptt);
1272 for j in 0..n_ptt {
1273 let off = start + j * 4;
1274 chapters.push(Ptt {
1275 pgcn: read_u16(buf, off)?,
1276 pgn: read_u16(buf, off + 2)?,
1277 });
1278 }
1279 titles.push(PttTitle { chapters });
1280 }
1281 Ok(Self {
1282 title_count,
1283 end_address,
1284 titles,
1285 })
1286 }
1287}
1288
1289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1309pub struct CellPlaybackInfo {
1310 pub category_byte0: u8,
1311 pub restricted: bool,
1312 pub still_time: u8,
1313 pub cell_command: u8,
1314 pub playback_time: PgcTime,
1315 pub first_vobu_start_sector: u32,
1316 pub first_ilvu_end_sector: u32,
1317 pub last_vobu_start_sector: u32,
1318 pub last_vobu_end_sector: u32,
1319}
1320
1321impl CellPlaybackInfo {
1322 fn parse(buf: &[u8]) -> Result<Self> {
1323 if buf.len() < 24 {
1324 return Err(Error::InvalidUdf("C_PBI entry shorter than 24 bytes"));
1325 }
1326 let category_byte0 = read_u8(buf, 0)?;
1327 let restricted = (read_u8(buf, 1)? & 0x80) != 0;
1328 let still_time = read_u8(buf, 2)?;
1329 let cell_command = read_u8(buf, 3)?;
1330 let mut t = [0u8; 4];
1331 t.copy_from_slice(&buf[4..8]);
1332 let playback_time = PgcTime::from_bytes(t);
1333 let first_vobu_start_sector = read_u32(buf, 8)?;
1334 let first_ilvu_end_sector = read_u32(buf, 12)?;
1335 let last_vobu_start_sector = read_u32(buf, 16)?;
1336 let last_vobu_end_sector = read_u32(buf, 20)?;
1337 Ok(Self {
1338 category_byte0,
1339 restricted,
1340 still_time,
1341 cell_command,
1342 playback_time,
1343 first_vobu_start_sector,
1344 first_ilvu_end_sector,
1345 last_vobu_start_sector,
1346 last_vobu_end_sector,
1347 })
1348 }
1349}
1350
1351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1353pub struct CellPositionInfo {
1354 pub vob_id: u16,
1355 pub cell_id: u8,
1356}
1357
1358impl CellPositionInfo {
1359 fn parse(buf: &[u8]) -> Result<Self> {
1360 if buf.len() < 4 {
1361 return Err(Error::InvalidUdf("C_POS entry shorter than 4 bytes"));
1362 }
1363 let vob_id = read_u16(buf, 0)?;
1364 let cell_id = read_u8(buf, 3)?;
1366 Ok(Self { vob_id, cell_id })
1367 }
1368}
1369
1370#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1382pub struct PaletteEntry {
1383 pub y: u8,
1385 pub cr: u8,
1387 pub cb: u8,
1389}
1390
1391impl PaletteEntry {
1392 fn parse(buf: &[u8]) -> Result<Self> {
1395 if buf.len() < 4 {
1396 return Err(Error::InvalidUdf("PGC palette entry shorter than 4 bytes"));
1397 }
1398 Ok(Self {
1399 y: buf[1],
1400 cr: buf[2],
1401 cb: buf[3],
1402 })
1403 }
1404}
1405
1406#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1416pub struct NavCommand {
1417 pub bytes: [u8; 8],
1419}
1420
1421impl NavCommand {
1422 fn parse(buf: &[u8]) -> Result<Self> {
1424 let slice = buf
1425 .get(0..8)
1426 .ok_or(Error::InvalidUdf("PGC command shorter than 8 bytes"))?;
1427 let mut bytes = [0u8; 8];
1428 bytes.copy_from_slice(slice);
1429 Ok(Self { bytes })
1430 }
1431
1432 pub fn command_type(&self) -> u8 {
1440 self.bytes[0] >> 5
1441 }
1442
1443 #[inline]
1453 pub fn decode_instruction(&self) -> crate::nav::NavInstruction {
1454 self.decode()
1455 }
1456}
1457
1458#[derive(Debug, Clone, Default, PartialEq, Eq)]
1472pub struct PgcCommandTable {
1473 pub pre: Vec<NavCommand>,
1475 pub post: Vec<NavCommand>,
1477 pub cell: Vec<NavCommand>,
1479 pub end_address: u16,
1482}
1483
1484impl PgcCommandTable {
1485 fn parse(buf: &[u8]) -> Result<Self> {
1489 if buf.len() < 8 {
1490 return Err(Error::InvalidUdf("PGC command table shorter than header"));
1491 }
1492 let pre_count = read_u16(buf, 0)?;
1493 let post_count = read_u16(buf, 2)?;
1494 let cell_count = read_u16(buf, 4)?;
1495 let end_address = read_u16(buf, 6)?;
1496
1497 let total = usize::from(pre_count) + usize::from(post_count) + usize::from(cell_count);
1498 if total > 128 {
1500 return Err(Error::InvalidUdf("PGC command table claims > 128 commands"));
1501 }
1502
1503 let read_list = |start: usize, count: u16| -> Result<Vec<NavCommand>> {
1504 let mut out = Vec::with_capacity(usize::from(count));
1505 for i in 0..usize::from(count) {
1506 let off = start + i * 8;
1507 let word = buf
1508 .get(off..off + 8)
1509 .ok_or(Error::InvalidUdf("PGC command table list past end"))?;
1510 out.push(NavCommand::parse(word)?);
1511 }
1512 Ok(out)
1513 };
1514
1515 let pre_start = 8usize;
1516 let post_start = pre_start + usize::from(pre_count) * 8;
1517 let cell_start = post_start + usize::from(post_count) * 8;
1518
1519 let pre = read_list(pre_start, pre_count)?;
1520 let post = read_list(post_start, post_count)?;
1521 let cell = read_list(cell_start, cell_count)?;
1522
1523 Ok(Self {
1524 pre,
1525 post,
1526 cell,
1527 end_address,
1528 })
1529 }
1530
1531 pub fn pre_instructions(&self) -> impl Iterator<Item = crate::nav::NavInstruction> + '_ {
1540 self.pre.iter().map(NavCommand::decode_instruction)
1541 }
1542
1543 pub fn post_instructions(&self) -> impl Iterator<Item = crate::nav::NavInstruction> + '_ {
1550 self.post.iter().map(NavCommand::decode_instruction)
1551 }
1552
1553 pub fn cell_instructions(&self) -> impl Iterator<Item = crate::nav::NavInstruction> + '_ {
1562 self.cell.iter().map(NavCommand::decode_instruction)
1563 }
1564
1565 pub fn cell_instruction(&self, index_1based: u16) -> Option<crate::nav::NavInstruction> {
1576 if index_1based == 0 {
1577 return None;
1578 }
1579 let idx = usize::from(index_1based - 1);
1580 self.cell.get(idx).map(NavCommand::decode_instruction)
1581 }
1582}
1583
1584#[derive(Debug, Clone)]
1599pub struct Pgc {
1600 pub number_of_programs: u8,
1602 pub number_of_cells: u8,
1604 pub playback_time: PgcTime,
1606 pub prohibited_user_ops: u32,
1608 pub next_pgcn: u16,
1610 pub prev_pgcn: u16,
1612 pub goup_pgcn: u16,
1614 pub still_time: u8,
1616 pub playback_mode: u8,
1619 pub palette: [PaletteEntry; 16],
1622 pub offset_commands: u16,
1624 pub offset_program_map: u16,
1626 pub offset_cell_playback: u16,
1628 pub offset_cell_position: u16,
1630 pub program_map: Vec<u8>,
1632 pub cells: Vec<CellPlaybackInfo>,
1634 pub cell_positions: Vec<CellPositionInfo>,
1636 pub commands: Option<PgcCommandTable>,
1639}
1640
1641impl Pgc {
1642 pub fn parse(buf: &[u8]) -> Result<Self> {
1646 if buf.len() < 0xEC {
1647 return Err(Error::InvalidUdf("PGC: buffer shorter than header"));
1648 }
1649 let number_of_programs = read_u8(buf, 0x0002)?;
1650 let number_of_cells = read_u8(buf, 0x0003)?;
1651 let mut t = [0u8; 4];
1652 t.copy_from_slice(&buf[0x0004..0x0008]);
1653 let playback_time = PgcTime::from_bytes(t);
1654 let prohibited_user_ops = read_u32(buf, 0x0008)?;
1655 let next_pgcn = read_u16(buf, 0x009C)?;
1656 let prev_pgcn = read_u16(buf, 0x009E)?;
1657 let goup_pgcn = read_u16(buf, 0x00A0)?;
1658 let still_time = read_u8(buf, 0x00A2)?;
1659 let playback_mode = read_u8(buf, 0x00A3)?;
1660
1661 let mut palette = [PaletteEntry::default(); 16];
1665 for (i, slot) in palette.iter_mut().enumerate() {
1666 let base = 0x00A4 + i * 4;
1667 *slot = PaletteEntry::parse(&buf[base..base + 4])?;
1668 }
1669
1670 let offset_commands = read_u16(buf, 0x00E4)?;
1671 let offset_program_map = read_u16(buf, 0x00E6)?;
1672 let offset_cell_playback = read_u16(buf, 0x00E8)?;
1673 let offset_cell_position = read_u16(buf, 0x00EA)?;
1674
1675 let mut program_map = Vec::with_capacity(usize::from(number_of_programs));
1678 if offset_program_map != 0 {
1679 let base = usize::from(offset_program_map);
1680 for i in 0..usize::from(number_of_programs) {
1681 program_map.push(read_u8(buf, base + i)?);
1682 }
1683 }
1684
1685 let mut cells = Vec::with_capacity(usize::from(number_of_cells));
1687 if offset_cell_playback != 0 {
1688 let base = usize::from(offset_cell_playback);
1689 for i in 0..usize::from(number_of_cells) {
1690 let entry = &buf
1691 .get(base + i * 24..base + (i + 1) * 24)
1692 .ok_or(Error::InvalidUdf("PGC: C_PBI past end of buffer"))?;
1693 cells.push(CellPlaybackInfo::parse(entry)?);
1694 }
1695 }
1696
1697 let mut cell_positions = Vec::with_capacity(usize::from(number_of_cells));
1699 if offset_cell_position != 0 {
1700 let base = usize::from(offset_cell_position);
1701 for i in 0..usize::from(number_of_cells) {
1702 let entry = &buf
1703 .get(base + i * 4..base + (i + 1) * 4)
1704 .ok_or(Error::InvalidUdf("PGC: C_POS past end of buffer"))?;
1705 cell_positions.push(CellPositionInfo::parse(entry)?);
1706 }
1707 }
1708
1709 let commands = if offset_commands != 0 {
1712 let base = usize::from(offset_commands);
1713 let tbl = buf
1714 .get(base..)
1715 .ok_or(Error::InvalidUdf("PGC: command table past end of buffer"))?;
1716 Some(PgcCommandTable::parse(tbl)?)
1717 } else {
1718 None
1719 };
1720
1721 Ok(Self {
1722 number_of_programs,
1723 number_of_cells,
1724 playback_time,
1725 prohibited_user_ops,
1726 next_pgcn,
1727 prev_pgcn,
1728 goup_pgcn,
1729 still_time,
1730 playback_mode,
1731 palette,
1732 offset_commands,
1733 offset_program_map,
1734 offset_cell_playback,
1735 offset_cell_position,
1736 program_map,
1737 cells,
1738 cell_positions,
1739 commands,
1740 })
1741 }
1742
1743 #[inline]
1752 pub fn uop_mask(&self) -> crate::uops::UopMask {
1753 crate::uops::UopMask::from_bits(self.prohibited_user_ops)
1754 }
1755
1756 #[inline]
1761 pub fn is_user_op_allowed(&self, op: crate::uops::UserOp) -> bool {
1762 self.uop_mask().is_allowed(op)
1763 }
1764}
1765
1766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1772pub struct PgciSrp {
1773 pub category: u32,
1774 pub offset: u32,
1776}
1777
1778#[derive(Debug, Clone)]
1780pub struct Pgci {
1781 pub number_of_pgcs: u16,
1782 pub end_address: u32,
1783 pub srp: Vec<PgciSrp>,
1784 pub pgcs: Vec<Pgc>,
1785}
1786
1787impl Pgci {
1788 pub fn parse(buf: &[u8]) -> Result<Self> {
1791 if buf.len() < 8 {
1792 return Err(Error::InvalidUdf("PGCI: shorter than 8-byte header"));
1793 }
1794 let number_of_pgcs = read_u16(buf, 0)?;
1795 let end_address = read_u32(buf, 4)?;
1796 let n = usize::from(number_of_pgcs);
1797 let srp_end = 8usize.saturating_add(n * 8);
1798 if buf.len() < srp_end {
1799 return Err(Error::InvalidUdf("PGCI: SRP list past end of buffer"));
1800 }
1801 let mut srp = Vec::with_capacity(n);
1802 for i in 0..n {
1803 let base = 8 + i * 8;
1804 srp.push(PgciSrp {
1805 category: read_u32(buf, base)?,
1806 offset: read_u32(buf, base + 4)?,
1807 });
1808 }
1809 let mut pgcs = Vec::with_capacity(n);
1810 for entry in &srp {
1811 let off = entry.offset as usize;
1812 if off == 0 || off >= buf.len() {
1813 return Err(Error::InvalidUdf("PGCI: PGC offset out of range"));
1814 }
1815 let pgc_buf = &buf[off..];
1816 pgcs.push(Pgc::parse(pgc_buf)?);
1817 }
1818 Ok(Self {
1819 number_of_pgcs,
1820 end_address,
1821 srp,
1822 pgcs,
1823 })
1824 }
1825}
1826
1827pub mod menu_existence {
1854 pub const ROOT: u8 = 0x80;
1856 pub const SUBPICTURE: u8 = 0x40;
1858 pub const AUDIO: u8 = 0x20;
1860 pub const ANGLE: u8 = 0x10;
1862 pub const PTT: u8 = 0x08;
1864}
1865
1866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1872pub enum MenuType {
1873 Title,
1875 Root,
1877 Subpicture,
1879 Audio,
1881 Angle,
1883 Ptt,
1885 Unknown(u8),
1887}
1888
1889impl MenuType {
1890 pub fn from_nibble(n: u8) -> Self {
1892 match n & 0x0F {
1893 2 => MenuType::Title,
1894 3 => MenuType::Root,
1895 4 => MenuType::Subpicture,
1896 5 => MenuType::Audio,
1897 6 => MenuType::Angle,
1898 7 => MenuType::Ptt,
1899 other => MenuType::Unknown(other),
1900 }
1901 }
1902}
1903
1904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1912pub struct PgciLuSrp {
1913 pub category: u32,
1915 pub offset: u32,
1918}
1919
1920impl PgciLuSrp {
1921 pub fn is_entry_pgc(&self) -> bool {
1923 (self.category >> 24) & 0x80 != 0
1924 }
1925
1926 pub fn menu_type(&self) -> MenuType {
1929 MenuType::from_nibble((self.category >> 24) as u8)
1930 }
1931
1932 pub fn parental_mask(&self) -> u16 {
1934 (self.category & 0xFFFF) as u16
1935 }
1936}
1937
1938#[derive(Debug, Clone)]
1941pub struct PgciLu {
1942 pub number_of_pgcs: u16,
1944 pub end_address: u32,
1947 pub srp: Vec<PgciLuSrp>,
1949 pub pgcs: Vec<Pgc>,
1951}
1952
1953impl PgciLu {
1954 pub fn parse(buf: &[u8]) -> Result<Self> {
1957 if buf.len() < 8 {
1958 return Err(Error::InvalidUdf("PGCI_LU: shorter than 8-byte header"));
1959 }
1960 let number_of_pgcs = read_u16(buf, 0)?;
1961 let end_address = read_u32(buf, 4)?;
1963 let n = usize::from(number_of_pgcs);
1964 let srp_end = 8usize
1965 .checked_add(
1966 n.checked_mul(8)
1967 .ok_or(Error::InvalidUdf("PGCI_LU: SRP list × 8 overflow"))?,
1968 )
1969 .ok_or(Error::InvalidUdf("PGCI_LU: SRP list size overflow"))?;
1970 if buf.len() < srp_end {
1971 return Err(Error::InvalidUdf("PGCI_LU: SRP list past end of buffer"));
1972 }
1973 let mut srp = Vec::with_capacity(n);
1974 for i in 0..n {
1975 let base = 8 + i * 8;
1976 srp.push(PgciLuSrp {
1977 category: read_u32(buf, base)?,
1978 offset: read_u32(buf, base + 4)?,
1979 });
1980 }
1981 let mut pgcs = Vec::with_capacity(n);
1982 for entry in &srp {
1983 let off = entry.offset as usize;
1984 if off == 0 || off >= buf.len() {
1985 return Err(Error::InvalidUdf("PGCI_LU: PGC offset out of range"));
1986 }
1987 pgcs.push(Pgc::parse(&buf[off..])?);
1988 }
1989 Ok(Self {
1990 number_of_pgcs,
1991 end_address,
1992 srp,
1993 pgcs,
1994 })
1995 }
1996}
1997
1998#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2007pub struct PgciUtSrp {
2008 pub language_code: u16,
2010 pub language_code_ext: u8,
2012 pub menu_existence: u8,
2014 pub offset: u32,
2017}
2018
2019impl PgciUtSrp {
2020 pub fn has_root_menu(&self) -> bool {
2022 self.menu_existence & menu_existence::ROOT != 0
2023 }
2024
2025 pub fn has_subpicture_menu(&self) -> bool {
2028 self.menu_existence & menu_existence::SUBPICTURE != 0
2029 }
2030
2031 pub fn has_audio_menu(&self) -> bool {
2034 self.menu_existence & menu_existence::AUDIO != 0
2035 }
2036
2037 pub fn has_angle_menu(&self) -> bool {
2040 self.menu_existence & menu_existence::ANGLE != 0
2041 }
2042
2043 pub fn has_ptt_menu(&self) -> bool {
2046 self.menu_existence & menu_existence::PTT != 0
2047 }
2048}
2049
2050#[derive(Debug, Clone)]
2060pub struct PgciUt {
2061 pub number_of_language_units: u16,
2063 pub end_address: u32,
2066 pub srp: Vec<PgciUtSrp>,
2068 pub language_units: Vec<PgciLu>,
2070}
2071
2072impl PgciUt {
2073 pub fn parse(buf: &[u8]) -> Result<Self> {
2077 if buf.len() < 8 {
2078 return Err(Error::InvalidUdf("PGCI_UT: shorter than 8-byte header"));
2079 }
2080 let number_of_language_units = read_u16(buf, 0)?;
2081 let end_address = read_u32(buf, 4)?;
2083 let n = usize::from(number_of_language_units);
2084 let srp_end = 8usize
2085 .checked_add(
2086 n.checked_mul(8)
2087 .ok_or(Error::InvalidUdf("PGCI_UT: SRP list × 8 overflow"))?,
2088 )
2089 .ok_or(Error::InvalidUdf("PGCI_UT: SRP list size overflow"))?;
2090 if buf.len() < srp_end {
2091 return Err(Error::InvalidUdf("PGCI_UT: SRP list past end of buffer"));
2092 }
2093 let mut srp = Vec::with_capacity(n);
2094 for i in 0..n {
2095 let base = 8 + i * 8;
2096 srp.push(PgciUtSrp {
2097 language_code: read_u16(buf, base)?,
2098 language_code_ext: buf[base + 2],
2099 menu_existence: buf[base + 3],
2100 offset: read_u32(buf, base + 4)?,
2101 });
2102 }
2103 let mut language_units = Vec::with_capacity(n);
2104 for entry in &srp {
2105 let off = entry.offset as usize;
2106 if off == 0 || off >= buf.len() {
2107 return Err(Error::InvalidUdf("PGCI_UT: LU offset out of range"));
2108 }
2109 language_units.push(PgciLu::parse(&buf[off..])?);
2110 }
2111 Ok(Self {
2112 number_of_language_units,
2113 end_address,
2114 srp,
2115 language_units,
2116 })
2117 }
2118
2119 pub fn language_unit(&self, language_code: u16) -> Option<&PgciLu> {
2122 self.srp
2123 .iter()
2124 .position(|s| s.language_code == language_code)
2125 .and_then(|i| self.language_units.get(i))
2126 }
2127}
2128
2129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2139pub struct CellAddrEntry {
2140 pub vob_id: u16,
2141 pub cell_id: u8,
2142 pub start_sector: u32,
2143 pub end_sector: u32,
2144}
2145
2146#[derive(Debug, Clone)]
2149pub struct VtsCAdt {
2150 pub number_of_vob_ids: u16,
2154 pub end_address: u32,
2156 pub entries: Vec<CellAddrEntry>,
2158}
2159
2160impl VtsCAdt {
2161 pub fn parse(buf: &[u8]) -> Result<Self> {
2164 if buf.len() < 8 {
2165 return Err(Error::InvalidUdf("C_ADT: shorter than 8-byte header"));
2166 }
2167 let number_of_vob_ids = read_u16(buf, 0)?;
2168 let end_address = read_u32(buf, 4)?;
2169 let body_bytes = (end_address as usize).saturating_add(1).saturating_sub(8);
2173 if body_bytes % 12 != 0 {
2174 return Err(Error::InvalidUdf(
2175 "C_ADT: end_address implies non-12-byte entry size",
2176 ));
2177 }
2178 let n = body_bytes / 12;
2179 let needed = 8 + n * 12;
2180 if buf.len() < needed {
2181 return Err(Error::InvalidUdf("C_ADT: buffer shorter than entry table"));
2182 }
2183 let mut entries = Vec::with_capacity(n);
2184 for i in 0..n {
2185 let base = 8 + i * 12;
2186 entries.push(CellAddrEntry {
2187 vob_id: read_u16(buf, base)?,
2188 cell_id: read_u8(buf, base + 2)?,
2189 start_sector: read_u32(buf, base + 4)?,
2191 end_sector: read_u32(buf, base + 8)?,
2192 });
2193 }
2194 Ok(Self {
2195 number_of_vob_ids,
2196 end_address,
2197 entries,
2198 })
2199 }
2200
2201 pub fn lookup(&self, vob_id: u16, cell_id: u8) -> Option<(u32, u32)> {
2204 self.entries
2205 .iter()
2206 .find(|e| e.vob_id == vob_id && e.cell_id == cell_id)
2207 .map(|e| (e.start_sector, e.end_sector))
2208 }
2209}
2210
2211#[derive(Debug, Clone)]
2238pub struct VobuAdmap {
2239 pub end_address: u32,
2242 pub entries: Vec<u32>,
2246}
2247
2248impl VobuAdmap {
2249 pub fn parse(buf: &[u8]) -> Result<Self> {
2252 if buf.len() < 4 {
2253 return Err(Error::InvalidUdf("VOBU_ADMAP: shorter than 4-byte header"));
2254 }
2255 let end_address = read_u32(buf, 0)?;
2256 let body_bytes = (end_address as usize).saturating_add(1).saturating_sub(4);
2260 if body_bytes % 4 != 0 {
2261 return Err(Error::InvalidUdf(
2262 "VOBU_ADMAP: end_address implies non-4-byte entry size",
2263 ));
2264 }
2265 let n = body_bytes / 4;
2266 let needed = 4 + n * 4;
2267 if buf.len() < needed {
2268 return Err(Error::InvalidUdf(
2269 "VOBU_ADMAP: buffer shorter than entry table",
2270 ));
2271 }
2272 let mut entries = Vec::with_capacity(n);
2273 for i in 0..n {
2274 entries.push(read_u32(buf, 4 + i * 4)?);
2275 }
2276 Ok(Self {
2277 end_address,
2278 entries,
2279 })
2280 }
2281
2282 #[inline]
2284 pub fn vobu_count(&self) -> usize {
2285 self.entries.len()
2286 }
2287
2288 pub fn vobu_start_sector(&self, vobu_number: u32) -> Option<u32> {
2292 if vobu_number == 0 {
2293 return None;
2294 }
2295 self.entries.get((vobu_number - 1) as usize).copied()
2296 }
2297
2298 pub fn vobu_containing(&self, sector: u32) -> Option<u32> {
2307 if self.entries.is_empty() {
2308 return None;
2309 }
2310 let idx = self.entries.partition_point(|&v| v <= sector);
2312 if idx == 0 {
2313 None
2314 } else {
2315 Some(idx as u32)
2316 }
2317 }
2318}
2319
2320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2334pub struct TmapEntry {
2335 pub sector: u32,
2337 pub discontinuous: bool,
2340}
2341
2342impl TmapEntry {
2343 pub const DISCONTINUITY_BIT: u32 = 1 << 31;
2346
2347 pub const SECTOR_MASK: u32 = 0x7FFF_FFFF;
2349
2350 fn from_raw(raw: u32) -> Self {
2352 Self {
2353 sector: raw & Self::SECTOR_MASK,
2354 discontinuous: (raw & Self::DISCONTINUITY_BIT) != 0,
2355 }
2356 }
2357}
2358
2359#[derive(Debug, Clone, PartialEq, Eq)]
2377pub struct VtsTmap {
2378 pub time_unit: u8,
2380 pub entries: Vec<TmapEntry>,
2382}
2383
2384impl VtsTmap {
2385 pub fn parse(buf: &[u8]) -> Result<Self> {
2388 if buf.len() < 4 {
2389 return Err(Error::InvalidUdf("VTS_TMAP: shorter than 4-byte header"));
2390 }
2391 let time_unit = read_u8(buf, 0)?;
2392 let number_of_entries = read_u16(buf, 2)?;
2394 let n = usize::from(number_of_entries);
2395 let needed = 4usize.saturating_add(n * 4);
2396 if buf.len() < needed {
2397 return Err(Error::InvalidUdf(
2398 "VTS_TMAP: buffer shorter than entry table",
2399 ));
2400 }
2401 let mut entries = Vec::with_capacity(n);
2402 for i in 0..n {
2403 let raw = read_u32(buf, 4 + i * 4)?;
2404 entries.push(TmapEntry::from_raw(raw));
2405 }
2406 Ok(Self { time_unit, entries })
2407 }
2408
2409 pub fn sector_at(&self, seconds: u32) -> Option<u32> {
2417 if self.entries.is_empty() || self.time_unit == 0 {
2418 return None;
2419 }
2420 let step = u32::from(self.time_unit);
2421 let idx_zero_based = (seconds / step) as usize;
2422 let idx = idx_zero_based.min(self.entries.len() - 1);
2423 Some(self.entries[idx].sector)
2424 }
2425
2426 pub fn total_seconds(&self) -> u32 {
2430 u32::from(self.time_unit) * self.entries.len() as u32
2431 }
2432}
2433
2434#[derive(Debug, Clone)]
2450pub struct VtsTmapti {
2451 pub number_of_pgcs: u16,
2453 pub end_address: u32,
2455 pub maps: Vec<VtsTmap>,
2458}
2459
2460impl VtsTmapti {
2461 pub fn parse(buf: &[u8]) -> Result<Self> {
2464 if buf.len() < 8 {
2465 return Err(Error::InvalidUdf("VTS_TMAPTI: shorter than 8-byte header"));
2466 }
2467 let number_of_pgcs = read_u16(buf, 0)?;
2468 let end_address = read_u32(buf, 4)?;
2469 let n = usize::from(number_of_pgcs);
2470 let offsets_end = 8usize.saturating_add(n * 4);
2471 if buf.len() < offsets_end {
2472 return Err(Error::InvalidUdf(
2473 "VTS_TMAPTI: offset list past end of buffer",
2474 ));
2475 }
2476 let mut offsets = Vec::with_capacity(n);
2477 for i in 0..n {
2478 offsets.push(read_u32(buf, 8 + i * 4)? as usize);
2479 }
2480 let mut maps = Vec::with_capacity(n);
2481 for off in &offsets {
2482 let tmap_buf = buf.get(*off..).ok_or(Error::InvalidUdf(
2483 "VTS_TMAPTI: VTS_TMAP offset past end of buffer",
2484 ))?;
2485 maps.push(VtsTmap::parse(tmap_buf)?);
2486 }
2487 Ok(Self {
2488 number_of_pgcs,
2489 end_address,
2490 maps,
2491 })
2492 }
2493
2494 pub fn get(&self, pgcn: u16) -> Option<&VtsTmap> {
2496 if pgcn == 0 {
2497 return None;
2498 }
2499 self.maps.get((pgcn - 1) as usize)
2500 }
2501}
2502
2503#[derive(Debug, Clone)]
2510pub struct DvdChapter {
2511 pub number: u16,
2513 pub pgcn: u16,
2515 pub pgn: u16,
2517 pub start_cell: u8,
2519 pub end_cell: u8,
2524 pub playback_time: PgcTime,
2528}
2529
2530#[derive(Debug, Clone)]
2533pub struct DvdTitle {
2534 pub number: u8,
2536 pub angle_count: u8,
2538 pub chapter_count: u16,
2540 pub chapters: Vec<DvdChapter>,
2542}
2543
2544#[derive(Debug, Clone)]
2549pub struct VtsIfo {
2550 pub vts_number: u8,
2552 pub title_count: u8,
2554 pub titles: Vec<DvdTitle>,
2556 pub pgcs: Vec<Pgc>,
2558 pub cell_adt: VtsCAdt,
2560 pub vobu_admap: Option<VobuAdmap>,
2566 pub time_map: Option<VtsTmapti>,
2570 pub mat: VtsiMat,
2575}
2576
2577impl VtsIfo {
2578 pub fn parse(buf: &[u8], vts_number: u8) -> Result<Self> {
2584 let mat = VtsiMat::parse(buf)?;
2585
2586 let ptt_off = (mat.vts_ptt_srpt_sector as usize)
2588 .checked_mul(DVD_SECTOR)
2589 .ok_or(Error::InvalidUdf("VTSI: PTT sector overflow"))?;
2590 let ptt_buf = buf
2591 .get(ptt_off..)
2592 .ok_or(Error::InvalidUdf("VTSI: PTT sector past end"))?;
2593 let ptt_srpt = VtsPttSrpt::parse(ptt_buf)?;
2594
2595 let pgci_off = (mat.vts_pgci_sector as usize)
2597 .checked_mul(DVD_SECTOR)
2598 .ok_or(Error::InvalidUdf("VTSI: PGCI sector overflow"))?;
2599 let pgci_buf = buf
2600 .get(pgci_off..)
2601 .ok_or(Error::InvalidUdf("VTSI: PGCI sector past end"))?;
2602 let pgci = Pgci::parse(pgci_buf)?;
2603
2604 let cadt_off = (mat.vts_c_adt_sector as usize)
2606 .checked_mul(DVD_SECTOR)
2607 .ok_or(Error::InvalidUdf("VTSI: C_ADT sector overflow"))?;
2608 let cadt_buf = buf
2609 .get(cadt_off..)
2610 .ok_or(Error::InvalidUdf("VTSI: C_ADT sector past end"))?;
2611 let cell_adt = VtsCAdt::parse(cadt_buf)?;
2612
2613 let vobu_admap = if mat.vts_vobu_admap_sector != 0 {
2615 let off = (mat.vts_vobu_admap_sector as usize)
2616 .checked_mul(DVD_SECTOR)
2617 .ok_or(Error::InvalidUdf("VTSI: VOBU_ADMAP sector overflow"))?;
2618 let body = buf
2619 .get(off..)
2620 .ok_or(Error::InvalidUdf("VTSI: VOBU_ADMAP sector past end"))?;
2621 Some(VobuAdmap::parse(body)?)
2622 } else {
2623 None
2624 };
2625
2626 let time_map = if mat.vts_tmapti_sector != 0 {
2628 let off = (mat.vts_tmapti_sector as usize)
2629 .checked_mul(DVD_SECTOR)
2630 .ok_or(Error::InvalidUdf("VTSI: TMAPTI sector overflow"))?;
2631 let body = buf
2632 .get(off..)
2633 .ok_or(Error::InvalidUdf("VTSI: TMAPTI sector past end"))?;
2634 Some(VtsTmapti::parse(body)?)
2635 } else {
2636 None
2637 };
2638
2639 let title_count_u8 = u8::try_from(ptt_srpt.title_count.min(255))
2646 .map_err(|_| Error::InvalidUdf("VTSI: title count > 255"))?;
2647 let mut titles = Vec::with_capacity(usize::from(title_count_u8));
2648 for (i, ptt_title) in ptt_srpt.titles.iter().enumerate() {
2649 let title_number = (i as u8).saturating_add(1);
2650 let mut chapters = Vec::with_capacity(ptt_title.chapters.len());
2651 for (ch_i, ptt) in ptt_title.chapters.iter().enumerate() {
2652 let pgc = pgci
2653 .pgcs
2654 .get(usize::from(ptt.pgcn.saturating_sub(1)))
2655 .ok_or(Error::InvalidUdf(
2656 "VTSI: PTT references PGCN past end of PGCI",
2657 ))?;
2658 let pgn_idx = usize::from(ptt.pgn.saturating_sub(1));
2659 let start_cell = *pgc
2660 .program_map
2661 .get(pgn_idx)
2662 .ok_or(Error::InvalidUdf("VTSI: PTT PGN past program_map"))?;
2663 let next_in_same_pgc = ptt_title.chapters.get(ch_i + 1).and_then(|next_ptt| {
2667 if next_ptt.pgcn == ptt.pgcn {
2668 pgc.program_map
2669 .get(usize::from(next_ptt.pgn.saturating_sub(1)))
2670 .copied()
2671 .map(|next_start| next_start.saturating_sub(1))
2672 } else {
2673 None
2674 }
2675 });
2676 let end_cell = next_in_same_pgc.unwrap_or(pgc.number_of_cells);
2677 chapters.push(DvdChapter {
2678 number: (ch_i as u16).saturating_add(1),
2679 pgcn: ptt.pgcn,
2680 pgn: ptt.pgn,
2681 start_cell,
2682 end_cell,
2683 playback_time: pgc.playback_time,
2684 });
2685 }
2686 let chapter_count = chapters.len() as u16;
2687 titles.push(DvdTitle {
2688 number: title_number,
2689 angle_count: 1,
2690 chapter_count,
2691 chapters,
2692 });
2693 }
2694
2695 Ok(Self {
2696 vts_number,
2697 title_count: title_count_u8,
2698 titles,
2699 pgcs: pgci.pgcs,
2700 cell_adt,
2701 vobu_admap,
2702 time_map,
2703 mat,
2704 })
2705 }
2706
2707 pub fn vobu_sector_at_pgc_time(&self, pgcn: u16, seconds: u32) -> Option<u32> {
2720 self.time_map
2721 .as_ref()
2722 .and_then(|t| t.get(pgcn))
2723 .and_then(|m| m.sector_at(seconds))
2724 }
2725}
2726
2727#[derive(Debug, Clone)]
2749pub struct VmgVtsAtrtEntry {
2750 pub vts_number: u8,
2752 pub vts_category: u32,
2754 pub attributes_blob: Vec<u8>,
2757}
2758
2759#[derive(Debug, Clone)]
2768pub struct VmgVtsAtrt {
2769 pub number_of_title_sets: u16,
2771 pub end_address: u32,
2774 pub entries: Vec<VmgVtsAtrtEntry>,
2776}
2777
2778impl VmgVtsAtrt {
2779 pub fn parse(buf: &[u8]) -> Result<Self> {
2782 if buf.len() < 8 {
2783 return Err(Error::InvalidUdf("VMG_VTS_ATRT: header < 8 bytes"));
2784 }
2785 let number_of_title_sets = read_u16(buf, 0)?;
2786 let end_address = read_u32(buf, 4)?;
2788
2789 let count = usize::from(number_of_title_sets);
2790 let offset_table_len = count.checked_mul(4).ok_or(Error::InvalidUdf(
2791 "VMG_VTS_ATRT: title-set count × 4 overflow",
2792 ))?;
2793 let header_len = 8usize
2794 .checked_add(offset_table_len)
2795 .ok_or(Error::InvalidUdf("VMG_VTS_ATRT: header length overflow"))?;
2796 if buf.len() < header_len {
2797 return Err(Error::InvalidUdf("VMG_VTS_ATRT: offset table past end"));
2798 }
2799
2800 let mut offsets = Vec::with_capacity(count);
2803 for i in 0..count {
2804 offsets.push(read_u32(buf, 8 + i * 4)?);
2805 }
2806
2807 let mut entries = Vec::with_capacity(count);
2808 for (i, &off) in offsets.iter().enumerate() {
2809 let entry_start = off as usize;
2815 let entry_hdr = buf
2816 .get(entry_start..entry_start + 8)
2817 .ok_or(Error::InvalidUdf("VMG_VTS_ATRT: entry header past buffer"))?;
2818 let entry_ea_rel =
2819 u32::from_be_bytes([entry_hdr[0], entry_hdr[1], entry_hdr[2], entry_hdr[3]])
2820 as usize;
2821 let vts_category =
2822 u32::from_be_bytes([entry_hdr[4], entry_hdr[5], entry_hdr[6], entry_hdr[7]]);
2823 let entry_total = entry_ea_rel
2828 .checked_add(1)
2829 .ok_or(Error::InvalidUdf("VMG_VTS_ATRT: entry EA overflow"))?;
2830 if entry_total < 8 {
2831 return Err(Error::InvalidUdf(
2832 "VMG_VTS_ATRT: entry length shorter than 8-byte header",
2833 ));
2834 }
2835 let next_start = offsets
2838 .get(i + 1)
2839 .map(|&n| n as usize)
2840 .unwrap_or((end_address as usize).saturating_add(1));
2841 if entry_start.saturating_add(entry_total) > next_start {
2842 return Err(Error::InvalidUdf(
2843 "VMG_VTS_ATRT: entry EA overlaps next entry",
2844 ));
2845 }
2846 let blob_end = entry_start
2847 .checked_add(entry_total)
2848 .ok_or(Error::InvalidUdf("VMG_VTS_ATRT: entry end overflow"))?;
2849 let blob = buf
2850 .get(entry_start + 8..blob_end)
2851 .ok_or(Error::InvalidUdf("VMG_VTS_ATRT: blob past buffer"))?
2852 .to_vec();
2853 entries.push(VmgVtsAtrtEntry {
2854 vts_number: (i as u8).saturating_add(1),
2855 vts_category,
2856 attributes_blob: blob,
2857 });
2858 }
2859
2860 Ok(Self {
2861 number_of_title_sets,
2862 end_address,
2863 entries,
2864 })
2865 }
2866
2867 pub fn entry(&self, vts_number: u8) -> Option<&VmgVtsAtrtEntry> {
2870 if vts_number == 0 {
2871 return None;
2872 }
2873 self.entries.get(usize::from(vts_number - 1))
2874 }
2875}
2876
2877#[derive(Debug, Clone)]
2892pub struct PtlMait {
2893 pub country_code: u16,
2895 pub masks: [Vec<u16>; 8],
2905}
2906
2907impl PtlMait {
2908 pub fn mask(&self, parental_level: u8, title_set: u8) -> Option<u16> {
2911 if !(1..=8).contains(&parental_level) {
2912 return None;
2913 }
2914 let level_idx = usize::from(parental_level - 1);
2915 self.masks[level_idx].get(usize::from(title_set)).copied()
2916 }
2917}
2918
2919#[derive(Debug, Clone)]
2932pub struct VmgPtlMait {
2933 pub number_of_countries: u16,
2935 pub number_of_title_sets: u16,
2938 pub end_address: u32,
2941 pub entries: Vec<PtlMait>,
2943}
2944
2945impl VmgPtlMait {
2946 pub fn parse(buf: &[u8]) -> Result<Self> {
2949 if buf.len() < 8 {
2950 return Err(Error::InvalidUdf("VMG_PTL_MAIT: header < 8 bytes"));
2951 }
2952 let number_of_countries = read_u16(buf, 0)?;
2953 let number_of_title_sets = read_u16(buf, 2)?;
2954 let end_address = read_u32(buf, 4)?;
2955
2956 let nts = usize::from(number_of_title_sets);
2957 let masks_per_level = nts
2958 .checked_add(1)
2959 .ok_or(Error::InvalidUdf("VMG_PTL_MAIT: nts + 1 overflow"))?;
2960 let level_block_bytes = masks_per_level
2961 .checked_mul(2)
2962 .ok_or(Error::InvalidUdf("VMG_PTL_MAIT: level block size overflow"))?;
2963 let body_bytes = level_block_bytes
2964 .checked_mul(8)
2965 .ok_or(Error::InvalidUdf("VMG_PTL_MAIT: body size overflow"))?;
2966
2967 let count = usize::from(number_of_countries);
2968 let header_len = 8usize
2970 .checked_add(
2971 count
2972 .checked_mul(8)
2973 .ok_or(Error::InvalidUdf("VMG_PTL_MAIT: country list × 8 overflow"))?,
2974 )
2975 .ok_or(Error::InvalidUdf("VMG_PTL_MAIT: header length overflow"))?;
2976 if buf.len() < header_len {
2977 return Err(Error::InvalidUdf("VMG_PTL_MAIT: country list past end"));
2978 }
2979
2980 let mut entries = Vec::with_capacity(count);
2981 for i in 0..count {
2982 let entry_base = 8 + i * 8;
2983 let country_code = read_u16(buf, entry_base)?;
2984 let body_offset = usize::from(read_u16(buf, entry_base + 4)?);
2986 let body_end = body_offset
2988 .checked_add(body_bytes)
2989 .ok_or(Error::InvalidUdf("VMG_PTL_MAIT: country body end overflow"))?;
2990 if body_end > buf.len() {
2991 return Err(Error::InvalidUdf(
2992 "VMG_PTL_MAIT: country body past buffer end",
2993 ));
2994 }
2995
2996 let mut masks: [Vec<u16>; 8] = Default::default();
3001 for level_storage_idx in 0..8 {
3002 let level_value = 8 - level_storage_idx; let block_start = body_offset + level_storage_idx * level_block_bytes;
3005 let mut row = Vec::with_capacity(masks_per_level);
3006 for slot in 0..masks_per_level {
3007 row.push(read_u16(buf, block_start + slot * 2)?);
3008 }
3009 masks[level_value - 1] = row;
3010 }
3011
3012 entries.push(PtlMait {
3013 country_code,
3014 masks,
3015 });
3016 }
3017
3018 Ok(Self {
3019 number_of_countries,
3020 number_of_title_sets,
3021 end_address,
3022 entries,
3023 })
3024 }
3025
3026 pub fn country(&self, country_code: u16) -> Option<&PtlMait> {
3029 self.entries.iter().find(|e| e.country_code == country_code)
3030 }
3031}
3032
3033#[cfg(test)]
3038mod tests {
3039 use super::*;
3040
3041 #[test]
3046 fn pgc_time_decode_ntsc_30() {
3047 let t = PgcTime::from_bytes([0x01, 0x23, 0x45, 0xE0]);
3052 assert_eq!(t.hours, 1);
3053 assert_eq!(t.minutes, 23);
3054 assert_eq!(t.seconds, 45);
3055 assert_eq!(t.frames, 20);
3056 assert_eq!(t.frame_rate, FrameRate::Ntsc30);
3057 assert_eq!(t.total_seconds(), 3600 + 23 * 60 + 45);
3058 }
3059
3060 #[test]
3061 fn pgc_time_decode_pal_25() {
3062 let t = PgcTime::from_bytes([0x00, 0x00, 0x01, 0x40]);
3064 assert_eq!(t.frame_rate, FrameRate::Pal25);
3065 assert_eq!(t.frames, 0);
3066 assert_eq!(t.total_seconds(), 1);
3067 }
3068
3069 #[test]
3075 fn pgc_time_to_ns_ntsc_30_integer_seconds() {
3076 let t = PgcTime::from_bytes([0x00, 0x00, 0x01, 0xC0]);
3078 assert_eq!(t.frame_rate, FrameRate::Ntsc30);
3079 assert_eq!(t.to_nanoseconds(), 1_000_000_000);
3080 }
3081
3082 #[test]
3083 fn pgc_time_to_ns_ntsc_30_half_second() {
3084 let t = PgcTime::from_bytes([0x00, 0x00, 0x01, 0xD5]);
3089 assert_eq!(t.frame_rate, FrameRate::Ntsc30);
3090 assert_eq!(t.frames, 15);
3091 assert_eq!(t.to_nanoseconds(), 1_500_000_000);
3092 }
3093
3094 #[test]
3095 fn pgc_time_to_ns_pal_25_frame_period() {
3096 let t = PgcTime::from_bytes([0x00, 0x00, 0x00, 0x41]);
3099 assert_eq!(t.frame_rate, FrameRate::Pal25);
3100 assert_eq!(t.frames, 1);
3101 assert_eq!(t.to_nanoseconds(), 40_000_000);
3102 }
3103
3104 #[test]
3105 fn pgc_time_to_ns_illegal_rate_drops_frames() {
3106 let t = PgcTime::from_bytes([0x00, 0x00, 0x02, 0x07]);
3110 assert_eq!(t.frame_rate, FrameRate::Illegal);
3111 assert_eq!(t.frames, 7);
3112 assert_eq!(t.to_nanoseconds(), 2_000_000_000);
3113 }
3114
3115 fn build_vmg_mat() -> Vec<u8> {
3120 let mut b = vec![0u8; 0x200];
3121 b[0..12].copy_from_slice(VMG_MAGIC);
3122 b[0x000C..0x0010].copy_from_slice(&1000u32.to_be_bytes());
3124 b[0x001C..0x0020].copy_from_slice(&4u32.to_be_bytes());
3126 b[0x0020..0x0022].copy_from_slice(&0x0011u16.to_be_bytes());
3128 b[0x0022..0x0026].copy_from_slice(&0x00FF_0000u32.to_be_bytes());
3130 b[0x0026..0x0028].copy_from_slice(&1u16.to_be_bytes());
3132 b[0x0028..0x002A].copy_from_slice(&1u16.to_be_bytes());
3134 b[0x002A] = 0;
3136 b[0x003E..0x0040].copy_from_slice(&2u16.to_be_bytes());
3138 let pid = b"OXIDEAV-TEST";
3140 b[0x0040..0x0040 + pid.len()].copy_from_slice(pid);
3141 b[0x0080..0x0084].copy_from_slice(&0x01FFu32.to_be_bytes());
3143 b[0x0084..0x0088].copy_from_slice(&0u32.to_be_bytes());
3145 b[0x00C4..0x00C8].copy_from_slice(&1u32.to_be_bytes());
3148 b[0x00D0..0x00D4].copy_from_slice(&2u32.to_be_bytes());
3152 b
3156 }
3157
3158 #[test]
3159 fn vmgi_mat_parse_roundtrip() {
3160 let buf = build_vmg_mat();
3161 let vmg = VmgIfo::parse(&buf).unwrap();
3162 assert_eq!(vmg.last_sector_vmg_set, 1000);
3163 assert_eq!(vmg.last_sector_ifo, 4);
3164 assert_eq!(vmg.version, 0x0011);
3165 assert_eq!(vmg.number_of_volumes, 1);
3166 assert_eq!(vmg.volume_number, 1);
3167 assert_eq!(vmg.side_id, 0);
3168 assert_eq!(vmg.number_of_title_sets, 2);
3169 assert_eq!(vmg.provider_id, "OXIDEAV-TEST");
3170 assert_eq!(vmg.tt_srpt_sector, 1);
3171 assert_eq!(vmg.vts_atrt_sector, 2);
3172 assert_eq!(vmg.menu_vob_sector, 0);
3173 }
3174
3175 #[test]
3176 fn vmgi_mat_rejects_bad_magic() {
3177 let mut buf = build_vmg_mat();
3178 buf[0..12].copy_from_slice(b"DVDVIDEO-BAD");
3179 let err = VmgIfo::parse(&buf).unwrap_err();
3180 match err {
3181 Error::InvalidUdf(_) => {}
3182 other => panic!("expected InvalidUdf, got {other:?}"),
3183 }
3184 }
3185
3186 fn build_vtsi_mat(
3191 ptt_srpt_sector: u32,
3192 pgci_sector: u32,
3193 c_adt_sector: u32,
3194 title_vob_sector: u32,
3195 ) -> Vec<u8> {
3196 let mut b = vec![0u8; 0x200];
3197 b[0..12].copy_from_slice(VTS_MAGIC);
3198 b[0x000C..0x0010].copy_from_slice(&100_000u32.to_be_bytes());
3200 b[0x001C..0x0020].copy_from_slice(&15u32.to_be_bytes());
3202 b[0x0020..0x0022].copy_from_slice(&0x0011u16.to_be_bytes());
3204 b[0x0080..0x0084].copy_from_slice(&0x01FFu32.to_be_bytes());
3207 b[0x00C4..0x00C8].copy_from_slice(&title_vob_sector.to_be_bytes());
3210 b[0x00C8..0x00CC].copy_from_slice(&ptt_srpt_sector.to_be_bytes());
3212 b[0x00CC..0x00D0].copy_from_slice(&pgci_sector.to_be_bytes());
3214 b[0x00E0..0x00E4].copy_from_slice(&c_adt_sector.to_be_bytes());
3220 b
3222 }
3223
3224 #[test]
3225 fn vtsi_mat_parse_roundtrip() {
3226 let buf = build_vtsi_mat(1, 2, 3, 42);
3227 let mat = VtsiMat::parse(&buf).unwrap();
3228 assert_eq!(mat.last_sector_title_set, 100_000);
3229 assert_eq!(mat.last_sector_ifo, 15);
3230 assert_eq!(mat.version, 0x0011);
3231 assert_eq!(mat.title_vob_sector, 42);
3232 assert_eq!(mat.vts_ptt_srpt_sector, 1);
3233 assert_eq!(mat.vts_pgci_sector, 2);
3234 assert_eq!(mat.vts_c_adt_sector, 3);
3235 }
3236
3237 fn build_tt_srpt(entries: &[(u8, u8, u16, u8, u8, u32)]) -> Vec<u8> {
3242 let n = entries.len();
3244 let len = 8 + n * 12;
3245 let mut b = vec![0u8; len];
3246 b[0..2].copy_from_slice(&(n as u16).to_be_bytes());
3247 let end_addr = (len - 1) as u32;
3250 b[4..8].copy_from_slice(&end_addr.to_be_bytes());
3251 for (i, e) in entries.iter().enumerate() {
3252 let base = 8 + i * 12;
3253 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; }
3262 b
3263 }
3264
3265 #[test]
3266 fn tt_srpt_parses_titles() {
3267 let entries = [
3270 (0x3F, 1u8, 15u16, 1u8, 1u8, 0x0000_0500u32),
3271 (0x3F, 1u8, 4u16, 1u8, 2u8, 0x0000_0500u32),
3272 (0x3F, 2u8, 1u16, 2u8, 1u8, 0x0000_8000u32),
3273 ];
3274 let buf = build_tt_srpt(&entries);
3275 let srpt = TtSrpt::parse(&buf).unwrap();
3276 assert_eq!(srpt.title_count, 3);
3277 assert_eq!(srpt.end_address, (8 + 3 * 12 - 1) as u32);
3278 assert_eq!(srpt.entries[0].chapter_count, 15);
3279 assert_eq!(srpt.entries[1].vts_title_number, 2);
3280 assert_eq!(srpt.entries[2].vts_number, 2);
3281 assert_eq!(srpt.entries[2].angle_count, 2);
3282 assert_eq!(srpt.entries[2].vts_start_sector, 0x0000_8000);
3283 }
3284
3285 fn build_c_adt(rows: &[(u16, u8, u32, u32)]) -> Vec<u8> {
3290 let n = rows.len();
3291 let len = 8 + n * 12;
3292 let mut b = vec![0u8; len];
3293 let distinct = {
3295 let mut v: Vec<u16> = rows.iter().map(|r| r.0).collect();
3296 v.sort();
3297 v.dedup();
3298 v.len() as u16
3299 };
3300 b[0..2].copy_from_slice(&distinct.to_be_bytes());
3301 let end_addr = (len - 1) as u32;
3303 b[4..8].copy_from_slice(&end_addr.to_be_bytes());
3304 for (i, r) in rows.iter().enumerate() {
3305 let base = 8 + i * 12;
3306 b[base..base + 2].copy_from_slice(&r.0.to_be_bytes());
3307 b[base + 2] = r.1;
3308 b[base + 4..base + 8].copy_from_slice(&r.2.to_be_bytes());
3310 b[base + 8..base + 12].copy_from_slice(&r.3.to_be_bytes());
3311 }
3312 b
3313 }
3314
3315 #[test]
3316 fn c_adt_parses_four_rows() {
3317 let rows = [
3319 (1u16, 1u8, 100u32, 199u32),
3320 (1u16, 2u8, 200u32, 299u32),
3321 (1u16, 3u8, 300u32, 399u32),
3322 (2u16, 1u8, 1000u32, 1999u32),
3323 ];
3324 let buf = build_c_adt(&rows);
3325 let adt = VtsCAdt::parse(&buf).unwrap();
3326 assert_eq!(adt.number_of_vob_ids, 2);
3327 assert_eq!(adt.entries.len(), 4);
3328 assert_eq!(adt.lookup(1, 2), Some((200, 299)));
3329 assert_eq!(adt.lookup(2, 1), Some((1000, 1999)));
3330 assert_eq!(adt.lookup(3, 1), None);
3331 }
3332
3333 fn build_pgc_with_cells(cells: &[CellPlaybackInfo], positions: &[CellPositionInfo]) -> Vec<u8> {
3338 assert_eq!(cells.len(), positions.len());
3339 let n = cells.len();
3340 let header_size = 0xEC;
3344 let prog_count = 1u8;
3345 let prog_map_size = (usize::from(prog_count) + 1) & !1; let pre_n = 1usize; let post_n = 1usize;
3348 let cmd_cell_n = 2usize;
3349 let cmd_table_size = 8 + (pre_n + post_n + cmd_cell_n) * 8;
3350 let cpbi_size = n * 24;
3351 let cpos_size = n * 4;
3352 let mut b = vec![0u8; header_size + cmd_table_size + prog_map_size + cpbi_size + cpos_size];
3353
3354 b[0x0002] = prog_count;
3356 b[0x0003] = n as u8;
3358 b[0x0004..0x0008].copy_from_slice(&[0x00, 0x05, 0x00, 0xE0]); for i in 0..16usize {
3368 let base = 0x00A4 + i * 4;
3369 b[base] = 0x00; b[base + 1] = 0x10 + i as u8; b[base + 2] = 0x80; b[base + 3] = 0x80; }
3374
3375 let off_cmd = header_size as u16; let off_pmap = (header_size + cmd_table_size) as u16;
3377 let off_cpbi = (header_size + cmd_table_size + prog_map_size) as u16;
3378 let off_cpos = (header_size + cmd_table_size + prog_map_size + cpbi_size) as u16;
3379 b[0x00E4..0x00E6].copy_from_slice(&off_cmd.to_be_bytes());
3380 b[0x00E6..0x00E8].copy_from_slice(&off_pmap.to_be_bytes());
3381 b[0x00E8..0x00EA].copy_from_slice(&off_cpbi.to_be_bytes());
3382 b[0x00EA..0x00EC].copy_from_slice(&off_cpos.to_be_bytes());
3383
3384 let ct = header_size;
3387 b[ct..ct + 2].copy_from_slice(&(pre_n as u16).to_be_bytes());
3388 b[ct + 2..ct + 4].copy_from_slice(&(post_n as u16).to_be_bytes());
3389 b[ct + 4..ct + 6].copy_from_slice(&(cmd_cell_n as u16).to_be_bytes());
3390 b[ct + 6..ct + 8].copy_from_slice(&((cmd_table_size - 1) as u16).to_be_bytes());
3391 let mut w = ct + 8;
3392 b[w] = 0xA0; b[w + 7] = 0x01;
3394 w += 8;
3395 b[w] = 0xB0; b[w + 7] = 0x02;
3397 w += 8;
3398 b[w] = 0xC0; b[w + 7] = 0x03;
3400 w += 8;
3401 b[w] = 0xC1; b[w + 7] = 0x04;
3403
3404 b[off_pmap as usize] = 1;
3406 for (i, c) in cells.iter().enumerate() {
3410 let base = off_cpbi as usize + i * 24;
3411 b[base] = c.category_byte0;
3412 b[base + 1] = if c.restricted { 0x80 } else { 0 };
3413 b[base + 2] = c.still_time;
3414 b[base + 3] = c.cell_command;
3415 b[base + 4] = 0x00;
3417 b[base + 5] = 0x01;
3418 b[base + 6] = 0x00;
3419 b[base + 7] = 0xE0;
3420 b[base + 8..base + 12].copy_from_slice(&c.first_vobu_start_sector.to_be_bytes());
3421 b[base + 12..base + 16].copy_from_slice(&c.first_ilvu_end_sector.to_be_bytes());
3422 b[base + 16..base + 20].copy_from_slice(&c.last_vobu_start_sector.to_be_bytes());
3423 b[base + 20..base + 24].copy_from_slice(&c.last_vobu_end_sector.to_be_bytes());
3424 }
3425
3426 for (i, p) in positions.iter().enumerate() {
3428 let base = off_cpos as usize + i * 4;
3429 b[base..base + 2].copy_from_slice(&p.vob_id.to_be_bytes());
3430 b[base + 3] = p.cell_id;
3431 }
3432 b
3433 }
3434
3435 fn make_cell(start: u32, end: u32) -> CellPlaybackInfo {
3436 CellPlaybackInfo {
3437 category_byte0: 0,
3438 restricted: false,
3439 still_time: 0,
3440 cell_command: 0,
3441 playback_time: PgcTime::from_bytes([0, 1, 0, 0xE0]),
3442 first_vobu_start_sector: start,
3443 first_ilvu_end_sector: start + 5,
3444 last_vobu_start_sector: end - 5,
3445 last_vobu_end_sector: end,
3446 }
3447 }
3448
3449 #[test]
3450 fn pgci_parses_one_pgc_with_three_cells() {
3451 let cells = [
3452 make_cell(1000, 1999),
3453 make_cell(2000, 2999),
3454 make_cell(3000, 3999),
3455 ];
3456 let positions = [
3457 CellPositionInfo {
3458 vob_id: 1,
3459 cell_id: 1,
3460 },
3461 CellPositionInfo {
3462 vob_id: 1,
3463 cell_id: 2,
3464 },
3465 CellPositionInfo {
3466 vob_id: 1,
3467 cell_id: 3,
3468 },
3469 ];
3470 let pgc_blob = build_pgc_with_cells(&cells, &positions);
3471
3472 let srp_size = 8usize;
3475 let body_off = 8 + srp_size; let total = body_off + pgc_blob.len();
3477 let mut b = vec![0u8; total];
3478 b[0..2].copy_from_slice(&1u16.to_be_bytes());
3480 b[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
3483 b[8..12].copy_from_slice(&0u32.to_be_bytes());
3485 b[12..16].copy_from_slice(&(body_off as u32).to_be_bytes());
3486 b[body_off..body_off + pgc_blob.len()].copy_from_slice(&pgc_blob);
3488
3489 let pgci = Pgci::parse(&b).unwrap();
3490 assert_eq!(pgci.number_of_pgcs, 1);
3491 assert_eq!(pgci.pgcs.len(), 1);
3492 let pgc = &pgci.pgcs[0];
3493 assert_eq!(pgc.number_of_programs, 1);
3494 assert_eq!(pgc.number_of_cells, 3);
3495 assert_eq!(pgc.cells.len(), 3);
3496 assert_eq!(pgc.cell_positions.len(), 3);
3497 assert_eq!(pgc.cells[0].first_vobu_start_sector, 1000);
3498 assert_eq!(pgc.cells[2].last_vobu_end_sector, 3999);
3499 assert_eq!(pgc.cell_positions[1].cell_id, 2);
3500 assert_eq!(pgc.playback_time.frame_rate, FrameRate::Ntsc30);
3501
3502 assert_eq!(
3504 pgc.palette[0],
3505 PaletteEntry {
3506 y: 0x10,
3507 cr: 0x80,
3508 cb: 0x80
3509 }
3510 );
3511 assert_eq!(
3512 pgc.palette[15],
3513 PaletteEntry {
3514 y: 0x1F,
3515 cr: 0x80,
3516 cb: 0x80
3517 }
3518 );
3519
3520 let cmds = pgc.commands.as_ref().expect("command table present");
3522 assert_eq!(cmds.pre.len(), 1);
3523 assert_eq!(cmds.post.len(), 1);
3524 assert_eq!(cmds.cell.len(), 2);
3525 assert_eq!(cmds.pre[0].bytes[0], 0xA0);
3526 assert_eq!(cmds.pre[0].bytes[7], 0x01);
3527 assert_eq!(cmds.pre[0].command_type(), 0b101);
3528 assert_eq!(cmds.post[0].bytes[0], 0xB0);
3529 assert_eq!(cmds.post[0].bytes[7], 0x02);
3530 assert_eq!(cmds.cell[0].bytes[7], 0x03);
3531 assert_eq!(cmds.cell[1].bytes[7], 0x04);
3532 }
3533
3534 #[test]
3539 fn palette_entry_skips_reserved_byte() {
3540 let e = PaletteEntry::parse(&[0xFF, 0x42, 0x10, 0xC0]).unwrap();
3542 assert_eq!(
3543 e,
3544 PaletteEntry {
3545 y: 0x42,
3546 cr: 0x10,
3547 cb: 0xC0
3548 }
3549 );
3550 assert!(PaletteEntry::parse(&[0x00, 0x01, 0x02]).is_err());
3552 }
3553
3554 #[test]
3555 fn command_table_carves_three_lists() {
3556 let pre = 2u16;
3558 let post = 1u16;
3559 let cell = 1u16;
3560 let total = (pre + post + cell) as usize;
3561 let size = 8 + total * 8;
3562 let mut b = vec![0u8; size];
3563 b[0..2].copy_from_slice(&pre.to_be_bytes());
3564 b[2..4].copy_from_slice(&post.to_be_bytes());
3565 b[4..6].copy_from_slice(&cell.to_be_bytes());
3566 b[6..8].copy_from_slice(&((size - 1) as u16).to_be_bytes());
3567 for i in 0..total {
3569 b[8 + i * 8 + 7] = (i + 1) as u8;
3570 }
3571 let t = PgcCommandTable::parse(&b).unwrap();
3572 assert_eq!(t.pre.len(), 2);
3573 assert_eq!(t.post.len(), 1);
3574 assert_eq!(t.cell.len(), 1);
3575 assert_eq!(t.end_address, (size - 1) as u16);
3576 assert_eq!(t.pre[0].bytes[7], 1);
3578 assert_eq!(t.pre[1].bytes[7], 2);
3579 assert_eq!(t.post[0].bytes[7], 3);
3580 assert_eq!(t.cell[0].bytes[7], 4);
3581 }
3582
3583 #[test]
3584 fn command_table_rejects_overlong_count() {
3585 let mut b = vec![0u8; 8];
3587 b[0..2].copy_from_slice(&129u16.to_be_bytes());
3588 assert!(PgcCommandTable::parse(&b).is_err());
3589 }
3590
3591 #[test]
3592 fn command_table_rejects_truncated_list() {
3593 let mut b = vec![0u8; 8 + 8];
3595 b[0..2].copy_from_slice(&2u16.to_be_bytes());
3596 assert!(PgcCommandTable::parse(&b).is_err());
3597 }
3598
3599 fn synth_command_table() -> PgcCommandTable {
3607 let pre = 1u16;
3608 let post = 1u16;
3609 let cell = 3u16;
3610 let total = (pre + post + cell) as usize;
3611 let size = 8 + total * 8;
3612 let mut b = vec![0u8; size];
3613 b[0..2].copy_from_slice(&pre.to_be_bytes());
3614 b[2..4].copy_from_slice(&post.to_be_bytes());
3615 b[4..6].copy_from_slice(&cell.to_be_bytes());
3616 b[6..8].copy_from_slice(&((size - 1) as u16).to_be_bytes());
3617 let post_off = 8 + 8;
3628 b[post_off] = 0b0011_0000;
3629 b[post_off + 1] = 0x01;
3630 let cell0 = post_off + 8;
3632 b[cell0] = 0b0011_0000;
3633 b[cell0 + 1] = 0x01;
3634 let cell1 = cell0 + 8;
3636 b[cell1] = 0x00;
3637 b[cell1 + 1] = 0x02;
3638 PgcCommandTable::parse(&b).unwrap()
3640 }
3641
3642 #[test]
3643 fn pgc_cmd_table_typed_iterators_walk_all_three_lists() {
3644 let t = synth_command_table();
3645 let pre: Vec<_> = t.pre_instructions().collect();
3646 let post: Vec<_> = t.post_instructions().collect();
3647 let cell: Vec<_> = t.cell_instructions().collect();
3648 assert_eq!(pre.len(), 1);
3649 assert_eq!(post.len(), 1);
3650 assert_eq!(cell.len(), 3);
3651 assert!(matches!(pre[0], crate::nav::NavInstruction::Nop));
3653 assert!(matches!(post[0], crate::nav::NavInstruction::Exit));
3655 assert!(matches!(cell[0], crate::nav::NavInstruction::Exit));
3657 assert!(matches!(cell[1], crate::nav::NavInstruction::Break));
3658 assert!(matches!(cell[2], crate::nav::NavInstruction::Nop));
3659 }
3660
3661 #[test]
3662 fn pgc_cmd_table_cell_instruction_uses_one_based_index() {
3663 let t = synth_command_table();
3664 assert!(t.cell_instruction(0).is_none());
3667 assert!(matches!(
3669 t.cell_instruction(1),
3670 Some(crate::nav::NavInstruction::Exit)
3671 ));
3672 assert!(matches!(
3674 t.cell_instruction(2),
3675 Some(crate::nav::NavInstruction::Break)
3676 ));
3677 assert!(matches!(
3679 t.cell_instruction(3),
3680 Some(crate::nav::NavInstruction::Nop)
3681 ));
3682 assert!(t.cell_instruction(4).is_none());
3684 assert!(t.cell_instruction(u16::MAX).is_none());
3685 }
3686
3687 #[test]
3688 fn nav_command_decode_instruction_matches_nav_decode() {
3689 let mut bytes = [0u8; 8];
3693 bytes[0] = 0b0011_0000; bytes[1] = 0x01; let raw = NavCommand { bytes };
3696 assert_eq!(raw.decode_instruction(), raw.decode());
3697 assert!(matches!(
3698 raw.decode_instruction(),
3699 crate::nav::NavInstruction::Exit
3700 ));
3701 }
3702
3703 #[test]
3704 fn pgc_without_command_table_yields_none() {
3705 let mut b = vec![0u8; 0xEC];
3708 b[0x0002] = 0; b[0x0003] = 0; b[0x0004..0x0008].copy_from_slice(&[0x00, 0x00, 0x00, 0xC0]); let pgc = Pgc::parse(&b).unwrap();
3713 assert!(pgc.commands.is_none());
3714 assert_eq!(pgc.palette[7], PaletteEntry::default());
3716 }
3717
3718 #[test]
3723 fn ptt_srpt_walks_two_titles_five_chapters() {
3724 let n_titles = 2usize;
3728 let n_chaps = 5usize;
3729 let offsets_size = n_titles * 4;
3730 let header_size = 8 + offsets_size;
3731 let title_body_size = n_chaps * 4;
3732 let total = header_size + n_titles * title_body_size;
3733 let mut b = vec![0u8; total];
3734 b[0..2].copy_from_slice(&(n_titles as u16).to_be_bytes());
3736 b[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
3738 for ti in 0..n_titles {
3741 let off = (header_size + ti * title_body_size) as u32;
3742 b[8 + ti * 4..8 + ti * 4 + 4].copy_from_slice(&off.to_be_bytes());
3743 }
3744 for ti in 0..n_titles {
3746 for ci in 0..n_chaps {
3747 let base = header_size + ti * title_body_size + ci * 4;
3748 let pgcn = (ti + 1) as u16;
3749 let pgn = (ci + 1) as u16;
3750 b[base..base + 2].copy_from_slice(&pgcn.to_be_bytes());
3751 b[base + 2..base + 4].copy_from_slice(&pgn.to_be_bytes());
3752 }
3753 }
3754
3755 let srpt = VtsPttSrpt::parse(&b).unwrap();
3756 assert_eq!(srpt.title_count, 2);
3757 assert_eq!(srpt.titles.len(), 2);
3758 for ti in 0..n_titles {
3759 assert_eq!(srpt.titles[ti].chapters.len(), 5);
3760 assert_eq!(srpt.titles[ti].chapters[0].pgcn, (ti + 1) as u16);
3761 assert_eq!(srpt.titles[ti].chapters[0].pgn, 1);
3762 assert_eq!(srpt.titles[ti].chapters[4].pgn, 5);
3763 }
3764 }
3765
3766 fn make_composite_vts() -> Vec<u8> {
3771 let mut img = vec![0u8; DVD_SECTOR * 4];
3777
3778 let mat = build_vtsi_mat(1, 2, 3, 100);
3780 img[0..mat.len()].copy_from_slice(&mat);
3781
3782 let cells: Vec<CellPlaybackInfo> = (0..5)
3787 .map(|i| make_cell(1000 + i * 1000, 1999 + i * 1000))
3788 .collect();
3789 let positions: Vec<CellPositionInfo> = (0..5)
3790 .map(|i| CellPositionInfo {
3791 vob_id: 1,
3792 cell_id: (i + 1) as u8,
3793 })
3794 .collect();
3795 let header_size = 0xEC;
3798 let prog_count = 3u8;
3799 let prog_map_size = (usize::from(prog_count) + 1) & !1; let cpbi_size = 5 * 24;
3801 let cpos_size = 5 * 4;
3802 let pgc_blob_len = header_size + prog_map_size + cpbi_size + cpos_size;
3803 let mut pgc_blob = vec![0u8; pgc_blob_len];
3804 pgc_blob[0x0002] = prog_count;
3805 pgc_blob[0x0003] = 5; pgc_blob[0x0004..0x0008].copy_from_slice(&[0x00, 0x15, 0x00, 0xE0]);
3807 let off_pmap = header_size as u16;
3808 let off_cpbi = (header_size + prog_map_size) as u16;
3809 let off_cpos = (header_size + prog_map_size + cpbi_size) as u16;
3810 pgc_blob[0x00E6..0x00E8].copy_from_slice(&off_pmap.to_be_bytes());
3811 pgc_blob[0x00E8..0x00EA].copy_from_slice(&off_cpbi.to_be_bytes());
3812 pgc_blob[0x00EA..0x00EC].copy_from_slice(&off_cpos.to_be_bytes());
3813 pgc_blob[header_size] = 1; pgc_blob[header_size + 1] = 3; pgc_blob[header_size + 2] = 5; for (i, c) in cells.iter().enumerate() {
3817 let base = header_size + prog_map_size + i * 24;
3818 pgc_blob[base + 4..base + 8].copy_from_slice(&[0, 1, 0, 0xE0]);
3819 pgc_blob[base + 8..base + 12].copy_from_slice(&c.first_vobu_start_sector.to_be_bytes());
3820 pgc_blob[base + 12..base + 16].copy_from_slice(&c.first_ilvu_end_sector.to_be_bytes());
3821 pgc_blob[base + 16..base + 20].copy_from_slice(&c.last_vobu_start_sector.to_be_bytes());
3822 pgc_blob[base + 20..base + 24].copy_from_slice(&c.last_vobu_end_sector.to_be_bytes());
3823 }
3824 for (i, p) in positions.iter().enumerate() {
3825 let base = header_size + prog_map_size + cpbi_size + i * 4;
3826 pgc_blob[base..base + 2].copy_from_slice(&p.vob_id.to_be_bytes());
3827 pgc_blob[base + 3] = p.cell_id;
3828 }
3829 let srp_size = 8usize;
3831 let body_off = 8 + srp_size;
3832 let pgci_total = body_off + pgc_blob.len();
3833 let mut pgci = vec![0u8; pgci_total];
3834 pgci[0..2].copy_from_slice(&1u16.to_be_bytes());
3835 pgci[4..8].copy_from_slice(&((pgci_total - 1) as u32).to_be_bytes());
3836 pgci[12..16].copy_from_slice(&(body_off as u32).to_be_bytes());
3837 pgci[body_off..body_off + pgc_blob.len()].copy_from_slice(&pgc_blob);
3838 img[2 * DVD_SECTOR..2 * DVD_SECTOR + pgci.len()].copy_from_slice(&pgci);
3839
3840 let n_titles = 1usize;
3843 let n_chaps = 3usize;
3844 let header_sz = 8 + n_titles * 4;
3845 let title_body = n_chaps * 4;
3846 let total = header_sz + title_body;
3847 let mut ptt = vec![0u8; total];
3848 ptt[0..2].copy_from_slice(&(n_titles as u16).to_be_bytes());
3849 ptt[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
3850 ptt[8..12].copy_from_slice(&(header_sz as u32).to_be_bytes());
3851 for ci in 0..n_chaps {
3852 let base = header_sz + ci * 4;
3853 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());
3855 }
3857 img[DVD_SECTOR..DVD_SECTOR + ptt.len()].copy_from_slice(&ptt);
3858
3859 let cadt_rows: Vec<(u16, u8, u32, u32)> = (0..5)
3861 .map(|i| {
3862 (
3863 1u16,
3864 (i + 1) as u8,
3865 1000 + i as u32 * 1000,
3866 1999 + i as u32 * 1000,
3867 )
3868 })
3869 .collect();
3870 let cadt = build_c_adt(&cadt_rows);
3871 img[3 * DVD_SECTOR..3 * DVD_SECTOR + cadt.len()].copy_from_slice(&cadt);
3872
3873 img
3874 }
3875
3876 #[test]
3877 fn composite_vts_roundtrip() {
3878 let img = make_composite_vts();
3879 let vts = VtsIfo::parse(&img, 1).unwrap();
3880 assert_eq!(vts.vts_number, 1);
3881 assert_eq!(vts.title_count, 1);
3882 assert_eq!(vts.pgcs.len(), 1);
3883 assert_eq!(vts.pgcs[0].number_of_cells, 5);
3885 assert_eq!(vts.pgcs[0].number_of_programs, 3);
3886 let t = &vts.titles[0];
3888 assert_eq!(t.chapter_count, 3);
3889 assert_eq!(t.chapters[0].start_cell, 1);
3892 assert_eq!(t.chapters[0].end_cell, 2);
3893 assert_eq!(t.chapters[1].start_cell, 3);
3895 assert_eq!(t.chapters[1].end_cell, 4);
3896 assert_eq!(t.chapters[2].start_cell, 5);
3898 assert_eq!(t.chapters[2].end_cell, 5);
3899 assert_eq!(vts.cell_adt.lookup(1, 1), Some((1000, 1999)));
3901 assert_eq!(vts.cell_adt.lookup(1, 5), Some((5000, 5999)));
3902 assert!(vts.vobu_admap.is_none());
3905 assert!(vts.time_map.is_none());
3906 }
3907
3908 fn build_vobu_admap(entries: &[u32]) -> Vec<u8> {
3913 let n = entries.len();
3915 let len = 4 + n * 4;
3916 let mut b = vec![0u8; len];
3917 let end_addr = (len - 1) as u32;
3918 b[0..4].copy_from_slice(&end_addr.to_be_bytes());
3919 for (i, s) in entries.iter().enumerate() {
3920 b[4 + i * 4..4 + (i + 1) * 4].copy_from_slice(&s.to_be_bytes());
3921 }
3922 b
3923 }
3924
3925 #[test]
3926 fn vobu_admap_parses_three_vobus() {
3927 let entries = [0u32, 200u32, 450u32];
3928 let buf = build_vobu_admap(&entries);
3929 let map = VobuAdmap::parse(&buf).unwrap();
3930 assert_eq!(map.vobu_count(), 3);
3931 assert_eq!(map.entries, entries);
3932 assert_eq!(map.end_address, (buf.len() - 1) as u32);
3933 assert_eq!(map.vobu_start_sector(1), Some(0));
3935 assert_eq!(map.vobu_start_sector(2), Some(200));
3936 assert_eq!(map.vobu_start_sector(3), Some(450));
3937 assert_eq!(map.vobu_start_sector(4), None);
3938 assert_eq!(map.vobu_start_sector(0), None);
3939 }
3940
3941 #[test]
3942 fn vobu_admap_partition_locates_containing_vobu() {
3943 let buf = build_vobu_admap(&[0, 100, 300, 600]);
3945 let map = VobuAdmap::parse(&buf).unwrap();
3946 assert_eq!(map.vobu_containing(0), Some(1));
3949 assert_eq!(map.vobu_containing(99), Some(1));
3950 assert_eq!(map.vobu_containing(100), Some(2));
3951 assert_eq!(map.vobu_containing(299), Some(2));
3952 assert_eq!(map.vobu_containing(300), Some(3));
3953 assert_eq!(map.vobu_containing(599), Some(3));
3954 assert_eq!(map.vobu_containing(600), Some(4));
3955 assert_eq!(map.vobu_containing(1_000_000), Some(4));
3958 }
3959
3960 #[test]
3961 fn vobu_admap_first_entry_above_zero_returns_none_for_pre_sector() {
3962 let buf = build_vobu_admap(&[100, 200, 300]);
3965 let map = VobuAdmap::parse(&buf).unwrap();
3966 assert_eq!(map.vobu_containing(0), None);
3967 assert_eq!(map.vobu_containing(99), None);
3968 assert_eq!(map.vobu_containing(100), Some(1));
3969 }
3970
3971 #[test]
3972 fn vobu_admap_empty_map_lookups_return_none() {
3973 let mut b = vec![0u8; 4];
3975 b[0..4].copy_from_slice(&3u32.to_be_bytes());
3976 let map = VobuAdmap::parse(&b).unwrap();
3977 assert_eq!(map.vobu_count(), 0);
3978 assert_eq!(map.vobu_start_sector(1), None);
3979 assert_eq!(map.vobu_containing(0), None);
3980 }
3981
3982 #[test]
3983 fn vobu_admap_rejects_non_multiple_end_address() {
3984 let mut b = vec![0u8; 8];
3987 b[0..4].copy_from_slice(&5u32.to_be_bytes());
3988 assert!(VobuAdmap::parse(&b).is_err());
3989 }
3990
3991 #[test]
3992 fn vobu_admap_rejects_truncated_buffer() {
3993 let mut b = vec![0u8; 4 + 4];
3997 b[0..4].copy_from_slice(&11u32.to_be_bytes());
3998 assert!(VobuAdmap::parse(&b).is_err());
3999 }
4000
4001 fn build_tmap(time_unit: u8, entries: &[(u32, bool)]) -> Vec<u8> {
4006 let n = entries.len();
4007 let len = 4 + n * 4;
4008 let mut b = vec![0u8; len];
4009 b[0] = time_unit;
4010 b[2..4].copy_from_slice(&(n as u16).to_be_bytes());
4012 for (i, (sector, disc)) in entries.iter().enumerate() {
4013 let mut raw = *sector & TmapEntry::SECTOR_MASK;
4014 if *disc {
4015 raw |= TmapEntry::DISCONTINUITY_BIT;
4016 }
4017 b[4 + i * 4..4 + (i + 1) * 4].copy_from_slice(&raw.to_be_bytes());
4018 }
4019 b
4020 }
4021
4022 #[test]
4023 fn tmap_decodes_entries_and_discontinuity() {
4024 let buf = build_tmap(4, &[(100, false), (250, true), (400, false)]);
4026 let map = VtsTmap::parse(&buf).unwrap();
4027 assert_eq!(map.time_unit, 4);
4028 assert_eq!(map.entries.len(), 3);
4029 assert_eq!(
4030 map.entries[0],
4031 TmapEntry {
4032 sector: 100,
4033 discontinuous: false
4034 }
4035 );
4036 assert_eq!(
4037 map.entries[1],
4038 TmapEntry {
4039 sector: 250,
4040 discontinuous: true
4041 }
4042 );
4043 assert_eq!(map.total_seconds(), 12);
4044 }
4045
4046 #[test]
4047 fn tmap_sector_at_brackets_seconds_per_time_unit() {
4048 let buf = build_tmap(5, &[(10, false), (20, false), (30, false)]);
4051 let map = VtsTmap::parse(&buf).unwrap();
4052 assert_eq!(map.sector_at(0), Some(10));
4053 assert_eq!(map.sector_at(4), Some(10));
4054 assert_eq!(map.sector_at(5), Some(20));
4055 assert_eq!(map.sector_at(9), Some(20));
4056 assert_eq!(map.sector_at(10), Some(30));
4057 assert_eq!(map.sector_at(14), Some(30));
4058 assert_eq!(map.sector_at(15), Some(30));
4062 assert_eq!(map.sector_at(1_000_000), Some(30));
4063 }
4064
4065 #[test]
4066 fn tmap_empty_map_yields_no_sector() {
4067 let buf = build_tmap(2, &[]);
4070 let map = VtsTmap::parse(&buf).unwrap();
4071 assert_eq!(map.time_unit, 2);
4072 assert!(map.entries.is_empty());
4073 assert_eq!(map.sector_at(0), None);
4074 assert_eq!(map.sector_at(60), None);
4075 assert_eq!(map.total_seconds(), 0);
4076 }
4077
4078 #[test]
4079 fn tmap_zero_time_unit_yields_no_sector() {
4080 let buf = build_tmap(0, &[(1, false), (2, false)]);
4084 let map = VtsTmap::parse(&buf).unwrap();
4085 assert_eq!(map.sector_at(0), None);
4086 }
4087
4088 #[test]
4089 fn tmap_rejects_truncated_buffer() {
4090 let mut b = vec![0u8; 4 + 4];
4093 b[0] = 1;
4094 b[2..4].copy_from_slice(&2u16.to_be_bytes());
4095 assert!(VtsTmap::parse(&b).is_err());
4096 }
4097
4098 fn build_tmapti(maps: &[Vec<u8>]) -> Vec<u8> {
4099 let n = maps.len();
4101 let offsets_size = n * 4;
4102 let header_size = 8 + offsets_size;
4103 let body_size: usize = maps.iter().map(|m| m.len()).sum();
4104 let total = header_size + body_size;
4105 let mut b = vec![0u8; total];
4106 b[0..2].copy_from_slice(&(n as u16).to_be_bytes());
4107 b[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
4109 let mut cursor = header_size;
4110 for (i, m) in maps.iter().enumerate() {
4111 let off = cursor as u32;
4112 b[8 + i * 4..8 + (i + 1) * 4].copy_from_slice(&off.to_be_bytes());
4113 b[cursor..cursor + m.len()].copy_from_slice(m);
4114 cursor += m.len();
4115 }
4116 b
4117 }
4118
4119 #[test]
4120 fn tmapti_walks_two_pgc_maps() {
4121 let map_a = build_tmap(2, &[(0, false), (50, false), (100, false)]);
4122 let map_b = build_tmap(3, &[(200, false), (400, true)]);
4123 let buf = build_tmapti(&[map_a, map_b]);
4124 let table = VtsTmapti::parse(&buf).unwrap();
4125 assert_eq!(table.number_of_pgcs, 2);
4126 assert_eq!(table.maps.len(), 2);
4127 let m1 = table.get(1).unwrap();
4129 assert_eq!(m1.time_unit, 2);
4130 assert_eq!(m1.entries.len(), 3);
4131 let m2 = table.get(2).unwrap();
4132 assert_eq!(m2.time_unit, 3);
4133 assert!(m2.entries[1].discontinuous);
4134 assert_eq!(table.get(0), None);
4135 assert_eq!(table.get(3), None);
4136 }
4137
4138 #[test]
4139 fn tmapti_carries_empty_map_per_spec_invariant() {
4140 let empty = build_tmap(0, &[]);
4144 let buf = build_tmapti(&[empty]);
4145 let table = VtsTmapti::parse(&buf).unwrap();
4146 assert_eq!(table.number_of_pgcs, 1);
4147 let m = table.get(1).unwrap();
4148 assert!(m.entries.is_empty());
4149 assert_eq!(m.sector_at(0), None);
4150 }
4151
4152 #[test]
4153 fn tmapti_rejects_short_offset_list() {
4154 let mut b = vec![0u8; 8];
4157 b[0..2].copy_from_slice(&3u16.to_be_bytes());
4158 assert!(VtsTmapti::parse(&b).is_err());
4159 }
4160
4161 fn make_composite_vts_with_admap_and_tmap() -> Vec<u8> {
4166 let mut img = vec![0u8; DVD_SECTOR * 6];
4174
4175 let mat = build_vtsi_mat(1, 2, 3, 100);
4179 img[0..mat.len()].copy_from_slice(&mat);
4180 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)
4186 .map(|i| make_cell(1000 + i * 1000, 1999 + i * 1000))
4187 .collect();
4188 let positions: Vec<CellPositionInfo> = (0..5)
4189 .map(|i| CellPositionInfo {
4190 vob_id: 1,
4191 cell_id: (i + 1) as u8,
4192 })
4193 .collect();
4194 let header_size = 0xEC;
4195 let prog_count = 3u8;
4196 let prog_map_size = (usize::from(prog_count) + 1) & !1;
4197 let cpbi_size = 5 * 24;
4198 let cpos_size = 5 * 4;
4199 let pgc_blob_len = header_size + prog_map_size + cpbi_size + cpos_size;
4200 let mut pgc_blob = vec![0u8; pgc_blob_len];
4201 pgc_blob[0x0002] = prog_count;
4202 pgc_blob[0x0003] = 5;
4203 pgc_blob[0x0004..0x0008].copy_from_slice(&[0x00, 0x15, 0x00, 0xE0]);
4204 let off_pmap = header_size as u16;
4205 let off_cpbi = (header_size + prog_map_size) as u16;
4206 let off_cpos = (header_size + prog_map_size + cpbi_size) as u16;
4207 pgc_blob[0x00E6..0x00E8].copy_from_slice(&off_pmap.to_be_bytes());
4208 pgc_blob[0x00E8..0x00EA].copy_from_slice(&off_cpbi.to_be_bytes());
4209 pgc_blob[0x00EA..0x00EC].copy_from_slice(&off_cpos.to_be_bytes());
4210 pgc_blob[header_size] = 1;
4211 pgc_blob[header_size + 1] = 3;
4212 pgc_blob[header_size + 2] = 5;
4213 for (i, c) in cells.iter().enumerate() {
4214 let base = header_size + prog_map_size + i * 24;
4215 pgc_blob[base + 4..base + 8].copy_from_slice(&[0, 1, 0, 0xE0]);
4216 pgc_blob[base + 8..base + 12].copy_from_slice(&c.first_vobu_start_sector.to_be_bytes());
4217 pgc_blob[base + 12..base + 16].copy_from_slice(&c.first_ilvu_end_sector.to_be_bytes());
4218 pgc_blob[base + 16..base + 20].copy_from_slice(&c.last_vobu_start_sector.to_be_bytes());
4219 pgc_blob[base + 20..base + 24].copy_from_slice(&c.last_vobu_end_sector.to_be_bytes());
4220 }
4221 for (i, p) in positions.iter().enumerate() {
4222 let base = header_size + prog_map_size + cpbi_size + i * 4;
4223 pgc_blob[base..base + 2].copy_from_slice(&p.vob_id.to_be_bytes());
4224 pgc_blob[base + 3] = p.cell_id;
4225 }
4226 let srp_size = 8usize;
4227 let body_off = 8 + srp_size;
4228 let pgci_total = body_off + pgc_blob.len();
4229 let mut pgci = vec![0u8; pgci_total];
4230 pgci[0..2].copy_from_slice(&1u16.to_be_bytes());
4231 pgci[4..8].copy_from_slice(&((pgci_total - 1) as u32).to_be_bytes());
4232 pgci[12..16].copy_from_slice(&(body_off as u32).to_be_bytes());
4233 pgci[body_off..body_off + pgc_blob.len()].copy_from_slice(&pgc_blob);
4234 img[2 * DVD_SECTOR..2 * DVD_SECTOR + pgci.len()].copy_from_slice(&pgci);
4235
4236 let n_titles = 1usize;
4238 let n_chaps = 3usize;
4239 let header_sz = 8 + n_titles * 4;
4240 let title_body = n_chaps * 4;
4241 let total = header_sz + title_body;
4242 let mut ptt = vec![0u8; total];
4243 ptt[0..2].copy_from_slice(&(n_titles as u16).to_be_bytes());
4244 ptt[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
4245 ptt[8..12].copy_from_slice(&(header_sz as u32).to_be_bytes());
4246 for ci in 0..n_chaps {
4247 let base = header_sz + ci * 4;
4248 ptt[base..base + 2].copy_from_slice(&1u16.to_be_bytes());
4249 ptt[base + 2..base + 4].copy_from_slice(&((ci + 1) as u16).to_be_bytes());
4250 }
4251 img[DVD_SECTOR..DVD_SECTOR + ptt.len()].copy_from_slice(&ptt);
4252
4253 let cadt_rows: Vec<(u16, u8, u32, u32)> = (0..5)
4255 .map(|i| {
4256 (
4257 1u16,
4258 (i + 1) as u8,
4259 1000 + i as u32 * 1000,
4260 1999 + i as u32 * 1000,
4261 )
4262 })
4263 .collect();
4264 let cadt = build_c_adt(&cadt_rows);
4265 img[3 * DVD_SECTOR..3 * DVD_SECTOR + cadt.len()].copy_from_slice(&cadt);
4266
4267 let admap = build_vobu_admap(&[0, 250, 600, 1100]);
4270 img[4 * DVD_SECTOR..4 * DVD_SECTOR + admap.len()].copy_from_slice(&admap);
4271
4272 let tmap = build_tmap(4, &[(0, false), (250, false), (600, false)]);
4275 let tmapti = build_tmapti(&[tmap]);
4276 img[5 * DVD_SECTOR..5 * DVD_SECTOR + tmapti.len()].copy_from_slice(&tmapti);
4277
4278 img
4279 }
4280
4281 #[test]
4282 fn composite_vts_materialises_admap_and_tmap() {
4283 let img = make_composite_vts_with_admap_and_tmap();
4284 let vts = VtsIfo::parse(&img, 1).unwrap();
4285 let admap = vts.vobu_admap.as_ref().expect("VOBU_ADMAP materialised");
4286 assert_eq!(admap.vobu_count(), 4);
4287 assert_eq!(admap.vobu_start_sector(1), Some(0));
4288 assert_eq!(admap.vobu_start_sector(4), Some(1100));
4289 assert_eq!(admap.vobu_containing(700), Some(3));
4291
4292 let tmapti = vts.time_map.as_ref().expect("VTS_TMAPTI materialised");
4293 assert_eq!(tmapti.number_of_pgcs, 1);
4294 let pgc_map = tmapti.get(1).unwrap();
4295 assert_eq!(pgc_map.time_unit, 4);
4296 assert_eq!(pgc_map.entries.len(), 3);
4297
4298 assert_eq!(vts.vobu_sector_at_pgc_time(1, 5), Some(250));
4301 assert_eq!(vts.vobu_sector_at_pgc_time(1, 0), Some(0));
4303 assert_eq!(vts.vobu_sector_at_pgc_time(1, 9), Some(600));
4305 assert_eq!(vts.vobu_sector_at_pgc_time(2, 0), None);
4307 }
4308
4309 #[allow(clippy::too_many_arguments)]
4316 fn pack_audio_attr(
4317 coding: u8,
4318 mc_ext: bool,
4319 lang_type: u8,
4320 app_mode: u8,
4321 quant: u8,
4322 sample_rate: u8,
4323 chans_minus_one: u8,
4324 lang: [u8; 2],
4325 code_ext: u8,
4326 app_info: u8,
4327 ) -> [u8; 8] {
4328 let mut b = [0u8; 8];
4329 b[0] = ((coding & 0b111) << 5)
4330 | (if mc_ext { 0b1_0000 } else { 0 })
4331 | ((lang_type & 0b11) << 2)
4332 | (app_mode & 0b11);
4333 b[1] = ((quant & 0b11) << 6) | ((sample_rate & 0b11) << 4) | (chans_minus_one & 0b111);
4334 b[2] = lang[0];
4335 b[3] = lang[1];
4336 b[5] = code_ext;
4337 b[7] = app_info;
4338 b
4339 }
4340
4341 #[allow(clippy::too_many_arguments)]
4342 fn pack_video_attr(
4343 coding: u8,
4344 standard: u8,
4345 aspect: u8,
4346 pan_scan_disallowed: bool,
4347 letterbox_disallowed: bool,
4348 cc1: bool,
4349 cc2: bool,
4350 resolution: u8,
4351 letterbox_src: bool,
4352 film_pal: bool,
4353 ) -> [u8; 2] {
4354 let mut b = [0u8; 2];
4355 b[0] = ((coding & 0b11) << 6)
4356 | ((standard & 0b11) << 4)
4357 | ((aspect & 0b11) << 2)
4358 | (if pan_scan_disallowed { 0b10 } else { 0 })
4359 | (if letterbox_disallowed { 0b01 } else { 0 });
4360 b[1] = (if cc1 { 0b1000_0000 } else { 0 })
4361 | (if cc2 { 0b0100_0000 } else { 0 })
4362 | ((resolution & 0b111) << 3)
4363 | (if letterbox_src { 0b0000_0100 } else { 0 })
4364 | (if film_pal { 0b0000_0001 } else { 0 });
4365 b
4366 }
4367
4368 fn pack_subp_attr(coding: u8, lang_type: u8, lang: [u8; 2], code_ext: u8) -> [u8; 6] {
4369 let mut b = [0u8; 6];
4370 b[0] = ((coding & 0b111) << 5) | (lang_type & 0b11);
4371 b[2] = lang[0];
4372 b[3] = lang[1];
4373 b[5] = code_ext;
4374 b
4375 }
4376
4377 #[test]
4378 fn video_attributes_mpeg2_pal_16x9_full_d1() {
4379 let raw = pack_video_attr(1, 1, 3, false, false, false, false, 0, false, true);
4380 let v = VideoAttributes::parse(&raw);
4381 assert_eq!(v.coding_mode, VideoCodingMode::Mpeg2);
4382 assert_eq!(v.standard, VideoStandard::Pal);
4383 assert_eq!(v.aspect_ratio, VideoAspectRatio::Ratio16x9);
4384 assert_eq!(v.resolution, VideoResolution::FullD1);
4385 assert_eq!(
4386 v.resolution.dimensions(VideoStandard::Pal),
4387 Some((720, 576))
4388 );
4389 assert!(v.film_source_pal);
4390 assert!(!v.line21_field1_cc);
4391 }
4392
4393 #[test]
4394 fn video_attributes_mpeg2_ntsc_4x3_sif_with_cc() {
4395 let raw = pack_video_attr(1, 0, 0, true, false, true, true, 3, true, false);
4396 let v = VideoAttributes::parse(&raw);
4397 assert_eq!(v.coding_mode, VideoCodingMode::Mpeg2);
4398 assert_eq!(v.standard, VideoStandard::Ntsc);
4399 assert_eq!(v.aspect_ratio, VideoAspectRatio::Ratio4x3);
4400 assert_eq!(v.resolution, VideoResolution::Sif);
4401 assert_eq!(
4402 v.resolution.dimensions(VideoStandard::Ntsc),
4403 Some((352, 240))
4404 );
4405 assert!(v.pan_scan_disallowed);
4406 assert!(v.line21_field1_cc);
4407 assert!(v.line21_field2_cc);
4408 assert!(v.letterboxed_source);
4409 }
4410
4411 #[test]
4412 fn video_attributes_reserved_aspect_and_resolution() {
4413 let raw = pack_video_attr(1, 0, 1, false, false, false, false, 5, false, false);
4414 let v = VideoAttributes::parse(&raw);
4415 assert_eq!(v.aspect_ratio, VideoAspectRatio::Reserved(1));
4416 assert_eq!(v.resolution, VideoResolution::Reserved(5));
4417 assert_eq!(v.resolution.dimensions(VideoStandard::Ntsc), None);
4418 }
4419
4420 #[test]
4421 fn audio_attributes_ac3_stereo_english() {
4422 let raw = pack_audio_attr(0, false, 1, 0, 0, 0, 1, *b"en", 0, 0);
4425 let a = AudioAttributes::parse(&raw);
4426 assert_eq!(a.coding_mode, AudioCodingMode::Ac3);
4427 assert!(!a.multichannel_extension_present);
4428 assert_eq!(a.language_type, AudioLanguageType::Iso639);
4429 assert_eq!(a.application_mode, AudioApplicationMode::Unspecified);
4430 assert_eq!(a.channel_count, 2);
4431 assert_eq!(a.sample_rate_hz(), Some(48_000));
4432 assert_eq!(&a.language_code, b"en");
4433 assert!(!a.dolby_surround_suitable());
4434 }
4435
4436 #[test]
4437 fn audio_attributes_lpcm_24bit_six_channel() {
4438 let raw = pack_audio_attr(4, false, 0, 0, 2, 0, 5, [0, 0], 0, 0);
4440 let a = AudioAttributes::parse(&raw);
4441 assert_eq!(a.coding_mode, AudioCodingMode::Lpcm);
4442 assert_eq!(a.quantization, AudioQuantizationDrc::Lpcm24);
4443 assert_eq!(a.channel_count, 6);
4444 }
4445
4446 #[test]
4447 fn audio_attributes_mpeg2_drc_flag() {
4448 let raw = pack_audio_attr(3, false, 0, 0, 1, 0, 1, [0, 0], 0, 0);
4449 let a = AudioAttributes::parse(&raw);
4450 assert_eq!(a.coding_mode, AudioCodingMode::Mpeg2Ext);
4451 assert_eq!(a.quantization, AudioQuantizationDrc::Drc);
4452 }
4453
4454 #[test]
4455 fn audio_attributes_surround_dolby_suitable_bit() {
4456 let raw = pack_audio_attr(0, false, 0, 2, 0, 0, 1, [0, 0], 0, 0b0000_1000);
4458 let a = AudioAttributes::parse(&raw);
4459 assert_eq!(a.application_mode, AudioApplicationMode::Surround);
4460 assert!(a.dolby_surround_suitable());
4461 }
4462
4463 #[test]
4464 fn audio_attributes_karaoke_channel_assignment_3_0() {
4465 let raw = pack_audio_attr(0, true, 0, 1, 0, 0, 2, [0, 0], 0, 0b0011_0010);
4467 let a = AudioAttributes::parse(&raw);
4468 assert_eq!(a.application_mode, AudioApplicationMode::Karaoke);
4469 assert!(a.multichannel_extension_present);
4470 assert_eq!(a.karaoke_channel_assignment(), Some(3));
4471 assert_eq!(a.karaoke_mc_intro_present(), Some(true));
4472 assert_eq!(a.karaoke_duet(), Some(false));
4473 }
4474
4475 #[test]
4476 fn subpicture_attributes_2bit_rle_japanese() {
4477 let raw = pack_subp_attr(0, 1, *b"ja", 0);
4478 let s = SubpictureAttributes::parse(&raw);
4479 assert_eq!(s.coding_mode, SubpictureCodingMode::Rle2Bit);
4480 assert_eq!(s.language_type, SubpictureLanguageType::Iso639);
4481 assert_eq!(&s.language_code, b"ja");
4482 }
4483
4484 #[test]
4485 fn mc_extension_entry_decodes_per_channel_flags() {
4486 let raw = [
4487 0b0000_0001, 0b0000_0000,
4489 0b0000_1010, 0b0000_0101, 0b0000_1001, 0,
4493 0,
4494 0,
4495 ];
4496 let m = McExtensionEntry::parse(&raw);
4497 assert!(m.ach0_guide_melody);
4498 assert!(!m.ach1_guide_melody);
4499 assert!(m.ach2_guide_vocal_1);
4500 assert!(m.ach2_guide_melody_1);
4501 assert!(m.ach3_guide_vocal_2);
4502 assert!(m.ach3_sound_effect_a);
4503 assert!(m.ach4_guide_vocal_1);
4504 assert!(m.ach4_sound_effect_b);
4505 assert!(!m.ach4_guide_melody_b);
4506 }
4507
4508 fn build_vtsi_mat_with_attrs() -> Vec<u8> {
4512 let mut b = vec![0u8; 0x03D8];
4513 b[0..12].copy_from_slice(VTS_MAGIC);
4514 b[0x0080..0x0084].copy_from_slice(&(0x03D7u32).to_be_bytes());
4515 let v_menu = pack_video_attr(1, 1, 0, false, false, false, false, 0, false, false);
4520 b[0x0100..0x0102].copy_from_slice(&v_menu);
4521 b[0x0102..0x0104].copy_from_slice(&1u16.to_be_bytes());
4522 let a_menu = pack_audio_attr(2, false, 1, 0, 0, 0, 1, *b"en", 1, 0);
4523 b[0x0104..0x010C].copy_from_slice(&a_menu);
4524 b[0x0154..0x0156].copy_from_slice(&1u16.to_be_bytes());
4525 let s_menu = pack_subp_attr(0, 1, *b"ja", 0);
4526 b[0x0156..0x015C].copy_from_slice(&s_menu);
4527
4528 let v_title = pack_video_attr(1, 0, 3, false, false, false, false, 0, false, false);
4532 b[0x0200..0x0202].copy_from_slice(&v_title);
4533 b[0x0202..0x0204].copy_from_slice(&2u16.to_be_bytes());
4534 let a0 = pack_audio_attr(0, false, 1, 0, 0, 0, 5, *b"en", 1, 0);
4535 let a1 = pack_audio_attr(0, false, 1, 0, 0, 0, 1, *b"fr", 1, 0);
4536 b[0x0204..0x020C].copy_from_slice(&a0);
4537 b[0x020C..0x0214].copy_from_slice(&a1);
4538 b[0x0254..0x0256].copy_from_slice(&2u16.to_be_bytes());
4539 let s0 = pack_subp_attr(0, 1, *b"en", 0);
4540 let s1 = pack_subp_attr(0, 1, *b"de", 0);
4541 b[0x0256..0x025C].copy_from_slice(&s0);
4542 b[0x025C..0x0262].copy_from_slice(&s1);
4543
4544 b
4546 }
4547
4548 #[test]
4549 fn vtsi_mat_decodes_menu_and_title_attribute_blocks() {
4550 let buf = build_vtsi_mat_with_attrs();
4551 let mat = VtsiMat::parse(&buf).unwrap();
4552
4553 let menu_v = mat.menu_attributes.video.unwrap();
4554 assert_eq!(menu_v.standard, VideoStandard::Pal);
4555 assert_eq!(menu_v.aspect_ratio, VideoAspectRatio::Ratio4x3);
4556 assert_eq!(mat.menu_attributes.audio_streams.len(), 1);
4557 assert_eq!(
4558 mat.menu_attributes.audio_streams[0].coding_mode,
4559 AudioCodingMode::Mpeg1
4560 );
4561 assert_eq!(mat.menu_attributes.subpicture_streams.len(), 1);
4562 assert_eq!(
4563 &mat.menu_attributes.subpicture_streams[0].language_code,
4564 b"ja"
4565 );
4566
4567 let title_v = mat.title_attributes.video.unwrap();
4568 assert_eq!(title_v.standard, VideoStandard::Ntsc);
4569 assert_eq!(title_v.aspect_ratio, VideoAspectRatio::Ratio16x9);
4570 assert_eq!(mat.title_attributes.audio_streams.len(), 2);
4571 assert_eq!(mat.title_attributes.audio_streams[0].channel_count, 6);
4572 assert_eq!(&mat.title_attributes.audio_streams[1].language_code, b"fr");
4573 assert_eq!(mat.title_attributes.subpicture_streams.len(), 2);
4574 assert_eq!(
4575 &mat.title_attributes.subpicture_streams[1].language_code,
4576 b"de"
4577 );
4578 assert_eq!(mat.title_attributes.multichannel_extension.len(), 24);
4579 assert!(!mat.title_attributes.multichannel_extension[0].ach0_guide_melody);
4580 }
4581
4582 #[test]
4583 fn vtsi_mat_short_buffer_leaves_attributes_partial() {
4584 let buf = build_vtsi_mat(1, 2, 3, 42);
4587 let mat = VtsiMat::parse(&buf).unwrap();
4588 let menu_v = mat.menu_attributes.video.unwrap();
4591 assert_eq!(menu_v.coding_mode, VideoCodingMode::Mpeg1);
4592 assert!(mat.title_attributes.video.is_none());
4594 assert!(mat.title_attributes.audio_streams.is_empty());
4595 assert!(mat.title_attributes.multichannel_extension.is_empty());
4596 }
4597
4598 fn build_vts_atrt(entries: &[(u32, Vec<u8>)]) -> Vec<u8> {
4605 let header = 8 + 4 * entries.len();
4607 let mut offsets = Vec::with_capacity(entries.len());
4609 let mut bodies = Vec::with_capacity(entries.len());
4610 let mut cursor = header;
4611 for (cat, blob) in entries {
4612 offsets.push(cursor as u32);
4613 let entry_total = 8 + blob.len();
4614 let entry_ea = (entry_total - 1) as u32;
4615 let mut e = Vec::with_capacity(entry_total);
4616 e.extend_from_slice(&entry_ea.to_be_bytes());
4617 e.extend_from_slice(&cat.to_be_bytes());
4618 e.extend_from_slice(blob);
4619 bodies.push(e);
4620 cursor += entry_total;
4621 }
4622 let total = cursor;
4623 let mut out = vec![0u8; total];
4624 out[0..2].copy_from_slice(&(entries.len() as u16).to_be_bytes());
4625 out[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
4627 for (i, off) in offsets.iter().enumerate() {
4628 out[8 + i * 4..8 + (i + 1) * 4].copy_from_slice(&off.to_be_bytes());
4629 }
4630 for (i, body) in bodies.iter().enumerate() {
4631 let start = offsets[i] as usize;
4632 out[start..start + body.len()].copy_from_slice(body);
4633 }
4634 out
4635 }
4636
4637 #[test]
4638 fn vts_atrt_walks_two_entries() {
4639 let blob_a = (0..0x300u32).map(|i| (i & 0xFF) as u8).collect::<Vec<_>>();
4640 let blob_b = vec![0x55u8; 0x300];
4641 let buf = build_vts_atrt(&[(0, blob_a.clone()), (1, blob_b.clone())]);
4642 let atrt = VmgVtsAtrt::parse(&buf).unwrap();
4643 assert_eq!(atrt.number_of_title_sets, 2);
4644 assert_eq!(atrt.entries.len(), 2);
4645
4646 let e1 = atrt.entry(1).unwrap();
4647 assert_eq!(e1.vts_number, 1);
4648 assert_eq!(e1.vts_category, 0);
4649 assert_eq!(e1.attributes_blob, blob_a);
4650
4651 let e2 = atrt.entry(2).unwrap();
4652 assert_eq!(e2.vts_number, 2);
4653 assert_eq!(e2.vts_category, 1); assert_eq!(e2.attributes_blob, blob_b);
4655
4656 assert!(atrt.entry(0).is_none());
4657 assert!(atrt.entry(3).is_none());
4658 }
4659
4660 #[test]
4661 fn vts_atrt_rejects_short_header() {
4662 let buf = vec![0u8; 4];
4663 assert!(VmgVtsAtrt::parse(&buf).is_err());
4664 }
4665
4666 #[test]
4667 fn vts_atrt_rejects_offset_list_past_end() {
4668 let mut buf = vec![0u8; 8];
4669 buf[0..2].copy_from_slice(&5u16.to_be_bytes()); assert!(VmgVtsAtrt::parse(&buf).is_err());
4672 }
4673
4674 #[test]
4675 fn vts_atrt_rejects_entry_ea_overlapping_next_entry() {
4676 let blob = vec![0xAAu8; 0x100];
4679 let mut buf = build_vts_atrt(&[(0, blob.clone()), (0, blob.clone())]);
4680 let e1_off = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
4684 buf[e1_off..e1_off + 4].copy_from_slice(&0xFFFFu32.to_be_bytes());
4685 assert!(VmgVtsAtrt::parse(&buf).is_err());
4686 }
4687
4688 fn build_ptl_mait(
4697 country_codes: &[u16],
4698 nts: u16,
4699 masks_per_country: &[[Vec<u16>; 8]],
4700 ) -> Vec<u8> {
4701 assert_eq!(country_codes.len(), masks_per_country.len());
4702 let header_len = 8 + 8 * country_codes.len();
4703 let level_block_bytes = (usize::from(nts) + 1) * 2;
4704 let body_bytes = level_block_bytes * 8;
4705 let total = header_len + body_bytes * country_codes.len();
4706
4707 let mut buf = vec![0u8; total];
4708 buf[0..2].copy_from_slice(&(country_codes.len() as u16).to_be_bytes());
4709 buf[2..4].copy_from_slice(&nts.to_be_bytes());
4710 buf[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
4711
4712 for (i, (&cc, masks)) in country_codes
4713 .iter()
4714 .zip(masks_per_country.iter())
4715 .enumerate()
4716 {
4717 let body_offset = header_len + i * body_bytes;
4718 let entry_base = 8 + i * 8;
4720 buf[entry_base..entry_base + 2].copy_from_slice(&cc.to_be_bytes());
4721 buf[entry_base + 4..entry_base + 6]
4723 .copy_from_slice(&(body_offset as u16).to_be_bytes());
4724 for storage_idx in 0..8usize {
4727 let level_value = 8 - storage_idx; let row = &masks[level_value - 1]; assert_eq!(row.len(), usize::from(nts) + 1);
4730 let block_start = body_offset + storage_idx * level_block_bytes;
4731 for (slot, &m) in row.iter().enumerate() {
4732 buf[block_start + slot * 2..block_start + slot * 2 + 2]
4733 .copy_from_slice(&m.to_be_bytes());
4734 }
4735 }
4736 }
4737 buf
4738 }
4739
4740 #[test]
4741 fn ptl_mait_walks_two_countries() {
4742 let mut country_a_masks: [Vec<u16>; 8] = Default::default();
4744 let mut country_b_masks: [Vec<u16>; 8] = Default::default();
4745 for lvl_idx in 0..8 {
4746 let lvl = (lvl_idx + 1) as u16;
4748 country_a_masks[lvl_idx] = vec![lvl, lvl + 0x10, lvl + 0x20];
4749 country_b_masks[lvl_idx] =
4750 vec![0xF000 | lvl, 0xF000 | (lvl + 0x10), 0xF000 | (lvl + 0x20)];
4751 }
4752 let buf = build_ptl_mait(
4754 &[0x7553, 0x6A70],
4755 2,
4756 &[country_a_masks.clone(), country_b_masks.clone()],
4757 );
4758
4759 let pm = VmgPtlMait::parse(&buf).unwrap();
4760 assert_eq!(pm.number_of_countries, 2);
4761 assert_eq!(pm.number_of_title_sets, 2);
4762 assert_eq!(pm.entries.len(), 2);
4763
4764 let us = pm.country(0x7553).expect("US country present");
4765 assert_eq!(us.mask(1, 0), Some(1));
4767 assert_eq!(us.mask(1, 1), Some(0x11));
4769 assert_eq!(us.mask(1, 2), Some(0x21));
4771 assert_eq!(us.mask(8, 2), Some(0x28));
4773
4774 let jp = pm.country(0x6A70).unwrap();
4775 assert_eq!(jp.mask(1, 0), Some(0xF001));
4776 assert_eq!(jp.mask(8, 2), Some(0xF028));
4777
4778 assert!(pm.country(0x4242).is_none());
4779 assert_eq!(us.mask(0, 0), None);
4781 assert_eq!(us.mask(9, 0), None);
4782 assert_eq!(us.mask(1, 3), None);
4784 }
4785
4786 #[test]
4787 fn ptl_mait_zero_countries_decodes_empty() {
4788 let buf = build_ptl_mait(&[], 2, &[]);
4789 let pm = VmgPtlMait::parse(&buf).unwrap();
4790 assert_eq!(pm.number_of_countries, 0);
4791 assert!(pm.entries.is_empty());
4792 }
4793
4794 #[test]
4795 fn ptl_mait_rejects_short_header() {
4796 let buf = vec![0u8; 4];
4797 assert!(VmgPtlMait::parse(&buf).is_err());
4798 }
4799
4800 #[test]
4801 fn ptl_mait_rejects_country_list_past_end() {
4802 let mut buf = vec![0u8; 8];
4803 buf[0..2].copy_from_slice(&5u16.to_be_bytes()); buf[2..4].copy_from_slice(&2u16.to_be_bytes());
4805 assert!(VmgPtlMait::parse(&buf).is_err());
4807 }
4808
4809 #[test]
4810 fn ptl_mait_rejects_body_offset_past_buffer() {
4811 let mut buf = vec![0u8; 16];
4815 buf[0..2].copy_from_slice(&1u16.to_be_bytes());
4816 buf[2..4].copy_from_slice(&2u16.to_be_bytes());
4817 buf[4..8].copy_from_slice(&63u32.to_be_bytes());
4818 buf[8..10].copy_from_slice(&0x7553u16.to_be_bytes());
4819 buf[12..14].copy_from_slice(&16u16.to_be_bytes()); assert!(VmgPtlMait::parse(&buf).is_err());
4821 }
4822
4823 fn empty_pgc_body() -> Vec<u8> {
4830 vec![0u8; 0xEC]
4831 }
4832
4833 fn build_pgci_ut(units: &[(u16, u8, u8, Vec<u32>)]) -> Vec<u8> {
4838 let header_len = 8 + 8 * units.len();
4839 let lu_lens: Vec<usize> = units
4843 .iter()
4844 .map(|(_, _, _, pgcs)| 8 + pgcs.len() * 8 + pgcs.len() * 0xEC)
4845 .collect();
4846 let total: usize = header_len + lu_lens.iter().sum::<usize>();
4847 let mut buf = vec![0u8; total];
4848 buf[0..2].copy_from_slice(&(units.len() as u16).to_be_bytes());
4850 buf[4..8].copy_from_slice(&((total - 1) as u32).to_be_bytes());
4851
4852 let mut lu_offset = header_len;
4853 for (i, ((lang, ext, exist, pgcs), &lu_len)) in units.iter().zip(lu_lens.iter()).enumerate()
4854 {
4855 let srp_base = 8 + i * 8;
4857 buf[srp_base..srp_base + 2].copy_from_slice(&lang.to_be_bytes());
4858 buf[srp_base + 2] = *ext;
4859 buf[srp_base + 3] = *exist;
4860 buf[srp_base + 4..srp_base + 8].copy_from_slice(&(lu_offset as u32).to_be_bytes());
4861
4862 buf[lu_offset..lu_offset + 2].copy_from_slice(&(pgcs.len() as u16).to_be_bytes());
4864 buf[lu_offset + 4..lu_offset + 8].copy_from_slice(&((lu_len - 1) as u32).to_be_bytes());
4865
4866 let inner_srp_end = 8 + pgcs.len() * 8;
4868 for (j, &cat) in pgcs.iter().enumerate() {
4869 let inner_base = lu_offset + 8 + j * 8;
4870 buf[inner_base..inner_base + 4].copy_from_slice(&cat.to_be_bytes());
4871 let pgc_offset_in_lu = (inner_srp_end + j * 0xEC) as u32;
4872 buf[inner_base + 4..inner_base + 8]
4873 .copy_from_slice(&pgc_offset_in_lu.to_be_bytes());
4874
4875 let pgc_global = lu_offset + pgc_offset_in_lu as usize;
4878 let empty = empty_pgc_body();
4879 buf[pgc_global..pgc_global + 0xEC].copy_from_slice(&empty);
4880 }
4881
4882 lu_offset += lu_len;
4883 }
4884 buf
4885 }
4886
4887 #[test]
4888 fn pgci_ut_walks_two_language_units() {
4889 let units = vec![
4894 (
4895 0x656E, 0,
4897 menu_existence::ROOT | menu_existence::AUDIO,
4898 vec![0x83_00_00_00, 0x00_00_00_00],
4899 ),
4900 (
4901 0x6A70, 0,
4903 menu_existence::ROOT,
4904 vec![0x82_00_00_00, 0x00_00_00_00],
4905 ),
4906 ];
4907 let buf = build_pgci_ut(&units);
4908 let table = PgciUt::parse(&buf).unwrap();
4909
4910 assert_eq!(table.number_of_language_units, 2);
4911 assert_eq!(table.srp.len(), 2);
4912 assert_eq!(table.language_units.len(), 2);
4913
4914 let en_srp = &table.srp[0];
4916 assert_eq!(en_srp.language_code, 0x656E);
4917 assert!(en_srp.has_root_menu());
4918 assert!(en_srp.has_audio_menu());
4919 assert!(!en_srp.has_subpicture_menu());
4920 assert!(!en_srp.has_angle_menu());
4921 assert!(!en_srp.has_ptt_menu());
4922
4923 let en_lu = &table.language_units[0];
4924 assert_eq!(en_lu.number_of_pgcs, 2);
4925 assert_eq!(en_lu.srp.len(), 2);
4926 assert_eq!(en_lu.pgcs.len(), 2);
4927 assert!(en_lu.srp[0].is_entry_pgc());
4928 assert_eq!(en_lu.srp[0].menu_type(), MenuType::Root);
4929 assert!(!en_lu.srp[1].is_entry_pgc());
4930
4931 let jp_srp = &table.srp[1];
4933 assert_eq!(jp_srp.language_code, 0x6A70);
4934 let jp_lu = &table.language_units[1];
4935 assert_eq!(jp_lu.srp[0].menu_type(), MenuType::Title);
4936
4937 assert_eq!(
4939 table.language_unit(0x656E).map(|lu| lu.number_of_pgcs),
4940 Some(2)
4941 );
4942 assert!(table.language_unit(0x4242).is_none());
4943 }
4944
4945 #[test]
4946 fn pgci_ut_zero_language_units_decodes_empty() {
4947 let buf = build_pgci_ut(&[]);
4948 let table = PgciUt::parse(&buf).unwrap();
4949 assert_eq!(table.number_of_language_units, 0);
4950 assert!(table.srp.is_empty());
4951 assert!(table.language_units.is_empty());
4952 }
4953
4954 #[test]
4955 fn pgci_ut_rejects_short_header() {
4956 let buf = vec![0u8; 4];
4957 assert!(PgciUt::parse(&buf).is_err());
4958 }
4959
4960 #[test]
4961 fn pgci_ut_rejects_srp_list_past_buffer() {
4962 let mut buf = vec![0u8; 8];
4964 buf[0..2].copy_from_slice(&5u16.to_be_bytes());
4965 assert!(PgciUt::parse(&buf).is_err());
4966 }
4967
4968 #[test]
4969 fn pgci_ut_rejects_lu_offset_zero() {
4970 let mut buf = vec![0u8; 16];
4973 buf[0..2].copy_from_slice(&1u16.to_be_bytes());
4974 buf[8..10].copy_from_slice(&0x656Eu16.to_be_bytes());
4976 buf[11] = menu_existence::ROOT;
4977 assert!(PgciUt::parse(&buf).is_err());
4979 }
4980
4981 #[test]
4982 fn pgci_ut_rejects_lu_offset_past_buffer() {
4983 let mut buf = vec![0u8; 16];
4985 buf[0..2].copy_from_slice(&1u16.to_be_bytes());
4986 buf[8..10].copy_from_slice(&0x656Eu16.to_be_bytes());
4987 buf[11] = menu_existence::ROOT;
4988 buf[12..16].copy_from_slice(&64u32.to_be_bytes()); assert!(PgciUt::parse(&buf).is_err());
4990 }
4991
4992 #[test]
4993 fn pgci_lu_rejects_pgc_offset_past_buffer() {
4994 let mut buf = vec![0u8; 16];
4997 buf[0..2].copy_from_slice(&1u16.to_be_bytes());
4998 buf[12..16].copy_from_slice(&512u32.to_be_bytes());
5000 assert!(PgciLu::parse(&buf).is_err());
5001 }
5002
5003 #[test]
5004 fn pgci_lu_srp_decodes_parental_mask() {
5005 let units = vec![(0x656E, 0, menu_existence::ROOT, vec![0x83_00_00_FF_u32])];
5008 let buf = build_pgci_ut(&units);
5009 let table = PgciUt::parse(&buf).unwrap();
5010 let lu = &table.language_units[0];
5011 assert!(lu.srp[0].is_entry_pgc());
5012 assert_eq!(lu.srp[0].menu_type(), MenuType::Root);
5013 assert_eq!(lu.srp[0].parental_mask(), 0x00FF);
5014 }
5015
5016 #[test]
5017 fn menu_type_unknown_for_undefined_nibble() {
5018 assert_eq!(MenuType::from_nibble(1), MenuType::Unknown(1));
5020 assert_eq!(MenuType::from_nibble(0), MenuType::Unknown(0));
5021 assert_eq!(MenuType::from_nibble(0xF3), MenuType::Root);
5023 }
5024}