Skip to main content

ds_rom/rom/raw/
fnt.rs

1use std::{
2    borrow::Cow,
3    io::{self, Write},
4    mem::size_of,
5};
6
7use bytemuck::{Pod, PodCastError, Zeroable};
8use encoding_rs::SHIFT_JIS;
9use snafu::{Backtrace, Snafu};
10
11use super::RawHeaderError;
12
13/// File Name Table or FNT for short. Contains the names of every file and directory in the ROM. This is the raw struct, see
14/// the plain one [here](super::super::Files).
15pub struct Fnt<'a> {
16    /// Every directory has one subtable, indexed by `dir_id & 0xfff`
17    pub subtables: Box<[FntSubtable<'a>]>,
18}
19
20/// A directory entry in the FNT's directory list.
21#[repr(C)]
22#[derive(Clone, Copy, Zeroable, Pod)]
23pub struct FntDirectory {
24    /// Offset to this directory's subtable, which contains the names of immediate children (both files and directories).
25    pub subtable_offset: u32,
26    /// The first file ID.
27    pub first_file_id: u16,
28    /// The parent ID. If this is the root directory, this number is actually the total number of directories, as the root has
29    /// no parent anyway.
30    pub parent_id: u16,
31}
32
33/// Contains a directory's immediate children (files and folders).
34pub struct FntSubtable<'a> {
35    /// Reference to [`FntDirectory`].
36    pub directory: Cow<'a, FntDirectory>,
37    /// Raw subtable data.
38    pub data: Cow<'a, [u8]>,
39}
40
41/// Errors related to [`Fnt`].
42#[derive(Debug, Snafu)]
43pub enum RawFntError {
44    /// See [`RawHeaderError`].
45    #[snafu(transparent)]
46    RawHeader {
47        /// Source error.
48        source: RawHeaderError,
49    },
50    /// Occurs when the input it too small to fit an FNT.
51    #[snafu(display("file name table must be at least {} bytes long:\n{backtrace}", size_of::<FntDirectory>()))]
52    InvalidSize {
53        /// Backtrace to the source of the error.
54        backtrace: Backtrace,
55    },
56    /// Occurs when the input is not aligned enough.
57    #[snafu(display("expected {expected}-alignment but got {actual}-alignment:\n{backtrace}"))]
58    Misaligned {
59        /// Expected alignment.
60        expected: usize,
61        /// Actual input alignment.
62        actual: usize,
63        /// Backtrace to the source of the error.
64        backtrace: Backtrace,
65    },
66}
67
68impl<'a> Fnt<'a> {
69    fn check_size(data: &'_ [u8]) -> Result<(), RawFntError> {
70        let size = size_of::<FntDirectory>();
71        if data.len() < size {
72            InvalidSizeSnafu {}.fail()
73        } else {
74            Ok(())
75        }
76    }
77
78    fn handle_pod_cast<T>(result: Result<T, PodCastError>) -> T {
79        match result {
80            Ok(x) => x,
81            Err(PodCastError::TargetAlignmentGreaterAndInputNotAligned) => unreachable!(),
82            Err(PodCastError::AlignmentMismatch) => panic!(),
83            Err(PodCastError::OutputSliceWouldHaveSlop) => panic!(),
84            Err(PodCastError::SizeMismatch) => unreachable!(),
85        }
86    }
87
88    /// Reinterprets a `&[u8]` as an [`Fnt`].
89    ///
90    /// # Errors
91    ///
92    /// This function will return an error if the input is too small or not aligned enough.
93    pub fn borrow_from_slice(data: &'a [u8]) -> Result<Self, RawFntError> {
94        Self::check_size(data)?;
95        let addr = data as *const [u8] as *const () as usize;
96        if !addr.is_multiple_of(4) {
97            return MisalignedSnafu { expected: 4usize, actual: 1usize << addr.trailing_zeros() as usize }.fail();
98        }
99
100        let size = size_of::<FntDirectory>();
101        let root_dir: &FntDirectory = Self::handle_pod_cast(bytemuck::try_from_bytes(&data[..size]));
102
103        // the root entry has no parent, so `parent_id` is instead the number of directories
104        let num_dirs = root_dir.parent_id as usize;
105        let directories: &[FntDirectory] = Self::handle_pod_cast(bytemuck::try_cast_slice(&data[..size * num_dirs]));
106
107        let mut subtables = Vec::with_capacity(directories.len());
108        for directory in directories {
109            let start = directory.subtable_offset as usize;
110            subtables.push(FntSubtable { directory: Cow::Borrowed(directory), data: Cow::Borrowed(&data[start..]) });
111        }
112
113        Ok(Self { subtables: subtables.into_boxed_slice() })
114    }
115
116    /// Builds the FNT to be placed in a ROM.
117    ///
118    /// # Errors
119    ///
120    /// This function will return an error if an I/O operation fails.
121    pub fn build(mut self) -> Result<Box<[u8]>, io::Error> {
122        let mut bytes = vec![];
123        let mut subtable_offset = (self.subtables.len() * size_of::<FntDirectory>()) as u32;
124
125        let num_directories = self.subtables.len() as u16;
126
127        if let Some(root) = self.subtables.first_mut() {
128            // the root entry has no parent, so `parent_id` is instead the number of directories
129            root.directory.to_mut().parent_id = num_directories;
130        }
131
132        for subtable in self.subtables.iter_mut() {
133            subtable.directory.to_mut().subtable_offset = subtable_offset;
134            bytes.write_all(bytemuck::bytes_of(subtable.directory.as_ref()))?;
135            subtable_offset += subtable.data.len() as u32 + 1; // +1 for 0-byte terminator, see loop below
136        }
137
138        for subtable in self.subtables.iter() {
139            bytes.write_all(&subtable.data)?;
140            bytes.push(0);
141        }
142
143        Ok(bytes.into_boxed_slice())
144    }
145}
146
147impl FntSubtable<'_> {
148    /// Returns an iterator over all immediate children (files and directories) in this subtable.
149    pub fn iter(&self) -> IterFntSubtable<'_> {
150        IterFntSubtable { data: &self.data, id: self.directory.first_file_id }
151    }
152}
153
154/// Iterates over immediate children (files and directories) in a subtable.
155pub struct IterFntSubtable<'a> {
156    data: &'a [u8],
157    id: u16,
158}
159
160impl<'a> Iterator for IterFntSubtable<'a> {
161    type Item = FntFile<'a>;
162
163    fn next(&mut self) -> Option<Self::Item> {
164        if self.data.is_empty() || self.data[0] == 0 {
165            return None;
166        }
167
168        let length = self.data[0] as usize & 0x7f;
169        let subdir = self.data[0] & 0x80 != 0;
170        self.data = &self.data[1..];
171
172        let (name, _, had_errors) = SHIFT_JIS.decode(&self.data[..length]);
173        if had_errors {
174            log::warn!("The file name '{name}' contains a malformed byte sequence");
175        }
176
177        self.data = &self.data[length..];
178
179        let id = if subdir {
180            let id = u16::from_le_bytes([self.data[0], self.data[1]]);
181            self.data = &self.data[2..];
182            id
183        } else {
184            let id = self.id;
185            self.id += 1;
186            id
187        };
188
189        Some(FntFile { id, name })
190    }
191}
192
193/// A file/directory inside an [`FntSubtable`].
194pub struct FntFile<'a> {
195    /// File ID if less than `0xf000`, otherwise it's a directory ID.
196    pub id: u16,
197    /// File/directory name.
198    pub name: Cow<'a, str>,
199}