diff options
| author | A Farzat <a@farzat.xyz> | 2026-06-03 17:58:09 +0300 |
|---|---|---|
| committer | A Farzat <a@farzat.xyz> | 2026-06-03 18:00:23 +0300 |
| commit | addad2857c1da8050260b0357bb1885a94d3ba3b (patch) | |
| tree | bb6a4927b546518110cda7e2e57bd6b9cc698305 | |
| parent | 8acafd3dd66a755e4e71ef5f8ad335c8a12b16aa (diff) | |
| download | repo2markdown-addad2857c1da8050260b0357bb1885a94d3ba3b.tar.gz repo2markdown-addad2857c1da8050260b0357bb1885a94d3ba3b.zip | |
Reject incorrect inputs which go outside fs root
| -rw-r--r-- | src/lib.rs | 63 |
1 files changed, 54 insertions, 9 deletions
@@ -4,6 +4,7 @@ use std::{env, path::{Component, Path, PathBuf}}; pub enum NormalizeError { EmptyInput, CwdNotAbsolute, + InputOutsideFileSystemRoot, } pub fn normalize_path(root: &Path, origin: &Path, input: &Path) -> Result<PathBuf, NormalizeError> { @@ -35,20 +36,54 @@ fn normalize_path_with_preset_cwd( } else { cwd.join(root) }; - let mut stack = Vec::new(); - for component in input.components() { + let normalized_input = normalize_lexically(&input)?; + Ok(normalize_to_root(normalized_input, &root)) +} + +fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> { + let mut lexical = 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(Component::ParentDir) => return Err(NormalizeError::InputOutsideFileSystemRoot), + Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => { + lexical.push(p); + iter.next(); + lexical.as_os_str().len() + } + Some(Component::Prefix(prefix)) => { + lexical.push(prefix.as_os_str()); + iter.next(); + if let Some(p @ Component::RootDir) = iter.peek() { + lexical.push(p); + iter.next(); + } + lexical.as_os_str().len() + } + None => return Ok(PathBuf::new()), + Some(Component::Normal(_)) => 0, + }; + + for component in iter { match component { - Component::CurDir => (), + Component::RootDir => unreachable!(), + Component::Prefix(_) => return Err(NormalizeError::InputOutsideFileSystemRoot), + Component::CurDir => continue, Component::ParentDir => { - stack.pop(); + // It's an error if ParentDir causes us to go above the "root". + if lexical.as_os_str().len() == root { + return Err(NormalizeError::InputOutsideFileSystemRoot); + } else { + lexical.pop(); + } } - Component::Prefix(_) => stack.push(component), - Component::Normal(_) => stack.push(component), - Component::RootDir => stack.push(component), + Component::Normal(path) => lexical.push(path), } } - let normalized_input = PathBuf::from_iter(stack); - Ok(normalize_to_root(normalized_input, &root)) + Ok(lexical) } fn normalize_to_root(target: PathBuf, mut root: &Path) -> PathBuf { @@ -153,4 +188,14 @@ mod tests { let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); 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_dir = Path::new("outside"); + let input = Path::new("../../../main.rs"); + let result = normalize_path_with_preset_cwd(root, origin_dir, input, fake_cwd); + assert!(matches!(result, Err(NormalizeError::InputOutsideFileSystemRoot))); + } } |
