Files
moss-kernel/libkernel/src/fs/path.rs
Ashwin Naren aa5a9c7b79 fix typos
2026-01-23 01:26:04 -08:00

346 lines
10 KiB
Rust

//! A module for path manipulation that works with string slices.
//!
//! This module provides a `Path` struct that is a thin wrapper around `&str`,
//! offering various methods for path inspection and manipulation.
use alloc::{borrow::ToOwned, vec::Vec};
use super::pathbuf::PathBuf;
/// Represents a path slice, akin to `&str`.
///
/// This struct provides a number of methods for inspecting a path,
/// including breaking the path into its components, determining if it's
/// absolute, and more.
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct Path {
inner: str,
}
impl Path {
/// Creates a new `Path` from a string slice.
///
/// This is a cost-free conversion.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// let path = Path::new("/usr/bin/ls");
/// ```
pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> &Self {
unsafe { &*(s.as_ref() as *const str as *const Path) }
}
/// Returns the underlying string slice.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// let path = Path::new("/etc/passwd");
/// assert_eq!(path.as_str(), "/etc/passwd");
/// ```
pub fn as_str(&self) -> &str {
&self.inner
}
/// Determines whether the path is absolute.
///
/// An absolute path starts with a `/`.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// assert!(Path::new("/home/user").is_absolute());
/// assert!(!Path::new("home/user").is_absolute());
/// ```
pub fn is_absolute(&self) -> bool {
self.inner.starts_with('/')
}
/// Determines whether the path is relative.
///
/// A relative path does not start with a `/`.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// assert!(Path::new("src/main.rs").is_relative());
/// assert!(!Path::new("/src/main.rs").is_relative());
/// ```
pub fn is_relative(&self) -> bool {
!self.is_absolute()
}
/// Produces an iterator over the components of the path.
///
/// Components are the non-empty parts of the path separated by `/`.
/// Repeated separators are ignored.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// let path = Path::new("/some//path/./file.txt");
/// let components: Vec<_> = path.components().collect();
/// assert_eq!(components, vec!["some", "path", "file.txt"]);
/// ```
pub fn components(&self) -> Components<'_> {
Components {
remaining: &self.inner,
}
}
/// Joins two paths together.
///
/// This will allocate a new `PathBuf` to hold the joined path. If the
/// `other` path is absolute, it replaces the current one.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
/// use libkernel::fs::pathbuf::PathBuf;
//
/// let path1 = Path::new("/usr/local");
/// let path2 = Path::new("bin/rustc");
/// assert_eq!(path1.join(path2), PathBuf::from("/usr/local/bin/rustc"));
///
/// let path3 = Path::new("/etc/init.d");
/// assert_eq!(path1.join(path3), PathBuf::from("/etc/init.d"));
/// ```
pub fn join(&self, other: &Path) -> PathBuf {
let mut ret: PathBuf = PathBuf::with_capacity(self.inner.len() + other.inner.len());
ret.push(self);
ret.push(other);
ret
}
/// Strips a prefix from the path.
///
/// If the path starts with `base`, returns a new `Path` slice with the
/// prefix removed. Otherwise, returns `None`.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// let path = Path::new("/usr/lib/x86_64-linux-gnu");
/// let prefix = Path::new("/usr");
///
/// assert_eq!(path.strip_prefix(prefix), Some(Path::new("lib/x86_64-linux-gnu")));
/// assert_eq!(prefix.strip_prefix(path), None);
/// ```
pub fn strip_prefix(&self, base: &Path) -> Option<&Path> {
if self.inner.starts_with(&base.inner) {
// If the prefixes are the same, and they have the same length, the
// whole string is the prefix.
if base.inner.len() == self.inner.len() {
return Some(Path::new(""));
}
if self.inner.as_bytes().get(base.inner.len()) == Some(&b'/') {
let stripped = &self.inner[base.inner.len()..];
// If the base ends with a slash, we don't want a leading slash on the result
if base.inner.ends_with('/') {
return Some(Path::new(stripped));
}
return Some(Path::new(&stripped[1..]));
}
}
None
}
/// Returns the parent directory of the path.
///
/// Returns `None` if the path is a root directory or has no parent.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// let path = Path::new("/foo/bar");
/// assert_eq!(path.parent(), Some(Path::new("/foo")));
///
/// let path_root = Path::new("/");
/// assert_eq!(path_root.parent(), None);
/// ```
pub fn parent(&self) -> Option<&Path> {
let mut components = self.components().collect::<Vec<_>>();
if components.len() <= 1 {
return None;
}
components.pop();
let parent_len = components.iter().map(|s| s.len()).sum::<usize>() + components.len() - 1;
let end = if self.is_absolute() {
parent_len + 1
} else {
parent_len
};
Some(Path::new(&self.inner[..end]))
}
/// Returns the final component of the path, if there is one.
///
/// This is the file name for a file path, or the directory name for a
/// directory path.
///
/// # Examples
///
/// ```
/// use libkernel::fs::path::Path;
///
/// assert_eq!(Path::new("/home/user/file.txt").file_name(), Some("file.txt"));
/// assert_eq!(Path::new("/home/user/").file_name(), Some("user"));
/// assert_eq!(Path::new("/").file_name(), None);
/// ```
pub fn file_name(&self) -> Option<&str> {
self.components().last()
}
}
impl AsRef<Path> for str {
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
impl ToOwned for Path {
type Owned = PathBuf;
fn to_owned(&self) -> Self::Owned {
PathBuf::from(self.as_str())
}
}
/// An iterator over the components of a `Path`.
#[derive(Clone, Debug)]
pub struct Components<'a> {
remaining: &'a str,
}
impl<'a> Iterator for Components<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
// Trim leading slashes
self.remaining = self.remaining.trim_start_matches('/');
if self.remaining.is_empty() {
return None;
}
match self.remaining.find('/') {
Some(index) => {
let component = &self.remaining[..index];
self.remaining = &self.remaining[index..];
if component == "." {
self.next()
} else {
Some(component)
}
}
None => {
let component = self.remaining;
self.remaining = "";
if component == "." {
self.next()
} else {
Some(component)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::Path;
use alloc::vec::Vec;
#[test]
fn test_new_path() {
let p = Path::new("/a/b/c");
assert_eq!(p.as_str(), "/a/b/c");
}
#[test]
fn test_is_absolute() {
assert!(Path::new("/").is_absolute());
assert!(Path::new("/a/b").is_absolute());
assert!(!Path::new("a/b").is_absolute());
assert!(!Path::new("").is_absolute());
}
#[test]
fn test_is_relative() {
assert!(Path::new("a/b").is_relative());
assert!(Path::new("").is_relative());
assert!(!Path::new("/a/b").is_relative());
}
#[test]
fn test_components() {
let p = Path::new("/a/b/c");
let mut comps = p.components();
assert_eq!(comps.next(), Some("a"));
assert_eq!(comps.next(), Some("b"));
assert_eq!(comps.next(), Some("c"));
assert_eq!(comps.next(), None);
let p2 = Path::new("a//b/./c/");
assert_eq!(p2.components().collect::<Vec<_>>(), vec!["a", "b", "c"]);
}
#[test]
fn test_join() {
assert_eq!(Path::new("/a/b").join(Path::new("c/d")), "/a/b/c/d".into());
assert_eq!(Path::new("/a/b/").join(Path::new("c")), "/a/b/c".into());
assert_eq!(Path::new("a").join(Path::new("b")), "a/b".into());
assert_eq!(Path::new("/a").join(Path::new("/b")), "/b".into());
assert_eq!(Path::new("").join(Path::new("a")), "a".into());
}
#[test]
fn test_strip_prefix() {
let p = Path::new("/a/b/c");
assert_eq!(p.strip_prefix(Path::new("/a")), Some(Path::new("b/c")));
assert_eq!(p.strip_prefix(Path::new("/a/b")), Some(Path::new("c")));
assert_eq!(p.strip_prefix(Path::new("/a/b/c")), Some(Path::new("")));
assert_eq!(p.strip_prefix(Path::new("/d")), None);
assert_eq!(p.strip_prefix(Path::new("/a/b/c/d")), None);
assert_eq!(Path::new("/a/bc").strip_prefix(Path::new("/a/b")), None);
}
#[test]
fn test_parent() {
assert_eq!(Path::new("/a/b/c").parent(), Some(Path::new("/a/b")));
assert_eq!(Path::new("/a/b").parent(), Some(Path::new("/a")));
assert_eq!(Path::new("/a").parent(), None);
assert_eq!(Path::new("/").parent(), None);
assert_eq!(Path::new("a/b").parent(), Some(Path::new("a")));
assert_eq!(Path::new("a").parent(), None);
}
#[test]
fn test_file_name() {
assert_eq!(Path::new("/a/b/c.txt").file_name(), Some("c.txt"));
assert_eq!(Path::new("/a/b/").file_name(), Some("b"));
assert_eq!(Path::new("c.txt").file_name(), Some("c.txt"));
assert_eq!(Path::new("/").file_name(), None);
assert_eq!(Path::new(".").file_name(), None);
}
}