1#![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
52pub 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
65pub 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
77pub 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
87pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
92 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 to.to_owned()
107}
108
109pub 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 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
135pub 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 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 Ok(existing_file)
151 } else {
152 Err(error)
153 }
154 }
155 }
156 } else {
157 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 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 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 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}