jujutsu_lib/
file_util.rs

1// Copyright 2021 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fs::File;
16use std::iter;
17use std::path::{Component, Path, PathBuf};
18
19use tempfile::{NamedTempFile, PersistError};
20
21/// Turns the given `to` path into relative path starting from the `from` path.
22///
23/// Both `from` and `to` paths are supposed to be absolute and normalized in the
24/// same manner.
25pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
26    // Find common prefix.
27    for (i, base) in from.ancestors().enumerate() {
28        if let Ok(suffix) = to.strip_prefix(base) {
29            if i == 0 && suffix.as_os_str().is_empty() {
30                return ".".into();
31            } else {
32                let mut result = PathBuf::from_iter(iter::repeat("..").take(i));
33                result.push(suffix);
34                return result;
35            }
36        }
37    }
38
39    // No common prefix found. Return the original (absolute) path.
40    to.to_owned()
41}
42
43/// Consumes as much `..` and `.` as possible without considering symlinks.
44pub fn normalize_path(path: &Path) -> PathBuf {
45    let mut result = PathBuf::new();
46    for c in path.components() {
47        match c {
48            Component::CurDir => {}
49            Component::ParentDir
50                if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
51            {
52                // Do not pop ".."
53                let popped = result.pop();
54                assert!(popped);
55            }
56            _ => {
57                result.push(c);
58            }
59        }
60    }
61
62    if result.as_os_str().is_empty() {
63        ".".into()
64    } else {
65        result
66    }
67}
68
69// Like NamedTempFile::persist(), but also succeeds if the target already
70// exists.
71pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
72    temp_file: NamedTempFile,
73    new_path: P,
74) -> Result<File, PersistError> {
75    match temp_file.persist(&new_path) {
76        Ok(file) => Ok(file),
77        Err(PersistError { error, file }) => {
78            if let Ok(existing_file) = File::open(new_path) {
79                Ok(existing_file)
80            } else {
81                Err(PersistError { error, file })
82            }
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use std::io::Write;
90
91    use test_case::test_case;
92
93    use super::*;
94
95    #[test]
96    fn normalize_too_many_dot_dot() {
97        assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
98        assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
99        assert_eq!(
100            normalize_path(Path::new("foo/../../..")),
101            Path::new("../..")
102        );
103        assert_eq!(
104            normalize_path(Path::new("foo/../../../bar/baz/..")),
105            Path::new("../../bar")
106        );
107    }
108
109    #[test]
110    fn test_persist_no_existing_file() {
111        let temp_dir = testutils::new_temp_dir();
112        let target = temp_dir.path().join("file");
113        let mut temp_file = NamedTempFile::new_in(&temp_dir).unwrap();
114        temp_file.write_all(b"contents").unwrap();
115        assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
116    }
117
118    #[test_case(false ; "existing file open")]
119    #[test_case(true ; "existing file closed")]
120    fn test_persist_target_exists(existing_file_closed: bool) {
121        let temp_dir = testutils::new_temp_dir();
122        let target = temp_dir.path().join("file");
123        let mut temp_file = NamedTempFile::new_in(&temp_dir).unwrap();
124        temp_file.write_all(b"contents").unwrap();
125
126        let mut file = File::create(&target).unwrap();
127        file.write_all(b"contents").unwrap();
128        if existing_file_closed {
129            drop(file);
130        }
131
132        assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
133    }
134}