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