Skip to content

Add Path::has_trailing_sep and related methods #142506

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
187 changes: 184 additions & 3 deletions library/std/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,99 @@ impl PathBuf {
}
}

/// Sets whether the path has a trailing [separator](MAIN_SEPARATOR).
///
/// The value returned by [`has_trailing_sep`](Path::has_trailing_sep) will be equivalent to
/// the provided value if possible.
///
/// # Examples
///
/// ```
/// #![feature(path_trailing_sep)]
/// use std::path::PathBuf;
///
/// let mut p = PathBuf::from("dir");
///
/// assert!(!p.has_trailing_sep());
/// p.set_trailing_sep(false);
/// assert!(!p.has_trailing_sep());
/// p.set_trailing_sep(true);
/// assert!(p.has_trailing_sep());
/// p.set_trailing_sep(false);
/// assert!(!p.has_trailing_sep());
///
/// p = PathBuf::from("/");
/// assert!(p.has_trailing_sep());
/// p.set_trailing_sep(false);
/// assert!(p.has_trailing_sep());
/// ```
#[unstable(feature = "path_trailing_sep", issue = "142503")]
pub fn set_trailing_sep(&mut self, trailing_sep: bool) {
if trailing_sep { self.push_trailing_sep() } else { self.pop_trailing_sep() }
}

/// Adds a trailing [separator](MAIN_SEPARATOR) to the path.
///
/// This acts similarly to [`Path::with_trailing_sep`], but mutates the underlying `PathBuf`.
///
/// # Examples
///
/// ```
/// #![feature(path_trailing_sep)]
/// use std::ffi::OsStr;
/// use std::path::PathBuf;
///
/// let mut p = PathBuf::from("dir");
///
/// assert!(!p.has_trailing_sep());
/// p.push_trailing_sep();
/// assert!(p.has_trailing_sep());
/// p.push_trailing_sep();
/// assert!(p.has_trailing_sep());
///
/// p = PathBuf::from("dir/");
/// p.push_trailing_sep();
/// assert_eq!(p.as_os_str(), OsStr::new("dir/"));
/// ```
#[unstable(feature = "path_trailing_sep", issue = "142503")]
pub fn push_trailing_sep(&mut self) {
if !self.has_trailing_sep() {
self.push("");
}
}

/// Removes a trailing [separator](MAIN_SEPARATOR) from the path, if possible.
///
/// This acts similarly to [`Path::trim_trailing_sep`], but mutates the underlying `PathBuf`.
///
/// # Examples
///
/// ```
/// #![feature(path_trailing_sep)]
/// use std::ffi::OsStr;
/// use std::path::PathBuf;
///
/// let mut p = PathBuf::from("dir//");
///
/// assert!(p.has_trailing_sep());
/// assert_eq!(p.as_os_str(), OsStr::new("dir//"));
/// p.pop_trailing_sep();
/// assert!(!p.has_trailing_sep());
/// assert_eq!(p.as_os_str(), OsStr::new("dir"));
/// p.pop_trailing_sep();
/// assert!(!p.has_trailing_sep());
/// assert_eq!(p.as_os_str(), OsStr::new("dir"));
///
/// p = PathBuf::from("/");
/// assert!(p.has_trailing_sep());
/// p.pop_trailing_sep();
/// assert!(p.has_trailing_sep());
/// ```
#[unstable(feature = "path_trailing_sep", issue = "142503")]
pub fn pop_trailing_sep(&mut self) {
self.inner.truncate(self.trim_trailing_sep().as_os_str().len());
}

