use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::error::{OpticaldiscsError, Result};
use crate::sector_reader::{
GdromSectorReader, SectorReader, GDROM_HD_START_LBA, MODE1_DATA_OFFSET, RAW_SECTOR_SIZE,
SECTOR_SIZE,
};
#[derive(Debug, Clone)]
pub struct GdiTrack {
pub number: u32,
pub start_lba: u64,
pub track_type: u32,
pub sector_size: u32,
pub filename: String,
pub offset: u64,
}
impl GdiTrack {
pub fn is_data(&self) -> bool {
self.track_type == 4
}
}
pub fn parse_gdi(path: &Path) -> Result<Vec<GdiTrack>> {
let text = std::fs::read_to_string(path).map_err(OpticaldiscsError::Io)?;
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
let count: usize = lines
.next()
.and_then(|l| l.trim().parse().ok())
.ok_or_else(|| OpticaldiscsError::Parse("empty or invalid .gdi header".into()))?;
let mut tracks = Vec::with_capacity(count);
for line in lines {
if let Some(track) = parse_gdi_line(line) {
tracks.push(track);
}
}
if tracks.is_empty() {
return Err(OpticaldiscsError::Parse("no tracks in .gdi".into()));
}
Ok(tracks)
}
fn parse_gdi_line(line: &str) -> Option<GdiTrack> {
let line = line.trim();
let (number, rest) = take_uint(line)?;
let (start_lba, rest) = take_uint(rest)?;
let (track_type, rest) = take_uint(rest)?;
let (sector_size, rest) = take_uint(rest)?;
let rest = rest.trim_start();
let (filename, after) = if let Some(stripped) = rest.strip_prefix('"') {
let end = stripped.find('"')?;
(stripped[..end].to_string(), &stripped[end + 1..])
} else {
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
(rest[..end].to_string(), &rest[end..])
};
let offset = after.trim().parse::<u64>().unwrap_or(0);
Some(GdiTrack {
number: number as u32,
start_lba,
track_type: track_type as u32,
sector_size: sector_size as u32,
filename,
offset,
})
}
fn take_uint(s: &str) -> Option<(u64, &str)> {
let s = s.trim_start();
let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
if end == 0 {
return None;
}
let value = s[..end].parse().ok()?;
Some((value, &s[end..]))
}
pub fn open_gdi_hd_reader(gdi_path: &Path) -> Result<Box<dyn SectorReader>> {
let tracks = parse_gdi(gdi_path)?;
let hd = tracks
.iter()
.find(|t| t.is_data() && t.start_lba >= GDROM_HD_START_LBA)
.ok_or_else(|| OpticaldiscsError::Parse("no high-density data track in .gdi".into()))?;
let dir = gdi_path.parent().unwrap_or_else(|| Path::new("."));
let reader = GdiTrackReader::open(dir, hd)?;
Ok(Box::new(GdromSectorReader::new(Box::new(reader))))
}
struct GdiTrackReader {
file: BufReader<File>,
physical_sector_size: u64,
data_offset: u64,
file_byte_offset: u64,
}
impl GdiTrackReader {
fn open(dir: &Path, track: &GdiTrack) -> Result<Self> {
let path: PathBuf = dir.join(&track.filename);
let file = File::open(&path).map_err(OpticaldiscsError::Io)?;
let (physical_sector_size, data_offset) = match track.sector_size as u64 {
RAW_SECTOR_SIZE => (RAW_SECTOR_SIZE, MODE1_DATA_OFFSET),
SECTOR_SIZE => (SECTOR_SIZE, 0),
other => (
other,
if other >= RAW_SECTOR_SIZE {
MODE1_DATA_OFFSET
} else {
0
},
),
};
Ok(Self {
file: BufReader::new(file),
physical_sector_size,
data_offset,
file_byte_offset: track.offset,
})
}
}
impl SectorReader for GdiTrackReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
let pos = self.file_byte_offset + lba * self.physical_sector_size + self.data_offset;
self.file
.seek(SeekFrom::Start(pos))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.file
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_standard_gdi() {
let text = "3\n\
1 0 4 2352 track01.bin 0\n\
2 600 0 2352 track02.raw 0\n\
3 45000 4 2352 track03.bin 0\n";
let dir = tempfile::tempdir().unwrap();
let gdi = dir.path().join("game.gdi");
std::fs::write(&gdi, text).unwrap();
let tracks = parse_gdi(&gdi).unwrap();
assert_eq!(tracks.len(), 3);
assert_eq!(tracks[2].number, 3);
assert_eq!(tracks[2].start_lba, 45000);
assert!(tracks[2].is_data());
assert_eq!(tracks[2].sector_size, 2352);
assert_eq!(tracks[2].filename, "track03.bin");
}
#[test]
fn parses_quoted_filename() {
let t = parse_gdi_line("3 45000 4 2352 \"my track 03.bin\" 0").unwrap();
assert_eq!(t.filename, "my track 03.bin");
assert_eq!(t.start_lba, 45000);
}
}