Skip to content

Make two Paths unequal if they differ in trailing slash #87339

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

Closed
wants to merge 2 commits into from
Closed
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
37 changes: 33 additions & 4 deletions library/std/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,7 @@ impl ToOwned for Path {
impl cmp::PartialEq for PathBuf {
#[inline]
fn eq(&self, other: &PathBuf) -> bool {
self.components() == other.components()
Path::eq(self, other)
}
}

Expand All @@ -1756,15 +1756,15 @@ impl cmp::Eq for PathBuf {}
impl cmp::PartialOrd for PathBuf {
#[inline]
fn partial_cmp(&self, other: &PathBuf) -> Option<cmp::Ordering> {
Some(compare_components(self.components(), other.components()))
Path::partial_cmp(self, other)
}
}

#[stable(feature = "rust1", since = "1.0.0")]
impl cmp::Ord for PathBuf {
#[inline]
fn cmp(&self, other: &PathBuf) -> cmp::Ordering {
compare_components(self.components(), other.components())
Path::cmp(self, other)
}
}

Expand Down Expand Up @@ -2718,6 +2718,33 @@ impl Path {
let inner = unsafe { Box::from_raw(rw) };
PathBuf { inner: OsString::from(inner) }
}

/// Returns true if the path's syntax indicates it necessarily can only
/// refer to a directory.
///
/// This is relevant to the PartialEq and PartialOrd impls of Path. For
/// example, compared as paths "r/s/" and "r/s/." and "r/s/./" and "r/s/./."
/// are all in the same equivalence class, but a different class from "r/s".
/// That's because if "r/s" is a file then "r/s" exists while most
/// filesystems would consider that "r/s/" does not exist, and thus these
/// must not be equal paths.
fn syntactically_dir(&self) -> bool {
let components = self.components();
let bytes_after_prefix =
&components.path[components.prefix.as_ref().map_or(0, Prefix::len)..];
let mut bytes_iter = bytes_after_prefix.iter();
let last_byte = bytes_iter.next_back();
let second_last_byte = bytes_iter.next_back();
match (second_last_byte, last_byte) {
// Path ends in separator, like "path/to/"
(_, Some(&sep)) if components.is_sep_byte(sep) => true,
// Path ends in separator followed by CurDir, like "path/to/."
(Some(&sep), Some(b'.')) if components.is_sep_byte(sep) => true,
// Path is ".", because "." and "./." need to be considered equal
(None, Some(b'.')) => true,
_ => false,
}
}
}

#[stable(feature = "rust1", since = "1.0.0")]
Expand Down Expand Up @@ -2779,6 +2806,7 @@ impl cmp::PartialEq for Path {
#[inline]
fn eq(&self, other: &Path) -> bool {
self.components() == other.components()
&& self.syntactically_dir() == other.syntactically_dir()
}
}

Expand All @@ -2798,7 +2826,7 @@ impl cmp::Eq for Path {}
impl cmp::PartialOrd for Path {
#[inline]
fn partial_cmp(&self, other: &Path) -> Option<cmp::Ordering> {
Some(compare_components(self.components(), other.components()))
Some(Ord::cmp(self, other))
}
}

Expand All @@ -2807,6 +2835,7 @@ impl cmp::Ord for Path {
#[inline]
fn cmp(&self, other: &Path) -> cmp::Ordering {
compare_components(self.components(), other.components())
.then_with(|| self.syntactically_dir().cmp(&other.syntactically_dir()))
}
}

Expand Down
51 changes: 45 additions & 6 deletions library/std/src/path/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1427,7 +1427,11 @@ pub fn test_compare() {
let eq = path1 == path2;
assert!(eq == $eq, "{:?} == {:?}, expected {:?}, got {:?}",
$path1, $path2, $eq, eq);
assert!($eq == (hash(path1) == hash(path2)),

// They have the same hash if the paths are equal or differ only in
// trailing separator.
assert_eq!($eq || $path1.trim_end_matches('/') == $path2.trim_end_matches('/'),
hash(path1) == hash(path2),
"{:?} == {:?}, expected {:?}, got {} and {}",
$path1, $path2, $eq, hash(path1), hash(path2));

Expand Down Expand Up @@ -1480,7 +1484,7 @@ pub fn test_compare() {
);

tc!("foo/", "foo",
eq: true,
eq: false,
starts_with: true,
ends_with: true,
relative_from: Some("")
Expand Down Expand Up @@ -1532,6 +1536,32 @@ pub fn test_compare() {
}
}

#[test]
fn test_trailing_separator() {
let file = Path::new("tmp/f");
let dir = Path::new("tmp/f/");

assert_ne!(file, dir);
assert!(file < dir);
assert_eq!(dir, Path::new("tmp/f//"));
assert_eq!(file.components(), dir.components());

if cfg!(windows) {
assert_eq!(dir, Path::new(r"tmp\f\"));
assert_eq!(dir, Path::new(r"tmp\f\\"));
}

let dir_dot = Path::new("tmp/f/.");
assert_ne!(file, dir_dot);
assert!(file < dir_dot);
assert_eq!(dir, dir_dot);

let file = file.to_owned();
let dir = dir.to_owned();
assert_ne!(file, dir);
assert!(file < dir);
}

#[test]
fn test_components_debug() {
let path = Path::new("/tmp");
Expand Down Expand Up @@ -1629,10 +1659,19 @@ fn test_ord() {
ord!(Less, "1", "2");
ord!(Less, "/foo/bar", "/foo./bar");
ord!(Less, "foo/bar", "foo/bar.");
ord!(Equal, "foo/./bar", "foo/bar/");
ord!(Equal, "foo/bar", "foo/bar/");
ord!(Equal, "foo/bar", "foo/bar/.");
ord!(Equal, "foo/bar", "foo/bar//");
ord!(Equal, "foo/./bar", "foo/bar");
ord!(Less, "foo/./bar", "foo/bar/");
ord!(Equal, "foo/./bar/", "foo/bar/");
ord!(Less, "foo/bar", "foo/bar/");
ord!(Less, "foo/bar", "foo/bar/.");
ord!(Less, "foo/bar", "foo/bar//");
ord!(Equal, "foo/bar/", "foo/bar//");
ord!(Equal, "foo/bar/", "foo/bar/.");
ord!(Equal, "foo/bar/", "foo/bar/./");
ord!(Equal, "foo/bar/", "foo/bar/./.");
ord!(Less, "/", "./");
ord!(Equal, ".", "./");
ord!(Equal, ".", "./.");
}

#[bench]
Expand Down