/// Updates [`self.file_name`] to `file_name`.
///
/// If [`self.file_name`] was [`None`], this is equivalent to pushing
Expand Down Expand Up @@ -1612,7 +1705,7 @@ impl PathBuf {
let new = extension.as_encoded_bytes();
if !new.is_empty() {
// truncate until right after the file name
// this is necessary for trimming the trailing slash
// this is necessary for trimming the trailing separator
let end_file_name = file_name[file_name.len()..].as_ptr().addr();
let start = self.inner.as_encoded_bytes().as_ptr().addr();
self.inner.truncate(end_file_name.wrapping_sub(start));
Expand Down Expand Up @@ -2722,6 +2815,94 @@ impl Path {
self.file_name().map(rsplit_file_at_dot).and_then(|(before, after)| before.and(after))
}

/// Checks whether the path ends in a trailing [separator](MAIN_SEPARATOR).
///
/// This is generally done to ensure that a path is treated as a directory, not a file,
/// although it does not actually guarantee that such a path is a directory on the underlying
/// file system.
///
/// Despite this behavior, two paths are still considered the same in Rust whether they have a
/// trailing separator or not.
///
/// # Examples
///
/// ```
/// #![feature(path_trailing_sep)]
/// use std::path::Path;
///
/// assert!(Path::new("dir/").has_trailing_sep());
/// assert!(!Path::new("file.rs").has_trailing_sep());
/// ```
#[unstable(feature = "path_trailing_sep", issue = "142503")]
#[must_use]
#[inline]
pub fn has_trailing_sep(&self) -> bool {
self.as_os_str().as_encoded_bytes().last().copied().is_some_and(is_sep_byte)
}

/// Ensures that a path has a trailing [separator](MAIN_SEPARATOR),
/// allocating a [`PathBuf`] if necessary.
///
/// The resulting path will return true for [`has_trailing_sep`](Self::has_trailing_sep).
///
/// # Examples
///
/// ```
/// #![feature(path_trailing_sep)]
/// use std::ffi::OsStr;
/// use std::path::Path;
///
/// assert_eq!(Path::new("dir//").with_trailing_sep().as_os_str(), OsStr::new("dir//"));
/// assert_eq!(Path::new("dir/").with_trailing_sep().as_os_str(), OsStr::new("dir/"));
/// assert!(!Path::new("dir").has_trailing_sep());
/// assert!(Path::new("dir").with_trailing_sep().has_trailing_sep());
/// ```
#[unstable(feature = "path_trailing_sep", issue = "142503")]
#[must_use]
#[inline]
pub fn with_trailing_sep(&self) -> Cow<'_, Path> {
if self.has_trailing_sep() { Cow::Borrowed(self) } else { Cow::Owned(self.join("")) }
}

/// Trims a trailing [separator](MAIN_SEPARATOR) from a path, if possible.
///
/// The resulting path will return false for [`has_trailing_sep`](Self::has_trailing_sep) for
/// most paths.
///
/// Some paths, like `/`, cannot be trimmed in this way.
///
/// # Examples
///
/// ```
/// #![feature(path_trailing_sep)]
/// use std::ffi::OsStr;
/// use std::path::Path;
///
/// assert_eq!(Path::new("dir//").trim_trailing_sep().as_os_str(), OsStr::new("dir"));
/// assert_eq!(Path::new("dir/").trim_trailing_sep().as_os_str(), OsStr::new("dir"));
/// assert_eq!(Path::new("dir").trim_trailing_sep().as_os_str(), OsStr::new("dir"));
/// assert_eq!(Path::new("/").trim_trailing_sep().as_os_str(), OsStr::new("/"));
/// assert_eq!(Path::new("//").trim_trailing_sep().as_os_str(), OsStr::new("//"));
/// ```
#[unstable(feature = "path_trailing_sep", issue = "142503")]
#[must_use]
#[inline]
pub fn trim_trailing_sep(&self) -> &Path {
if self.has_trailing_sep() && (!self.has_root() || self.parent().is_some()) {
let mut bytes = self.inner.as_encoded_bytes();
while let Some((last, init)) = bytes.split_last()
&& is_sep_byte(*last)
{
bytes = init;
}

// SAFETY: Trimming trailing ASCII bytes will retain the validity of the string.
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
} else {
self
}
}

/// Creates an owned [`PathBuf`] with `path` adjoined to `self`.
///
/// If `path` is absolute, it replaces the current path.
Expand Down Expand Up @@ -2876,7 +3057,7 @@ impl Path {
/// `a/b` all have `a` and `b` as components, but `./a/b` starts with
/// an additional [`CurDir`] component.
///
/// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
/// * Trailing separators are normalized away, so `/a/b` and `/a/b/` are equivalent.
///
/// Note that no other normalization takes place; in particular, `a/c`
/// and `a/b/../c` are distinct, to account for the possibility that `b`
Expand Down Expand Up @@ -3646,7 +3827,7 @@ impl Error for NormalizeError {}
///
/// On POSIX platforms, 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.
/// components and trailing separators.
///
/// On Windows, for verbatim paths, this will simply return the path as given. For other
/// paths, this is currently equivalent to calling
Expand Down
46 changes: 39 additions & 7 deletions library/std/tests/path.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#![feature(
clone_to_uninit,
path_add_extension,
path_file_prefix,
maybe_uninit_slice,
normalize_lexically
)]
// tidy-alphabetical-start
#![feature(clone_to_uninit)]
#![feature(maybe_uninit_slice)]
#![feature(normalize_lexically)]
#![feature(path_add_extension)]
#![feature(path_file_prefix)]
#![feature(path_trailing_sep)]
// tidy-alphabetical-end

