Skip to main content

jj_core/
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//! Contains file utilities which work in a cross-platform way.
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/// An error which can occur when accessing paths.
41#[derive(Debug, Error)]
42#[error("Cannot access {path}")]
43pub struct PathError {
44    /// The path which is inaccessible.
45    pub path: PathBuf,
46    /// The underlying error source.
47    pub source: io::Error,
48}
49
50/// An extension trait to `io::Result` to allow adding a path as context.
51pub trait IoResultExt<T> {
52    /// Provide more context to the `io::Result`.
53    fn context(self, path: impl AsRef<Path>) -> Result<T, PathError>;
54}
55
56impl<T> IoResultExt<T> for io::Result<T> {
57    fn context(self, path: impl AsRef<Path>) -> Result<T, PathError> {
58        self.map_err(|error| PathError {
59            path: path.as_ref().to_path_buf(),
60            source: error,
61        })
62    }
63}
64
65/// Creates a directory or does nothing if the directory already exists.
66///
67/// Returns the underlying error if the directory can't be created.
68/// The function will also fail if intermediate directories on the path do not
69/// already exist.
70pub fn create_or_reuse_dir(dirname: &Path) -> io::Result<()> {
71    match fs::create_dir(dirname) {
72        Ok(()) => Ok(()),
73        Err(_) if dirname.is_dir() => Ok(()),
74        Err(e) => Err(e),
75    }
76}
77
78/// Removes all files in the directory, but not the directory itself.
79///
80/// The directory must exist, and there should be no sub directories.
81pub fn remove_dir_contents(dirname: &Path) -> Result<(), PathError> {
82    for entry in dirname.read_dir().context(dirname)? {
83        let entry = entry.context(dirname)?;
84        let path = entry.path();
85        fs::remove_file(&path).context(&path)?;
86    }
87    Ok(())
88}
89
90/// Checks if path points at an empty directory.
91pub fn is_empty_dir(path: &Path) -> Result<bool, PathError> {
92    match path.read_dir() {
93        Ok(mut entries) => Ok(entries.next().is_none()),
94        Err(error) => match error.kind() {
95            ErrorKind::NotADirectory => Ok(false),
96            ErrorKind::NotFound => Ok(false),
97            _ => Err(error).context(path)?,
98        },
99    }
100}
101
102/// An error which occurs when we encounter a path which isn't UTF-8 encoded.
103#[derive(Debug, Error)]
104#[error(transparent)]
105pub struct BadPathEncoding(platform::BadOsStrEncoding);
106
107/// Constructs [`Path`] from `bytes` in platform-specific manner.
108///
109/// On Unix, this function never fails because paths are just bytes. On Windows,
110/// this may return error if the input wasn't well-formed UTF-8.
111pub fn path_from_bytes(bytes: &[u8]) -> Result<&Path, BadPathEncoding> {
112    let s = platform::os_str_from_bytes(bytes).map_err(BadPathEncoding)?;
113    Ok(Path::new(s))
114}
115
116/// Converts `path` to bytes in platform-specific manner.
117///
118/// On Unix, this function never fails because paths are just bytes. On Windows,
119/// this may return error if the input wasn't well-formed UTF-8.
120///
121/// The returned byte sequence can be considered a superset of ASCII (such as
122/// UTF-8 bytes.)
123pub fn path_to_bytes(path: &Path) -> Result<&[u8], BadPathEncoding> {
124    platform::os_str_to_bytes(path.as_ref()).map_err(BadPathEncoding)
125}
126
127/// Expands "~/" to the user's home directory.
128pub fn expand_home_path(path_str: &str) -> PathBuf {
129    if let Some(remainder) = path_str.strip_prefix("~/")
130        && let Ok(home_dir) = etcetera::home_dir()
131    {
132        return home_dir.join(remainder);
133    }
134    PathBuf::from(path_str)
135}
136
137/// Turns the given `to` path into relative path starting from the `from` path.
138///
139/// Both `from` and `to` paths are supposed to be absolute and normalized in the
140/// same manner. If `from` and `to` share no common prefix, the returned path is
141/// unchanged. This also means `relative_path(abs, rel)` will return `rel`.
142pub fn relative_path(from: &Path, to: &Path) -> PathBuf {
143    let Some((from_suffix, to_suffix)) = strip_common_path_prefix(from, to) else {
144        // No common prefix found. Return the original path.
145        return to.to_owned();
146    };
147    let depth = from_suffix.components().count();
148    let mut relative = PathBuf::with_capacity(2 * depth + 1 + to_suffix.as_os_str().len());
149    for _ in 0..depth {
150        relative.push(Component::ParentDir);
151    }
152    if !to_suffix.as_os_str().is_empty() {
153        relative.push(to_suffix);
154    } else if depth == 0 {
155        relative.push(Component::CurDir);
156    }
157    relative
158}
159
160fn strip_common_path_prefix<'a, 'b>(
161    path1: &'a Path,
162    path2: &'b Path,
163) -> Option<(&'a Path, &'b Path)> {
164    let mut components1 = path1.components();
165    let mut components2 = path2.components();
166    let mut suffix_paths = None;
167    while let (Some(c1), Some(c2)) = (components1.next(), components2.next()) {
168        if c1 != c2 {
169            break;
170        }
171        suffix_paths = Some((components1.as_path(), components2.as_path()));
172    }
173    suffix_paths
174}
175
176/// Consumes as much `..` and `.` as possible without considering symlinks.
177pub fn normalize_path(path: &Path) -> PathBuf {
178    let mut result = PathBuf::new();
179    for c in path.components() {
180        match c {
181            Component::CurDir => {}
182            Component::ParentDir
183                if matches!(result.components().next_back(), Some(Component::Normal(_))) =>
184            {
185                // Do not pop ".."
186                let popped = result.pop();
187                assert!(popped);
188            }
189            _ => {
190                result.push(c);
191            }
192        }
193    }
194
195    if result.as_os_str().is_empty() {
196        ".".into()
197    } else {
198        result
199    }
200}
201
202/// Converts the given `path` to Unix-like path separated by "/".
203///
204/// The returned path might not work on Windows if it was canonicalized. On
205/// Unix, this function is noop.
206pub fn slash_path(path: &Path) -> Cow<'_, Path> {
207    if cfg!(windows) {
208        Cow::Owned(to_slash_separated(path).into())
209    } else {
210        Cow::Borrowed(path)
211    }
212}
213
214fn to_slash_separated(path: &Path) -> OsString {
215    let mut buf = OsString::with_capacity(path.as_os_str().len());
216    let mut components = path.components();
217    match components.next() {
218        Some(c) => buf.push(c),
219        None => return buf,
220    }
221    for c in components {
222        buf.push("/");
223        buf.push(c);
224    }
225    buf
226}
227
228/// Persists the temporary file after synchronizing the content.
229///
230/// After system crash, the persisted file should have a valid content if
231/// existed. However, the persisted file name (or directory entry) could be
232/// lost. It's up to caller to synchronize the directory entries.
233///
234/// See also <https://lwn.net/Articles/457667/> for the behavior on Linux.
235pub fn persist_temp_file<P: AsRef<Path>>(
236    temp_file: NamedTempFile,
237    new_path: P,
238) -> io::Result<File> {
239    // Ensure persisted file content is flushed to disk.
240    temp_file.as_file().sync_data()?;
241    temp_file
242        .persist(new_path)
243        .map_err(|PersistError { error, file: _ }| error)
244}
245
246/// Like [`persist_temp_file()`], but doesn't try to overwrite the existing
247/// target on Windows.
248pub fn persist_content_addressed_temp_file<P: AsRef<Path>>(
249    temp_file: NamedTempFile,
250    new_path: P,
251) -> io::Result<File> {
252    // Ensure new file content is flushed to disk, so the old file content
253    // wouldn't be lost if existed at the same location.
254    temp_file.as_file().sync_data()?;
255    if cfg!(windows) {
256        // On Windows, overwriting file can fail if the file is opened without
257        // FILE_SHARE_DELETE for example. We don't need to take a risk if the
258        // file already exists.
259        match temp_file.persist_noclobber(&new_path) {
260            Ok(file) => Ok(file),
261            Err(PersistError { error, file: _ }) => {
262                if let Ok(existing_file) = File::open(new_path) {
263                    // TODO: Update mtime to help GC keep this file
264                    Ok(existing_file)
265                } else {
266                    Err(error)
267                }
268            }
269        }
270    } else {
271        // On Unix, rename() is atomic and should succeed even if the
272        // destination file exists. Checking if the target exists might involve
273        // non-atomic operation, so don't use persist_noclobber().
274        temp_file
275            .persist(new_path)
276            .map_err(|PersistError { error, file: _ }| error)
277    }
278}
279
280/// Opaque value that can be tested to know whether file or directory paths
281/// point to the same filesystem entity.
282///
283/// The primary use case is to detect file name aliases on case-insensitive
284/// filesystem. On Unix, device and inode numbers are compared.
285#[derive(Debug, Eq, Hash, PartialEq)]
286pub struct FileIdentity(platform::FileIdentity);
287
288impl FileIdentity {
289    /// Queries file identity without following symlinks.
290    pub fn from_symlink_path(path: impl AsRef<Path>) -> io::Result<Self> {
291        platform::file_identity_from_symlink_path(path.as_ref()).map(Self)
292    }
293
294    /// Queries file identity of the given `file`.
295    // TODO: do not consume file object
296    pub fn from_file(file: File) -> io::Result<Self> {
297        platform::file_identity_from_file(file).map(Self)
298    }
299}
300
301/// Reads from an async source and writes to a sync destination. Does not spawn
302/// a task, so writes will block.
303pub async fn copy_async_to_sync<R: AsyncRead, W: Write + ?Sized>(
304    reader: R,
305    writer: &mut W,
306) -> io::Result<usize> {
307    let mut buf = vec![0; 16 << 10];
308    let mut total_written_bytes = 0;
309
310    let mut reader = std::pin::pin!(reader);
311    loop {
312        let written_bytes = reader.read(&mut buf).await?;
313        if written_bytes == 0 {
314            return Ok(total_written_bytes);
315        }
316        writer.write_all(&buf[0..written_bytes])?;
317        total_written_bytes += written_bytes;
318    }
319}
320
321#[cfg(unix)]
322mod platform {
323    use std::convert::Infallible;
324    use std::ffi::OsStr;
325    use std::fs;
326    use std::fs::File;
327    use std::io;
328    use std::os::unix::ffi::OsStrExt as _;
329    use std::os::unix::fs::MetadataExt as _;
330    use std::os::unix::fs::PermissionsExt;
331    use std::os::unix::fs::symlink;
332    use std::path::Path;
333
334    pub type BadOsStrEncoding = Infallible;
335
336    pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
337        Ok(OsStr::from_bytes(data))
338    }
339
340    pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
341        Ok(data.as_bytes())
342    }
343
344    /// Whether changing executable bits is permitted on the filesystem of this
345    /// directory, and whether attempting to flip one has an observable effect.
346    pub fn check_executable_bit_support(path: impl AsRef<Path>) -> io::Result<bool> {
347        // Get current permissions and try to flip just the user's executable bit.
348        let temp_file = tempfile::tempfile_in(path)?;
349        let old_mode = temp_file.metadata()?.permissions().mode();
350        let new_mode = old_mode ^ 0o100;
351        let result = temp_file.set_permissions(PermissionsExt::from_mode(new_mode));
352        match result {
353            // If permission was denied, we do not have executable bit support.
354            Err(err) if err.kind() == io::ErrorKind::PermissionDenied => Ok(false),
355            Err(err) => Err(err),
356            Ok(()) => {
357                // Verify that the permission change was not silently ignored.
358                let mode = temp_file.metadata()?.permissions().mode();
359                Ok(mode == new_mode)
360            }
361        }
362    }
363
364    /// Symlinks are always available on Unix.
365    pub fn check_symlink_support() -> io::Result<bool> {
366        Ok(true)
367    }
368
369    /// Creates a new symlink `link` pointing to the `original` path.
370    ///
371    /// On Unix, the `original` path doesn't have to be a directory.
372    pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
373        symlink(original, link)
374    }
375
376    /// Creates a new symlink `link` pointing to the `original` path.
377    ///
378    /// On Unix, the `original` path doesn't have to be a file.
379    pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
380        symlink(original, link)
381    }
382
383    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
384    pub struct FileIdentity {
385        // https://github.com/BurntSushi/same-file/blob/1.0.6/src/unix.rs#L30
386        dev: u64,
387        ino: u64,
388    }
389
390    impl FileIdentity {
391        fn from_metadata(metadata: fs::Metadata) -> Self {
392            Self {
393                dev: metadata.dev(),
394                ino: metadata.ino(),
395            }
396        }
397    }
398
399    pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
400        path.symlink_metadata().map(FileIdentity::from_metadata)
401    }
402
403    pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
404        file.metadata().map(FileIdentity::from_metadata)
405    }
406}
407
408#[cfg(windows)]
409mod platform {
410    use std::fs::File;
411    use std::fs::OpenOptions;
412    use std::io;
413    use std::os::windows::fs::OpenOptionsExt as _;
414    pub use std::os::windows::fs::symlink_dir;
415    pub use std::os::windows::fs::symlink_file;
416    use std::path::Path;
417
418    use winreg::RegKey;
419    use winreg::enums::HKEY_LOCAL_MACHINE;
420
421    pub use super::fallback::BadOsStrEncoding;
422    pub use super::fallback::os_str_from_bytes;
423    pub use super::fallback::os_str_to_bytes;
424
425    /// Symlinks may or may not be enabled on Windows. They require the
426    /// Developer Mode setting, which is stored in the registry key below.
427    ///
428    /// Note: If developer mode is not enabled, the error code of symlink
429    /// creation will be 1314, `ERROR_PRIVILEGE_NOT_HELD`.
430    pub fn check_symlink_support() -> io::Result<bool> {
431        let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
432        let sideloading =
433            hklm.open_subkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock")?;
434        let developer_mode: u32 = sideloading.get_value("AllowDevelopmentWithoutDevLicense")?;
435        Ok(developer_mode == 1)
436    }
437
438    pub type FileIdentity = same_file::Handle;
439
440    pub fn file_identity_from_symlink_path(path: &Path) -> io::Result<FileIdentity> {
441        // `same_file::Handle::from_path()` follows symlinks, because it opens
442        // the path without `FILE_FLAG_OPEN_REPARSE_POINT`. Open the file the
443        // same way it does (read access, plus `FILE_FLAG_BACKUP_SEMANTICS` so a
444        // directory can be opened too), but add `FILE_FLAG_OPEN_REPARSE_POINT`
445        // so the handle refers to the symlink itself instead of its target.
446        // This matches the Unix implementation, which uses `symlink_metadata()`.
447        // The reparse-point flag is ignored for paths that aren't reparse
448        // points, so regular files and hard links are unaffected.
449        const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
450        const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
451        let file = OpenOptions::new()
452            .read(true)
453            .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
454            .open(path)?;
455        same_file::Handle::from_file(file)
456    }
457
458    pub fn file_identity_from_file(file: File) -> io::Result<FileIdentity> {
459        same_file::Handle::from_file(file)
460    }
461}
462
463#[cfg_attr(unix, expect(dead_code))]
464mod fallback {
465    use std::ffi::OsStr;
466
467    use thiserror::Error;
468
469    // Define error per platform so we can explicitly say UTF-8 is expected.
470    #[derive(Debug, Error)]
471    #[error("Invalid UTF-8 sequence")]
472    pub struct BadOsStrEncoding;
473
474    pub fn os_str_from_bytes(data: &[u8]) -> Result<&OsStr, BadOsStrEncoding> {
475        Ok(str::from_utf8(data).map_err(|_| BadOsStrEncoding)?.as_ref())
476    }
477
478    pub fn os_str_to_bytes(data: &OsStr) -> Result<&[u8], BadOsStrEncoding> {
479        Ok(data.to_str().ok_or(BadOsStrEncoding)?.as_ref())
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use futures::io::Cursor;
486    use itertools::Itertools as _;
487    use pollster::FutureExt as _;
488    use test_case::test_case;
489
490    use super::*;
491    use crate::tests::TestResult;
492    use crate::tests::new_temp_dir;
493
494    #[test]
495    #[cfg(unix)]
496    fn exec_bit_support_in_temp_dir() -> TestResult {
497        // Temporary directories on Unix should always have executable support.
498        // Note that it would be problematic to test in a non-temp directory, as
499        // a developer's filesystem may or may not have executable bit support.
500        let dir = new_temp_dir();
501        let supported = check_executable_bit_support(dir.path())?;
502        assert!(supported);
503        Ok(())
504    }
505
506    #[test]
507    fn test_path_bytes_roundtrip() -> TestResult {
508        let bytes = b"ascii";
509        let path = path_from_bytes(bytes)?;
510        assert_eq!(path_to_bytes(path)?, bytes);
511
512        let bytes = b"utf-8.\xc3\xa0";
513        let path = path_from_bytes(bytes)?;
514        assert_eq!(path_to_bytes(path)?, bytes);
515
516        let bytes = b"latin1.\xe0";
517        if cfg!(unix) {
518            let path = path_from_bytes(bytes)?;
519            assert_eq!(path_to_bytes(path)?, bytes);
520        } else {
521            assert!(path_from_bytes(bytes).is_err());
522        }
523        Ok(())
524    }
525
526    #[test]
527    fn test_relative_path() {
528        // Compare as strings, not as (normalized) paths
529        let p = |slash_path: &str| {
530            let mut native_path = slash_path.replace('/', std::path::MAIN_SEPARATOR_STR);
531            if cfg!(windows) && slash_path.starts_with('/') {
532                native_path.insert_str(0, "c:"); // make the path truly absolute
533            }
534            native_path
535        };
536        let relative = |from: &str, to: &str| {
537            relative_path(p(from).as_ref(), p(to).as_ref())
538                .into_os_string()
539                .into_string()
540                .unwrap()
541        };
542
543        assert_eq!(relative("/foo/bar", "/foo/bar"), p("."));
544        assert_eq!(relative("/foo", "/foo/bar"), p("bar"));
545        assert_eq!(relative("/", "/foo/bar"), p("foo/bar"));
546
547        assert_eq!(relative("/foo/bar/baz", "/foo/bar"), p(".."));
548        assert_eq!(relative("/foo/baz", "/foo/bar"), p("../bar"));
549        assert_eq!(relative("/baz", "/foo/bar"), p("../foo/bar"));
550
551        assert_eq!(relative("/foo/bar/baz/qux", "/foo/bar"), p("../.."));
552        assert_eq!(relative("/foo/baz/qux", "/foo/bar"), p("../../bar"));
553        assert_eq!(relative("/baz/qux", "/foo/bar"), p("../../foo/bar"));
554
555        // No common prefix components
556        assert_eq!(relative("foo/bar", "/foo/bar"), p("/foo/bar"));
557        assert_eq!(relative("/foo/bar", "foo/bar"), p("foo/bar"));
558        assert_eq!(relative("./foo/bar", "/./foo/bar"), p("/./foo/bar"));
559        assert_eq!(relative("/./foo/bar", "./foo/bar"), p("./foo/bar"));
560        assert_eq!(relative("/foo", ""), p("")); // or "."
561        assert_eq!(relative("", "/foo"), p("/foo"));
562        assert_eq!(relative("", ""), p("")); // or "."
563
564        // Redundant components are skipped by Path::components()
565        assert_eq!(relative("/./foo/./bar", "/foo/bar"), p("."));
566        assert_eq!(relative("/foo/bar", "/./foo/./bar"), p("."));
567        assert_eq!(relative("/./foo/./bar", "/foo"), p(".."));
568        assert_eq!(relative("/foo", "/./foo/./bar"), p("bar"));
569    }
570
571    #[test]
572    fn test_relative_path_windows() {
573        // Compare as strings, not as (normalized) paths
574        let relative = |from: &str, to: &str| {
575            relative_path(from.as_ref(), to.as_ref())
576                .into_os_string()
577                .into_string()
578                .unwrap()
579        };
580
581        if cfg!(windows) {
582            assert_eq!(relative(r"c:\foo\bar", r"c:\foo\bar"), ".");
583            assert_eq!(relative(r"c:\foo", r"c:\foo\bar"), "bar");
584            assert_eq!(relative(r"c:\", r"c:\foo\bar"), r"foo\bar");
585
586            assert_eq!(relative(r"d:\foo", r"c:\foo\bar"), r"c:\foo\bar");
587            assert_eq!(relative(r"d:\", r"c:\foo\bar"), r"c:\foo\bar");
588
589            assert_eq!(relative(r"\\foo\bar", r"\foo\bar"), r"\foo\bar");
590            assert_eq!(relative(r"\\foo\bar", r"\\foo\bar"), ".");
591            assert_eq!(relative(r"\\foo\bar\baz", r"\\foo\bar\baz"), ".");
592            assert_eq!(relative(r"\\foo\bar\baz", r"\\foo\bar\qux"), r"..\qux");
593            assert_eq!(
594                relative(r"\\foo\bar\baz", r"\\qux\bar\baz"),
595                r"\\qux\bar\baz"
596            );
597        }
598    }
599
600    #[test]
601    fn normalize_too_many_dot_dot() {
602        assert_eq!(normalize_path(Path::new("foo/..")), Path::new("."));
603        assert_eq!(normalize_path(Path::new("foo/../..")), Path::new(".."));
604        assert_eq!(
605            normalize_path(Path::new("foo/../../..")),
606            Path::new("../..")
607        );
608        assert_eq!(
609            normalize_path(Path::new("foo/../../../bar/baz/..")),
610            Path::new("../../bar")
611        );
612    }
613
614    #[test]
615    fn test_slash_path() {
616        assert_eq!(slash_path(Path::new("")), Path::new(""));
617        assert_eq!(slash_path(Path::new("foo")), Path::new("foo"));
618        assert_eq!(slash_path(Path::new("foo/bar")), Path::new("foo/bar"));
619        assert_eq!(slash_path(Path::new("foo/bar/..")), Path::new("foo/bar/.."));
620        assert_eq!(
621            slash_path(Path::new(r"foo\bar")),
622            if cfg!(windows) {
623                Path::new("foo/bar")
624            } else {
625                Path::new(r"foo\bar")
626            }
627        );
628        assert_eq!(
629            slash_path(Path::new(r"..\foo\bar")),
630            if cfg!(windows) {
631                Path::new("../foo/bar")
632            } else {
633                Path::new(r"..\foo\bar")
634            }
635        );
636    }
637
638    #[test]
639    fn test_persist_no_existing_file() -> TestResult {
640        let temp_dir = new_temp_dir();
641        let target = temp_dir.path().join("file");
642        let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
643        temp_file.write_all(b"contents")?;
644        assert!(persist_content_addressed_temp_file(temp_file, target).is_ok());
645        Ok(())
646    }
647
648    #[test_case(false ; "existing file open")]
649    #[test_case(true ; "existing file closed")]
650    fn test_persist_target_exists(existing_file_closed: bool) -> TestResult {
651        let temp_dir = new_temp_dir();
652        let target = temp_dir.path().join("file");
653        let mut temp_file = NamedTempFile::new_in(&temp_dir)?;
654        temp_file.write_all(b"contents")?;
655
656        let mut file = File::create(&target)?;
657        file.write_all(b"contents")?;
658        if existing_file_closed {
659            drop(file);
660        }
661
662        assert!(persist_content_addressed_temp_file(temp_file, &target).is_ok());
663        Ok(())
664    }
665
666    #[test]
667    fn test_file_identity_hard_link() -> TestResult {
668        let temp_dir = new_temp_dir();
669        let file_path = temp_dir.path().join("file");
670        let other_file_path = temp_dir.path().join("other_file");
671        let link_path = temp_dir.path().join("link");
672        fs::write(&file_path, "")?;
673        fs::write(&other_file_path, "")?;
674        fs::hard_link(&file_path, &link_path)?;
675        assert_eq!(
676            FileIdentity::from_symlink_path(&file_path)?,
677            FileIdentity::from_symlink_path(&link_path)?
678        );
679        assert_ne!(
680            FileIdentity::from_symlink_path(&other_file_path)?,
681            FileIdentity::from_symlink_path(&link_path)?
682        );
683        assert_eq!(
684            FileIdentity::from_symlink_path(&file_path)?,
685            FileIdentity::from_file(File::open(&link_path)?)?
686        );
687        Ok(())
688    }
689
690    #[cfg(unix)]
691    #[test]
692    fn test_file_identity_unix_symlink_dir() -> TestResult {
693        let temp_dir = new_temp_dir();
694        let dir_path = temp_dir.path().join("dir");
695        let symlink_path = temp_dir.path().join("symlink");
696        fs::create_dir(&dir_path)?;
697        std::os::unix::fs::symlink("dir", &symlink_path)?;
698        // symlink should be identical to itself
699        assert_eq!(
700            FileIdentity::from_symlink_path(&symlink_path)?,
701            FileIdentity::from_symlink_path(&symlink_path)?
702        );
703        // symlink should be different from the target directory
704        assert_ne!(
705            FileIdentity::from_symlink_path(&dir_path)?,
706            FileIdentity::from_symlink_path(&symlink_path)?
707        );
708        // File::open() follows symlinks
709        assert_eq!(
710            FileIdentity::from_symlink_path(&dir_path)?,
711            FileIdentity::from_file(File::open(&symlink_path)?)?
712        );
713        assert_ne!(
714            FileIdentity::from_symlink_path(&symlink_path)?,
715            FileIdentity::from_file(File::open(&symlink_path)?)?
716        );
717        Ok(())
718    }
719
720    #[cfg(windows)]
721    #[test]
722    fn test_file_identity_windows_symlink_file() -> TestResult {
723        if !check_symlink_support()? {
724            return Ok(());
725        }
726        let temp_dir = new_temp_dir();
727        let file_path = temp_dir.path().join("file");
728        let symlink_path = temp_dir.path().join("symlink");
729        fs::write(&file_path, "")?;
730        symlink_file("file", &symlink_path)?;
731        // symlink should be identical to itself
732        assert_eq!(
733            FileIdentity::from_symlink_path(&symlink_path)?,
734            FileIdentity::from_symlink_path(&symlink_path)?
735        );
736        // symlink should be different from the target file
737        assert_ne!(
738            FileIdentity::from_symlink_path(&file_path)?,
739            FileIdentity::from_symlink_path(&symlink_path)?
740        );
741        // File::open() follows symlinks
742        assert_eq!(
743            FileIdentity::from_symlink_path(&file_path)?,
744            FileIdentity::from_file(File::open(&symlink_path)?)?
745        );
746        assert_ne!(
747            FileIdentity::from_symlink_path(&symlink_path)?,
748            FileIdentity::from_file(File::open(&symlink_path)?)?
749        );
750        Ok(())
751    }
752
753    #[cfg(windows)]
754    #[test]
755    fn test_file_identity_windows_symlink_dir() -> TestResult {
756        if !check_symlink_support()? {
757            return Ok(());
758        }
759        let temp_dir = new_temp_dir();
760        let dir_path = temp_dir.path().join("dir");
761        let symlink_path = temp_dir.path().join("symlink");
762        fs::create_dir(&dir_path)?;
763        symlink_dir("dir", &symlink_path)?;
764        // symlink should be identical to itself
765        assert_eq!(
766            FileIdentity::from_symlink_path(&symlink_path)?,
767            FileIdentity::from_symlink_path(&symlink_path)?
768        );
769        // symlink should be different from the target directory. The
770        // `File::open()` follow-through is not checked here because File::open()
771        // can't open a directory on Windows.
772        assert_ne!(
773            FileIdentity::from_symlink_path(&dir_path)?,
774            FileIdentity::from_symlink_path(&symlink_path)?
775        );
776        Ok(())
777    }
778
779    #[test]
780    fn test_file_identity_directory() -> TestResult {
781        let temp_dir = new_temp_dir();
782        let dir_path = temp_dir.path().join("dir");
783        let other_dir_path = temp_dir.path().join("other_dir");
784        fs::create_dir(&dir_path)?;
785        fs::create_dir(&other_dir_path)?;
786        // a directory should be identical to itself
787        assert_eq!(
788            FileIdentity::from_symlink_path(&dir_path)?,
789            FileIdentity::from_symlink_path(&dir_path)?
790        );
791        // distinct directories should differ
792        assert_ne!(
793            FileIdentity::from_symlink_path(&dir_path)?,
794            FileIdentity::from_symlink_path(&other_dir_path)?
795        );
796        Ok(())
797    }
798
799    #[cfg(unix)]
800    #[test]
801    fn test_file_identity_unix_symlink_loop() -> TestResult {
802        let temp_dir = new_temp_dir();
803        let lower_file_path = temp_dir.path().join("file");
804        let upper_file_path = temp_dir.path().join("FILE");
805        let lower_symlink_path = temp_dir.path().join("symlink");
806        let upper_symlink_path = temp_dir.path().join("SYMLINK");
807        fs::write(&lower_file_path, "")?;
808        std::os::unix::fs::symlink("symlink", &lower_symlink_path)?;
809        let is_icase_fs = upper_file_path.try_exists()?;
810        // symlink should be identical to itself
811        assert_eq!(
812            FileIdentity::from_symlink_path(&lower_symlink_path)?,
813            FileIdentity::from_symlink_path(&lower_symlink_path)?
814        );
815        assert_ne!(
816            FileIdentity::from_symlink_path(&lower_symlink_path)?,
817            FileIdentity::from_symlink_path(&lower_file_path)?
818        );
819        if is_icase_fs {
820            assert_eq!(
821                FileIdentity::from_symlink_path(&lower_symlink_path)?,
822                FileIdentity::from_symlink_path(&upper_symlink_path)?
823            );
824        } else {
825            assert!(FileIdentity::from_symlink_path(&upper_symlink_path).is_err());
826        }
827        Ok(())
828    }
829
830    #[test]
831    fn test_copy_async_to_sync_small() -> TestResult {
832        let input = b"hello";
833        let mut output = vec![];
834
835        let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
836        assert!(result.is_ok());
837        assert_eq!(result?, 5);
838        assert_eq!(output, input);
839        Ok(())
840    }
841
842    #[test]
843    fn test_copy_async_to_sync_large() -> TestResult {
844        // More than 1 buffer worth of data
845        let input = (0..100u8).cycle().take(40000).collect_vec();
846        let mut output = vec![];
847
848        let result = copy_async_to_sync(Cursor::new(&input), &mut output).block_on();
849        assert!(result.is_ok());
850        assert_eq!(result?, 40000);
851        assert_eq!(output, input);
852        Ok(())
853    }
854}