From 64a8cb2fb5415e210adf97c661eda404b2ee8036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl?= Date: Wed, 25 Mar 2026 12:05:28 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20tolerate=20multiple=20case-sensitiv?= =?UTF-8?q?e=20matches=20is=20they=20are=20the=20same=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modda-lib/Cargo.toml | 1 + .../resources/test/file_lookup/conflicts/MOD1 | 1 + .../resources/test/file_lookup/conflicts/MOD2 | 1 + .../resources/test/file_lookup/conflicts/mod1 | 1 + .../resources/test/file_lookup/conflicts/mod2 | 1 + modda-lib/src/download.rs | 2 +- modda-lib/src/utils/insensitive.rs | 54 +++++++++++++++++-- 7 files changed, 55 insertions(+), 6 deletions(-) create mode 120000 modda-lib/resources/test/file_lookup/conflicts/MOD1 create mode 100644 modda-lib/resources/test/file_lookup/conflicts/MOD2 create mode 100644 modda-lib/resources/test/file_lookup/conflicts/mod1 create mode 100644 modda-lib/resources/test/file_lookup/conflicts/mod2 diff --git a/modda-lib/Cargo.toml b/modda-lib/Cargo.toml index b1d564e..3cc67b1 100644 --- a/modda-lib/Cargo.toml +++ b/modda-lib/Cargo.toml @@ -38,6 +38,7 @@ path-absolutize = "3.1.1" percent-encoding = "2.3.2" regex = "1.12.3" reqwest = { version = "0.13.2", features = ["stream", "json"] } +same-file = "1.0.6" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" serde_path_to_error = "0.1.20" diff --git a/modda-lib/resources/test/file_lookup/conflicts/MOD1 b/modda-lib/resources/test/file_lookup/conflicts/MOD1 new file mode 120000 index 0000000..24eb79d --- /dev/null +++ b/modda-lib/resources/test/file_lookup/conflicts/MOD1 @@ -0,0 +1 @@ +mod1 \ No newline at end of file diff --git a/modda-lib/resources/test/file_lookup/conflicts/MOD2 b/modda-lib/resources/test/file_lookup/conflicts/MOD2 new file mode 100644 index 0000000..f6531e1 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/conflicts/MOD2 @@ -0,0 +1 @@ +This is NOT a symlink to mod2 diff --git a/modda-lib/resources/test/file_lookup/conflicts/mod1 b/modda-lib/resources/test/file_lookup/conflicts/mod1 new file mode 100644 index 0000000..4f9f8e3 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/conflicts/mod1 @@ -0,0 +1 @@ +MOD1 is a symlink to mod1 diff --git a/modda-lib/resources/test/file_lookup/conflicts/mod2 b/modda-lib/resources/test/file_lookup/conflicts/mod2 new file mode 100644 index 0000000..67baeb0 --- /dev/null +++ b/modda-lib/resources/test/file_lookup/conflicts/mod2 @@ -0,0 +1 @@ +MOD2 is NOT a symlink to mod2 diff --git a/modda-lib/src/download.rs b/modda-lib/src/download.rs index 64416e0..5dbc1d2 100644 --- a/modda-lib/src/download.rs +++ b/modda-lib/src/download.rs @@ -89,7 +89,7 @@ impl Downloader { // Indicatif setup - let pb = match (total_size) { + let pb = match total_size { Some(total_size) => Self::progress_with_size(total_size)?, None => match &download_opts.size_hint{ None =>{ diff --git a/modda-lib/src/utils/insensitive.rs b/modda-lib/src/utils/insensitive.rs index 371c73b..665fc12 100644 --- a/modda-lib/src/utils/insensitive.rs +++ b/modda-lib/src/utils/insensitive.rs @@ -3,7 +3,9 @@ use std::io::ErrorKind; use std::path::{Path, PathBuf}; use anyhow::{Result, bail}; -use log::debug; +use itertools::Itertools; +use log::{debug, info}; +use same_file::Handle; /// Looks for a file matching the argument, allowing case-insensitive matches. /// @@ -20,10 +22,15 @@ pub fn find_insensitive

(base: P, searched: &str) -> Result> }, [ref name] => Ok(Some(PathBuf::from(name))), _ => { - let msg = format!("More than one candidate ({count}) for lookup of {searched:?} in {base:?}", - count = candidates.len()); - debug!("{msg}"); - bail!(msg) + match handle_multiple_matches(&candidates, searched) { + Ok(result) => Ok(result), + Err(_) => { + let msg = format!("More than one candidate ({count}) for lookup of {searched:?} in {base:?}", + count = candidates.len()); + debug!("{msg}"); + bail!(msg) + } + } }, } } @@ -110,6 +117,24 @@ pub fn find_all_insensitive

(base: P, searched: &str) -> Result> Ok(result) } +fn handle_multiple_matches(candidates: &[PathBuf], searched: &str) -> Result> { + let handles: Result, std::io::Error> = candidates.iter() + .map(|path| Handle::from_path(path)) + .collect(); + let handles = match handles { + Err(err) => bail!("Couldn't open all matching candidates\n {:?}", err), + Ok(handles) => handles, + }; + if handles.iter().all_equal() { + let chosen = itertools::sorted(candidates.iter()).next().cloned(); + info!("multiple ({count}) candidates for {searched} but all are the same file - choose {chosen:?}", + count = candidates.len()); + Ok(chosen) + } else { + bail!("Multiple matches for {searched}, not the same file") + } +} + #[cfg(test)] mod tests { use std::collections::HashSet; @@ -275,4 +300,23 @@ mod tests { ].iter().collect::>(), ); } + + #[cfg(target_family = "unix")] + #[test] + fn multiple_candidates_symlink_same_file() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/conflicts"); + assert_eq!( + find_insensitive(&base, "mod1").unwrap(), + Some(PathBuf::from(&base).join("MOD1")), + ); + } + + #[cfg(target_family = "unix")] + #[test] + fn multiple_candidates_symlink_different_file() { + let _ = env_logger::builder().is_test(true).filter_level(log::LevelFilter::Debug).try_init(); + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/file_lookup/conflicts"); + assert!(find_insensitive(&base, "mod2").is_err()); + } }