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; const 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], 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 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 let root_entry = read_root_entry(&mut file, header.fst_ofst)?;
142 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 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 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 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}