//! Explicit data-directory isolation for portable use and native release QA. //! Ordinary launches continue to use the operating system's app-data directory. use std::{ ffi::OsString, io, path::{Path, PathBuf}, }; /// Match canonical watched paths, including Windows verbatim prefixes, even /// before a first-use model or scratch directory exists. pub fn canonical_exclusion(path: &Path) -> io::Result { if !path.is_absolute() { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Excluded directory must be absolute", )); } let mut ancestor = path.to_path_buf(); let mut suffix = Vec::new(); loop { match std::fs::canonicalize(&ancestor) { Ok(mut resolved) => { for part in suffix.iter().rev() { resolved.push(part); } return Ok(resolved); } Err(error) if error.kind() == io::ErrorKind::NotFound => { let name = ancestor.file_name().ok_or(error)?; suffix.push(name.to_os_string()); if !ancestor.pop() { return Err(io::Error::new( io::ErrorKind::NotFound, "Excluded directory has no existing ancestor", )); } } Err(error) => return Err(error), } } } pub fn resolve(default: PathBuf, override_path: Option) -> io::Result { let Some(value) = override_path else { return Ok(default); }; let path = PathBuf::from(value); if !path.is_absolute() || path.as_os_str().len() > 32768 { return Err(io::Error::new( io::ErrorKind::InvalidInput, "SEARCHI_DATA_DIR must be an absolute bounded directory path", )); } Ok(path) } #[cfg(test)] mod tests { use super::*; #[test] fn missing_exclusion_matches_canonical_root_without_creating_directories() { let root = tempfile::tempdir().unwrap(); let missing = root.path().join("Models").join("MediaScratch"); let canonical = std::fs::canonicalize(root.path()).unwrap(); let exclusion = canonical_exclusion(&missing).unwrap(); assert_eq!(exclusion, canonical.join("Models/MediaScratch")); assert!(exclusion.starts_with(&canonical)); assert!(!missing.exists()); assert_eq!(canonical_exclusion(root.path()).unwrap(), canonical); } #[test] fn relative_exclusion_is_rejected() { assert!(canonical_exclusion(Path::new("relative/cache")).is_err()); } #[test] fn explicit_directory_isolates_settings_and_index_without_changing_default() { let default = std::env::temp_dir().join("searchi-default"); let isolated = std::env::temp_dir().join("searchi-isolated"); assert_eq!(resolve(default.clone(), None).unwrap(), default); assert_eq!( resolve(default, Some(isolated.clone().into_os_string())).unwrap(), isolated ); } #[test] fn invalid_override_is_rejected_without_falling_back_to_user_data() { let default = std::env::temp_dir().join("searchi-default"); for value in [OsString::new(), OsString::from("relative/data")] { assert_eq!( resolve(default.clone(), Some(value)).unwrap_err().kind(), io::ErrorKind::InvalidInput ); } } }