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> {
70 Self::open_with_options(path, RimReadOptions::default())
71 }
72
73 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 pub fn new(section: SectionReader<R>) -> Result<Self, RimBinaryError> {
100 Self::new_with_options(section, RimReadOptions::default())
101 }
102
103 pub fn new_with_options(
126 section: SectionReader<R>,
127 options: RimReadOptions,
128 ) -> Result<Self, RimBinaryError> {
129 let header_bytes = read_region(§ion, 0, u64_from(FILE_HEADER_SIZE)?, "RIM header")?;
130 let header = RimHeader::parse(&header_bytes, options)?;
131
132 let keys_table = read_region(
133 §ion,
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 let defect = EntryDefect::check_bounds(offset, size, section.len());
149
150 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 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 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 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 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 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 pub fn entries(&self) -> &[RimIndexEntry] {
265 &self.entries
266 }
267
268 pub fn len(&self) -> usize {
270 self.entries.len()
271 }
272
273 pub fn is_empty(&self) -> bool {
275 self.entries.is_empty()
276 }
277
278 pub fn contains(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> bool {
283 self.lookup.contains_key(&(*resref, resource_type))
284 }
285
286 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 pub fn has_defects(&self) -> bool {
299 self.entries.iter().any(|entry| entry.defect.is_some())
300 }
301
302 pub fn reserved_0x08(&self) -> u32 {
306 self.reserved_0x08
307 }
308
309 pub fn reserved_0x14(&self) -> u32 {
311 self.reserved_0x14
312 }
313
314 pub fn reserved_0x18(&self) -> &[u8; 96] {
316 &self.reserved_0x18
317 }
318}
319
320fn 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
337fn 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 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 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}