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