util/
path.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! Path-related utilities.
9
10use std::borrow::Cow;
11use std::collections::HashSet;
12use std::env;
13use std::ffi::OsStr;
14use std::fs;
15use std::fs::remove_file as fs_remove_file;
16use std::io;
17use std::io::ErrorKind;
18use std::io::Write;
19#[cfg(unix)]
20use std::os::unix::fs::PermissionsExt;
21use std::path::Component;
22use std::path::Path;
23use std::path::PathBuf;
24
25use anyhow::bail;
26use anyhow::Context;
27use fn_error_context::context;
28
29use crate::errors::IOContext;
30
31/// Pick a random file name `path.$RAND.atomic` as `real_path`. Write `data` to
32/// it.  Then modify the symlink `path` to point to `real_path`.  Attempt to
33/// delete files that are no longer referred.
34///
35/// Since the symlink itself cannot be mmap-ed on Windows, this function is
36/// suitable for large mmap buffer on Windows. Without a symlink the mmap
37/// file has to be removed first, otherwise it cannot be replaced.
38///
39/// Unlike `tempfile::NamedTempFile`, this function does not `chmod` the file.
40///
41/// This function has a side effect of creating a `path.lock` file for
42/// locking.
43///
44/// Attention: the deletion attempt is based on file name. So do not use
45/// confusing file names like `path.0001.atomic` in the same directory.
46pub fn atomic_write_symlink(path: &Path, data: &[u8]) -> io::Result<()> {
47    let append_name = |suffix: &str| -> PathBuf {
48        let mut s = path.to_path_buf().into_os_string();
49        s.push(suffix);
50        s.into()
51    };
52    let temp_name = || -> PathBuf { append_name(&format!(".{:x}.atomic", rand::random::<u32>())) };
53
54    // Protect racy write operations by a lock.
55    let _lock = crate::lock::PathLock::exclusive(append_name(".lock"))?;
56
57    // Pick a name. Open the file.
58    let (real_path, mut file) = loop {
59        let real_path = temp_name();
60        match fs::OpenOptions::new()
61            .create_new(true)
62            .write(true)
63            .open(&real_path)
64        {
65            Ok(file) => break Ok((real_path, file)),
66            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue, // try another file name
67            Err(e) => {
68                break Err(e).path_context("error opening atomic symlink real path", &real_path);
69            }
70        }
71    }?;
72    let real_file_name = real_path
73        .file_name()
74        .expect("real_path should have a file name");
75
76    // Write the content.
77    file.write_all(data)
78        .path_context("error writing atomic symlink data to real path", &real_path)?;
79    drop(file);
80
81    // Update the symlink by creating a temporary symlink and rename it.
82    let symlink_tmp_path = loop {
83        let symlink_path = temp_name();
84        match symlink_file(Path::new(real_file_name), &symlink_path) {
85            Ok(()) => break Ok(symlink_path),
86            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue, // try another file name
87            Err(e) => {
88                break Err(e).path_context("error creating temp atomic symlink", &symlink_path);
89            }
90        }
91    }?;
92
93    // Overwrite the original symlink. This works on both Windows and Linux.
94    fs::rename(symlink_tmp_path, path).path_context("error renaming temp atomic symlink", path)?;
95
96    // Scan. Remove unreferenced files.
97    let _ = (|| -> io::Result<()> {
98        let looks_like_atomic = |s: &OsStr, prefix: &OsStr| -> bool {
99            if let (Some(s), Some(prefix)) = (s.to_str(), prefix.to_str()) {
100                s.starts_with(prefix) && s.ends_with(".atomic")
101            } else {
102                false
103            }
104        };
105        if let (Some(dir), Some(prefix)) = (path.parent(), path.file_name()) {
106            for entry in fs::read_dir(dir).path_context("error reading atomic symlink dir", dir)? {
107                let entry = entry.path_context("error reading atomic symlink dir entry", dir)?;
108                let name = entry.file_name();
109                if name != prefix && looks_like_atomic(&name, prefix) && name != real_file_name {
110                    let _ = remove_file(&entry.path());
111                }
112            }
113        }
114        Ok(())
115    })();
116
117    Ok(())
118}
119
120/// Create symlink for a file.
121pub fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
122    #[cfg(windows)]
123    return std::os::windows::fs::symlink_file(src, dst);
124    #[cfg(unix)]
125    return std::os::unix::fs::symlink(src, dst);
126    #[cfg(all(not(unix), not(windows)))]
127    return Err(io::Error::new(
128        ErrorKind::Other,
129        "symlink is not supported by the system",
130    ));
131}
132
133/// Create symlink for a dir.
134pub fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
135    #[cfg(windows)]
136    return std::os::windows::fs::symlink_dir(src, dst);
137    #[cfg(not(windows))]
138    symlink_file(src, dst)
139}
140
141/// Removes the UNC prefix `\\?\` on Windows. Does nothing on unices.
142pub fn strip_unc_prefix(path: &Path) -> &Path {
143    path.strip_prefix(r"\\?\").unwrap_or(path)
144}
145
146/// Return the absolute and normalized path without accessing the filesystem.
147///
148/// Unlike [`fs::canonicalize`], do not follow symlinks.
149///
150/// This function does not access the filesystem. Therefore it can behave
151/// differently from the kernel or other library functions in corner cases.
152/// For example:
153///
154/// - On some systems with symlink support, `foo/bar/..` and `foo` can be
155///   different as seen by the kernel, if `foo/bar` is a symlink. This
156///   function always returns `foo` in this case.
157/// - On Windows, the official normalization rules are much more complicated.
158///   See https://github.com/rust-lang/rust/pull/47363#issuecomment-357069527.
159///   For example, this function cannot translate "drive relative" path like
160///   "X:foo" to an absolute path.
161///
162/// Return an error if `std::env::current_dir()` fails or if this function
163/// fails to produce an absolute path.
164pub fn absolute(path: impl AsRef<Path>) -> io::Result<PathBuf> {
165    let path = path.as_ref();
166    let path = if path.is_absolute() {
167        path.to_path_buf()
168    } else {
169        std::env::current_dir()?.join(path)
170    };
171
172    if !path.is_absolute() {
173        return Err(io::Error::new(
174            io::ErrorKind::Other,
175            format!("cannot get absolute path from {:?}", path),
176        ));
177    }
178
179    Ok(normalize(&path))
180}
181
182/// Normalize path to collapse "..", ".", and duplicate separators. This
183/// function does not access the filesystem, so it can return an
184/// incorrect result if the path contains symlinks.
185///
186///     # use std::path::Path;
187///     # use util::path::normalize;
188///     assert_eq!(normalize("foo/.//bar/../baz/".as_ref()), Path::new("foo/baz"));
189///
190///     // Interesting edge cases:
191///     assert_eq!(normalize("".as_ref()), Path::new("."));
192///     assert_eq!(normalize("..".as_ref()), Path::new(".."));
193///     assert_eq!(normalize("/..".as_ref()), Path::new("/"));
194///
195/// This behavior matches that of Python's `os.path.normpath` and Go's `path.Clean`.
196pub fn normalize(path: &Path) -> PathBuf {
197    let mut result = PathBuf::new();
198    let mut poppable: usize = 0;
199    let mut has_root = false;
200    for component in path.components() {
201        match component {
202            Component::Normal(_) => {
203                poppable += 1;
204                result.push(component);
205            }
206            Component::RootDir => {
207                has_root = true;
208                result.push(component);
209            }
210            Component::Prefix(_) => {
211                result.push(component);
212            }
213            Component::ParentDir => {
214                if poppable > 0 {
215                    result.pop();
216                    poppable -= 1;
217                } else if !has_root {
218                    result.push(component);
219                }
220            }
221            Component::CurDir => {}
222        }
223    }
224
225    if result.as_os_str().is_empty() {
226        return ".".into();
227    }
228
229    result
230}
231
232/// Given cwd, return `path` relative to `root`, or None if `path` is not under `root`.
233/// This is analagous to pathutil.canonpath() in Python.
234pub fn root_relative_path(root: &Path, cwd: &Path, path: &Path) -> io::Result<Option<PathBuf>> {
235    // Make `path` absolute. I'm not sure why `root` is included.
236    // Maybe in case `cwd` is empty? Or to allow root-relative `cwd`?
237    let path = normalize(&root.join(cwd).join(path));
238
239    // Handle easy case when `path` lexically starts w/ `root`.
240    if let Ok(suffix) = path.strip_prefix(root) {
241        return Ok(Some(suffix.to_path_buf()));
242    }
243
244    // Resolve symlinks in `root` so we can do lexical `strip_prefix` below.
245    let root = root.canonicalize().path_context("canonicalizing", root)?;
246
247    // Test parents of `path` looking for symlinks that point under `root`.
248    let mut test = PathBuf::new();
249    let mut path_parts = path.components();
250    while let Some(part) = path_parts.next() {
251        test.push(part);
252        if test.is_symlink() {
253            // TODO: this makes our loop O(n^2)
254            test = test.canonicalize().path_context("canonicalizing", &test)?;
255        }
256        if let Ok(suffix) = test.strip_prefix(&root) {
257            return Ok(Some(suffix.join(path_parts)));
258        }
259    }
260
261    Ok(None)
262}
263
264/// Remove the file pointed by `path`.
265pub fn remove_file<P: AsRef<Path>>(path: P) -> io::Result<()> {
266    let path = path.as_ref();
267    // On Windows, try to rename the file before removing.
268    // This allows re-creating a same file.
269    // See https://boostgsoc13.github.io/boost.afio/doc/html/afio/FAQ/deleting_open_files.html
270    let path: Cow<Path> = if cfg!(windows) {
271        let tmp_path = path.with_extension(format!("tmp.{:x}", rand::random::<u16>()));
272        fs::rename(path, &tmp_path)?;
273        Cow::Owned(tmp_path)
274    } else {
275        Cow::Borrowed(path)
276    };
277    // This borrow is not needless when `#[cfg(windows)]`
278    #[allow(clippy::needless_borrows_for_generic_args)]
279    let result = fs_remove_file(&path);
280    #[cfg(windows)]
281    match &result {
282        Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
283            // The file might be a directory symlink
284            if let Ok(r) = remove_directory_symlink(&path) {
285                if r {
286                    return Ok(());
287                }
288            }
289            // This file might be mmapp-ed. Try a different way.
290            if windows_remove_mmap_file(&path).is_ok() {
291                return Ok(());
292            }
293        }
294        _ => {}
295    }
296    result.map_err(Into::into)
297}
298
299#[cfg(windows)]
300/// Tries to remove a symlink in case it was a directory symlink
301fn remove_directory_symlink(path: &Path) -> io::Result<bool> {
302    let metadata = path.symlink_metadata()?;
303    if metadata.is_symlink() {
304        std::fs::remove_dir(path)?;
305        return Ok(true);
306    }
307    Ok(false)
308}
309
310/// Deletes a file even if it is being mmap-ed on Windows.
311/// See https://stackoverflow.com/questions/54138684.
312#[cfg(windows)]
313fn windows_remove_mmap_file(path: &Path) -> io::Result<()> {
314    use std::os::windows::ffi::OsStrExt;
315
316    use winapi::shared::minwindef::FALSE;
317    use winapi::um::fileapi::CreateFileW;
318    use winapi::um::fileapi::OPEN_EXISTING;
319    use winapi::um::handleapi::CloseHandle;
320    use winapi::um::handleapi::INVALID_HANDLE_VALUE;
321    use winapi::um::winbase::FILE_FLAG_DELETE_ON_CLOSE;
322    use winapi::um::winnt::DELETE;
323    use winapi::um::winnt::FILE_SHARE_DELETE;
324    use winapi::um::winnt::FILE_SHARE_READ;
325    use winapi::um::winnt::FILE_SHARE_WRITE;
326
327    let wpath: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
328    let handle = unsafe {
329        CreateFileW(
330            wpath.as_ptr(),
331            DELETE,
332            FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
333            std::ptr::null_mut(),
334            OPEN_EXISTING,
335            FILE_FLAG_DELETE_ON_CLOSE,
336            std::ptr::null_mut(),
337        )
338    };
339    if handle == INVALID_HANDLE_VALUE {
340        let err = io::Error::last_os_error();
341        if err.kind() == io::ErrorKind::NotFound {
342            Ok(())
343        } else {
344            Err(err)
345        }
346    } else if unsafe { CloseHandle(handle) } == FALSE {
347        Err(io::Error::last_os_error())
348    } else {
349        Ok(())
350    }
351}
352
353#[cfg(unix)]
354fn add_stat_context<T, E: Into<anyhow::Error>>(
355    res: anyhow::Result<T, E>,
356    path: Option<&Path>,
357) -> anyhow::Result<T, anyhow::Error> {
358    use std::os::unix::fs::MetadataExt;
359
360    let res = res.map_err(Into::into);
361
362    if let Some(path) = path {
363        if let Ok(md) = path.metadata() {
364            return res.context(format!(
365                "stat({:?}) = dev:{} ino:{} mode:0o{:o} uid:{} gid:{} mtime:{}",
366                path,
367                md.dev(),
368                md.ino(),
369                md.mode(),
370                md.uid(),
371                md.gid(),
372                md.mtime()
373            ));
374        }
375    }
376
377    res
378}
379
380/// Resolve leaf symlinks until we get to a non-symlink or non-existent, returning Ok(dest).
381/// Propagates unexpected errors like permission errors.
382#[cfg(unix)]
383fn resolve_symlinks(path: &Path) -> anyhow::Result<PathBuf> {
384    fn inner(path: PathBuf, seen: &mut HashSet<PathBuf>) -> anyhow::Result<PathBuf> {
385        if seen.contains(&path) {
386            bail!("symlink cycle containing {:?}", path);
387        }
388
389        seen.insert(path.clone());
390
391        match path.read_link() {
392            Ok(target) => inner(target, seen),
393
394            // Not a symlink.
395            Err(err) if err.kind() == io::ErrorKind::InvalidInput => Ok(path),
396            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(path),
397
398            // Unexpected error reading file.
399            Err(err) => add_stat_context(
400                Err(err).context(format!("statting {:?}", path)),
401                path.parent(),
402            ),
403        }
404    }
405
406    let mut seen = HashSet::new();
407    let mut res = inner(path.to_path_buf(), &mut seen);
408    if seen.len() > 1 {
409        res = res.with_context(|| format!("traversing symlinks from {:?}", path));
410    }
411    res
412}
413
414/// Create the directory with specified permission on UNIX systems. Return
415/// Ok(()) if directory already exists. We create a temporary directory at the
416/// parent directory of the the directory being created, run chmod to change the
417/// permission then rename the temporary directory to the desired name to
418/// prevent leaking directory with incorrect permissions.
419#[cfg(unix)]
420#[context("creating dir {:?} with mode 0o{:o}", path, mode)]
421fn create_dir_with_mode(path: &Path, mode: u32) -> anyhow::Result<()> {
422    use anyhow::anyhow;
423
424    let path = resolve_symlinks(path)?;
425
426    match path.metadata() {
427        Ok(md) if md.is_file() => {
428            return Err(anyhow!(io::Error::from(ErrorKind::AlreadyExists)))
429                .context(format!("path exists as a file: {:?}", path));
430        }
431        Ok(md) => {
432            // Symlinks were resolved above - assume is_dir.
433            if md.permissions().mode() & mode != mode {
434                // Best effort to fix permissions.
435                let _ = fs::set_permissions(path, fs::Permissions::from_mode(mode));
436            }
437            return Ok(());
438        }
439        // Fall through and try creating it.
440        Err(err) if err.kind() == io::ErrorKind::NotFound => {}
441        Err(err) => {
442            return add_stat_context(
443                Err(err).context(format!("error statting {:?}", path)),
444                path.parent(),
445            );
446        }
447    };
448
449    let parent = path.parent().ok_or_else(|| {
450        io::Error::new(
451            ErrorKind::NotFound,
452            format!("`{:?}` does not have a parent directory", path),
453        )
454    })?;
455
456    let parent = resolve_symlinks(parent)?;
457
458    let temp = add_stat_context(tempfile::TempDir::new_in(&parent), Some(&parent))
459        .with_context(|| format!("creating temp dir in {:?}", parent))?;
460
461    fs::set_permissions(&temp, fs::Permissions::from_mode(mode))
462        .with_context(|| format!("setting permissions on temp dir {:?}", temp))?;
463
464    let temp = temp.into_path();
465    if let Err(e) = fs::rename(&temp, &path) {
466        // In the unlikely event where the rename fails, we attempt to clean up the
467        // previously leaked temporary file before returning.
468        let _ = fs::remove_dir(&temp);
469
470        // The rename may fail if the desinated directory already exists and is not empty. In this
471        // case it will return `ENOTEMPTY` instead of `EEXIST`. Rust does not have an
472        // `io::ErrorKind` for such error, and it will be categorized into `ErrorKind::Other`. We
473        // have to use `libc::ENOTEMPTY` because the integer value of `ENOTEMPTY` varies depends on
474        // platform we are on.
475        // Similarly, when the destinated directory is a file, we get `ENOTDIR` instead of `EEXIST`.
476        match e.raw_os_error() {
477            Some(libc::ENOTEMPTY) | Some(libc::ENOTDIR) => {
478                Err(io::Error::from(ErrorKind::AlreadyExists).into())
479            }
480            _ => Err::<(), anyhow::Error>(e.into())
481                .context(format!("renaming temp dir {:?} to {:?}", temp, &path)),
482        }
483    } else {
484        Ok(())
485    }
486}
487
488/// Create the directory. Return Ok(()) if directory already exists.
489/// The mode argument is ignored on non-UNIX systems.
490#[cfg(not(unix))]
491#[context("creating dir {:?}", path)]
492fn create_dir_with_mode(path: &Path, _mode: u32) -> anyhow::Result<()> {
493    match fs::create_dir(path) {
494        Ok(()) => Ok(()),
495        Err(err) if err.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => Ok(()),
496        Err(err) => Err(err.into()),
497    }
498}
499
500fn is_io_error_kind(err: &anyhow::Error, kind: ErrorKind) -> bool {
501    err.downcast_ref::<io::Error>()
502        .is_some_and(|err| err.kind() == kind)
503}
504
505/// Create the directory and ignore failures when a directory of the same name already exists.
506pub fn create_dir(path: impl AsRef<Path>) -> anyhow::Result<()> {
507    create_dir_with_mode(path.as_ref(), 0o755)
508}
509
510/// Create the directory with group write permission on UNIX systems.
511pub fn create_shared_dir(path: impl AsRef<Path>) -> anyhow::Result<()> {
512    create_dir_with_mode(path.as_ref(), 0o2775)
513}
514
515/// Create the directory and its ancestors. The mode argument is ignored on non-UNIX systems.
516pub fn create_dir_all_with_mode(path: impl AsRef<Path>, mode: u32) -> anyhow::Result<()> {
517    let mut to_create = vec![path.as_ref()];
518    while let Some(dir) = to_create.pop() {
519        match create_dir_with_mode(dir, mode) {
520            Ok(()) => continue,
521            Err(err) if is_io_error_kind(&err, io::ErrorKind::NotFound) => {
522                to_create.push(dir);
523                match dir.parent() {
524                    Some(parent) => to_create.push(parent),
525                    None => return Err(err),
526                }
527            }
528            Err(err) => return Err(err),
529        }
530    }
531    Ok(())
532}
533
534/// Create the directory and ancestors with group write permission on UNIX systems.
535pub fn create_shared_dir_all(path: impl AsRef<Path>) -> anyhow::Result<()> {
536    create_dir_all_with_mode(path, 0o2775)
537}
538
539/// Expand the user's home directory and any environment variables references in
540/// the given path.
541///
542/// This function is designed to emulate the behavior of Mercurial's `util.expandpath`
543/// function, which in turn uses Python's `os.path.expand{user,vars}` functions. This
544/// results in behavior that is notably different from the default expansion behavior
545/// of the `shellexpand` crate. In particular:
546///
547/// - If a reference to an environment variable is missing or invalid, the reference
548///   is left unchanged in the resulting path rather than emitting an error.
549///
550/// - Home directory expansion explicitly happens after environment variable
551///   expansion, meaning that if an environment variable is expanded into a
552///   string starting with a tilde (`~`), the tilde will be expanded into the
553///   user's home directory.
554pub fn expand_path(path: impl AsRef<str>) -> PathBuf {
555    expand_path_impl(path.as_ref(), |k| env::var(k).ok(), dirs::home_dir)
556}
557
558/// Same as `expand_path` but explicitly takes closures for environment variable
559/// and home directory lookup for the sake of testability.
560fn expand_path_impl<E, H>(path: &str, getenv: E, homedir: H) -> PathBuf
561where
562    E: FnMut(&str) -> Option<String>,
563    H: FnOnce() -> Option<PathBuf>,
564{
565    // The shellexpand crate does not expand Windows environment variables
566    // like `%PROGRAMDATA%`. We'd like to expand them too. So let's do some
567    // pre-processing.
568    //
569    // XXX: Doing this preprocessing has the unfortunate side-effect that
570    // if an environment variable fails to expand on Windows, the resulting
571    // string will contain a UNIX-style environment variable reference.
572    //
573    // e.g., "/foo/%MISSING%/bar" will expand to "/foo/${MISSING}/bar"
574    //
575    // The current approach is good enough for now, but likely needs to
576    // be improved later for correctness.
577    let path = {
578        let mut new_path = String::new();
579        let mut is_starting = true;
580        for ch in path.chars() {
581            if ch == '%' {
582                if is_starting {
583                    new_path.push_str("${");
584                } else {
585                    new_path.push('}');
586                }
587                is_starting = !is_starting;
588            } else if cfg!(windows) && ch == '/' {
589                // Only on Windows, change "/" to "\" automatically.
590                // This makes sure "%include /foo" works as expected.
591                new_path.push('\\')
592            } else {
593                new_path.push(ch);
594            }
595        }
596        new_path
597    };
598
599    let path = shellexpand::env_with_context_no_errors(&path, getenv);
600    shellexpand::tilde_with_context(&path, homedir)
601        .as_ref()
602        .into()
603}
604
605/// Return a relative [`PathBuf`] to the path from the base path.
606pub fn relativize(base: &Path, path: &Path) -> PathBuf {
607    let mut base_iter = base.iter();
608    let mut path_iter = path.iter();
609    let mut rel_path = PathBuf::new();
610    loop {
611        match (base_iter.next(), path_iter.next()) {
612            (Some(ref c), Some(ref p)) if c == p => continue,
613
614            // Examples:
615            // b: foo/bar/baz/
616            // p: foo/bar/biz/buzz.html
617            // This is the common case where we have to go up some number of directories
618            // (so one "../" per unique path component of base) and then back down.
619            //
620            // b: foo/bar/baz/biz/
621            // p: foo/bar/
622            // If foo/bar was a file and then the user replaced it with a directory, and now
623            // the user is in a subdirectory of that directory, then one "../" per unique path
624            // component of base.
625            (Some(_c), remaining_path) => {
626                // Find the common prefix of path and base. Prefix with one "../" per unique
627                // path component of base and then append the unique sequence of components from
628                // path.
629                rel_path.push(".."); // This is for the current component, c.
630                for _ in base_iter {
631                    rel_path.push("..");
632                }
633
634                if let Some(p) = remaining_path {
635                    rel_path.push(p);
636                    for component in path_iter {
637                        rel_path.push(component);
638                    }
639                }
640                break;
641            }
642
643            // Example:
644            // b: foo/bar/
645            // p: foo/bar/baz/buzz.html
646            (None, Some(p)) => {
647                rel_path.push(p);
648                for component in path_iter {
649                    rel_path.push(component);
650                }
651                break;
652            }
653
654            // Example:
655            // b: foo/bar/baz/
656            // p: foo/bar/baz/
657            // If foo/bar/baz was a file and then the user replaced it with a directory, which
658            // is also the user's current directory, then "" should be returned.
659            (None, None) => {
660                break;
661            }
662        }
663    }
664
665    rel_path
666}
667
668/// Replace forward slashes (/) with backward slashes (\) on a wide coded-path
669#[cfg(windows)]
670pub fn replace_slash_with_backslash(path: &Path) -> PathBuf {
671    use std::os::windows::ffi::OsStrExt;
672    use std::os::windows::ffi::OsStringExt;
673
674    use widestring::U16Str;
675
676    // Convert OsString to Vec<u16> (UTF-16 representation on Windows)
677    let mut utf16_string: Vec<u16> = path.as_os_str().encode_wide().collect();
678
679    let to_replace = U16Str::from_slice(&mut utf16_string)
680        .char_indices()
681        .filter_map(|(i, c)| {
682            c.ok()
683                .map(|c| if c == '/' { Some(i) } else { None })
684                .flatten()
685        })
686        .collect::<Vec<_>>();
687
688    for i in to_replace {
689        utf16_string[i] = '\\' as u16;
690    }
691
692    PathBuf::from(std::ffi::OsString::from_wide(&utf16_string))
693}
694
695#[cfg(test)]
696mod tests {
697    use std::fs::create_dir_all;
698    use std::fs::File;
699
700    use anyhow::Result;
701    use tempfile::TempDir;
702
703    use super::*;
704
705    #[cfg(windows)]
706    mod windows {
707        use std::os::windows::ffi::OsStrExt;
708        use std::os::windows::ffi::OsStringExt;
709
710        use super::*;
711
712        #[test]
713        fn test_absolute_fullpath() {
714            assert_eq!(absolute("C:/foo").unwrap(), Path::new("C:\\foo"));
715            assert_eq!(
716                absolute("x:\\a/b\\./.\\c").unwrap(),
717                Path::new("x:\\a\\b\\c")
718            );
719            assert_eq!(
720                absolute("y:/a/b\\../..\\c\\../d\\./.").unwrap(),
721                Path::new("y:\\d")
722            );
723            assert_eq!(
724                absolute("z:/a/b\\../..\\../..\\..").unwrap(),
725                Path::new("z:\\")
726            );
727        }
728
729        #[test]
730        fn test_normalize_path() {
731            assert_eq!(normalize(r"a/b\c\..\.".as_ref()), Path::new(r"a\b"));
732            assert_eq!(normalize("z:/a//b/./".as_ref()), Path::new(r"z:\a\b"));
733        }
734
735        #[test]
736        fn test_replace_slash_with_backslash() {
737            // Test replacing a normal string
738            let utf16_bytes: &[u16] = &[0xd83c, 0xdf31, 0x002f, 0x0073, 0x0061, 0x0070];
739            let path = PathBuf::from(std::ffi::OsString::from_wide(utf16_bytes));
740            let expected = "🌱\\sap".encode_utf16().collect::<Vec<_>>();
741            assert_eq!(
742                replace_slash_with_backslash(&path)
743                    .as_os_str()
744                    .encode_wide()
745                    .collect::<Vec<_>>(),
746                expected,
747            );
748
749            // Test replacing string with the / character encoded on it
750            // This string is the same as "expected" above, but with one character corrupted
751            let utf16_bytes: &[u16] = &[0xd83c, 0x002f, 0x002f, 0x0073, 0x0061, 0x0070];
752            // 0x005c is the UTF-16 character for backslash
753            let expected: Vec<u16> = Vec::from([0xd83c, 0x005c, 0x005c, 0x0073, 0x0061, 0x0070]);
754            let path = PathBuf::from(std::ffi::OsString::from_wide(utf16_bytes));
755            assert_eq!(
756                replace_slash_with_backslash(&path)
757                    .as_os_str()
758                    .encode_wide()
759                    .collect::<Vec<_>>(),
760                expected,
761            );
762
763            // Another case of / being on unexpected places
764            let utf16_bytes: &[u16] = &[0x002f, 0xdf31, 0x002f, 0x0073, 0x0061, 0x0070];
765            let expected: Vec<u16> = Vec::from([0x005c, 0xdf31, 0x005c, 0x0073, 0x0061, 0x0070]);
766            let path = PathBuf::from(std::ffi::OsString::from_wide(utf16_bytes));
767            assert_eq!(
768                replace_slash_with_backslash(&path)
769                    .as_os_str()
770                    .encode_wide()
771                    .collect::<Vec<_>>(),
772                expected,
773            );
774        }
775    }
776
777    #[cfg(unix)]
778    mod unix {
779        use super::*;
780
781        #[test]
782        fn test_absolute_fullpath() {
783            assert_eq!(absolute("/a/./b\\c/../d/.").unwrap(), Path::new("/a/d"));
784            assert_eq!(absolute("/a/../../../../b").unwrap(), Path::new("/b"));
785            assert_eq!(absolute("/../../..").unwrap(), Path::new("/"));
786            assert_eq!(absolute("/../../../").unwrap(), Path::new("/"));
787            assert_eq!(
788                absolute("//foo///bar//baz").unwrap(),
789                Path::new("/foo/bar/baz")
790            );
791            assert_eq!(absolute("//").unwrap(), Path::new("/"));
792        }
793
794        #[test]
795        fn test_normalize_path() {
796            assert_eq!(normalize("/a/./b/../d/.".as_ref()), Path::new("/a/d"));
797            assert_eq!(normalize("./a/b/c/../../".as_ref()), Path::new("a"));
798            assert_eq!(normalize("".as_ref()), Path::new("."));
799            assert_eq!(normalize(".".as_ref()), Path::new("."));
800            assert_eq!(normalize("..".as_ref()), Path::new(".."));
801            assert_eq!(normalize("/..".as_ref()), Path::new("/"));
802            assert_eq!(normalize("/../..".as_ref()), Path::new("/"));
803            assert_eq!(normalize("./..".as_ref()), Path::new(".."));
804            assert_eq!(normalize("../../..".as_ref()), Path::new("../../.."));
805            assert_eq!(normalize("////".as_ref()), Path::new("/"));
806        }
807
808        #[test]
809        fn test_create_dir_mode() -> Result<()> {
810            let tempdir = TempDir::new()?;
811            let mut path = tempdir.path().to_path_buf();
812            path.push("dir");
813            create_dir(&path)?;
814            assert!(path.is_dir());
815            let metadata = path.metadata()?;
816            assert_eq!(metadata.permissions().mode(), 0o40755);
817            // check we don't have temporary directory left
818            assert_eq!(tempdir.path().read_dir()?.count(), 1);
819            Ok(())
820        }
821
822        #[test]
823        fn test_create_shared_dir() -> Result<()> {
824            let tempdir = TempDir::new()?;
825            let mut path = tempdir.path().to_path_buf();
826            path.push("shared");
827            create_shared_dir(&path)?;
828            assert!(path.is_dir());
829            let metadata = path.metadata()?;
830            assert_eq!(metadata.permissions().mode(), 0o42775);
831            // check we don't have temporary directory left
832            assert_eq!(tempdir.path().read_dir()?.count(), 1);
833            Ok(())
834        }
835
836        #[test]
837        fn test_fixup_perms() -> Result<()> {
838            let tempdir = TempDir::new()?;
839            let mut path = tempdir.path().to_path_buf();
840            path.push("shared");
841
842            // Create it without the SGID bit.
843            create_dir_with_mode(&path, 0o775)?;
844            let metadata = path.metadata()?;
845            assert_eq!(metadata.permissions().mode(), 0o40775);
846
847            // Fix it up.
848            create_shared_dir(&path)?;
849            let metadata = path.metadata()?;
850            assert_eq!(metadata.permissions().mode(), 0o42775);
851
852            Ok(())
853        }
854
855        #[test]
856        fn test_create_dir_no_perms() -> Result<()> {
857            let tempdir = TempDir::new()?;
858            let mut path = tempdir.path().to_path_buf();
859            path.push("nope");
860
861            std::fs::create_dir(&path)?;
862            std::fs::set_permissions(&path, fs::Permissions::from_mode(0o0))?;
863
864            let err = create_dir_with_mode(&path.join("dir"), 0o775).unwrap_err();
865            assert!(is_io_error_kind(&err, io::ErrorKind::PermissionDenied));
866
867            // Make sure we give parent dir's info in error.
868            assert!(format!("{:?}", err).contains(&format!("stat({:?}) = ", path)));
869
870            Ok(())
871        }
872    }
873
874    fn test_create_dir_all_fn(
875        create_fn: &dyn Fn(&PathBuf) -> anyhow::Result<()>,
876        mode: u32,
877    ) -> Result<()> {
878        let tempdir = TempDir::new()?;
879        let path = tempdir.path().join("foo").join("bar");
880        create_fn(&path)?;
881        assert!(path.is_dir());
882
883        #[cfg(unix)]
884        {
885            let metadata = path.metadata()?;
886            assert_eq!(metadata.permissions().mode(), mode);
887        }
888
889        #[cfg(windows)]
890        let _ = mode;
891
892        // Sanity that there is no error if directory already exists.
893        create_fn(&path)?;
894
895        #[cfg(unix)]
896        {
897            let broken_symlink = tempdir.path().join("foo").join("bar").join("oops");
898            symlink_file(&tempdir.path().join("doesnt_exist"), &broken_symlink)?;
899
900            // Resolve symlinks first, allowing us to create dirs across broken symlinks.
901            assert!(create_fn(&broken_symlink.join("nope")).is_ok());
902            assert!(tempdir.path().join("doesnt_exist").join("nope").is_dir());
903        }
904
905        // Sanity that we get errors if there is a regular file in the way.
906        let regular_file = tempdir.path().join("regular_file");
907        File::create(&regular_file)?;
908        assert!(create_fn(&regular_file).is_err());
909        assert!(create_fn(&regular_file.join("no_can_do")).is_err());
910
911        Ok(())
912    }
913
914    #[test]
915    #[cfg_attr(windows, ignore)] // FIXME: see D45267362, this needs a different test on Windows on 1.69+
916    fn test_atomic_write_symlink() -> Result<()> {
917        let dir = tempfile::tempdir().unwrap();
918        let path = dir.path();
919        let j = |name| -> PathBuf { path.join(name) };
920        // a: unrelated file
921        fs::write(j("a"), b"1")?;
922        // c: symlink -> c.xxxx: "2"
923        atomic_write_symlink(&j("c"), b"2")?;
924        // Keep an mmap of the current "c".
925        let file = fs::OpenOptions::new().read(true).open(j("c"))?;
926        let mmap = unsafe { memmap2::Mmap::map(&file) }?;
927        // This file should be automatically deleted.
928        fs::write(j("c.aaaa.atomic"), b"0")?;
929        // Rewrite c with different data.
930        atomic_write_symlink(&j("c"), b"3")?;
931        // mmap has the old content.
932        assert_eq!(mmap.as_ref(), b"2");
933        // Reading c gets new data.
934        assert_eq!(fs::read(j("c"))?, b"3");
935        // a: should exist
936        assert!(j("a").exists());
937        // 4 files: a, c, c.xxxx, c.lock
938        let count = || {
939            fs::read_dir(path)
940                .unwrap()
941                .filter(|e| e.as_ref().unwrap().path().exists())
942                .count()
943        };
944        assert_eq!(count(), 4);
945
946        // It's possible to replace a non-symlink to a symlink.
947        // (but this might fail if "a" is mmap-ed on Windows).
948        atomic_write_symlink(&j("a"), b"4")?;
949        // Exercise the GC logic a bit.
950        atomic_write_symlink(&j("a"), b"5")?;
951        atomic_write_symlink(&j("a"), b"6")?;
952        // 6 files: a, a.xxxx, a.lock, c, c.xxxx, c.lock
953        assert_eq!(count(), 6);
954        Ok(())
955    }
956
957    #[test]
958    fn test_mmap_delete() {
959        let dir = tempfile::tempdir().unwrap();
960        let path = dir.path().join("foo");
961        fs::write(&path, b"bar").unwrap();
962        let file = fs::OpenOptions::new().read(true).open(&path).unwrap();
963        let _mmap = unsafe { memmap2::Mmap::map(&file) }.unwrap();
964        remove_file(&path).unwrap();
965        assert!(!path.exists());
966        // The file can be recreated in-place.
967        fs::write(&path, b"baz").unwrap();
968    }
969
970    #[test]
971    fn test_create_dir_non_exist() -> Result<()> {
972        let tempdir = TempDir::new()?;
973        let mut path = tempdir.path().to_path_buf();
974        path.push("dir");
975        create_dir(&path)?;
976        assert!(path.is_dir());
977        Ok(())
978    }
979
980    #[test]
981    fn test_create_dir_exist() -> Result<()> {
982        let tempdir = TempDir::new()?;
983        let mut path = tempdir.path().to_path_buf();
984        path.push("dir");
985        create_dir(&path)?;
986        assert!(&path.is_dir());
987        create_dir(&path)?;
988        assert!(&path.is_dir());
989        Ok(())
990    }
991
992    #[test]
993    fn test_create_dir_file_exist() -> Result<()> {
994        let tempdir = TempDir::new()?;
995        let mut path = tempdir.path().to_path_buf();
996        path.push("dir");
997        File::create(&path)?;
998
999        let err = create_dir(&path).unwrap_err();
1000        assert!(is_io_error_kind(&err, io::ErrorKind::AlreadyExists));
1001
1002        Ok(())
1003    }
1004
1005    #[test]
1006    fn test_create_dir_with_nonexistent_parent() -> Result<()> {
1007        let tempdir = TempDir::new()?;
1008        let mut path = tempdir.path().to_path_buf();
1009        path.push("nonexistentparent");
1010        path.push("dir");
1011        let err = create_dir(&path).unwrap_err();
1012        assert!(is_io_error_kind(&err, ErrorKind::NotFound));
1013        Ok(())
1014    }
1015
1016    #[test]
1017    fn test_create_dir_without_empty_path() {
1018        let empty = Path::new("");
1019        let err = create_dir(empty).unwrap_err();
1020        assert!(is_io_error_kind(&err, ErrorKind::NotFound));
1021    }
1022
1023    #[test]
1024    fn test_path_expansion() {
1025        fn getenv(key: &str) -> Option<String> {
1026            match key {
1027                "foo" => Some("~/a".into()),
1028                "bar" => Some("b".into()),
1029                _ => None,
1030            }
1031        }
1032
1033        fn homedir() -> Option<PathBuf> {
1034            Some(PathBuf::from("/home/user"))
1035        }
1036
1037        let path = "$foo/${bar}/$baz";
1038        let expected = PathBuf::from("/home/user/a/b/$baz");
1039
1040        assert_eq!(expand_path_impl(path, getenv, homedir), expected);
1041    }
1042
1043    #[test]
1044    fn test_create_shared_dir_all() -> Result<()> {
1045        test_create_dir_all_fn(&|path| create_shared_dir_all(path), 0o42775)
1046    }
1047
1048    #[test]
1049    fn test_create_dir_all_with_mode() -> Result<()> {
1050        test_create_dir_all_fn(&|path| create_dir_all_with_mode(path, 0o777), 0o40777)
1051    }
1052
1053    #[test]
1054    fn test_relativize_absolute_paths() {
1055        let check = |base, path, expected| {
1056            assert_eq!(
1057                relativize(Path::new(base), Path::new(path)),
1058                Path::new(expected)
1059            );
1060        };
1061        check("/", "/", "");
1062        check("/foo/bar/baz", "/foo/bar/baz", "");
1063        check("/foo/bar", "/foo/bar/baz", "baz");
1064        check("/foo", "/foo/bar/baz", "bar/baz");
1065        check("/foo/bar/baz", "/foo/bar", "..");
1066        check("/foo/bar/baz", "/foo", "../..");
1067        check("/foo/bar/baz", "/foo/BAR", "../../BAR");
1068        check("/foo/bar/baz", "/foo/BAR/BAZ", "../../BAR/BAZ");
1069    }
1070
1071    #[test]
1072    fn test_relativize_platform_absolute_paths() {
1073        // This test with Windows-style absolute paths on Windows, and Unix-style path on Unix
1074        let cwd = Path::new(".").canonicalize().unwrap();
1075        let result = relativize(&cwd, &cwd.join("a").join("b"));
1076        assert_eq!(result, Path::new("a").join("b"));
1077    }
1078
1079    #[test]
1080    fn test_root_relative_path() -> Result<()> {
1081        let tempdir = TempDir::new()?;
1082
1083        let parent = tempdir.path().join("parent");
1084        let root = parent.join("root");
1085        let child = root.join("child");
1086        create_dir_all(&child)?;
1087
1088        assert_eq!(
1089            root_relative_path(&root, &root, ".".as_ref())?,
1090            Some(PathBuf::from(""))
1091        );
1092        assert_eq!(
1093            root_relative_path(&root, &child, ".".as_ref())?,
1094            Some(PathBuf::from("child"))
1095        );
1096        assert_eq!(
1097            root_relative_path(&root, &child, "foo".as_ref())?,
1098            Some(["child", "foo"].iter().collect::<PathBuf>()),
1099        );
1100
1101        let symlink_to_root = parent.join("symlink_to_root");
1102        symlink_dir(&root, &symlink_to_root)?;
1103        assert_eq!(
1104            root_relative_path(&root, &root, &symlink_to_root.join("child"))?,
1105            Some(PathBuf::from("child")),
1106        );
1107
1108        let symlink_to_child = parent.join("symlink_to_child");
1109        symlink_dir(&child, &symlink_to_child)?;
1110        assert_eq!(
1111            root_relative_path(&root, &root, &symlink_to_child.join("foo"))?,
1112            Some(["child", "foo"].iter().collect::<PathBuf>()),
1113        );
1114
1115        assert!(root_relative_path(&root, &parent, "foo".as_ref())?.is_none());
1116
1117        // Sanity that we don't treat "root" as prefix of "rootbeer".
1118        assert!(root_relative_path(&root, &root, &parent.join("rootbeer"))?.is_none());
1119
1120        Ok(())
1121    }
1122}