use std::fmt;
use bytes::Bytes;
use structfs_ll_store::LLPath;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PathError {
InvalidComponent {
component: String,
position: usize,
message: String,
},
InvalidPath { message: String },
}
impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathError::InvalidComponent {
component,
position,
message,
} => {
write!(
f,
"invalid path component '{}' at position {}: {}",
component, position, message
)
}
PathError::InvalidPath { message } => {
write!(f, "invalid path: {}", message)
}
}
}
}
impl std::error::Error for PathError {}
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Path(LLPath);
#[inline]
fn component_str(component: &Bytes) -> &str {
unsafe { std::str::from_utf8_unchecked(component) }
}
fn ll_from_strings(components: Vec<String>) -> LLPath {
components
.into_iter()
.map(|s| Bytes::from(s.into_bytes()))
.collect()
}
impl Path {
pub fn parse(s: &str) -> Result<Self, PathError> {
if s.is_empty() {
return Ok(Path(LLPath::new()));
}
let components: Vec<String> = s
.split('/')
.filter(|c| !c.is_empty())
.map(|c| c.to_string())
.collect();
for (i, component) in components.iter().enumerate() {
Self::validate_component(component, i)?;
}
Ok(Path(ll_from_strings(components)))
}
pub fn from_components(components: Vec<String>) -> Self {
for (i, component) in components.iter().enumerate() {
Self::validate_component(component, i).expect("invalid component");
}
Path(ll_from_strings(components))
}
#[doc(hidden)]
pub fn from_validated_components(components: Vec<String>) -> Self {
#[cfg(debug_assertions)]
for (i, component) in components.iter().enumerate() {
Self::validate_component(component, i).expect("invalid pre-validated component");
}
Path(ll_from_strings(components))
}
pub fn try_from_components(components: Vec<String>) -> Result<Self, PathError> {
for (i, component) in components.iter().enumerate() {
Self::validate_component(component, i)?;
}
Ok(Path(ll_from_strings(components)))
}
pub fn validate_component(component: &str, position: usize) -> Result<(), PathError> {
structfs_path_validation::validate_component(component).map_err(|message| {
PathError::InvalidComponent {
component: component.to_string(),
position,
message,
}
})
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(component_str)
}
#[must_use]
pub fn join(&self, other: &Path) -> Path {
let mut components = self.0.components().to_vec();
components.extend(other.0.iter().cloned());
Path(LLPath::from_components(components))
}
#[must_use]
pub fn child(&self, component: impl Into<PathComponent>) -> Path {
let mut components = self.0.components().to_vec();
components.push(Bytes::from(component.into().into_string().into_bytes()));
Path(LLPath::from_components(components))
}
pub fn push(&mut self, component: impl Into<PathComponent>) {
self.0
.push(Bytes::from(component.into().into_string().into_bytes()));
}
pub fn has_prefix(&self, prefix: &Path) -> bool {
prefix.0.len() <= self.0.len()
&& prefix.0.components() == &self.0.components()[..prefix.0.len()]
}
#[must_use]
pub fn strip_prefix(&self, prefix: &Path) -> Option<Path> {
if self.has_prefix(prefix) {
Some(Path(LLPath::from_components(
self.0.components()[prefix.0.len()..].to_vec(),
)))
} else {
None
}
}
pub fn slice(&self, start: usize, end: usize) -> Path {
Path(LLPath::from_components(
self.0.components()[start..end].to_vec(),
))
}
pub fn as_ll(&self) -> &LLPath {
&self.0
}
pub fn into_ll(self) -> LLPath {
self.0
}
pub fn validate(ll: LLPath) -> Result<Self, PathError> {
for (i, component) in ll.iter().enumerate() {
let s = std::str::from_utf8(component.as_ref()).map_err(|_| {
PathError::InvalidComponent {
component: format!("{:?}", component.as_ref()),
position: i,
message: "not valid UTF-8".to_string(),
}
})?;
Self::validate_component(s, i)?;
}
Ok(Path(ll))
}
pub fn from_ll_unchecked(ll: LLPath) -> Self {
#[cfg(debug_assertions)]
for (i, component) in ll.iter().enumerate() {
let s = std::str::from_utf8(component.as_ref())
.expect("pre-validated LL component is not UTF-8");
Self::validate_component(s, i).expect("invalid pre-validated LL component");
}
Path(ll)
}
pub fn to_ll_path(&self) -> LLPath {
self.0.clone()
}
pub fn try_from_ll_path(ll_path: &[impl AsRef<[u8]>]) -> Result<Self, PathError> {
let ll: LLPath = ll_path
.iter()
.map(|b| Bytes::copy_from_slice(b.as_ref()))
.collect();
Self::validate(ll)
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for component in self.iter() {
if !first {
f.write_str("/")?;
}
f.write_str(component)?;
first = false;
}
Ok(())
}
}
impl std::ops::Index<usize> for Path {
type Output = str;
fn index(&self, i: usize) -> &Self::Output {
component_str(&self.0[i])
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PathComponent(String);
impl PathComponent {
pub fn try_new(s: impl Into<String>) -> Result<Self, PathError> {
let s = s.into();
Path::validate_component(&s, 0)?;
Ok(Self(s))
}
pub fn encode(s: &str) -> Self {
let encoded = namecode::encode(s);
debug_assert!(Path::validate_component(&encoded, 0).is_ok());
Self(encoded)
}
pub fn decode(&self) -> Result<String, PathError> {
match namecode::decode(&self.0) {
Ok(decoded) => Ok(decoded),
Err(namecode::DecodeError::NotEncoded) => Ok(self.0.clone()),
Err(e) => Err(PathError::InvalidComponent {
component: self.0.clone(),
position: 0,
message: format!("malformed namecode encoding: {}", e),
}),
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn validated_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for PathComponent {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for PathComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<usize> for PathComponent {
fn from(i: usize) -> Self {
Self(i.to_string())
}
}
impl From<u64> for PathComponent {
fn from(i: u64) -> Self {
Self(i.to_string())
}
}
impl From<PathComponent> for Path {
fn from(c: PathComponent) -> Self {
Path(LLPath::from_components(vec![Bytes::from(
c.into_string().into_bytes(),
)]))
}
}
impl FromIterator<PathComponent> for Path {
fn from_iter<I: IntoIterator<Item = PathComponent>>(iter: I) -> Self {
Path(
iter.into_iter()
.map(|c| Bytes::from(c.into_string().into_bytes()))
.collect(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::path;
#[test]
fn parse_basic_paths() {
assert_eq!(Path::parse("").unwrap().len(), 0);
assert_eq!(Path::parse("foo").unwrap().len(), 1);
assert_eq!(Path::parse("foo/bar").unwrap().len(), 2);
assert_eq!(Path::parse("foo/bar/baz").unwrap().len(), 3);
}
#[test]
fn normalize_slashes() {
assert_eq!(
Path::parse("foo/bar/").unwrap(),
Path::parse("foo/bar").unwrap()
);
assert_eq!(
Path::parse("foo//bar").unwrap(),
Path::parse("foo/bar").unwrap()
);
assert_eq!(
Path::parse("/foo/bar").unwrap(),
Path::parse("foo/bar").unwrap()
);
}
#[test]
fn numeric_components_allowed() {
let p = Path::parse("items/0/name").unwrap();
assert_eq!(p.len(), 3);
assert_eq!(&p[1], "0");
}
#[test]
fn unicode_identifiers_allowed() {
let p = Path::parse("usuarios/名前").unwrap();
assert_eq!(p.len(), 2);
}
#[test]
fn invalid_components_rejected() {
assert!(Path::parse("foo/bar baz").is_err()); assert!(Path::parse("foo/bar-baz").is_err()); assert!(Path::parse("foo/.hidden").is_err()); assert!(Path::parse("foo/123abc").is_err()); }
#[test]
fn has_prefix_works() {
let p = path!("foo/bar/baz");
assert!(p.has_prefix(&path!("")));
assert!(p.has_prefix(&path!("foo")));
assert!(p.has_prefix(&path!("foo/bar")));
assert!(p.has_prefix(&path!("foo/bar/baz")));
assert!(!p.has_prefix(&path!("bar")));
assert!(!p.has_prefix(&path!("foo/bar/baz/qux")));
}
#[test]
fn strip_prefix_works() {
let p = path!("foo/bar/baz");
assert_eq!(p.strip_prefix(&path!("foo")), Some(path!("bar/baz")));
assert_eq!(p.strip_prefix(&path!("foo/bar")), Some(path!("baz")));
assert_eq!(p.strip_prefix(&path!("other")), None);
}
#[test]
fn ll_conversion_roundtrips() {
let p = path!("users/123/name");
let ll = p.to_ll_path();
let p2 = Path::try_from_ll_path(&ll.iter().collect::<Vec<_>>()).unwrap();
assert_eq!(p, p2);
}
#[test]
fn path_error_display_invalid_component() {
let err = PathError::InvalidComponent {
component: "bad-name".to_string(),
position: 2,
message: "test message".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("bad-name"));
assert!(display.contains("position 2"));
assert!(display.contains("test message"));
}
#[test]
fn path_error_display_invalid_path() {
let err = PathError::InvalidPath {
message: "some reason".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("invalid path"));
assert!(display.contains("some reason"));
}
#[test]
fn path_error_is_error() {
let err: Box<dyn std::error::Error> = Box::new(PathError::InvalidPath {
message: "test".to_string(),
});
let _ = err.to_string();
}
#[test]
fn from_components_valid() {
let p = Path::from_components(vec!["foo".to_string(), "bar".to_string()]);
assert_eq!(p.len(), 2);
}
#[test]
#[should_panic(expected = "invalid component")]
fn from_components_invalid_panics() {
Path::from_components(vec!["foo".to_string(), "bad-name".to_string()]);
}
#[test]
fn try_from_components_valid() {
let p = Path::try_from_components(vec!["foo".to_string(), "bar".to_string()]).unwrap();
assert_eq!(p.len(), 2);
}
#[test]
fn try_from_components_invalid() {
let result = Path::try_from_components(vec!["foo".to_string(), "bad-name".to_string()]);
assert!(result.is_err());
}
#[test]
fn validate_empty_component_rejected() {
let result = Path::try_from_components(vec!["".to_string()]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("empty component"));
}
#[test]
fn validate_underscore_alone_rejected() {
let result = Path::parse("_");
assert!(result.is_err());
}
#[test]
fn validate_underscore_with_continuation_allowed() {
let p = Path::parse("_foo").unwrap();
assert_eq!(p.len(), 1);
}
#[test]
fn validate_invalid_character_in_middle() {
let result = Path::parse("foo$bar");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("invalid character"));
}
#[test]
fn index_trait() {
let p = path!("foo/bar/baz");
assert_eq!(&p[0], "foo");
assert_eq!(&p[1], "bar");
assert_eq!(&p[2], "baz");
}
#[test]
fn slice_method() {
let p = path!("a/b/c/d");
let sliced = p.slice(1, 3);
assert_eq!(sliced.len(), 2);
assert_eq!(sliced.to_string(), "b/c");
}
#[test]
fn join_method() {
let p1 = path!("foo/bar");
let p2 = path!("baz/qux");
let joined = p1.join(&p2);
assert_eq!(joined.to_string(), "foo/bar/baz/qux");
}
#[test]
fn join_with_empty() {
let p1 = path!("foo");
let p2 = path!("");
assert_eq!(p1.join(&p2), p1);
let p3 = path!("");
let p4 = path!("bar");
assert_eq!(p3.join(&p4), p4);
}
#[test]
fn iter_method() {
let p = path!("a/b/c");
let components: Vec<&str> = p.iter().collect();
assert_eq!(components.len(), 3);
assert_eq!(components[0], "a");
assert_eq!(components[1], "b");
assert_eq!(components[2], "c");
}
#[test]
fn is_empty() {
assert!(path!("").is_empty());
assert!(!path!("foo").is_empty());
}
#[test]
fn display_impl() {
let p = path!("foo/bar/baz");
assert_eq!(format!("{}", p), "foo/bar/baz");
}
#[test]
fn display_empty() {
let p = path!("");
assert_eq!(format!("{}", p), "");
}
#[test]
fn ll_conversion_invalid_utf8() {
let invalid_utf8: Vec<&[u8]> = vec![&[0xff, 0xfe]];
let result = Path::try_from_ll_path(&invalid_utf8);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("not valid UTF-8"));
}
#[test]
fn path_ord() {
let p1 = path!("a/b");
let p2 = path!("a/c");
let p3 = path!("b/a");
assert!(p1 < p2);
assert!(p2 < p3);
}
#[test]
fn path_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(path!("foo"));
set.insert(path!("bar"));
set.insert(path!("foo")); assert_eq!(set.len(), 2);
}
#[test]
fn macro_component_style() {
let p = path!("users", 123, "name");
assert_eq!(p.to_string(), "users/123/name");
assert_eq!(p, path!("users/123/name"));
}
#[test]
fn macro_empty() {
let p = path!();
assert!(p.is_empty());
}
#[test]
fn macro_with_runtime_component() {
let name = PathComponent::try_new("alice").unwrap();
let p = path!("users", name, "profile");
assert_eq!(p.to_string(), "users/alice/profile");
}
#[test]
fn macro_mixed_literal_forms() {
let p = path!("a/b", "c");
assert_eq!(p.to_string(), "a/b/c");
}
#[test]
fn path_component_validates() {
assert!(PathComponent::try_new("accounts").is_ok());
assert!(PathComponent::try_new("42").is_ok());
assert!(PathComponent::try_new("café").is_ok());
assert!(PathComponent::try_new("_private").is_ok());
assert!(PathComponent::try_new("").is_err());
assert!(PathComponent::try_new("my-account").is_err());
assert!(PathComponent::try_new("my account").is_err());
assert!(PathComponent::try_new(".hidden").is_err());
assert!(PathComponent::try_new("_").is_err());
assert!(PathComponent::try_new("a/b").is_err());
}
#[test]
fn path_component_encode_roundtrip() {
for original in [
"plain",
"my-account",
"hello world",
"slashes/and spaces",
"oxide-🦀",
"123-456",
] {
let component = PathComponent::encode(original);
assert!(PathComponent::try_new(component.as_str()).is_ok());
assert_eq!(component.decode().unwrap(), original);
}
}
#[test]
fn path_component_encode_passthrough() {
let component = PathComponent::encode("plain");
assert_eq!(component.as_str(), "plain");
assert_eq!(component.decode().unwrap(), "plain");
}
#[test]
fn path_component_from_index() {
let c: PathComponent = 7usize.into();
assert_eq!(c.as_str(), "7");
let c: PathComponent = 7u64.into();
assert_eq!(c.as_str(), "7");
}
#[test]
fn child_and_push() {
let base = path!("users");
let p = base.child(PathComponent::try_new("alice").unwrap());
assert_eq!(p.to_string(), "users/alice");
let mut p2 = path!("items");
p2.push(3usize);
assert_eq!(p2.to_string(), "items/3");
}
#[test]
fn path_from_component_iter() {
let p: Path = ["a", "b", "c"]
.iter()
.map(|s| PathComponent::try_new(*s).unwrap())
.collect();
assert_eq!(p.to_string(), "a/b/c");
}
#[test]
fn validate_component_public() {
assert!(Path::validate_component("foo", 0).is_ok());
let err = Path::validate_component("bad-name", 2).unwrap_err();
assert!(err.to_string().contains("position 2"));
}
#[test]
fn path_clone() {
let p1 = path!("foo/bar");
let p2 = p1.clone();
assert_eq!(p1, p2);
}
#[test]
fn path_debug() {
let p = path!("foo/bar");
let debug = format!("{:?}", p);
assert!(debug.contains("foo"));
assert!(debug.contains("bar"));
}
#[test]
fn as_ll_and_into_ll_widen_losslessly() {
let p = path!("users/123/name");
let ll = p.as_ll();
assert_eq!(ll.len(), 3);
assert_eq!(ll[0].as_ref(), b"users");
assert_eq!(ll[2].as_ref(), b"name");
assert_eq!(p.clone().into_ll(), ll.clone());
}
#[test]
fn validate_narrows_and_rejects() {
let ll: LLPath = [Bytes::from_static(b"a"), Bytes::from_static(b"b")]
.into_iter()
.collect();
assert_eq!(Path::validate(ll).unwrap(), path!("a/b"));
let bad: LLPath = [Bytes::from_static(b"a-b")].into_iter().collect();
assert!(Path::validate(bad).is_err());
let non_utf8: LLPath = [Bytes::from_static(&[0xff, 0xfe])].into_iter().collect();
assert!(Path::validate(non_utf8).is_err());
}
#[test]
fn widen_then_narrow_roundtrips() {
let p = path!("users/名前/0");
assert_eq!(Path::validate(p.clone().into_ll()).unwrap(), p);
assert_eq!(Path::from_ll_unchecked(p.clone().into_ll()), p);
}
}