use crate::model::VolumeLimits;
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PortablePath(String);
impl PortablePath {
pub const ROOT: &'static str = "/";
pub fn parse(value: &str, limits: VolumeLimits) -> Result<Self, PathError> {
let path_bytes = u32::try_from(value.len()).map_err(|_| PathError::PathTooLong)?;
if path_bytes > limits.maximum_path_bytes {
return Err(PathError::PathTooLong);
}
if value == Self::ROOT {
return Ok(Self(value.to_owned()));
}
if !value.starts_with('/') {
return Err(PathError::NotAbsolute);
}
if value.ends_with('/') {
return Err(PathError::TrailingSeparator);
}
let mut depth = 0_u16;
for component in value.strip_prefix('/').unwrap_or(value).split('/') {
if component.is_empty() {
return Err(PathError::EmptyComponent);
}
if component == "." {
return Err(PathError::CurrentComponent);
}
if component == ".." {
return Err(PathError::ParentComponent);
}
if component.as_bytes().contains(&0) {
return Err(PathError::Nul);
}
let component_bytes =
u32::try_from(component.len()).map_err(|_| PathError::ComponentTooLong)?;
if component_bytes > limits.maximum_component_bytes {
return Err(PathError::ComponentTooLong);
}
depth = depth.checked_add(1).ok_or(PathError::TooDeep)?;
if depth > limits.maximum_path_depth {
return Err(PathError::TooDeep);
}
}
Ok(Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_within(&self, ancestor: &Self) -> bool {
if ancestor.0 == Self::ROOT {
return true;
}
self.0 == ancestor.0
|| self
.0
.strip_prefix(&ancestor.0)
.is_some_and(|suffix| suffix.starts_with('/'))
}
#[must_use]
pub fn depth(&self) -> usize {
if self.0 == Self::ROOT {
0
} else {
self.0
.strip_prefix('/')
.unwrap_or(&self.0)
.split('/')
.count()
}
}
pub fn components(&self) -> impl Iterator<Item = &str> {
self.0
.strip_prefix('/')
.unwrap_or_default()
.split('/')
.filter(|component| !component.is_empty())
}
}
impl fmt::Debug for PortablePath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("PortablePath")
.field(&self.0)
.finish()
}
}
impl fmt::Display for PortablePath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
pub enum PathError {
#[error("portable path must be absolute")]
NotAbsolute,
#[error("portable path must not end in a separator")]
TrailingSeparator,
#[error("portable path contains an empty component")]
EmptyComponent,
#[error("portable path contains a current-directory component")]
CurrentComponent,
#[error("portable path contains a parent component")]
ParentComponent,
#[error("portable path contains NUL")]
Nul,
#[error("portable path exceeds its byte bound")]
PathTooLong,
#[error("portable path component exceeds its byte bound")]
ComponentTooLong,
#[error("portable path exceeds its depth bound")]
TooDeep,
}
#[cfg(test)]
#[path = "tests/path.rs"]
mod tests;