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