diff --git a/rust/hg-core/src/utils/path.rs b/rust/hg-core/src/utils/path.rs deleted file mode 100644 --- a/rust/hg-core/src/utils/path.rs +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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) -> io::Result { - 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>(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>(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) -> 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) -> 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(path: &str, getenv: E, homedir: H) -> PathBuf -where - E: FnMut(&str) -> Option, - H: FnOnce() -> Option, -{ - // 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 { - match key { - "foo" => Some("~/a".into()), - "bar" => Some("b".into()), - _ => None, - } - } - - fn homedir() -> Option { - 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); - } -}