use std::{env, path::{Component, Path, PathBuf}}; #[derive(Debug)] pub enum NormalizeError { EmptyInput, CwdNotAbsolute, } pub fn normalize_path(root: &Path, origin: &Path, input: &Path) -> Result { let cwd = env::current_dir().map_err(|_| NormalizeError::CwdNotAbsolute)?; normalize_path_with_preset_cwd(root, origin, input, &cwd) } fn normalize_path_with_preset_cwd( root: &Path, origin: &Path, input: &Path, cwd: &Path, ) -> Result { if input.as_os_str().is_empty() { return Err(NormalizeError::EmptyInput); } if !cwd.is_absolute() { return Err(NormalizeError::CwdNotAbsolute); } let input = if input.is_absolute() { input.to_path_buf() } else if origin.is_absolute() { origin.join(input) } else { cwd.join(origin).join(input) }; let root = if root.is_absolute() { root.to_path_buf() } else { cwd.join(root) }; let mut stack = Vec::new(); for component in input.components() { match component { Component::CurDir => (), Component::ParentDir => { stack.pop(); } Component::Prefix(_) => stack.push(component), Component::Normal(_) => stack.push(component), Component::RootDir => stack.push(component), } } let normalized_input = PathBuf::from_iter(stack); Ok(normalize_to_root(normalized_input, &root)) } fn normalize_to_root(target: PathBuf, mut root: &Path) -> PathBuf { let mut prefix = PathBuf::new(); loop { if let Ok(suffix) = target.strip_prefix(root) { return prefix.join(suffix); } if let Some(new_root) = root.parent() { prefix.push(".."); root = new_root; } else { return target; } } } #[cfg(test)] mod tests { use std::path::Path; use super::{NormalizeError, normalize_path_with_preset_cwd}; #[test] fn empty_path_returns_error() { let fake_cwd = Path::new("/sandbox"); let root = Path::new(""); let origin_dir = Path::new(""); let input = Path::new(""); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert!(matches!(result, Err(NormalizeError::EmptyInput))); } #[test] fn plain_filename_with_root_at_cwd_returns_filename() { let fake_cwd = Path::new("/sandbox"); let root = Path::new(""); let origin_dir = Path::new(""); let input = Path::new("main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert!(result.is_ok()); assert_eq!(result.unwrap(), Path::new("main.rs")); } #[test] fn relative_path_from_origin_is_resolved() { let fake_cwd = Path::new("/sandbox"); let root = Path::new(""); let origin_dir = Path::new("src"); let input = Path::new("../main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert!(result.is_ok()); assert_eq!(result.unwrap(), Path::new("main.rs")); } #[test] fn path_is_made_relative_to_root() { let fake_cwd = Path::new("/sandbox"); let root = Path::new("/project"); let origin_dir = Path::new("/project/src"); let input = Path::new("main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert_eq!(result.unwrap(), Path::new("src/main.rs")); } #[test] fn path_is_made_relative_to_root_from_outside() { let fake_cwd = Path::new("/sandbox"); let root = Path::new("/project"); let origin_dir = Path::new("/outside"); let input = Path::new("main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert_eq!(result.unwrap(), Path::new("../outside/main.rs")); } #[test] fn path_is_made_relative_to_root_even_if_root_dir_is_relative() { let fake_cwd = Path::new("/sandbox"); let root = Path::new("project"); let origin_dir = Path::new("/sandbox/outside"); let input = Path::new("main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert_eq!(result.unwrap(), Path::new("../outside/main.rs")); } #[test] fn absolute_inputs_work() { let fake_cwd = Path::new("/sandbox"); let root = Path::new("project"); let origin_dir = Path::new("/sandbox/outside"); let input = Path::new("/sandbox/main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert_eq!(result.unwrap(), Path::new("../main.rs")); } #[test] fn path_is_made_relative_to_root_even_if_origin_dir_is_relative() { let fake_cwd = Path::new("/sandbox"); let root = Path::new("/sandbox/project"); let origin_dir = Path::new("outside"); let input = Path::new("main.rs"); let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); assert_eq!(result.unwrap(), Path::new("../outside/main.rs")); } }