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