1use crate::containment::is_lexically_inside;
7use crate::error::PathError;
8use crate::internal::components::is_drive_relative_path;
9use crate::internal::validation::reject_nul_path;
10use crate::normalize::normalize;
11use std::path::{Path, PathBuf};
12
13pub fn absolute(path: impl AsRef<Path>) -> Result<PathBuf, PathError> {
23 let path = path.as_ref();
24 reject_nul_path(path)?;
25
26 if is_drive_relative_path(path) {
27 return Err(PathError::drive_relative(path));
28 }
29
30 if path.is_absolute() {
31 return normalize(path);
32 }
33
34 let cwd = std::env::current_dir()
35 .map_err(|e| PathError::CurrentDirectoryUnavailable { source: e })?;
36 normalize(cwd.join(path))
37}
38
39pub fn resolve_against(
49 base: impl AsRef<Path>,
50 input: impl AsRef<Path>,
51) -> Result<PathBuf, PathError> {
52 let base = base.as_ref();
53 let input = input.as_ref();
54 reject_nul_path(base)?;
55 reject_nul_path(input)?;
56
57 if is_drive_relative_path(input) {
58 return Err(PathError::drive_relative(input));
59 }
60 if is_drive_relative_path(base) {
61 return Err(PathError::drive_relative(base));
62 }
63
64 if input.is_absolute() {
65 return normalize(input);
66 }
67
68 normalize(base.join(input))
69}
70
71pub fn join_relative(
82 base: impl AsRef<Path>,
83 child: impl AsRef<Path>,
84) -> Result<PathBuf, PathError> {
85 let base = base.as_ref();
86 let child = child.as_ref();
87 reject_nul_path(base)?;
88 reject_nul_path(child)?;
89
90 if is_drive_relative_path(child) {
91 return Err(PathError::drive_relative(child));
92 }
93 if child.is_absolute() {
94 return Err(PathError::absolute_child(child));
95 }
96
97 if child
99 .components()
100 .next()
101 .is_some_and(|c| matches!(c, std::path::Component::Prefix(_)))
102 {
103 return Err(PathError::absolute_child(child));
104 }
105
106 normalize(base.join(child))
107}
108
109pub fn resolve_inside(
121 root: impl AsRef<Path>,
122 child: impl AsRef<Path>,
123) -> Result<PathBuf, PathError> {
124 let root = root.as_ref();
125 let child = child.as_ref();
126 reject_nul_path(root)?;
127 reject_nul_path(child)?;
128
129 if is_drive_relative_path(root) {
130 return Err(PathError::drive_relative(root));
131 }
132 if is_drive_relative_path(child) {
133 return Err(PathError::drive_relative(child));
134 }
135
136 let joined = join_relative(root, child)?;
137 let root_norm = normalize(root)?;
138
139 if !is_lexically_inside(&joined, &root_norm) {
140 return Err(PathError::root_escape(&joined));
141 }
142
143 Ok(joined)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn join_rejects_absolute() {
152 #[cfg(unix)]
153 {
154 assert!(matches!(
155 join_relative("/repo", "/etc/passwd"),
156 Err(PathError::AbsoluteChildPath { .. })
157 ));
158 }
159 }
160
161 #[test]
162 fn resolve_inside_blocks_escape() {
163 let err = resolve_inside("/repo", "../../etc/passwd").unwrap_err();
164 assert!(matches!(err, PathError::RootEscape { .. }));
165 }
166
167 #[test]
168 fn resolve_inside_allows_nested() {
169 let p = resolve_inside("/repo", "src/main.rs").unwrap();
170 assert_eq!(p, PathBuf::from("/repo/src/main.rs"));
171 }
172}