use std::collections::HashMap;
use std::fmt;
use std::io::{self, Read, Seek, SeekFrom, Write};
use binread::{BinReaderExt, NullString};
use itertools::Itertools;
use log::{debug, trace, warn};
use thiserror::Error;
use crate::util;
pub const U8_MAGIC: u32 = 0x55aa382d;
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ParseU8Error {
#[error("file is not a U8 archive (magic: {0:#x})")]
BadMagic(u32),
#[error("unexpected node type: {0:?}")]
UnexpectedNodeType(u8),
#[error("I/O error")]
IoError(#[from] io::Error),
}
impl From<binread::Error> for ParseU8Error {
fn from(error: binread::Error) -> ParseU8Error {
ParseU8Error::IoError(io::Error::new(io::ErrorKind::Other, error))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct U8FileNode {
pub offset: u32,
pub size: u32,
}
pub type U8FolderNode = HashMap<String, U8Node>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum U8Node {
File(U8FileNode),
Folder(U8FolderNode),
}
impl U8Node {
pub const FILE_TYPE: u8 = 0;
pub const FOLDER_TYPE: u8 = 1;
pub fn type_value(&self) -> u8 {
match self {
Self::File(_) => Self::FILE_TYPE,
Self::Folder(_) => Self::FOLDER_TYPE,
}
}
const DISPLAY_INDENT: usize = 2;
const DISPLAY_OFFSET_RIGHT_EDGE: usize = 60;
const DISPLAY_SIZE_RIGHT_EDGE: usize = 70;
fn fmt_with_indent(
&self,
f: &mut fmt::Formatter<'_>,
name: &str,
indent: usize,
) -> fmt::Result {
match self {
Self::File(U8FileNode { offset, size }) => {
write!(f, "{}{}", " ".repeat(indent), name)?;
let mut amount_written = indent + name.len();
util::write_right_aligned_str(
f,
&mut amount_written,
Self::DISPLAY_OFFSET_RIGHT_EDGE,
&format!(" {offset:#x}"),
)?;
util::write_right_aligned_str(
f,
&mut amount_written,
Self::DISPLAY_SIZE_RIGHT_EDGE,
&format!(" {size:#x}"),
)?;
}
Self::Folder(_) => {
write!(f, "{}{}/", " ".repeat(indent), name)?;
for (child_name, child) in self.iter() {
writeln!(f)?;
child.fmt_with_indent(f, child_name, indent + Self::DISPLAY_INDENT)?;
}
}
};
Ok(())
}
#[allow(dead_code)]
pub fn as_file(&self) -> Option<&U8FileNode> {
match self {
Self::File(file) => Some(file),
Self::Folder(_) => None,
}
}
#[allow(dead_code)]
pub fn as_mut_file(&mut self) -> Option<&mut U8FileNode> {
match self {
Self::File(file) => Some(file),
Self::Folder(_) => None,
}
}
#[allow(dead_code)]
pub fn as_folder(&self) -> Option<&U8FolderNode> {
match self {
Self::File(_) => None,
Self::Folder(folder) => Some(folder),
}
}
#[allow(dead_code)]
pub fn as_mut_folder(&mut self) -> Option<&mut U8FolderNode> {
match self {
Self::File(_) => None,
Self::Folder(folder) => Some(folder),
}
}
#[allow(dead_code)]
pub fn iter(&self) -> std::vec::IntoIter<(&String, &Self)> {
match self {
U8Node::File(_) => Vec::new().into_iter(),
U8Node::Folder(children) => children.iter().sorted_by_key(|x| x.0.to_lowercase()),
}
}
#[allow(dead_code)]
pub fn iter_mut(&mut self) -> std::vec::IntoIter<(&String, &mut Self)> {
match self {
U8Node::File(_) => Vec::new().into_iter(),
U8Node::Folder(children) => children.iter_mut().sorted_by_key(|x| x.0.to_lowercase()),
}
}
#[allow(dead_code)]
pub fn child(&self, name: &str) -> Option<&Self> {
if let U8Node::Folder(_) = self {
let name = name.to_lowercase();
for (child_name, child) in self.iter() {
if child_name.to_lowercase() == name {
return Some(child);
}
}
}
None
}
#[allow(dead_code)]
pub fn child_mut(&mut self, name: &str) -> Option<&mut Self> {
if let U8Node::Folder(_) = self {
let name = name.to_lowercase();
for (child_name, child) in self.iter_mut() {
if child_name.to_lowercase() == name {
return Some(child);
}
}
}
None
}
#[allow(dead_code)]
pub fn get(&self, path: &str) -> Option<&Self> {
let mut current = self;
for component in path.split('/') {
if component.is_empty() {
continue;
}
current = match current.child(component) {
Some(child) => child,
None => return None,
}
}
Some(current)
}
pub fn get_mut(&mut self, path: &str) -> Option<&mut Self> {
let mut current = self;
for component in path.split('/') {
if component.is_empty() {
continue;
}
current = match current.child_mut(component) {
Some(child) => child,
None => return None,
}
}
Some(current)
}
}
impl fmt::Display for U8Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FILENAME")?;
let mut amount_written = "FILENAME".len();
util::write_right_aligned_str(
f,
&mut amount_written,
Self::DISPLAY_OFFSET_RIGHT_EDGE,
"OFFSET",
)?;
util::write_right_aligned_str(
f,
&mut amount_written,
Self::DISPLAY_SIZE_RIGHT_EDGE + 1,
"SIZE\n",
)?;
self.fmt_with_indent(f, "", 0)
}
}
pub fn read<SR: Seek + Read>(file: &mut SR) -> Result<(U8Node, u32), ParseU8Error> {
debug!("Reading U8 FNT");
file.seek(SeekFrom::Start(0))?;
let magic: u32 = file.read_be()?;
if magic != U8_MAGIC {
return Err(ParseU8Error::BadMagic(magic));
}
file.seek(SeekFrom::Start(4))?;
let root_node_offs: u32 = file.read_be()?;
if root_node_offs != 0x20 {
warn!("Unusual root node offset: {root_node_offs:#x}");
}
trace!("root_node_offs={root_node_offs:#x}");
file.seek(SeekFrom::Current(4))?;
let data_table_offs: u32 = file.read_be()?;
trace!("data_table_offs={data_table_offs:#x}");
file.seek(SeekFrom::Start((root_node_offs + 8).into()))?;
let root_node_size: u32 = file.read_be()?;
trace!("root_node_size={root_node_size:#x}");
let string_table_offs = root_node_offs + 12 * root_node_size;
trace!("string_table_offs={string_table_offs:#x}");
fn visit_node<SR: Seek + Read>(
idx: &mut u32,
file: &mut SR,
root_node_offs: u32,
string_table_offs: u32,
data_table_offs: u32,
) -> Result<(String, U8Node), ParseU8Error> {
let my_node_idx = *idx;
let node_offs = root_node_offs + 12 * my_node_idx;
trace!("Visiting node {idx} at {node_offs:#x}");
file.seek(SeekFrom::Start(node_offs.into()))?;
let first_u32: u32 = file.read_be()?;
let node_type: u8 = (first_u32 >> 24).try_into().unwrap();
let name_offs = first_u32 & 0x00ffffff;
let data_offs: u32 = file.read_be()?;
let size: u32 = file.read_be()?;
trace!("Node {idx} header: ({node_type}, {name_offs:#x}, {data_offs:#x}, {size:#x})");
file.seek(SeekFrom::Start((string_table_offs + name_offs).into()))?;
let name = file.read_be::<NullString>()?.to_string();
trace!("Node {idx} name: {name:?}");
match node_type {
U8Node::FILE_TYPE => {
*idx += 1;
Ok((
name,
U8Node::File(U8FileNode {
offset: data_offs - data_table_offs,
size,
}),
))
}
U8Node::FOLDER_TYPE => {
let mut folder: U8FolderNode = U8FolderNode::new();
trace!("Visiting children of node {my_node_idx}");
*idx += 1;
while *idx < size {
match visit_node(
idx,
file,
root_node_offs,
string_table_offs,
data_table_offs,
) {
Ok(res_tuple) => folder.insert(res_tuple.0, res_tuple.1),
Err(why) => return Err(why),
};
}
trace!("Returning to parent dir (node {my_node_idx})");
Ok((name, U8Node::Folder(folder)))
}
_ => Err(ParseU8Error::UnexpectedNodeType(node_type)),
}
}
let res = (
visit_node(
&mut 0,
file,
root_node_offs,
string_table_offs,
data_table_offs,
)?
.1,
data_table_offs,
);
file.seek(SeekFrom::Start(data_table_offs.into()))?;
trace!("Done reading U8 FNT");
Ok(res)
}
pub fn write<SW: Seek + Write>(file: &mut SW, root: &U8Node) -> Result<(), io::Error> {
debug!("Writing U8 FNT");
let initial_file_offset = file.stream_position()?;
trace!("initial_file_offset={initial_file_offset:#x}");
file.write_all(b"U\xaa8-\0\0\0\x20\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")?;
let mut strings_table: Vec<u8> = Vec::new();
let mut file_data_offsets_to_write: Vec<(u64, u32)> = Vec::new();
fn visit_node<SW: Seek + Write>(
node: &U8Node,
name: &str,
idx: &mut u32,
recursion_depth: i32,
file: &mut SW,
strings_table: &mut Vec<u8>,
file_data_offsets_to_write: &mut Vec<(u64, u32)>,
) -> Result<(), io::Error> {
trace!("Visiting node {idx}: {name:?}");
let my_node_idx = *idx;
*idx += 1;
let my_node_offs = file.stream_position()?;
trace!("node_offs={my_node_offs:#x}");
let name_offs: u32 = strings_table.len().try_into().unwrap();
trace!("name_offs={name_offs:#x}");
strings_table.extend_from_slice(name.as_bytes());
strings_table.push(0);
file.write_all(&[0; 12])?;
let (data_offs, size) = match node {
U8Node::File(U8FileNode { offset, size }) => {
trace!(
"Adding to the fix-ups list: ({:#x}, {:#x})",
my_node_offs + 4,
*offset
);
file_data_offsets_to_write.push((my_node_offs + 4, *offset));
(0_u32, *size)
}
U8Node::Folder(_) => {
trace!("Visiting children of node {my_node_idx}");
for (child_name, child) in node.iter() {
visit_node(
child,
child_name,
idx,
recursion_depth + 1,
file,
strings_table,
file_data_offsets_to_write,
)?;
}
trace!("Returning to parent dir (node {my_node_idx})");
(recursion_depth.try_into().unwrap_or(0), *idx)
}
};
let saved_offs = file.stream_position()?;
trace!("saved_offs={saved_offs:#x}");
trace!(
"Writing new node header: ({}, {name_offs:#x}, {data_offs:#x}, {size:#x})",
node.type_value()
);
file.seek(SeekFrom::Start(my_node_offs))?;
let first_u32: u32 = (u32::from(node.type_value()) << 24) | name_offs;
file.write_all(&first_u32.to_be_bytes())?;
file.write_all(&data_offs.to_be_bytes())?;
file.write_all(&size.to_be_bytes())?;
file.seek(SeekFrom::Start(saved_offs))?;
Ok(())
}
visit_node(
root,
"",
&mut 0,
-1,
file,
&mut strings_table,
&mut file_data_offsets_to_write,
)?;
file.write_all(&strings_table)?;
let end_of_header: u32 = (file.stream_position()? - 0x20 - initial_file_offset)
.try_into()
.unwrap();
trace!("end_of_header={end_of_header:#x}");
util::write_zeros_to_align_to(file, 0x20, initial_file_offset)?;
let data_table_offset: u32 = (file.stream_position()? - initial_file_offset)
.try_into()
.unwrap();
trace!("data_table_offset={data_table_offset:#x}");
trace!(
"Fixing {} file-data offsets",
file_data_offsets_to_write.len()
);
for (offs, relative_value) in file_data_offsets_to_write {
file.seek(SeekFrom::Start(offs as u64))?;
file.write_all(&(data_table_offset + relative_value).to_be_bytes())?;
}
trace!("Writing the remaining header values");
file.seek(SeekFrom::Start(initial_file_offset + 8))?;
file.write_all(&end_of_header.to_be_bytes())?;
file.write_all(&data_table_offset.to_be_bytes())?;
file.seek(SeekFrom::Start(
initial_file_offset + u64::from(data_table_offset),
))?;
trace!("Done writing U8 FNT");
Ok(())
}
#[cfg(test)]
#[allow(clippy::unnecessary_wraps)]
mod tests {
use super::*;
use std::io::Cursor;
type TestResult = Result<(), Box<dyn std::error::Error>>;
mod u8node_display {
use super::*;
#[test]
fn test_empty_fnt() -> TestResult {
let root = U8Node::Folder(U8FolderNode::new());
assert_eq!(
format!("\n{}", root),
r"
FILENAME OFFSET SIZE
/"
);
Ok(())
}
#[test]
fn test_simple_fnt() -> TestResult {
let root = U8Node::Folder(U8FolderNode::from([
(
"a".to_owned(),
U8Node::File(U8FileNode {
offset: 0x1,
size: 0x3,
}),
),
(
"bb".to_owned(),
U8Node::Folder(U8FolderNode::from([
(
"ccc".to_owned(),
U8Node::File(U8FileNode {
offset: 0x5,
size: 0x7,
}),
),
(
"dddd".to_owned(),
U8Node::File(U8FileNode {
offset: 0x9,
size: 0xb,
}),
),
])),
),
(
"eeeee".to_owned(),
U8Node::File(U8FileNode {
offset: 0xd,
size: 0xf,
}),
),
]));
assert_eq!(
format!("\n{}", root),
r"
FILENAME OFFSET SIZE
/
a 0x1 0x3
bb/
ccc 0x5 0x7
dddd 0x9 0xb
eeeee 0xd 0xf"
);
Ok(())
}
}
mod read {
use super::*;
#[test]
fn test_empty_fnt() -> TestResult {
let mut cursor = Cursor::new(
concat_bytes!(
b"U\xaa8-\0\0\0 \0\0\0\r\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x01\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
)
.to_vec(),
);
let (root, data_table_offs) = read(&mut cursor)?;
assert_eq!(root, U8Node::Folder(U8FolderNode::new()));
assert_eq!(data_table_offs, 0x40);
Ok(())
}
#[test]
fn test_simple_fnt() -> TestResult {
let mut cursor = Cursor::new(
concat_bytes!(
b"U\xaa8-\0\0\0 \0\0\0]\0\0\0\x80\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x01\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x01\0\0\0\x81\0\0\0\x03\x01\0\0\x03\0\0\0\0",
b"\0\0\0\x05\0\0\0\x06\0\0\0\x85\0\0\0\x07\0\0\0\n\0\0\0\x89\0\0\0\x0b\0\0\0\x0f",
b"\0\0\0\x8d\0\0\0\x0f\0a\0bb\0ccc\0dddd\0eeeee\0\0\0\0",
)
.to_vec(),
);
let (root, data_table_offs) = read(&mut cursor)?;
assert_eq!(
root,
U8Node::Folder(U8FolderNode::from([
(
"a".to_owned(),
U8Node::File(U8FileNode { offset: 1, size: 3 })
),
(
"bb".to_owned(),
U8Node::Folder(U8FolderNode::from([
(
"ccc".to_owned(),
U8Node::File(U8FileNode { offset: 5, size: 7 })
),
(
"dddd".to_owned(),
U8Node::File(U8FileNode {
offset: 9,
size: 11
})
),
]))
),
(
"eeeee".to_owned(),
U8Node::File(U8FileNode {
offset: 13,
size: 15
})
),
]))
);
assert_eq!(data_table_offs, 0x80);
Ok(())
}
}
mod write {
use super::*;
fn test_writing_a_fnt(root: &U8Node, expected_output: &[u8]) -> TestResult {
for initial_offset in [0, 1, 2, 3, 4, 8, 12, 50] {
for already_written_past_there in [0, 1, 2, 3, 30, 300, 3000] {
let total_length = initial_offset + already_written_past_there;
let mut cursor = Cursor::new(vec![7; total_length]);
cursor.seek(SeekFrom::Start(initial_offset.try_into()?))?;
write(&mut cursor, root)?;
let end_of_fnt = cursor.stream_position()?.try_into()?;
assert_eq!(
&cursor.into_inner()[initial_offset..end_of_fnt],
expected_output
);
}
}
Ok(())
}
#[test]
fn test_empty_fnt() -> TestResult {
test_writing_a_fnt(
&U8Node::Folder(U8FolderNode::new()),
concat_bytes!(
b"U\xaa8-\0\0\0 \0\0\0\r\0\0\0@\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x01\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
),
)
}
#[test]
fn test_simple_fnt() -> TestResult {
test_writing_a_fnt(
&U8Node::Folder(U8FolderNode::from([
("a".to_owned(), U8Node::File(U8FileNode{offset: 1, size: 3})),
("bb".to_owned(), U8Node::Folder(U8FolderNode::from([
("ccc".to_owned(), U8Node::File(U8FileNode{offset: 5, size: 7})),
("dddd".to_owned(), U8Node::File(U8FileNode{offset: 9, size: 11})),
]))),
("eeeee".to_owned(), U8Node::File(U8FileNode{offset: 13, size: 15})),
])),
concat_bytes!(
b"U\xaa8-\0\0\0 \0\0\0]\0\0\0\x80\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
b"\x01\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x01\0\0\0\x81\0\0\0\x03\x01\0\0\x03\0\0\0\0",
b"\0\0\0\x05\0\0\0\x06\0\0\0\x85\0\0\0\x07\0\0\0\n\0\0\0\x89\0\0\0\x0b\0\0\0\x0f",
b"\0\0\0\x8d\0\0\0\x0f\0a\0bb\0ccc\0dddd\0eeeee\0\0\0\0",
),
)
}
}
}