1use 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#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct RimIndexEntry {
28 pub resref: ResRef,
30 pub resource_type: ResourceTypeCode,
32 pub offset: u64,
35 pub size: u64,
37 pub defect: Option<EntryDefect>,
40}
41
42impl RimIndexEntry {
43 pub fn is_readable(&self) -> bool {
45 self.defect.is_none()
46 }
47}
48
49#[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 pub fn open(path: impl AsRef<Path>) -> Result<Self, RimBinaryError> {
68 Self::open_with_options(path, RimReadOptions::default())
69 }
70
71 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 pub fn new(section: SectionReader<R>) -> Result<Self, RimBinaryError> {
88 Self::new_with_options(section, RimReadOptions::default())
89 }
90
91 pub fn new_with_options(
98 section: SectionReader<R>,
99 options: RimReadOptions,
100 ) -> Result<Self, RimBinaryError> {
101 let header_bytes = read_region(§ion, 0, u64_from(FILE_HEADER_SIZE)?, "RIM header")?;
102 let header = RimHeader::parse(&header_bytes, options)?;
103
104 let keys_table = read_region(
105 §ion,
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 let defect = EntryDefect::check_bounds(offset, size, section.len());
121
122 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 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 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 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 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 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 pub fn entries(&self) -> &[RimIndexEntry] {
228 &self.entries
229 }
230
231 pub fn len(&self) -> usize {
233 self.entries.len()
234 }
235
236 pub fn is_empty(&self) -> bool {
238 self.entries.is_empty()
239 }
240
241 pub fn contains(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> bool {
246 self.lookup.contains_key(&(*resref, resource_type))
247 }
248
249 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 pub fn has_defects(&self) -> bool {
262 self.entries.iter().any(|entry| entry.defect.is_some())
263 }
264
265 pub fn reserved_0x08(&self) -> u32 {
269 self.reserved_0x08
270 }
271
272 pub fn reserved_0x14(&self) -> u32 {
274 self.reserved_0x14
275 }
276
277 pub fn reserved_0x18(&self) -> &[u8; 96] {
279 &self.reserved_0x18
280 }
281}
282
283fn 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
300fn 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 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 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}