aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-12 01:00:46 +0300
committerValentin Popov <valentin@popov.link>2026-02-12 01:00:46 +0300
commit3c06e768d603ea4452fc95cc48951af9270f76a1 (patch)
tree453f9b747788ec4b0a12cf42a3ef5f96c53c254c
parent70ed6480c2b2b2ecab4956216c1e8e85b0938b4c (diff)
downloadfparkan-3c06e768d603ea4452fc95cc48951af9270f76a1.tar.xz
fparkan-3c06e768d603ea4452fc95cc48951af9270f76a1.zip
feat: добавить поддержку атомарной замены файлов для Windows и тесты на максимальную длину имени
-rw-r--r--crates/nres/Cargo.toml3
-rw-r--r--crates/nres/src/lib.rs49
-rw-r--r--crates/nres/src/tests.rs35
-rw-r--r--crates/rsli/src/tests.rs96
4 files changed, 171 insertions, 12 deletions
diff --git a/crates/nres/Cargo.toml b/crates/nres/Cargo.toml
index 7f85352..25f3494 100644
--- a/crates/nres/Cargo.toml
+++ b/crates/nres/Cargo.toml
@@ -5,3 +5,6 @@ edition = "2021"
[dependencies]
common = { path = "../common" }
+
+[target.'cfg(windows)'.dependencies]
+windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem"] }
diff --git a/crates/nres/src/lib.rs b/crates/nres/src/lib.rs
index 1fa3b39..e0631e3 100644
--- a/crates/nres/src/lib.rs
+++ b/crates/nres/src/lib.rs
@@ -651,18 +651,43 @@ fn write_atomic(path: &Path, content: &[u8]) -> Result<()> {
file.flush()?;
drop(file);
- match fs::rename(&tmp_path, path) {
- Ok(()) => Ok(()),
- Err(rename_err) => {
- if path.exists() {
- fs::remove_file(path)?;
- fs::rename(&tmp_path, path)?;
- Ok(())
- } else {
- let _ = fs::remove_file(&tmp_path);
- Err(Error::Io(rename_err))
- }
- }
+ if let Err(err) = replace_file_atomically(&tmp_path, path) {
+ let _ = fs::remove_file(&tmp_path);
+ return Err(Error::Io(err));
+ }
+
+ Ok(())
+}
+
+#[cfg(not(windows))]
+fn replace_file_atomically(src: &Path, dst: &Path) -> std::io::Result<()> {
+ fs::rename(src, dst)
+}
+
+#[cfg(windows)]
+fn replace_file_atomically(src: &Path, dst: &Path) -> std::io::Result<()> {
+ use std::iter;
+ use std::os::windows::ffi::OsStrExt;
+ use windows_sys::Win32::Storage::FileSystem::{
+ MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
+ };
+
+ let src_wide: Vec<u16> = src.as_os_str().encode_wide().chain(iter::once(0)).collect();
+ let dst_wide: Vec<u16> = dst.as_os_str().encode_wide().chain(iter::once(0)).collect();
+
+ // Replace destination in one OS call, avoiding remove+rename gaps on Windows.
+ let ok = unsafe {
+ MoveFileExW(
+ src_wide.as_ptr(),
+ dst_wide.as_ptr(),
+ MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
+ )
+ };
+
+ if ok == 0 {
+ Err(std::io::Error::last_os_error())
+ } else {
+ Ok(())
}
}
diff --git a/crates/nres/src/tests.rs b/crates/nres/src/tests.rs
index 51ec8b4..6de02e5 100644
--- a/crates/nres/src/tests.rs
+++ b/crates/nres/src/tests.rs
@@ -771,6 +771,41 @@ fn nres_synthetic_read_find_and_edit() {
}
#[test]
+fn nres_max_name_length_roundtrip() {
+ let max_name = "12345678901234567890123456789012345";
+ assert_eq!(max_name.len(), 35);
+
+ let src = build_nres_bytes(&[SyntheticEntry {
+ kind: 9,
+ attr1: 1,
+ attr2: 2,
+ attr3: 3,
+ name: max_name,
+ data: b"payload",
+ }]);
+
+ let archive = Archive::open_bytes(Arc::from(src.into_boxed_slice()), OpenOptions::default())
+ .expect("open synthetic nres failed");
+
+ assert_eq!(archive.entry_count(), 1);
+ assert_eq!(archive.find(max_name), Some(EntryId(0)));
+ assert_eq!(
+ archive.find(&max_name.to_ascii_lowercase()),
+ Some(EntryId(0))
+ );
+
+ let entry = archive.get(EntryId(0)).expect("missing entry 0");
+ assert_eq!(entry.meta.name, max_name);
+ assert_eq!(
+ archive
+ .read(EntryId(0))
+ .expect("read payload failed")
+ .as_slice(),
+ b"payload"
+ );
+}
+
+#[test]
fn nres_find_falls_back_when_sort_index_is_out_of_range() {
let mut bytes = build_nres_bytes(&[
SyntheticEntry {
diff --git a/crates/rsli/src/tests.rs b/crates/rsli/src/tests.rs
index 33fc71b..07807d3 100644
--- a/crates/rsli/src/tests.rs
+++ b/crates/rsli/src/tests.rs
@@ -803,6 +803,102 @@ fn rsli_synthetic_all_methods_roundtrip() {
}
#[test]
+fn rsli_empty_archive_roundtrip() {
+ let bytes = build_rsli_bytes(&[], &RsliBuildOptions::default());
+ let path = write_temp_file("rsli-empty", &bytes);
+
+ let library = Library::open_path(&path).expect("open empty rsli failed");
+ assert_eq!(library.entry_count(), 0);
+ assert_eq!(library.find("ANYTHING"), None);
+
+ let rebuilt = library
+ .rebuild_from_parsed_metadata()
+ .expect("rebuild empty rsli failed");
+ assert_eq!(rebuilt, bytes, "empty rsli roundtrip mismatch");
+
+ let _ = fs::remove_file(&path);
+}
+
+#[test]
+fn rsli_max_name_length_without_nul_roundtrip() {
+ let max_name = "NAME12345678";
+ assert_eq!(max_name.len(), 12);
+
+ let bytes = build_rsli_bytes(
+ &[SyntheticRsliEntry {
+ name: max_name.to_string(),
+ method_raw: 0x000,
+ plain: b"payload".to_vec(),
+ declared_packed_size: None,
+ }],
+ &RsliBuildOptions::default(),
+ );
+ let path = write_temp_file("rsli-max-name", &bytes);
+
+ let library = Library::open_path(&path).expect("open max-name rsli failed");
+ assert_eq!(library.entry_count(), 1);
+ assert_eq!(library.find(max_name), Some(EntryId(0)));
+ assert_eq!(
+ library.find(&max_name.to_ascii_lowercase()),
+ Some(EntryId(0))
+ );
+ assert_eq!(
+ library.entries[0]
+ .name_raw
+ .iter()
+ .position(|byte| *byte == 0),
+ None,
+ "name_raw must occupy full 12 bytes without NUL"
+ );
+
+ let entry = library.get(EntryId(0)).expect("missing entry");
+ assert_eq!(entry.meta.name, max_name);
+ assert_eq!(
+ library.load(EntryId(0)).expect("load failed"),
+ b"payload",
+ "payload mismatch"
+ );
+
+ let _ = fs::remove_file(&path);
+}
+
+#[test]
+fn rsli_lzss_large_payload_over_4k_roundtrip() {
+ let plain: Vec<u8> = (0..10_000u32).map(|v| (v % 251) as u8).collect();
+ let entries = vec![
+ SyntheticRsliEntry {
+ name: "LZSS4K".to_string(),
+ method_raw: 0x040,
+ plain: plain.clone(),
+ declared_packed_size: None,
+ },
+ SyntheticRsliEntry {
+ name: "XLZS4K".to_string(),
+ method_raw: 0x060,
+ plain: plain.clone(),
+ declared_packed_size: None,
+ },
+ ];
+ let bytes = build_rsli_bytes(&entries, &RsliBuildOptions::default());
+ let path = write_temp_file("rsli-lzss-4k", &bytes);
+
+ let library = Library::open_path(&path).expect("open large-lzss rsli failed");
+ assert_eq!(library.entry_count(), entries.len());
+
+ for entry in &entries {
+ let id = library
+ .find(&entry.name)
+ .unwrap_or_else(|| panic!("find failed for {}", entry.name));
+ let loaded = library
+ .load(id)
+ .unwrap_or_else(|err| panic!("load failed for {}: {err}", entry.name));
+ assert_eq!(loaded, plain, "payload mismatch for {}", entry.name);
+ }
+
+ let _ = fs::remove_file(&path);
+}
+
+#[test]
fn rsli_find_falls_back_when_sort_table_corrupted_in_memory() {
let entries = vec![
SyntheticRsliEntry {