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::Read;
24use std::io::Write;
25use std::path::Component;
26use std::path::Path;
27use std::path::PathBuf;
28use std::pin::Pin;
29use std::task::Poll;
30
31use tempfile::NamedTempFile;
32use tempfile::PersistError;
33use thiserror::Error;
34use tokio::io::AsyncRead;
35use tokio::io::AsyncReadExt as _;
36use tokio::io::ReadBuf;
37
38#[cfg(unix)]
39pub use self::platform::check_executable_bit_support;
40pub use self::platform::check_symlink_support;
41pub use self::platform::symlink_dir;
42pub use self::platform::symlink_file;
43
44#[derive(Debug, Error)]
45#[error("Cannot access {path}")]
46pub struct PathError {
47 pub path: PathBuf,
48 pub source: io::Error,
49}
50
51pub trait IoResultExt<T> {
52 fn context(self, path: impl AsRef<Path>) -> Result<T, PathError>;
53}
54
55impl<T> IoResultExt<T> for io::Result<T> {
56 fn context(self, path: impl AsRef<Path>) -> Result<T, PathError> {
57 self.map_err(|error| PathError {
58 path: path.as_ref().to_path_buf(),
59 source: error,
60 })
61 }
62}
63
64pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> {
70 match fs::create_dir(dirname) {
71 Ok(()) => Ok(()),
72 Err(_) if dirname.is_dir() => Ok(()),
73 Err(e) => Err(e),
74 }
75}
76
77pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> {
81 for entry in dirname.read_dir().context(dirname)? {
82 let entry = entry.context(dirname)?;
83 let path = entry.path();
84 fs::remove_file(&path).context(&path)?;
85 }
86 Ok(())
87}
88
89pub fn is_empty_dir(path: &Path) -> Result<bool, PathError> {
91 match path.read_dir() {
92 Ok(mut entries) => Ok(entries.next().is_none()),
93 Err(error) => match error.kind() {
94 ErrorKind::NotADirectory => Ok(false),
95 ErrorKind::NotFound => Ok(false),
96 _ => Err(error).context(path)?,
97 },
98 }
99}
100
101#[derive(Debug, Error)]
102#[error(transparent)]
103pub struct BadPathEncoding(platform::BadOsStrEncoding);
104
105pub fn path_from_bytes(bytes: &[u8]) -> Result<&Path, BadPathEncoding> {
110 let s = platform::os_str_from_bytes(bytes).map_err(BadPathEncoding)?;
111 Ok(Path::new(s))
112}
113
114pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> {
122 platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding)
123}
124
125pub fn expand_home_path(path_str: &str) -> PathBuf {
127 if let Some(remainder) = path_str.strip_prefix("~/")
128 && let Ok(home_dir) = etcetera::home_dir()
129 {
130 return home_dir.join(remainder);
131 }
132 PathBuf::from(path_str)
133}
134
135pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
140 for (i, base) in from.ancestors().enumerate() {
142 if let Ok(suffix) = to.strip_prefix(base) {
143 if i == 0 && suffix.as_os_str().is_empty() {
144 return ".".into();
145 } else {
146 return std::iter::repeat_n(Path::new(".."), i)
147 .chain(std::iter::once(suffix))
148 .collect();
149 }
150 }
151 }
152
153 to.to_owned()
155}
156
157pub fn normalize_path(path: &Path) -> PathBuf {
159 let mut result = PathBuf::new();
160 for c in path.components() {
161 match c {
162 Component::CurDir => {}
163 Component::ParentDir
164 if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
165 {
166 let popped = result.pop();
168 assert!(popped);
169 }
170 _ => {
171 result.push(c);
172 }
173 }
174 }
175
176 if result.as_os_str().is_empty() {
177 ".".into()
178 } else {
179 result
180 }
181}
182
183pub fn slash_path(path: &Path) -> Cow<'_, Path> {
188 if cfg!(windows) {
189 Cow::Owned(to_slash_separated(path).into())
190 } else {
191 Cow::Borrowed(path)
192 }
193}
194
195fn to_slash_separated(path: &Path) -> OsString {
196 let mut buf = OsString::with_capacity(path.as_os_str().len());
197 let mut components = path.components();
198 match components.next() {
199 Some(c) => buf.push(c),
200 None => return buf,
201 }
202 for c in components {
203 buf.push("/");
204 buf.push(c);
205 }
206 buf
207}
208
209pub fn persist_temp_file<P: AsRef<Path>>(
217 temp_file: NamedTempFile,
218 new_path: P,
219) -> io::Result<File> {
220 temp_file.as_file().sync_data()?;
222 temp_file
223 .persist(new_path)
224 .map_err(|PersistError { error, file: _ }| error)
225}
226
227pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
230 temp_file: NamedTempFile,
231 new_path: P,
232) -> io::Result<File> {
233 temp_file.as_file().sync_data()?;
236 if cfg!(windows) {
237 match temp_file.persist_noclobber(&new_path) {
241 Ok(file) => Ok(file),
242 Err(PersistError { error, file: _ }) => {
243 if let Ok(existing_file) = File::open(new_path) {
244 Ok(existing_file)
246 } else {
247 Err(error)
248 }
249 }
250 }
251 } else {
252 temp_file
256 .persist(new_path)
257 .map_err(|PersistError { error, file: _ }| error)
258 }
259}
260
261#[derive(Debug, Eq, Hash, PartialEq)]
267pub struct FileIdentity(platform::FileIdentity);
268
269impl FileIdentity {
270 pub fn from_symlink_path(path: impl AsRef<Path>) -> io::Result<Self> {
274 platform::file_identity_from_symlink_path(path.as_ref()).map(Self)
275 }
276
277 pub fn from_file(file: File) -> io::Result<Self> {
280 platform::file_identity_from_file(file).map(Self)
281 }
282}
283
284pub async fn copy_async_to_sync<R: AsyncRead, W: Write + ?Sized>(
287 reader: R,
288 writer: &mut W,
289) -> io::Result<usize> {
290 let mut buf = vec![0; 16 << 10];
291 let mut total_written_bytes = 0;
292
293 let mut reader = std::pin::pin!(reader);
294 loop {
295 let written_bytes = reader.read(&mut buf).await?;
296 if written_bytes == 0 {
297 return Ok(total_written_bytes);
298 }
299 writer.write_all(&buf[0..written_bytes])?;
300 total_written_bytes += written_bytes;
301 }
302}
303
304pub struct BlockingAsyncReader<R> {
308 reader: R,
309}
310
311impl<R: Read + Unpin> BlockingAsyncReader<R> {
312 pub fn new(reader: R) -> Self {
314 Self { reader }
315 }
316}
317
318impl<R: Read + Unpin> AsyncRead for BlockingAsyncReader<R> {
319 fn poll_read(
320 mut self: Pin<&mut Self>,
321 _cx: &mut std::task::Context<'_>,
322 buf: &mut ReadBuf<'_>,
323 ) -> Poll<io::Result<()>> {
324 let num_bytes_read = self.reader.read(buf.initialize_unfilled())?;
325 buf.advance(num_bytes_read);
326 Poll::Ready(Ok(()))
327 }
328}
329
330#[cfg(unix)]
331mod platform {
332 use std::convert::Infallible;
333 use std::ffi::OsStr;
334 use std::fs;
335 use std::fs::File;
336 use std::io;
337 use std::os::unix::ffi::OsStrExt as _;
338 use std::os::unix::fs::MetadataExt as _;
339 use std::os::unix::fs::PermissionsExt;
340 use std::os::unix::fs::symlink;
341 use std::path::Path;
342
343 pub type BadOsStrEncoding = Infallible;
344
345 pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
346 Ok(OsStr::from_bytes(data))
347 }
348
349 pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
350 Ok(data.as_bytes())
351 }
352
353 pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> {
356 let temp_file = tempfile::tempfile_in(path)?;
358 let old_mode = temp_file.metadata()?.permissions().mode();
359 let new_mode = old_mode ^ 0o100;
360 let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode));
361 match result {
362 Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false),
364 Err(err) => Err(err),
365 Ok(()) => {
366 let mode = temp_file.metadata()?.permissions().mode();
368 Ok(mode == new_mode)
369 }
370 }
371 }
372
373 pub fn check_symlink_support() -> io::Result<bool> {
375 Ok(true)
376 }
377
378 pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
382 symlink(original, link)
383 }
384
385 pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
389 symlink(original, link)
390 }
391
392 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
393 pub struct FileIdentity {
394 dev: u64,
396 ino: u64,
397 }
398
399 impl FileIdentity {
400 fn from_metadata(metadata: fs::Metadata) -> Self {
401 Self {
402 dev: metadata.dev(),
403 ino: metadata.ino(),
404 }
405 }
406 }
407
408 pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
409 path.symlink_metadata().map(FileIdentity::from_metadata)
410 }
411
412 pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
413 file.metadata().map(FileIdentity::from_metadata)
414 }
415}
416
417#[cfg(windows)]
418mod platform {
419 use std::fs::File;
420 use std::io;
421 pub use std::os::windows::fs::symlink_dir;
422 pub use std::os::windows::fs::symlink_file;
423 use std::path::Path;
424
425 use winreg::RegKey;
426 use winreg::enums::HKEY_LOCAL_MACHINE;
427
428 pub use super::fallback::BadOsStrEncoding;
429 pub use super::fallback::os_str_from_bytes;
430 pub use super::fallback::os_str_to_bytes;
431
432 pub fn check_symlink_support() -> io::Result<bool> {
438 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
439 let sideloading =
440 hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?;
441 let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?;
442 Ok(developer_mode == 1)
443 }
444
445 pub type FileIdentity = same_file::Handle;
446
447 pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
454 same_file::Handle::from_path(path)
455 }
456
457 pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
458 same_file::Handle::from_file(file)
459 }
460}
461
462#[cfg_attr(unix, expect(dead_code))]
463mod fallback {
464 use std::ffi::OsStr;
465
466 use thiserror::Error;
467
468 #[derive(Debug, Error)]
470 #[error("Invalid UTF-8 sequence")]
471 pub struct BadOsStrEncoding;
472
473 pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
474 Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref())
475 }
476
477 pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
478 Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref())
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use std::io::Cursor;
485 use std::io::Write as _;
486
487 use itertools::Itertools as _;
488 use pollster::FutureExt as _;
489 use test_case::test_case;
490
491 use super::*;
492 use crate::tests::TestResult;
493 use crate::tests::new_temp_dir;
494
495 #[test]
496 #[cfg(unix)]
497 fn exec_bit_support_in_temp_dir() -> TestResult {
498 let dir = new_temp_dir();
502 let supported = check_executable_bit_support(dir.path())?;
503 assert!(supported);
504 Ok(())
505 }
506
507 #[test]
508 fn test_path_bytes_roundtrip() -> TestResult {
509 let bytes = b"ascii";
510 let path = path_from_bytes(bytes)?;
511 assert_eq!(path_to_bytes(path)?, bytes);
512
513 let bytes = b"utf-8.\xc3\xa0";
514 let path = path_from_bytes(bytes)?;
515 assert_eq!(path_to_bytes(path)?, bytes);
516
517 let bytes = b"latin1.\xe0";
518 if cfg!(unix) {
519 let path = path_from_bytes(bytes)?;
520 assert_eq!(path_to_bytes(path)?, bytes);
521 } else {
522 assert!(path_from_bytes(bytes).is_err());
523 }
524 Ok(())
525 }
526
527 #[test]
528 fn normalize_too_many_dot_dot() {
529 assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
530 assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
531 assert_eq!(
532 normalize_path(Path::new("foo/../../..")),
533 Path::new("../..")
534 );
535 assert_eq!(
536 normalize_path(Path::new("foo/../../../bar/baz/..")),
537 Path::new("../../bar")
538 );
539 }
540
541 #[test]
542 fn test_slash_path() {
543 assert_eq!(slash_path(Path::new("")), Path::new(""));
544 assert_eq!(slash_path(Path::new("foo")), Path::new("foo"));
545 assert_eq!(slash_path(Path::new("foo/bar")), Path::new("foo/bar"));
546 assert_eq!(slash_path(Path::new("foo/bar/..")), Path::new("foo/bar/.."));
547 assert_eq!(
548 slash_path(Path::new(r"foo\bar")),
549 if cfg!(windows) {
550 Path::new("foo/bar")
551 } else {
552 Path::new(r"foo\bar")
553 }
554 );
555 assert_eq!(
556 slash_path(Path::new(r"..\foo\bar")),
557 if cfg!(windows) {
558 Path::new("../foo/bar")
559 } else {
560 Path::new(r"..\foo\bar")
561 }
562 );
563 }
564
565 #[test]
566 fn test_persist_no_existing_file() -> TestResult {
567 let temp_dir = new_temp_dir();
568 let target = temp_dir.path().join("file");
569 let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
570 temp_file.write_all(b"contents")?;
571 assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
572 Ok(())
573 }
574
575 #[test_case(false ; "existing file open")]
576 #[test_case(true ; "existing file closed")]
577 fn test_persist_target_exists(existing_file_closed: bool) -> TestResult {
578 let temp_dir = new_temp_dir();
579 let target = temp_dir.path().join("file");
580 let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
581 temp_file.write_all(b"contents")?;
582
583 let mut file = File::create(&target)?;
584 file.write_all(b"contents")?;
585 if existing_file_closed {
586 drop(file);
587 }
588
589 assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
590 Ok(())
591 }
592
593 #[test]
594 fn test_file_identity_hard_link() -> TestResult {
595 let temp_dir = new_temp_dir();
596 let file_path = temp_dir.path().join("file");
597 let other_file_path = temp_dir.path().join("other_file");
598 let link_path = temp_dir.path().join("link");
599 fs::write(&file_path, "")?;
600 fs::write(&other_file_path, "")?;
601 fs::hard_link(&file_path, &link_path)?;
602 assert_eq!(
603 FileIdentity::from_symlink_path(&file_path)?,
604 FileIdentity::from_symlink_path(&link_path)?
605 );
606 assert_ne!(
607 FileIdentity::from_symlink_path(&other_file_path)?,
608 FileIdentity::from_symlink_path(&link_path)?
609 );
610 assert_eq!(
611 FileIdentity::from_symlink_path(&file_path)?,
612 FileIdentity::from_file(File::open(&link_path)?)?
613 );
614 Ok(())
615 }
616
617 #[cfg(unix)]
618 #[test]
619 fn test_file_identity_unix_symlink_dir() -> TestResult {
620 let temp_dir = new_temp_dir();
621 let dir_path = temp_dir.path().join("dir");
622 let symlink_path = temp_dir.path().join("symlink");
623 fs::create_dir(&dir_path)?;
624 std::os::unix::fs::symlink("dir", &symlink_path)?;
625 assert_eq!(
627 FileIdentity::from_symlink_path(&symlink_path)?,
628 FileIdentity::from_symlink_path(&symlink_path)?
629 );
630 assert_ne!(
632 FileIdentity::from_symlink_path(&dir_path)?,
633 FileIdentity::from_symlink_path(&symlink_path)?
634 );
635 assert_eq!(
637 FileIdentity::from_symlink_path(&dir_path)?,
638 FileIdentity::from_file(File::open(&symlink_path)?)?
639 );
640 assert_ne!(
641 FileIdentity::from_symlink_path(&symlink_path)?,
642 FileIdentity::from_file(File::open(&symlink_path)?)?
643 );
644 Ok(())
645 }
646
647 #[cfg(unix)]
648 #[test]
649 fn test_file_identity_unix_symlink_loop() -> TestResult {
650 let temp_dir = new_temp_dir();
651 let lower_file_path = temp_dir.path().join("file");
652 let upper_file_path = temp_dir.path().join("FILE");
653 let lower_symlink_path = temp_dir.path().join("symlink");
654 let upper_symlink_path = temp_dir.path().join("SYMLINK");
655 fs::write(&lower_file_path, "")?;
656 std::os::unix::fs::symlink("symlink", &lower_symlink_path)?;
657 let is_icase_fs = upper_file_path.try_exists()?;
658 assert_eq!(
660 FileIdentity::from_symlink_path(&lower_symlink_path)?,
661 FileIdentity::from_symlink_path(&lower_symlink_path)?
662 );
663 assert_ne!(
664 FileIdentity::from_symlink_path(&lower_symlink_path)?,
665 FileIdentity::from_symlink_path(&lower_file_path)?
666 );
667 if is_icase_fs {
668 assert_eq!(
669 FileIdentity::from_symlink_path(&lower_symlink_path)?,
670 FileIdentity::from_symlink_path(&upper_symlink_path)?
671 );
672 } else {
673 assert!(FileIdentity::from_symlink_path(&upper_symlink_path).is_err());
674 }
675 Ok(())
676 }
677
678 #[test]
679 fn test_copy_async_to_sync_small() -> TestResult {
680 let input = b"hello";
681 let mut output = vec![];
682
683 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
684 assert!(result.is_ok());
685 assert_eq!(result?, 5);
686 assert_eq!(output, input);
687 Ok(())
688 }
689
690 #[test]
691 fn test_copy_async_to_sync_large() -> TestResult {
692 let input = (0..100u8).cycle().take(40000).collect_vec();
694 let mut output = vec![];
695
696 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
697 assert!(result.is_ok());
698 assert_eq!(result?, 40000);
699 assert_eq!(output, input);
700 Ok(())
701 }
702
703 #[test]
704 fn test_blocking_async_reader() -> TestResult {
705 let input = b"hello";
706 let sync_reader = Cursor::new(&input);
707 let mut async_reader = BlockingAsyncReader::new(sync_reader);
708
709 let mut buf = [0u8; 3];
710 let num_bytes_read = async_reader.read(&mut buf).block_on()?;
711 assert_eq!(num_bytes_read, 3);
712 assert_eq!(&buf, &input[0..3]);
713
714 let num_bytes_read = async_reader.read(&mut buf).block_on()?;
715 assert_eq!(num_bytes_read, 2);
716 assert_eq!(&buf[0..2], &input[3..5]);
717 Ok(())
718 }
719
720 #[test]
721 fn test_blocking_async_reader_read_to_end() -> TestResult {
722 let input = b"hello";
723 let sync_reader = Cursor::new(&input);
724 let mut async_reader = BlockingAsyncReader::new(sync_reader);
725
726 let mut buf = vec![];
727 let num_bytes_read = async_reader.read_to_end(&mut buf).block_on()?;
728 assert_eq!(num_bytes_read, input.len());
729 assert_eq!(&buf, &input);
730 Ok(())
731 }
732}