cut/
path.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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
22/// Normalize the representation of `path` by eliminating redundant `.`
23/// components and applying `..` components.  Does not access the filesystem
24/// (e.g. to resolve symlinks or test for file existence), unlike
25/// `std::fs::canonicalize`.
26///
27/// Fails if the normalized path attempts to access the parent of a root
28/// directory or volume prefix.  Returns the normalized path on success.
29pub(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            // Components that contribute to the path as-is.
36            verbatim @ (C::Prefix(_) | C::RootDir | C::Normal(_)) => stack.push(verbatim),
37
38            // Equivalent of a `.` path component -- can be ignored.
39            C::CurDir => { /* nop */ }
40
41            // Going up in the directory hierarchy, which may fail if that's not possible.
42            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
65/// Return the path to `dst` relative to `src`.  If `src` is a file, the path is
66/// relative to the directory that contains it, while if it is a directory, the
67/// path is relative to it.  Returns an error if either `src` or `dst` do not
68/// exist.
69pub(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    // (1). Strip matching prefix
87    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    // (2) Push parent directory components (moving out of directories in `base`)
96    let mut stack = vec![];
97    for _ in s_comps {
98        stack.push(C::ParentDir)
99    }
100
101    // (3) Push extension directory components (moving into directories unique to
102    // `ext`)
103    for comp in d_comps {
104        stack.push(comp)
105    }
106
107    // (4) Check for base == ext case
108    if stack.is_empty() {
109        stack.push(C::CurDir)
110    }
111
112    Ok(stack.into_iter().collect())
113}
114
115/// Returns the shortest prefix of `path` that doesn't exist, or `None` if
116/// `path` already exists.
117pub(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    // Invariant: parent == { path.pop(); path }
127    //         && !path.exists()
128    while !parent.exists() {
129        parent.pop();
130        path.pop();
131    }
132
133    Some(path)
134}
135
136/// Recursively copy the contents of `src` to `dst`.  Fails if `src`
137/// transitively contains a symlink.  Only copies paths that pass the `keep`
138/// predicate.
139pub(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        // Paths relative to files will be relative to their directory, whereas paths
220        // relative to directories will not.
221        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        // Paths relative to files will be relative to their directory, whereas paths
235        // relative to directories will not.
236        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        // Set-up some things to copy:
314        //
315        // src/foo:         bar
316        // src/baz/qux/quy: plugh
317        // src/baz/quz:     xyzzy
318
319        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        // Copy without filtering
327        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        // Filter a file
334        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        // Filter a directory
341        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        // Filtering a file gets rid of its (empty) parent
347        deep_copy(&src, dst.join("cpy-3"), &mut |p| !p.ends_with("quy")).unwrap();
348
349        // Because qux is now empty, it also doesn't exist in the copy, even though we
350        // only explicitly filtered `quy`.
351        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}