Skip to main content

asc_hap/
resource.rs

1//! `resources.index`: the compiled resource table of a HAP/HSP, which
2//! maps resource ids to types, names and one value per qualifier set.
3//!
4//! Two layouts exist, told apart by the tool name that opens the file:
5//! `Restool <version>` (every value is its own record, found through one
6//! id table per qualifier set) and `RestoolV2 <version>` (one name table
7//! per type, then per resource the qualifier sets it has a value under).
8//! [`ResourceIndex::parse`] reads either and refuses anything else.
9//!
10//! Parsing walks the tables once and keeps slices of the input: names
11//! and values are decoded when asked for.
12
13mod qualifier;
14mod reader;
15mod v1;
16mod v2;
17mod value;
18
19use std::borrow::Cow;
20use std::fmt;
21use std::ops::Range;
22
23pub use qualifier::{Qualifier, QualifierKey, Qualifiers};
24use reader::Reader;
25pub use value::{Attr, ResourceRef, ResourceType, ResourceValue, UnknownResourceType};
26
27/// Why a `resources.index` could not be read. Offsets are file offsets.
28#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29#[non_exhaustive]
30pub enum ResourceIndexError {
31    /// The file does not open with a tool name this crate knows the
32    /// layout of.
33    #[error("unknown resource index version {version:?} (known: \"Restool <version>\", \"RestoolV2 <version>\")")]
34    UnknownVersion {
35        /// The version text of the header, read lossily.
36        version: String,
37    },
38    #[error("{what} at {offset:#x} needs {needed} bytes, {available} are left")]
39    Truncated { what: &'static str, offset: usize, needed: usize, available: usize },
40    #[error("{what} offset {offset:#x} is outside the index of {len:#x} bytes")]
41    OutOfBounds { what: &'static str, offset: u32, len: usize },
42    #[error("expected the tag {expected:?} at {offset:#x}, found {found:?}")]
43    BadTag { expected: &'static str, offset: usize, found: [u8; 4] },
44    /// A table row and the block it points at name different resources.
45    #[error("the table lists resource {listed:#010x} at {offset:#x}, where {found:#010x} is stored")]
46    IdMismatch { listed: u32, found: u32, offset: usize },
47    /// A value names a qualifier set the header does not define.
48    #[error("resource {id:#010x} has a value for qualifier set {config}, which the index does not define")]
49    UnknownQualifiers { id: u32, config: u32 },
50    /// Tables that share or repeat blocks describe more values than a
51    /// well-formed index of this size can.
52    #[error("the tables describe more than {limit} values, more than an index of this size holds")]
53    TooManyValues { limit: usize },
54}
55
56type Result<T> = std::result::Result<T, ResourceIndexError>;
57
58/// Which of the two layouts an index uses.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum IndexFormat {
61    /// `Restool <version>`.
62    V1,
63    /// `RestoolV2 <version>`.
64    V2,
65}
66
67impl IndexFormat {
68    /// Whether the stored length of a value counts a closing NUL.
69    fn terminates_values(self) -> bool {
70        match self {
71            IndexFormat::V1 => true,
72            IndexFormat::V2 => false,
73        }
74    }
75}
76
77/// One value of one resource, as slices of the input.
78#[derive(Debug, Clone, Copy)]
79struct Entry<'a> {
80    id: u32,
81    kind: ResourceType,
82    /// Position in `ResourceIndex::qualifiers`.
83    qualifiers: usize,
84    name: &'a [u8],
85    value: &'a [u8],
86}
87
88/// Collects the entries of an index under a cap, so tables that point
89/// many rows at the same block cannot multiply into unbounded work.
90struct Entries<'a> {
91    entries: Vec<Entry<'a>>,
92    limit: usize,
93}
94
95impl<'a> Entries<'a> {
96    /// Every value of a well-formed index owns a table row of 8 bytes.
97    const MIN_BYTES_PER_VALUE: usize = 8;
98
99    fn for_index(bytes: &[u8]) -> Entries<'a> {
100        Entries { entries: Vec::new(), limit: bytes.len() / Self::MIN_BYTES_PER_VALUE }
101    }
102
103    fn push(&mut self, entry: Entry<'a>) -> Result<()> {
104        if self.entries.len() >= self.limit {
105            return Err(ResourceIndexError::TooManyValues { limit: self.limit });
106        }
107        self.entries.push(entry);
108        Ok(())
109    }
110}
111
112/// What a layout's reader hands back.
113struct Tables<'a> {
114    qualifiers: Vec<Qualifiers>,
115    entries: Vec<Entry<'a>>,
116}
117
118/// A parsed `resources.index`, borrowing the bytes it was read from.
119///
120/// ```
121/// # fn demo(bytes: &[u8]) -> Result<(), asc_hap::ResourceIndexError> {
122/// use asc_hap::{ResourceIndex, ResourceRef, ResourceType};
123///
124/// let index = ResourceIndex::parse(bytes)?;
125/// // `$r('app.string.app_name')` compiled to `{ id: 0x1000000, type: 10003, .. }`.
126/// for resource in index.by_id(0x0100_0000) {
127///     println!("{resource}"); // 0x01000000 string/app_name [base] "Bridge"
128/// }
129/// // `"label": "$string:app_name"` in module.json.
130/// let label = ResourceRef::parse("$string:app_name").expect("a reference");
131/// assert!(index.resolve(label).all(|r| r.kind() == ResourceType::String));
132/// # Ok(())
133/// # }
134/// ```
135#[derive(Debug, Clone)]
136pub struct ResourceIndex<'a> {
137    version: Cow<'a, str>,
138    format: IndexFormat,
139    qualifiers: Vec<Qualifiers>,
140    /// Sorted by id, then by qualifier set in file order.
141    entries: Vec<Entry<'a>>,
142    /// Positions in `entries`, sorted by type and name.
143    by_name: Vec<usize>,
144}
145
146impl<'a> ResourceIndex<'a> {
147    /// Bytes of the version text that opens both layouts.
148    const VERSION_LEN: usize = 128;
149
150    /// Reads the tables of an index. Values are not decoded here.
151    ///
152    /// # Errors
153    ///
154    /// [`ResourceIndexError::UnknownVersion`] when the header names
155    /// neither layout; the other variants when a table is cut short,
156    /// points outside the file or contradicts another.
157    pub fn parse(bytes: &'a [u8]) -> Result<ResourceIndex<'a>> {
158        let mut reader = Reader::new(bytes);
159        let version = reader.take(Self::VERSION_LEN, "version")?;
160        let version = String::from_utf8_lossy(version.split(|&b| b == 0).next().unwrap_or_default());
161        let format = match version.split(' ').next() {
162            Some("Restool") => IndexFormat::V1,
163            Some("RestoolV2") => IndexFormat::V2,
164            _ => return Err(ResourceIndexError::UnknownVersion { version: version.into_owned() }),
165        };
166        let Tables { qualifiers, mut entries } = match format {
167            IndexFormat::V1 => v1::read(bytes, reader)?,
168            IndexFormat::V2 => v2::read(bytes, reader)?,
169        };
170        entries.sort_by_key(|e| (e.id, e.qualifiers));
171        let mut by_name: Vec<usize> = (0..entries.len()).collect();
172        by_name.sort_by_key(|&i| (entries[i].kind.code(), entries[i].name, i));
173        Ok(ResourceIndex { version, format, qualifiers, entries, by_name })
174    }
175
176    /// The version text of the header: `Restool 5.0.1.011`.
177    pub fn version(&self) -> &str {
178        &self.version
179    }
180
181    pub fn format(&self) -> IndexFormat {
182        self.format
183    }
184
185    /// The qualifier sets the index defines, in file order.
186    pub fn qualifiers(&self) -> &[Qualifiers] {
187        &self.qualifiers
188    }
189
190    /// Number of values: one per resource and qualifier set.
191    pub fn len(&self) -> usize {
192        self.entries.len()
193    }
194
195    pub fn is_empty(&self) -> bool {
196        self.entries.is_empty()
197    }
198
199    /// Every value, by id and then by qualifier set in file order.
200    pub fn iter(&self) -> Resources<'_> {
201        Resources { index: self, order: Order::ById(0..self.entries.len()) }
202    }
203
204    /// The values of the resource with this id, one per qualifier set.
205    /// The id is the `id` of a compiled `$r(...)` object and the
206    /// `labelId` / `iconId` / `descriptionId` of `module.json`.
207    pub fn by_id(&self, id: u32) -> Resources<'_> {
208        let start = self.entries.partition_point(|e| e.id < id);
209        let end = start + self.entries[start..].partition_point(|e| e.id == id);
210        Resources { index: self, order: Order::ById(start..end) }
211    }
212
213    /// The values of the resource with this type and name.
214    pub fn by_name(&self, kind: ResourceType, name: &str) -> Resources<'_> {
215        let key = (kind.code(), name.as_bytes());
216        let of = |&i: &usize| (self.entries[i].kind.code(), self.entries[i].name);
217        let start = self.by_name.partition_point(|i| of(i) < key);
218        let end = start + self.by_name[start..].partition_point(|i| of(i) == key);
219        Resources { index: self, order: Order::ByName(start..end) }
220    }
221
222    /// The values a reference points at: by name, or, for the decimal ids
223    /// that values inside the index use (`$color:16777245`), by id among
224    /// the resources of that type. A name wins over an id spelled alike.
225    pub fn resolve(&self, reference: ResourceRef<'_>) -> Resources<'_> {
226        let named = self.by_name(reference.kind, reference.name);
227        if named.len() > 0 {
228            return named;
229        }
230        match reference.name.parse() {
231            Ok(id) if self.by_id(id).all(|r| r.kind() == reference.kind) => self.by_id(id),
232            _ => named,
233        }
234    }
235}
236
237impl<'r> IntoIterator for &'r ResourceIndex<'_> {
238    type Item = Resource<'r>;
239    type IntoIter = Resources<'r>;
240
241    fn into_iter(self) -> Resources<'r> {
242        self.iter()
243    }
244}
245
246#[derive(Debug, Clone)]
247enum Order {
248    /// A range of `ResourceIndex::entries`.
249    ById(Range<usize>),
250    /// A range of `ResourceIndex::by_name`.
251    ByName(Range<usize>),
252}
253
254/// Values of an index, from [`ResourceIndex::iter`] or one of its lookups.
255#[derive(Debug, Clone)]
256pub struct Resources<'r> {
257    index: &'r ResourceIndex<'r>,
258    order: Order,
259}
260
261impl<'r> Resources<'r> {
262    fn resource(&self, position: usize) -> Resource<'r> {
263        let position = match self.order {
264            Order::ById(_) => position,
265            Order::ByName(_) => self.index.by_name[position],
266        };
267        Resource { index: self.index, entry: &self.index.entries[position] }
268    }
269
270    fn range(&mut self) -> &mut Range<usize> {
271        match &mut self.order {
272            Order::ById(range) | Order::ByName(range) => range,
273        }
274    }
275}
276
277impl<'r> Iterator for Resources<'r> {
278    type Item = Resource<'r>;
279
280    fn next(&mut self) -> Option<Resource<'r>> {
281        let position = self.range().next()?;
282        Some(self.resource(position))
283    }
284
285    fn size_hint(&self) -> (usize, Option<usize>) {
286        match &self.order {
287            Order::ById(range) | Order::ByName(range) => range.size_hint(),
288        }
289    }
290}
291
292impl DoubleEndedIterator for Resources<'_> {
293    fn next_back(&mut self) -> Option<Self::Item> {
294        let position = self.range().next_back()?;
295        Some(self.resource(position))
296    }
297}
298
299impl ExactSizeIterator for Resources<'_> {}
300
301/// One value of one resource: its id, type, name, the qualifier set the
302/// value is for, and the value.
303#[derive(Debug, Clone, Copy)]
304pub struct Resource<'r> {
305    index: &'r ResourceIndex<'r>,
306    entry: &'r Entry<'r>,
307}
308
309impl<'r> Resource<'r> {
310    pub fn id(&self) -> u32 {
311        self.entry.id
312    }
313
314    pub fn kind(&self) -> ResourceType {
315        self.entry.kind
316    }
317
318    /// The name, read lossily when it is not UTF-8. A media or profile
319    /// resource is named after its file without the extension.
320    pub fn name(&self) -> Cow<'r, str> {
321        String::from_utf8_lossy(self.entry.name)
322    }
323
324    pub fn qualifiers(&self) -> &'r Qualifiers {
325        &self.index.qualifiers[self.entry.qualifiers]
326    }
327
328    /// The stored bytes of the value, before any decoding.
329    pub fn raw_value(&self) -> &'r [u8] {
330        self.entry.value
331    }
332
333    pub fn value(&self) -> ResourceValue<'r> {
334        ResourceValue::decode(self.entry.kind, self.entry.value, self.index.format.terminates_values())
335    }
336}
337
338/// `0x01000000 string/app_name [base] "Bridge"`: id, type/name, the
339/// qualifier set as its directory name, the value on one line.
340impl fmt::Display for Resource<'_> {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        write!(f, "{:#010x} {}/{} [{}] {}", self.id(), self.kind(), self.name(), self.qualifiers(), self.value())
343    }
344}
345
346#[cfg(test)]
347mod tests;