use std::path::PathBuf;
use derive_more::Display;
use thiserror::Error;
use super::NodeName;
use zarrs_storage::{StorePrefix, StorePrefixError};
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display)]
#[display("{}", _0.to_string_lossy())]
pub struct NodePath(PathBuf);
#[derive(Clone, Debug, Error)]
#[error("invalid node path {0}")]
pub struct NodePathError(String);
impl NodePath {
pub fn new(path: &str) -> Result<Self, NodePathError> {
if Self::validate(path) {
Ok(Self(PathBuf::from(path)))
} else {
Err(NodePathError(path.to_string()))
}
}
#[must_use]
pub fn root() -> Self {
Self(PathBuf::from("/"))
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn as_str(&self) -> &str {
self.0.to_str().unwrap()
}
#[must_use]
pub fn as_path(&self) -> &std::path::Path {
&self.0
}
#[must_use]
pub fn validate(path: &str) -> bool {
if path == "/" {
return true;
}
if !path.starts_with('/') || path.ends_with('/') {
return false;
}
for component in path[1..].split('/') {
if component.is_empty() || !NodeName::validate(component) {
return false;
}
}
true
}
#[must_use]
pub fn is_root(&self) -> bool {
self.0.as_os_str() == "/"
}
#[must_use]
pub fn parent(&self) -> Option<Self> {
let s = self.as_str();
let (parent, child) = s.rsplit_once('/')?;
if child.is_empty() {
None
} else if parent.is_empty() {
Some(Self::root())
} else {
Self::new(parent).ok()
}
}
pub fn join(&self, relative: &str) -> Result<Self, NodePathError> {
let relative = relative.strip_prefix('/').unwrap_or(relative);
if relative.is_empty() {
return Ok(self.clone());
}
for component in relative.split('/') {
if component.is_empty() || !NodeName::validate(component) {
return Err(NodePathError(format!(
"invalid node name `{component}` in '{relative}'"
)));
}
}
let joined = if self.is_root() {
format!("/{}", relative)
} else {
format!("{}/{}", self.as_str(), relative)
};
Self::new(&joined)
}
}
impl TryFrom<&str> for NodePath {
type Error = NodePathError;
fn try_from(path: &str) -> Result<Self, Self::Error> {
Self::new(path)
}
}
impl TryFrom<&StorePrefix> for NodePath {
type Error = NodePathError;
fn try_from(prefix: &StorePrefix) -> Result<Self, Self::Error> {
let path = "/".to_string() + prefix.as_str().strip_suffix('/').unwrap();
Self::new(&path)
}
}
impl TryInto<StorePrefix> for &NodePath {
type Error = StorePrefixError;
fn try_into(self) -> Result<StorePrefix, StorePrefixError> {
let path = self.as_str();
if path.eq("/") {
StorePrefix::new("")
} else {
StorePrefix::new(path.strip_prefix('/').unwrap_or(path).to_string() + "/")
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn node_path() {
assert!(NodePath::new("/").is_ok());
assert!(NodePath::new("/a/b").is_ok());
assert_eq!(NodePath::new("/a/b").unwrap().to_string(), "/a/b");
assert!(NodePath::new("/a/b/").is_err());
assert_eq!(
NodePath::new("/a/b/").unwrap_err().to_string(),
"invalid node path /a/b/"
);
assert!(NodePath::new("/a//b").is_err());
assert_eq!(NodePath::new("/a/b").unwrap().as_path(), Path::new("/a/b/"));
}
#[test]
fn node_path_is_root() {
assert!(NodePath::root().is_root());
assert!(NodePath::new("/").unwrap().is_root());
assert!(!NodePath::new("/foo").unwrap().is_root());
assert!(!NodePath::new("/a/b/c").unwrap().is_root());
}
#[test]
fn node_path_validate_rejects_invalid_components() {
assert!(!NodePath::validate("/__foo"));
assert!(!NodePath::validate("/foo/__bar"));
assert!(!NodePath::validate("/foo/__bar/baz"));
assert!(!NodePath::validate("/."));
assert!(!NodePath::validate("/.."));
assert!(!NodePath::validate("/foo/../bar"));
assert!(!NodePath::validate("/..."));
assert!(!NodePath::validate("////"));
assert!(NodePath::validate("/"));
assert!(NodePath::validate("/a/b"));
assert!(NodePath::validate("/foo__bar")); assert!(NodePath::validate("/foo./bar")); assert!(NodePath::validate("/foo/bar..")); }
#[test]
fn node_path_join() {
let root = NodePath::root();
assert_eq!(root.join("foo").unwrap().as_str(), "/foo");
assert_eq!(root.join("sub/leaf").unwrap().as_str(), "/sub/leaf");
assert_eq!(root.join("/foo").unwrap().as_str(), "/foo");
assert_eq!(root.join("").unwrap(), root);
let base = NodePath::new("/group").unwrap();
assert_eq!(base.join("sub").unwrap().as_str(), "/group/sub");
assert_eq!(base.join("sub/leaf").unwrap().as_str(), "/group/sub/leaf");
assert_eq!(base.join("/sub").unwrap().as_str(), "/group/sub");
assert!(NodePath::new("/foo").unwrap().join("./bar").is_err());
assert!(root.join("./bar").is_err());
assert!(root.join("../bar").is_err());
assert!(base.join("sub/../other").is_err());
assert!(base.join("../other").is_err());
assert!(root.join(".../bar").is_err());
assert!(root.join("foo//bar").is_err());
assert!(root.join("foo/").is_err());
assert!(root.join("__bar").is_err());
assert!(root.join("foo/__bar").is_err());
}
#[test]
fn node_path_join_valid_prefixes() {
assert_eq!(
NodePath::root().join("foo__bar").unwrap().as_str(),
"/foo__bar"
);
assert_eq!(
NodePath::root().join("foo__bar/baz").unwrap().as_str(),
"/foo__bar/baz"
);
assert_eq!(NodePath::root().join("foo.").unwrap().as_str(), "/foo.");
assert_eq!(NodePath::root().join("..foo").unwrap().as_str(), "/..foo");
assert_eq!(NodePath::root().join("foo..").unwrap().as_str(), "/foo..");
}
#[test]
fn node_path_parent() {
assert!(NodePath::root().parent().is_none());
assert_eq!(
NodePath::new("/foo").unwrap().parent().unwrap().as_str(),
"/"
);
assert_eq!(
NodePath::new("/foo/bar")
.unwrap()
.parent()
.unwrap()
.as_str(),
"/foo"
);
assert_eq!(
NodePath::new("/foo/bar/baz")
.unwrap()
.parent()
.unwrap()
.as_str(),
"/foo/bar"
);
}
}