Skip to content

Commit

Permalink
std::path::absolute
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisDenton committed Dec 8, 2021
1 parent f9e77f2 commit 0660732
Show file tree
Hide file tree
Showing 7 changed files with 210 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 @@ -225,6 +225,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
80 changes: 79 additions & 1 deletion library/std/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,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 @@ -3141,3 +3141,81 @@ 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 paths 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 = "none")]
pub fn absolute<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
if path.as_os_str().is_empty() {
return Err(io::Error::new_const(
io::ErrorKind::InvalidInput,
&"cannot make an empty path absolute",
));
}
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 @@ -1665,6 +1665,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
7 changes: 6 additions & 1 deletion library/std/src/sys/sgx/path.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::ffi::OsStr;
use crate::path::Prefix;
use crate::path::{Path, PathBuf, Prefix};
use crate::sys::unsupported;

#[inline]
pub fn is_sep_byte(b: u8) -> bool {
Expand All @@ -17,3 +18,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()
}
7 changes: 6 additions & 1 deletion library/std/src/sys/solid/path.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::ffi::OsStr;
use crate::path::Prefix;
use crate::path::{Path, PathBuf, Prefix};
use crate::sys::unsupported;

#[inline]
pub fn is_sep_byte(b: u8) -> bool {
Expand All @@ -17,3 +18,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()
}
45 changes: 44 additions & 1 deletion library/std/src/sys/unix/path.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::env;
use crate::ffi::OsStr;
use crate::path::Prefix;
use crate::io;
use crate::os::unix::ffi::OsStrExt;
use crate::path::{Path, PathBuf, Prefix};

#[inline]
pub fn is_sep_byte(b: u8) -> bool {
Expand All @@ -18,3 +21,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().as_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 0660732

Please sign in to comment.