1use crate::{
4 directory, error,
5 path::{Component, Path, PathBuf},
6};
7
8#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
10#[error("{} is outside {}", path.display(), root.display())]
11pub struct OutsideRoot {
12 root: PathBuf,
13 path: PathBuf,
14}
15
16impl OutsideRoot {
17 #[must_use]
19 pub fn root(&self) -> &Path {
20 &self.root
21 }
22
23 #[must_use]
25 pub fn path(&self) -> &Path {
26 &self.path
27 }
28}
29
30pub fn relative_to<'a>(root: &Path, path: &'a Path) -> Result<&'a Path, OutsideRoot> {
38 match path.strip_prefix(root) {
39 Ok(relative) => Ok(relative),
40 Err(_) => Err(OutsideRoot {
41 root: root.to_path_buf(),
42 path: path.to_path_buf(),
43 }),
44 }
45}
46
47#[derive(Debug, thiserror::Error)]
49#[error("{kind}")]
50pub struct ResolveError {
51 root: PathBuf,
52 path: PathBuf,
53 #[source]
54 kind: Box<ResolveErrorKind>,
55}
56
57#[derive(Debug, thiserror::Error)]
58enum ResolveErrorKind {
59 #[error(
60 "{} is not a non-empty relative path without parent traversal",
61 path.display()
62 )]
63 InvalidRelativePath { path: PathBuf },
64 #[error(transparent)]
65 Filesystem(error::Error),
66 #[error(transparent)]
67 OutsideRoot(OutsideRoot),
68}
69
70impl ResolveError {
71 #[must_use]
73 pub fn root(&self) -> &Path {
74 &self.root
75 }
76
77 #[must_use]
79 pub fn path(&self) -> &Path {
80 &self.path
81 }
82
83 #[must_use]
85 pub fn outside_root(&self) -> Option<&OutsideRoot> {
86 match self.kind.as_ref() {
87 ResolveErrorKind::OutsideRoot(source) => Some(source),
88 ResolveErrorKind::InvalidRelativePath { .. } | ResolveErrorKind::Filesystem(_) => None,
89 }
90 }
91}
92
93pub fn resolve_existing(root: &Path, relative: &Path) -> Result<PathBuf, ResolveError> {
102 if relative.is_empty()
103 || !relative
104 .components()
105 .all(|component| matches!(component, Component::Normal(_) | Component::Current))
106 {
107 return Err(ResolveError {
108 root: root.to_path_buf(),
109 path: relative.to_path_buf(),
110 kind: Box::new(ResolveErrorKind::InvalidRelativePath {
111 path: relative.to_path_buf(),
112 }),
113 });
114 }
115
116 let canonical_root = directory::canonicalize(root).map_err(|source| ResolveError {
117 root: root.to_path_buf(),
118 path: root.to_path_buf(),
119 kind: Box::new(ResolveErrorKind::Filesystem(source)),
120 })?;
121 let candidate = root.join(relative);
122 let canonical_candidate =
123 directory::canonicalize(&candidate).map_err(|source| ResolveError {
124 root: canonical_root.clone(),
125 path: candidate,
126 kind: Box::new(ResolveErrorKind::Filesystem(source)),
127 })?;
128
129 relative_to(&canonical_root, &canonical_candidate).map_err(|source| ResolveError {
130 root: canonical_root,
131 path: canonical_candidate.clone(),
132 kind: Box::new(ResolveErrorKind::OutsideRoot(source)),
133 })?;
134
135 Ok(canonical_candidate)
136}
137
138#[cfg(all(test, not(target_family = "wasm")))]
139mod tests {
140 use googletest::prelude::*;
141
142 use super::{relative_to, resolve_existing};
143 use crate::{directory, file, link, temporary::Directory};
144
145 #[gtest]
146 fn relative_paths_retain_native_components() -> Result<()> {
147 let root = crate::path::Path::new("repository");
148 let path = crate::path::Path::new("repository/specs/class/manifest.toml");
149
150 verify_that!(
151 relative_to(root, path).or_fail()?,
152 eq(crate::path::Path::new("specs/class/manifest.toml"))
153 )?;
154
155 Ok(())
156 }
157
158 #[gtest]
159 fn existing_paths_are_canonical_and_confined() -> Result<()> {
160 let directory = Directory::new().or_fail()?;
161 let root = directory.path().join("repository");
162 let component = root.join("parts/core.toml");
163
164 directory::ensure(component.parent().or_fail()?).or_fail()?;
165 file::write_text(&component, "[auras]").or_fail()?;
166
167 verify_eq!(
168 resolve_existing(&root, crate::path::Path::new("parts/core.toml")).or_fail()?,
169 directory::canonicalize(&component).or_fail()?
170 )?;
171
172 Ok(())
173 }
174
175 #[gtest]
176 fn file_links_cannot_escape_the_canonical_root() -> Result<()> {
177 let directory = Directory::new().or_fail()?;
178 let root = directory.path().join("repository");
179 let component = root.join("parts/core.toml");
180 let outside = directory.path().join("outside.toml");
181
182 directory::ensure(component.parent().or_fail()?).or_fail()?;
183 file::write_text(&outside, "[auras]").or_fail()?;
184 link::file(&outside, &component).or_fail()?;
185
186 let error = resolve_existing(&root, crate::path::Path::new("parts/core.toml"))
187 .err()
188 .or_fail()?;
189
190 verify_eq!(
191 error.outside_root().map(super::OutsideRoot::path),
192 Some(directory::canonicalize(&outside).or_fail()?.as_ref())
193 )?;
194
195 Ok(())
196 }
197}