Files
searchi/apps/desktop/src-tauri/src/data_directory.rs
T

100 lines
3.4 KiB
Rust

//! 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<PathBuf> {
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<OsString>) -> io::Result<PathBuf> {
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
);
}
}
}