gcn_disk 0.5.0

Gamecube file header library and utilities.
Documentation
// 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![])
    }
}