diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index 67846e7883570..25183984a3faf 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -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)] diff --git a/library/std/src/path.rs b/library/std/src/path.rs index 9ade2847e8ea9..d2e11467ea384 100644 --- a/library/std/src/path.rs +++ b/library/std/src/path.rs @@ -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}; //////////////////////////////////////////////////////////////////////////////// @@ -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>(path: P) -> io::Result { + 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) +} diff --git a/library/std/src/path/tests.rs b/library/std/src/path/tests.rs index 2bf499e1ab823..cf35254a2e365 100644 --- a/library/std/src/path/tests.rs +++ b/library/std/src/path/tests.rs @@ -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"; diff --git a/library/std/src/sys/sgx/path.rs b/library/std/src/sys/sgx/path.rs index 840a7ae042625..9cfc61bf174fa 100644 --- a/library/std/src/sys/sgx/path.rs +++ b/library/std/src/sys/sgx/path.rs @@ -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 { @@ -17,3 +18,7 @@ pub fn parse_prefix(_: &OsStr) -> Option> { pub const MAIN_SEP_STR: &str = "/"; pub const MAIN_SEP: char = '/'; + +pub(crate) fn absolute(_path: &Path) -> io::Result { + unsupported() +} diff --git a/library/std/src/sys/solid/path.rs b/library/std/src/sys/solid/path.rs index 4a14332d4999c..ed532dc989b1f 100644 --- a/library/std/src/sys/solid/path.rs +++ b/library/std/src/sys/solid/path.rs @@ -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 { @@ -17,3 +18,7 @@ pub fn parse_prefix(_: &OsStr) -> Option> { pub const MAIN_SEP_STR: &str = "\\"; pub const MAIN_SEP: char = '\\'; + +pub(crate) fn absolute(_path: &Path) -> io::Result { + unsupported() +} diff --git a/library/std/src/sys/unix/path.rs b/library/std/src/sys/unix/path.rs index 717add9ec48db..1fdabab4598a7 100644 --- a/library/std/src/sys/unix/path.rs +++ b/library/std/src/sys/unix/path.rs @@ -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 { @@ -18,3 +21,43 @@ pub fn parse_prefix(_: &OsStr) -> Option> { 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 { + // 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 characters, the + // first component following the leading characters may be + // interpreted in an implementation-defined manner, although more than + // two leading characters shall be treated as a single + // 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- character and contains one or more trailing + // characters". + // A trailing is also meaningful if "a symbolic link is + // encountered during pathname resolution". + if path_os.ends_with(b"/") { + normalized.push(""); + } + + Ok(normalized) +} diff --git a/library/std/src/sys/windows/path.rs b/library/std/src/sys/windows/path.rs index 79e0eaf6c34c6..e54fcaed4957d 100644 --- a/library/std/src/sys/windows/path.rs +++ b/library/std/src/sys/windows/path.rs @@ -260,3 +260,19 @@ pub(crate) fn maybe_verbatim(path: &Path) -> io::Result> { )?; Ok(path) } + +/// Make a Windows path absolute. +pub(crate) fn absolute(path: &Path) -> io::Result { + 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, + ) +}