1use std::{
6 fs, io,
7 path::{Path, PathBuf},
8};
9
10use anyhow::{Result, bail};
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14pub(crate) enum Error {
15 #[error("Path attempts to access parent of root directory: {}", .0.display())]
16 ParentOfRoot(PathBuf),
17
18 #[error("Unexpected symlink: {}", .0.display())]
19 Symlink(PathBuf),
20}
21
22pub(crate) fn normalize_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
30 use std::path::Component as C;
31
32 let mut stack = vec![];
33 for component in path.as_ref().components() {
34 match component {
35 verbatim @ (C::Prefix(_) | C::RootDir | C::Normal(_)) => stack.push(verbatim),
37
38 C::CurDir => { }
40
41 C::ParentDir => match stack.last() {
43 None | Some(C::ParentDir) => {
44 stack.push(C::ParentDir);
45 }
46
47 Some(C::Normal(_)) => {
48 stack.pop();
49 }
50
51 Some(C::CurDir) => {
52 unreachable!("Component::CurDir never added to the stack");
53 }
54
55 Some(C::RootDir | C::Prefix(_)) => {
56 bail!(Error::ParentOfRoot(path.as_ref().to_path_buf()))
57 }
58 },
59 }
60 }
61
62 Ok(stack.iter().collect())
63}
64
65pub(crate) fn path_relative_to<P, Q>(src: P, dst: Q) -> io::Result<PathBuf>
70where
71 P: AsRef<Path>,
72 Q: AsRef<Path>,
73{
74 use std::path::Component as C;
75
76 let mut src = fs::canonicalize(src)?;
77 let dst = fs::canonicalize(dst)?;
78
79 if src.is_file() {
80 src.pop();
81 }
82
83 let mut s_comps = src.components().peekable();
84 let mut d_comps = dst.components().peekable();
85
86 while let (Some(s_comp), Some(d_comp)) = (s_comps.peek(), d_comps.peek()) {
88 if s_comp != d_comp {
89 break;
90 }
91 s_comps.next();
92 d_comps.next();
93 }
94
95 let mut stack = vec![];
97 for _ in s_comps {
98 stack.push(C::ParentDir)
99 }
100
101 for comp in d_comps {
104 stack.push(comp)
105 }
106
107 if stack.is_empty() {
109 stack.push(C::CurDir)
110 }
111
112 Ok(stack.into_iter().collect())
113}
114
115pub(crate) fn shortest_new_prefix(path: impl AsRef<Path>) -> Option<PathBuf> {
118 if path.as_ref().exists() {
119 return None;
120 }
121
122 let mut path = path.as_ref().to_owned();
123 let mut parent = path.clone();
124 parent.pop();
125
126 while !parent.exists() {
129 parent.pop();
130 path.pop();
131 }
132
133 Some(path)
134}
135
136pub(crate) fn deep_copy<P, Q, K>(src: P, dst: Q, keep: &mut K) -> Result<()>
140where
141 P: AsRef<Path>,
142 Q: AsRef<Path>,
143 K: FnMut(&Path) -> bool,
144{
145 let src = src.as_ref();
146 let dst = dst.as_ref();
147
148 if !keep(src) {
149 return Ok(());
150 }
151
152 if src.is_file() {
153 fs::create_dir_all(dst.parent().expect("files have parents"))?;
154 fs::copy(src, dst)?;
155 return Ok(());
156 }
157
158 if src.is_symlink() {
159 bail!(Error::Symlink(src.to_path_buf()));
160 }
161
162 for entry in fs::read_dir(src)? {
163 let entry = entry?;
164 deep_copy(
165 src.join(entry.file_name()),
166 dst.join(entry.file_name()),
167 keep,
168 )?
169 }
170
171 Ok(())
172}
173
174#[cfg(test)]
175mod tests {
176 use expect_test::expect;
177 use tempfile::tempdir;
178
179 use super::*;
180
181 #[test]
182 fn test_normalize_path_identity() {
183 assert_eq!(normalize_path("/a/b").unwrap(), PathBuf::from("/a/b"));
184 assert_eq!(normalize_path("/").unwrap(), PathBuf::from("/"));
185 assert_eq!(normalize_path("a/b").unwrap(), PathBuf::from("a/b"));
186 }
187
188 #[test]
189 fn test_normalize_path_absolute() {
190 assert_eq!(normalize_path("/a/./b").unwrap(), PathBuf::from("/a/b"));
191 assert_eq!(normalize_path("/a/../b").unwrap(), PathBuf::from("/b"));
192 }
193
194 #[test]
195 fn test_normalize_path_relative() {
196 assert_eq!(normalize_path("a/./b").unwrap(), PathBuf::from("a/b"));
197 assert_eq!(normalize_path("a/../b").unwrap(), PathBuf::from("b"));
198 assert_eq!(normalize_path("a/../../b").unwrap(), PathBuf::from("../b"));
199 }
200
201 #[test]
202 fn test_normalize_path_error() {
203 expect!["Path attempts to access parent of root directory: /a/../.."]
204 .assert_eq(&format!("{}", normalize_path("/a/../..").unwrap_err()))
205 }
206
207 #[test]
208 fn test_path_relative_to_equal() {
209 let cut = env!("CARGO_MANIFEST_DIR");
210 assert_eq!(path_relative_to(cut, cut).unwrap(), PathBuf::from("."));
211 }
212
213 #[test]
214 fn test_path_relative_to_file() {
215 let cut = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
216 let toml = cut.join("Cargo.toml");
217 let src = cut.join("src");
218
219 assert_eq!(path_relative_to(&toml, &src).unwrap(), PathBuf::from("src"));
222 assert_eq!(
223 path_relative_to(&src, &toml).unwrap(),
224 PathBuf::from("../Cargo.toml")
225 );
226 }
227
228 #[test]
229 fn test_path_relative_to_related() {
230 let cut = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
231 let src = cut.join("src");
232 let repo_root = cut.join("../..");
233
234 assert_eq!(path_relative_to(&cut, &src).unwrap(), PathBuf::from("src"));
237 assert_eq!(path_relative_to(&src, &cut).unwrap(), PathBuf::from(".."));
238
239 assert_eq!(
240 path_relative_to(&repo_root, &src).unwrap(),
241 PathBuf::from("iota-execution/cut/src"),
242 );
243
244 assert_eq!(
245 path_relative_to(&src, &repo_root).unwrap(),
246 PathBuf::from("../../.."),
247 );
248 }
249
250 #[test]
251 fn test_path_relative_to_unrelated() {
252 let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
253 let iota_adapter = repo_root.join("iota-execution/latest/iota-adapter");
254 let vm_runtime = repo_root.join("external-crates/move/crates/move-vm-runtime");
255
256 assert_eq!(
257 path_relative_to(iota_adapter, vm_runtime).unwrap(),
258 PathBuf::from("../../../external-crates/move/crates/move-vm-runtime"),
259 );
260 }
261
262 #[test]
263 fn test_path_relative_to_nonexistent() {
264 let tmp = tempdir().unwrap();
265 let i_dont_exist = tmp.path().join("i_dont_exist");
266
267 expect!["No such file or directory (os error 2)"].assert_eq(&format!(
268 "{}",
269 path_relative_to(&i_dont_exist, &tmp).unwrap_err()
270 ));
271
272 expect!["No such file or directory (os error 2)"].assert_eq(&format!(
273 "{}",
274 path_relative_to(&tmp, &i_dont_exist).unwrap_err()
275 ));
276 }
277
278 #[test]
279 fn test_shortest_new_prefix_current() {
280 let tmp = tempdir().unwrap();
281 let foo = tmp.path().join("foo");
282 assert_eq!(shortest_new_prefix(&foo), Some(foo));
283 }
284
285 #[test]
286 fn test_shortest_new_prefix_parent() {
287 let tmp = tempdir().unwrap();
288 let foo = tmp.path().join("foo");
289 let bar = tmp.path().join("foo/bar");
290 assert_eq!(shortest_new_prefix(bar), Some(foo));
291 }
292
293 #[test]
294 fn test_shortest_new_prefix_transitive() {
295 let tmp = tempdir().unwrap();
296 let foo = tmp.path().join("foo");
297 let qux = tmp.path().join("foo/bar/baz/qux");
298 assert_eq!(shortest_new_prefix(qux), Some(foo));
299 }
300
301 #[test]
302 fn test_shortest_new_prefix_not_new() {
303 let tmp = tempdir().unwrap();
304 assert_eq!(None, shortest_new_prefix(tmp.path()));
305 }
306
307 #[test]
308 fn test_deep_copy() {
309 let tmp = tempdir().unwrap();
310 let src = tmp.path().join("src");
311 let dst = tmp.path().join("dst");
312
313 fs::create_dir_all(src.join("baz/qux")).unwrap();
320 fs::write(src.join("foo"), "bar").unwrap();
321 fs::write(src.join("baz/qux/quy"), "plugh").unwrap();
322 fs::write(src.join("baz/quz"), "xyzzy").unwrap();
323
324 let read = |path: &str| fs::read_to_string(dst.join(path)).unwrap();
325
326 deep_copy(&src, dst.join("cpy-0"), &mut |_| true).unwrap();
328
329 assert_eq!(read("cpy-0/foo"), "bar");
330 assert_eq!(read("cpy-0/baz/qux/quy"), "plugh");
331 assert_eq!(read("cpy-0/baz/quz"), "xyzzy");
332
333 deep_copy(&src, dst.join("cpy-1"), &mut |p| !p.ends_with("foo")).unwrap();
335
336 assert!(!dst.join("cpy-1/foo").exists());
337 assert_eq!(read("cpy-1/baz/qux/quy"), "plugh");
338 assert_eq!(read("cpy-1/baz/quz"), "xyzzy");
339
340 deep_copy(&src, dst.join("cpy-2"), &mut |p| !p.ends_with("baz")).unwrap();
342
343 assert_eq!(read("cpy-2/foo"), "bar");
344 assert!(!dst.join("cpy-2/baz").exists());
345
346 deep_copy(&src, dst.join("cpy-3"), &mut |p| !p.ends_with("quy")).unwrap();
348
349 assert_eq!(read("cpy-3/foo"), "bar");
352 assert!(!dst.join("cpy-3/baz/qux").exists());
353 assert_eq!(read("cpy-3/baz/quz"), "xyzzy");
354 }
355}