@@ -1403,6 +1403,88 @@ impl PathBuf {
14031403 }
14041404 }
14051405
1406+ /// Sets whether the path has a trailing [separator](MAIN_SEPARATOR).
1407+ ///
1408+ /// The value returned by [`has_trailing_sep`](Self::has_trailing_sep) will be equivalent to
1409+ /// the provided value.
1410+ ///
1411+ /// # Examples
1412+ ///
1413+ /// ```
1414+ /// #![feature(path_trailing_sep)]
1415+ /// use std::ffi::OsStr;
1416+ /// use std::path::{PathBuf, Path};
1417+ ///
1418+ /// let mut p = PathBuf::from("dir");
1419+ ///
1420+ /// assert!(!p.has_trailing_sep());
1421+ /// p.set_trailing_sep(false);
1422+ /// assert!(!p.has_trailing_sep());
1423+ /// p.set_trailing_sep(true);
1424+ /// assert!(p.has_trailing_sep());
1425+ /// assert_eq!(p.file_name(), None);
1426+ /// p.set_trailing_sep(false);
1427+ /// assert!(!p.has_trailing_sep());
1428+ /// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1429+ /// assert_eq!(p, Path::new("dir"));
1430+ /// ```
1431+ #[ unstable( feature = "path_trailing_sep" , issue = "142503" ) ]
1432+ pub fn set_trailing_sep ( & mut self , trailing_sep : bool ) {
1433+ if trailing_sep { self . push_trailing_sep ( ) } else { self . pop_trailing_sep ( ) }
1434+ }
1435+
1436+ /// Adds a trailing [separator](MAIN_SEPARATOR) to the path.
1437+ ///
1438+ /// This acts similarly to [`Path::with_trailing_sep`], but mutates the underlying `PathBuf`.
1439+ ///
1440+ /// # Examples
1441+ ///
1442+ /// ```
1443+ /// #![feature(path_trailing_sep)]
1444+ /// use std::ffi::OsStr;
1445+ /// use std::path::{Path, PathBuf};
1446+ ///
1447+ /// let mut p = PathBuf::from("dir");
1448+ ///
1449+ /// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1450+ /// p.push_trailing_sep();
1451+ /// assert_eq!(p.file_name(), None);
1452+ /// p.push_trailing_sep();
1453+ /// assert_eq!(p.file_name(), None);
1454+ /// assert_eq!(p, Path::new("dir/"));
1455+ /// ```
1456+ #[ unstable( feature = "path_trailing_sep" , issue = "142503" ) ]
1457+ pub fn push_trailing_sep ( & mut self ) {
1458+ if !self . has_trailing_sep ( ) {
1459+ self . push ( "" ) ;
1460+ }
1461+ }
1462+
1463+ /// Removes a trailing [separator](MAIN_SEPARATOR) from the path, if possible.
1464+ ///
1465+ /// This acts similarly to [`Path::trim_trailing_sep`], but mutates the underlying `PathBuf`.
1466+ ///
1467+ /// # Examples
1468+ ///
1469+ /// ```
1470+ /// #![feature(path_trailing_sep)]
1471+ /// use std::ffi::OsStr;
1472+ /// use std::path::{Path, PathBuf};
1473+ ///
1474+ /// let mut p = PathBuf::from("dir/");
1475+ ///
1476+ /// assert_eq!(p.file_name(), None);
1477+ /// p.pop_trailing_sep();
1478+ /// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1479+ /// p.pop_trailing_sep();
1480+ /// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1481+ /// assert_eq!(p, Path::new("/dir"));
1482+ /// ```
1483+ #[ unstable( feature = "path_trailing_sep" , issue = "142503" ) ]
1484+ pub fn pop_trailing_sep ( & mut self ) {
1485+ self . inner . truncate ( self . trim_trailing_sep ( ) . as_os_str ( ) . len ( ) ) ;
1486+ }
1487+
14061488 /// Updates [`self.file_name`] to `file_name`.
14071489 ///
14081490 /// If [`self.file_name`] was [`None`], this is equivalent to pushing
@@ -1603,7 +1685,7 @@ impl PathBuf {
16031685 let new = extension. as_encoded_bytes ( ) ;
16041686 if !new. is_empty ( ) {
16051687 // truncate until right after the file name
1606- // this is necessary for trimming the trailing slash
1688+ // this is necessary for trimming the trailing separator
16071689 let end_file_name = file_name[ file_name. len ( ) ..] . as_ptr ( ) . addr ( ) ;
16081690 let start = self . inner . as_encoded_bytes ( ) . as_ptr ( ) . addr ( ) ;
16091691 self . inner . truncate ( end_file_name. wrapping_sub ( start) ) ;
@@ -2686,6 +2768,90 @@ impl Path {
26862768 self . file_name ( ) . map ( rsplit_file_at_dot) . and_then ( |( before, after) | before. and ( after) )
26872769 }
26882770
2771+ /// Checks whether the path ends in a trailing [separator](MAIN_SEPARATOR).
2772+ ///
2773+ /// This is generally done to ensure that a path is treated as a directory, not a file,
2774+ /// although it does not actually guarantee that such a path is a directory on the underlying
2775+ /// file system.
2776+ ///
2777+ /// Despite this behavior, two paths are still considered the same in Rust whether they have a
2778+ /// trailing separator or not.
2779+ ///
2780+ /// # Examples
2781+ ///
2782+ /// ```
2783+ /// #![feature(path_trailing_sep)]
2784+ /// use std::path::Path;
2785+ ///
2786+ /// assert!(Path::new("dir/").has_trailing_sep());
2787+ /// assert!(!Path::new("file.rs").has_trailing_sep());
2788+ /// ```
2789+ #[ unstable( feature = "path_trailing_sep" , issue = "142503" ) ]
2790+ #[ must_use]
2791+ #[ inline]
2792+ pub fn has_trailing_sep ( & self ) -> bool {
2793+ self . as_os_str ( ) . as_encoded_bytes ( ) . last ( ) . copied ( ) . is_some_and ( is_sep_byte)
2794+ }
2795+
2796+ /// Ensures that a path has a trailing [separator](MAIN_SEPARATOR),
2797+ /// allocating a [`PathBuf`] if necessary.
2798+ ///
2799+ /// The resulting path will return true for [`has_trailing_sep`](Self::has_trailing_sep) and
2800+ /// `None` for [`file_name`](Self::file_name).
2801+ ///
2802+ /// # Examples
2803+ ///
2804+ /// ```
2805+ /// #![feature(path_trailing_sep)]
2806+ /// use std::ffi::OsStr;
2807+ /// use std::path::Path;
2808+ ///
2809+ /// assert_eq!(Path::new("dir//").with_trailing_sep().file_name(), None);
2810+ /// assert_eq!(Path::new("dir/").with_trailing_sep().file_name(), None);
2811+ /// assert_eq!(Path::new("dir").with_trailing_sep().file_name(), None);
2812+ /// assert_eq!(Path::new("dir").file_name(), Some(OsStr::new("dir")));
2813+ /// ```
2814+ #[ unstable( feature = "path_trailing_sep" , issue = "142503" ) ]
2815+ #[ must_use]
2816+ #[ inline]
2817+ pub fn with_trailing_sep ( & self ) -> Cow < ' _ , Path > {
2818+ if self . has_trailing_sep ( ) { Cow :: Borrowed ( self ) } else { Cow :: Owned ( self . join ( "" ) ) }
2819+ }
2820+
2821+ /// Trims a trailing [separator](MAIN_SEPARATOR) from a path, if possible.
2822+ ///
2823+ /// The resulting path will return false for [`has_trailing_sep`](Self::has_trailing_sep).
2824+ ///
2825+ /// # Examples
2826+ ///
2827+ /// ```
2828+ /// #![feature(path_trailing_sep)]
2829+ /// use std::ffi::OsStr;
2830+ /// use std::path::Path;
2831+ ///
2832+ /// assert_eq!(Path::new("dir//").trim_trailing_sep().file_name(), Some(OsStr::new("dir")));
2833+ /// assert_eq!(Path::new("dir/").trim_trailing_sep().file_name(), Some(OsStr::new("dir")));
2834+ /// assert_eq!(Path::new("dir").trim_trailing_sep().file_name(), Some(OsStr::new("dir")));
2835+ /// ```
2836+ #[ unstable( feature = "path_trailing_sep" , issue = "142503" ) ]
2837+ #[ must_use]
2838+ #[ inline]
2839+ pub fn trim_trailing_sep ( & self ) -> & Path {
2840+ if self . has_trailing_sep ( ) && ( !self . has_root ( ) || self . parent ( ) . is_some ( ) ) {
2841+ let mut bytes = self . inner . as_encoded_bytes ( ) ;
2842+ while let Some ( ( last, init) ) = bytes. split_last ( )
2843+ && is_sep_byte ( * last)
2844+ {
2845+ bytes = init;
2846+ }
2847+
2848+ // SAFETY: Trimming trailing ASCII bytes will retain the validity of the string.
2849+ Path :: new ( unsafe { OsStr :: from_encoded_bytes_unchecked ( bytes) } )
2850+ } else {
2851+ self
2852+ }
2853+ }
2854+
26892855 /// Creates an owned [`PathBuf`] with `path` adjoined to `self`.
26902856 ///
26912857 /// If `path` is absolute, it replaces the current path.
@@ -2840,7 +3006,7 @@ impl Path {
28403006 /// `a/b` all have `a` and `b` as components, but `./a/b` starts with
28413007 /// an additional [`CurDir`] component.
28423008 ///
2843- /// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
3009+ /// * Trailing separators are normalized away, so `/a/b` and `/a/b/` are equivalent.
28443010 ///
28453011 /// Note that no other normalization takes place; in particular, `a/c`
28463012 /// and `a/b/../c` are distinct, to account for the possibility that `b`
@@ -3610,7 +3776,7 @@ impl Error for NormalizeError {}
36103776///
36113777/// On POSIX platforms, the path is resolved using [POSIX semantics][posix-semantics],
36123778/// except that it stops short of resolving symlinks. This means it will keep `..`
3613- /// components and trailing slashes .
3779+ /// components and trailing separators .
36143780///
36153781/// On Windows, for verbatim paths, this will simply return the path as given. For other
36163782/// paths, this is currently equivalent to calling
0 commit comments