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    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    // (2) Push parent directory components (moving out of directories in `base`)
99    let mut stack = vec![];
100    for _ in s_comps {
101        stack.push(C::ParentDir)
102    }
103
104    // (3) Push extension directory components (moving into directories unique to
105    // `ext`)
106    for comp in d_comps {
107        stack.push(comp)
108    }
109
110    // (4) Check for base == ext case
111    if stack.is_empty() {
112        stack.push(C::CurDir)
113    }
114
115    Ok(stack.into_iter().collect())
116}
117
118/// Returns the shortest prefix of `path` that doesn't exist, or `None` if
119/// `path` already exists.
120pub(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    // Invariant: parent == { path.pop(); path }
130    //         && !path.exists()
131    while !parent.exists() {
132        parent.pop();
133        path.pop();
134    }
135
136    Some(path)
137}
138
139/// Recursively copy the contents of `src` to `dst`.  Fails if `src`
140/// transitively contains a symlink.  Only copies paths that pass the `keep`
141/// predicate.
142pub(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        // Paths relative to files will be relative to their directory, whereas paths
223        // relative to directories will not.
224        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        // Paths relative to files will be relative to their directory, whereas paths
238        // relative to directories will not.
239        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        // Set-up some things to copy:
317        //
318        // src/foo:         bar
319        // src/baz/qux/quy: plugh
320        // src/baz/quz:     xyzzy
321
322        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        // Copy without filtering
330        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        // Filter a file
337        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        // Filter a directory
344        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        // Filtering a file gets rid of its (empty) parent
350        deep_copy(&src, dst.join("cpy-3"), &mut |p| !p.ends_with("quy")).unwrap();
351
352        // Because qux is now empty, it also doesn't exist in the copy, even though we
353        // only explicitly filtered `quy`.
354        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}