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 {
137 let Some((from_suffix, to_suffix)) = strip_common_path_prefix(from, to) else {
138 return to.to_owned();
140 };
141 let depth = from_suffix.components().count();
142 let mut relative = PathBuf::with_capacity(2 * depth + 1 + to_suffix.as_os_str().len());
143 for _ in 0..depth {
144 relative.push(Component::ParentDir);
145 }
146 if !to_suffix.as_os_str().is_empty() {
147 relative.push(to_suffix);
148 } else if depth == 0 {
149 relative.push(Component::CurDir);
150 }
151 relative
152}
153
154fn strip_common_path_prefix<'a, 'b>(
155 path1: &'a Path,
156 path2: &'b Path,
157) -> Option<(&'a Path, &'b Path)> {
158 let mut components1 = path1.components();
159 let mut components2 = path2.components();
160 let mut suffix_paths = None;
161 while let (Some(c1), Some(c2)) = (components1.next(), components2.next()) {
162 if c1 != c2 {
163 break;
164 }
165 suffix_paths = Some((components1.as_path(), components2.as_path()));
166 }
167 suffix_paths
168}
169
170pub fn normalize_path(path: &Path) -> PathBuf {
172 let mut result = PathBuf::new();
173 for c in path.components() {
174 match c {
175 Component::CurDir => {}
176 Component::ParentDir
177 if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
178 {
179 let popped = result.pop();
181 assert!(popped);
182 }
183 _ => {
184 result.push(c);
185 }
186 }
187 }
188
189 if result.as_os_str().is_empty() {
190 ".".into()
191 } else {
192 result
193 }
194}
195
196pub fn slash_path(path: &Path) -> Cow<'_, Path> {
201 if cfg!(windows) {
202 Cow::Owned(to_slash_separated(path).into())
203 } else {
204 Cow::Borrowed(path)
205 }
206}
207
208fn to_slash_separated(path: &Path) -> OsString {
209 let mut buf = OsString::with_capacity(path.as_os_str().len());
210 let mut components = path.components();
211 match components.next() {
212 Some(c) => buf.push(c),
213 None => return buf,
214 }
215 for c in components {
216 buf.push("/");
217 buf.push(c);
218 }
219 buf
220}
221
222pub fn persist_temp_file<P: AsRef<Path>>(
230 temp_file: NamedTempFile,
231 new_path: P,
232) -> io::Result<File> {
233 temp_file.as_file().sync_data()?;
235 temp_file
236 .persist(new_path)
237 .map_err(|PersistError { error, file: _ }| error)
238}
239
240pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
243 temp_file: NamedTempFile,
244 new_path: P,
245) -> io::Result<File> {
246 temp_file.as_file().sync_data()?;
249 if cfg!(windows) {
250 match temp_file.persist_noclobber(&new_path) {
254 Ok(file) => Ok(file),
255 Err(PersistError { error, file: _ }) => {
256 if let Ok(existing_file) = File::open(new_path) {
257 Ok(existing_file)
259 } else {
260 Err(error)
261 }
262 }
263 }
264 } else {
265 temp_file
269 .persist(new_path)
270 .map_err(|PersistError { error, file: _ }| error)
271 }
272}
273
274#[derive(Debug, Eq, Hash, PartialEq)]
280pub struct FileIdentity(platform::FileIdentity);
281
282impl FileIdentity {
283 pub fn from_symlink_path(path: impl AsRef<Path>) -> io::Result<Self> {
285 platform::file_identity_from_symlink_path(path.as_ref()).map(Self)
286 }
287
288 pub fn from_file(file: File) -> io::Result<Self> {
291 platform::file_identity_from_file(file).map(Self)
292 }
293}
294
295pub async fn copy_async_to_sync<R: AsyncRead, W: Write + ?Sized>(
298 reader: R,
299 writer: &mut W,
300) -> io::Result<usize> {
301 let mut buf = vec![0; 16 << 10];
302 let mut total_written_bytes = 0;
303
304 let mut reader = std::pin::pin!(reader);
305 loop {
306 let written_bytes = reader.read(&mut buf).await?;
307 if written_bytes == 0 {
308 return Ok(total_written_bytes);
309 }
310 writer.write_all(&buf[0..written_bytes])?;
311 total_written_bytes += written_bytes;
312 }
313}
314
315#[cfg(unix)]
316mod platform {
317 use std::convert::Infallible;
318 use std::ffi::OsStr;
319 use std::fs;
320 use std::fs::File;
321 use std::io;
322 use std::os::unix::ffi::OsStrExt as _;
323 use std::os::unix::fs::MetadataExt as _;
324 use std::os::unix::fs::PermissionsExt;
325 use std::os::unix::fs::symlink;
326 use std::path::Path;
327
328 pub type BadOsStrEncoding = Infallible;
329
330 pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
331 Ok(OsStr::from_bytes(data))
332 }
333
334 pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
335 Ok(data.as_bytes())
336 }
337
338 pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> {
341 let temp_file = tempfile::tempfile_in(path)?;
343 let old_mode = temp_file.metadata()?.permissions().mode();
344 let new_mode = old_mode ^ 0o100;
345 let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode));
346 match result {
347 Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false),
349 Err(err) => Err(err),
350 Ok(()) => {
351 let mode = temp_file.metadata()?.permissions().mode();
353 Ok(mode == new_mode)
354 }
355 }
356 }
357
358 pub fn check_symlink_support() -> io::Result<bool> {
360 Ok(true)
361 }
362
363 pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
367 symlink(original, link)
368 }
369
370 pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
374 symlink(original, link)
375 }
376
377 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
378 pub struct FileIdentity {
379 dev: u64,
381 ino: u64,
382 }
383
384 impl FileIdentity {
385 fn from_metadata(metadata: fs::Metadata) -> Self {
386 Self {
387 dev: metadata.dev(),
388 ino: metadata.ino(),
389 }
390 }
391 }
392
393 pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
394 path.symlink_metadata().map(FileIdentity::from_metadata)
395 }
396
397 pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
398 file.metadata().map(FileIdentity::from_metadata)
399 }
400}
401
402#[cfg(windows)]
403mod platform {
404 use std::fs::File;
405 use std::fs::OpenOptions;
406 use std::io;
407 use std::os::windows::fs::OpenOptionsExt as _;
408 pub use std::os::windows::fs::symlink_dir;
409 pub use std::os::windows::fs::symlink_file;
410 use std::path::Path;
411
412 use winreg::RegKey;
413 use winreg::enums::HKEY_LOCAL_MACHINE;
414
415 pub use super::fallback::BadOsStrEncoding;
416 pub use super::fallback::os_str_from_bytes;
417 pub use super::fallback::os_str_to_bytes;
418
419 pub fn check_symlink_support() -> io::Result<bool> {
425 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
426 let sideloading =
427 hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?;
428 let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?;
429 Ok(developer_mode == 1)
430 }
431
432 pub type FileIdentity = same_file::Handle;
433
434 pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
435 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
444 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
445 let file = OpenOptions::new()
446 .read(true)
447 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
448 .open(path)?;
449 same_file::Handle::from_file(file)
450 }
451
452 pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
453 same_file::Handle::from_file(file)
454 }
455}
456
457#[cfg_attr(unix, expect(dead_code))]
458mod fallback {
459 use std::ffi::OsStr;
460
461 use thiserror::Error;
462
463 #[derive(Debug, Error)]
465 #[error("Invalid UTF-8 sequence")]
466 pub struct BadOsStrEncoding;
467
468 pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
469 Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref())
470 }
471
472 pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
473 Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref())
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use std::io::Write as _;
480
481 use futures::io::Cursor;
482 use itertools::Itertools as _;
483 use pollster::FutureExt as _;
484 use test_case::test_case;
485
486 use super::*;
487 use crate::tests::TestResult;
488 use crate::tests::new_temp_dir;
489
490 #[test]
491 #[cfg(unix)]
492 fn exec_bit_support_in_temp_dir() -> TestResult {
493 let dir = new_temp_dir();
497 let supported = check_executable_bit_support(dir.path())?;
498 assert!(supported);
499 Ok(())
500 }
501
502 #[test]
503 fn test_path_bytes_roundtrip() -> TestResult {
504 let bytes = b"ascii";
505 let path = path_from_bytes(bytes)?;
506 assert_eq!(path_to_bytes(path)?, bytes);
507
508 let bytes = b"utf-8.\xc3\xa0";
509 let path = path_from_bytes(bytes)?;
510 assert_eq!(path_to_bytes(path)?, bytes);
511
512 let bytes = b"latin1.\xe0";
513 if cfg!(unix) {
514 let path = path_from_bytes(bytes)?;
515 assert_eq!(path_to_bytes(path)?, bytes);
516 } else {
517 assert!(path_from_bytes(bytes).is_err());
518 }
519 Ok(())
520 }
521
522 #[test]
523 fn test_relative_path() {
524 let p = |slash_path: &str| {
526 let mut native_path = slash_path.replace('/', std::path::MAIN_SEPARATOR_STR);
527 if cfg!(windows) && slash_path.starts_with('/') {
528 native_path.insert_str(0, "c:"); }
530 native_path
531 };
532 let relative = |from: &str, to: &str| {
533 relative_path(p(from).as_ref(), p(to).as_ref())
534 .into_os_string()
535 .into_string()
536 .unwrap()
537 };
538
539 assert_eq!(relative("/foo/bar", "/foo/bar"), p("."));
540 assert_eq!(relative("/foo", "/foo/bar"), p("bar"));
541 assert_eq!(relative("/", "/foo/bar"), p("foo/bar"));
542
543 assert_eq!(relative("/foo/bar/baz", "/foo/bar"), p(".."));
544 assert_eq!(relative("/foo/baz", "/foo/bar"), p("../bar"));
545 assert_eq!(relative("/baz", "/foo/bar"), p("../foo/bar"));
546
547 assert_eq!(relative("/foo/bar/baz/qux", "/foo/bar"), p("../.."));
548 assert_eq!(relative("/foo/baz/qux", "/foo/bar"), p("../../bar"));
549 assert_eq!(relative("/baz/qux", "/foo/bar"), p("../../foo/bar"));
550
551 assert_eq!(relative("foo/bar", "/foo/bar"), p("/foo/bar"));
553 assert_eq!(relative("/foo/bar", "foo/bar"), p("foo/bar"));
554 assert_eq!(relative("./foo/bar", "/./foo/bar"), p("/./foo/bar"));
555 assert_eq!(relative("/./foo/bar", "./foo/bar"), p("./foo/bar"));
556 assert_eq!(relative("/foo", ""), p("")); assert_eq!(relative("", "/foo"), p("/foo"));
558 assert_eq!(relative("", ""), p("")); assert_eq!(relative("/./foo/./bar", "/foo/bar"), p("."));
562 assert_eq!(relative("/foo/bar", "/./foo/./bar"), p("."));
563 assert_eq!(relative("/./foo/./bar", "/foo"), p(".."));
564 assert_eq!(relative("/foo", "/./foo/./bar"), p("bar"));
565 }
566
567 #[test]
568 fn test_relative_path_windows() {
569 let relative = |from: &str, to: &str| {
571 relative_path(from.as_ref(), to.as_ref())
572 .into_os_string()
573 .into_string()
574 .unwrap()
575 };
576
577 if cfg!(windows) {
578 assert_eq!(relative(r"c:\foo\bar", r"c:\foo\bar"), ".");
579 assert_eq!(relative(r"c:\foo", r"c:\foo\bar"), "bar");
580 assert_eq!(relative(r"c:\", r"c:\foo\bar"), r"foo\bar");
581
582 assert_eq!(relative(r"d:\foo", r"c:\foo\bar"), r"c:\foo\bar");
583 assert_eq!(relative(r"d:\", r"c:\foo\bar"), r"c:\foo\bar");
584
585 assert_eq!(relative(r"\\foo\bar", r"\foo\bar"), r"\foo\bar");
586 assert_eq!(relative(r"\\foo\bar", r"\\foo\bar"), ".");
587 assert_eq!(relative(r"\\foo\bar\baz", r"\\foo\bar\baz"), ".");
588 assert_eq!(relative(r"\\foo\bar\baz", r"\\foo\bar\qux"), r"..\qux");
589 assert_eq!(
590 relative(r"\\foo\bar\baz", r"\\qux\bar\baz"),
591 r"\\qux\bar\baz"
592 );
593 }
594 }
595
596 #[test]
597 fn normalize_too_many_dot_dot() {
598 assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
599 assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
600 assert_eq!(
601 normalize_path(Path::new("foo/../../..")),
602 Path::new("../..")
603 );
604 assert_eq!(
605 normalize_path(Path::new("foo/../../../bar/baz/..")),
606 Path::new("../../bar")
607 );
608 }
609
610 #[test]
611 fn test_slash_path() {
612 assert_eq!(slash_path(Path::new("")), Path::new(""));
613 assert_eq!(slash_path(Path::new("foo")), Path::new("foo"));
614 assert_eq!(slash_path(Path::new("foo/bar")), Path::new("foo/bar"));
615 assert_eq!(slash_path(Path::new("foo/bar/..")), Path::new("foo/bar/.."));
616 assert_eq!(
617 slash_path(Path::new(r"foo\bar")),
618 if cfg!(windows) {
619 Path::new("foo/bar")
620 } else {
621 Path::new(r"foo\bar")
622 }
623 );
624 assert_eq!(
625 slash_path(Path::new(r"..\foo\bar")),
626 if cfg!(windows) {
627 Path::new("../foo/bar")
628 } else {
629 Path::new(r"..\foo\bar")
630 }
631 );
632 }
633
634 #[test]
635 fn test_persist_no_existing_file() -> TestResult {
636 let temp_dir = new_temp_dir();
637 let target = temp_dir.path().join("file");
638 let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
639 temp_file.write_all(b"contents")?;
640 assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
641 Ok(())
642 }
643
644 #[test_case(false ; "existing file open")]
645 #[test_case(true ; "existing file closed")]
646 fn test_persist_target_exists(existing_file_closed: bool) -> TestResult {
647 let temp_dir = new_temp_dir();
648 let target = temp_dir.path().join("file");
649 let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
650 temp_file.write_all(b"contents")?;
651
652 let mut file = File::create(&target)?;
653 file.write_all(b"contents")?;
654 if existing_file_closed {
655 drop(file);
656 }
657
658 assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
659 Ok(())
660 }
661
662 #[test]
663 fn test_file_identity_hard_link() -> TestResult {
664 let temp_dir = new_temp_dir();
665 let file_path = temp_dir.path().join("file");
666 let other_file_path = temp_dir.path().join("other_file");
667 let link_path = temp_dir.path().join("link");
668 fs::write(&file_path, "")?;
669 fs::write(&other_file_path, "")?;
670 fs::hard_link(&file_path, &link_path)?;
671 assert_eq!(
672 FileIdentity::from_symlink_path(&file_path)?,
673 FileIdentity::from_symlink_path(&link_path)?
674 );
675 assert_ne!(
676 FileIdentity::from_symlink_path(&other_file_path)?,
677 FileIdentity::from_symlink_path(&link_path)?
678 );
679 assert_eq!(
680 FileIdentity::from_symlink_path(&file_path)?,
681 FileIdentity::from_file(File::open(&link_path)?)?
682 );
683 Ok(())
684 }
685
686 #[cfg(unix)]
687 #[test]
688 fn test_file_identity_unix_symlink_dir() -> TestResult {
689 let temp_dir = new_temp_dir();
690 let dir_path = temp_dir.path().join("dir");
691 let symlink_path = temp_dir.path().join("symlink");
692 fs::create_dir(&dir_path)?;
693 std::os::unix::fs::symlink("dir", &symlink_path)?;
694 assert_eq!(
696 FileIdentity::from_symlink_path(&symlink_path)?,
697 FileIdentity::from_symlink_path(&symlink_path)?
698 );
699 assert_ne!(
701 FileIdentity::from_symlink_path(&dir_path)?,
702 FileIdentity::from_symlink_path(&symlink_path)?
703 );
704 assert_eq!(
706 FileIdentity::from_symlink_path(&dir_path)?,
707 FileIdentity::from_file(File::open(&symlink_path)?)?
708 );
709 assert_ne!(
710 FileIdentity::from_symlink_path(&symlink_path)?,
711 FileIdentity::from_file(File::open(&symlink_path)?)?
712 );
713 Ok(())
714 }
715
716 #[cfg(windows)]
717 #[test]
718 fn test_file_identity_windows_symlink_file() -> TestResult {
719 if !check_symlink_support()? {
720 return Ok(());
721 }
722 let temp_dir = new_temp_dir();
723 let file_path = temp_dir.path().join("file");
724 let symlink_path = temp_dir.path().join("symlink");
725 fs::write(&file_path, "")?;
726 symlink_file("file", &symlink_path)?;
727 assert_eq!(
729 FileIdentity::from_symlink_path(&symlink_path)?,
730 FileIdentity::from_symlink_path(&symlink_path)?
731 );
732 assert_ne!(
734 FileIdentity::from_symlink_path(&file_path)?,
735 FileIdentity::from_symlink_path(&symlink_path)?
736 );
737 assert_eq!(
739 FileIdentity::from_symlink_path(&file_path)?,
740 FileIdentity::from_file(File::open(&symlink_path)?)?
741 );
742 assert_ne!(
743 FileIdentity::from_symlink_path(&symlink_path)?,
744 FileIdentity::from_file(File::open(&symlink_path)?)?
745 );
746 Ok(())
747 }
748
749 #[cfg(windows)]
750 #[test]
751 fn test_file_identity_windows_symlink_dir() -> TestResult {
752 if !check_symlink_support()? {
753 return Ok(());
754 }
755 let temp_dir = new_temp_dir();
756 let dir_path = temp_dir.path().join("dir");
757 let symlink_path = temp_dir.path().join("symlink");
758 fs::create_dir(&dir_path)?;
759 symlink_dir("dir", &symlink_path)?;
760 assert_eq!(
762 FileIdentity::from_symlink_path(&symlink_path)?,
763 FileIdentity::from_symlink_path(&symlink_path)?
764 );
765 assert_ne!(
769 FileIdentity::from_symlink_path(&dir_path)?,
770 FileIdentity::from_symlink_path(&symlink_path)?
771 );
772 Ok(())
773 }
774
775 #[test]
776 fn test_file_identity_directory() -> TestResult {
777 let temp_dir = new_temp_dir();
778 let dir_path = temp_dir.path().join("dir");
779 let other_dir_path = temp_dir.path().join("other_dir");
780 fs::create_dir(&dir_path)?;
781 fs::create_dir(&other_dir_path)?;
782 assert_eq!(
784 FileIdentity::from_symlink_path(&dir_path)?,
785 FileIdentity::from_symlink_path(&dir_path)?
786 );
787 assert_ne!(
789 FileIdentity::from_symlink_path(&dir_path)?,
790 FileIdentity::from_symlink_path(&other_dir_path)?
791 );
792 Ok(())
793 }
794
795 #[cfg(unix)]
796 #[test]
797 fn test_file_identity_unix_symlink_loop() -> TestResult {
798 let temp_dir = new_temp_dir();
799 let lower_file_path = temp_dir.path().join("file");
800 let upper_file_path = temp_dir.path().join("FILE");
801 let lower_symlink_path = temp_dir.path().join("symlink");
802 let upper_symlink_path = temp_dir.path().join("SYMLINK");
803 fs::write(&lower_file_path, "")?;
804 std::os::unix::fs::symlink("symlink", &lower_symlink_path)?;
805 let is_icase_fs = upper_file_path.try_exists()?;
806 assert_eq!(
808 FileIdentity::from_symlink_path(&lower_symlink_path)?,
809 FileIdentity::from_symlink_path(&lower_symlink_path)?
810 );
811 assert_ne!(
812 FileIdentity::from_symlink_path(&lower_symlink_path)?,
813 FileIdentity::from_symlink_path(&lower_file_path)?
814 );
815 if is_icase_fs {
816 assert_eq!(
817 FileIdentity::from_symlink_path(&lower_symlink_path)?,
818 FileIdentity::from_symlink_path(&upper_symlink_path)?
819 );
820 } else {
821 assert!(FileIdentity::from_symlink_path(&upper_symlink_path).is_err());
822 }
823 Ok(())
824 }
825
826 #[test]
827 fn test_copy_async_to_sync_small() -> TestResult {
828 let input = b"hello";
829 let mut output = vec![];
830
831 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
832 assert!(result.is_ok());
833 assert_eq!(result?, 5);
834 assert_eq!(output, input);
835 Ok(())
836 }
837
838 #[test]
839 fn test_copy_async_to_sync_large() -> TestResult {
840 let input = (0..100u8).cycle().take(40000).collect_vec();
842 let mut output = vec![];
843
844 let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
845 assert!(result.is_ok());
846 assert_eq!(result?, 40000);
847 assert_eq!(output, input);
848 Ok(())
849 }
850}