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