1use std::io::{Cursor, Read};
4
5use super::layout::{self, ErfHeader};
6use super::{binary, Erf, ErfBinaryError, ErfFileType, ErfReadMode, ErfReadOptions, ErfResource};
7
8#[cfg_attr(
12 feature = "tracing",
13 tracing::instrument(level = "debug", skip(reader))
14)]
15pub fn read_erf<R: Read>(reader: &mut R) -> Result<Erf, ErfBinaryError> {
16 read_erf_with_options(reader, ErfReadOptions::default())
17}
18
19#[cfg_attr(
21 feature = "tracing",
22 tracing::instrument(level = "debug", skip(reader))
23)]
24pub fn read_erf_with_options<R: Read>(
25 reader: &mut R,
26 options: ErfReadOptions,
27) -> Result<Erf, ErfBinaryError> {
28 let mut bytes = Vec::new();
29 reader.read_to_end(&mut bytes)?;
30 crate::trace_debug!(bytes_len = bytes.len(), "read erf-family bytes from reader");
31 read_erf_from_bytes_with_options(&bytes, options)
32}
33
34#[cfg_attr(
36 feature = "tracing",
37 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
38)]
39pub fn read_erf_from_bytes(bytes: &[u8]) -> Result<Erf, ErfBinaryError> {
40 read_erf_from_bytes_with_options(bytes, ErfReadOptions::default())
41}
42
43#[cfg_attr(
45 feature = "tracing",
46 tracing::instrument(
47 level = "debug",
48 skip(bytes),
49 fields(bytes_len = bytes.len(), input_mode = ?options.input)
50 )
51)]
52pub fn read_erf_from_bytes_with_options(
53 bytes: &[u8],
54 options: ErfReadOptions,
55) -> Result<Erf, ErfBinaryError> {
56 let header = ErfHeader::parse(bytes, options)?;
57 let entry_count = header.entry_count;
58
59 let localized_strings = if header.language_count > 0 {
60 binary::check_slice_in_bounds(
61 bytes,
62 header.localized_strings_offset,
63 header.localized_string_size,
64 "localized string block",
65 )?;
66 let block = bytes
67 .get(
68 header.localized_strings_offset
69 ..header.localized_strings_offset + header.localized_string_size,
70 )
71 .ok_or_else(|| {
72 ErfBinaryError::InvalidData("localized string block is missing".into())
73 })?;
74 layout::parse_localized_strings(block, header.language_count)?
75 } else {
76 Vec::new()
77 };
78
79 let keys_table_size = header.keys_table_size()?;
80 let resources_table_size = header.resources_table_size()?;
81 binary::check_slice_in_bounds(bytes, header.keys_offset, keys_table_size, "keys table")?;
82 binary::check_slice_in_bounds(
83 bytes,
84 header.resources_offset,
85 resources_table_size,
86 "resources table",
87 )?;
88 let keys_table = bytes
89 .get(header.keys_offset..header.keys_offset + keys_table_size)
90 .ok_or_else(|| ErfBinaryError::InvalidData("keys table is missing".into()))?;
91 let resources_table = bytes
92 .get(header.resources_offset..header.resources_offset + resources_table_size)
93 .ok_or_else(|| ErfBinaryError::InvalidData("resources table is missing".into()))?;
94
95 let keys = (0..entry_count)
96 .map(|index| layout::parse_key_entry(keys_table, index))
97 .collect::<Result<Vec<_>, _>>()?;
98
99 let mut resources_meta = Vec::with_capacity(entry_count);
100 for resource_index in 0..entry_count {
101 let (data_offset, data_size) =
102 layout::parse_resource_entry(resources_table, resource_index)?;
103 binary::check_slice_in_bounds(
104 bytes,
105 data_offset,
106 data_size,
107 &format!("resource data[{resource_index}]"),
108 )?;
109 resources_meta.push((data_offset, data_size));
110 }
111
112 let mut resources = Vec::with_capacity(entry_count);
113 for (key_index, key) in keys.into_iter().enumerate() {
114 let (data_offset, data_size) = resources_meta.get(key.resource_id).ok_or_else(|| {
115 ErfBinaryError::InvalidData(format!(
116 "keys[{key_index}] references missing resource id {}",
117 key.resource_id
118 ))
119 })?;
120 let data = bytes
121 .get(*data_offset..(*data_offset + *data_size))
122 .ok_or_else(|| {
123 ErfBinaryError::InvalidData(format!(
124 "resource data slice missing for keys[{key_index}]"
125 ))
126 })?
127 .to_vec();
128 resources.push(ErfResource {
129 resref: key.resref,
130 resource_type: key.resource_type,
131 data,
132 });
133 }
134
135 let erf = Erf {
136 file_type: header.file_type,
137 build_year: header.build_year,
138 build_day: header.build_day,
139 description_strref: header.description_strref,
140 reserved: header.reserved,
141 localized_strings,
142 resources,
143 };
144 crate::trace_debug!(
145 file_type = ?erf.file_type,
146 localized_string_count = erf.localized_strings.len(),
147 resource_count = erf.resources.len(),
148 "parsed erf-family archive from bytes"
149 );
150 Ok(erf)
151}
152
153#[cfg_attr(
159 feature = "tracing",
160 tracing::instrument(level = "debug", skip(reader))
161)]
162pub fn read_save_archive<R: Read>(reader: &mut R) -> Result<Erf, ErfBinaryError> {
163 let mut erf = read_erf_with_options(
164 reader,
165 ErfReadOptions {
166 input: ErfReadMode::CompatibilityAurora,
167 },
168 )?;
169 match erf.file_type {
170 ErfFileType::Mod | ErfFileType::Sav => {
171 erf.file_type = ErfFileType::Mod;
172 Ok(erf)
173 }
174 _ => Err(ErfBinaryError::InvalidData(
175 "save archive must use MOD/SAV signature".into(),
176 )),
177 }
178}
179
180#[cfg_attr(
185 feature = "tracing",
186 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
187)]
188pub fn read_save_archive_from_bytes(bytes: &[u8]) -> Result<Erf, ErfBinaryError> {
189 let mut cursor = Cursor::new(bytes);
190 read_save_archive(&mut cursor)
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::erf::{
197 write_erf_to_vec, write_erf_to_vec_with_options, write_save_archive_to_vec,
198 ErfLocalizedString, ErfWriteMode, ErfWriteOptions, ModLayout, FILE_HEADER_SIZE,
199 KEY_ENTRY_SIZE, MOD_BLANK_BLOCK_ENTRY_SIZE,
200 };
201 use rakata_core::{LanguageId, ResRef, ResourceTypeCode, StrRef};
202
203 const TEST_ERF: &[u8] = include_bytes!(concat!(
204 env!("CARGO_MANIFEST_DIR"),
205 "/../../fixtures/test.erf"
206 ));
207 const TEST_MOD: &[u8] = include_bytes!(concat!(
208 env!("CARGO_MANIFEST_DIR"),
209 "/../../fixtures/capsule.mod"
210 ));
211
212 #[test]
213 fn parses_erf_fixture() {
214 let erf = read_erf_from_bytes(TEST_ERF).expect("fixture should parse");
215 assert_eq!(erf.file_type, ErfFileType::Erf);
216 assert_eq!(erf.resources.len(), 3);
217 assert_eq!(
218 erf.resource(
219 &ResRef::new("1").unwrap(),
220 ResourceTypeCode::from_raw_id(10)
221 ),
222 Some(b"abc".as_slice())
223 );
224 assert_eq!(
225 erf.resource(
226 &ResRef::new("2").unwrap(),
227 ResourceTypeCode::from_raw_id(10)
228 ),
229 Some(b"def".as_slice())
230 );
231 assert_eq!(
232 erf.resource(
233 &ResRef::new("3").unwrap(),
234 ResourceTypeCode::from_raw_id(10)
235 ),
236 Some(b"ghi".as_slice())
237 );
238 }
239
240 #[test]
241 fn parses_mod_fixture() {
242 let erf = read_erf_from_bytes(TEST_MOD).expect("fixture should parse");
243 assert_eq!(erf.file_type, ErfFileType::Mod);
244 assert_eq!(erf.resources.len(), 3);
245 assert_eq!(erf.resources[0].resref.as_bytes(), b"001ebo");
246 assert!(!erf.resources[0].data.is_empty());
247 }
248
249 #[test]
250 fn roundtrip_synthetic_erf_with_localized_strings() {
251 let mut erf = Erf::new(ErfFileType::Erf);
252 erf.build_year = 123;
253 erf.build_day = 42;
254 erf.description_strref = StrRef::invalid();
255 erf.localized_strings.push(ErfLocalizedString {
256 language_id: LanguageId::from_raw(0),
257 text: "Test Module".into(),
258 });
259 erf.localized_strings.push(ErfLocalizedString {
260 language_id: LanguageId::from_raw(3),
261 text: "Modulo".into(),
262 });
263 erf.push_resource(
264 ResRef::new("alpha").expect("valid resref"),
265 ResourceTypeCode::from_raw_id(2017),
266 b"abc".to_vec(),
267 );
268 erf.push_resource(
269 ResRef::new("beta").expect("valid resref"),
270 ResourceTypeCode::from_raw_id(2018),
271 b"defghi".to_vec(),
272 );
273
274 let bytes = write_erf_to_vec(&erf).expect("write should succeed");
275 let parsed = read_erf_from_bytes(&bytes).expect("read should succeed");
276 assert_eq!(parsed, erf);
277 }
278
279 #[test]
280 fn roundtrip_preserves_unknown_resource_type_ids() {
281 let mut erf = Erf::new(ErfFileType::Erf);
282 erf.push_resource(
283 ResRef::new("mystery").expect("valid resref"),
284 ResourceTypeCode::from_raw_id(42424),
285 vec![1, 2, 3],
286 );
287
288 let bytes = write_erf_to_vec(&erf).expect("write should succeed");
289 let parsed = read_erf_from_bytes(&bytes).expect("read should succeed");
290 assert_eq!(parsed.resources.len(), 1);
291 assert_eq!(parsed.resources[0].resource_type.raw_id(), 42424);
292 assert_eq!(parsed.resources[0].resource_type.known_type(), None);
293 }
294
295 #[test]
296 fn read_write_roundtrip_preserves_fixture_semantics() {
297 let parsed = read_erf_from_bytes(TEST_ERF).expect("fixture should parse");
298 let bytes = write_erf_to_vec(&parsed).expect("write should succeed");
299 let reparsed = read_erf_from_bytes(&bytes).expect("re-read should succeed");
300 assert_eq!(reparsed, parsed);
301 }
302
303 #[test]
304 fn byte_exact_roundtrip_erf_fixture() {
305 let parsed = read_erf_from_bytes(TEST_ERF).expect("fixture should parse");
306 let bytes = write_erf_to_vec(&parsed).expect("write should succeed");
307 assert_eq!(
308 bytes.as_slice(),
309 TEST_ERF,
310 "ERF roundtrip is not byte-exact"
311 );
312 }
313
314 #[test]
315 fn reserved_bytes_survive_roundtrip() {
316 let mut erf = Erf::new(ErfFileType::Erf);
317 erf.reserved[0] = 0xAB;
318 erf.reserved[57] = 0xCD;
319 erf.reserved[115] = 0xEF;
320 erf.push_resource(
321 ResRef::new("a").expect("valid resref"),
322 ResourceTypeCode::from_raw_id(10),
323 b"test".to_vec(),
324 );
325
326 let bytes = write_erf_to_vec(&erf).expect("write should succeed");
327 let parsed = read_erf_from_bytes(&bytes).expect("read should succeed");
328 assert_eq!(parsed.reserved[0], 0xAB);
329 assert_eq!(parsed.reserved[57], 0xCD);
330 assert_eq!(parsed.reserved[115], 0xEF);
331 }
332
333 #[test]
334 fn writer_is_deterministic_for_parsed_fixture() {
335 let parsed = read_erf_from_bytes(TEST_ERF).expect("fixture should parse");
336 let first = write_erf_to_vec(&parsed).expect("first write should succeed");
337 let second = write_erf_to_vec(&parsed).expect("second write should succeed");
338 assert_eq!(first, second, "canonical ERF writer output drifted");
339 }
340
341 #[test]
342 fn rejects_invalid_version() {
343 let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
344 bytes[0..4].copy_from_slice(b"ERF ");
345 bytes[4..8].copy_from_slice(b"V9.9");
346 let err = read_erf_from_bytes(&bytes).expect_err("must fail");
347 assert!(matches!(err, ErfBinaryError::InvalidVersion(_)));
348 }
349
350 #[test]
351 fn canonical_reader_rejects_v11_erf_version() {
352 let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
353 bytes[0..4].copy_from_slice(b"ERF ");
354 bytes[4..8].copy_from_slice(b"V1.1");
355 let err = read_erf_from_bytes(&bytes).expect_err("must fail");
356 assert!(matches!(err, ErfBinaryError::InvalidVersion(_)));
357 }
358
359 #[test]
360 fn compatibility_reader_accepts_v11_erf_version() {
361 let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
362 bytes[0..4].copy_from_slice(b"ERF ");
363 bytes[4..8].copy_from_slice(b"V1.1");
364 let erf = read_erf_from_bytes_with_options(
365 &bytes,
366 ErfReadOptions {
367 input: ErfReadMode::CompatibilityAurora,
368 },
369 )
370 .expect("compatibility mode should accept V1.1");
371 assert_eq!(erf.file_type, ErfFileType::Erf);
372 }
373
374 #[test]
375 fn rejects_truncated_header() {
376 let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
377 let err = read_erf_from_bytes(&bytes).expect_err("must fail");
378 assert!(matches!(err, ErfBinaryError::InvalidHeader(_)));
379 }
380
381 #[test]
382 fn rejects_out_of_range_resource_id() {
383 let mut bytes = TEST_ERF.to_vec();
384 let key_offset = usize::try_from(u32::from_le_bytes(
385 bytes[24..28].try_into().expect("key offset"),
386 ))
387 .expect("header offset fits in usize");
388 bytes[key_offset + 16..key_offset + 20].copy_from_slice(&9_u32.to_le_bytes());
389
390 let err = read_erf_from_bytes(&bytes).expect_err("must fail");
391 assert!(matches!(err, ErfBinaryError::InvalidData(_)));
392 }
393
394 #[test]
395 fn fallback_offsets_handle_zero_header_offsets() {
396 let mut bytes = TEST_ERF.to_vec();
397 bytes[24..28].copy_from_slice(&0_u32.to_le_bytes());
398 bytes[28..32].copy_from_slice(&0_u32.to_le_bytes());
399
400 let erf = read_erf_from_bytes(&bytes).expect("should parse with fallback offsets");
401 assert_eq!(erf.resources.len(), 3);
402 assert_eq!(
403 erf.resource(
404 &ResRef::new("1").unwrap(),
405 ResourceTypeCode::from_raw_id(10)
406 ),
407 Some(b"abc".as_slice())
408 );
409 }
410
411 #[test]
412 fn resref_validation_rejects_long_names() {
413 let result = ResRef::new("this_name_is_too_long");
415 assert!(result.is_err());
416 }
417
418 #[test]
419 fn canonical_writer_normalizes_sav_signature_to_mod() {
420 let mut erf = Erf::new(ErfFileType::Sav);
421 erf.description_strref = StrRef::from_raw(0);
422 erf.push_resource(
423 ResRef::new("save001").expect("valid resref"),
424 ResourceTypeCode::from_raw_id(2017),
425 vec![1, 2, 3, 4],
426 );
427
428 let bytes = write_erf_to_vec(&erf).expect("write should succeed");
429 assert_eq!(&bytes[0..4], b"MOD ");
430 let parsed = read_erf_from_bytes(&bytes).expect("canonical read should succeed");
431 assert_eq!(parsed.file_type, ErfFileType::Mod);
432 }
433
434 #[test]
435 fn writer_supports_sav_signature_in_compatibility_mode() {
436 let mut erf = Erf::new(ErfFileType::Sav);
437 erf.description_strref = StrRef::from_raw(0);
438 erf.push_resource(
439 ResRef::new("save001").expect("valid resref"),
440 ResourceTypeCode::from_raw_id(2017),
441 vec![1, 2, 3, 4],
442 );
443
444 let bytes = write_erf_to_vec_with_options(
445 &erf,
446 ErfWriteOptions {
447 output: ErfWriteMode::CompatibilityAurora,
448 ..ErfWriteOptions::default()
449 },
450 )
451 .expect("write should succeed");
452 assert_eq!(&bytes[0..4], b"SAV ");
453 let parsed = read_erf_from_bytes_with_options(
454 &bytes,
455 ErfReadOptions {
456 input: ErfReadMode::CompatibilityAurora,
457 },
458 )
459 .expect("compatibility read should succeed");
460 assert_eq!(parsed.file_type, ErfFileType::Sav);
461 }
462
463 #[test]
464 fn canonical_reader_rejects_sav_signature() {
465 let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
466 bytes[0..4].copy_from_slice(b"SAV ");
467 bytes[4..8].copy_from_slice(b"V1.0");
468 let err = read_erf_from_bytes(&bytes).expect_err("canonical mode should reject SAV");
469 assert!(matches!(err, ErfBinaryError::InvalidMagic(_)));
470 }
471
472 #[test]
473 fn canonical_reader_accepts_hak_signature() {
474 let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
475 bytes[0..4].copy_from_slice(b"HAK ");
476 bytes[4..8].copy_from_slice(b"V1.0");
477 let parsed = read_erf_from_bytes(&bytes).expect("canonical mode should parse HAK");
478 assert_eq!(parsed.file_type, ErfFileType::Hak);
479 }
480
481 #[test]
482 fn writer_defaults_to_tight_mod_layout() {
483 let mut mod_erf = Erf::new(ErfFileType::Mod);
484 mod_erf.push_resource(
485 ResRef::new("a").expect("valid resref"),
486 ResourceTypeCode::from_raw_id(10),
487 b"abc".to_vec(),
488 );
489 mod_erf.push_resource(
490 ResRef::new("b").expect("valid resref"),
491 ResourceTypeCode::from_raw_id(10),
492 b"def".to_vec(),
493 );
494 mod_erf.push_resource(
495 ResRef::new("c").expect("valid resref"),
496 ResourceTypeCode::from_raw_id(10),
497 b"ghi".to_vec(),
498 );
499
500 let bytes = write_erf_to_vec(&mod_erf).expect("write should succeed");
501 let entry_count = 3usize;
502 let keys_offset = usize::try_from(u32::from_le_bytes(
503 bytes[24..28].try_into().expect("keys offset"),
504 ))
505 .expect("header offset fits in usize");
506 let resources_offset = usize::try_from(u32::from_le_bytes(
507 bytes[28..32].try_into().expect("resources offset"),
508 ))
509 .expect("header offset fits in usize");
510
511 let expected_delta = KEY_ENTRY_SIZE * entry_count;
512 assert_eq!(resources_offset - keys_offset, expected_delta);
513 }
514
515 #[test]
516 fn writer_can_include_mod_blank_block_between_keys_and_resource_table() {
517 let mut mod_erf = Erf::new(ErfFileType::Mod);
518 mod_erf.push_resource(
519 ResRef::new("a").expect("valid resref"),
520 ResourceTypeCode::from_raw_id(10),
521 b"abc".to_vec(),
522 );
523 mod_erf.push_resource(
524 ResRef::new("b").expect("valid resref"),
525 ResourceTypeCode::from_raw_id(10),
526 b"def".to_vec(),
527 );
528 mod_erf.push_resource(
529 ResRef::new("c").expect("valid resref"),
530 ResourceTypeCode::from_raw_id(10),
531 b"ghi".to_vec(),
532 );
533
534 let bytes = write_erf_to_vec_with_options(
535 &mod_erf,
536 ErfWriteOptions {
537 mod_layout: ModLayout::WithBlankBlock,
538 ..ErfWriteOptions::default()
539 },
540 )
541 .expect("write should succeed");
542 let entry_count = 3usize;
543 let keys_offset = usize::try_from(u32::from_le_bytes(
544 bytes[24..28].try_into().expect("keys offset"),
545 ))
546 .expect("header offset fits in usize");
547 let resources_offset = usize::try_from(u32::from_le_bytes(
548 bytes[28..32].try_into().expect("resources offset"),
549 ))
550 .expect("header offset fits in usize");
551
552 let expected_delta =
553 (KEY_ENTRY_SIZE * entry_count) + (MOD_BLANK_BLOCK_ENTRY_SIZE * entry_count);
554 assert_eq!(resources_offset - keys_offset, expected_delta);
555
556 let blank_start = keys_offset + (KEY_ENTRY_SIZE * entry_count);
557 let blank_end = resources_offset;
558 assert!(bytes[blank_start..blank_end].iter().all(|byte| *byte == 0));
559 }
560
561 #[test]
562 fn writer_does_not_insert_blank_block_for_generic_erf() {
563 let mut erf = Erf::new(ErfFileType::Erf);
564 erf.push_resource(
565 ResRef::new("a").expect("valid resref"),
566 ResourceTypeCode::from_raw_id(10),
567 b"abc".to_vec(),
568 );
569 erf.push_resource(
570 ResRef::new("b").expect("valid resref"),
571 ResourceTypeCode::from_raw_id(10),
572 b"def".to_vec(),
573 );
574
575 let bytes = write_erf_to_vec(&erf).expect("write should succeed");
576 let entry_count = 2usize;
577 let keys_offset = usize::try_from(u32::from_le_bytes(
578 bytes[24..28].try_into().expect("keys offset"),
579 ))
580 .expect("header offset fits in usize");
581 let resources_offset = usize::try_from(u32::from_le_bytes(
582 bytes[28..32].try_into().expect("resources offset"),
583 ))
584 .expect("header offset fits in usize");
585
586 let expected_delta = KEY_ENTRY_SIZE * entry_count;
587 assert_eq!(resources_offset - keys_offset, expected_delta);
588 }
589
590 #[test]
591 fn save_helper_reads_mod_signature() {
592 let mut erf = Erf::new(ErfFileType::Mod);
593 erf.push_resource(
594 ResRef::new("save001").expect("valid resref"),
595 ResourceTypeCode::from_raw_id(2017),
596 vec![1, 2, 3, 4],
597 );
598 let bytes = write_erf_to_vec(&erf).expect("write should succeed");
599
600 let parsed = read_save_archive_from_bytes(&bytes).expect("save helper should parse MOD");
601 assert_eq!(parsed.file_type, ErfFileType::Mod);
602 assert_eq!(parsed.resources.len(), 1);
603 }
604
605 #[test]
606 fn save_helper_reads_sav_signature_in_compatibility_mode() {
607 let mut erf = Erf::new(ErfFileType::Sav);
608 erf.push_resource(
609 ResRef::new("save001").expect("valid resref"),
610 ResourceTypeCode::from_raw_id(2017),
611 vec![1, 2, 3, 4],
612 );
613 let bytes = write_erf_to_vec_with_options(
614 &erf,
615 ErfWriteOptions {
616 output: ErfWriteMode::CompatibilityAurora,
617 ..ErfWriteOptions::default()
618 },
619 )
620 .expect("write should succeed");
621 assert_eq!(&bytes[0..4], b"SAV ");
622
623 let parsed = read_save_archive_from_bytes(&bytes).expect("save helper should parse SAV");
624 assert_eq!(parsed.file_type, ErfFileType::Mod);
625 assert_eq!(parsed.resources.len(), 1);
626 }
627
628 #[test]
629 fn save_helper_writer_emits_canonical_mod_signature() {
630 let mut erf = Erf::new(ErfFileType::Sav);
631 erf.push_resource(
632 ResRef::new("save001").expect("valid resref"),
633 ResourceTypeCode::from_raw_id(2017),
634 vec![1, 2, 3, 4],
635 );
636
637 let bytes = write_save_archive_to_vec(&erf).expect("save helper write should succeed");
638 assert_eq!(&bytes[0..4], b"MOD ");
639
640 let parsed = read_erf_from_bytes(&bytes).expect("canonical read should succeed");
641 assert_eq!(parsed.file_type, ErfFileType::Mod);
642 }
643}