just playing with tangled
at ig/vimdiffwarn 259 lines 8.3 kB view raw
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}