diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-10 02:56:30 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-10 02:56:30 +0300 |
| commit | 4af183ad74bfaafa0dc9db8116d361582debe536 (patch) | |
| tree | 5aeb0dd5130bbaeadaf1a32968a5559aaefd6c07 /crates/nres | |
| parent | ab413bd7512257b5292539cda2d574452bae2d04 (diff) | |
| download | fparkan-4af183ad74bfaafa0dc9db8116d361582debe536.tar.xz fparkan-4af183ad74bfaafa0dc9db8116d361582debe536.zip | |
feat: добавить новые тесты для обработки не-NRes байтов и минимальной структуры архива
Diffstat (limited to 'crates/nres')
| -rw-r--r-- | crates/nres/src/tests.rs | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/crates/nres/src/tests.rs b/crates/nres/src/tests.rs index 76e2b01..47aad53 100644 --- a/crates/nres/src/tests.rs +++ b/crates/nres/src/tests.rs @@ -73,6 +73,14 @@ fn panic_message(payload: Box<dyn Any + Send>) -> String { String::from("panic without message") } +fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { + let slice = bytes + .get(offset..offset + 4) + .expect("u32 read out of bounds in test"); + let arr: [u8; 4] = slice.try_into().expect("u32 conversion failed in test"); + u32::from_le_bytes(arr) +} + fn build_nres_bytes(entries: &[SyntheticEntry<'_>]) -> Vec<u8> { let mut out = vec![0u8; 16]; let mut offsets = Vec::with_capacity(entries.len()); @@ -301,6 +309,203 @@ fn nres_raw_mode_exposes_whole_file() { } #[test] +fn nres_raw_mode_accepts_non_nres_bytes() { + let payload = b"not-an-nres-archive".to_vec(); + let bytes: Arc<[u8]> = Arc::from(payload.clone().into_boxed_slice()); + + match Archive::open_bytes(bytes.clone(), OpenOptions::default()) { + Err(Error::InvalidMagic { .. }) => {} + other => panic!("expected InvalidMagic without raw_mode, got {other:?}"), + } + + let archive = Archive::open_bytes( + bytes, + OpenOptions { + raw_mode: true, + sequential_hint: false, + prefetch_pages: false, + }, + ) + .expect("raw_mode should accept any bytes"); + + assert_eq!(archive.entry_count(), 1); + assert_eq!(archive.find("raw"), Some(EntryId(0))); + assert_eq!( + archive + .read(EntryId(0)) + .expect("raw read failed") + .as_slice(), + payload.as_slice() + ); +} + +#[test] +fn nres_open_options_hints_do_not_change_payload() { + let payload: Vec<u8> = (0..70_000u32).map(|v| (v % 251) as u8).collect(); + let src = build_nres_bytes(&[SyntheticEntry { + kind: 7, + attr1: 70, + attr2: 700, + attr3: 7000, + name: "big.bin", + data: &payload, + }]); + let arc: Arc<[u8]> = Arc::from(src.into_boxed_slice()); + + let baseline = Archive::open_bytes(arc.clone(), OpenOptions::default()) + .expect("baseline open should succeed"); + let hinted = Archive::open_bytes( + arc, + OpenOptions { + raw_mode: false, + sequential_hint: true, + prefetch_pages: true, + }, + ) + .expect("open with hints should succeed"); + + assert_eq!(baseline.entry_count(), 1); + assert_eq!(hinted.entry_count(), 1); + assert_eq!(baseline.find("BIG.BIN"), Some(EntryId(0))); + assert_eq!(hinted.find("big.bin"), Some(EntryId(0))); + assert_eq!( + baseline + .read(EntryId(0)) + .expect("baseline read failed") + .as_slice(), + hinted + .read(EntryId(0)) + .expect("hinted read failed") + .as_slice() + ); +} + +#[test] +fn nres_commit_empty_archive_has_minimal_layout() { + let mut path = std::env::temp_dir(); + path.push(format!( + "nres-empty-commit-{}-{}.lib", + std::process::id(), + unix_time_nanos() + )); + fs::write(&path, build_nres_bytes(&[])).expect("write empty archive failed"); + + Archive::edit_path(&path) + .expect("edit_path failed for empty archive") + .commit() + .expect("commit failed for empty archive"); + + let bytes = fs::read(&path).expect("failed to read committed archive"); + assert_eq!(bytes.len(), 16, "empty archive must contain only header"); + assert_eq!(&bytes[0..4], b"NRes"); + assert_eq!(read_u32_le(&bytes, 4), 0x100); + assert_eq!(read_u32_le(&bytes, 8), 0); + assert_eq!(read_u32_le(&bytes, 12), 16); + + let _ = fs::remove_file(&path); +} + +#[test] +fn nres_commit_recomputes_header_directory_and_sort_table() { + let mut path = std::env::temp_dir(); + path.push(format!( + "nres-commit-layout-{}-{}.lib", + std::process::id(), + unix_time_nanos() + )); + fs::write(&path, build_nres_bytes(&[])).expect("write empty archive failed"); + + let mut editor = Archive::edit_path(&path).expect("edit_path failed"); + editor + .add(NewEntry { + kind: 10, + attr1: 1, + attr2: 2, + attr3: 3, + name: "Zulu", + data: b"aaaaa", + }) + .expect("add #0 failed"); + editor + .add(NewEntry { + kind: 11, + attr1: 4, + attr2: 5, + attr3: 6, + name: "alpha", + data: b"bbbbbbbb", + }) + .expect("add #1 failed"); + editor + .add(NewEntry { + kind: 12, + attr1: 7, + attr2: 8, + attr3: 9, + name: "Beta", + data: b"cccc", + }) + .expect("add #2 failed"); + editor.commit().expect("commit failed"); + + let bytes = fs::read(&path).expect("failed to read committed archive"); + assert_eq!(&bytes[0..4], b"NRes"); + assert_eq!(read_u32_le(&bytes, 4), 0x100); + + let entry_count = usize::try_from(read_u32_le(&bytes, 8)).expect("entry_count overflow"); + let total_size = usize::try_from(read_u32_le(&bytes, 12)).expect("total_size overflow"); + assert_eq!(entry_count, 3); + assert_eq!(total_size, bytes.len()); + + let directory_offset = total_size + .checked_sub(entry_count * 64) + .expect("invalid directory offset"); + assert!(directory_offset >= 16); + + let mut sort_indices = Vec::new(); + let mut prev_data_end = 16usize; + for idx in 0..entry_count { + let base = directory_offset + idx * 64; + let data_size = usize::try_from(read_u32_le(&bytes, base + 12)).expect("size overflow"); + let data_offset = usize::try_from(read_u32_le(&bytes, base + 56)).expect("offset overflow"); + let sort_index = + usize::try_from(read_u32_le(&bytes, base + 60)).expect("sort index overflow"); + + assert_eq!( + data_offset % 8, + 0, + "entry #{idx} data offset must be 8-byte aligned" + ); + assert!( + data_offset >= prev_data_end, + "entry #{idx} offset regressed" + ); + assert!( + data_offset + data_size <= directory_offset, + "entry #{idx} overlaps directory" + ); + prev_data_end = data_offset + data_size; + sort_indices.push(sort_index); + } + + let names = ["Zulu", "alpha", "Beta"]; + let mut expected_sort: Vec<usize> = (0..names.len()).collect(); + expected_sort + .sort_by(|a, b| cmp_name_case_insensitive(names[*a].as_bytes(), names[*b].as_bytes())); + assert_eq!( + sort_indices, expected_sort, + "sort table must contain original indexes in case-insensitive alphabetical order" + ); + + let archive = Archive::open_path(&path).expect("re-open failed"); + assert_eq!(archive.find("zulu"), Some(EntryId(0))); + assert_eq!(archive.find("ALPHA"), Some(EntryId(1))); + assert_eq!(archive.find("beta"), Some(EntryId(2))); + + let _ = fs::remove_file(&path); +} + +#[test] fn nres_synthetic_read_find_and_edit() { let payload_a = b"alpha"; let payload_b = b"B"; |
