Add API docs for RelPath (#38923)

Max Brunsfeld created

Also reduce the use of `unsafe` in that module.

Release Notes:

- N/A

Change summary

crates/util/src/rel_path.rs | 80 +++++++++++++++++++++++++++++---------
1 file changed, 61 insertions(+), 19 deletions(-)

Detailed changes

crates/util/src/rel_path.rs 🔗

@@ -9,18 +9,40 @@ use std::{
     sync::Arc,
 };
 
+/// A file system path that is guaranteed to be relative and normalized.
+///
+/// This type can be used to represent paths in a uniform way, regardless of
+/// whether they refer to Windows or POSIX file systems, and regardless of
+/// the host platform.
+///
+/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can
+/// be displayed in either POSIX or Windows format.
+///
+/// Relative paths are also guaranteed to be valid unicode.
 #[repr(transparent)]
 #[derive(PartialEq, Eq, Hash, Serialize)]
 pub struct RelPath(str);
 
+/// An owned representation of a file system path that is guaranteed to be
+/// relative and normalized.
+///
+/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
 #[derive(Clone, Serialize, Deserialize)]
 pub struct RelPathBuf(String);
 
 impl RelPath {
+    /// Creates an empty [`RelPath`].
     pub fn empty() -> &'static Self {
-        unsafe { Self::new_unchecked("") }
+        Self::new_unchecked("")
     }
 
+    /// Converts a path with a given style into a [`RelPath`].
+    ///
+    /// Returns an error if the path is absolute, or is not valid unicode.
+    ///
+    /// This method will normalize the path by removing `.` components,
+    /// processing `..` components, and removing trailing separators. It does
+    /// not allocate unless it's necessary to reformat the path.
     #[track_caller]
     pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
         let mut path = path.to_str().context("non utf-8 path")?;
@@ -49,7 +71,7 @@ impl RelPath {
         }
 
         let mut result = match string {
-            Cow::Borrowed(string) => Cow::Borrowed(unsafe { Self::new_unchecked(string) }),
+            Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
             Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
         };
 
@@ -67,7 +89,7 @@ impl RelPath {
                             return Err(anyhow!("path is not relative: {result:?}"));
                         }
                     }
-                    other => normalized.push(&RelPath::unix(other)?),
+                    other => normalized.push(RelPath::new_unchecked(other)),
                 }
             }
             result = Cow::Owned(normalized)
@@ -76,6 +98,10 @@ impl RelPath {
         Ok(result)
     }
 
+    /// Converts a path that is already normalized and uses '/' separators
+    /// into a [`RelPath`] .
+    ///
+    /// Returns an error if the path is not already in the correct format.
     #[track_caller]
     pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
         let path = path.as_ref();
@@ -85,7 +111,8 @@ impl RelPath {
         }
     }
 
-    unsafe fn new_unchecked(s: &str) -> &Self {
+    fn new_unchecked(s: &str) -> &Self {
+        // Safety: `RelPath` is a transparent wrapper around `str`.
         unsafe { &*(s as *const str as *const Self) }
     }
 
@@ -140,7 +167,7 @@ impl RelPath {
         }
         if let Some(suffix) = self.0.strip_prefix(&other.0) {
             if let Some(suffix) = suffix.strip_prefix('/') {
-                return Ok(unsafe { Self::new_unchecked(suffix) });
+                return Ok(Self::new_unchecked(suffix));
             } else if suffix.is_empty() {
                 return Ok(Self::empty());
             }
@@ -173,29 +200,31 @@ impl RelPath {
         } else {
             Cow::Owned(format!("{}/{}", &self.0, &other.0))
         };
-        Arc::from(unsafe { Self::new_unchecked(result.as_ref()) })
-    }
-
-    pub fn to_proto(&self) -> String {
-        self.as_unix_str().to_owned()
+        Arc::from(Self::new_unchecked(result.as_ref()))
     }
 
     pub fn to_rel_path_buf(&self) -> RelPathBuf {
         RelPathBuf(self.0.to_string())
     }
 
-    pub fn from_proto(path: &str) -> Result<Arc<Self>> {
-        Ok(Arc::from(Self::unix(path)?))
+    pub fn into_arc(&self) -> Arc<Self> {
+        Arc::from(self)
     }
 
-    pub fn as_unix_str(&self) -> &str {
-        &self.0
+    /// Convert the path into the wire representation.
+    pub fn to_proto(&self) -> String {
+        self.as_unix_str().to_owned()
     }
 
-    pub fn into_arc(&self) -> Arc<Self> {
-        Arc::from(self)
+    /// Load the path from its wire representation.
+    pub fn from_proto(path: &str) -> Result<Arc<Self>> {
+        Ok(Arc::from(Self::unix(path)?))
     }
 
+    /// Convert the path into a string with the given path style.
+    ///
+    /// Whenever a path is presented to the user, it should be converted to
+    /// a string via this method.
     pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
         match style {
             PathStyle::Posix => Cow::Borrowed(&self.0),
@@ -203,6 +232,19 @@ impl RelPath {
         }
     }
 
+    /// Get the internal unix-style representation of the path.
+    ///
+    /// This should not be shown to the user.
+    pub fn as_unix_str(&self) -> &str {
+        &self.0
+    }
+
+    /// Interprets the path as a [`std::path::Path`], suitable for file system calls.
+    ///
+    /// This is guaranteed to be a valid path regardless of the host platform, because
+    /// the `/` is accepted as a path separator on windows.
+    ///
+    /// This should not be shown to the user.
     pub fn as_std_path(&self) -> &Path {
         Path::new(&self.0)
     }
@@ -271,7 +313,7 @@ impl RelPathBuf {
     }
 
     pub fn as_rel_path(&self) -> &RelPath {
-        unsafe { RelPath::new_unchecked(self.0.as_str()) }
+        RelPath::new_unchecked(self.0.as_str())
     }
 
     pub fn set_extension(&mut self, extension: &str) -> bool {
@@ -340,7 +382,7 @@ const SEPARATOR: char = '/';
 
 impl<'a> RelPathComponents<'a> {
     pub fn rest(&self) -> &'a RelPath {
-        unsafe { RelPath::new_unchecked(self.0) }
+        RelPath::new_unchecked(self.0)
     }
 }
 
@@ -374,7 +416,7 @@ impl<'a> Iterator for RelPathAncestors<'a> {
         } else {
             self.0 = None;
         }
-        Some(unsafe { RelPath::new_unchecked(result) })
+        Some(RelPath::new_unchecked(result))
     }
 }