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_str) = std::env::var("HOME")
129 {
130 return PathBuf::from(home_dir_str).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 let mut result = PathBuf::from_iter(std::iter::repeat_n("..", i));
147 result.push(suffix);
148 return result;
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::new_temp_dir;
493
494 #[test]
495 #[cfg(unix)]
496 fn exec_bit_support_in_temp_dir() {
497 let dir = new_temp_dir();
501 let supported = check_executable_bit_support(dir.path()).unwrap();
502 assert!(supported);
503 }
504
505 #[test]
506 fn test_path_bytes_roundtrip() {
507 let bytes = b"ascii";
508 let path = path_from_bytes(bytes).unwrap();
509 assert_eq!(path_to_bytes(path).unwrap(), bytes);
510
511 let bytes = b"utf-8.\xc3\xa0";
512 let path = path_from_bytes(bytes).unwrap();
513 assert_eq!(path_to_bytes(path).unwrap(), bytes);
514
515 let bytes = b"latin1.\xe0";
516 if cfg!(unix) {
517 let path = path_from_bytes(bytes).unwrap();
518 assert_eq!(path_to_bytes(path).unwrap(), bytes);
519 } else {
520 assert!(path_from_bytes(bytes).is_err());
521 }
522 }
523
524 #[test]
525 fn normalize_too_many_dot_dot() {
526 assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
527 assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
528 assert_eq!(
529 normalize_path(Path::new("foo/../../..")),
530 Path::new("../..")
531 );
532 assert_eq!(
533 normalize_path(Path::new("foo/../../../bar/baz/..")),
534 Path::new("../../bar")
535 );
536 }
537
538 #[test]
539 fn test_slash_path() {
540 assert_eq!(slash_path(Path::new("")), Path::new(""));
541 assert_eq!(slash_path(Path::new("foo")), Path::new("foo"));
542 assert_eq!(slash_path(Path::new("foo/bar")), Path::new("foo/bar"));
543 assert_eq!(slash_path(Path::new("foo/bar/..")), Path::new("foo/bar/.."));
544 assert_eq!(
545 slash_path(Path::new(r"foo\bar")),
546 if cfg!(windows) {
547 Path::new("foo/bar")
548 } else {
549 Path::new(r"foo\bar")
550 }
551 );
552 assert_eq!(
553 slash_path(Path::new(r"..\foo\bar")),
554 if cfg!(windows) {
555 Path::new("../foo/bar")
556 } else {
557 Path::new(r"..\foo\bar")
558 }
559 );
560 }
561
562 #[test]
563 fn test_persist_no_existing_file() {
564 let temp_dir = new_temp_dir();
565 let target = temp_dir.path().join("file");
566 let mut temp_file = NamedTempFile::new_in(&temp_dir).unwrap();
567 temp_file.write_all(b"contents").unwrap();
568 assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
569 }
570
571 #[test_case(false ; "existing file open")]
572 #[test_case(true ; "existing file closed")]
573 fn test_persist_target_exists(existing_file_closed: bool) {
574 let temp_dir = new_temp_dir();
575 let target = temp_dir.path().join("file");
576 let mut temp_file = NamedTempFile::new_in(&temp_dir).unwrap();
577 temp_file.write_all(b"contents").unwrap();
578
579 let mut file = File::create(&target).unwrap();
580 file.write_all(b"contents").unwrap();
581 if existing_file_closed {
582 drop(file);
583 }
584
585 assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
586 }
587
588 #[test]
589 fn test_file_identity_hard_link() {
590 let temp_dir = new_temp_dir();
591 let file_path = temp_dir.path().join("file");
592 let other_file_path = temp_dir.path().join("other_file");
593 let link_path = temp_dir.path().join("link");
594 fs::write(&file_path, "").unwrap();
595 fs::write(&other_file_path, "").unwrap();
596 fs::hard_link(&file_path, &link_path).unwrap();
597 assert_eq!(
598 FileIdentity::from_symlink_path(&file_path).unwrap(),
599 FileIdentity::from_symlink_path(&link_path).unwrap()
600 );
601 assert_ne!(
602 FileIdentity::from_symlink_path(&other_file_path).unwrap(),
603 FileIdentity::from_symlink_path(&link_path).unwrap()
604 );
605 assert_eq!(
606 FileIdentity::from_symlink_path(&file_path).unwrap(),
607 FileIdentity::from_file(File::open(&link_path).unwrap()).unwrap()
608 );
609 }
610
611 #[cfg(unix)]
612 #[test]
613 fn test_file_identity_unix_symlink_dir() {
614 let temp_dir = new_temp_dir();
615 let dir_path = temp_dir.path().join("dir");
616 let symlink_path = temp_dir.path().join("symlink");
617 fs::create_dir(&dir_path).unwrap();
618 std::os::unix::fs::symlink("dir", &symlink_path).unwrap();
619 assert_eq!(
621 FileIdentity::from_symlink_path(&symlink_path).unwrap(),
622 FileIdentity::from_symlink_path(&symlink_path).unwrap()
623 );
624 assert_ne!(
626 FileIdentity::from_symlink_path(&dir_path).unwrap(),
627 FileIdentity::from_symlink_path(&symlink_path).unwrap()
628 );
629 assert_eq!(
631 FileIdentity::from_symlink_path(&dir_path).unwrap(),
632 FileIdentity::from_file(File::open(&symlink_path).unwrap()).unwrap()
633 );
634 assert_ne!(
635 FileIdentity::from_symlink_path(&symlink_path).unwrap(),
636 FileIdentity::from_file(File::open(&symlink_path).unwrap()).unwrap()
637 );
638 }
639
640 #[cfg(unix)]
641 #[test]
642 fn test_file_identity_unix_symlink_loop() {
643 let temp_dir = new_temp_dir();
644 let lower_file_path = temp_dir.path().join("file");
645 let upper_file_path = temp_dir.path().join("FILE");
646 let lower_symlink_path = temp_dir.path().join("symlink");
647 let upper_symlink_path = temp_dir.path().join("SYMLINK");
648 fs::write(&lower_file_path, "").unwrap();
649 std::os::unix::fs::symlink("symlink", &lower_symlink_path).unwrap();
650 let is_icase_fs = upper_file_path.try_exists().unwrap();
651 assert_eq!(
653 FileIdentity::from_symlink_path(&lower_symlink_path).unwrap(),
654 FileIdentity::from_symlink_path(&lower_symlink_path).unwrap()
655 );
656 assert_ne!(
657 FileIdentity::from_symlink_path(&lower_symlink_path).unwrap(),
658 FileIdentity::from_symlink_path(&lower_file_path).unwrap()
659 );
660 if is_icase_fs {
661 assert_eq!(
662 FileIdentity::from_symlink_path(&lower_symlink_path).unwrap(),
663 FileIdentity::from_symlink_path(&upper_symlink_path).unwrap()
664 );
665 } else {
666 assert!(FileIdentity::from_symlink_path(&upper_symlink_path).is_err());
667 }
668 }
669
670 #[test]
671 fn test_copy_async_to_sync_small() {
672 let input = b"hello";
673 let mut output = vec![];
674
675 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
676 assert!(result.is_ok());
677 assert_eq!(result.unwrap(), 5);
678 assert_eq!(output, input);
679 }
680
681 #[test]
682 fn test_copy_async_to_sync_large() {
683 let input = (0..100u8).cycle().take(40000).collect_vec();
685 let mut output = vec![];
686
687 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
688 assert!(result.is_ok());
689 assert_eq!(result.unwrap(), 40000);
690 assert_eq!(output, input);
691 }
692
693 #[test]
694 fn test_blocking_async_reader() {
695 let input = b"hello";
696 let sync_reader = Cursor::new(&input);
697 let mut async_reader = BlockingAsyncReader::new(sync_reader);
698
699 let mut buf = [0u8; 3];
700 let num_bytes_read = async_reader.read(&mut buf).block_on().unwrap();
701 assert_eq!(num_bytes_read, 3);
702 assert_eq!(&buf, &input[0..3]);
703
704 let num_bytes_read = async_reader.read(&mut buf).block_on().unwrap();
705 assert_eq!(num_bytes_read, 2);
706 assert_eq!(&buf[0..2], &input[3..5]);
707 }
708
709 #[test]
710 fn test_blocking_async_reader_read_to_end() {
711 let input = b"hello";
712 let sync_reader = Cursor::new(&input);
713 let mut async_reader = BlockingAsyncReader::new(sync_reader);
714
715 let mut buf = vec![];
716 let num_bytes_read = async_reader.read_to_end(&mut buf).block_on().unwrap();
717 assert_eq!(num_bytes_read, input.len());
718 assert_eq!(&buf, &input);
719 }
720}