Skip to content

Commit 9b6cd12

Browse files
committed
Add Path::has_trailing_sep and related methods
1 parent 5e749eb commit 9b6cd12

File tree

2 files changed

+223
-10
lines changed

2 files changed

+223
-10
lines changed

library/std/src/path.rs

Lines changed: 184 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,99 @@ impl PathBuf {
14121412
}
14131413
}
14141414

1415+
/// Sets whether the path has a trailing [separator](MAIN_SEPARATOR).
1416+
///
1417+
/// The value returned by [`has_trailing_sep`](Path::has_trailing_sep) will be equivalent to
1418+
/// the provided value if possible.
1419+
///
1420+
/// # Examples
1421+
///
1422+
/// ```
1423+
/// #![feature(path_trailing_sep)]
1424+
/// use std::path::PathBuf;
1425+
///
1426+
/// let mut p = PathBuf::from("dir");
1427+
///
1428+
/// assert!(!p.has_trailing_sep());
1429+
/// p.set_trailing_sep(false);
1430+
/// assert!(!p.has_trailing_sep());
1431+
/// p.set_trailing_sep(true);
1432+
/// assert!(p.has_trailing_sep());
1433+
/// p.set_trailing_sep(false);
1434+
/// assert!(!p.has_trailing_sep());
1435+
///
1436+
/// p = PathBuf::from("/");
1437+
/// assert!(p.has_trailing_sep());
1438+
/// p.set_trailing_sep(false);
1439+
/// assert!(p.has_trailing_sep());
1440+
/// ```
1441+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
1442+
pub fn set_trailing_sep(&mut self, trailing_sep: bool) {
1443+
if trailing_sep { self.push_trailing_sep() } else { self.pop_trailing_sep() }
1444+
}
1445+
1446+
/// Adds a trailing [separator](MAIN_SEPARATOR) to the path.
1447+
///
1448+
/// This acts similarly to [`Path::with_trailing_sep`], but mutates the underlying `PathBuf`.
1449+
///
1450+
/// # Examples
1451+
///
1452+
/// ```
1453+
/// #![feature(path_trailing_sep)]
1454+
/// use std::ffi::OsStr;
1455+
/// use std::path::PathBuf;
1456+
///
1457+
/// let mut p = PathBuf::from("dir");
1458+
///
1459+
/// assert!(!p.has_trailing_sep());
1460+
/// p.push_trailing_sep();
1461+
/// assert!(p.has_trailing_sep());
1462+
/// p.push_trailing_sep();
1463+
/// assert!(p.has_trailing_sep());
1464+
///
1465+
/// p = PathBuf::from("dir/");
1466+
/// p.push_trailing_sep();
1467+
/// assert_eq!(p.as_os_str(), OsStr::new("dir/"));
1468+
/// ```
1469+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
1470+
pub fn push_trailing_sep(&mut self) {
1471+
if !self.has_trailing_sep() {
1472+
self.push("");
1473+
}
1474+
}
1475+
1476+
/// Removes a trailing [separator](MAIN_SEPARATOR) from the path, if possible.
1477+
///
1478+
/// This acts similarly to [`Path::trim_trailing_sep`], but mutates the underlying `PathBuf`.
1479+
///
1480+
/// # Examples
1481+
///
1482+
/// ```
1483+
/// #![feature(path_trailing_sep)]
1484+
/// use std::ffi::OsStr;
1485+
/// use std::path::PathBuf;
1486+
///
1487+
/// let mut p = PathBuf::from("dir//");
1488+
///
1489+
/// assert!(p.has_trailing_sep());
1490+
/// assert_eq!(p.as_os_str(), OsStr::new("dir//"));
1491+
/// p.pop_trailing_sep();
1492+
/// assert!(!p.has_trailing_sep());
1493+
/// assert_eq!(p.as_os_str(), OsStr::new("dir"));
1494+
/// p.pop_trailing_sep();
1495+
/// assert!(!p.has_trailing_sep());
1496+
/// assert_eq!(p.as_os_str(), OsStr::new("dir"));
1497+
///
1498+
/// p = PathBuf::from("/");
1499+
/// assert!(p.has_trailing_sep());
1500+
/// p.pop_trailing_sep();
1501+
/// assert!(p.has_trailing_sep());
1502+
/// ```
1503+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
1504+
pub fn pop_trailing_sep(&mut self) {
1505+
self.inner.truncate(self.trim_trailing_sep().as_os_str().len());
1506+
}
1507+
14151508
/// Updates [`self.file_name`] to `file_name`.
14161509
///
14171510
/// If [`self.file_name`] was [`None`], this is equivalent to pushing
@@ -1612,7 +1705,7 @@ impl PathBuf {
16121705
let new = extension.as_encoded_bytes();
16131706
if !new.is_empty() {
16141707
// truncate until right after the file name
1615-
// this is necessary for trimming the trailing slash
1708+
// this is necessary for trimming the trailing separator
16161709
let end_file_name = file_name[file_name.len()..].as_ptr().addr();
16171710
let start = self.inner.as_encoded_bytes().as_ptr().addr();
16181711
self.inner.truncate(end_file_name.wrapping_sub(start));
@@ -2722,6 +2815,94 @@ impl Path {
27222815
self.file_name().map(rsplit_file_at_dot).and_then(|(before, after)| before.and(after))
27232816
}
27242817

2818+
/// Checks whether the path ends in a trailing [separator](MAIN_SEPARATOR).
2819+
///
2820+
/// This is generally done to ensure that a path is treated as a directory, not a file,
2821+
/// although it does not actually guarantee that such a path is a directory on the underlying
2822+
/// file system.
2823+
///
2824+
/// Despite this behavior, two paths are still considered the same in Rust whether they have a
2825+
/// trailing separator or not.
2826+
///
2827+
/// # Examples
2828+
///
2829+
/// ```
2830+
/// #![feature(path_trailing_sep)]
2831+
/// use std::path::Path;
2832+
///
2833+
/// assert!(Path::new("dir/").has_trailing_sep());
2834+
/// assert!(!Path::new("file.rs").has_trailing_sep());
2835+
/// ```
2836+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
2837+
#[must_use]
2838+
#[inline]
2839+
pub fn has_trailing_sep(&self) -> bool {
2840+
self.as_os_str().as_encoded_bytes().last().copied().is_some_and(is_sep_byte)
2841+
}
2842+
2843+
/// Ensures that a path has a trailing [separator](MAIN_SEPARATOR),
2844+
/// allocating a [`PathBuf`] if necessary.
2845+
///
2846+
/// The resulting path will return true for [`has_trailing_sep`](Self::has_trailing_sep).
2847+
///
2848+
/// # Examples
2849+
///
2850+
/// ```
2851+
/// #![feature(path_trailing_sep)]
2852+
/// use std::ffi::OsStr;
2853+
/// use std::path::Path;
2854+
///
2855+
/// assert_eq!(Path::new("dir//").with_trailing_sep().as_os_str(), OsStr::new("dir//"));
2856+
/// assert_eq!(Path::new("dir/").with_trailing_sep().as_os_str(), OsStr::new("dir/"));
2857+
/// assert!(!Path::new("dir").has_trailing_sep());
2858+
/// assert!(Path::new("dir").with_trailing_sep().has_trailing_sep());
2859+
/// ```
2860+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
2861+
#[must_use]
2862+
#[inline]
2863+
pub fn with_trailing_sep(&self) -> Cow<'_, Path> {
2864+
if self.has_trailing_sep() { Cow::Borrowed(self) } else { Cow::Owned(self.join("")) }
2865+
}
2866+
2867+
/// Trims a trailing [separator](MAIN_SEPARATOR) from a path, if possible.
2868+
///
2869+
/// The resulting path will return false for [`has_trailing_sep`](Self::has_trailing_sep) for
2870+
/// most paths.
2871+
///
2872+
/// Some paths, like `/`, cannot be trimmed in this way.
2873+
///
2874+
/// # Examples
2875+
///
2876+
/// ```
2877+
/// #![feature(path_trailing_sep)]
2878+
/// use std::ffi::OsStr;
2879+
/// use std::path::Path;
2880+
///
2881+
/// assert_eq!(Path::new("dir//").trim_trailing_sep().as_os_str(), OsStr::new("dir"));
2882+
/// assert_eq!(Path::new("dir/").trim_trailing_sep().as_os_str(), OsStr::new("dir"));
2883+
/// assert_eq!(Path::new("dir").trim_trailing_sep().as_os_str(), OsStr::new("dir"));
2884+
/// assert_eq!(Path::new("/").trim_trailing_sep().as_os_str(), OsStr::new("/"));
2885+
/// assert_eq!(Path::new("//").trim_trailing_sep().as_os_str(), OsStr::new("//"));
2886+
/// ```
2887+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
2888+
#[must_use]
2889+
#[inline]
2890+
pub fn trim_trailing_sep(&self) -> &Path {
2891+
if self.has_trailing_sep() && (!self.has_root() || self.parent().is_some()) {
2892+
let mut bytes = self.inner.as_encoded_bytes();
2893+
while let Some((last, init)) = bytes.split_last()
2894+
&& is_sep_byte(*last)
2895+
{
2896+
bytes = init;
2897+
}
2898+
2899+
// SAFETY: Trimming trailing ASCII bytes will retain the validity of the string.
2900+
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
2901+
} else {
2902+
self
2903+
}
2904+
}
2905+
27252906
/// Creates an owned [`PathBuf`] with `path` adjoined to `self`.
27262907
///
27272908
/// If `path` is absolute, it replaces the current path.
@@ -2876,7 +3057,7 @@ impl Path {
28763057
/// `a/b` all have `a` and `b` as components, but `./a/b` starts with
28773058
/// an additional [`CurDir`] component.
28783059
///
2879-
/// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
3060+
/// * Trailing separators are normalized away, so `/a/b` and `/a/b/` are equivalent.
28803061
///
28813062
/// Note that no other normalization takes place; in particular, `a/c`
28823063
/// and `a/b/../c` are distinct, to account for the possibility that `b`
@@ -3646,7 +3827,7 @@ impl Error for NormalizeError {}
36463827
///
36473828
/// On POSIX platforms, the path is resolved using [POSIX semantics][posix-semantics],
36483829
/// except that it stops short of resolving symlinks. This means it will keep `..`
3649-
/// components and trailing slashes.
3830+
/// components and trailing separators.
36503831
///
36513832
/// On Windows, for verbatim paths, this will simply return the path as given. For other
36523833
/// paths, this is currently equivalent to calling

library/std/tests/path.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
#![feature(
2-
clone_to_uninit,
3-
path_add_extension,
4-
path_file_prefix,
5-
maybe_uninit_slice,
6-
normalize_lexically
7-
)]
1+
// tidy-alphabetical-start
2+
#![feature(clone_to_uninit)]
3+
#![feature(maybe_uninit_slice)]
4+
#![feature(normalize_lexically)]
5+
#![feature(path_add_extension)]
6+
#![feature(path_file_prefix)]
7+
#![feature(path_trailing_sep)]
8+
// tidy-alphabetical-end
89

