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
13pub struct Fnt<'a> {
16 pub subtables: Box<[FntSubtable<'a>]>,
18}
19
20#[repr(C)]
22#[derive(Clone, Copy, Zeroable, Pod)]
23pub struct FntDirectory {
24 pub subtable_offset: u32,
26 pub first_file_id: u16,
28 pub parent_id: u16,
31}
32
33pub struct FntSubtable<'a> {
35 pub directory: Cow<'a, FntDirectory>,
37 pub data: Cow<'a, [u8]>,
39}
40
41#[derive(Debug, Snafu)]
43pub enum RawFntError {
44 #[snafu(transparent)]
46 RawHeader {
47 source: RawHeaderError,
49 },
50 #[snafu(display("file name table must be at least {} bytes long:\n{backtrace}", size_of::<FntDirectory>()))]
52 InvalidSize {
53 backtrace: Backtrace,
55 },
56 #[snafu(display("expected {expected}-alignment but got {actual}-alignment:\n{backtrace}"))]
58 Misaligned {
59 expected: usize,
61 actual: usize,
63 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 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 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 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 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; }
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 pub fn iter(&self) -> IterFntSubtable<'_> {
150 IterFntSubtable { data: &self.data, id: self.directory.first_file_id }
151 }
152}
153
154pub 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
193pub struct FntFile<'a> {
195 pub id: u16,
197 pub name: Cow<'a, str>,
199}