path.rs
314 lines
| 9.6 KiB
| application/rls-services+xml
|
RustLexer
Gregory Szorc
|
r44524 | /* | ||
* 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 | ||||
Gregory Szorc
|
r44525 | /// different as seen by the kernel, if `foo/bar` is a symlink. This function | ||
/// always returns `foo` in this case. | ||||
Gregory Szorc
|
r44524 | /// - 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 { | ||||
Gregory Szorc
|
r44525 | Component::Normal(_) | ||
| Component::RootDir | ||||
| Component::Prefix(_) => { | ||||
Gregory Szorc
|
r44524 | 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`. | ||||
/// | ||||
Gregory Szorc
|
r44525 | /// 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. | ||||
Gregory Szorc
|
r44524 | /// 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)?; | ||||
Gregory Szorc
|
r44525 | // Ignore errors when removing the file, it will be cleaned up at a later | ||
// time. | ||||
Gregory Szorc
|
r44524 | let _ = fs_remove_file(dest_path); | ||
Ok(()) | ||||
} | ||||
Gregory Szorc
|
r44525 | /// Create the directory and ignore failures when a directory of the same name | ||
/// already exists. | ||||
Gregory Szorc
|
r44524 | 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) | ||||
} | ||||
} | ||||
} | ||||
} | ||||
Gregory Szorc
|
r44525 | /// Expand the user's home directory and any environment variables references | ||
/// in the given path. | ||||
Gregory Szorc
|
r44524 | /// | ||
Gregory Szorc
|
r44525 | /// 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: | ||||
Gregory Szorc
|
r44524 | /// | ||
Gregory Szorc
|
r44525 | /// - 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. | ||||
Gregory Szorc
|
r44524 | /// | ||
/// - 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) | ||||
} | ||||
Gregory Szorc
|
r44525 | /// Same as `expand_path` but explicitly takes closures for environment | ||
/// variable and home directory lookup for the sake of testability. | ||||
Gregory Szorc
|
r44524 | 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() { | ||||
Gregory Szorc
|
r44525 | assert_eq!( | ||
absolute("/a/./b\\c/../d/.").unwrap(), | ||||
Path::new("/a/d") | ||||
); | ||||
Gregory Szorc
|
r44524 | 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); | ||||
} | ||||
} | ||||