use crate::error::DiscError;
use std::fmt;
pub const CD_FRAME_SIZE: usize = 2352;
pub const CD_SUBCODE_SIZE: usize = 96;
pub const CD_TOTAL_FRAME_SIZE: usize = CD_FRAME_SIZE + CD_SUBCODE_SIZE;
pub const CD_FRAMES_PER_SECOND: u32 = 75;
pub const CDROM_TRACK_METADATA2_TAG: u32 = u32::from_be_bytes(*b"CHT2");
pub const CDROM_TRACK_METADATA_TAG: u32 = u32::from_be_bytes(*b"CHTR");
pub const CDROM_OLD_METADATA_TAG: u32 = u32::from_be_bytes(*b"CHCD");
pub const GDROM_TRACK_METADATA_TAG: u32 = u32::from_be_bytes(*b"CHGT");
pub const GDROM_OLD_METADATA_TAG: u32 = u32::from_be_bytes(*b"CHGD");
pub const HARD_DISK_METADATA_TAG: u32 = u32::from_be_bytes(*b"GDDD");
pub const HARD_DISK_IDENT_METADATA_TAG: u32 = u32::from_be_bytes(*b"IDNT");
pub const DVD_METADATA_TAG: u32 = u32::from_be_bytes(*b"DVD ");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DiscType {
#[default]
Cd,
GdRom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CdTrackType {
Mode1,
Mode1Raw,
Mode2,
Mode2Form1,
Mode2Form2,
Mode2FormMix,
Mode2Raw,
Audio,
}
impl CdTrackType {
pub fn sector_size(&self) -> usize {
match self {
Self::Mode1 => 2048,
Self::Mode1Raw => 2352,
Self::Mode2 => 2336,
Self::Mode2Form1 => 2048,
Self::Mode2Form2 => 2324,
Self::Mode2FormMix => 2336,
Self::Mode2Raw => 2352,
Self::Audio => 2352,
}
}
pub fn chd_metadata_type_str(&self) -> &'static str {
match self {
Self::Mode1 => "MODE1",
Self::Mode1Raw => "MODE1_RAW",
Self::Mode2 => "MODE2",
Self::Mode2Form1 => "MODE2_FORM1",
Self::Mode2Form2 => "MODE2_FORM2",
Self::Mode2FormMix => "MODE2_FORM_MIX",
Self::Mode2Raw => "MODE2_RAW",
Self::Audio => "AUDIO",
}
}
pub fn cue_mode_str(&self) -> &'static str {
match self {
Self::Mode1 => "MODE1/2048",
Self::Mode1Raw => "MODE1/2352",
Self::Mode2 => "MODE2/2336",
Self::Mode2Form1 => "MODE2/2048",
Self::Mode2Form2 => "MODE2/2324",
Self::Mode2FormMix => "MODE2/2336",
Self::Mode2Raw => "MODE2/2352",
Self::Audio => "AUDIO",
}
}
pub fn is_audio(&self) -> bool {
matches!(self, Self::Audio)
}
pub fn parse_cue_or_metadata(s: &str) -> Result<Self, DiscError> {
let trimmed = s.trim();
match trimmed {
"MODE1" | "VMODE1" | "MODE1/2048" => Ok(Self::Mode1),
"MODE1_RAW" | "VMODE1_RAW" | "MODE1/2352" => Ok(Self::Mode1Raw),
"MODE2" | "VMODE2" | "MODE2/2336" => Ok(Self::Mode2),
"MODE2_FORM1" | "VMODE2_FORM1" | "MODE2/2048" => Ok(Self::Mode2Form1),
"MODE2_FORM2" | "VMODE2_FORM2" | "/2324" | "MODE2/2324" => Ok(Self::Mode2Form2),
"MODE2_FORM_MIX" | "VMODE2_FORM_MIX" => Ok(Self::Mode2FormMix),
"MODE2_RAW" | "VMODE2_RAW" | "MODE2/2352" | "CDI/2352" => Ok(Self::Mode2Raw),
"AUDIO" | "VAUDIO" => Ok(Self::Audio),
other => Err(DiscError::UnsupportedTrackType(other.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CdSubcodeType {
#[default]
None,
Rw,
RwRaw,
}
impl CdSubcodeType {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "NONE",
Self::Rw => "RW",
Self::RwRaw => "RW_RAW",
}
}
pub fn parse(s: &str) -> Result<Self, DiscError> {
match s.trim() {
"NONE" => Ok(Self::None),
"RW" => Ok(Self::Rw),
"RW_RAW" => Ok(Self::RwRaw),
other => Err(DiscError::InvalidMetadata(format!(
"unknown subcode type: {}",
other
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Msf {
pub m: u8,
pub s: u8,
pub f: u8,
}
impl Msf {
pub fn new(m: u8, s: u8, f: u8) -> Self {
Self { m, s, f }
}
pub fn from_lba(lba: u32) -> Self {
let f = (lba % CD_FRAMES_PER_SECOND) as u8;
let total_s = lba / CD_FRAMES_PER_SECOND;
let s = (total_s % 60) as u8;
let m = (total_s / 60) as u8;
Self { m, s, f }
}
pub fn to_lba(&self) -> u32 {
((self.m as u32 * 60) + self.s as u32) * CD_FRAMES_PER_SECOND + self.f as u32
}
pub fn parse(s: &str) -> Result<Self, DiscError> {
let parts: Vec<&str> = s.trim().split(':').collect();
if parts.len() != 3 {
return Err(DiscError::InvalidCue(format!(
"invalid MSF format: '{}'",
s
)));
}
let m = parts[0]
.parse::<u8>()
.map_err(|_| DiscError::InvalidCue(format!("invalid minute in MSF: '{}'", s)))?;
let s_val = parts[1]
.parse::<u8>()
.map_err(|_| DiscError::InvalidCue(format!("invalid second in MSF: '{}'", s)))?;
let f = parts[2]
.parse::<u8>()
.map_err(|_| DiscError::InvalidCue(format!("invalid frame in MSF: '{}'", s)))?;
Ok(Self { m, s: s_val, f })
}
}
impl fmt::Display for Msf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}:{:02}:{:02}", self.m, self.s, self.f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CdTrack {
pub track_num: u32,
pub track_type: CdTrackType,
pub subcode_type: CdSubcodeType,
pub frames: u32,
pub pad_frames: u32,
pub pregap_frames: u32,
pub pregap_type: Option<CdTrackType>,
pub pregap_subcode: Option<CdSubcodeType>,
pub postgap_frames: u32,
pub file_name: Option<String>,
pub file_offset: u64,
pub sub_file_name: Option<String>,
pub sub_file_offset: u64,
}
impl CdTrack {
pub fn new(track_num: u32, track_type: CdTrackType, frames: u32) -> Self {
Self {
track_num,
track_type,
subcode_type: CdSubcodeType::None,
frames,
pad_frames: 0,
pregap_frames: 0,
pregap_type: None,
pregap_subcode: None,
postgap_frames: 0,
file_name: None,
file_offset: 0,
sub_file_name: None,
sub_file_offset: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CdToc {
pub disc_type: DiscType,
pub tracks: Vec<CdTrack>,
}
impl CdToc {
pub fn new(disc_type: DiscType) -> Self {
Self {
disc_type,
tracks: Vec::new(),
}
}
pub fn total_frames(&self) -> u32 {
self.tracks.iter().map(|t| t.frames).sum()
}
pub fn total_logical_bytes(&self) -> u64 {
self.total_frames() as u64 * CD_TOTAL_FRAME_SIZE as u64
}
pub fn find_track(&self, track_num: u32) -> Option<&CdTrack> {
self.tracks.iter().find(|t| t.track_num == track_num)
}
}
pub fn validate_descriptor_path(path_str: &str) -> Result<(), DiscError> {
let p = std::path::Path::new(path_str);
if p.is_absolute()
|| p.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(DiscError::InvalidMetadata(format!(
"path traversal or absolute path not allowed in descriptor: '{}'",
path_str
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cue_or_metadata_standard_and_v_prefixed() {
let cases = [
("MODE1", CdTrackType::Mode1),
("VMODE1", CdTrackType::Mode1),
("MODE1/2048", CdTrackType::Mode1),
("MODE1_RAW", CdTrackType::Mode1Raw),
("VMODE1_RAW", CdTrackType::Mode1Raw),
("MODE1/2352", CdTrackType::Mode1Raw),
("MODE2", CdTrackType::Mode2),
("VMODE2", CdTrackType::Mode2),
("MODE2/2336", CdTrackType::Mode2),
("MODE2_FORM1", CdTrackType::Mode2Form1),
("VMODE2_FORM1", CdTrackType::Mode2Form1),
("MODE2/2048", CdTrackType::Mode2Form1),
("MODE2_FORM2", CdTrackType::Mode2Form2),
("VMODE2_FORM2", CdTrackType::Mode2Form2),
("/2324", CdTrackType::Mode2Form2),
("MODE2/2324", CdTrackType::Mode2Form2),
("MODE2_FORM_MIX", CdTrackType::Mode2FormMix),
("VMODE2_FORM_MIX", CdTrackType::Mode2FormMix),
("MODE2_RAW", CdTrackType::Mode2Raw),
("VMODE2_RAW", CdTrackType::Mode2Raw),
("MODE2/2352", CdTrackType::Mode2Raw),
("CDI/2352", CdTrackType::Mode2Raw),
("AUDIO", CdTrackType::Audio),
("VAUDIO", CdTrackType::Audio),
];
for (input, expected) in cases {
assert_eq!(
CdTrackType::parse_cue_or_metadata(input).expect("parse track type"),
expected,
"failed for input: {}",
input
);
}
assert!(matches!(
CdTrackType::parse_cue_or_metadata("UNKNOWN_TYPE"),
Err(DiscError::UnsupportedTrackType(_))
));
}
}