use crate::error::DiscError;
use crate::types::{CdToc, CdTrack, CdTrackType, DiscType};
use std::path::Path;
#[derive(Debug, Default)]
pub struct GdiSheet;
impl GdiSheet {
pub fn parse(content: &str, parent_dir: Option<&Path>) -> Result<CdToc, DiscError> {
let lines: Vec<&str> = content
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect();
if lines.is_empty() {
return Err(DiscError::InvalidGdi("empty GDI file".to_string()));
}
let track_count: usize = lines[0].parse().map_err(|_| {
DiscError::InvalidGdi(format!("invalid track count header: '{}'", lines[0]))
})?;
if lines.len() - 1 < track_count {
return Err(DiscError::InvalidGdi(format!(
"expected {} tracks in GDI, found {}",
track_count,
lines.len() - 1
)));
}
struct RawGdiEntry {
track_num: u32,
start_lba: u32,
track_type_code: u32,
sector_size: usize,
filename: String,
offset: u64,
}
let mut raw_entries = Vec::new();
for &line in &lines[1..=track_count] {
let parts = tokenize_gdi_line(line)?;
if parts.len() < 6 {
return Err(DiscError::InvalidGdi(format!(
"invalid GDI track line: '{}'",
line
)));
}
let track_num = parts[0]
.parse::<u32>()
.map_err(|_| DiscError::InvalidGdi(format!("invalid track num: '{}'", parts[0])))?;
let start_lba = parts[1]
.parse::<u32>()
.map_err(|_| DiscError::InvalidGdi(format!("invalid start LBA: '{}'", parts[1])))?;
let track_type_code = parts[2].parse::<u32>().map_err(|_| {
DiscError::InvalidGdi(format!("invalid track type code: '{}'", parts[2]))
})?;
let sector_size = parts[3].parse::<usize>().map_err(|_| {
DiscError::InvalidGdi(format!("invalid sector size: '{}'", parts[3]))
})?;
if sector_size == 0 {
return Err(DiscError::InvalidGdi(
"sector size must be non-zero".to_string(),
));
}
let filename = parts[4].clone();
crate::types::validate_descriptor_path(&filename)?;
let offset = parts[5].parse::<u64>().map_err(|_| {
DiscError::InvalidGdi(format!("invalid byte offset: '{}'", parts[5]))
})?;
raw_entries.push(RawGdiEntry {
track_num,
start_lba,
track_type_code,
sector_size,
filename,
offset,
});
}
let mut tracks = Vec::new();
for i in 0..raw_entries.len() {
let entry = &raw_entries[i];
let t_type = match (entry.track_type_code, entry.sector_size) {
(0, _) => CdTrackType::Audio,
(4, 2048) => CdTrackType::Mode1,
(4, _) => CdTrackType::Mode1Raw,
(code, _) => {
return Err(DiscError::UnsupportedTrackType(format!(
"unsupported GDI track type code: {}",
code
)));
}
};
let file_frames = if let Some(parent) = parent_dir {
let p = parent.join(&entry.filename);
let bytes = std::fs::metadata(&p)?.len();
let available = bytes.saturating_sub(entry.offset);
(available / entry.sector_size as u64) as u32
} else {
0
};
let total_frames = if i + 1 < raw_entries.len() {
raw_entries[i + 1].start_lba.saturating_sub(entry.start_lba)
} else {
file_frames
};
let pad_frames = total_frames.saturating_sub(file_frames);
let mut track = CdTrack::new(entry.track_num, t_type, total_frames);
track.pad_frames = pad_frames;
track.file_name = Some(entry.filename.clone());
track.file_offset = entry.offset;
tracks.push(track);
}
Ok(CdToc {
disc_type: DiscType::GdRom,
tracks,
})
}
pub fn format(toc: &CdToc, base_name: &str) -> String {
let mut out = String::new();
out.push_str(&format!("{}\n", toc.tracks.len()));
let mut current_lba: u32 = 0;
for track in &toc.tracks {
let is_audio = track.track_type.is_audio();
let type_code = if is_audio { 0 } else { 4 };
let sector_size = track.track_type.sector_size();
let ext = if is_audio { "raw" } else { "bin" };
let filename = format!("{}{:02}.{}", base_name, track.track_num, ext);
let quoted_filename = if filename.contains(' ') {
format!("\"{}\"", filename)
} else {
filename
};
out.push_str(&format!(
"{} {} {} {} {} 0\n",
track.track_num, current_lba, type_code, sector_size, quoted_filename
));
current_lba += track.frames;
}
out
}
}
fn tokenize_gdi_line(line: &str) -> Result<Vec<String>, DiscError> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut in_single_quote = false;
let mut in_double_quote = false;
for ch in line.chars() {
if !in_single_quote && ch == '"' {
in_double_quote = !in_double_quote;
} else if !in_double_quote && ch == '\'' {
in_single_quote = !in_single_quote;
} else if !in_single_quote && !in_double_quote && ch.is_whitespace() {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
} else {
current.push(ch);
}
}
if in_single_quote || in_double_quote {
return Err(DiscError::InvalidGdi(format!(
"unclosed quote in GDI track line: '{}'",
line
)));
}
if !current.is_empty() {
tokens.push(current);
}
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gdi_format() {
let mut toc = CdToc::new(DiscType::GdRom);
let t1 = CdTrack::new(1, CdTrackType::Mode1Raw, 4);
let mut t2 = CdTrack::new(2, CdTrackType::Audio, 44996);
t2.pad_frames = 44992;
let t3 = CdTrack::new(3, CdTrackType::Mode1Raw, 4);
toc.tracks.push(t1);
toc.tracks.push(t2);
toc.tracks.push(t3);
let gdi = GdiSheet::format(&toc, "out");
assert_eq!(
gdi,
"3\n1 0 4 2352 out01.bin 0\n2 4 0 2352 out02.raw 0\n3 45000 4 2352 out03.bin 0\n"
);
}
#[test]
fn test_gdi_tokenize_quoted() {
let line = " 1 0 4 2352 \"4x4 Evo (USA) (Track 01).bin\" 0";
let tokens = tokenize_gdi_line(line).unwrap();
assert_eq!(
tokens,
vec!["1", "0", "4", "2352", "4x4 Evo (USA) (Track 01).bin", "0"]
);
}
#[test]
fn test_gdi_parse_quoted() {
let gdi = "3\n1 0 4 2352 \"track 01.bin\" 0\n2 450 0 2352 'track 02.raw' 0\n3 45000 4 2352 track03.bin 0\n";
let toc = GdiSheet::parse(gdi, None).unwrap();
assert_eq!(toc.tracks.len(), 3);
assert_eq!(toc.tracks[0].file_name.as_deref(), Some("track 01.bin"));
assert_eq!(toc.tracks[1].file_name.as_deref(), Some("track 02.raw"));
assert_eq!(toc.tracks[2].file_name.as_deref(), Some("track03.bin"));
}
}