use crate::error::DiscError;
use crate::types::{
CdSubcodeType, CdToc, CdTrack, CdTrackType, DiscType, CDROM_OLD_METADATA_TAG,
CDROM_TRACK_METADATA2_TAG, CDROM_TRACK_METADATA_TAG, GDROM_OLD_METADATA_TAG,
GDROM_TRACK_METADATA_TAG,
};
use chdlady_core::{MetadataEntry, CHD_MDFLAGS_CHECKSUM};
pub fn parse_track_metadata(s: &str) -> Result<CdTrack, DiscError> {
let clean = s.trim().trim_matches('\0');
let mut track_num: Option<u32> = None;
let mut track_type: Option<CdTrackType> = None;
let mut subcode_type = CdSubcodeType::None;
let mut frames: Option<u32> = None;
let mut pad_frames: u32 = 0;
let mut pregap_frames: u32 = 0;
let mut pgtype: Option<CdTrackType> = None;
let mut pgsub: Option<CdSubcodeType> = None;
let mut postgap_frames: u32 = 0;
for token in clean.split_whitespace() {
if let Some((key, val)) = token.split_once(':') {
match key {
"TRACK" => {
track_num = Some(val.parse::<u32>().map_err(|_| {
DiscError::InvalidMetadata(format!("invalid TRACK number in '{}'", s))
})?);
}
"TYPE" => {
track_type = Some(CdTrackType::parse_cue_or_metadata(val)?);
}
"SUBTYPE" => {
subcode_type = CdSubcodeType::parse(val)?;
}
"FRAMES" => {
frames = Some(val.parse::<u32>().map_err(|_| {
DiscError::InvalidMetadata(format!("invalid FRAMES count in '{}'", s))
})?);
}
"PAD" => {
pad_frames = val.parse::<u32>().map_err(|_| {
DiscError::InvalidMetadata(format!("invalid PAD count in '{}'", s))
})?;
}
"PREGAP" => {
pregap_frames = val.parse::<u32>().map_err(|_| {
DiscError::InvalidMetadata(format!("invalid PREGAP count in '{}'", s))
})?;
}
"PGTYPE" => {
pgtype = Some(CdTrackType::parse_cue_or_metadata(val)?);
}
"PGSUB" => {
pgsub = Some(CdSubcodeType::parse(val)?);
}
"POSTGAP" => {
postgap_frames = val.parse::<u32>().map_err(|_| {
DiscError::InvalidMetadata(format!("invalid POSTGAP count in '{}'", s))
})?;
}
_ => {}
}
}
}
let track_num = track_num
.ok_or_else(|| DiscError::InvalidMetadata(format!("missing TRACK number in '{}'", s)))?;
let track_type =
track_type.ok_or_else(|| DiscError::InvalidMetadata(format!("missing TYPE in '{}'", s)))?;
let frames =
frames.ok_or_else(|| DiscError::InvalidMetadata(format!("missing FRAMES in '{}'", s)))?;
Ok(CdTrack {
track_num,
track_type,
subcode_type,
frames,
pad_frames,
pregap_frames,
pregap_type: pgtype,
pregap_subcode: pgsub,
postgap_frames,
file_name: None,
file_offset: 0,
sub_file_name: None,
sub_file_offset: 0,
})
}
pub fn format_track_metadata(track: &CdTrack, disc_type: DiscType) -> Vec<u8> {
let type_str = track.track_type.chd_metadata_type_str();
let sub_str = track.subcode_type.as_str();
let pg_type_str = track
.pregap_type
.map(|t| t.chd_metadata_type_str())
.unwrap_or(if track.track_type.is_audio() {
"VAUDIO"
} else {
type_str
});
let pg_sub_str = track.pregap_subcode.map(|s| s.as_str()).unwrap_or(sub_str);
let text = match disc_type {
DiscType::GdRom => {
format!(
"TRACK:{} TYPE:{} SUBTYPE:{} FRAMES:{} PAD:{} PREGAP:{} PGTYPE:{} PGSUB:{} POSTGAP:{}",
track.track_num,
type_str,
sub_str,
track.frames,
track.pad_frames,
track.pregap_frames,
pg_type_str,
pg_sub_str,
track.postgap_frames
)
}
DiscType::Cd => {
format!(
"TRACK:{} TYPE:{} SUBTYPE:{} FRAMES:{} PREGAP:{} PGTYPE:{} PGSUB:{} POSTGAP:{}",
track.track_num,
type_str,
sub_str,
track.frames,
track.pregap_frames,
pg_type_str,
pg_sub_str,
track.postgap_frames
)
}
};
let mut bytes = text.into_bytes();
bytes.push(0); bytes
}
pub fn parse_toc_from_metadata(entries: &[MetadataEntry]) -> Result<CdToc, DiscError> {
let mut tracks = Vec::new();
let mut disc_type = DiscType::Cd;
for entry in entries {
if entry.metatag == GDROM_TRACK_METADATA_TAG || entry.metatag == GDROM_OLD_METADATA_TAG {
disc_type = DiscType::GdRom;
let text = String::from_utf8_lossy(&entry.value);
tracks.push(parse_track_metadata(&text)?);
} else if entry.metatag == CDROM_TRACK_METADATA2_TAG
|| entry.metatag == CDROM_TRACK_METADATA_TAG
|| entry.metatag == CDROM_OLD_METADATA_TAG
{
let text = String::from_utf8_lossy(&entry.value);
tracks.push(parse_track_metadata(&text)?);
}
}
if tracks.is_empty() {
return Err(DiscError::InvalidMetadata(
"no optical disc track metadata found in CHD container".to_string(),
));
}
tracks.sort_by_key(|t| t.track_num);
Ok(CdToc { disc_type, tracks })
}
pub fn create_metadata_from_toc(toc: &CdToc) -> Vec<MetadataEntry> {
let tag = match toc.disc_type {
DiscType::Cd => CDROM_TRACK_METADATA2_TAG,
DiscType::GdRom => GDROM_TRACK_METADATA_TAG,
};
toc.tracks
.iter()
.map(|track| {
let value = format_track_metadata(track, toc.disc_type);
MetadataEntry {
metatag: tag,
flags: CHD_MDFLAGS_CHECKSUM,
offset: 0,
next: 0,
value,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_parse_cd() {
let meta = "TRACK:1 TYPE:MODE1_RAW SUBTYPE:NONE FRAMES:4 PREGAP:0 PGTYPE:MODE1 PGSUB:NONE POSTGAP:0\0";
let track = parse_track_metadata(meta).expect("parse metadata");
assert_eq!(track.track_num, 1);
assert_eq!(track.track_type, CdTrackType::Mode1Raw);
assert_eq!(track.frames, 4);
assert_eq!(track.pad_frames, 0);
assert_eq!(track.pregap_frames, 0);
let formatted = format_track_metadata(&track, DiscType::Cd);
let s = String::from_utf8_lossy(&formatted);
assert_eq!(s, meta);
}
#[test]
fn test_metadata_parse_gdrom() {
let meta = "TRACK:2 TYPE:AUDIO SUBTYPE:NONE FRAMES:44996 PAD:44992 PREGAP:0 PGTYPE:MODE1 PGSUB:NONE POSTGAP:0\0";
let track = parse_track_metadata(meta).expect("parse metadata");
assert_eq!(track.track_num, 2);
assert_eq!(track.track_type, CdTrackType::Audio);
assert_eq!(track.frames, 44996);
assert_eq!(track.pad_frames, 44992);
let formatted = format_track_metadata(&track, DiscType::GdRom);
let s = String::from_utf8_lossy(&formatted);
assert_eq!(s, meta);
}
#[test]
fn test_metadata_parse_v_prefixed_types() {
let meta = "TRACK:2 TYPE:MODE1_RAW SUBTYPE:NONE FRAMES:8634 PREGAP:231 PGTYPE:VMODE1_RAW PGSUB:NONE POSTGAP:0\0";
let track = parse_track_metadata(meta).expect("parse metadata with VMODE1_RAW");
assert_eq!(track.track_num, 2);
assert_eq!(track.track_type, CdTrackType::Mode1Raw);
assert_eq!(track.frames, 8634);
assert_eq!(track.pregap_frames, 231);
assert_eq!(track.pregap_type, Some(CdTrackType::Mode1Raw));
}
}