use std::clone::CloneToUninit;
use std::ffi::OsStr;
Expand Down Expand Up @@ -2532,3 +2533,34 @@ fn normalize_lexically() {
check_err(r"\\?\UNC\server\share\a\..\..");
}
}

#[test]
pub fn test_trim_trailing_sep() {
assert_eq!(Path::new("/").trim_trailing_sep().as_os_str(), OsStr::new("/"));
assert_eq!(Path::new("//").trim_trailing_sep().as_os_str(), OsStr::new("//"));
assert_eq!(Path::new("").trim_trailing_sep().as_os_str(), OsStr::new(""));
assert_eq!(Path::new(".").trim_trailing_sep().as_os_str(), OsStr::new("."));
assert_eq!(Path::new("./").trim_trailing_sep().as_os_str(), OsStr::new("."));
assert_eq!(Path::new(".//").trim_trailing_sep().as_os_str(), OsStr::new("."));
assert_eq!(Path::new("..").trim_trailing_sep().as_os_str(), OsStr::new(".."));
assert_eq!(Path::new("../").trim_trailing_sep().as_os_str(), OsStr::new(".."));
assert_eq!(Path::new("..//").trim_trailing_sep().as_os_str(), OsStr::new(".."));

#[cfg(any(windows, target_os = "cygwin"))]
{
assert_eq!(Path::new("\\").trim_trailing_sep().as_os_str(), OsStr::new("\\"));
assert_eq!(Path::new("\\\\").trim_trailing_sep().as_os_str(), OsStr::new("\\\\"));
assert_eq!(Path::new("c:/").trim_trailing_sep().as_os_str(), OsStr::new("c:/"));
assert_eq!(Path::new("c://").trim_trailing_sep().as_os_str(), OsStr::new("c://"));
assert_eq!(Path::new("c:./").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
assert_eq!(Path::new("c:.//").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
assert_eq!(Path::new("c:../").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
assert_eq!(Path::new("c:..//").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
assert_eq!(Path::new("c:\\").trim_trailing_sep().as_os_str(), OsStr::new("c:\\"));
assert_eq!(Path::new("c:\\\\").trim_trailing_sep().as_os_str(), OsStr::new("c:\\\\"));
assert_eq!(Path::new("c:.\\").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
assert_eq!(Path::new("c:.\\\\").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
assert_eq!(Path::new("c:..\\").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
assert_eq!(Path::new("c:..\\\\").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
}
}
Loading