pub struct Path<'a>(&'a str);
impl<'a> Path<'a> {
pub const PATH_SEP: char = '/';
pub const DRIVE_SEP: char = ':';
pub fn new(path_str: &'a str) -> Result<Path<'a>, crate::Error> {
if path_str.is_empty() {
return Err(crate::Error::InvalidPath);
}
if let Some((drive_specifier, directory_path)) = path_str.split_once(Self::DRIVE_SEP) {
if drive_specifier.contains(Self::PATH_SEP) {
return Err(crate::Error::InvalidPath);
}
if directory_path.contains(Self::DRIVE_SEP) {
return Err(crate::Error::InvalidPath);
}
if !directory_path.is_empty() && !directory_path.starts_with(Self::PATH_SEP) {
return Err(crate::Error::InvalidPath);
}
} else if path_str.starts_with(Self::PATH_SEP) {
return Err(crate::Error::InvalidPath);
}
for ch in path_str.chars() {
if ch.is_control() {
return Err(crate::Error::InvalidPath);
}
}
Ok(Path(path_str))
}
pub fn is_absolute_path(&self) -> bool {
self.drive_specifier().is_some()
}
pub fn drive_specifier(&self) -> Option<&str> {
if let Some((drive_specifier, _directory_path)) = self.0.split_once(Self::DRIVE_SEP) {
Some(drive_specifier)
} else {
None
}
}
pub fn drive_path(&self) -> Option<&str> {
if let Some((_drive_specifier, drive_path)) = self.0.split_once(Self::DRIVE_SEP) {
if drive_path.is_empty() {
Some("/")
} else {
Some(drive_path)
}
} else {
Some(self.0)
}
}
pub fn directory(&self) -> Option<&str> {
let Some(drive_path) = self.drive_path() else {
return None;
};
if let Some((directory, _filename)) = drive_path.rsplit_once(Self::PATH_SEP) {
if directory.is_empty() {
Some("/")
} else {
Some(directory)
}
} else {
Some(drive_path)
}
}
pub fn filename(&self) -> Option<&str> {
let Some(drive_path) = self.drive_path() else {
return None;
};
if let Some((_directory, filename)) = drive_path.rsplit_once(Self::PATH_SEP) {
if filename.is_empty() {
None
} else {
Some(filename)
}
} else {
Some(drive_path)
}
}
pub fn extension(&self) -> Option<&str> {
let Some(filename) = self.filename() else {
return None;
};
if let Some((_basename, extension)) = filename.rsplit_once('.') {
Some(extension)
} else {
None
}
}
pub fn as_str(&self) -> &str {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_path() {
let path_str = "HD0:/DOCUMENTS/JUNE/SALES.TXT";
let path = Path::new(path_str).unwrap();
assert!(path.is_absolute_path());
assert_eq!(path.drive_specifier(), Some("HD0"));
assert_eq!(path.drive_path(), Some("/DOCUMENTS/JUNE/SALES.TXT"));
assert_eq!(path.directory(), Some("/DOCUMENTS/JUNE"));
assert_eq!(path.filename(), Some("SALES.TXT"));
assert_eq!(path.extension(), Some("TXT"));
}
#[test]
fn bare_drive() {
let path_str = "HD0:";
let path = Path::new(path_str).unwrap();
assert!(path.is_absolute_path());
assert_eq!(path.drive_specifier(), Some("HD0"));
assert_eq!(path.drive_path(), Some("/"));
assert_eq!(path.directory(), Some("/"));
assert_eq!(path.filename(), None);
assert_eq!(path.extension(), None);
}
#[test]
fn relative_path() {
let path_str = "DOCUMENTS/JUNE/SALES.TXT";
let path = Path::new(path_str).unwrap();
assert!(!path.is_absolute_path());
assert_eq!(path.drive_specifier(), None);
assert_eq!(path.drive_path(), Some("DOCUMENTS/JUNE/SALES.TXT"));
assert_eq!(path.directory(), Some("DOCUMENTS/JUNE"));
assert_eq!(path.filename(), Some("SALES.TXT"));
assert_eq!(path.extension(), Some("TXT"));
}
#[test]
fn full_dir() {
let path_str = "HD0:/DOCUMENTS/JUNE/";
let path = Path::new(path_str).unwrap();
assert!(path.is_absolute_path());
assert_eq!(path.drive_specifier(), Some("HD0"));
assert_eq!(path.drive_path(), Some("/DOCUMENTS/JUNE/"));
assert_eq!(path.directory(), Some("/DOCUMENTS/JUNE"));
assert_eq!(path.filename(), None);
assert_eq!(path.extension(), None);
}
#[test]
fn relative_dir() {
let path_str = "DOCUMENTS/";
let path = Path::new(path_str).unwrap();
assert!(!path.is_absolute_path());
assert_eq!(path.drive_specifier(), None);
assert_eq!(path.drive_path(), Some("DOCUMENTS/"));
assert_eq!(path.directory(), Some("DOCUMENTS"));
assert_eq!(path.filename(), None);
assert_eq!(path.extension(), None);
}
}