gc_image/
lib.rs

1use std::path::Path;
2use std::fs;
3use std::io::prelude::*;
4use encoding_rs::{SHIFT_JIS, UTF_8};
5use thiserror::Error;
6
7const DVD_HEADER_SIZE: usize = 0x0440;
8const DVD_MAGIC_NUMBER: u32 = 0xC2339F3D;
9const DVD_IMAGE_SIZE: u64 = 1_459_978_240;
10const GAME_NAME_SIZE: usize = 0x03e0;
11const CONSOLE_ID: u8 = 0x47; //'G' in ASCII
12const FILE_ENTRY_SIZE: usize = 0x0C;
13const BANNER_NAME: &str = "opening.bnr";
14const BANNER_SZ: usize = 6_496;
15
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum ImageError {
19    #[error("error reading file")]
20    IOError(#[from] std::io::Error),
21    #[error("invalid image file")]
22    InvalidFileType,
23    #[error("invalid region byte: {byte}")]
24    InvalidRegion {
25        byte: u8
26    },
27    #[error("invalid image header ({0})")]
28    InvalidHeader(String),
29    #[error("invalid banner data({0})")]
30    InvalidBanner(String),
31    #[error("{0} was not found in the image")]
32    FileNotFound(String),
33}
34
35#[derive(Copy, Clone)]
36pub enum Region {
37    USA,
38    EUR,
39    JPN,
40    FRA
41}
42
43impl Region {
44    fn from_byte(byte: u8) -> Result<Region, ImageError> {
45        match byte {
46            b'E' => {
47                Ok(Region::USA)
48            },
49            b'J' => {
50                Ok(Region::JPN)
51            },
52            b'P' => {
53                Ok(Region::EUR)
54            },
55            b'F' => {
56                Ok(Region::FRA)
57            },
58            _ => {
59                Err(ImageError::InvalidRegion {
60                    byte
61                })
62            }
63        }
64    }
65}
66
67pub struct GCImage {
68    pub header: DVDHeader,
69    pub banner: Banner,
70    pub region: Region
71}
72
73pub struct DVDHeader {
74    pub game_code: [u8; 4],
75    pub maker_code: [u8; 2],
76    pub disk_id: u8,
77    pub version: u8,
78    pub audio_streaming: bool,
79    pub stream_buf_sz: u8,
80    pub magic_word: u32,
81    pub game_name: String,
82    pub dol_ofst: u32,
83    pub fst_ofst: u32,
84    pub fst_sz: u32,
85    pub max_fst_sz: u32
86}
87
88pub struct Banner {
89    pub magic_word: [u8; 4],
90    pub graphical_data: [u8; 0x1800], //RGB5A1 format
91    pub game_name: String,
92    pub developer: String,
93    pub full_game_title: String,
94    pub full_developer_name: String,
95    pub description: String
96}
97
98struct FileData {
99    file_offset: u32,
100    file_length: u32
101}
102
103struct DirData {
104    parent_offset: u32,
105    next_offset: u32
106}
107
108struct RootDirectory {
109    num_entries: u32,
110    string_table_ofst: u32
111}
112
113enum EntryType {
114    File(FileData),
115    Directory(DirData)
116}
117
118struct Entry {
119    filename_ofst: u32,
120    entry: EntryType
121}
122
123impl GCImage {
124    pub fn open(path: &Path) -> Result<GCImage, ImageError> {
125        let metadata = fs::metadata(path)?;
126        if metadata.len() != DVD_IMAGE_SIZE {
127            return Err(ImageError::InvalidFileType);
128        }
129        let mut file = fs::File::open(path)?;
130        file.seek(std::io::SeekFrom::Start(0))?;
131
132        //Read and parse DVD Image header
133        let mut data: [u8; DVD_HEADER_SIZE] = [0; DVD_HEADER_SIZE];
134        file.read_exact(&mut data)?;
135        let header = parse_header(&data);
136        validate_header(&header)?;
137
138        let region = Region::from_byte(header.game_code[3])?;
139
140        //Read and parse banner file. TODO, don't spam list files here. Maybe return an Iterator to each file entry?
141        let root_entry = read_root_entry(&mut file, header.fst_ofst)?;
142        //list_files(&mut file, header.fst_ofst, &root_entry);
143        let banner = read_banner(&mut file, header.fst_ofst, &root_entry, region)?;
144        validate_banner(&banner)?;
145        Ok(GCImage {
146            header,
147            banner,
148            region
149        })
150    }
151}
152
153fn parse_header(data: &[u8]) -> DVDHeader {
154    assert!(data.len() == DVD_HEADER_SIZE);
155    let mut game_code = [0; 4];
156    game_code.clone_from_slice(&data[0..=0x3]);
157    let mut maker_code = [0; 2]; 
158    maker_code.clone_from_slice(&data[0x4..=0x5]);
159    let disk_id = data[0x6];
160    let version = data[0x7];
161    let audio_streaming = data[0x8] != 0;
162    let stream_buf_sz = data[0x9];
163    let magic_word = u8_arr_to_u32( &data[0x001c..=0x001f] );
164    let mut game_name = [0; GAME_NAME_SIZE];
165    game_name.clone_from_slice(&data[0x0020..=0x03ff]);
166    let game_name = String::from_utf8(game_name.to_vec()).unwrap();
167    let dol_ofst = u8_arr_to_u32(&data[0x0420..=0x0423]);
168    let fst_ofst = u8_arr_to_u32(&data[0x0424..=0x0427]);
169    let fst_sz = u8_arr_to_u32(&data[0x0428..=0x042B]);
170    let max_fst_sz = u8_arr_to_u32(&data[0x042C..=0x042F]);
171    DVDHeader {
172        game_code,
173        maker_code,
174        disk_id,
175        version,
176        audio_streaming,
177        stream_buf_sz,
178        magic_word,
179        game_name,
180        dol_ofst,
181        fst_ofst,
182        fst_sz,
183        max_fst_sz
184    }
185}
186
187fn read_banner(file: &mut fs::File, fst_ofst: u32, root_entry: &RootDirectory, region: Region) -> Result<Banner, ImageError> {
188    let banner_entry = find_file(file, fst_ofst, root_entry, BANNER_NAME)?;
189    match banner_entry.entry {
190        EntryType::File(file_data) => {
191            let mut data = [0; BANNER_SZ];
192            if file_data.file_length as usize != BANNER_SZ {
193                return Err(ImageError::InvalidBanner("malformed banner file".to_string()));
194            }
195            file.seek(std::io::SeekFrom::Start(file_data.file_offset as u64))?;
196            file.read_exact(&mut data)?;
197
198            let mut magic_word = [0; 0x4];
199            magic_word.copy_from_slice(&data[0..0x4]);
200            let mut graphical_data = [0; 0x1800];
201            graphical_data.copy_from_slice(&data[0x0020..0x1820]);
202            let game_name = byte_slice_to_string(&data[0x1820..0x1840], region);
203            let developer = byte_slice_to_string(&data[0x1840..0x1860], region);
204            let full_game_title = byte_slice_to_string(&data[0x1860..0x18a0], region);
205            let full_developer_name = byte_slice_to_string(&data[0x18a0..0x18e0], region) ;
206            let description = byte_slice_to_string(&data[0x18e0..0x1960], region);
207            Ok(Banner {
208                magic_word,
209                graphical_data,
210                game_name,
211                developer,
212                full_game_title,
213                full_developer_name,
214                description
215            })
216        },
217        _ => { Err(ImageError::InvalidBanner("opening.bnr must be a file".to_string())) }
218    }
219}
220
221fn read_root_entry(file: &mut fs::File, fst_ofst: u32) -> Result<RootDirectory, ImageError> {
222    file.seek(std::io::SeekFrom::Start(fst_ofst as u64))?;
223    let mut data = [0; FILE_ENTRY_SIZE];
224    file.read_exact(&mut data)?;
225
226    let flags = data[0];
227    //Root Entry Should always be a directory
228    if flags != 1 {
229        return Err(ImageError::InvalidHeader("invalid root directory entry".to_string()));
230    }
231    let num_entries = u8_arr_to_u32(&data[0x08..0x0C]);
232    let string_table_ofst = num_entries * FILE_ENTRY_SIZE as u32;
233
234    Ok(RootDirectory {
235        num_entries,
236        string_table_ofst
237    })
238}
239
240fn read_entry(file: &mut fs::File, ofst: u32) -> Result<Entry, ImageError> {
241    file.seek(std::io::SeekFrom::Start(ofst as u64))?;
242    let mut data = [0; FILE_ENTRY_SIZE];
243    file.read_exact(&mut data)?;
244
245    let flags = data[0];
246    let filename_ofst = u8_arr_to_u24(&data[0x01..0x04]);
247    let entry = if flags == 0 {
248        //File
249        let file_offset = u8_arr_to_u32(&data[0x04..0x08]);
250        let file_length = u8_arr_to_u32(&data[0x08..0x0C]);
251        EntryType::File(FileData {
252            file_offset,
253            file_length
254        })
255    } else {
256        //Directory
257        let parent_offset = u8_arr_to_u32(&data[0x04..0x08]);
258        let next_offset = u8_arr_to_u32(&data[0x08..0x0C]);
259        EntryType::Directory(DirData {
260            parent_offset,
261            next_offset
262        })
263    };
264
265    Ok(Entry {
266        entry,
267        filename_ofst
268    })
269}
270
271fn list_files(file: &mut fs::File, fst_ofst: u32, root_entry: &RootDirectory) {
272    for i in 0..root_entry.num_entries {
273        let ofst = ( i * FILE_ENTRY_SIZE as u32 ) + fst_ofst;
274        let entry = read_entry(file, ofst).unwrap();
275        let ofst = entry.filename_ofst + root_entry.string_table_ofst + fst_ofst;
276        let filename = read_string(file, ofst as u64);
277        let offsets = match entry.entry {
278            EntryType::File(file_data) => {
279                format!("File Offset: {}, File Length: {}", file_data.file_offset, file_data.file_length)
280            },
281            EntryType::Directory(dir_data) => {
282                format!("Parent Offset: {}, Next Offset: {}", dir_data.parent_offset, dir_data.next_offset)
283            }
284        };
285        println!("{:03} - {} - {}", i, filename, offsets);
286    }
287}
288
289fn find_file(img_file: &mut fs::File, fst_ofst: u32, root_entry: &RootDirectory, name: &str) -> Result<Entry, ImageError> {
290    for i in 0..root_entry.num_entries {
291        let ofst = ( i * FILE_ENTRY_SIZE as u32 ) + fst_ofst;
292        let entry = read_entry(img_file, ofst)?;
293        let ofst = entry.filename_ofst + root_entry.string_table_ofst + fst_ofst;
294        let filename = read_string(img_file, ofst as u64);
295        match entry.entry {
296            EntryType::File(_) => {
297                if filename == name {
298                    return Ok(entry);
299                }
300            }
301            _ => {}
302        }
303    }
304    Err(ImageError::FileNotFound(name.to_string()))
305}
306
307fn read_string(file: &mut fs::File, ofst: u64) -> String {
308    let mut bytes = Vec::new();
309
310    file.seek(std::io::SeekFrom::Start(ofst as u64)).unwrap();
311
312    for byte in file.bytes() {
313        let byte = byte.unwrap();
314        if byte == 0 {
315            break;
316        }
317        bytes.push(byte);
318    }
319
320    String::from_utf8(bytes).unwrap()
321}
322
323fn byte_slice_to_string(bytes: &[u8], region: Region) -> String {
324    match region {
325        Region::USA |
326        Region::EUR |
327        Region::FRA => {
328            let (s, _, _) = UTF_8.decode(bytes);
329            s.to_string()
330        },
331        Region::JPN => {
332            let(s, _, _) = SHIFT_JIS.decode(&bytes);
333            s.to_string()
334        }
335    }
336}
337
338fn validate_header(hdr: &DVDHeader) -> Result<(), ImageError> {
339    if hdr.magic_word != DVD_MAGIC_NUMBER {
340        return Err(ImageError::InvalidHeader("incorrect or missing magic number".to_string()));
341    }
342    if (hdr.fst_ofst as u64) >= DVD_IMAGE_SIZE {
343        return Err(ImageError::InvalidHeader("malformed filesystem table offset".to_string()));
344    }
345    if (hdr.dol_ofst as u64) >= DVD_IMAGE_SIZE {
346        return Err(ImageError::InvalidHeader("malformed bootfile offset".to_string()));
347    }
348    if hdr.game_code[0] != CONSOLE_ID {
349        return Err(ImageError::InvalidHeader("incorrect console id".to_string()));
350    }
351    Ok(())
352}
353
354fn validate_banner(bnr: &Banner) -> Result<(), ImageError> {
355    if bnr.magic_word[0] != b'B' ||
356       bnr.magic_word[1] != b'N' ||
357       bnr.magic_word[2] != b'R' ||
358       ( bnr.magic_word[3] != b'1' && bnr.magic_word[3] != b'2' ) {
359        Err(ImageError::InvalidBanner("invalid banner magic word".to_string()))
360    } else {
361        Ok(())
362    }
363}
364
365fn u8_arr_to_u32(arr: &[u8]) -> u32 {
366    assert!(arr.len() == 4);
367    let b1 = ( arr[0]  as u32) << 24;
368    let b2 = ( arr[1]  as u32) << 16;
369    let b3 = ( arr[2]  as u32) << 8;
370    let b4 = arr[3] as u32;
371    b1 | b2 | b3 | b4
372}
373
374fn u8_arr_to_u24(arr: &[u8]) -> u32 {
375    assert!(arr.len() == 3);
376    let b1 = ( arr[0] as u32) << 16;
377    let b2 = ( arr[1]  as u32 ) << 8;
378    let b3 = arr[2] as u32;
379    b1 | b2 | b3
380}
381
382#[cfg(test)]
383mod tests {
384    #[test]
385    fn load_iso() {
386        assert!(true);
387    }
388}