1mod bluray;
12mod dvd;
13mod encrypt;
14pub mod mapfile;
15
16use crate::drive::Drive;
17use crate::error::{Error, Result};
18use crate::sector::SectorReader;
19use crate::udf;
20
21use encrypt::HandshakeResult;
22
23pub use crate::labels::{LabelPurpose, LabelQualifier};
27
28#[derive(Debug)]
32pub struct Disc {
33 pub volume_id: String,
35 pub meta_title: Option<String>,
37 pub format: DiscFormat,
39 pub capacity_sectors: u32,
41 pub capacity_bytes: u64,
43 pub layers: u8,
45 pub titles: Vec<DiscTitle>,
47 pub region: DiscRegion,
49 pub aacs: Option<AacsState>,
51 pub css: Option<crate::css::CssState>,
53 pub encrypted: bool,
55 pub content_format: ContentFormat,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum ContentFormat {
62 BdTs,
64 MpegPs,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
70pub enum DiscFormat {
71 Uhd,
73 BluRay,
75 Dvd,
77 Unknown,
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub enum DiscRegion {
84 Free,
86 BluRay(Vec<BdRegion>),
88 Dvd(Vec<u8>),
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
94pub enum BdRegion {
95 A,
97 B,
99 C,
101}
102
103#[derive(Debug, Clone)]
105pub struct DiscTitle {
106 pub playlist: String,
108 pub playlist_id: u16,
110 pub duration_secs: f64,
112 pub size_bytes: u64,
114 pub clips: Vec<Clip>,
116 pub streams: Vec<Stream>,
118 pub chapters: Vec<Chapter>,
120 pub extents: Vec<Extent>,
122 pub content_format: ContentFormat,
124 pub codec_privates: Vec<Option<Vec<u8>>>,
127}
128
129#[derive(Debug, Clone)]
131pub struct Clip {
132 pub clip_id: String,
134 pub in_time: u32,
136 pub out_time: u32,
138 pub duration_secs: f64,
140 pub source_packets: u32,
142}
143
144#[derive(Debug, Clone)]
146pub enum Stream {
147 Video(VideoStream),
148 Audio(AudioStream),
149 Subtitle(SubtitleStream),
150}
151
152#[derive(Debug, Clone)]
154pub struct VideoStream {
155 pub pid: u16,
157 pub codec: Codec,
159 pub resolution: Resolution,
161 pub frame_rate: FrameRate,
163 pub hdr: HdrFormat,
165 pub color_space: ColorSpace,
167 pub secondary: bool,
169 pub label: String,
171}
172
173#[derive(Debug, Clone)]
175pub struct AudioStream {
176 pub pid: u16,
178 pub codec: Codec,
180 pub channels: AudioChannels,
182 pub language: String,
184 pub sample_rate: SampleRate,
186 pub secondary: bool,
188 pub purpose: LabelPurpose,
191 pub label: String,
194}
195
196#[derive(Debug, Clone)]
198pub struct SubtitleStream {
199 pub pid: u16,
201 pub codec: Codec,
203 pub language: String,
205 pub forced: bool,
207 pub qualifier: LabelQualifier,
210 pub codec_data: Option<Vec<u8>>,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq)]
216pub enum Codec {
217 Hevc,
219 H264,
220 Vc1,
221 Mpeg2,
222 Mpeg1,
223 Av1,
224 TrueHd,
226 DtsHdMa,
227 DtsHdHr,
228 Dts,
229 Ac3,
230 Ac3Plus,
231 Lpcm,
232 Aac,
233 Mp2,
234 Mp3,
235 Flac,
236 Opus,
237 Pgs,
239 DvdSub,
240 Srt,
241 Ssa,
242 Unknown(u8),
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum Resolution {
249 R480i,
251 R480p,
253 R576i,
255 R576p,
257 R720p,
259 R1080i,
261 R1080p,
263 R2160p,
265 R4320p,
267 Unknown,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq)]
273pub enum FrameRate {
274 F23_976,
276 F24,
278 F25,
280 F29_97,
282 F30,
284 F50,
286 F59_94,
288 F60,
290 Unknown,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub enum AudioChannels {
297 Mono,
299 Stereo,
301 Stereo21,
303 Quad,
305 Surround50,
307 Surround51,
309 Surround61,
311 Surround71,
313 Unknown,
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum SampleRate {
320 S44_1,
322 S48,
324 S96,
326 S192,
328 S48_96,
330 S48_192,
332 Unknown,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq)]
338pub enum HdrFormat {
339 Sdr,
340 Hdr10,
341 Hdr10Plus,
342 DolbyVision,
343 Hlg,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq)]
348pub enum ColorSpace {
349 Bt709,
350 Bt2020,
351 Unknown,
352}
353
354#[derive(Debug, Clone)]
356pub struct Chapter {
357 pub time_secs: f64,
359 pub name: String,
361}
362
363#[derive(Debug, Clone, Copy)]
365pub struct Extent {
366 pub start_lba: u32,
367 pub sector_count: u32,
368}
369
370impl Codec {
373 pub fn name(&self) -> &'static str {
375 for (_, name, v) in Self::ALL_CODECS {
376 if v == self {
377 return name;
378 }
379 }
380 "Unknown"
381 }
382
383 pub fn id(&self) -> &'static str {
385 for (id, _, v) in Self::ALL_CODECS {
386 if v == self {
387 return id;
388 }
389 }
390 "unknown"
391 }
392
393 const ALL_CODECS: &[(&'static str, &'static str, Codec)] = &[
394 ("hevc", "HEVC", Codec::Hevc),
395 ("h264", "H.264", Codec::H264),
396 ("vc1", "VC-1", Codec::Vc1),
397 ("mpeg2", "MPEG-2", Codec::Mpeg2),
398 ("mpeg1", "MPEG-1", Codec::Mpeg1),
399 ("av1", "AV1", Codec::Av1),
400 ("truehd", "TrueHD", Codec::TrueHd),
401 ("dtshd_ma", "DTS-HD MA", Codec::DtsHdMa),
402 ("dtshd_hr", "DTS-HD HR", Codec::DtsHdHr),
403 ("dts", "DTS", Codec::Dts),
404 ("ac3", "AC-3", Codec::Ac3),
405 ("eac3", "EAC-3", Codec::Ac3Plus),
406 ("lpcm", "LPCM", Codec::Lpcm),
407 ("aac", "AAC", Codec::Aac),
408 ("mp2", "MP2", Codec::Mp2),
409 ("mp3", "MP3", Codec::Mp3),
410 ("flac", "FLAC", Codec::Flac),
411 ("opus", "Opus", Codec::Opus),
412 ("pgs", "PGS", Codec::Pgs),
413 ("dvdsub", "DVD Subtitle", Codec::DvdSub),
414 ("srt", "SRT", Codec::Srt),
415 ("ssa", "SSA", Codec::Ssa),
416 ];
417
418 fn from_coding_type(ct: u8) -> Self {
419 match ct {
420 0x24 => Codec::Hevc,
421 0x1B => Codec::H264,
422 0xEA => Codec::Vc1,
423 0x02 => Codec::Mpeg2,
424 0x83 => Codec::TrueHd,
425 0x86 => Codec::DtsHdMa,
426 0x85 => Codec::DtsHdHr,
427 0x82 => Codec::Dts,
428 0x81 => Codec::Ac3,
429 0x84 | 0xA1 => Codec::Ac3Plus,
430 0x80 => Codec::Lpcm,
431 0xA2 => Codec::DtsHdHr,
432 0x90 | 0x91 => Codec::Pgs,
433 ct => Codec::Unknown(ct),
434 }
435 }
436}
437
438impl std::fmt::Display for Codec {
439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 f.write_str(self.name())
441 }
442}
443
444impl Resolution {
445 pub fn from_video_format(vf: u8) -> Self {
447 match vf {
448 1 => Resolution::R480i,
449 2 => Resolution::R576i,
450 3 => Resolution::R480p,
451 4 => Resolution::R1080i,
452 5 => Resolution::R720p,
453 6 => Resolution::R1080p,
454 7 => Resolution::R576p,
455 8 => Resolution::R2160p,
456 _ => Resolution::Unknown,
457 }
458 }
459
460 pub fn pixels(&self) -> (u32, u32) {
462 match self {
463 Resolution::R480i | Resolution::R480p => (720, 480),
464 Resolution::R576i | Resolution::R576p => (720, 576),
465 Resolution::R720p => (1280, 720),
466 Resolution::R1080i | Resolution::R1080p => (1920, 1080),
467 Resolution::R2160p => (3840, 2160),
468 Resolution::R4320p => (7680, 4320),
469 Resolution::Unknown => (1920, 1080),
470 }
471 }
472
473 pub fn is_uhd(&self) -> bool {
475 matches!(self, Resolution::R2160p | Resolution::R4320p)
476 }
477
478 pub fn is_hd(&self) -> bool {
480 !matches!(
481 self,
482 Resolution::R480i
483 | Resolution::R480p
484 | Resolution::R576i
485 | Resolution::R576p
486 | Resolution::Unknown
487 )
488 }
489
490 pub fn is_sd(&self) -> bool {
492 matches!(
493 self,
494 Resolution::R480i | Resolution::R480p | Resolution::R576i | Resolution::R576p
495 )
496 }
497
498 pub fn from_height(h: u32) -> Self {
500 match h {
501 0..=480 => Resolution::R480p,
502 481..=576 => Resolution::R576p,
503 577..=720 => Resolution::R720p,
504 721..=1080 => Resolution::R1080p,
505 1081..=2160 => Resolution::R2160p,
506 _ => Resolution::R4320p,
507 }
508 }
509}
510
511impl FrameRate {
514 pub fn from_video_rate(vr: u8) -> Self {
516 match vr {
517 1 => FrameRate::F23_976,
518 2 => FrameRate::F24,
519 3 => FrameRate::F25,
520 4 => FrameRate::F29_97,
521 5 => FrameRate::F30,
522 6 => FrameRate::F50,
523 7 => FrameRate::F59_94,
524 8 => FrameRate::F60,
525 _ => FrameRate::Unknown,
526 }
527 }
528
529 pub fn as_fraction(&self) -> (u32, u32) {
531 match self {
532 FrameRate::F23_976 => (24000, 1001),
533 FrameRate::F24 => (24, 1),
534 FrameRate::F25 => (25, 1),
535 FrameRate::F29_97 => (30000, 1001),
536 FrameRate::F30 => (30, 1),
537 FrameRate::F50 => (50, 1),
538 FrameRate::F59_94 => (60000, 1001),
539 FrameRate::F60 => (60, 1),
540 FrameRate::Unknown => (0, 1),
541 }
542 }
543}
544
545impl AudioChannels {
548 pub fn from_audio_format(af: u8) -> Self {
550 match af {
551 1 => AudioChannels::Mono,
552 3 => AudioChannels::Stereo,
553 6 => AudioChannels::Surround51,
554 12 => AudioChannels::Surround71,
555 _ if af > 0 => AudioChannels::Unknown,
556 _ => AudioChannels::Unknown,
557 }
558 }
559
560 pub fn count(&self) -> u8 {
562 match self {
563 AudioChannels::Mono => 1,
564 AudioChannels::Stereo => 2,
565 AudioChannels::Stereo21 => 3,
566 AudioChannels::Quad => 4,
567 AudioChannels::Surround50 => 5,
568 AudioChannels::Surround51 => 6,
569 AudioChannels::Surround61 => 7,
570 AudioChannels::Surround71 => 8,
571 AudioChannels::Unknown => 6,
572 }
573 }
574
575 pub fn from_count(n: u8) -> Self {
577 match n {
578 1 => AudioChannels::Mono,
579 2 => AudioChannels::Stereo,
580 3 => AudioChannels::Stereo21,
581 4 => AudioChannels::Quad,
582 5 => AudioChannels::Surround50,
583 6 => AudioChannels::Surround51,
584 7 => AudioChannels::Surround61,
585 8 => AudioChannels::Surround71,
586 _ => AudioChannels::Unknown,
587 }
588 }
589}
590
591impl SampleRate {
594 pub fn from_audio_rate(ar: u8) -> Self {
596 match ar {
597 1 => SampleRate::S48,
598 4 => SampleRate::S96,
599 5 => SampleRate::S192,
600 12 => SampleRate::S48_192,
601 14 => SampleRate::S48_96,
602 _ => SampleRate::Unknown,
603 }
604 }
605
606 pub fn hz(&self) -> f64 {
608 match self {
609 SampleRate::S44_1 => 44100.0,
610 SampleRate::S48 | SampleRate::S48_96 | SampleRate::S48_192 => 48000.0,
611 SampleRate::S96 => 96000.0,
612 SampleRate::S192 => 192000.0,
613 SampleRate::Unknown => 48000.0,
614 }
615 }
616
617 pub fn from_hz(hz: u32) -> Self {
619 match hz {
620 44100 => SampleRate::S44_1,
621 48000 => SampleRate::S48,
622 96000 => SampleRate::S96,
623 192000 => SampleRate::S192,
624 _ => SampleRate::Unknown,
625 }
626 }
627}
628
629impl HdrFormat {
632 pub fn name(&self) -> &'static str {
633 match self {
634 HdrFormat::Sdr => "SDR",
635 HdrFormat::Hdr10 => "HDR10",
636 HdrFormat::Hdr10Plus => "HDR10+",
637 HdrFormat::DolbyVision => "Dolby Vision",
638 HdrFormat::Hlg => "HLG",
639 }
640 }
641
642 const ALL_HDR: &[(&'static str, HdrFormat)] = &[
643 ("sdr", HdrFormat::Sdr),
644 ("hdr10", HdrFormat::Hdr10),
645 ("hdr10+", HdrFormat::Hdr10Plus),
646 ("dv", HdrFormat::DolbyVision),
647 ("hlg", HdrFormat::Hlg),
648 ];
649
650 pub fn id(&self) -> &'static str {
652 for (id, v) in Self::ALL_HDR {
653 if v == self {
654 return id;
655 }
656 }
657 "sdr"
658 }
659}
660
661impl std::fmt::Display for HdrFormat {
662 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
663 f.write_str(self.name())
664 }
665}
666
667impl ColorSpace {
668 pub fn name(&self) -> &'static str {
669 match self {
670 ColorSpace::Bt709 => "BT.709",
671 ColorSpace::Bt2020 => "BT.2020",
672 ColorSpace::Unknown => "",
673 }
674 }
675}
676
677impl std::fmt::Display for ColorSpace {
678 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679 f.write_str(self.name())
680 }
681}
682
683macro_rules! enum_str {
689 ($name:ident, $default:expr, [ $( ($s:expr, $v:expr) ),* $(,)? ]) => {
690 impl $name {
691 const ALL: &[(&'static str, $name)] = &[ $( ($s, $v), )* ];
692 }
693 impl std::fmt::Display for $name {
694 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
695 for (s, v) in $name::ALL {
696 if v == self { return f.write_str(s); }
697 }
698 f.write_str("")
699 }
700 }
701 impl std::str::FromStr for $name {
702 type Err = ();
703 fn from_str(s: &str) -> std::result::Result<Self, ()> {
704 for (k, v) in $name::ALL {
705 if *k == s { return Ok(*v); }
706 }
707 Ok($default)
708 }
709 }
710 };
711}
712
713enum_str!(
714 Resolution,
715 Resolution::Unknown,
716 [
717 ("480i", Resolution::R480i),
718 ("480p", Resolution::R480p),
719 ("576i", Resolution::R576i),
720 ("576p", Resolution::R576p),
721 ("720p", Resolution::R720p),
722 ("1080i", Resolution::R1080i),
723 ("1080p", Resolution::R1080p),
724 ("2160p", Resolution::R2160p),
725 ("4320p", Resolution::R4320p),
726 ]
727);
728
729enum_str!(
730 FrameRate,
731 FrameRate::Unknown,
732 [
733 ("23.976", FrameRate::F23_976),
734 ("24", FrameRate::F24),
735 ("25", FrameRate::F25),
736 ("29.97", FrameRate::F29_97),
737 ("30", FrameRate::F30),
738 ("50", FrameRate::F50),
739 ("59.94", FrameRate::F59_94),
740 ("60", FrameRate::F60),
741 ]
742);
743
744enum_str!(
745 AudioChannels,
746 AudioChannels::Unknown,
747 [
748 ("mono", AudioChannels::Mono),
749 ("stereo", AudioChannels::Stereo),
750 ("2.1", AudioChannels::Stereo21),
751 ("4.0", AudioChannels::Quad),
752 ("5.0", AudioChannels::Surround50),
753 ("5.1", AudioChannels::Surround51),
754 ("6.1", AudioChannels::Surround61),
755 ("7.1", AudioChannels::Surround71),
756 ]
757);
758
759enum_str!(
760 SampleRate,
761 SampleRate::Unknown,
762 [
763 ("44.1kHz", SampleRate::S44_1),
764 ("48kHz", SampleRate::S48),
765 ("96kHz", SampleRate::S96),
766 ("192kHz", SampleRate::S192),
767 ("48/96kHz", SampleRate::S48_96),
768 ("48/192kHz", SampleRate::S48_192),
769 ]
770);
771
772impl std::str::FromStr for Codec {
773 type Err = ();
774 fn from_str(s: &str) -> std::result::Result<Self, ()> {
775 for (id, _, v) in Codec::ALL_CODECS {
776 if *id == s {
777 return Ok(*v);
778 }
779 }
780 Ok(Codec::Unknown(0))
781 }
782}
783
784impl std::str::FromStr for HdrFormat {
785 type Err = ();
786 fn from_str(s: &str) -> std::result::Result<Self, ()> {
787 for (id, v) in HdrFormat::ALL_HDR {
788 if *id == s {
789 return Ok(*v);
790 }
791 }
792 for (_id, v) in HdrFormat::ALL_HDR {
794 if HdrFormat::name(v) == s {
795 return Ok(*v);
796 }
797 }
798 Ok(HdrFormat::Sdr)
799 }
800}
801
802impl DiscTitle {
803 pub fn empty() -> Self {
805 Self {
806 playlist: String::new(),
807 playlist_id: 0,
808 duration_secs: 0.0,
809 size_bytes: 0,
810 clips: Vec::new(),
811 streams: Vec::new(),
812 chapters: Vec::new(),
813 extents: Vec::new(),
814 content_format: ContentFormat::BdTs,
815 codec_privates: Vec::new(),
816 }
817 }
818
819 pub fn duration_display(&self) -> String {
821 let hrs = (self.duration_secs / 3600.0) as u32;
822 let mins = ((self.duration_secs % 3600.0) / 60.0) as u32;
823 format!("{hrs}h {mins:02}m")
824 }
825
826 pub fn size_gb(&self) -> f64 {
828 self.size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
829 }
830
831 pub fn total_sectors(&self) -> u64 {
833 self.extents.iter().map(|e| e.sector_count as u64).sum()
834 }
835}
836
837#[derive(Debug)]
841pub struct AacsState {
842 pub version: u8,
844 pub bus_encryption: bool,
846 pub mkb_version: Option<u32>,
848 pub disc_hash: String,
850 pub key_source: KeySource,
852 pub vuk: [u8; 16],
854 pub unit_keys: Vec<(u32, [u8; 16])>,
856 pub read_data_key: Option<[u8; 16]>,
858 pub volume_id: [u8; 16],
860}
861
862#[derive(Debug, Clone, Copy, PartialEq)]
864pub enum KeySource {
865 KeyDb,
867 KeyDbDerived,
869 ProcessingKey,
871 DeviceKey,
873}
874
875impl KeySource {
876 pub fn name(&self) -> &'static str {
877 match self {
878 KeySource::KeyDb => "KEYDB",
879 KeySource::KeyDbDerived => "KEYDB (derived)",
880 KeySource::ProcessingKey => "MKB + processing key",
881 KeySource::DeviceKey => "MKB + device key",
882 }
883 }
884}
885
886const KEYDB_SEARCH_PATHS: &[&str] = &[
890 ".config/aacs/KEYDB.cfg", ".config/freemkv/keydb.cfg", ];
893const KEYDB_SYSTEM_PATH: &str = "/etc/aacs/KEYDB.cfg";
894
895#[derive(Default)]
897pub struct ScanOptions {
898 pub keydb_path: Option<std::path::PathBuf>,
901}
902
903impl ScanOptions {
904 fn resolve_keydb(&self) -> Option<std::path::PathBuf> {
906 if let Some(p) = &self.keydb_path {
907 if p.exists() {
908 return Some(p.clone());
909 }
910 }
911 if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
912 for relative in KEYDB_SEARCH_PATHS {
913 let p = std::path::PathBuf::from(&home).join(relative);
914 if p.exists() {
915 return Some(p);
916 }
917 }
918 }
919 let p = std::path::PathBuf::from(KEYDB_SYSTEM_PATH);
920 if p.exists() {
921 return Some(p);
922 }
923 None
924 }
925}
926
927#[derive(Debug)]
929pub struct DiscId {
930 pub volume_id: String,
932 pub meta_title: Option<String>,
934 pub format: DiscFormat,
936 pub capacity_sectors: u32,
938 pub encrypted: bool,
940 pub layers: u8,
942}
943
944impl DiscId {
945 pub fn name(&self) -> &str {
947 self.meta_title.as_deref().unwrap_or(&self.volume_id)
948 }
949}
950
951impl Disc {
952 pub fn identify(session: &mut Drive) -> Result<DiscId> {
956 let (capacity, mut buffered, udf_fs) = Self::read_udf(session)?;
957
958 let meta_title = Self::read_meta_title(&mut buffered, &udf_fs);
959 let format = if udf_fs.find_dir("/BDMV").is_some() {
960 DiscFormat::BluRay } else if udf_fs.find_dir("/VIDEO_TS").is_some() {
962 DiscFormat::Dvd
963 } else {
964 DiscFormat::Unknown
965 };
966 let encrypted =
967 udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
968 let layers = if capacity > 24_000_000 { 2 } else { 1 };
969
970 Ok(DiscId {
971 volume_id: udf_fs.volume_id,
972 meta_title,
973 format,
974 capacity_sectors: capacity,
975 encrypted,
976 layers,
977 })
978 }
979
980 pub fn capacity_gb(&self) -> f64 {
982 self.capacity_sectors as f64 * 2048.0 / (1024.0 * 1024.0 * 1024.0)
983 }
984
985 fn read_udf(session: &mut Drive) -> Result<(u32, udf::BufferedSectorReader<'_>, udf::UdfFs)> {
988 let capacity = Self::read_capacity(session).unwrap_or(0);
989 let batch = detect_max_batch_sectors(session.device_path());
990 let mut buffered = udf::BufferedSectorReader::new(session, batch);
991 let udf_fs = udf::read_filesystem(&mut buffered)?;
992 buffered.prefetch(udf_fs.metadata_start(), udf_fs.metadata_sectors());
993 Ok((capacity, buffered, udf_fs))
994 }
995
996 pub fn scan(session: &mut Drive, opts: &ScanOptions) -> Result<Self> {
1012 let handshake = Self::do_handshake(session, opts);
1014
1015 session.set_speed(0xFFFF);
1018
1019 let (capacity, mut buffered, udf_fs) = Self::read_udf(session)?;
1021
1022 if let Ok(ranges) = udf_fs.metadata_sector_ranges(&mut buffered) {
1025 buffered.prefetch_ranges(&ranges);
1026 }
1027
1028 let mut disc = Self::scan_with(&mut buffered, capacity, handshake, opts, udf_fs)?;
1029
1030 if disc.css.is_none()
1033 && disc.content_format == ContentFormat::MpegPs
1034 && !disc.titles.is_empty()
1035 {
1036 let lba = disc.titles[0].extents.iter().find_map(|ext| {
1037 let mut buf = vec![0u8; 2048];
1038 if session
1039 .read_sectors(ext.start_lba, 1, &mut buf, true)
1040 .is_ok()
1041 && crate::css::is_scrambled(&buf)
1042 {
1043 return Some(ext.start_lba);
1044 }
1045 None
1046 });
1047
1048 if let Some(lba) = lba {
1049 if let Ok(title_key) =
1050 crate::css::auth::authenticate_and_read_title_key(session, lba)
1051 {
1052 disc.css = Some(crate::css::CssState { title_key });
1053 disc.encrypted = true;
1054 }
1055 }
1056 }
1057
1058 Ok(disc)
1059 }
1060
1061 pub fn scan_image(
1064 reader: &mut dyn SectorReader,
1065 capacity: u32,
1066 opts: &ScanOptions,
1067 ) -> Result<Self> {
1068 let udf_fs = udf::read_filesystem(reader)?;
1069 Self::scan_with(reader, capacity, None, opts, udf_fs)
1070 }
1071
1072 fn scan_with(
1074 reader: &mut dyn SectorReader,
1075 capacity: u32,
1076 handshake: Option<HandshakeResult>,
1077 opts: &ScanOptions,
1078 udf_fs: udf::UdfFs,
1079 ) -> Result<Self> {
1080 let encrypted =
1082 udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
1083
1084 let aacs = if encrypted {
1085 if let Some(keydb_path) = opts.resolve_keydb() {
1086 Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref()).ok()
1087 } else {
1088 None
1089 }
1090 } else {
1091 None
1092 };
1093
1094 let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
1096 (
1097 Self::scan_bluray_titles(reader, &udf_fs),
1098 ContentFormat::BdTs,
1099 )
1100 } else if udf_fs.find_dir("/VIDEO_TS").is_some() {
1101 (
1102 Self::scan_dvd_titles(reader, &udf_fs),
1103 ContentFormat::MpegPs,
1104 )
1105 } else {
1106 (Vec::new(), ContentFormat::BdTs)
1107 };
1108 titles.sort_by(|a, b| {
1109 b.duration_secs
1110 .partial_cmp(&a.duration_secs)
1111 .unwrap_or(std::cmp::Ordering::Equal)
1112 });
1113
1114 let meta_title = Self::read_meta_title(reader, &udf_fs);
1116 crate::labels::apply(reader, &udf_fs, &mut titles);
1117 crate::labels::fill_defaults(&mut titles);
1118
1119 let format = Self::detect_format(&titles);
1121 let layers = if capacity > 24_000_000 { 2 } else { 1 };
1122 let region = DiscRegion::Free;
1123
1124 let css = if content_format == ContentFormat::MpegPs && !titles.is_empty() {
1126 crate::css::crack_key(reader, &titles[0].extents)
1127 } else {
1128 None
1129 };
1130 let encrypted = encrypted || css.is_some();
1131
1132 Ok(Disc {
1133 volume_id: udf_fs.volume_id.clone(),
1134 meta_title,
1135 format,
1136 capacity_sectors: capacity,
1137 capacity_bytes: capacity as u64 * 2048,
1138 layers,
1139 titles,
1140 region,
1141 aacs,
1142 css,
1143 encrypted,
1144 content_format,
1145 })
1146 }
1147
1148 fn detect_format(titles: &[DiscTitle]) -> DiscFormat {
1152 for title in titles.iter().take(3) {
1153 for stream in &title.streams {
1154 if let Stream::Video(v) = stream {
1155 if v.resolution.is_uhd() {
1156 return DiscFormat::Uhd;
1157 }
1158 if v.resolution.is_hd() {
1159 return DiscFormat::BluRay;
1160 }
1161 if v.resolution.is_sd() {
1162 return DiscFormat::Dvd;
1163 }
1164 }
1165 }
1166 }
1167 DiscFormat::Unknown
1168 }
1169
1170 fn read_capacity(session: &mut Drive) -> Result<u32> {
1171 let cdb = [
1172 crate::scsi::SCSI_READ_CAPACITY,
1173 0x00,
1174 0x00,
1175 0x00,
1176 0x00,
1177 0x00,
1178 0x00,
1179 0x00,
1180 0x00,
1181 0x00,
1182 ];
1183 let mut buf = [0u8; 8];
1184 session.scsi_execute(
1185 &cdb,
1186 crate::scsi::DataDirection::FromDevice,
1187 &mut buf,
1188 5_000,
1189 )?;
1190 let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
1191 Ok(lba + 1)
1192 }
1193}
1194
1195impl Disc {
1196 pub fn decrypt_keys(&self) -> crate::decrypt::DecryptKeys {
1199 if let Some(ref aacs) = self.aacs {
1200 crate::decrypt::DecryptKeys::Aacs {
1201 unit_keys: aacs.unit_keys.clone(),
1202 read_data_key: aacs.read_data_key,
1203 }
1204 } else if let Some(ref css) = self.css {
1205 crate::decrypt::DecryptKeys::Css {
1206 title_key: css.title_key,
1207 }
1208 } else {
1209 crate::decrypt::DecryptKeys::None
1210 }
1211 }
1212
1213 pub fn copy(
1232 &self,
1233 reader: &mut dyn SectorReader,
1234 path: &std::path::Path,
1235 opts: &CopyOptions,
1236 ) -> Result<CopyResult> {
1237 use std::io::{Seek, SeekFrom, Write};
1238
1239 let total_bytes = self.capacity_sectors as u64 * 2048;
1240 let keys = if opts.decrypt {
1241 self.decrypt_keys()
1242 } else {
1243 crate::decrypt::DecryptKeys::None
1244 };
1245
1246 let mapfile_path = mapfile_path_for(path);
1248 if !opts.resume {
1249 let _ = std::fs::remove_file(&mapfile_path);
1250 }
1251 let mut map =
1252 mapfile::Mapfile::open_or_create(&mapfile_path, total_bytes, env!("CARGO_PKG_VERSION"))
1253 .map_err(|e| Error::IoError { source: e })?;
1254
1255 let file = if opts.resume
1259 && std::fs::metadata(path)
1260 .map(|m| m.len() > 0)
1261 .unwrap_or(false)
1262 {
1263 std::fs::OpenOptions::new()
1264 .write(true)
1265 .open(path)
1266 .map_err(|e| Error::IoError { source: e })?
1267 } else {
1268 let f = std::fs::File::create(path).map_err(|e| Error::IoError { source: e })?;
1269 f.set_len(total_bytes)
1270 .map_err(|e| Error::IoError { source: e })?;
1271 f
1272 };
1273
1274 let mut file = file;
1275 let batch: u16 = match opts.batch_sectors {
1276 Some(b) => b,
1277 None if opts.skip_forward => 32, None => DEFAULT_BATCH_SECTORS,
1279 };
1280
1281 let skip_init = 256 * 1024u64; let skip_max = (total_bytes / 100).max(skip_init); let mut skip_size = skip_init;
1285
1286 let mut buf = vec![0u8; batch as usize * 2048];
1287 let mut bytes_done = 0u64;
1288 let mut halt_requested = false;
1289
1290 'outer: loop {
1293 let regions_to_do = map.ranges_with(&[
1294 mapfile::SectorStatus::NonTried,
1295 mapfile::SectorStatus::NonTrimmed,
1296 mapfile::SectorStatus::NonScraped,
1297 ]);
1298 if regions_to_do.is_empty() {
1299 break;
1300 }
1301 let Some((region_pos, region_size)) = map.next_with(0, mapfile::SectorStatus::NonTried)
1305 else {
1306 break;
1307 };
1308 let region_end = region_pos + region_size;
1309 let mut pos = region_pos;
1310
1311 while pos < region_end {
1312 if let Some(ref h) = opts.halt {
1313 if h.load(std::sync::atomic::Ordering::Relaxed) {
1314 halt_requested = true;
1315 break 'outer;
1316 }
1317 }
1318 let block_bytes = (region_end - pos).min(batch as u64 * 2048);
1319 let lba = (pos / 2048) as u32;
1320 let count = (block_bytes / 2048) as u16;
1321 let bytes = count as usize * 2048;
1322
1323 let recovery = !opts.skip_on_error; let read_ok = reader
1325 .read_sectors(lba, count, &mut buf[..bytes], recovery)
1326 .is_ok();
1327
1328 if read_ok {
1329 if opts.decrypt {
1330 crate::decrypt::decrypt_sectors(&mut buf[..bytes], &keys, 0)?;
1331 }
1332 file.seek(SeekFrom::Start(pos))
1333 .map_err(|e| Error::IoError { source: e })?;
1334 file.write_all(&buf[..bytes])
1335 .map_err(|e| Error::IoError { source: e })?;
1336 map.record(pos, block_bytes, mapfile::SectorStatus::Finished)
1337 .map_err(|e| Error::IoError { source: e })?;
1338 bytes_done = bytes_done.saturating_add(block_bytes);
1339 skip_size = skip_init; pos += block_bytes;
1341 } else if opts.skip_on_error {
1342 buf[..bytes].fill(0);
1344 file.seek(SeekFrom::Start(pos))
1345 .map_err(|e| Error::IoError { source: e })?;
1346 file.write_all(&buf[..bytes])
1347 .map_err(|e| Error::IoError { source: e })?;
1348 map.record(pos, block_bytes, mapfile::SectorStatus::NonTrimmed)
1349 .map_err(|e| Error::IoError { source: e })?;
1350 pos += block_bytes;
1351
1352 if opts.skip_forward && pos < region_end {
1353 let jump = skip_size.min(region_end - pos);
1355 if jump > 0 {
1356 map.record(pos, jump, mapfile::SectorStatus::NonTrimmed)
1357 .map_err(|e| Error::IoError { source: e })?;
1358 pos += jump;
1359 }
1360 skip_size = (skip_size * 2).min(skip_max);
1361 }
1362 } else {
1363 return Err(Error::DiscRead { sector: lba as u64 });
1365 }
1366
1367 if let Some(cb) = opts.on_progress {
1368 let stats = map.stats();
1369 cb(stats.bytes_good, total_bytes);
1370 }
1371 }
1372 }
1373
1374 file.sync_all().map_err(|e| Error::IoError { source: e })?;
1375 let stats = map.stats();
1376 Ok(CopyResult {
1377 bytes_total: total_bytes,
1378 bytes_good: stats.bytes_good,
1379 bytes_unreadable: stats.bytes_unreadable,
1380 bytes_pending: stats.bytes_pending,
1381 complete: stats.bytes_pending == 0 && !halt_requested,
1382 halted: halt_requested,
1383 })
1384 }
1385}
1386
1387#[derive(Default)]
1390pub struct CopyOptions<'a> {
1391 pub decrypt: bool,
1392 pub resume: bool,
1395 pub batch_sectors: Option<u16>,
1398 pub skip_on_error: bool,
1401 pub skip_forward: bool,
1405 pub on_progress: Option<&'a dyn Fn(u64, u64)>,
1406 pub halt: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
1407}
1408
1409#[derive(Debug, Clone, Copy)]
1414pub struct CopyResult {
1415 pub bytes_total: u64,
1416 pub bytes_good: u64,
1417 pub bytes_unreadable: u64,
1418 pub bytes_pending: u64,
1419 pub complete: bool,
1420 pub halted: bool,
1421}
1422
1423pub fn mapfile_path_for(iso_path: &std::path::Path) -> std::path::PathBuf {
1425 let mut s = iso_path.as_os_str().to_os_string();
1426 s.push(".mapfile");
1427 std::path::PathBuf::from(s)
1428}
1429
1430#[derive(Default)]
1432pub struct PatchOptions<'a> {
1433 pub decrypt: bool,
1434 pub block_sectors: Option<u16>,
1436 pub full_recovery: bool,
1439 pub on_progress: Option<&'a dyn Fn(u64, u64)>,
1440 pub halt: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
1441}
1442
1443#[derive(Debug, Clone, Copy)]
1445pub struct PatchResult {
1446 pub bytes_total: u64,
1447 pub bytes_good: u64,
1448 pub bytes_unreadable: u64,
1449 pub bytes_pending: u64,
1450 pub bytes_recovered_this_pass: u64,
1451 pub halted: bool,
1452}
1453
1454impl Disc {
1455 pub fn patch(
1462 &self,
1463 reader: &mut dyn SectorReader,
1464 path: &std::path::Path,
1465 opts: &PatchOptions,
1466 ) -> Result<PatchResult> {
1467 use std::io::{Seek, SeekFrom, Write};
1468
1469 let mapfile_path = mapfile_path_for(path);
1470 let mut map =
1471 mapfile::Mapfile::load(&mapfile_path).map_err(|e| Error::IoError { source: e })?;
1472 let total_bytes = map.total_size();
1473 let keys = if opts.decrypt {
1474 self.decrypt_keys()
1475 } else {
1476 crate::decrypt::DecryptKeys::None
1477 };
1478
1479 let mut file = std::fs::OpenOptions::new()
1480 .write(true)
1481 .open(path)
1482 .map_err(|e| Error::IoError { source: e })?;
1483
1484 let block_sectors = opts.block_sectors.unwrap_or(1);
1485 let _ = opts.full_recovery;
1489
1490 let bytes_good_before = map.stats().bytes_good;
1491 let mut halted = false;
1492 let mut buf = vec![0u8; block_sectors as usize * 2048];
1493
1494 let bad_ranges = map.ranges_with(&[
1498 mapfile::SectorStatus::NonTried,
1499 mapfile::SectorStatus::NonTrimmed,
1500 mapfile::SectorStatus::NonScraped,
1501 mapfile::SectorStatus::Unreadable,
1502 ]);
1503
1504 'outer: for (range_pos, range_size) in bad_ranges {
1505 let mut pos = range_pos;
1506 let end = range_pos + range_size;
1507 while pos < end {
1508 if let Some(ref h) = opts.halt {
1509 if h.load(std::sync::atomic::Ordering::Relaxed) {
1510 halted = true;
1511 break 'outer;
1512 }
1513 }
1514 let block_bytes = (end - pos).min(block_sectors as u64 * 2048);
1515 let lba = (pos / 2048) as u32;
1516 let count = (block_bytes / 2048) as u16;
1517 let bytes = count as usize * 2048;
1518 let read_ok = reader
1519 .read_sectors(lba, count, &mut buf[..bytes], true)
1520 .is_ok();
1521 if read_ok {
1522 if opts.decrypt {
1523 crate::decrypt::decrypt_sectors(&mut buf[..bytes], &keys, 0)?;
1524 }
1525 file.seek(SeekFrom::Start(pos))
1526 .map_err(|e| Error::IoError { source: e })?;
1527 file.write_all(&buf[..bytes])
1528 .map_err(|e| Error::IoError { source: e })?;
1529 map.record(pos, block_bytes, mapfile::SectorStatus::Finished)
1530 .map_err(|e| Error::IoError { source: e })?;
1531 } else {
1532 map.record(pos, block_bytes, mapfile::SectorStatus::Unreadable)
1533 .map_err(|e| Error::IoError { source: e })?;
1534 }
1535 pos += block_bytes;
1536
1537 if let Some(cb) = opts.on_progress {
1538 let s = map.stats();
1539 cb(s.bytes_good, total_bytes);
1540 }
1541 }
1542 }
1543
1544 file.sync_all().map_err(|e| Error::IoError { source: e })?;
1545 let stats = map.stats();
1546 Ok(PatchResult {
1547 bytes_total: total_bytes,
1548 bytes_good: stats.bytes_good,
1549 bytes_unreadable: stats.bytes_unreadable,
1550 bytes_pending: stats.bytes_pending,
1551 bytes_recovered_this_pass: stats.bytes_good.saturating_sub(bytes_good_before),
1552 halted,
1553 })
1554 }
1555}
1556
1557const MAX_BATCH_SECTORS: u16 = 510;
1558const DEFAULT_BATCH_SECTORS: u16 = 60;
1559const MIN_BATCH_SECTORS: u16 = 3;
1560
1561pub fn detect_max_batch_sectors(device_path: &str) -> u16 {
1566 let dev_name = device_path.rsplit('/').next().unwrap_or("");
1567 if dev_name.is_empty() {
1568 return DEFAULT_BATCH_SECTORS;
1569 }
1570
1571 let block_name = if dev_name.starts_with("sg") {
1573 let block_dir = format!("/sys/class/scsi_generic/{dev_name}/device/block");
1574 std::fs::read_dir(&block_dir)
1575 .ok()
1576 .and_then(|mut entries| entries.next())
1577 .and_then(|e| e.ok())
1578 .map(|e| e.file_name().to_string_lossy().to_string())
1579 } else {
1580 Some(dev_name.to_string())
1581 };
1582
1583 if let Some(bname) = block_name {
1584 let sysfs_path = format!("/sys/block/{bname}/queue/max_hw_sectors_kb");
1585 if let Ok(content) = std::fs::read_to_string(&sysfs_path) {
1586 if let Ok(kb) = content.trim().parse::<u32>() {
1587 let sectors = (kb / 2) as u16;
1589 let aligned = (sectors / 3) * 3;
1591 if aligned >= MIN_BATCH_SECTORS {
1592 return aligned.min(MAX_BATCH_SECTORS);
1593 }
1594 }
1595 }
1596 }
1597 DEFAULT_BATCH_SECTORS
1599}
1600
1601#[cfg(test)]
1606mod tests {
1607 use super::*;
1608
1609 fn title_with_video(codec: Codec, resolution: Resolution) -> DiscTitle {
1611 DiscTitle {
1612 playlist: "00800.mpls".into(),
1613 playlist_id: 800,
1614 duration_secs: 7200.0,
1615 size_bytes: 0,
1616 clips: Vec::new(),
1617 streams: vec![Stream::Video(VideoStream {
1618 pid: 0x1011,
1619 codec,
1620 resolution,
1621 frame_rate: FrameRate::F23_976,
1622 hdr: HdrFormat::Sdr,
1623 color_space: ColorSpace::Bt709,
1624 secondary: false,
1625 label: String::new(),
1626 })],
1627 chapters: Vec::new(),
1628 extents: Vec::new(),
1629 content_format: ContentFormat::BdTs,
1630 codec_privates: Vec::new(),
1631 }
1632 }
1633
1634 #[test]
1635 fn detect_format_uhd() {
1636 let titles = vec![title_with_video(Codec::Hevc, Resolution::R2160p)];
1637 assert_eq!(Disc::detect_format(&titles), DiscFormat::Uhd);
1638 }
1639
1640 #[test]
1641 fn detect_format_bluray() {
1642 let titles = vec![title_with_video(Codec::H264, Resolution::R1080p)];
1643 assert_eq!(Disc::detect_format(&titles), DiscFormat::BluRay);
1644 }
1645
1646 #[test]
1647 fn detect_format_dvd() {
1648 let titles = vec![title_with_video(Codec::Mpeg2, Resolution::R480i)];
1649 assert_eq!(Disc::detect_format(&titles), DiscFormat::Dvd);
1650 }
1651
1652 #[test]
1653 fn detect_format_empty() {
1654 let titles: Vec<DiscTitle> = Vec::new();
1655 assert_eq!(Disc::detect_format(&titles), DiscFormat::Unknown);
1656 }
1657
1658 #[test]
1659 fn content_format_default_bdts() {
1660 let t = title_with_video(Codec::H264, Resolution::R1080p);
1661 assert_eq!(t.content_format, ContentFormat::BdTs);
1662 }
1663
1664 #[test]
1665 fn content_format_dvd_mpegps() {
1666 let t = DiscTitle {
1667 content_format: ContentFormat::MpegPs,
1668 ..title_with_video(Codec::Mpeg2, Resolution::R480i)
1669 };
1670 assert_eq!(t.content_format, ContentFormat::MpegPs);
1671 }
1672
1673 #[test]
1674 fn disc_capacity_gb() {
1675 let disc = Disc {
1677 volume_id: String::new(),
1678 meta_title: None,
1679 format: DiscFormat::BluRay,
1680 capacity_sectors: 12_219_392,
1681 capacity_bytes: 12_219_392u64 * 2048,
1682 layers: 1,
1683 titles: Vec::new(),
1684 region: DiscRegion::Free,
1685 aacs: None,
1686 css: None,
1687 encrypted: false,
1688 content_format: ContentFormat::BdTs,
1689 };
1690 let gb = disc.capacity_gb();
1691 assert!((gb - 23.3).abs() < 0.1, "expected ~23.3 GB, got {}", gb);
1693
1694 let disc_zero = Disc {
1696 capacity_sectors: 0,
1697 capacity_bytes: 0,
1698 ..disc
1699 };
1700 assert_eq!(disc_zero.capacity_gb(), 0.0);
1701 }
1702
1703 #[test]
1704 fn disc_title_duration_display_edge_cases() {
1705 let mut t = DiscTitle::empty();
1706
1707 t.duration_secs = 0.0;
1709 assert_eq!(t.duration_display(), "0h 00m");
1710
1711 t.duration_secs = 1.0;
1713 assert_eq!(t.duration_display(), "0h 00m");
1714
1715 t.duration_secs = 59.0 * 60.0;
1717 assert_eq!(t.duration_display(), "0h 59m");
1718
1719 t.duration_secs = 24.0 * 3600.0;
1721 assert_eq!(t.duration_display(), "24h 00m");
1722 }
1723}