aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-10 02:56:30 +0300
committerValentin Popov <valentin@popov.link>2026-02-10 02:56:30 +0300
commit4af183ad74bfaafa0dc9db8116d361582debe536 (patch)
tree5aeb0dd5130bbaeadaf1a32968a5559aaefd6c07
parentab413bd7512257b5292539cda2d574452bae2d04 (diff)
downloadfparkan-4af183ad74bfaafa0dc9db8116d361582debe536.tar.xz
fparkan-4af183ad74bfaafa0dc9db8116d361582debe536.zip
feat: добавить новые тесты для обработки не-NRes байтов и минимальной структуры архива
-rw-r--r--crates/nres/src/tests.rs205
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";