From bc894031347eecfe922cd83d363b0101df1b35ab Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Fri, 8 Aug 2025 11:29:47 +0100 Subject: [PATCH] Reapply "Support plugin and archive symlinks" This reverts commit da0e35bb2b66f99f566fdd4272ea42e2623a153f. The minimum version of libloadorder has been increased because 18.5.0 included some changes to support symlinked plugins, without which symlink support is incomplete. --- Cargo.toml | 2 +- src/game.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 1 - 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 20e523fd..9f95082e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true [dependencies] crc32fast = "1" esplugin = "6.1" -libloadorder = "18.4" +libloadorder = "18.5" log = { version = "0.4.6", features = ["std"] } loot-condition-interpreter = "5" petgraph = ">= 0.6, < 0.9" diff --git a/src/game.rs b/src/game.rs index 2fba71ca..ca6089b2 100644 --- a/src/game.rs +++ b/src/game.rs @@ -625,14 +625,15 @@ fn find_archives( data_path: &Path, ) -> std::io::Result> { let extension = archive_file_extension(game_type); + let allow_symlinks = allow_archive_symlinks(game_type); let mut archive_paths = Vec::new(); for path in additional_data_paths { - let paths = find_archives_in_path(path, extension)?; + let paths = find_archives_in_path(path, extension, allow_symlinks)?; archive_paths.extend(paths); } - let paths = find_archives_in_path(data_path, extension)?; + let paths = find_archives_in_path(data_path, extension, allow_symlinks)?; archive_paths.extend(paths); Ok(archive_paths) @@ -645,18 +646,29 @@ fn archive_file_extension(game_type: GameType) -> &'static str { } } +fn allow_archive_symlinks(game_type: GameType) -> bool { + // Assuming support for archive symlinks matches support for plugin symlinks. + !cfg!(windows) || matches!(game_type, GameType::OblivionRemastered | GameType::OpenMW) +} + fn find_archives_in_path( parent_path: &Path, archive_file_extension: &str, + allow_symlinks: bool, ) -> std::io::Result> { if !parent_path.exists() { return Ok(Vec::new()); } + #[expect( + clippy::filetype_is_file, + reason = "Only files are supported except in specific cases" + )] let paths = std::fs::read_dir(parent_path)? .filter_map(Result::ok) .filter(|e| { - e.file_type().map(|f| f.is_file()).unwrap_or(false) + e.file_type() + .is_ok_and(|f| f.is_file() || (allow_symlinks && f.is_symlink())) && iends_with_ascii(&e.file_name().to_string_lossy(), archive_file_extension) }) .map(|e| e.path()) @@ -2051,6 +2063,61 @@ mod tests { } } + mod find_archives { + use tempfile::tempdir; + + use super::*; + + #[cfg(windows)] + pub(crate) fn symlink_file(original: &Path, link: &Path) { + std::os::windows::fs::symlink_file(original, link).unwrap(); + } + + #[cfg(unix)] + pub(crate) fn symlink_file(original: &Path, link: &Path) { + std::os::unix::fs::symlink(original, link).unwrap(); + } + + #[cfg(unix)] + #[parameterized_test(ALL_GAME_TYPES)] + fn should_find_symlinked_archives_on_linux(game_type: GameType) { + test_find_symlinked_archives(game_type, true); + } + + #[cfg(windows)] + #[parameterized_test(ALL_GAME_TYPES)] + fn should_find_symlinked_archives_for_only_openmw_and_oblivion_remastered_on_windows( + game_type: GameType, + ) { + test_find_symlinked_archives( + game_type, + matches!(game_type, GameType::OpenMW | GameType::OblivionRemastered), + ); + } + + fn test_find_symlinked_archives(game_type: GameType, should_include_symlinks: bool) { + let tmp_dir = tempdir().unwrap(); + + let extension = archive_file_extension(game_type); + let archive_path = tmp_dir.path().join(format!("archive{extension}")); + let symlink_path = tmp_dir.path().join(format!("archive.symlink{extension}")); + + std::fs::File::create(&archive_path).unwrap(); + + symlink_file(&archive_path, &symlink_path); + + let archives = find_archives(game_type, &[], tmp_dir.path()).unwrap(); + + assert!(archives.contains(&archive_path)); + + if should_include_symlinks { + assert!(archives.contains(&symlink_path)); + } else { + assert!(!archives.contains(&symlink_path)); + } + } + } + #[test] fn to_plugin_sorting_data_should_filter_out_files_with_false_constraints() { let game_type = GameType::Oblivion; diff --git a/src/lib.rs b/src/lib.rs index 114174be..6a784c4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ // Allow some lints that are denied at the workspace level. #![allow( - clippy::filetype_is_file, clippy::must_use_candidate, clippy::missing_errors_doc, clippy::wildcard_enum_match_arm