Skip to main content

jj_lib/
file_util.rs

1// Copyright 2021 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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
60/// Creates a directory or does nothing if the directory already exists.
61///
62/// Returns the underlying error if the directory can't be created.
63/// The function will also fail if intermediate directories on the path do not
64/// already exist.
65pub 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
73/// Removes all files in the directory, but not the directory itself.
74///
75/// The directory must exist, and there should be no sub directories.
76pub 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
85/// Checks if path points at an empty directory.
86pub 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
101/// Constructs [`Path`] from `bytes` in platform-specific manner.
102///
103/// On Unix, this function never fails because paths are just bytes. On Windows,
104/// this may return error if the input wasn't well-formed UTF-8.
105pub 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
110/// Converts `path` to bytes in platform-specific manner.
111///
112/// On Unix, this function never fails because paths are just bytes. On Windows,
113/// this may return error if the input wasn't well-formed UTF-8.
114///
115/// The returned byte sequence can be considered a superset of ASCII (such as
116/// UTF-8 bytes.)
117pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> {
118    platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding)
119}
120
121/// Expands "~/" to the user's home directory.
122pub 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
131/// Turns the given `to` path into relative path starting from the `from` path.
132///
133/// Both `from` and `to` paths are supposed to be absolute and normalized in the
134/// same manner.
135pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
136    // Find common prefix.
137    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    // No common prefix found. Return the original (absolute) path.
150    to.to_owned()
151}
152
153/// Consumes as much `..` and `.` as possible without considering symlinks.
154pub 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                // Do not pop ".."
163                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
179/// Converts the given `path` to Unix-like path separated by "/".
180///
181/// The returned path might not work on Windows if it was canonicalized. On
182/// Unix, this function is noop.
183pub 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
205/// Persists the temporary file after synchronizing the content.
206///
207/// After system crash, the persisted file should have a valid content if
208/// existed. However, the persisted file name (or directory entry) could be
209/// lost. It's up to caller to synchronize the directory entries.
210///
211/// See also <https://lwn.net/Articles/457667/> for the behavior on Linux.
212pub fn persist_temp_file<P: AsRef<Path>>(
213    temp_file: NamedTempFile,
214    new_path: P,
215) -> io::Result<File> {
216    // Ensure persisted file content is flushed to disk.
217    temp_file.as_file().sync_data()?;
218    temp_file
219        .persist(new_path)
220        .map_err(|PersistError { error, file: _ }| error)
221}
222
223/// Like [`persist_temp_file()`], but doesn't try to overwrite the existing
224/// target on Windows.
225pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
226    temp_file: NamedTempFile,
227    new_path: P,
228) -> io::Result<File> {
229    // Ensure new file content is flushed to disk, so the old file content
230    // wouldn't be lost if existed at the same location.
231    temp_file.as_file().sync_data()?;
232    if cfg!(windows) {
233        // On Windows, overwriting file can fail if the file is opened without
234        // FILE_SHARE_DELETE for example. We don't need to take a risk if the
235        // file already exists.
236        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                    // TODO: Update mtime to help GC keep this file
241                    Ok(existing_file)
242                } else {
243                    Err(error)
244                }
245            }
246        }
247    } else {
248        // On Unix, rename() is atomic and should succeed even if the
249        // destination file exists. Checking if the target exists might involve
250        // non-atomic operation, so don't use persist_noclobber().
251        temp_file
252            .persist(new_path)
253            .map_err(|PersistError { error, file: _ }| error)
254    }
255}
256
257/// Opaque value that can be tested to know whether file or directory paths
258/// point to the same filesystem entity.
259///
260/// The primary use case is to detect file name aliases on case-insensitive
261/// filesystem. On Unix, device and inode numbers are compared.
262#[derive(Debug, Eq, Hash, PartialEq)]
263pub struct FileIdentity(platform::FileIdentity);
264
265impl FileIdentity {
266    /// Queries file identity without following symlinks.
267    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    /// Queries file identity of the given `file`.
272    // TODO: do not consume file object
273    pub fn from_file(file: File) -> io::Result<Self> {
274        platform::file_identity_from_file(file).map(Self)
275    }
276}
277
278/// Reads from an async source and writes to a sync destination. Does not spawn
279/// a task, so writes will block.
280pub 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    /// Whether changing executable bits is permitted on the filesystem of this
322    /// directory, and whether attempting to flip one has an observable effect.
323    pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> {
324        // Get current permissions and try to flip just the user's executable bit.
325        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            // If permission was denied, we do not have executable bit support.
331            Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false),
332            Err(err) => Err(err),
333            Ok(()) => {
334                // Verify that the permission change was not silently ignored.
335                let mode = temp_file.metadata()?.permissions().mode();
336                Ok(mode == new_mode)
337            }
338        }
339    }
340
341    /// Symlinks are always available on Unix.
342    pub fn check_symlink_support() -> io::Result<bool> {
343        Ok(true)
344    }
345
346    /// Creates a new symlink `link` pointing to the `original` path.
347    ///
348    /// On Unix, the `original` path doesn't have to be a directory.
349    pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
350        symlink(original, link)
351    }
352
353    /// Creates a new symlink `link` pointing to the `original` path.
354    ///
355    /// On Unix, the `original` path doesn't have to be a file.
356    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        // https://github.com/BurntSushi/same-file/blob/1.0.6/src/unix.rs#L30
363        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    /// Symlinks may or may not be enabled on Windows. They require the
403    /// Developer Mode setting, which is stored in the registry key below.
404    ///
405    /// Note: If developer mode is not enabled, the error code of symlink
406    /// creation will be 1314, `ERROR_PRIVILEGE_NOT_HELD`.
407    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        // `same_file::Handle::from_path()` follows symlinks, because it opens
419        // the path without `FILE_FLAG_OPEN_REPARSE_POINT`. Open the file the
420        // same way it does (read access, plus `FILE_FLAG_BACKUP_SEMANTICS` so a
421        // directory can be opened too), but add `FILE_FLAG_OPEN_REPARSE_POINT`
422        // so the handle refers to the symlink itself instead of its target.
423        // This matches the Unix implementation, which uses `symlink_metadata()`.
424        // The reparse-point flag is ignored for paths that aren't reparse
425        // points, so regular files and hard links are unaffected.
426        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    // Define error per platform so we can explicitly say UTF-8 is expected.
447    #[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        // Temporary directories on Unix should always have executable support.
477        // Note that it would be problematic to test in a non-temp directory, as
478        // a developer's filesystem may or may not have executable bit support.
479        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        // symlink should be identical to itself
604        assert_eq!(
605            FileIdentity::from_symlink_path(&symlink_path)?,
606            FileIdentity::from_symlink_path(&symlink_path)?
607        );
608        // symlink should be different from the target directory
609        assert_ne!(
610            FileIdentity::from_symlink_path(&dir_path)?,
611            FileIdentity::from_symlink_path(&symlink_path)?
612        );
613        // File::open() follows symlinks
614        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        // symlink should be identical to itself
637        assert_eq!(
638            FileIdentity::from_symlink_path(&symlink_path)?,
639            FileIdentity::from_symlink_path(&symlink_path)?
640        );
641        // symlink should be different from the target file
642        assert_ne!(
643            FileIdentity::from_symlink_path(&file_path)?,
644            FileIdentity::from_symlink_path(&symlink_path)?
645        );
646        // File::open() follows symlinks
647        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        // symlink should be identical to itself
670        assert_eq!(
671            FileIdentity::from_symlink_path(&symlink_path)?,
672            FileIdentity::from_symlink_path(&symlink_path)?
673        );
674        // symlink should be different from the target directory. The
675        // `File::open()` follow-through is not checked here because File::open()
676        // can't open a directory on Windows.
677        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        // a directory should be identical to itself
692        assert_eq!(
693            FileIdentity::from_symlink_path(&dir_path)?,
694            FileIdentity::from_symlink_path(&dir_path)?
695        );
696        // distinct directories should differ
697        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        // symlink should be identical to itself
716        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        // More than 1 buffer worth of data
750        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}