jj_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
15#![allow(missing_docs)]
16
17use std::fs;
18use std::fs::File;
19use std::io;
20use std::iter;
21use std::path::Component;
22use std::path::Path;
23use std::path::PathBuf;
24
25use tempfile::NamedTempFile;
26use tempfile::PersistError;
27use thiserror::Error;
28
29pub use self::platform::*;
30
31#[derive(Debug, Error)]
32#[error("Cannot access {path}")]
33pub struct PathError {
34    pub path: PathBuf,
35    #[source]
36    pub error: io::Error,
37}
38
39pub trait IoResultExt<T> {
40    fn context(self, path: impl AsRef<Path>) -> Result<T, PathError>;
41}
42
43impl<T> IoResultExt<T> for io::Result<T> {
44    fn context(self, path: impl AsRef<Path>) -> Result<T, PathError> {
45        self.map_err(|error| PathError {
46            path: path.as_ref().to_path_buf(),
47            error,
48        })
49    }
50}
51
52/// Creates a directory or does nothing if the directory already exists.
53///
54/// Returns the underlying error if the directory can't be created.
55/// The function will also fail if intermediate directories on the path do not
56/// already exist.
57pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> {
58    match fs::create_dir(dirname) {
59        Ok(()) => Ok(()),
60        Err(_) if dirname.is_dir() => Ok(()),
61        Err(e) => Err(e),
62    }
63}
64
65/// Removes all files in the directory, but not the directory itself.
66///
67/// The directory must exist, and there should be no sub directories.
68pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> {
69    for entry in dirname.read_dir().context(dirname)? {
70        let entry = entry.context(dirname)?;
71        let path = entry.path();
72        fs::remove_file(&path).context(&path)?;
73    }
74    Ok(())
75}
76
77/// Expands "~/" to "$HOME/".
78pub fn expand_home_path(path_str: &str) -> PathBuf {
79    if let Some(remainder) = path_str.strip_prefix("~/") {
80        if let Ok(home_dir_str) = std::env::var("HOME") {
81            return PathBuf::from(home_dir_str).join(remainder);
82        }
83    }
84    PathBuf::from(path_str)
85}
86
87/// Turns the given `to` path into relative path starting from the `from` path.
88///
89/// Both `from` and `to` paths are supposed to be absolute and normalized in the
90/// same manner.
91pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
92    // Find common prefix.
93    for (i, base) in from.ancestors().enumerate() {
94        if let Ok(suffix) = to.strip_prefix(base) {
95            if i == 0 && suffix.as_os_str().is_empty() {
96                return ".".into();
97            } else {
98                let mut result = PathBuf::from_iter(iter::repeat("..").take(i));
99                result.push(suffix);
100                return result;
101            }
102        }
103    }
104
105    // No common prefix found. Return the original (absolute) path.
106    to.to_owned()
107}
108
109/// Consumes as much `..` and `.` as possible without considering symlinks.
110pub fn normalize_path(path: &Path) -> PathBuf {
111    let mut result = PathBuf::new();
112    for c in path.components() {
113        match c {
114            Component::CurDir => {}
115            Component::ParentDir
116                if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
117            {
118                // Do not pop ".."
119                let popped = result.pop();
120                assert!(popped);
121            }
122            _ => {
123                result.push(c);
124            }
125        }
126    }
127
128    if result.as_os_str().is_empty() {
129        ".".into()
130    } else {
131        result
132    }
133}
134
135/// Like `NamedTempFile::persist()`, but doesn't try to overwrite the existing
136/// target on Windows.
137pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
138    temp_file: NamedTempFile,
139    new_path: P,
140) -> io::Result<File> {
141    if cfg!(windows) {
142        // On Windows, overwriting file can fail if the file is opened without
143        // FILE_SHARE_DELETE for example. We don't need to take a risk if the
144        // file already exists.
145        match temp_file.persist_noclobber(&new_path) {
146            Ok(file) => Ok(file),
147            Err(PersistError { error, file: _ }) => {
148                if let Ok(existing_file) = File::open(new_path) {
149                    // TODO: Update mtime to help GC keep this file
150                    Ok(existing_file)
151                } else {
152                    Err(error)
153                }
154            }
155        }
156    } else {
157        // On Unix, rename() is atomic and should succeed even if the
158        // destination file exists. Checking if the target exists might involve
159        // non-atomic operation, so don't use persist_noclobber().
160        temp_file
161            .persist(new_path)
162            .map_err(|PersistError { error, file: _ }| error)
163    }
164}
165
166#[cfg(unix)]
167mod platform {
168    use std::io;
169    use std::os::unix::fs::symlink;
170    use std::path::Path;
171
172    /// Symlinks are always available on UNIX
173    pub fn check_symlink_support() -> io::Result<bool> {
174        Ok(true)
175    }
176
177    pub fn try_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
178        symlink(original, link)
179    }
180}
181
182#[cfg(windows)]
183mod platform {
184    use std::io;
185    use std::os::windows::fs::symlink_file;
186    use std::path::Path;
187
188    use winreg::enums::HKEY_LOCAL_MACHINE;
189    use winreg::RegKey;
190
191    /// Symlinks may or may not be enabled on Windows. They require the
192    /// Developer Mode setting, which is stored in the registry key below.
193    pub fn check_symlink_support() -> io::Result<bool> {
194        let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
195        let sideloading =
196            hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?;
197        let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?;
198        Ok(developer_mode == 1)
199    }
200
201    pub fn try_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
202        // this will create a nonfunctional link for directories, but at the moment
203        // we don't have enough information in the tree to determine whether the
204        // symlink target is a file or a directory
205        // note: if developer mode is not enabled the error code will be 1314,
206        // ERROR_PRIVILEGE_NOT_HELD
207
208        symlink_file(original, link)
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use std::io::Write;
215
216    use test_case::test_case;
217
218    use super::*;
219    use crate::tests::new_temp_dir;
220
221    #[test]
222    fn normalize_too_many_dot_dot() {
223        assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
224        assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
225        assert_eq!(
226            normalize_path(Path::new("foo/../../..")),
227            Path::new("../..")
228        );
229        assert_eq!(
230            normalize_path(Path::new("foo/../../../bar/baz/..")),
231            Path::new("../../bar")
232        );
233    }
234
235    #[test]
236    fn test_persist_no_existing_file() {
237        let temp_dir = new_temp_dir();
238        let target = temp_dir.path().join("file");
239        let mut temp_file = NamedTempFile::new_in(&temp_dir).unwrap();
240        temp_file.write_all(b"contents").unwrap();
241        assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
242    }
243
244    #[test_case(false ; "existing file open")]
245    #[test_case(true ; "existing file closed")]
246    fn test_persist_target_exists(existing_file_closed: bool) {
247        let temp_dir = new_temp_dir();
248        let target = temp_dir.path().join("file");
249        let mut temp_file = NamedTempFile::new_in(&temp_dir).unwrap();
250        temp_file.write_all(b"contents").unwrap();
251
252        let mut file = File::create(&target).unwrap();
253        file.write_all(b"contents").unwrap();
254        if existing_file_closed {
255            drop(file);
256        }
257
258        assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
259    }
260}