anvilsign in

collin/anvil

main / vendor / gitserver-core / src / path.rs
1use std::path::{
2 Component,
3 Path,
4 PathBuf,
5};
6
7use crate::error::{
8 Error,
9 Result,
10};
11
12/// Normalize a path by resolving `.` and `..` components lexically,
13/// without touching the filesystem.
14fn normalize(path: &Path) -> PathBuf {
15 let mut out = PathBuf::new();
16 for component in path.components() {
17 match component {
18 Component::ParentDir => {
19 out.pop();
20 }
21 Component::CurDir => {}
22 c => out.push(c),
23 }
24 }
25 out
26}
27
28/// Resolve a relative repo path against a root directory.
29/// Returns the canonical absolute path if it is within root.
30pub fn resolve_repo_path(root: &Path, relative: &str) -> Result<PathBuf> {
31 let candidate = root.join(relative);
32
33 let canonical_root = root
34 .canonicalize()
35 .map_err(|_| Error::RepoNotFound(relative.to_string()))?;
36
37 // Lexically normalize the candidate to detect traversal before hitting the filesystem.
38 let normalized = normalize(&canonical_root.join(relative));
39 if !normalized.starts_with(&canonical_root) {
40 return Err(Error::PathTraversal(candidate));
41 }
42
43 let canonical = candidate
44 .canonicalize()
45 .map_err(|_| Error::RepoNotFound(relative.to_string()))?;
46
47 if !canonical.starts_with(&canonical_root) {
48 return Err(Error::PathTraversal(candidate));
49 }
50
51 Ok(canonical)
52}
53
54#[cfg(test)]
55mod tests {
56 use tempfile::TempDir;
57
58 use super::*;
59
60 #[test]
61 fn resolve_simple_path() {
62 let root = TempDir::new().unwrap();
63 let repo_dir = root.path().join("myrepo.git");
64 std::fs::create_dir(&repo_dir).unwrap();
65
66 let resolved = resolve_repo_path(root.path(), "myrepo.git").unwrap();
67 assert_eq!(resolved, repo_dir.canonicalize().unwrap());
68 }
69
70 #[test]
71 fn resolve_nested_path() {
72 let root = TempDir::new().unwrap();
73 let repo_dir = root.path().join("org/project.git");
74 std::fs::create_dir_all(&repo_dir).unwrap();
75
76 let resolved = resolve_repo_path(root.path(), "org/project.git").unwrap();
77 assert_eq!(resolved, repo_dir.canonicalize().unwrap());
78 }
79
80 #[test]
81 fn reject_traversal() {
82 let root = TempDir::new().unwrap();
83 let err = resolve_repo_path(root.path(), "../etc/passwd").unwrap_err();
84 assert!(matches!(err, Error::PathTraversal(_)));
85 }
86
87 #[test]
88 fn reject_traversal_in_middle() {
89 let root = TempDir::new().unwrap();
90 let repo_dir = root.path().join("legit");
91 std::fs::create_dir(&repo_dir).unwrap();
92
93 let err = resolve_repo_path(root.path(), "legit/../../etc/passwd").unwrap_err();
94 assert!(matches!(err, Error::PathTraversal(_)));
95 }
96
97 #[test]
98 fn reject_nonexistent_path() {
99 let root = TempDir::new().unwrap();
100 let err = resolve_repo_path(root.path(), "nonexistent.git").unwrap_err();
101 assert!(matches!(err, Error::RepoNotFound(_)));
102 }
103}