1#![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
51pub 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
64pub 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
76pub 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
86pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
91 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 to.to_owned()
106}
107
108pub 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 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
134pub 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 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 Ok(existing_file)
150 } else {
151 Err(error)
152 }
153 }
154 }
155 } else {
156 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 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 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 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}