From f947b14863bba8cf2a06fc05c700a3623ada0c29 Mon Sep 17 00:00:00 2001 From: A Farzat Date: Thu, 4 Jun 2026 04:11:13 +0300 Subject: Move normalizer to a child module --- src/lib.rs | 234 ------------------------------------------------------ src/normalizer.rs | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 234 deletions(-) delete mode 100644 src/lib.rs create mode 100644 src/normalizer.rs diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 0878126..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::{ - env, - path::{Component, Path, PathBuf}, -}; - -#[derive(Debug)] -pub enum NormalizeError { - EmptyInput, - EscapesFilesystemRoot, - FailedToGetCurDir, - InvalidMultiplePrefix, -} - -pub struct Normalizer { - root: PathBuf, - origin_base: PathBuf, -} - -impl Normalizer { - pub fn new(root: &Path, origin_base: &Path) -> Result { - let cwd = env::current_dir().map_err(|_| NormalizeError::FailedToGetCurDir)?; - Self::new_with_cwd(root, origin_base, &cwd) - } - - fn new_with_cwd(root: &Path, origin_base: &Path, cwd: &Path) -> Result { - Ok(Self { - root: normalize_components(&absolutize(root, cwd))?, - origin_base: normalize_components(&absolutize(origin_base, cwd))?, - }) - } - - pub fn normalize(&self, input: &Path) -> Result { - if input.as_os_str().is_empty() { - return Err(NormalizeError::EmptyInput); - } - let input = absolutize(input, &self.origin_base); - let normalized_input = normalize_components(&input)?; - Ok(make_relative_to_root(normalized_input, &self.root)) - } -} - -fn absolutize(path: &Path, base: &Path) -> PathBuf { - if path.is_absolute() { - path.to_path_buf() - } else { - base.join(path) - } -} - -/// # Invariant -/// `path` must be an absolute path. -/// Violations indicate a bug in the caller. -fn normalize_components(path: &Path) -> Result { - debug_assert!( - path.is_absolute(), - "Input must be an absolute path: {:?}", - path - ); - let mut normalized = PathBuf::new(); - let mut iter = path.components().peekable(); - - // Find the root, if any, and add it to the lexical path. - // Here we treat the Windows path "C:\" as a single "root" even though - // `components` splits it into two: (Prefix, RootDir). - let root = match iter.peek() { - Some(p @ Component::RootDir) => { - normalized.push(p); - iter.next(); - normalized.as_os_str().len() - } - Some(Component::Prefix(prefix)) => { - normalized.push(prefix.as_os_str()); - iter.next(); - if let Some(p @ Component::RootDir) = iter.peek() { - normalized.push(p); - iter.next(); - } - normalized.as_os_str().len() - } - _ => unreachable!( - "normalize_components received a non-absolute path: {:?}", - path - ), - }; - - for component in iter { - match component { - Component::RootDir => unreachable!(), - Component::Prefix(_) => return Err(NormalizeError::InvalidMultiplePrefix), - Component::CurDir => continue, - Component::ParentDir => { - // It's an error if ParentDir causes us to go above the "root". - if normalized.as_os_str().len() == root { - return Err(NormalizeError::EscapesFilesystemRoot); - } else { - normalized.pop(); - } - } - Component::Normal(path) => normalized.push(path), - } - } - Ok(normalized) -} - -/// # Invariant -/// `target` and `root` must be absolute paths. -/// Violations indicate a bug in the caller. -fn make_relative_to_root(target: PathBuf, mut root: &Path) -> PathBuf { - debug_assert!( - target.is_absolute(), - "Target must be an absolute path: {:?}", - target - ); - debug_assert!( - root.is_absolute(), - "Root must be an absolute path: {:?}", - root - ); - let mut upward = PathBuf::new(); - loop { - if let Ok(suffix) = target.strip_prefix(root) { - return upward.join(suffix); - } - if let Some(new_root) = root.parent() { - upward.push(".."); - root = new_root; - } else { - return target; - } - } -} - -#[cfg(test)] -mod tests { - use std::path::Path; - - use super::{NormalizeError, Normalizer}; - - #[test] - fn empty_path_returns_error() { - let fake_cwd = Path::new("/sandbox"); - let normalizer = Normalizer::new_with_cwd(Path::new(""), Path::new(""), fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("")); - assert!(matches!(result, Err(NormalizeError::EmptyInput))); - } - - #[test] - fn plain_filename_with_root_at_cwd_returns_filename() { - let fake_cwd = Path::new("/sandbox"); - let normalizer = Normalizer::new_with_cwd(Path::new(""), Path::new(""), fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("main.rs")); - 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_base = Path::new("src"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("../main.rs")); - 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_base = Path::new("/project/src"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("main.rs")); - 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_base = Path::new("/outside"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("main.rs")); - 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_base = Path::new("/sandbox/outside"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("main.rs")); - 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_base = Path::new("/sandbox/outside"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("/sandbox/main.rs")); - assert_eq!(result.unwrap(), Path::new("../main.rs")); - } - - #[test] - fn path_is_made_relative_to_root_even_if_origin_base_is_relative() { - let fake_cwd = Path::new("/sandbox"); - let root = Path::new("/sandbox/project"); - let origin_base = Path::new("outside"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("main.rs")); - assert_eq!(result.unwrap(), Path::new("../outside/main.rs")); - } - - #[test] - fn input_cannot_go_above_root() { - let fake_cwd = Path::new("/sandbox"); - let root = Path::new("project"); - let origin_base = Path::new("outside"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("../../../main.rs")); - assert!(matches!(result, Err(NormalizeError::EscapesFilesystemRoot))); - } - - #[test] - fn should_handle_unnormalized_root() { - let fake_cwd = Path::new("/sandbox"); - let root = Path::new("../project"); - let origin_base = Path::new("outside"); - let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); - let result = normalizer.normalize(Path::new("main.rs")); - assert_eq!(result.unwrap(), Path::new("../sandbox/outside/main.rs")); - } -} diff --git a/src/normalizer.rs b/src/normalizer.rs new file mode 100644 index 0000000..0878126 --- /dev/null +++ b/src/normalizer.rs @@ -0,0 +1,234 @@ +use std::{ + env, + path::{Component, Path, PathBuf}, +}; + +#[derive(Debug)] +pub enum NormalizeError { + EmptyInput, + EscapesFilesystemRoot, + FailedToGetCurDir, + InvalidMultiplePrefix, +} + +pub struct Normalizer { + root: PathBuf, + origin_base: PathBuf, +} + +impl Normalizer { + pub fn new(root: &Path, origin_base: &Path) -> Result { + let cwd = env::current_dir().map_err(|_| NormalizeError::FailedToGetCurDir)?; + Self::new_with_cwd(root, origin_base, &cwd) + } + + fn new_with_cwd(root: &Path, origin_base: &Path, cwd: &Path) -> Result { + Ok(Self { + root: normalize_components(&absolutize(root, cwd))?, + origin_base: normalize_components(&absolutize(origin_base, cwd))?, + }) + } + + pub fn normalize(&self, input: &Path) -> Result { + if input.as_os_str().is_empty() { + return Err(NormalizeError::EmptyInput); + } + let input = absolutize(input, &self.origin_base); + let normalized_input = normalize_components(&input)?; + Ok(make_relative_to_root(normalized_input, &self.root)) + } +} + +fn absolutize(path: &Path, base: &Path) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else { + base.join(path) + } +} + +/// # Invariant +/// `path` must be an absolute path. +/// Violations indicate a bug in the caller. +fn normalize_components(path: &Path) -> Result { + debug_assert!( + path.is_absolute(), + "Input must be an absolute path: {:?}", + path + ); + let mut normalized = PathBuf::new(); + let mut iter = path.components().peekable(); + + // Find the root, if any, and add it to the lexical path. + // Here we treat the Windows path "C:\" as a single "root" even though + // `components` splits it into two: (Prefix, RootDir). + let root = match iter.peek() { + Some(p @ Component::RootDir) => { + normalized.push(p); + iter.next(); + normalized.as_os_str().len() + } + Some(Component::Prefix(prefix)) => { + normalized.push(prefix.as_os_str()); + iter.next(); + if let Some(p @ Component::RootDir) = iter.peek() { + normalized.push(p); + iter.next(); + } + normalized.as_os_str().len() + } + _ => unreachable!( + "normalize_components received a non-absolute path: {:?}", + path + ), + }; + + for component in iter { + match component { + Component::RootDir => unreachable!(), + Component::Prefix(_) => return Err(NormalizeError::InvalidMultiplePrefix), + Component::CurDir => continue, + Component::ParentDir => { + // It's an error if ParentDir causes us to go above the "root". + if normalized.as_os_str().len() == root { + return Err(NormalizeError::EscapesFilesystemRoot); + } else { + normalized.pop(); + } + } + Component::Normal(path) => normalized.push(path), + } + } + Ok(normalized) +} + +/// # Invariant +/// `target` and `root` must be absolute paths. +/// Violations indicate a bug in the caller. +fn make_relative_to_root(target: PathBuf, mut root: &Path) -> PathBuf { + debug_assert!( + target.is_absolute(), + "Target must be an absolute path: {:?}", + target + ); + debug_assert!( + root.is_absolute(), + "Root must be an absolute path: {:?}", + root + ); + let mut upward = PathBuf::new(); + loop { + if let Ok(suffix) = target.strip_prefix(root) { + return upward.join(suffix); + } + if let Some(new_root) = root.parent() { + upward.push(".."); + root = new_root; + } else { + return target; + } + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::{NormalizeError, Normalizer}; + + #[test] + fn empty_path_returns_error() { + let fake_cwd = Path::new("/sandbox"); + let normalizer = Normalizer::new_with_cwd(Path::new(""), Path::new(""), fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("")); + assert!(matches!(result, Err(NormalizeError::EmptyInput))); + } + + #[test] + fn plain_filename_with_root_at_cwd_returns_filename() { + let fake_cwd = Path::new("/sandbox"); + let normalizer = Normalizer::new_with_cwd(Path::new(""), Path::new(""), fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("main.rs")); + 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_base = Path::new("src"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("../main.rs")); + 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_base = Path::new("/project/src"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("main.rs")); + 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_base = Path::new("/outside"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("main.rs")); + 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_base = Path::new("/sandbox/outside"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("main.rs")); + 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_base = Path::new("/sandbox/outside"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("/sandbox/main.rs")); + assert_eq!(result.unwrap(), Path::new("../main.rs")); + } + + #[test] + fn path_is_made_relative_to_root_even_if_origin_base_is_relative() { + let fake_cwd = Path::new("/sandbox"); + let root = Path::new("/sandbox/project"); + let origin_base = Path::new("outside"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("main.rs")); + assert_eq!(result.unwrap(), Path::new("../outside/main.rs")); + } + + #[test] + fn input_cannot_go_above_root() { + let fake_cwd = Path::new("/sandbox"); + let root = Path::new("project"); + let origin_base = Path::new("outside"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("../../../main.rs")); + assert!(matches!(result, Err(NormalizeError::EscapesFilesystemRoot))); + } + + #[test] + fn should_handle_unnormalized_root() { + let fake_cwd = Path::new("/sandbox"); + let root = Path::new("../project"); + let origin_base = Path::new("outside"); + let normalizer = Normalizer::new_with_cwd(root, origin_base, fake_cwd).unwrap(); + let result = normalizer.normalize(Path::new("main.rs")); + assert_eq!(result.unwrap(), Path::new("../sandbox/outside/main.rs")); + } +} -- cgit v1.3.1