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