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