1use std::collections::HashMap;
17use std::fs::File;
18use std::io::{Read, Seek};
19use std::path::Path;
20use std::sync::{Arc, Mutex};
21
22use rakata_core::{ResRef, ResourceTypeCode, StrRef};
23
24use super::layout::{self, ErfHeader};
25use super::{ErfBinaryError, ErfFileType, ErfLocalizedString, ErfReadOptions, FILE_HEADER_SIZE};
26use crate::archive::EntryDefect;
27use crate::section_reader::SectionReader;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ErfIndexEntry {
35 pub resref: ResRef,
37 pub resource_type: ResourceTypeCode,
39 pub offset: u64,
42 pub size: u64,
44 pub defect: Option<EntryDefect>,
47}
48
49impl ErfIndexEntry {
50 pub fn is_readable(&self) -> bool {
52 self.defect.is_none()
53 }
54}
55
56#[derive(Debug)]
62pub struct ErfIndex<R = File> {
63 section: SectionReader<R>,
64 file_type: ErfFileType,
65 build_year: u32,
66 build_day: u32,
67 description_strref: StrRef,
68 localized_strings: Vec<ErfLocalizedString>,
69 entries: Vec<ErfIndexEntry>,
70 lookup: HashMap<(ResRef, ResourceTypeCode), usize>,
71}
72
73impl ErfIndex<File> {
74 pub fn open(path: impl AsRef<Path>) -> Result<Self, ErfBinaryError> {
78 Self::open_with_options(path, ErfReadOptions::default())
79 }
80
81 pub fn open_with_options(
83 path: impl AsRef<Path>,
84 options: ErfReadOptions,
85 ) -> Result<Self, ErfBinaryError> {
86 let file = File::open(path)?;
87 let len = file.metadata()?.len();
88 Self::new_with_options(
89 SectionReader::whole(Arc::new(Mutex::new(file)), len),
90 options,
91 )
92 }
93}
94
95impl<R: Read + Seek> ErfIndex<R> {
96 pub fn new(section: SectionReader<R>) -> Result<Self, ErfBinaryError> {
98 Self::new_with_options(section, ErfReadOptions::default())
99 }
100
101 pub fn new_with_options(
108 section: SectionReader<R>,
109 options: ErfReadOptions,
110 ) -> Result<Self, ErfBinaryError> {
111 let header_bytes = read_region(§ion, 0, u64_from(FILE_HEADER_SIZE)?, "ERF header")?;
112 let header = ErfHeader::parse(&header_bytes, options)?;
113
114 let localized_strings = if header.language_count > 0 {
115 let block = read_region(
116 §ion,
117 u64_from(header.localized_strings_offset)?,
118 u64_from(header.localized_string_size)?,
119 "localized string block",
120 )?;
121 layout::parse_localized_strings(&block, header.language_count)?
122 } else {
123 Vec::new()
124 };
125
126 let keys_table = read_region(
127 §ion,
128 u64_from(header.keys_offset)?,
129 u64_from(header.keys_table_size()?)?,
130 "keys table",
131 )?;
132 let resources_table = read_region(
133 §ion,
134 u64_from(header.resources_offset)?,
135 u64_from(header.resources_table_size()?)?,
136 "resources 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 if key.resource_id >= header.entry_count {
144 return Err(ErfBinaryError::InvalidData(format!(
145 "keys[{key_index}] references missing resource id {}",
146 key.resource_id
147 )));
148 }
149 let (data_offset, data_size) =
150 layout::parse_resource_entry(&resources_table, key.resource_id)?;
151 let offset = u64_from(data_offset)?;
152 let size = u64_from(data_size)?;
153 let defect = EntryDefect::check_bounds(offset, size, section.len());
157
158 lookup
161 .entry((key.resref, key.resource_type))
162 .or_insert(entries.len());
163 entries.push(ErfIndexEntry {
164 resref: key.resref,
165 resource_type: key.resource_type,
166 offset,
167 size,
168 defect,
169 });
170 }
171
172 Ok(Self {
173 section,
174 file_type: header.file_type,
175 build_year: header.build_year,
176 build_day: header.build_day,
177 description_strref: header.description_strref,
178 localized_strings,
179 entries,
180 lookup,
181 })
182 }
183
184 pub fn resolve(
189 &self,
190 resref: &ResRef,
191 resource_type: ResourceTypeCode,
192 ) -> Result<Option<Vec<u8>>, ErfBinaryError> {
193 let Some(index) = self.lookup.get(&(*resref, resource_type)) else {
194 return Ok(None);
195 };
196 self.read_entry(*index).map(Some)
197 }
198
199 pub fn read_entry(&self, index: usize) -> Result<Vec<u8>, ErfBinaryError> {
204 let entry = self.entries.get(index).ok_or_else(|| {
205 ErfBinaryError::InvalidData(format!("resource index {index} is out of range"))
206 })?;
207 if let Some(defect) = &entry.defect {
208 return Err(ErfBinaryError::InvalidHeader(format!(
209 "resource data[{index}] ({}) is unreadable: {defect}",
210 entry.resref
211 )));
212 }
213 self.entry_section(index)
214 .ok_or_else(|| {
215 ErfBinaryError::InvalidHeader(format!("resource data[{index}] exceeds file bounds"))
216 })
217 .and_then(|section| {
218 section.read_all().map_err(|source| {
219 ErfBinaryError::InvalidData(format!(
220 "failed reading {} bytes for {}: {source}",
221 entry.size, entry.resref
222 ))
223 })
224 })
225 }
226
227 pub fn entry_section(&self, index: usize) -> Option<SectionReader<R>> {
234 let entry = self.entries.get(index)?;
235 if entry.defect.is_some() {
236 return None;
237 }
238 self.section.section(entry.offset, entry.size)
239 }
240
241 pub fn resource_section(
243 &self,
244 resref: &ResRef,
245 resource_type: ResourceTypeCode,
246 ) -> Option<SectionReader<R>> {
247 let index = *self.lookup.get(&(*resref, resource_type))?;
248 self.entry_section(index)
249 }
250
251 pub fn iter_resources(
256 &self,
257 ) -> impl Iterator<Item = Result<(&ErfIndexEntry, Vec<u8>), ErfBinaryError>> + '_ {
258 self.entries
259 .iter()
260 .enumerate()
261 .map(move |(index, entry)| self.read_entry(index).map(|bytes| (entry, bytes)))
262 }
263}
264
265impl<R> ErfIndex<R> {
266 pub fn entries(&self) -> &[ErfIndexEntry] {
268 &self.entries
269 }
270
271 pub fn len(&self) -> usize {
273 self.entries.len()
274 }
275
276 pub fn is_empty(&self) -> bool {
278 self.entries.is_empty()
279 }
280
281 pub fn file_type(&self) -> ErfFileType {
283 self.file_type
284 }
285
286 pub fn build_year(&self) -> u32 {
288 self.build_year
289 }
290
291 pub fn build_day(&self) -> u32 {
293 self.build_day
294 }
295
296 pub fn description_strref(&self) -> StrRef {
298 self.description_strref
299 }
300
301 pub fn localized_strings(&self) -> &[ErfLocalizedString] {
306 &self.localized_strings
307 }
308
309 pub fn contains(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> bool {
314 self.lookup.contains_key(&(*resref, resource_type))
315 }
316
317 pub fn defects(&self) -> impl Iterator<Item = (usize, &ErfIndexEntry, &EntryDefect)> + '_ {
322 self.entries
323 .iter()
324 .enumerate()
325 .filter_map(|(index, entry)| entry.defect.as_ref().map(|defect| (index, entry, defect)))
326 }
327
328 pub fn has_defects(&self) -> bool {
330 self.entries.iter().any(|entry| entry.defect.is_some())
331 }
332}
333
334fn read_region<R: Read + Seek>(
339 section: &SectionReader<R>,
340 offset: u64,
341 len: u64,
342 name: &str,
343) -> Result<Vec<u8>, ErfBinaryError> {
344 section
345 .section(offset, len)
346 .ok_or_else(|| ErfBinaryError::InvalidHeader(format!("{name} exceeds file bounds")))?
347 .read_all()
348 .map_err(ErfBinaryError::Io)
349}
350
351fn u64_from(value: usize) -> Result<u64, ErfBinaryError> {
353 u64::try_from(value)
354 .map_err(|_| ErfBinaryError::InvalidHeader("offset exceeds addressable range".into()))
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::erf::{read_erf_from_bytes, write_erf_to_vec, Erf, ErfResource};
361 use std::io::Cursor;
362
363 const TEST_ERF: &[u8] = include_bytes!(concat!(
364 env!("CARGO_MANIFEST_DIR"),
365 "/../../fixtures/test.erf"
366 ));
367
368 fn index_over(bytes: &[u8]) -> ErfIndex<Cursor<Vec<u8>>> {
369 let len = u64::try_from(bytes.len()).expect("fixture fits in u64");
370 let source = Arc::new(Mutex::new(Cursor::new(bytes.to_vec())));
371 ErfIndex::new(SectionReader::whole(source, len)).expect("fixture indexes")
372 }
373
374 fn archive_with(resources: Vec<ErfResource>) -> Vec<u8> {
375 let mut erf = Erf::new(ErfFileType::Erf);
376 erf.resources = resources;
377 write_erf_to_vec(&erf).expect("archive writes")
378 }
379
380 #[test]
381 fn indexes_the_same_resources_the_eager_reader_finds() {
382 let eager = read_erf_from_bytes(TEST_ERF).expect("fixture parses eagerly");
383 let index = index_over(TEST_ERF);
384
385 assert_eq!(index.len(), eager.resources.len());
386 assert_eq!(index.file_type(), eager.file_type);
387
388 for resource in &eager.resources {
389 let bytes = index
390 .resolve(&resource.resref, resource.resource_type)
391 .expect("resolve succeeds")
392 .expect("resource is present");
393 assert_eq!(bytes, resource.data, "bytes differ for {}", resource.resref);
394 }
395 }
396
397 #[test]
398 fn a_missing_resource_is_a_clean_miss_not_an_error() {
399 let index = index_over(TEST_ERF);
400 let absent = ResRef::new("nope").expect("valid resref");
401
402 let found = index
403 .resolve(&absent, ResourceTypeCode::from_raw_id(0xFFFF))
404 .expect("a miss is not an error");
405
406 assert!(found.is_none());
407 assert!(!index.contains(&absent, ResourceTypeCode::from_raw_id(0xFFFF)));
408 }
409
410 #[test]
411 fn iterating_yields_every_resource_with_its_bytes() {
412 let eager = read_erf_from_bytes(TEST_ERF).expect("fixture parses eagerly");
413 let index = index_over(TEST_ERF);
414
415 let collected = index
416 .iter_resources()
417 .collect::<Result<Vec<_>, _>>()
418 .expect("iteration succeeds");
419
420 assert_eq!(collected.len(), eager.resources.len());
421 for ((entry, bytes), expected) in collected.iter().zip(&eager.resources) {
422 assert_eq!(entry.resref, expected.resref);
423 assert_eq!(bytes, &expected.data);
424 }
425 }
426
427 #[test]
428 fn a_nested_archive_is_indexed_in_place() {
429 let inner_bytes = archive_with(vec![ErfResource {
431 resref: ResRef::new("buried").expect("valid resref"),
432 resource_type: ResourceTypeCode::from_raw_id(2037),
433 data: b"treasure".to_vec(),
434 }]);
435 let outer_bytes = archive_with(vec![ErfResource {
436 resref: ResRef::new("nested").expect("valid resref"),
437 resource_type: ResourceTypeCode::from_raw_id(2057),
438 data: inner_bytes.clone(),
439 }]);
440
441 let outer = index_over(&outer_bytes);
442 let window = outer
443 .resource_section(
444 &ResRef::new("nested").expect("valid resref"),
445 ResourceTypeCode::from_raw_id(2057),
446 )
447 .expect("nested window exists");
448 let inner = ErfIndex::new(window).expect("nested archive indexes");
449
450 let bytes = inner
451 .resolve(
452 &ResRef::new("buried").expect("valid resref"),
453 ResourceTypeCode::from_raw_id(2037),
454 )
455 .expect("nested resolve succeeds")
456 .expect("nested resource is present");
457
458 assert_eq!(bytes, b"treasure");
459 assert_eq!(inner.len(), 1);
461 }
462
463 #[test]
464 fn a_truncated_archive_either_fails_to_index_or_reports_defects() {
465 let truncated = &TEST_ERF[..TEST_ERF.len() / 2];
469 let len = u64::try_from(truncated.len()).expect("fits in u64");
470 let source = Arc::new(Mutex::new(Cursor::new(truncated.to_vec())));
471
472 match ErfIndex::new(SectionReader::whole(source, len)) {
473 Err(_) => {}
474 Ok(index) => assert!(
475 index.has_defects(),
476 "a truncated archive must not index clean"
477 ),
478 }
479 }
480
481 #[test]
482 fn one_out_of_bounds_entry_does_not_cost_the_whole_archive() {
483 let mut bytes = TEST_ERF.to_vec();
486 let header = crate::erf::layout::ErfHeader::parse(&bytes, ErfReadOptions::default())
487 .expect("fixture header parses");
488 let resources_offset = header.resources_offset;
489 bytes[resources_offset..resources_offset + 4].copy_from_slice(&u32::MAX.to_le_bytes());
490
491 let index = index_over(&bytes);
492 let eager_count = read_erf_from_bytes(TEST_ERF)
493 .expect("fixture parses eagerly")
494 .resources
495 .len();
496
497 assert_eq!(index.len(), eager_count);
499 assert_eq!(index.defects().count(), 1);
500 assert!(index.has_defects());
501
502 let (position, entry, _) = index.defects().next().expect("one defect");
503 assert!(!entry.is_readable());
504 assert!(index.read_entry(position).is_err());
505 assert!(index.entry_section(position).is_none());
506
507 for other in 0..index.len() {
509 if other != position {
510 assert!(
511 index.read_entry(other).is_ok(),
512 "entry {other} should still read"
513 );
514 }
515 }
516 }
517
518 #[test]
519 fn entry_metadata_is_available_without_reading_bytes() {
520 let index = index_over(TEST_ERF);
521
522 assert!(!index.is_empty());
523 for entry in index.entries() {
524 assert!(entry.offset + entry.size <= u64::try_from(TEST_ERF.len()).expect("fits"));
525 }
526 }
527}