1#![expect(missing_docs)]
16
17use std::borrow::Cow;
18use std::ffi::OsString;
19use std::fs;
20use std::fs::File;
21use std::io;
22use std::io::ErrorKind;
23use std::io::Write;
24use std::path::Component;
25use std::path::Path;
26use std::path::PathBuf;
27
28use futures::AsyncRead;
29use futures::AsyncReadExt as _;
30use tempfile::NamedTempFile;
31use tempfile::PersistError;
32use thiserror::Error;
33
34#[cfg(unix)]
35pub use self::platform::check_executable_bit_support;
36pub use self::platform::check_symlink_support;
37pub use self::platform::symlink_dir;
38pub use self::platform::symlink_file;
39
40#[derive(Debug, Error)]
41#[error("Cannot access {path}")]
42pub struct PathError {
43 pub path: PathBuf,
44 pub source: io::Error,
45}
46
47pub trait IoResultExt<T> {
48 fn context(self, path: impl AsRef<Path>) -> Result<T, PathError>;
49}
50
51impl<T> IoResultExt<T> for io::Result<T> {
52 fn context(self, path: impl AsRef<Path>) -> Result<T, PathError> {
53 self.map_err(|error| PathError {
54 path: path.as_ref().to_path_buf(),
55 source: error,
56 })
57 }
58}
59
60pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> {
66 match fs::create_dir(dirname) {
67 Ok(()) => Ok(()),
68 Err(_) if dirname.is_dir() => Ok(()),
69 Err(e) => Err(e),
70 }
71}
72
73pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> {
77 for entry in dirname.read_dir().context(dirname)? {
78 let entry = entry.context(dirname)?;
79 let path = entry.path();
80 fs::remove_file(&path).context(&path)?;
81 }
82 Ok(())
83}
84
85pub fn is_empty_dir(path: &Path) -> Result<bool, PathError> {
87 match path.read_dir() {
88 Ok(mut entries) => Ok(entries.next().is_none()),
89 Err(error) => match error.kind() {
90 ErrorKind::NotADirectory => Ok(false),
91 ErrorKind::NotFound => Ok(false),
92 _ => Err(error).context(path)?,
93 },
94 }
95}
96
97#[derive(Debug, Error)]
98#[error(transparent)]
99pub struct BadPathEncoding(platform::BadOsStrEncoding);
100
101pub fn path_from_bytes(bytes: &[u8]) -> Result<&Path, BadPathEncoding> {
106 let s = platform::os_str_from_bytes(bytes).map_err(BadPathEncoding)?;
107 Ok(Path::new(s))
108}
109
110pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> {
118 platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding)
119}
120
121pub fn expand_home_path(path_str: &str) -> PathBuf {
123 if let Some(remainder) = path_str.strip_prefix("~/")
124 && let Ok(home_dir) = etcetera::home_dir()
125 {
126 return home_dir.join(remainder);
127 }
128 PathBuf::from(path_str)
129}
130
131pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
136 for (i, base) in from.ancestors().enumerate() {
138 if let Ok(suffix) = to.strip_prefix(base) {
139 if i == 0 && suffix.as_os_str().is_empty() {
140 return ".".into();
141 } else {
142 return std::iter::repeat_n(Path::new(".."), i)
143 .chain(std::iter::once(suffix))
144 .collect();
145 }
146 }
147 }
148
149 to.to_owned()
151}
152
153pub fn normalize_path(path: &Path) -> PathBuf {
155 let mut result = PathBuf::new();
156 for c in path.components() {
157 match c {
158 Component::CurDir => {}
159 Component::ParentDir
160 if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
161 {
162 let popped = result.pop();
164 assert!(popped);
165 }
166 _ => {
167 result.push(c);
168 }
169 }
170 }
171
172 if result.as_os_str().is_empty() {
173 ".".into()
174 } else {
175 result
176 }
177}
178
179pub fn slash_path(path: &Path) -> Cow<'_, Path> {
184 if cfg!(windows) {
185 Cow::Owned(to_slash_separated(path).into())
186 } else {
187 Cow::Borrowed(path)
188 }
189}
190
191fn to_slash_separated(path: &Path) -> OsString {
192 let mut buf = OsString::with_capacity(path.as_os_str().len());
193 let mut components = path.components();
194 match components.next() {
195 Some(c) => buf.push(c),
196 None => return buf,
197 }
198 for c in components {
199 buf.push("/");
200 buf.push(c);
201 }
202 buf
203}
204
205pub fn persist_temp_file<P: AsRef<Path>>(
213 temp_file: NamedTempFile,
214 new_path: P,
215) -> io::Result<File> {
216 temp_file.as_file().sync_data()?;
218 temp_file
219 .persist(new_path)
220 .map_err(|PersistError { error, file: _ }| error)
221}
222
223pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
226 temp_file: NamedTempFile,
227 new_path: P,
228) -> io::Result<File> {
229 temp_file.as_file().sync_data()?;
232 if cfg!(windows) {
233 match temp_file.persist_noclobber(&new_path) {
237 Ok(file) => Ok(file),
238 Err(PersistError { error, file: _ }) => {
239 if let Ok(existing_file) = File::open(new_path) {
240 Ok(existing_file)
242 } else {
243 Err(error)
244 }
245 }
246 }
247 } else {
248 temp_file
252 .persist(new_path)
253 .map_err(|PersistError { error, file: _ }| error)
254 }
255}
256
257#[derive(Debug, Eq, Hash, PartialEq)]
263pub struct FileIdentity(platform::FileIdentity);
264
265impl FileIdentity {
266 pub fn from_symlink_path(path: impl AsRef<Path>) -> io::Result<Self> {
268 platform::file_identity_from_symlink_path(path.as_ref()).map(Self)
269 }
270
271 pub fn from_file(file: File) -> io::Result<Self> {
274 platform::file_identity_from_file(file).map(Self)
275 }
276}
277
278pub async fn copy_async_to_sync<R: AsyncRead, W: Write + ?Sized>(
281 reader: R,
282 writer: &mut W,
283) -> io::Result<usize> {
284 let mut buf = vec![0; 16 << 10];
285 let mut total_written_bytes = 0;
286
287 let mut reader = std::pin::pin!(reader);
288 loop {
289 let written_bytes = reader.read(&mut buf).await?;
290 if written_bytes == 0 {
291 return Ok(total_written_bytes);
292 }
293 writer.write_all(&buf[0..written_bytes])?;
294 total_written_bytes += written_bytes;
295 }
296}
297
298#[cfg(unix)]
299mod platform {
300 use std::convert::Infallible;
301 use std::ffi::OsStr;
302 use std::fs;
303 use std::fs::File;
304 use std::io;
305 use std::os::unix::ffi::OsStrExt as _;
306 use std::os::unix::fs::MetadataExt as _;
307 use std::os::unix::fs::PermissionsExt;
308 use std::os::unix::fs::symlink;
309 use std::path::Path;
310
311 pub type BadOsStrEncoding = Infallible;
312
313 pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
314 Ok(OsStr::from_bytes(data))
315 }
316
317 pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
318 Ok(data.as_bytes())
319 }
320
321 pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> {
324 let temp_file = tempfile::tempfile_in(path)?;
326 let old_mode = temp_file.metadata()?.permissions().mode();
327 let new_mode = old_mode ^ 0o100;
328 let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode));
329 match result {
330 Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false),
332 Err(err) => Err(err),
333 Ok(()) => {
334 let mode = temp_file.metadata()?.permissions().mode();
336 Ok(mode == new_mode)
337 }
338 }
339 }
340
341 pub fn check_symlink_support() -> io::Result<bool> {
343 Ok(true)
344 }
345
346 pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
350 symlink(original, link)
351 }
352
353 pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
357 symlink(original, link)
358 }
359
360 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
361 pub struct FileIdentity {
362 dev: u64,
364 ino: u64,
365 }
366
367 impl FileIdentity {
368 fn from_metadata(metadata: fs::Metadata) -> Self {
369 Self {
370 dev: metadata.dev(),
371 ino: metadata.ino(),
372 }
373 }
374 }
375
376 pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
377 path.symlink_metadata().map(FileIdentity::from_metadata)
378 }
379
380 pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
381 file.metadata().map(FileIdentity::from_metadata)
382 }
383}
384
385#[cfg(windows)]
386mod platform {
387 use std::fs::File;
388 use std::fs::OpenOptions;
389 use std::io;
390 use std::os::windows::fs::OpenOptionsExt as _;
391 pub use std::os::windows::fs::symlink_dir;
392 pub use std::os::windows::fs::symlink_file;
393 use std::path::Path;
394
395 use winreg::RegKey;
396 use winreg::enums::HKEY_LOCAL_MACHINE;
397
398 pub use super::fallback::BadOsStrEncoding;
399 pub use super::fallback::os_str_from_bytes;
400 pub use super::fallback::os_str_to_bytes;
401
402 pub fn check_symlink_support() -> io::Result<bool> {
408 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
409 let sideloading =
410 hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?;
411 let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?;
412 Ok(developer_mode == 1)
413 }
414
415 pub type FileIdentity = same_file::Handle;
416
417 pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
418 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
427 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
428 let file = OpenOptions::new()
429 .read(true)
430 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
431 .open(path)?;
432 same_file::Handle::from_file(file)
433 }
434
435 pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
436 same_file::Handle::from_file(file)
437 }
438}
439
440#[cfg_attr(unix, expect(dead_code))]
441mod fallback {
442 use std::ffi::OsStr;
443
444 use thiserror::Error;
445
446 #[derive(Debug, Error)]
448 #[error("Invalid UTF-8 sequence")]
449 pub struct BadOsStrEncoding;
450
451 pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
452 Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref())
453 }
454
455 pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
456 Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref())
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use std::io::Write as _;
463
464 use futures::io::Cursor;
465 use itertools::Itertools as _;
466 use pollster::FutureExt as _;
467 use test_case::test_case;
468
469 use super::*;
470 use crate::tests::TestResult;
471 use crate::tests::new_temp_dir;
472
473 #[test]
474 #[cfg(unix)]
475 fn exec_bit_support_in_temp_dir() -> TestResult {
476 let dir = new_temp_dir();
480 let supported = check_executable_bit_support(dir.path())?;
481 assert!(supported);
482 Ok(())
483 }
484
485 #[test]
486 fn test_path_bytes_roundtrip() -> TestResult {
487 let bytes = b"ascii";
488 let path = path_from_bytes(bytes)?;
489 assert_eq!(path_to_bytes(path)?, bytes);
490
491 let bytes = b"utf-8.\xc3\xa0";
492 let path = path_from_bytes(bytes)?;
493 assert_eq!(path_to_bytes(path)?, bytes);
494
495 let bytes = b"latin1.\xe0";
496 if cfg!(unix) {
497 let path = path_from_bytes(bytes)?;
498 assert_eq!(path_to_bytes(path)?, bytes);
499 } else {
500 assert!(path_from_bytes(bytes).is_err());
501 }
502 Ok(())
503 }
504
505 #[test]
506 fn normalize_too_many_dot_dot() {
507 assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
508 assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
509 assert_eq!(
510 normalize_path(Path::new("foo/../../..")),
511 Path::new("../..")
512 );
513 assert_eq!(
514 normalize_path(Path::new("foo/../../../bar/baz/..")),
515 Path::new("../../bar")
516 );
517 }
518
519 #[test]
520 fn test_slash_path() {
521 assert_eq!(slash_path(Path::new("")), Path::new(""));
522 assert_eq!(slash_path(Path::new("foo")), Path::new("foo"));
523 assert_eq!(slash_path(Path::new("foo/bar")), Path::new("foo/bar"));
524 assert_eq!(slash_path(Path::new("foo/bar/..")), Path::new("foo/bar/.."));
525 assert_eq!(
526 slash_path(Path::new(r"foo\bar")),
527 if cfg!(windows) {
528 Path::new("foo/bar")
529 } else {
530 Path::new(r"foo\bar")
531 }
532 );
533 assert_eq!(
534 slash_path(Path::new(r"..\foo\bar")),
535 if cfg!(windows) {
536 Path::new("../foo/bar")
537 } else {
538 Path::new(r"..\foo\bar")
539 }
540 );
541 }
542
543 #[test]
544 fn test_persist_no_existing_file() -> TestResult {
545 let temp_dir = new_temp_dir();
546 let target = temp_dir.path().join("file");
547 let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
548 temp_file.write_all(b"contents")?;
549 assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
550 Ok(())
551 }
552
553 #[test_case(false ; "existing file open")]
554 #[test_case(true ; "existing file closed")]
555 fn test_persist_target_exists(existing_file_closed: bool) -> TestResult {
556 let temp_dir = new_temp_dir();
557 let target = temp_dir.path().join("file");
558 let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
559 temp_file.write_all(b"contents")?;
560
561 let mut file = File::create(&target)?;
562 file.write_all(b"contents")?;
563 if existing_file_closed {
564 drop(file);
565 }
566
567 assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
568 Ok(())
569 }
570
571 #[test]
572 fn test_file_identity_hard_link() -> TestResult {
573 let temp_dir = new_temp_dir();
574 let file_path = temp_dir.path().join("file");
575 let other_file_path = temp_dir.path().join("other_file");
576 let link_path = temp_dir.path().join("link");
577 fs::write(&file_path, "")?;
578 fs::write(&other_file_path, "")?;
579 fs::hard_link(&file_path, &link_path)?;
580 assert_eq!(
581 FileIdentity::from_symlink_path(&file_path)?,
582 FileIdentity::from_symlink_path(&link_path)?
583 );
584 assert_ne!(
585 FileIdentity::from_symlink_path(&other_file_path)?,
586 FileIdentity::from_symlink_path(&link_path)?
587 );
588 assert_eq!(
589 FileIdentity::from_symlink_path(&file_path)?,
590 FileIdentity::from_file(File::open(&link_path)?)?
591 );
592 Ok(())
593 }
594
595 #[cfg(unix)]
596 #[test]
597 fn test_file_identity_unix_symlink_dir() -> TestResult {
598 let temp_dir = new_temp_dir();
599 let dir_path = temp_dir.path().join("dir");
600 let symlink_path = temp_dir.path().join("symlink");
601 fs::create_dir(&dir_path)?;
602 std::os::unix::fs::symlink("dir", &symlink_path)?;
603 assert_eq!(
605 FileIdentity::from_symlink_path(&symlink_path)?,
606 FileIdentity::from_symlink_path(&symlink_path)?
607 );
608 assert_ne!(
610 FileIdentity::from_symlink_path(&dir_path)?,
611 FileIdentity::from_symlink_path(&symlink_path)?
612 );
613 assert_eq!(
615 FileIdentity::from_symlink_path(&dir_path)?,
616 FileIdentity::from_file(File::open(&symlink_path)?)?
617 );
618 assert_ne!(
619 FileIdentity::from_symlink_path(&symlink_path)?,
620 FileIdentity::from_file(File::open(&symlink_path)?)?
621 );
622 Ok(())
623 }
624
625 #[cfg(windows)]
626 #[test]
627 fn test_file_identity_windows_symlink_file() -> TestResult {
628 if !check_symlink_support()? {
629 return Ok(());
630 }
631 let temp_dir = new_temp_dir();
632 let file_path = temp_dir.path().join("file");
633 let symlink_path = temp_dir.path().join("symlink");
634 fs::write(&file_path, "")?;
635 symlink_file("file", &symlink_path)?;
636 assert_eq!(
638 FileIdentity::from_symlink_path(&symlink_path)?,
639 FileIdentity::from_symlink_path(&symlink_path)?
640 );
641 assert_ne!(
643 FileIdentity::from_symlink_path(&file_path)?,
644 FileIdentity::from_symlink_path(&symlink_path)?
645 );
646 assert_eq!(
648 FileIdentity::from_symlink_path(&file_path)?,
649 FileIdentity::from_file(File::open(&symlink_path)?)?
650 );
651 assert_ne!(
652 FileIdentity::from_symlink_path(&symlink_path)?,
653 FileIdentity::from_file(File::open(&symlink_path)?)?
654 );
655 Ok(())
656 }
657
658 #[cfg(windows)]
659 #[test]
660 fn test_file_identity_windows_symlink_dir() -> TestResult {
661 if !check_symlink_support()? {
662 return Ok(());
663 }
664 let temp_dir = new_temp_dir();
665 let dir_path = temp_dir.path().join("dir");
666 let symlink_path = temp_dir.path().join("symlink");
667 fs::create_dir(&dir_path)?;
668 symlink_dir("dir", &symlink_path)?;
669 assert_eq!(
671 FileIdentity::from_symlink_path(&symlink_path)?,
672 FileIdentity::from_symlink_path(&symlink_path)?
673 );
674 assert_ne!(
678 FileIdentity::from_symlink_path(&dir_path)?,
679 FileIdentity::from_symlink_path(&symlink_path)?
680 );
681 Ok(())
682 }
683
684 #[test]
685 fn test_file_identity_directory() -> TestResult {
686 let temp_dir = new_temp_dir();
687 let dir_path = temp_dir.path().join("dir");
688 let other_dir_path = temp_dir.path().join("other_dir");
689 fs::create_dir(&dir_path)?;
690 fs::create_dir(&other_dir_path)?;
691 assert_eq!(
693 FileIdentity::from_symlink_path(&dir_path)?,
694 FileIdentity::from_symlink_path(&dir_path)?
695 );
696 assert_ne!(
698 FileIdentity::from_symlink_path(&dir_path)?,
699 FileIdentity::from_symlink_path(&other_dir_path)?
700 );
701 Ok(())
702 }
703
704 #[cfg(unix)]
705 #[test]
706 fn test_file_identity_unix_symlink_loop() -> TestResult {
707 let temp_dir = new_temp_dir();
708 let lower_file_path = temp_dir.path().join("file");
709 let upper_file_path = temp_dir.path().join("FILE");
710 let lower_symlink_path = temp_dir.path().join("symlink");
711 let upper_symlink_path = temp_dir.path().join("SYMLINK");
712 fs::write(&lower_file_path, "")?;
713 std::os::unix::fs::symlink("symlink", &lower_symlink_path)?;
714 let is_icase_fs = upper_file_path.try_exists()?;
715 assert_eq!(
717 FileIdentity::from_symlink_path(&lower_symlink_path)?,
718 FileIdentity::from_symlink_path(&lower_symlink_path)?
719 );
720 assert_ne!(
721 FileIdentity::from_symlink_path(&lower_symlink_path)?,
722 FileIdentity::from_symlink_path(&lower_file_path)?
723 );
724 if is_icase_fs {
725 assert_eq!(
726 FileIdentity::from_symlink_path(&lower_symlink_path)?,
727 FileIdentity::from_symlink_path(&upper_symlink_path)?
728 );
729 } else {
730 assert!(FileIdentity::from_symlink_path(&upper_symlink_path).is_err());
731 }
732 Ok(())
733 }
734
735 #[test]
736 fn test_copy_async_to_sync_small() -> TestResult {
737 let input = b"hello";
738 let mut output = vec![];
739
740 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
741 assert!(result.is_ok());
742 assert_eq!(result?, 5);
743 assert_eq!(output, input);
744 Ok(())
745 }
746
747 #[test]
748 fn test_copy_async_to_sync_large() -> TestResult {
749 let input = (0..100u8).cycle().take(40000).collect_vec();
751 let mut output = vec![];
752
753 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
754 assert!(result.is_ok());
755 assert_eq!(result?, 40000);
756 assert_eq!(output, input);
757 Ok(())
758 }
759}