gc_gcm/
lib.rs

1//! A library for working with GCM/ISO files (raw bit-for-bit disk images) for the Nintendo
2//! GameCube.
3//!
4//! Features:
5//!
6//! * GCM parser
7//!     * Disk metadata (game_id, internal name, etc.)
8//!     * Offsets to various sections of the GCM
9//! * GameCube filesystem parser
10//!     * Raw access to filesystem structures
11//!     * Iterate over directories in a high-level manner
12//!     * Information about the storage of files, allowing extraction
13//! * DOL executable parser
14//!     * The main executable for the game
15//!     * Allows for extraction or loading into memory
16//!     * Supports parsing extracted DOL files as well
17//! * Apploader parser
18//!     * The second stage loader for the game
19//!     * Primarily only useful for hardware accuracy purposes
20//!
21//! ```
22//! use gc_gcm::GcmFile;
23//!
24//! let iso = GcmFile::open("melee.iso").unwrap();
25//!
26//! println!("Name of game: {:?}", iso.internal_name);
27//! println!("Size of executable: {:x?}", iso.dol.raw_data.len());
28//! println!(
29//!     "Number of files: {}",
30//!     iso.filesystem.files
31//!         .iter()
32//!         .filter(|entry| matches!(entry, gc_gcm::FsNode::File { .. }))
33//!         .count()
34//! );
35//! ```
36//!
37//! Output:
38//!
39//! ```text
40//! Name of game: "Super Smash Bros Melee"
41//! Size of executable: 4385e0
42//! Number of files: 1209
43//! ```
44
45#![cfg_attr(feature = "no_std", no_std)]
46use binread::{derive_binread, BinRead, BinReaderExt, NullString, io::{self, SeekFrom}};
47use binread::file_ptr::{FilePtr, FilePtr32, IntoSeekFrom};
48use binread::helpers::read_bytes;
49use core::fmt;
50
51#[cfg(feature = "no_std")]
52mod std;
53
54#[cfg(feature = "no_std")]
55use crate::std::{string::String, vec::Vec};
56
57/// Top-level view of a GCM file
58#[derive(BinRead, Debug)]
59#[br(big)]
60struct GcmTop (
61    // magic value at 0x1c
62    #[br(seek_before = SeekFrom::Start(0x1c))]
63    pub GcmFile,
64);
65
66/// A 6-character ID for a game
67#[derive(BinRead)]
68pub struct GameId(pub [u8; 6]);
69
70/// A parsed GCM/ISO file
71#[derive_binread]
72#[derive(Debug)]
73#[br(magic = 0xc2339f3d_u32)]
74pub struct GcmFile {
75    #[br(seek_before = SeekFrom::Start(0))]
76    pub game_id: GameId,
77    pub disc_number: u8,
78    pub revision: u8,
79    
80    #[br(seek_before = SeekFrom::Start(0x20))]
81    #[br(map = NullString::into_string)]
82    pub internal_name: String,
83    
84    // just gonna skip debug stuff
85
86    #[br(seek_before = SeekFrom::Start(0x420))]
87    pub dol_offset: u32,
88
89    #[br(seek_before = SeekFrom::Start(0x420))]
90    #[br(parse_with = FilePtr32::parse)]
91    pub dol: DolFile,
92    
93    fs_offset: u32,
94    fs_size: u32,
95    max_fs_size: u32,
96    
97    #[br(seek_before = SeekFrom::Start(fs_offset as u64))]
98    #[br(args(fs_offset, fs_size))]
99    pub filesystem: FileSystem,
100
101    // raw data
102
103    #[br(seek_before = SeekFrom::Start(0))]
104    #[br(parse_with = read_bytes)]
105    #[br(count = 0x440)]
106    pub boot_bin: Vec<u8>,
107
108    #[br(seek_before = SeekFrom::Start(0x440))]
109    #[br(parse_with = read_bytes)]
110    #[br(count = 0x2000)]
111    pub bi2_bin: Vec<u8>,
112
113    #[br(seek_before = SeekFrom::Start(0x2440))]
114    pub apploader_header: ApploaderHeader,
115
116    #[br(seek_before = SeekFrom::Start(0x2440))]
117    #[br(parse_with = read_bytes)]
118    #[br(count = apploader_header.size + apploader_header.trailer_size + ApploaderHeader::SIZE)]
119    pub apploader: Vec<u8>,
120
121    #[br(seek_before = SeekFrom::Start(fs_offset as u64))]
122    #[br(parse_with = read_bytes)]
123    #[br(count = fs_size)]
124    pub fst_bytes: Vec<u8>,
125}
126
127#[derive(BinRead, Debug)]
128pub struct ApploaderHeader {
129    date: [u8; 0x10],
130    entrypoint_ptr: u32,
131    size: u32,
132    trailer_size: u32,
133    padding: [u8; 4],
134}
135
136impl ApploaderHeader {
137    const SIZE: u32 = 0x20;
138}
139
140/// The parsed GCM filesystem
141#[derive(BinRead, Debug)]
142#[br(import(offset: u32, size: u32))]
143pub struct FileSystem {
144    pub root: RootNode,
145
146    #[br(args(
147        offset as u64, // root offset
148        (offset + (root.total_node_count * FsNode::SIZE)) as u64 // name offset (after all entries)
149    ))]
150    #[br(count = root.total_node_count - 1)]
151    pub files: Vec<FsNode>,
152}
153
154/// The root node of the filesystem, under which all the other nodes fall
155#[derive(BinRead, Debug)]
156#[br(magic = 1u8)]
157pub struct RootNode {
158    #[br(map = U24::into)]
159    pub name_offset: u32,
160    pub node_start_index: u32,
161    pub total_node_count: u32,
162}
163
164type FilePtr24<T> = FilePtr<U24, T>;
165
166/// A given parsed node in the filesystem
167#[br(import(root_offset: u64, name_offset: u64))]
168#[derive(BinRead, Debug)]
169pub enum FsNode {
170    #[br(magic = 0u8)]
171    File {
172        #[br(offset = name_offset)]
173        #[br(parse_with = FilePtr24::parse)]
174        #[br(map = NullString::into_string)]
175        name: String,
176        offset: u32,
177        size: u32,
178    },
179
180    #[br(magic = 1u8)]
181    Directory {
182        #[br(offset = name_offset)]
183        #[br(parse_with = FilePtr24::parse)]
184        #[br(map = NullString::into_string)]
185        name: String,
186        parent_index: u32,
187        end_index: u32,
188    },
189}
190
191impl FsNode {
192    const SIZE: u32 = 0xC;
193}
194
195#[derive(BinRead, Clone, Copy)]
196struct U24([u8; 3]);
197
198impl IntoSeekFrom for U24 {
199    fn into_seek_from(self) -> SeekFrom {
200        u32::from(self).into_seek_from()
201    }
202}
203
204impl From<U24> for u32 {
205    fn from(U24(x): U24) -> Self {
206        u32::from_be_bytes([0, x[0], x[1], x[2]])
207    }
208}
209
210impl fmt::Debug for GameId {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        match core::str::from_utf8(&self.0[..]) {
213            Ok(id) => fmt::Debug::fmt(id, f),
214            Err(_) => write!(f, "GameId({:02x?})", &self.0[..])
215        }
216    }
217}
218
219mod dol;
220mod error;
221mod dir_listing;
222pub use error::GcmError;
223pub use dir_listing::*;
224pub use dol::*;
225
226impl GcmFile {
227    /// Parse a GcmFile from a reader that implements `io::Read` and `io::Seek`
228    pub fn from_reader<R>(reader: &mut R) -> Result<Self, GcmError>
229        where R: io::Read + io::Seek,
230    {
231        Ok(reader.read_be::<GcmTop>()?.0)
232    }
233}
234
235#[cfg(not(feature = "no_std"))]
236use ::std::path::Path;
237
238#[cfg(not(feature = "no_std"))]
239impl GcmFile {
240    /// Open a file from a given bath as a GcmFile.
241    pub fn open<P>(path: P) -> Result<Self, GcmError>
242        where P: AsRef<Path>,
243    {
244        let mut reader = ::std::io::BufReader::new(::std::fs::File::open(path)?);
245        Ok(reader.read_be::<GcmTop>()?.0)
246    }
247}