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 loop {
88 let Some(s_comp) = s_comps.peek() else { break };
89 let Some(d_comp) = d_comps.peek() else { break };
90 if s_comp != d_comp {
91 break;
92 }
93
94 s_comps.next();
95 d_comps.next();
96 }
97
98 let mut stack = vec![];
100 for _ in s_comps {
101 stack.push(C::ParentDir)
102 }
103
104 for comp in d_comps {
107 stack.push(comp)
108 }
109
110 if stack.is_empty() {
112 stack.push(C::CurDir)
113 }
114
115 Ok(stack.into_iter().collect())
116}
117
118pub(crate) fn shortest_new_prefix(path: impl AsRef<Path>) -> Option<PathBuf> {
121 if path.as_ref().exists() {
122 return None;
123 }
124
125 let mut path = path.as_ref().to_owned();
126 let mut parent = path.clone();
127 parent.pop();
128
129 while !parent.exists() {
132 parent.pop();
133 path.pop();
134 }
135
136 Some(path)
137}
138
139pub(crate) fn deep_copy<P, Q, K>(src: P, dst: Q, keep: &mut K) -> Result<()>
143where
144 P: AsRef<Path>,
145 Q: AsRef<Path>,
146 K: FnMut(&Path) -> bool,
147{
148 let src = src.as_ref();
149 let dst = dst.as_ref();
150
151 if !keep(src) {
152 return Ok(());
153 }
154
155 if src.is_file() {
156 fs::create_dir_all(dst.parent().expect("files have parents"))?;
157 fs::copy(src, dst)?;
158 return Ok(());
159 }
160
161 if src.is_symlink() {
162 bail!(Error::Symlink(src.to_path_buf()));
163 }
164
165 for entry in fs::read_dir(src)? {
166 let entry = entry?;
167 deep_copy(
168 src.join(entry.file_name()),
169 dst.join(entry.file_name()),
170 keep,
171 )?
172 }
173
174 Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179 use expect_test::expect;
180 use tempfile::tempdir;
181
182 use super::*;
183
184 #[test]
185 fn test_normalize_path_identity() {
186 assert_eq!(normalize_path("/a/b").unwrap(), PathBuf::from("/a/b"));
187 assert_eq!(normalize_path("/").unwrap(), PathBuf::from("/"));
188 assert_eq!(normalize_path("a/b").unwrap(), PathBuf::from("a/b"));
189 }
190
191 #[test]
192 fn test_normalize_path_absolute() {
193 assert_eq!(normalize_path("/a/./b").unwrap(), PathBuf::from("/a/b"));
194 assert_eq!(normalize_path("/a/../b").unwrap(), PathBuf::from("/b"));
195 }
196
197 #[test]
198 fn test_normalize_path_relative() {
199 assert_eq!(normalize_path("a/./b").unwrap(), PathBuf::from("a/b"));
200 assert_eq!(normalize_path("a/../b").unwrap(), PathBuf::from("b"));
201 assert_eq!(normalize_path("a/../../b").unwrap(), PathBuf::from("../b"));
202 }
203
204 #[test]
205 fn test_normalize_path_error() {
206 expect!["Path attempts to access parent of root directory: /a/../.."]
207 .assert_eq(&format!("{}", normalize_path("/a/../..").unwrap_err()))
208 }
209
210 #[test]
211 fn test_path_relative_to_equal() {
212 let cut = env!("CARGO_MANIFEST_DIR");
213 assert_eq!(path_relative_to(cut, cut).unwrap(), PathBuf::from("."));
214 }
215
216 #[test]
217 fn test_path_relative_to_file() {
218 let cut = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
219 let toml = cut.join("Cargo.toml");
220 let src = cut.join("src");
221
222 assert_eq!(path_relative_to(&toml, &src).unwrap(), PathBuf::from("src"));
225 assert_eq!(
226 path_relative_to(&src, &toml).unwrap(),
227 PathBuf::from("../Cargo.toml")
228 );
229 }
230
231 #[test]
232 fn test_path_relative_to_related() {
233 let cut = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
234 let src = cut.join("src");
235 let repo_root = cut.join("../..");
236
237 assert_eq!(path_relative_to(&cut, &src).unwrap(), PathBuf::from("src"));
240 assert_eq!(path_relative_to(&src, &cut).unwrap(), PathBuf::from(".."));
241
242 assert_eq!(
243 path_relative_to(&repo_root, &src).unwrap(),
244 PathBuf::from("iota-execution/cut/src"),
245 );
246
247 assert_eq!(
248 path_relative_to(&src, &repo_root).unwrap(),
249 PathBuf::from("../../.."),
250 );
251 }
252
253 #[test]
254 fn test_path_relative_to_unrelated() {
255 let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
256 let iota_adapter = repo_root.join("iota-execution/latest/iota-adapter");
257 let vm_runtime = repo_root.join("external-crates/move/crates/move-vm-runtime");
258
259 assert_eq!(
260 path_relative_to(iota_adapter, vm_runtime).unwrap(),
261 PathBuf::from("../../../external-crates/move/crates/move-vm-runtime"),
262 );
263 }
264
265 #[test]
266 fn test_path_relative_to_nonexistent() {
267 let tmp = tempdir().unwrap();
268 let i_dont_exist = tmp.path().join("i_dont_exist");
269
270 expect!["No such file or directory (os error 2)"].assert_eq(&format!(
271 "{}",
272 path_relative_to(&i_dont_exist, &tmp).unwrap_err()
273 ));
274
275 expect!["No such file or directory (os error 2)"].assert_eq(&format!(
276 "{}",
277 path_relative_to(&tmp, &i_dont_exist).unwrap_err()
278 ));
279 }
280
281 #[test]
282 fn test_shortest_new_prefix_current() {
283 let tmp = tempdir().unwrap();
284 let foo = tmp.path().join("foo");
285 assert_eq!(shortest_new_prefix(&foo), Some(foo));
286 }
287
288 #[test]
289 fn test_shortest_new_prefix_parent() {
290 let tmp = tempdir().unwrap();
291 let foo = tmp.path().join("foo");
292 let bar = tmp.path().join("foo/bar");
293 assert_eq!(shortest_new_prefix(bar), Some(foo));
294 }
295
296 #[test]
297 fn test_shortest_new_prefix_transitive() {
298 let tmp = tempdir().unwrap();
299 let foo = tmp.path().join("foo");
300 let qux = tmp.path().join("foo/bar/baz/qux");
301 assert_eq!(shortest_new_prefix(qux), Some(foo));
302 }
303
304 #[test]
305 fn test_shortest_new_prefix_not_new() {
306 let tmp = tempdir().unwrap();
307 assert_eq!(None, shortest_new_prefix(tmp.path()));
308 }
309
310 #[test]
311 fn test_deep_copy() {
312 let tmp = tempdir().unwrap();
313 let src = tmp.path().join("src");
314 let dst = tmp.path().join("dst");
315
316 fs::create_dir_all(src.join("baz/qux")).unwrap();
323 fs::write(src.join("foo"), "bar").unwrap();
324 fs::write(src.join("baz/qux/quy"), "plugh").unwrap();
325 fs::write(src.join("baz/quz"), "xyzzy").unwrap();
326
327 let read = |path: &str| fs::read_to_string(dst.join(path)).unwrap();
328
329 deep_copy(&src, dst.join("cpy-0"), &mut |_| true).unwrap();
331
332 assert_eq!(read("cpy-0/foo"), "bar");
333 assert_eq!(read("cpy-0/baz/qux/quy"), "plugh");
334 assert_eq!(read("cpy-0/baz/quz"), "xyzzy");
335
336 deep_copy(&src, dst.join("cpy-1"), &mut |p| !p.ends_with("foo")).unwrap();
338
339 assert!(!dst.join("cpy-1/foo").exists());
340 assert_eq!(read("cpy-1/baz/qux/quy"), "plugh");
341 assert_eq!(read("cpy-1/baz/quz"), "xyzzy");
342
343 deep_copy(&src, dst.join("cpy-2"), &mut |p| !p.ends_with("baz")).unwrap();
345
346 assert_eq!(read("cpy-2/foo"), "bar");
347 assert!(!dst.join("cpy-2/baz").exists());
348
349 deep_copy(&src, dst.join("cpy-3"), &mut |p| !p.ends_with("quy")).unwrap();
351
352 assert_eq!(read("cpy-3/foo"), "bar");
355 assert!(!dst.join("cpy-3/baz/qux").exists());
356 assert_eq!(read("cpy-3/baz/quz"), "xyzzy");
357 }
358}