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. If `from` and `to` share no common prefix, the returned path is
135/// unchanged. This also means `relative_path(abs, rel)` will return `rel`.
136pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
137    let Some((from_suffix, to_suffix)) = strip_common_path_prefix(from, to) else {
138        // No common prefix found. Return the original path.
139        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
170/// Consumes as much `..` and `.` as possible without considering symlinks.
171pub 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                // Do not pop ".."
180                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
196/// Converts the given `path` to Unix-like path separated by "/".
197///
198/// The returned path might not work on Windows if it was canonicalized. On
199/// Unix, this function is noop.
200pub 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
222/// Persists the temporary file after synchronizing the content.
223///
224/// After system crash, the persisted file should have a valid content if
225/// existed. However, the persisted file name (or directory entry) could be
226/// lost. It's up to caller to synchronize the directory entries.
227///
228/// See also <https://lwn.net/Articles/457667/> for the behavior on Linux.
229pub fn persist_temp_file<P: AsRef<Path>>(
230    temp_file: NamedTempFile,
231    new_path: P,
232) -> io::Result<File> {
233    // Ensure persisted file content is flushed to disk.
234    temp_file.as_file().sync_data()?;
235    temp_file
236        .persist(new_path)
237        .map_err(|PersistError { error, file: _ }| error)
238}
239
240/// Like [`persist_temp_file()`], but doesn't try to overwrite the existing
241/// target on Windows.
242pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
243    temp_file: NamedTempFile,
244    new_path: P,
245) -> io::Result<File> {
246    // Ensure new file content is flushed to disk, so the old file content
247    // wouldn't be lost if existed at the same location.
248    temp_file.as_file().sync_data()?;
249    if cfg!(windows) {
250        // On Windows, overwriting file can fail if the file is opened without
251        // FILE_SHARE_DELETE for example. We don't need to take a risk if the
252        // file already exists.
253        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                    // TODO: Update mtime to help GC keep this file
258                    Ok(existing_file)
259                } else {
260                    Err(error)
261                }
262            }
263        }
264    } else {
265        // On Unix, rename() is atomic and should succeed even if the
266        // destination file exists. Checking if the target exists might involve
267        // non-atomic operation, so don't use persist_noclobber().
268        temp_file
269            .persist(new_path)
270            .map_err(|PersistError { error, file: _ }| error)
271    }
272}
273
274/// Opaque value that can be tested to know whether file or directory paths
275/// point to the same filesystem entity.
276///
277/// The primary use case is to detect file name aliases on case-insensitive
278/// filesystem. On Unix, device and inode numbers are compared.
279#[derive(Debug, Eq, Hash, PartialEq)]
280pub struct FileIdentity(platform::FileIdentity);
281
282impl FileIdentity {
283    /// Queries file identity without following symlinks.
284    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    /// Queries file identity of the given `file`.
289    // TODO: do not consume file object
290    pub fn from_file(file: File) -> io::Result<Self> {
291        platform::file_identity_from_file(file).map(Self)
292    }
293}
294
295/// Reads from an async source and writes to a sync destination. Does not spawn
296/// a task, so writes will block.
297pub 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    /// Whether changing executable bits is permitted on the filesystem of this
339    /// directory, and whether attempting to flip one has an observable effect.
340    pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> {
341        // Get current permissions and try to flip just the user's executable bit.
342        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            // If permission was denied, we do not have executable bit support.
348            Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false),
349            Err(err) => Err(err),
350            Ok(()) => {
351                // Verify that the permission change was not silently ignored.
352                let mode = temp_file.metadata()?.permissions().mode();
353                Ok(mode == new_mode)
354            }
355        }
356    }
357
358    /// Symlinks are always available on Unix.
359    pub fn check_symlink_support() -> io::Result<bool> {
360        Ok(true)
361    }
362
363    /// Creates a new symlink `link` pointing to the `original` path.
364    ///
365    /// On Unix, the `original` path doesn't have to be a directory.
366    pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
367        symlink(original, link)
368    }
369
370    /// Creates a new symlink `link` pointing to the `original` path.
371    ///
372    /// On Unix, the `original` path doesn't have to be a file.
373    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        // https://github.com/BurntSushi/same-file/blob/1.0.6/src/unix.rs#L30
380        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    /// Symlinks may or may not be enabled on Windows. They require the
420    /// Developer Mode setting, which is stored in the registry key below.
421    ///
422    /// Note: If developer mode is not enabled, the error code of symlink
423    /// creation will be 1314, `ERROR_PRIVILEGE_NOT_HELD`.
424    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        // `same_file::Handle::from_path()` follows symlinks, because it opens
436        // the path without `FILE_FLAG_OPEN_REPARSE_POINT`. Open the file the
437        // same way it does (read access, plus `FILE_FLAG_BACKUP_SEMANTICS` so a
438        // directory can be opened too), but add `FILE_FLAG_OPEN_REPARSE_POINT`
439        // so the handle refers to the symlink itself instead of its target.
440        // This matches the Unix implementation, which uses `symlink_metadata()`.
441        // The reparse-point flag is ignored for paths that aren't reparse
442        // points, so regular files and hard links are unaffected.
443        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    // Define error per platform so we can explicitly say UTF-8 is expected.
464    #[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        // Temporary directories on Unix should always have executable support.
494        // Note that it would be problematic to test in a non-temp directory, as
495        // a developer's filesystem may or may not have executable bit support.
496        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        // Compare as strings, not as (normalized) paths
525        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:"); // make the path truly absolute
529            }
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        // No common prefix components
552        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("")); // or "."
557        assert_eq!(relative("", "/foo"), p("/foo"));
558        assert_eq!(relative("", ""), p("")); // or "."
559
560        // Redundant components are skipped by Path::components()
561        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        // Compare as strings, not as (normalized) paths
570        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        // symlink should be identical to itself
695        assert_eq!(
696            FileIdentity::from_symlink_path(&symlink_path)?,
697            FileIdentity::from_symlink_path(&symlink_path)?
698        );
699        // symlink should be different from the target directory
700        assert_ne!(
701            FileIdentity::from_symlink_path(&dir_path)?,
702            FileIdentity::from_symlink_path(&symlink_path)?
703        );
704        // File::open() follows symlinks
705        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        // symlink should be identical to itself
728        assert_eq!(
729            FileIdentity::from_symlink_path(&symlink_path)?,
730            FileIdentity::from_symlink_path(&symlink_path)?
731        );
732        // symlink should be different from the target file
733        assert_ne!(
734            FileIdentity::from_symlink_path(&file_path)?,
735            FileIdentity::from_symlink_path(&symlink_path)?
736        );
737        // File::open() follows symlinks
738        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        // symlink should be identical to itself
761        assert_eq!(
762            FileIdentity::from_symlink_path(&symlink_path)?,
763            FileIdentity::from_symlink_path(&symlink_path)?
764        );
765        // symlink should be different from the target directory. The
766        // `File::open()` follow-through is not checked here because File::open()
767        // can't open a directory on Windows.
768        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        // a directory should be identical to itself
783        assert_eq!(
784            FileIdentity::from_symlink_path(&dir_path)?,
785            FileIdentity::from_symlink_path(&dir_path)?
786        );
787        // distinct directories should differ
788        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        // symlink should be identical to itself
807        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        // More than 1 buffer worth of data
841        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}