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 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
#![no_std]
#![forbid(unsafe_code)]
#![allow(clippy::needless_return)]
//! This crate enables reading of Gameboy Filesystem (`GBFS`)-formatted data.
//! It's primarily designed for use in GBA games, and as such is fully `no_std` compatible (even `alloc` is not required).
mod error;
pub use error::*;
mod header;
use header::*;
use core::str;
use core::u32;
use arrayvec::{ArrayString, ArrayVec};
use byte_slice_cast::AsSliceOf;
/// Maximum length of a filename in bytes. Is 24 in the pin-eight C implementation
pub const FILENAME_LEN: usize = 24;
/// Length of a single file's entry in the directory that precedes the data.
const DIR_ENTRY_LEN: usize = 32;
// TODO: Allow control at build-time by user for different ROM use/flexibility tradeoffs.
const NUM_FS_ENTRIES: usize = 2048;
/// The name of a GBFS file. This is not a regular string because filenames have a limited length.
type Filename = ArrayString<FILENAME_LEN>;
#[derive(Debug, Copy, Clone)]
struct GBFSFileEntry {
/// Name of file; at most 24 bytes.
/// TODO: Once const fn's can perform subslicing, use a slice here
name: [u8; FILENAME_LEN],
/// Length of file in bytes
len: u32,
/// Offset of first file byte from start of filesystem
data_offset: u32,
}
impl GBFSFileEntry {
/// Compare the name with a Filename.
fn name_is_equal(&self, name: Filename) -> Result<bool, GBFSError> {
// Unfortunately, the const fn constructor for GBFSFilesystem
// can't use dynamically-sized data structures.
// Therefore, we have to strip out the trailing nulls from the filename here.
let no_nulls: ArrayVec<u8, { FILENAME_LEN }> =
self.name.iter().filter(|x| **x != 0).copied().collect();
let filename_str: &str = match str::from_utf8(no_nulls.as_ref()) {
Ok(s) => s,
Err(e) => return Err(GBFSError::Utf8Error(e)),
};
match Filename::from(filename_str) {
Err(_) => return Err(GBFSError::FilenameTooLong(FILENAME_LEN, filename_str.len())),
Ok(our_name) => return Ok(name == our_name),
}
}
}
/// A filesystem that files can be read from.
// Needed to ensure proper alignment for casting u8 slices to u16/u32 slices
#[repr(align(4))]
#[repr(C)]
#[derive(Clone)]
pub struct GBFSFilesystem<'a> {
/// Backing data slice
data: &'a [u8],
/// Filesystem header
hdr: GBFSHeader,
/// Directory
dir: [Option<GBFSFileEntry>; NUM_FS_ENTRIES],
}
impl<'a> GBFSFilesystem<'a> {
/// Constructs a new filesystem from a GBFS-formatted byte slice.
///
/// To make lifetime management easier it's probably a good idea to use a slice with a `static` lifetime here.
/// It's also a good idea to ensure this function is called at compile time with a `const` argument,
/// to avoid having to store the filesystem index in RAM.
pub const fn from_slice(data: &'a [u8]) -> Result<GBFSFilesystem<'a>, GBFSError> {
// TODO: Assert slice alignment
// Brace yourself for some very ugly code caused by the limitations of const fn below.
// Create the FS header
// Forgive me God, for I have sinned
// TODO: Clean up this mess (maybe a macro?)
if data.len() < header::GBFS_HEADER_LENGTH {
return Err(GBFSError::HeaderInvalid);
}
let hdr = match GBFSHeader::from_slice(&[
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
data[9], data[10], data[11], data[12], data[13], data[14], data[15], data[16],
data[17], data[18], data[19], data[20], data[21], data[22], data[23], data[24],
data[25], data[26], data[27], data[28], data[29], data[30], data[31],
]) {
Ok(val) => val,
Err(err) => return Err(err),
};
// Create the FS entry table
// Read directory entries
let mut dir_entries: [Option<GBFSFileEntry>; NUM_FS_ENTRIES] = [None; NUM_FS_ENTRIES];
// Can't use a for loop here because they're not yet supported in const fn's
let mut i = 0;
if (hdr.dir_num_members as usize) > NUM_FS_ENTRIES {
return Err(GBFSError::TooManyEntries(
NUM_FS_ENTRIES,
hdr.dir_num_members as usize,
));
}
while i < hdr.dir_num_members as usize {
let entry_start = hdr.dir_off as usize + (i * DIR_ENTRY_LEN);
// Extract filename
if data.len() < entry_start + FILENAME_LEN {
return Err(GBFSError::Truncated);
}
// TODO: DRY
let filename: [u8; FILENAME_LEN] = [
data[entry_start],
data[entry_start + 1],
data[entry_start + 2],
data[entry_start + 3],
data[entry_start + 4],
data[entry_start + 5],
data[entry_start + 6],
data[entry_start + 7],
data[entry_start + 8],
data[entry_start + 9],
data[entry_start + 10],
data[entry_start + 11],
data[entry_start + 12],
data[entry_start + 13],
data[entry_start + 14],
data[entry_start + 15],
data[entry_start + 16],
data[entry_start + 17],
data[entry_start + 18],
data[entry_start + 19],
data[entry_start + 20],
data[entry_start + 21],
data[entry_start + 22],
data[entry_start + 23],
];
// Extract length of file in bytes
if data.len() < entry_start + FILENAME_LEN + 4 {
return Err(GBFSError::Truncated);
};
let len = u32::from_le_bytes([
data[entry_start + FILENAME_LEN],
data[entry_start + FILENAME_LEN + 1],
data[entry_start + FILENAME_LEN + 2],
data[entry_start + FILENAME_LEN + 3],
]);
// Extract offset of file data from FS start
if data.len() < entry_start + FILENAME_LEN + 8 {
return Err(GBFSError::Truncated);
};
let data_offset = u32::from_le_bytes([
data[entry_start + FILENAME_LEN + 4],
data[entry_start + FILENAME_LEN + 5],
data[entry_start + FILENAME_LEN + 6],
data[entry_start + FILENAME_LEN + 7],
]);
dir_entries[i] = Some(GBFSFileEntry {
name: filename,
len,
data_offset,
});
i += 1;
}
return Ok(GBFSFilesystem {
data,
hdr,
dir: dir_entries,
});
}
/// Gets file data by index in directory table.
fn get_file_data_by_index(&self, index: usize) -> &'a [u8] {
// The storage format changes based on whether we have a static filesystem or
// once created at runtime.
let dir_entry_wrapped = self.dir[index];
let dir_entry = dir_entry_wrapped
// This should never trigger.
.expect("Attempt to access file with nonexistent index. This is a bug in gbfs_rs.");
return &self.data[dir_entry.data_offset as usize
..(dir_entry.data_offset as usize + dir_entry.len as usize)];
}
/// Returns a reference to the file data as a slice of u8's.
/// An error is returned if the file does not exist or the filename is invalid.
/// All filenames longer than `FILENAME_LEN` characters are invalid.
pub fn get_file_data_by_name(&self, str_name: &str) -> Result<&'a [u8], GBFSError> {
let name = match Filename::from(str_name) {
Ok(val) => val,
Err(_) => return Err(GBFSError::FilenameTooLong(FILENAME_LEN, str_name.len())),
};
// In this case, dir entries are stored in a fixed-size
// array using an Option to denote occupied slots.
for (i, entry) in self.dir.iter().enumerate() {
match entry {
Some(inner_entry) => {
if inner_entry.name_is_equal(name)? {
return Ok(self.get_file_data_by_index(i));
}
}
None => return Err(GBFSError::NoSuchFile(name)),
}
}
return Err(GBFSError::NoSuchFile(name));
}
/// Returns a reference to the file data as a slice of u32's.
/// An error is returned if the file does not exist, it's length is not a multiple of 2
/// or the filename is invalid.
/// All filenames longer than 24 characters are invalid.
pub fn get_file_data_by_name_as_u16_slice(&self, name: &str) -> Result<&'a [u16], GBFSError> {
return Ok(self.get_file_data_by_name(name)?.as_slice_of::<u16>()?);
}
/// Returns a reference to the file data as a slice of u32's.
/// An error is returned if the file does not exist, it's length is not a multiple of 4
/// or the filename is invalid.
/// All filenames longer than 24 characters are invalid.
pub fn get_file_data_by_name_as_u32_slice(&self, name: &str) -> Result<&'a [u32], GBFSError> {
return Ok(self.get_file_data_by_name(name)?.as_slice_of::<u32>()?);
}
}
impl<'a> IntoIterator for GBFSFilesystem<'a> {
type Item = &'a [u8];
type IntoIter = GBFSFilesystemIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
return GBFSFilesystemIterator {
fs: self,
next_file_index: 0,
};
}
}
/// Returns the data of each file in the filesystem.
pub struct GBFSFilesystemIterator<'a> {
fs: GBFSFilesystem<'a>,
next_file_index: usize,
}
impl<'a> Iterator for GBFSFilesystemIterator<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
if self.next_file_index < self.fs.hdr.dir_num_members as usize {
let ret = Some(self.fs.get_file_data_by_index(self.next_file_index));
self.next_file_index += 1;
return ret;
} else {
return None;
}
}
}