diff --git a/library/std/src/path.rs b/library/std/src/path.rs index 9d5778ed48cfe..8e918ee2a548b 100644 --- a/library/std/src/path.rs +++ b/library/std/src/path.rs @@ -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) } } @@ -1756,7 +1756,7 @@ impl cmp::Eq for PathBuf {} impl cmp::PartialOrd for PathBuf { #[inline] fn partial_cmp(&self, other: &PathBuf) -> Option { - Some(compare_components(self.components(), other.components())) + Path::partial_cmp(self, other) } } @@ -1764,7 +1764,7 @@ impl cmp::PartialOrd for PathBuf { impl cmp::Ord for PathBuf { #[inline] fn cmp(&self, other: &PathBuf) -> cmp::Ordering { - compare_components(self.components(), other.components()) + Path::cmp(self, other) } } @@ -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")] @@ -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() } } @@ -2798,7 +2826,7 @@ impl cmp::Eq for Path {} impl cmp::PartialOrd for Path { #[inline] fn partial_cmp(&self, other: &Path) -> Option { - Some(compare_components(self.components(), other.components())) + Some(Ord::cmp(self, other)) } } @@ -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())) } } diff --git a/library/std/src/path/tests.rs b/library/std/src/path/tests.rs index ce23cf6cd21b9..3eaf833dd8423 100644 --- a/library/std/src/path/tests.rs +++ b/library/std/src/path/tests.rs @@ -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)); @@ -1480,7 +1484,7 @@ pub fn test_compare() { ); tc!("foo/", "foo", - eq: true, + eq: false, starts_with: true, ends_with: true, relative_from: Some("") @@ -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"); @@ -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]