910
use std::clone::CloneToUninit;
1011
use std::ffi::OsStr;
@@ -2532,3 +2533,34 @@ fn normalize_lexically() {
25322533
check_err(r"\\?\UNC\server\share\a\..\..");
25332534
}
25342535
}
2536+
2537+
#[test]
2538+
pub fn test_trim_trailing_sep() {
2539+
assert_eq!(Path::new("/").trim_trailing_sep().as_os_str(), OsStr::new("/"));
2540+
assert_eq!(Path::new("//").trim_trailing_sep().as_os_str(), OsStr::new("//"));
2541+
assert_eq!(Path::new("").trim_trailing_sep().as_os_str(), OsStr::new(""));
2542+
assert_eq!(Path::new(".").trim_trailing_sep().as_os_str(), OsStr::new("."));
2543+
assert_eq!(Path::new("./").trim_trailing_sep().as_os_str(), OsStr::new("."));
2544+
assert_eq!(Path::new(".//").trim_trailing_sep().as_os_str(), OsStr::new("."));
2545+
assert_eq!(Path::new("..").trim_trailing_sep().as_os_str(), OsStr::new(".."));
2546+
assert_eq!(Path::new("../").trim_trailing_sep().as_os_str(), OsStr::new(".."));
2547+
assert_eq!(Path::new("..//").trim_trailing_sep().as_os_str(), OsStr::new(".."));
2548+
2549+
#[cfg(any(windows, target_os = "cygwin"))]
2550+
{
2551+
assert_eq!(Path::new("\\").trim_trailing_sep().as_os_str(), OsStr::new("\\"));
2552+
assert_eq!(Path::new("\\\\").trim_trailing_sep().as_os_str(), OsStr::new("\\\\"));
2553+
assert_eq!(Path::new("c:/").trim_trailing_sep().as_os_str(), OsStr::new("c:/"));
2554+
assert_eq!(Path::new("c://").trim_trailing_sep().as_os_str(), OsStr::new("c://"));
2555+
assert_eq!(Path::new("c:./").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
2556+
assert_eq!(Path::new("c:.//").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
2557+
assert_eq!(Path::new("c:../").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
2558+
assert_eq!(Path::new("c:..//").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
2559+
assert_eq!(Path::new("c:\\").trim_trailing_sep().as_os_str(), OsStr::new("c:\\"));
2560+
assert_eq!(Path::new("c:\\\\").trim_trailing_sep().as_os_str(), OsStr::new("c:\\\\"));
2561+
assert_eq!(Path::new("c:.\\").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
2562+
assert_eq!(Path::new("c:.\\\\").trim_trailing_sep().as_os_str(), OsStr::new("c:."));
2563+
assert_eq!(Path::new("c:..\\").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
2564+
assert_eq!(Path::new("c:..\\\\").trim_trailing_sep().as_os_str(), OsStr::new("c:.."));
2565+
}
2566+
}

0 commit comments

Comments
 (0)