restbl/
lib.rs

1//! # restbl
2//!
3//! A simple library to handle RSTB/RESTBL (resource size table) files from *The
4//! Legend of Zelda: Tears of the Kingdom*. Features:
5//! - Quick, zero-allocation parser
6//! - Optional `alloc` feature to support editable table which can be serialized to
7//!   binary or (with the `yaml` feature) YAML.
8//! - `no_std` support (optional `std` feature)
9//! - optional Serde support (`serde` feature)
10//! - `aarch64-nintendo-switch-freestanding` support (without the `std` feature)
11//!
12//! ## Example Usage
13//!
14//! ```rust
15//! use restbl::bin::ResTblReader;
16//!
17//! let bytes = std::fs::read("test/ResourceSizeTable.Product.110.rsizetable").unwrap();
18//!
19//! // Setup the quick, zero-allocation reader
20//! let reader = ResTblReader::new(bytes.as_slice()).unwrap();
21//! // Lookup an RSTB value
22//! assert_eq!(
23//!     reader.get("Bake/Scene/MainField_G_26_43.bkres"),
24//!     Some(31880)
25//! );
26//!
27//! #[cfg(feature = "alloc")]
28//! {
29//!     use restbl::ResourceSizeTable;
30//!     // Parse RSTB into owned table
31//!     let mut table = ResourceSizeTable::from_parser(&reader);
32//!     // Set the size for a resource
33//!     table.set("TexToGo/Etc_BaseCampWallWood_A_Alb.txtg", 777);
34//!     // Check the size
35//!     assert_eq!(
36//!         table.get("TexToGo/Etc_BaseCampWallWood_A_Alb.txtg"),
37//!         Some(777)
38//!     );
39//!     // Dump to YAML, if `yaml` feature enabled
40//!     #[cfg(feature = "yaml")]
41//!     {
42//!         let json_table = table.to_text();
43//!         // From YAML back to RSTB
44//!         let new_table = ResourceSizeTable::from_text(&json_table).unwrap();
45//!     }
46//! }
47//! ```
48//!
49//! ## Building for Switch
50//!
51//! To build for Switch, you will need to use the
52//! `aarch64-nintendo-switch-freestanding` target. The `std` feature is not
53//! supported, so you will need to use `--no-default-features`. Since [`cargo
54//! nx`](https://github.com/aarch64-switch-rs/cargo-nx) does not seem to support
55//! passing feature flags, you will need to run the full command yourself, as
56//! follows:
57//!
58//! ```
59//! cargo build -Z build-std=core,compiler_builtins,alloc --target aarch64-nintendo-switch-freestanding --no-default-features
60//! ```
61//!
62//! ## License
63//!
64//! This software is licensed under the terms of the GNU General Public License,
65//! version 3 or later.
66#![cfg_attr(not(any(feature = "std", test)), no_std)]
67#[cfg(feature = "alloc")]
68extern crate alloc;
69pub mod bin;
70#[cfg(feature = "yaml")]
71mod text;
72mod util;
73
74#[cfg(feature = "alloc")]
75use alloc::{
76    borrow::{Cow, ToOwned},
77    collections::BTreeMap,
78};
79#[cfg(not(feature = "alloc"))]
80pub use bin::ResTblReader;
81use thiserror_no_std::Error;
82use util::Name;
83
84/// Result type for this create
85pub type Result<T> = core::result::Result<T, Error>;
86
87/// Error type for this crate
88#[derive(Debug, Error)]
89pub enum Error {
90    #[error("Insufficient data: found {0} bytes, expected {1}")]
91    InsufficientData(usize, &'static str),
92    #[error("Invalid magic: {0:?}, expected \"RESTBL\"")]
93    InvalidMagic([u8; 6]),
94    #[error("Invalid table size: {0}, expected {1}")]
95    InvalidTableSize(usize, usize),
96    #[error(transparent)]
97    Utf8Error(#[from] core::str::Utf8Error),
98    #[error("Buffer too small for output: found {0} bytes, requires at least {1}")]
99    InsufficientBuffer(usize, usize),
100    #[cfg(feature = "std")]
101    #[error(transparent)]
102    IoError(#[from] std::io::Error),
103    #[cfg(all(feature = "alloc", feature = "yaml"))]
104    #[error("Invalid YAML line: {0}")]
105    YamlError(alloc::string::String),
106    #[cfg(feature = "yaml")]
107    #[error("Invalid number in YAML line: {0}")]
108    YamlInvalidNumber(#[from] core::num::ParseIntError),
109}
110
111/// Represents an index into the RSTB, which can be a canonical resource path or
112/// its hash
113#[derive(Debug)]
114pub enum TableIndex<'a> {
115    HashIndex(u32),
116    #[cfg(feature = "alloc")]
117    StringIndex(Cow<'a, str>),
118    #[cfg(not(feature = "alloc"))]
119    StringIndex(&'a str),
120}
121
122impl From<u32> for TableIndex<'_> {
123    fn from(value: u32) -> Self {
124        TableIndex::HashIndex(value)
125    }
126}
127
128impl<'a> From<&'a str> for TableIndex<'a> {
129    fn from(value: &'a str) -> Self {
130        #[cfg(feature = "alloc")]
131        {
132            TableIndex::StringIndex(value.into())
133        }
134        #[cfg(not(feature = "alloc"))]
135        {
136            TableIndex::StringIndex(value)
137        }
138    }
139}
140
141impl<'a> From<&'a Name> for TableIndex<'a> {
142    fn from(value: &'a Name) -> Self {
143        #[cfg(feature = "alloc")]
144        {
145            TableIndex::StringIndex(Cow::Borrowed(value.as_str()))
146        }
147        #[cfg(not(feature = "alloc"))]
148        {
149            TableIndex::StringIndex(value.as_str())
150        }
151    }
152}
153
154#[cfg(feature = "alloc")]
155impl From<Name> for TableIndex<'_> {
156    fn from(value: Name) -> Self {
157        TableIndex::StringIndex(Cow::Owned(value.as_str().to_owned()))
158    }
159}
160
161#[cfg(feature = "alloc")]
162impl From<alloc::string::String> for TableIndex<'_> {
163    fn from(value: alloc::string::String) -> Self {
164        TableIndex::StringIndex(value.into())
165    }
166}
167
168/// Data structure representing Tears of the Kingdom's resource size table
169/// (`ResourceSizeTable.Product.rsizetable.zs`). Requires the `alloc` feature.
170/// Can be serialized or deserialized to binary or (with the `text` feature) a
171/// YAML document.
172#[cfg(feature = "alloc")]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174#[derive(Debug, Default, Clone, PartialEq)]
175pub struct ResourceSizeTable {
176    pub crc_table: BTreeMap<u32, u32>,
177    pub name_table: BTreeMap<Name, u32>,
178}
179
180#[cfg(feature = "alloc")]
181impl ResourceSizeTable {
182    /// Construct an empty table
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Construct an owned table from a fast readonly parser
188    pub fn from_parser(parser: &bin::ResTblReader<'_>) -> Self {
189        let mut crc_table = BTreeMap::new();
190        let mut name_table = BTreeMap::new();
191        for entry in parser.iter() {
192            match entry {
193                bin::TableEntry::Hash(entry) => crc_table.insert(entry.hash(), entry.value()),
194                bin::TableEntry::Name(entry) => name_table.insert(entry.name(), entry.value()),
195            };
196        }
197        ResourceSizeTable {
198            crc_table,
199            name_table,
200        }
201    }
202
203    /// Get the total number of hash and name entries in the table
204    #[inline(always)]
205    pub fn len(&self) -> usize {
206        self.crc_table.len() + self.name_table.len()
207    }
208
209    #[inline(always)]
210    pub fn is_empty(&self) -> bool {
211        self.len() == 0
212    }
213
214    /// Check if the specified hash or resource name is present in the table.
215    /// Checks the name table first (if applicable) and then the hash table.
216    pub fn contains<'i, I: Into<TableIndex<'i>>>(&self, needle: I) -> bool {
217        fn inner(tbl: &ResourceSizeTable, needle: TableIndex) -> bool {
218            match needle {
219                TableIndex::HashIndex(hash) => tbl.crc_table.contains_key(&hash),
220                TableIndex::StringIndex(name) => {
221                    tbl.name_table.contains_key(&Name::from(name.as_ref())) || {
222                        let hash = util::hash_name(&name);
223                        tbl.crc_table.contains_key(&hash)
224                    }
225                }
226            }
227        }
228        inner(self, needle.into())
229    }
230
231    /// Returns the RSTB value for the specified hash or resource name if
232    /// present. Checks the name table first (if applicable) and then the hash
233    /// table.
234    pub fn get<'i, I: Into<TableIndex<'i>>>(&self, needle: I) -> Option<u32> {
235        fn inner(tbl: &ResourceSizeTable, needle: TableIndex) -> Option<u32> {
236            match needle {
237                TableIndex::HashIndex(hash) => tbl.crc_table.get(&hash),
238                TableIndex::StringIndex(name) => {
239                    tbl.name_table.get(&Name::from(name.as_ref())).or_else(|| {
240                        let hash = util::hash_name(&name);
241                        tbl.crc_table.get(&hash)
242                    })
243                }
244            }
245            .copied()
246        }
247        inner(self, needle.into())
248    }
249
250    /// Returns a mutable reference to the RSTB value for the specified hash or
251    /// resource name if present. Checks the name table first (if applicable)
252    /// and then the hash table.
253    pub fn get_mut<'i, I: Into<TableIndex<'i>>>(&mut self, needle: I) -> Option<&mut u32> {
254        fn inner<'a>(
255            tbl: &'a mut ResourceSizeTable,
256            needle: TableIndex<'_>,
257        ) -> Option<&'a mut u32> {
258            match needle {
259                TableIndex::HashIndex(hash) => tbl.crc_table.get_mut(&hash),
260                TableIndex::StringIndex(name) => tbl
261                    .name_table
262                    .get_mut(&Name::from(name.as_ref()))
263                    .or_else(|| {
264                        let hash = util::hash_name(&name);
265                        tbl.crc_table.get_mut(&hash)
266                    }),
267            }
268        }
269        inner(self, needle.into())
270    }
271
272    /// Set the RSTB value for the specified hash or resource name, returning
273    /// the original value if present. Checks the name table first (if
274    /// applicable) and then the hash table.
275    pub fn set<'i, I: Into<TableIndex<'i>>>(&mut self, res: I, value: u32) -> Option<u32> {
276        fn inner(tbl: &mut ResourceSizeTable, needle: TableIndex, value: u32) -> Option<u32> {
277            match needle {
278                TableIndex::HashIndex(hash) => tbl.crc_table.insert(hash, value),
279                TableIndex::StringIndex(name) => {
280                    match tbl.name_table.entry(Name::from(name.as_ref())) {
281                        alloc::collections::btree_map::Entry::Occupied(mut e) => {
282                            Some(e.insert(value))
283                        }
284                        alloc::collections::btree_map::Entry::Vacant(_) => {
285                            let hash = util::hash_name(&name);
286                            tbl.crc_table.insert(hash, value)
287                        }
288                    }
289                }
290            }
291        }
292        inner(self, res.into(), value)
293    }
294
295    /// Remove the RSTB value for the specified hash or resource name, returning
296    /// the original value if present. Checks the name table first (if
297    /// applicable) and then the hash table.
298    pub fn remove<'i, I: Into<TableIndex<'i>>>(&mut self, res: I) -> Option<u32> {
299        fn inner(tbl: &mut ResourceSizeTable, needle: TableIndex<'_>) -> Option<u32> {
300            match needle {
301                TableIndex::HashIndex(hash) => tbl.crc_table.remove(&hash),
302                TableIndex::StringIndex(name) => tbl
303                    .name_table
304                    .remove(&Name::from(name.as_ref()))
305                    .or_else(|| {
306                        let hash = util::hash_name(&name);
307                        tbl.crc_table.remove(&hash)
308                    }),
309            }
310        }
311        inner(self, res.into())
312    }
313
314    /// Set multiple RSTB entries from an iterator
315    pub fn extend<'i, N: Into<TableIndex<'i>>, I: Iterator<Item = (N, u32)>>(&mut self, iter: I) {
316        fn inner<'i, I: Iterator<Item = (TableIndex<'i>, u32)>>(
317            tbl: &mut ResourceSizeTable,
318            iter: I,
319        ) {
320            for (k, v) in iter {
321                tbl.set(k, v);
322            }
323        }
324        inner(self, iter.map(|(k, v)| (k.into(), v)))
325    }
326}
327
328#[cfg(test)]
329mod test {
330    pub(crate) static DATA: &[u8] =
331        include_bytes!("../test/ResourceSizeTable.Product.110.rsizetable");
332}