Skip to content

Commit

Permalink
Auto merge of rust-lang#91673 - ChrisDenton:path-absolute, r=Mark-Sim…
Browse files Browse the repository at this point in the history
…ulacrum

`std::path::absolute`

Implements rust-lang#59117 by adding a `std::path::absolute` function that creates an absolute path without reading the filesystem. This is intended to be a drop-in replacement for [`std::fs::canonicalize`](https://doc.rust-lang.org/std/fs/fn.canonicalize.html) in cases where it isn't necessary to resolve symlinks. It can be used on paths that don't exist or where resolving symlinks is unwanted. It can also be used to avoid circumstances where `canonicalize` might otherwise fail.

On Windows this is a wrapper around [`GetFullPathNameW`](https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew). On Unix it partially implements the POSIX [pathname resolution](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13) specification, stopping just short of actually resolving symlinks.
  • Loading branch information
bors committed Feb 13, 2022
2 parents c26fbf8 + 81cc3af commit 1f4681a
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 4 deletions.
1 change: 1 addition & 0 deletions library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
// std is implemented with unstable features, many of which are internal
// compiler details that will never be stable
// NB: the following list is sorted to minimize merge conflicts.
#![feature(absolute_path)]
#![feature(alloc_error_handler)]
#![feature(alloc_layout_extra)]
#![feature(allocator_api)]
Expand Down
78 changes: 77 additions & 1 deletion library/std/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ use crate::str::FromStr;
use crate::sync::Arc;

use crate::ffi::{OsStr, OsString};

use crate::sys;
use crate::sys::path::{is_sep_byte, is_verbatim_sep, parse_prefix, MAIN_SEP_STR};

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -3172,3 +3172,79 @@ impl Error for StripPrefixError {
"prefix not found"
}
}

/// Makes the path absolute without accessing the filesystem.
///
/// If the path is relative, the current directory is used as the base directory.
/// All intermediate components will be resolved according to platforms-specific
/// rules but unlike [`canonicalize`][crate::fs::canonicalize] this does not
/// resolve symlinks and may succeed even if the path does not exist.
///
/// If the `path` is empty or getting the
/// [current directory][crate::env::current_dir] fails then an error will be
/// returned.
///
/// # Examples
///
/// ## Posix paths
///
/// ```
/// #![feature(absolute_path)]
/// # #[cfg(unix)]
/// fn main() -> std::io::Result<()> {
/// use std::path::{self, Path};
///
/// // Relative to absolute
/// let absolute = path::absolute("foo/./bar")?;
/// assert!(absolute.ends_with("foo/bar"));
///
/// // Absolute to absolute
/// let absolute = path::absolute("/foo//test/.././bar.rs")?;
/// assert_eq!(absolute, Path::new("/foo/test/../bar.rs"));
/// Ok(())
/// }
/// # #[cfg(not(unix))]
/// # fn main() {}
/// ```
///
/// The path is resolved using [POSIX semantics][posix-semantics] except that
/// it stops short of resolving symlinks. This means it will keep `..`
/// components and trailing slashes.
///
/// ## Windows paths
///
/// ```
/// #![feature(absolute_path)]
/// # #[cfg(windows)]
/// fn main() -> std::io::Result<()> {
/// use std::path::{self, Path};
///
/// // Relative to absolute
/// let absolute = path::absolute("foo/./bar")?;
/// assert!(absolute.ends_with(r"foo\bar"));
///
/// // Absolute to absolute
/// let absolute = path::absolute(r"C:\foo//test\..\./bar.rs")?;
///
/// assert_eq!(absolute, Path::new(r"C:\foo\bar.rs"));
/// Ok(())
/// }
/// # #[cfg(not(windows))]
/// # fn main() {}
/// ```
///
/// For verbatim paths this will simply return the path as given. For other
/// paths this is currently equivalent to calling [`GetFullPathNameW`][windows-path]
/// This may change in the future.
///
/// [posix-semantics]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
/// [windows-path]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew
#[unstable(feature = "absolute_path", issue = "92750")]
pub fn absolute<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
if path.as_os_str().is_empty() {
Err(io::const_io_error!(io::ErrorKind::InvalidInput, "cannot make an empty path absolute",))
} else {
sys::path::absolute(path)
}
}
58 changes: 58 additions & 0 deletions library/std/src/path/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1700,6 +1700,64 @@ fn test_ord() {
ord!(Equal, "foo/bar", "foo/bar//");
}

#[test]
#[cfg(unix)]
fn test_unix_absolute() {
use crate::path::absolute;

assert!(absolute("").is_err());

let relative = "a/b";
let mut expected = crate::env::current_dir().unwrap();
expected.push(relative);
assert_eq!(absolute(relative).unwrap(), expected);

// Test how components are collected.
assert_eq!(absolute("/a/b/c").unwrap(), Path::new("/a/b/c"));
assert_eq!(absolute("/a//b/c").unwrap(), Path::new("/a/b/c"));
assert_eq!(absolute("//a/b/c").unwrap(), Path::new("//a/b/c"));
assert_eq!(absolute("///a/b/c").unwrap(), Path::new("/a/b/c"));
assert_eq!(absolute("/a/b/c/").unwrap(), Path::new("/a/b/c/"));
assert_eq!(absolute("/a/./b/../c/.././..").unwrap(), Path::new("/a/b/../c/../.."));
}

