Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std::path::absolute #91673

Merged
merged 2 commits into from
Feb 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3164,3 +3164,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 @@ -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/../.."));
Copy link

@axetroy axetroy Mar 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unix test without dot prefix.

assert_eq!(absolute("./a").unwrap(), Path::new("/pwd/a")); // return /pwd/./a
assert_eq!(absolute("../a").unwrap(), Path::new("/parent_pwd/a")); // return /pwd/../a

I have tried in darwin. it is wrong

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for reporting this. I'll add more tests and fix the dot prefix case.

}

#[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()) },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work for non \\?\ paths bigger than MAX_PATH? It would be nice to be able to do std::path::absolute() and then prefix \\?\ if necessary to handle long paths.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it does! The std already does exactly that. Unfortunately the current docs for GetFullPathNameW suggests otherwise but this was an oversight that's going to be corrected when the online docs are next rebuilt (see the sdk-api source).

super::os2path,
)
}