Skip to main content

rakata_formats/rim/
index.rs

1//! Lazy RIM index: the key table in memory, entry bytes on demand.
2//!
3//! The counterpart to [`ErfIndex`](crate::erf::ErfIndex) for the other
4//! archive container KotOR modules are built from. Same trade: parse the key
5//! table once, read a resource's bytes only when asked. A module's `_s.rim`
6//! and `_a.rim` are often larger than the handful of resources any one lookup
7//! touches, so mounting one should not cost the whole file.
8
9use std::collections::HashMap;
10use std::fs::File;
11use std::io::{Read, Seek};
12use std::path::Path;
13use std::sync::{Arc, Mutex};
14
15use rakata_core::{ResRef, ResourceTypeCode};
16
17use super::layout::{self, RimHeader};
18use super::{RimBinaryError, RimReadOptions, FILE_HEADER_SIZE};
19use crate::archive::EntryDefect;
20use crate::section_reader::SectionReader;
21
22/// One indexed resource: its identity and where its bytes live.
23///
24/// Offsets are relative to the start of the archive, which for an archive
25/// nested inside another is the start of its window rather than the file.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct RimIndexEntry {
28    /// Resource name.
29    pub resref: ResRef,
30    /// Resource type code.
31    pub resource_type: ResourceTypeCode,
32    /// Byte offset of the resource data within the archive, as declared by
33    /// the key table.
34    pub offset: u64,
35    /// Byte length of the resource data, as declared by the key table.
36    pub size: u64,
37    /// Set when the entry cannot be read, leaving the rest of the archive
38    /// usable. Reading a defective entry returns this as an error.
39    pub defect: Option<EntryDefect>,
40}
41
42impl RimIndexEntry {
43    /// Returns whether this entry's bytes can be read.
44    pub fn is_readable(&self) -> bool {
45        self.defect.is_none()
46    }
47}
48
49/// A lazily-read RIM archive.
50///
51/// Holds the archive's key table and reserved header fields; resource bytes
52/// are read from the backing source on each request.
53#[derive(Debug)]
54pub struct RimIndex<R = File> {
55    section: SectionReader<R>,
56    reserved_0x08: u32,
57    reserved_0x14: u32,
58    reserved_0x18: [u8; 96],
59    entries: Vec<RimIndexEntry>,
60    lookup: HashMap<(ResRef, ResourceTypeCode), usize>,
61}
62
63impl RimIndex<File> {
64    /// Opens an archive from disk and indexes it.
65    ///
66    /// Errors when the file cannot be opened or its key table does not parse.
67    pub fn open(path: impl AsRef<Path>) -> Result<Self, RimBinaryError> {
68        Self::open_with_options(path, RimReadOptions::default())
69    }
70
71    /// Opens an archive from disk with explicit read options.
72    pub fn open_with_options(
73        path: impl AsRef<Path>,
74        options: RimReadOptions,
75    ) -> Result<Self, RimBinaryError> {
76        let file = File::open(path)?;
77        let len = file.metadata()?.len();
78        Self::new_with_options(
79            SectionReader::whole(Arc::new(Mutex::new(file)), len),
80            options,
81        )
82    }
83}
84
85impl<R: Read + Seek> RimIndex<R> {
86    /// Indexes the archive occupying `section`.
87    pub fn new(section: SectionReader<R>) -> Result<Self, RimBinaryError> {
88        Self::new_with_options(section, RimReadOptions::default())
89    }
90
91    /// Indexes the archive occupying `section` with explicit read options.
92    ///
93    /// Reads the header and key table immediately. A table that will not
94    /// parse fails here, because nothing about the archive is knowable
95    /// without it. An entry whose declared byte range escapes the archive
96    /// does not: it is marked with a defect and the rest stays readable.
97    pub fn new_with_options(
98        section: SectionReader<R>,
99        options: RimReadOptions,
100    ) -> Result<Self, RimBinaryError> {
101        let header_bytes = read_region(&section, 0, u64_from(FILE_HEADER_SIZE)?, "RIM header")?;
102        let header = RimHeader::parse(&header_bytes, options)?;
103
104        let keys_table = read_region(
105            &section,
106            u64_from(header.keys_offset)?,
107            u64_from(header.keys_table_size()?)?,
108            "keys table",
109        )?;
110
111        let mut entries = Vec::with_capacity(header.entry_count);
112        let mut lookup = HashMap::with_capacity(header.entry_count);
113        for key_index in 0..header.entry_count {
114            let key = layout::parse_key_entry(&keys_table, key_index)?;
115            let offset = u64_from(key.data_offset)?;
116            let size = u64_from(key.data_size)?;
117            // An entry pointing outside the file poisons only itself: the
118            // rest of the archive stays readable, and a caller wanting
119            // strictness inspects the defects.
120            let defect = EntryDefect::check_bounds(offset, size, section.len());
121
122            // First entry wins on a duplicate name, matching the eager
123            // reader's first-match lookup.
124            lookup
125                .entry((key.resref, key.resource_type))
126                .or_insert(entries.len());
127            entries.push(RimIndexEntry {
128                resref: key.resref,
129                resource_type: key.resource_type,
130                offset,
131                size,
132                defect,
133            });
134        }
135
136        Ok(Self {
137            section,
138            reserved_0x08: header.reserved_0x08,
139            reserved_0x14: header.reserved_0x14,
140            reserved_0x18: header.reserved_0x18,
141            entries,
142            lookup,
143        })
144    }
145
146    /// Reads the bytes of the resource named `resref` with `resource_type`.
147    ///
148    /// Returns `Ok(None)` when the archive holds no such resource, and `Err`
149    /// only when a matching entry exists but its bytes cannot be read.
150    pub fn resolve(
151        &self,
152        resref: &ResRef,
153        resource_type: ResourceTypeCode,
154    ) -> Result<Option<Vec<u8>>, RimBinaryError> {
155        let Some(index) = self.lookup.get(&(*resref, resource_type)) else {
156            return Ok(None);
157        };
158        self.read_entry(*index).map(Some)
159    }
160
161    /// Reads the bytes of the entry at `index` in table order.
162    ///
163    /// Errors when `index` is out of range, the entry carries a defect, or
164    /// the read itself fails.
165    pub fn read_entry(&self, index: usize) -> Result<Vec<u8>, RimBinaryError> {
166        let entry = self.entries.get(index).ok_or_else(|| {
167            RimBinaryError::InvalidData(format!("resource index {index} is out of range"))
168        })?;
169        if let Some(defect) = &entry.defect {
170            return Err(RimBinaryError::InvalidHeader(format!(
171                "resource data[{index}] ({}) is unreadable: {defect}",
172                entry.resref
173            )));
174        }
175        self.entry_section(index)
176            .ok_or_else(|| {
177                RimBinaryError::InvalidHeader(format!("resource data[{index}] exceeds file bounds"))
178            })
179            .and_then(|section| {
180                section.read_all().map_err(|source| {
181                    RimBinaryError::InvalidData(format!(
182                        "failed reading {} bytes for {}: {source}",
183                        entry.size, entry.resref
184                    ))
185                })
186            })
187    }
188
189    /// Returns a window over the entry's bytes without reading them.
190    ///
191    /// Returns `None` when `index` is out of range or the entry carries a
192    /// defect.
193    pub fn entry_section(&self, index: usize) -> Option<SectionReader<R>> {
194        let entry = self.entries.get(index)?;
195        if entry.defect.is_some() {
196            return None;
197        }
198        self.section.section(entry.offset, entry.size)
199    }
200
201    /// Returns a window over the named resource's bytes without reading them.
202    pub fn resource_section(
203        &self,
204        resref: &ResRef,
205        resource_type: ResourceTypeCode,
206    ) -> Option<SectionReader<R>> {
207        let index = *self.lookup.get(&(*resref, resource_type))?;
208        self.entry_section(index)
209    }
210
211    /// Iterates every resource, pairing its entry with freshly-read bytes.
212    ///
213    /// Each item is read on demand, so a caller that stops early pays only
214    /// for what it consumed.
215    pub fn iter_resources(
216        &self,
217    ) -> impl Iterator<Item = Result<(&RimIndexEntry, Vec<u8>), RimBinaryError>> + '_ {
218        self.entries
219            .iter()
220            .enumerate()
221            .map(move |(index, entry)| self.read_entry(index).map(|bytes| (entry, bytes)))
222    }
223}
224
225impl<R> RimIndex<R> {
226    /// Returns the indexed entries in table order.
227    pub fn entries(&self) -> &[RimIndexEntry] {
228        &self.entries
229    }
230
231    /// Returns the number of indexed resources.
232    pub fn len(&self) -> usize {
233        self.entries.len()
234    }
235
236    /// Returns whether the archive holds no resources.
237    pub fn is_empty(&self) -> bool {
238        self.entries.is_empty()
239    }
240
241    /// Returns whether the archive holds the named resource.
242    ///
243    /// True even when the entry is defective: the archive claims the resource
244    /// exists, it just cannot be read.
245    pub fn contains(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> bool {
246        self.lookup.contains_key(&(*resref, resource_type))
247    }
248
249    /// Iterates the entries that cannot be read, with their table positions.
250    ///
251    /// Whatever mounts this archive is expected to surface these rather than
252    /// let a partially-readable archive pass as intact.
253    pub fn defects(&self) -> impl Iterator<Item = (usize, &RimIndexEntry, &EntryDefect)> + '_ {
254        self.entries
255            .iter()
256            .enumerate()
257            .filter_map(|(index, entry)| entry.defect.as_ref().map(|d| (index, entry, d)))
258    }
259
260    /// Returns whether any entry in the archive is unreadable.
261    pub fn has_defects(&self) -> bool {
262        self.entries.iter().any(|entry| entry.defect.is_some())
263    }
264
265    /// Returns the reserved header word at `0x08`.
266    ///
267    /// Preserved so a writer can reproduce the archive's header verbatim.
268    pub fn reserved_0x08(&self) -> u32 {
269        self.reserved_0x08
270    }
271
272    /// Returns the reserved header word at `0x14`.
273    pub fn reserved_0x14(&self) -> u32 {
274        self.reserved_0x14
275    }
276
277    /// Returns the reserved header block at `0x18`.
278    pub fn reserved_0x18(&self) -> &[u8; 96] {
279        &self.reserved_0x18
280    }
281}
282
283/// Reads a named region out of the backing section.
284///
285/// Maps an out-of-bounds region onto the same "exceeds file bounds" wording
286/// the eager reader produces for the equivalent failure.
287fn read_region<R: Read + Seek>(
288    section: &SectionReader<R>,
289    offset: u64,
290    len: u64,
291    name: &str,
292) -> Result<Vec<u8>, RimBinaryError> {
293    section
294        .section(offset, len)
295        .ok_or_else(|| RimBinaryError::InvalidHeader(format!("{name} exceeds file bounds")))?
296        .read_all()
297        .map_err(RimBinaryError::Io)
298}
299
300/// Widens a table-derived `usize` to `u64` for offset arithmetic.
301fn u64_from(value: usize) -> Result<u64, RimBinaryError> {
302    u64::try_from(value)
303        .map_err(|_| RimBinaryError::InvalidHeader("offset exceeds addressable range".into()))
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::rim::read_rim_from_bytes;
310    use std::io::Cursor;
311
312    const TEST_RIM: &[u8] = include_bytes!(concat!(
313        env!("CARGO_MANIFEST_DIR"),
314        "/../../fixtures/test.rim"
315    ));
316
317    fn index_over(bytes: &[u8]) -> RimIndex<Cursor<Vec<u8>>> {
318        let len = u64::try_from(bytes.len()).expect("fixture fits in u64");
319        let source = Arc::new(Mutex::new(Cursor::new(bytes.to_vec())));
320        RimIndex::new(SectionReader::whole(source, len)).expect("fixture indexes")
321    }
322
323    #[test]
324    fn indexes_the_same_resources_the_eager_reader_finds() {
325        let eager = read_rim_from_bytes(TEST_RIM).expect("fixture parses eagerly");
326        let index = index_over(TEST_RIM);
327
328        assert_eq!(index.len(), eager.resources.len());
329
330        for resource in &eager.resources {
331            let bytes = index
332                .resolve(&resource.resref, resource.resource_type)
333                .expect("resolve succeeds")
334                .expect("resource is present");
335            assert_eq!(bytes, resource.data, "bytes differ for {}", resource.resref);
336        }
337    }
338
339    #[test]
340    fn a_missing_resource_is_a_clean_miss_not_an_error() {
341        let index = index_over(TEST_RIM);
342        let absent = ResRef::new("nope").expect("valid resref");
343
344        let found = index
345            .resolve(&absent, ResourceTypeCode::from_raw_id(0xFFFF))
346            .expect("a miss is not an error");
347
348        assert!(found.is_none());
349        assert!(!index.contains(&absent, ResourceTypeCode::from_raw_id(0xFFFF)));
350    }
351
352    #[test]
353    fn iterating_yields_every_resource_with_its_bytes() {
354        let eager = read_rim_from_bytes(TEST_RIM).expect("fixture parses eagerly");
355        let index = index_over(TEST_RIM);
356
357        let collected = index
358            .iter_resources()
359            .collect::<Result<Vec<_>, _>>()
360            .expect("iteration succeeds");
361
362        assert_eq!(collected.len(), eager.resources.len());
363        for ((entry, bytes), expected) in collected.iter().zip(&eager.resources) {
364            assert_eq!(entry.resref, expected.resref);
365            assert_eq!(bytes, &expected.data);
366        }
367    }
368
369    #[test]
370    fn reserved_header_fields_survive_indexing() {
371        let eager = read_rim_from_bytes(TEST_RIM).expect("fixture parses eagerly");
372        let index = index_over(TEST_RIM);
373
374        assert_eq!(index.reserved_0x08(), eager.reserved_0x08);
375        assert_eq!(index.reserved_0x14(), eager.reserved_0x14);
376        assert_eq!(index.reserved_0x18(), &eager.reserved_0x18);
377    }
378
379    #[test]
380    fn one_out_of_bounds_entry_does_not_cost_the_whole_archive() {
381        // Point the first resource past the end, leaving the key table
382        // intact: the salvage case a damaged archive presents.
383        let mut bytes = TEST_RIM.to_vec();
384        let header = crate::rim::layout::RimHeader::parse(&bytes, RimReadOptions::default())
385            .expect("fixture header parses");
386        let data_offset_field = header.keys_offset + 24;
387        bytes[data_offset_field..data_offset_field + 4].copy_from_slice(&u32::MAX.to_le_bytes());
388
389        let index = index_over(&bytes);
390        let eager_count = read_rim_from_bytes(TEST_RIM)
391            .expect("fixture parses eagerly")
392            .resources
393            .len();
394
395        assert_eq!(index.len(), eager_count);
396        assert_eq!(index.defects().count(), 1);
397
398        let (position, entry, _) = index.defects().next().expect("one defect");
399        assert!(!entry.is_readable());
400        assert!(index.read_entry(position).is_err());
401        assert!(index.entry_section(position).is_none());
402
403        for other in 0..index.len() {
404            if other != position {
405                assert!(index.read_entry(other).is_ok(), "entry {other} should read");
406            }
407        }
408    }
409
410    #[test]
411    fn a_truncated_archive_either_fails_to_index_or_reports_defects() {
412        // Which of the two depends on where the cut lands: losing the table
413        // makes the archive unknowable, while losing only payload leaves the
414        // table describing entries that now point past the end.
415        let truncated = &TEST_RIM[..TEST_RIM.len() / 2];
416        let len = u64::try_from(truncated.len()).expect("fits in u64");
417        let source = Arc::new(Mutex::new(Cursor::new(truncated.to_vec())));
418
419        match RimIndex::new(SectionReader::whole(source, len)) {
420            Err(_) => {}
421            Ok(index) => assert!(
422                index.has_defects(),
423                "a truncated archive must not index clean"
424            ),
425        }
426    }
427}