1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>
use crate::error::Error;
use crate::utils::from_latin1_or_shift_jis;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str;
use byteorder::BigEndian;
use byteorder::ReadBytesExt;
pub struct FileEntry {
/// Index to the filename in the string table right after the FST.
pub filename_offset: u32, // It's actually 24 bits
/// The offset of the file in the disk.
pub offset: u32,
/// Size of the file.
pub size: u32,
/// The index of this entry
pub index: u32,
}
pub struct DirectoryEntry {
/// Index to the directory name in the string table right after the FST.
pub filename_offset: u32, // It's actually 24 bits
/// Index of the parent directory entry.
pub parent_index: u32,
/// Index of the entry following all of the contents of this directory.
pub end_index: u32,
/// The index of this entry
pub index: u32,
}
/// Represents an FST entry.
pub enum Entry {
File(FileEntry),
Directory(DirectoryEntry),
}
/// Filesystem structure.
pub struct Fst {
/// List of all entries in the filesystem.
pub entries: Vec<Entry>,
/// offset of the string table in the disk.
pub string_table_offset: u32,
}
pub trait FstRead {
/// Parses the FST from the object provided, returning an [`Fst`] object with the filesystem
/// information.
///
/// # Errors
///
/// Returns [`Error::Parse`] if the header cannot be found or if a field in the header contains
/// an unexpected value.
/// Returns [`Error::Io`] if an IO error took place while reading from the file.
fn read_fst(&mut self) -> Result<Fst, Error>;
}
impl<T: Read + Seek> FstRead for T {
fn read_fst(&mut self) -> Result<Fst, Error> {
// Check file header, and 2 magic bytes
self.seek(SeekFrom::Start(0x0424))?;
let fst_addr = self.read_u32::<BigEndian>()?;
self.seek(SeekFrom::Start(fst_addr.into()))?;
if self.read_u8()? == 0 {
return Err(Error::Parse("invalid root filesystem entry".into()));
}
self.seek(SeekFrom::Current(7))?;
let num_entries = self.read_u32::<BigEndian>()?;
// Go back to the beginning so we include root entry
self.seek(SeekFrom::Start(fst_addr.into()))?;
let mut fst_entries: Vec<Entry> = Vec::new();
for index in 0..num_entries {
let mut data = [0u8; 12];
self.read_exact(&mut data)?;
let entry = match data[0] {
0 => Entry::File(FileEntry {
filename_offset: u32::from_be_bytes([0, data[1], data[2], data[3]]),
offset: u32::from_be_bytes(data[4..8].try_into().unwrap()),
size: u32::from_be_bytes(data[8..12].try_into().unwrap()),
index,
}),
1 => Entry::Directory(DirectoryEntry {
filename_offset: u32::from_be_bytes([0, data[1], data[2], data[3]]),
parent_index: u32::from_be_bytes(data[4..8].try_into().unwrap()),
end_index: u32::from_be_bytes(data[8..12].try_into().unwrap()),
index,
}),
_ => return Err(Error::Parse("invalid filesystem entry found".into())),
};
fst_entries.push(entry);
}
let fst_string_addr = fst_addr + 12 * num_entries;
Ok(Fst {
entries: fst_entries,
string_table_offset: fst_string_addr,
})
}
}
impl Fst {
/// Gets the number of entries (files and directories) in the FST.
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub const fn get_number_of_entries(&self) -> u32 {
// This `as` use is OK because the GCN cannot have more than 24-bits worth of file
// entries.
self.entries.len() as u32
}
/// Returns the filename for the file entry at the given index.
///
/// # Errors
///
/// [`Error::Io`] if there was an underlying error reading or seeking from the IO object,
/// [`Error::Parse`] if the filename is not a valid latin1 or SHIFT JIS string.
pub fn get_filename<T: Read + Seek>(&self, io: &mut T, index: u32) -> Result<String, Error> {
let index = index as usize; // This library won't work on 16 bit address platforms
self.get_entry_filename(io, &self.entries[index])
}
/// Returns the filename for the file entry at the given index.
///
/// # Errors
///
/// [`Error::Io`] if there was an underlying error reading or seeking from the IO object,
/// [`Error::Parse`] if the filename is not a valid latin1 or SHIFT JIS string.
pub fn get_entry_filename<T: Read + Seek>(
&self,
io: &mut T,
entry: &Entry,
) -> Result<String, Error> {
let str_offset = match entry {
Entry::File(file) => file.filename_offset,
Entry::Directory(dir) => dir.filename_offset,
};
let string_location: u64 = (self.string_table_offset + str_offset).into();
io.seek(SeekFrom::Start(string_location))?;
let mut buffered = BufReader::with_capacity(0x1000, io);
let mut data = vec![];
buffered.read_until(b'\0', &mut data)?;
// Apparently filenames can be latin1 or SHIFT JIS...
from_latin1_or_shift_jis(&data[0..(data.len() - 1)])
}
/// Finds the filename by its name, returning its offset in the string table.
///
/// String offsets can't be any more than 24 bits, due to the definition of an string offset in
/// the FST File and Dictionary entries.
///
/// # Errors
///
/// [`Error::Io`] if there was an underlying error reading or seeking from the IO object,
/// [`Error::Parse`] if the filename is not a valid latin1 or SHIFT JIS string.
pub fn find_filename<T: Read + Seek>(&self, io: &mut T, filename: &str) -> Result<u32, Error> {
let string_location: u64 = (self.string_table_offset).into();
io.seek(SeekFrom::Start(string_location))?;
let mut buffered = BufReader::with_capacity(0x8000, io);
let start = buffered.stream_position()?;
let mut end = start;
for _ in &self.entries {
end = buffered.stream_position()?;
let mut data = vec![];
buffered.read_until(b'\0', &mut data)?;
if from_latin1_or_shift_jis(&data[0..(data.len() - 1)])? == filename {
break;
}
}
// This is fine, there can't be any more than 24 bits worth of string offsets
#[allow(clippy::cast_possible_truncation)]
let index = (end - start) as u32;
Ok(index)
}
/// Returns the data belonging to the file specified.
///
/// # Errors
/// [`Error::Io`] if there was an underlying error reading or seeking from the IO object,
/// and see [`Fst::get_filename`] for other errors.
pub fn get_file<T: Read + Seek>(&self, io: &mut T, filename: &str) -> Result<Vec<u8>, Error> {
let found_index = self.find_filename(io, filename)?;
for entry in &self.entries {
let str_offset = match &entry {
Entry::File(file) => file.filename_offset,
Entry::Directory(dir) => dir.filename_offset,
};
if found_index == str_offset {
match &entry {
Entry::File(file) => {
io.seek(SeekFrom::Start(file.offset.into()))?;
// This is fine, as this library won't work on a 16-bit address platform
let size = file.size as usize;
let mut data = vec![0u8; size];
io.read_exact(&mut data)?;
return Ok(data);
}
Entry::Directory(_) => {
return Err(Error::Parse(
"requested index is a directory, not a file".into(),
));
}
}
}
}
Ok(vec![])
}
}