D7573: hg-core: vendor Facebook's path utils module

indygreg (Gregory Szorc) phabricator at mercurial-scm.org
Sat Dec 7 20:18:32 UTC 2019


indygreg created this revision.
Herald added subscribers: mercurial-devel, kevincox, durin42.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  The added file was imported from
  https://github.com/facebookexperimental/eden/blob/d1d8fb939a39aa331ae7f162c39cbcaa511d474b/eden/scm/lib/util/src/path.rs
  without modifications. The file is not yet integrated into our project. This
  will be done in subsequent commits.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D7573

AFFECTED FILES
  rust/hg-core/src/utils/path.rs

CHANGE DETAILS

diff --git a/rust/hg-core/src/utils/path.rs b/rust/hg-core/src/utils/path.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/utils/path.rs
@@ -0,0 +1,305 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License version 2.
+ */
+
+//! Path-related utilities.
+
+use std::env;
+#[cfg(not(unix))]
+use std::fs::rename;
+use std::fs::{self, remove_file as fs_remove_file};
+use std::io::{self, ErrorKind};
+use std::path::{Component, Path, PathBuf};
+
+use anyhow::Result;
+#[cfg(not(unix))]
+use tempfile::Builder;
+
+/// Normalize a canonicalized Path for display.
+///
+/// This removes the UNC prefix `\\?\` on Windows.
+pub fn normalize_for_display(path: &str) -> &str {
+    if cfg!(windows) && path.starts_with(r"\\?\") {
+        &path[4..]
+    } else {
+        path
+    }
+}
+
+/// Similar to [`normalize_for_display`]. But work on bytes.
+pub fn normalize_for_display_bytes(path: &[u8]) -> &[u8] {
+    if cfg!(windows) && path.starts_with(br"\\?\") {
+        &path[4..]
+    } else {
+        path
+    }
+}
+
+/// Return the absolute and normalized path without accessing the filesystem.
+///
+/// Unlike [`fs::canonicalize`], do not follow symlinks.
+///
+/// This function does not access the filesystem. Therefore it can behave
+/// differently from the kernel or other library functions in corner cases.
+/// For example:
+///
+/// - On some systems with symlink support, `foo/bar/..` and `foo` can be
+///   different as seen by the kernel, if `foo/bar` is a symlink. This
+///   function always returns `foo` in this case.
+/// - On Windows, the official normalization rules are much more complicated.
+///   See https://github.com/rust-lang/rust/pull/47363#issuecomment-357069527.
+///   For example, this function cannot translate "drive relative" path like
+///   "X:foo" to an absolute path.
+///
+/// Return an error if `std::env::current_dir()` fails or if this function
+/// fails to produce an absolute path.
+pub fn absolute(path: impl AsRef<Path>) -> io::Result<PathBuf> {
+    let path = path.as_ref();
+    let path = if path.is_absolute() {
+        path.to_path_buf()
+    } else {
+        std::env::current_dir()?.join(path)
+    };
+
+    if !path.is_absolute() {
+        return Err(io::Error::new(
+            io::ErrorKind::Other,
+            format!("cannot get absoltue path from {:?}", path),
+        ));
+    }
+
+    let mut result = PathBuf::new();
+    for component in path.components() {
+        match component {
+            Component::Normal(_) | Component::RootDir | Component::Prefix(_) => {
+                result.push(component);
+            }
+            Component::ParentDir => {
+                result.pop();
+            }
+            Component::CurDir => (),
+        }
+    }
+    Ok(result)
+}
+
+/// Remove the file pointed by `path`.
+#[cfg(unix)]
+pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
+    fs_remove_file(path)?;
+    Ok(())
+}
+
+/// Remove the file pointed by `path`.
+///
+/// On Windows, removing a file can fail for various reasons, including if the file is memory
+/// mapped. This can happen when the repository is accessed concurrently while a background task is
+/// trying to remove a packfile. To solve this, we can rename the file before trying to remove it.
+/// If the remove operation fails, a future repack will clean it up.
+#[cfg(not(unix))]
+pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
+    let path = path.as_ref();
+    let extension = path
+        .extension()
+        .and_then(|ext| ext.to_str())
+        .map_or(".to-delete".to_owned(), |ext| ".".to_owned() + ext + "-tmp");
+
+    let dest_path = Builder::new()
+        .prefix("")
+        .suffix(&extension)
+        .rand_bytes(8)
+        .tempfile_in(path.parent().unwrap())?
+        .into_temp_path();
+
+    rename(path, &dest_path)?;
+
+    // Ignore errors when removing the file, it will be cleaned up at a later time.
+    let _ = fs_remove_file(dest_path);
+    Ok(())
+}
+
+/// Create the directory and ignore failures when a directory of the same name already exists.
+pub fn create_dir(path: impl AsRef<Path>) -> io::Result<()> {
+    match fs::create_dir(path.as_ref()) {
+        Ok(()) => Ok(()),
+        Err(e) => {
+            if e.kind() == ErrorKind::AlreadyExists && path.as_ref().is_dir() {
+                Ok(())
+            } else {
+                Err(e)
+            }
+        }
+    }
+}
+
+/// Expand the user's home directory and any environment variables references in
+/// the given path.
+///
+/// This function is designed to emulate the behavior of Mercurial's `util.expandpath`
+/// function, which in turn uses Python's `os.path.expand{user,vars}` functions. This
+/// results in behavior that is notably different from the default expansion behavior
+/// of the `shellexpand` crate. In particular:
+///
+/// - If a reference to an environment variable is missing or invalid, the reference
+///   is left unchanged in the resulting path rather than emitting an error.
+///
+/// - Home directory expansion explicitly happens after environment variable
+///   expansion, meaning that if an environment variable is expanded into a
+///   string starting with a tilde (`~`), the tilde will be expanded into the
+///   user's home directory.
+///
+pub fn expand_path(path: impl AsRef<str>) -> PathBuf {
+    expand_path_impl(path.as_ref(), |k| env::var(k).ok(), dirs::home_dir)
+}
+
+/// Same as `expand_path` but explicitly takes closures for environment variable
+/// and home directory lookup for the sake of testability.
+fn expand_path_impl<E, H>(path: &str, getenv: E, homedir: H) -> PathBuf
+where
+    E: FnMut(&str) -> Option<String>,
+    H: FnOnce() -> Option<PathBuf>,
+{
+    // The shellexpand crate does not expand Windows environment variables
+    // like `%PROGRAMDATA%`. We'd like to expand them too. So let's do some
+    // pre-processing.
+    //
+    // XXX: Doing this preprocessing has the unfortunate side-effect that
+    // if an environment variable fails to expand on Windows, the resulting
+    // string will contain a UNIX-style environment variable reference.
+    //
+    // e.g., "/foo/%MISSING%/bar" will expand to "/foo/${MISSING}/bar"
+    //
+    // The current approach is good enough for now, but likely needs to
+    // be improved later for correctness.
+    let path = {
+        let mut new_path = String::new();
+        let mut is_starting = true;
+        for ch in path.chars() {
+            if ch == '%' {
+                if is_starting {
+                    new_path.push_str("${");
+                } else {
+                    new_path.push('}');
+                }
+                is_starting = !is_starting;
+            } else if cfg!(windows) && ch == '/' {
+                // Only on Windows, change "/" to "\" automatically.
+                // This makes sure "%include /foo" works as expected.
+                new_path.push('\\')
+            } else {
+                new_path.push(ch);
+            }
+        }
+        new_path
+    };
+
+    let path = shellexpand::env_with_context_no_errors(&path, getenv);
+    shellexpand::tilde_with_context(&path, homedir)
+        .as_ref()
+        .into()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use std::fs::File;
+
+    use tempfile::TempDir;
+
+    #[cfg(windows)]
+    mod windows {
+        use super::*;
+
+        #[test]
+        fn test_absolute_fullpath() {
+            assert_eq!(absolute("C:/foo").unwrap(), Path::new("C:\\foo"));
+            assert_eq!(
+                absolute("x:\\a/b\\./.\\c").unwrap(),
+                Path::new("x:\\a\\b\\c")
+            );
+            assert_eq!(
+                absolute("y:/a/b\\../..\\c\\../d\\./.").unwrap(),
+                Path::new("y:\\d")
+            );
+            assert_eq!(
+                absolute("z:/a/b\\../..\\../..\\..").unwrap(),
+                Path::new("z:\\")
+            );
+        }
+    }
+
+    #[cfg(unix)]
+    mod unix {
+        use super::*;
+
+        #[test]
+        fn test_absolute_fullpath() {
+            assert_eq!(absolute("/a/./b\\c/../d/.").unwrap(), Path::new("/a/d"));
+            assert_eq!(absolute("/a/../../../../b").unwrap(), Path::new("/b"));
+            assert_eq!(absolute("/../../..").unwrap(), Path::new("/"));
+            assert_eq!(absolute("/../../../").unwrap(), Path::new("/"));
+            assert_eq!(
+                absolute("//foo///bar//baz").unwrap(),
+                Path::new("/foo/bar/baz")
+            );
+            assert_eq!(absolute("//").unwrap(), Path::new("/"));
+        }
+    }
+
+    #[test]
+    fn test_create_dir_non_exist() -> Result<()> {
+        let tempdir = TempDir::new()?;
+        let mut path = tempdir.path().to_path_buf();
+        path.push("dir");
+        create_dir(&path)?;
+        assert!(path.is_dir());
+        Ok(())
+    }
+
+    #[test]
+    fn test_create_dir_exist() -> Result<()> {
+        let tempdir = TempDir::new()?;
+        let mut path = tempdir.path().to_path_buf();
+        path.push("dir");
+        create_dir(&path)?;
+        assert!(&path.is_dir());
+        create_dir(&path)?;
+        assert!(&path.is_dir());
+        Ok(())
+    }
+
+    #[test]
+    fn test_create_dir_file_exist() -> Result<()> {
+        let tempdir = TempDir::new()?;
+        let mut path = tempdir.path().to_path_buf();
+        path.push("dir");
+        File::create(&path)?;
+        let err = create_dir(&path).unwrap_err();
+        assert_eq!(err.kind(), ErrorKind::AlreadyExists);
+        Ok(())
+    }
+
+    #[test]
+    fn test_path_expansion() {
+        fn getenv(key: &str) -> Option<String> {
+            match key {
+                "foo" => Some("~/a".into()),
+                "bar" => Some("b".into()),
+                _ => None,
+            }
+        }
+
+        fn homedir() -> Option<PathBuf> {
+            Some(PathBuf::from("/home/user"))
+        }
+
+        let path = "$foo/${bar}/$baz";
+        let expected = PathBuf::from("/home/user/a/b/$baz");
+
+        assert_eq!(expand_path_impl(&path, getenv, homedir), expected);
+    }
+}



To: indygreg, #hg-reviewers
Cc: durin42, kevincox, mercurial-devel


More information about the Mercurial-devel mailing list