#[test]
#[cfg(windows)]
fn test_windows_absolute() {
use crate::path::absolute;
// An empty path is an error.
assert!(absolute("").is_err());

let relative = r"a\b";
let mut expected = crate::env::current_dir().unwrap();
expected.push(relative);
assert_eq!(absolute(relative).unwrap(), expected);

macro_rules! unchanged(
($path:expr) => {
assert_eq!(absolute($path).unwrap(), Path::new($path));
}
);

unchanged!(r"C:\path\to\file");
unchanged!(r"C:\path\to\file\");
unchanged!(r"\\server\share\to\file");
unchanged!(r"\\server.\share.\to\file");
unchanged!(r"\\.\PIPE\name");
unchanged!(r"\\.\C:\path\to\COM1");
unchanged!(r"\\?\C:\path\to\file");
unchanged!(r"\\?\UNC\server\share\to\file");
unchanged!(r"\\?\PIPE\name");
// Verbatim paths are always unchanged, no matter what.
unchanged!(r"\\?\path.\to/file..");

assert_eq!(absolute(r"C:\path..\to.\file.").unwrap(), Path::new(r"C:\path..\to\file"));
assert_eq!(absolute(r"C:\path\to\COM1").unwrap(), Path::new(r"\\.\COM1"));
assert_eq!(absolute(r"C:\path\to\COM1.txt").unwrap(), Path::new(r"\\.\COM1"));
assert_eq!(absolute(r"C:\path\to\COM1 .txt").unwrap(), Path::new(r"\\.\COM1"));
assert_eq!(absolute(r"C:\path\to\cOnOuT$").unwrap(), Path::new(r"\\.\cOnOuT$"));
}

#[bench]
fn bench_path_cmp_fast_path_buf_sort(b: &mut test::Bencher) {
let prefix = "my/home";
Expand Down
8 changes: 7 additions & 1 deletion library/std/src/sys/sgx/path.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::ffi::OsStr;
use crate::path::Prefix;
use crate::io;
use crate::path::{Path, PathBuf, Prefix};
use crate::sys::unsupported;

#[inline]
pub fn is_sep_byte(b: u8) -> bool {
Expand All @@ -17,3 +19,7 @@ pub fn parse_prefix(_: &OsStr) -> Option<Prefix<'_>> {

pub const MAIN_SEP_STR: &str = "/";
pub const MAIN_SEP: char = '/';

pub(crate) fn absolute(_path: &Path) -> io::Result<PathBuf> {
unsupported()
}
8 changes: 7 additions & 1 deletion library/std/src/sys/solid/path.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::ffi::OsStr;
use crate::path::Prefix;
use crate::io;
use crate::path::{Path, PathBuf, Prefix};
use crate::sys::unsupported;

#[inline]
pub fn is_sep_byte(b: u8) -> bool {
Expand All @@ -17,3 +19,7 @@ pub fn parse_prefix(_: &OsStr) -> Option<Prefix<'_>> {

pub const MAIN_SEP_STR: &str = "\\";
pub const MAIN_SEP: char = '\\';

pub(crate) fn absolute(_path: &Path) -> io::Result<PathBuf> {
unsupported()
}
44 changes: 43 additions & 1 deletion library/std/src/sys/unix/path.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::env;
use crate::ffi::OsStr;
use crate::path::Prefix;
use crate::io;
use crate::path::{Path, PathBuf, Prefix};

#[inline]
pub fn is_sep_byte(b: u8) -> bool {
Expand All @@ -18,3 +20,43 @@ pub fn parse_prefix(_: &OsStr) -> Option<Prefix<'_>> {

pub const MAIN_SEP_STR: &str = "/";
pub const MAIN_SEP: char = '/';

/// Make a POSIX path absolute without changing its semantics.
pub(crate) fn absolute(path: &Path) -> io::Result<PathBuf> {
// This is mostly a wrapper around collecting `Path::components`, with
// exceptions made where this conflicts with the POSIX specification.
// See 4.13 Pathname Resolution, IEEE Std 1003.1-2017
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13

let mut components = path.components();
let path_os = path.as_os_str().bytes();

let mut normalized = if path.is_absolute() {
// "If a pathname begins with two successive <slash> characters, the
// first component following the leading <slash> characters may be
// interpreted in an implementation-defined manner, although more than
// two leading <slash> characters shall be treated as a single <slash>
// character."
if path_os.starts_with(b"//") && !path_os.starts_with(b"///") {
components.next();
PathBuf::from("//")
} else {
PathBuf::new()
}
} else {
env::current_dir()?
};
normalized.extend(components);

// "Interfaces using pathname resolution may specify additional constraints
// when a pathname that does not name an existing directory contains at
// least one non- <slash> character and contains one or more trailing
// <slash> characters".
// A trailing <slash> is also meaningful if "a symbolic link is
// encountered during pathname resolution".
if path_os.ends_with(b"/") {
normalized.push("");
}

Ok(normalized)
}
16 changes: 16 additions & 0 deletions library/std/src/sys/windows/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,19 @@ pub(crate) fn maybe_verbatim(path: &Path) -> io::Result<Vec<u16>> {
)?;
Ok(path)
}

/// Make a Windows path absolute.
pub(crate) fn absolute(path: &Path) -> io::Result<PathBuf> {
if path.as_os_str().bytes().starts_with(br"\\?\") {
return Ok(path.into());
}
let path = to_u16s(path)?;
let lpfilename = path.as_ptr();
fill_utf16_buf(
// SAFETY: `fill_utf16_buf` ensures the `buffer` and `size` are valid.
// `lpfilename` is a pointer to a null terminated string that is not
// invalidated until after `GetFullPathNameW` returns successfully.
|buffer, size| unsafe { c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()) },
super::os2path,
)
}

0 comments on commit 1f4681a

Please sign in to comment.