use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use super::PathComponent;
use super::PathComponents;
use super::PathSemantics;
use super::RelativePath;
use crate::error::FsError;
use crate::error::FsOperation;
use crate::error::FsResult;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Path {
absolute: bool,
text: String,
literal: bool,
semantics: PathSemantics,
}
impl Path {
#[inline]
#[must_use]
pub fn root() -> Self {
Self {
absolute: true,
text: "/".to_owned(),
literal: false,
semantics: PathSemantics::Hierarchical,
}
}
#[inline]
pub fn from_components<I, S>(absolute: bool, components: I) -> FsResult<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let components = components
.into_iter()
.map(|value| PathComponent::parse(value.as_ref()))
.collect::<FsResult<Vec<_>>>()?;
if !absolute && components.is_empty() {
return Err(invalid_path());
}
let joined = components
.iter()
.map(PathComponent::as_str)
.collect::<Vec<_>>()
.join("/");
Ok(Self {
absolute,
text: if absolute {
if joined.is_empty() {
"/".to_owned()
} else {
format!("/{joined}")
}
} else {
joined
},
literal: false,
semantics: PathSemantics::Hierarchical,
})
}
#[inline]
pub fn parse(text: &str) -> FsResult<Self> {
Self::parse_with_semantics(text, PathSemantics::Hierarchical)
}
#[inline]
pub fn parse_literal(text: &str) -> FsResult<Self> {
Self::parse_with_semantics(text, PathSemantics::ObjectKey)
}
pub fn parse_with_semantics(text: &str, semantics: PathSemantics) -> FsResult<Self> {
if text.is_empty() || text.contains('\0') {
return Err(invalid_path());
}
if semantics != PathSemantics::Hierarchical {
return Ok(Self {
absolute: text.starts_with('/'),
text: text.to_owned(),
literal: true,
semantics,
});
}
let absolute = text.starts_with('/');
let mut components = Vec::new();
for component in text.split('/') {
match component {
"" | "." => {}
".." => {
if components.pop().is_none() {
return Err(invalid_path());
}
}
value => components.push(value),
}
}
let text = if absolute {
if components.is_empty() {
"/".to_owned()
} else {
format!("/{}", components.join("/"))
}
} else {
components.join("/")
};
if text.is_empty() {
return Err(invalid_path());
}
Ok(Self {
absolute,
text,
literal: false,
semantics,
})
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[inline]
#[must_use]
pub fn file_name(&self) -> Option<&str> {
if self.text == "/" || (self.literal && self.text.ends_with('/')) {
return None;
}
self.text.rsplit('/').find(|component| !component.is_empty())
}
#[inline]
#[must_use]
pub const fn is_absolute(&self) -> bool {
self.absolute
}
#[inline]
#[must_use]
pub const fn semantics(&self) -> PathSemantics {
self.semantics
}
#[inline]
#[must_use]
pub fn components(&self) -> PathComponents<'_> {
PathComponents::new(&self.text, self.absolute, self.literal)
}
#[inline]
#[must_use]
pub fn child(&self, component: &PathComponent) -> Self {
self.append(component.as_str())
}
#[inline]
#[must_use]
pub fn join(&self, relative: &RelativePath) -> Self {
self.append(relative.as_str())
}
fn append(&self, suffix: &str) -> Self {
let text = if self.text == "/" {
format!("/{suffix}")
} else {
format!("{}/{}", self.text, suffix)
};
Self {
absolute: self.absolute,
text,
literal: self.literal,
semantics: self.semantics,
}
}
}
impl Display for Path {
#[inline]
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str(self.as_str())
}
}
impl AsRef<str> for Path {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
fn invalid_path() -> FsError {
FsError::invalid_path(
FsOperation::ParsePath,
"path must be non-empty, NUL-free, and remain within its root",
)
}
#[cfg(test)]
mod tests {
use std::hint::black_box;
use super::Path;
use crate::path::PathComponent;
use crate::path::PathSemantics;
use crate::path::RelativePath;
#[test]
fn path_accessors_and_constructors_are_executed_at_runtime() {
let root: fn() -> Path = black_box(Path::root);
let parse_literal: fn(&str) -> crate::error::FsResult<Path> = black_box(Path::parse_literal);
let parse_with_semantics: fn(&str, PathSemantics) -> crate::error::FsResult<Path> =
black_box(Path::parse_with_semantics);
let as_str: for<'a> fn(&'a Path) -> &'a str = black_box(Path::as_str);
let file_name: for<'a> fn(&'a Path) -> Option<&'a str> = black_box(Path::file_name);
let is_absolute: fn(&Path) -> bool = black_box(Path::is_absolute);
let semantics: fn(&Path) -> PathSemantics = black_box(Path::semantics);
let components = black_box(Path::components);
let child: fn(&Path, &PathComponent) -> Path = black_box(Path::child);
let join: fn(&Path, &RelativePath) -> Path = black_box(Path::join);
let as_ref: for<'a> fn(&'a Path) -> &'a str = black_box(<Path as AsRef<str>>::as_ref);
let built = Path::from_components(true, vec!["reports", "daily.csv"]).expect("components should form a path");
assert!(Path::from_components(false, Vec::<&str>::new()).is_err());
let literal = parse_literal("bucket/key").expect("literal path should parse");
let provider = parse_with_semantics("bucket/key", PathSemantics::ProviderSpecific)
.expect("provider-specific path should parse");
let component = PathComponent::parse("archive").expect("component should parse");
let relative = RelativePath::parse("daily.csv").expect("relative path should parse");
assert_eq!("/", as_str(&root()));
assert_eq!(Some("daily.csv"), file_name(&built));
assert!(is_absolute(&built));
assert_eq!(PathSemantics::ObjectKey, semantics(&literal));
assert_eq!(PathSemantics::ProviderSpecific, semantics(&provider));
let parent = Path::parse("/reports").expect("parent path should parse");
assert_eq!("/reports/daily.csv", as_str(&join(&parent, &relative)));
assert_eq!(
"/reports/archive",
as_str(&child(&Path::parse("/reports").unwrap(), &component))
);
assert_eq!("reports/daily.csv", components(&built).collect::<Vec<_>>().join("/"));
assert_eq!(as_str(&built), as_ref(&built));
}
}