use std::fmt;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct BundleRelativePath(PathBuf);
impl BundleRelativePath {
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
impl FromStr for BundleRelativePath {
type Err = BundlePathError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.is_empty() || value.contains(['\0', '\\']) {
return Err(BundlePathError::Invalid(value.to_owned()));
}
let path = PathBuf::from(value);
let all_normal = path
.components()
.all(|component| matches!(component, Component::Normal(_)));
let windows_prefix = value
.split('/')
.next()
.is_some_and(|segment| segment.contains(':'));
if !all_normal || windows_prefix {
return Err(BundlePathError::Invalid(value.to_owned()));
}
Ok(Self(path))
}
}
impl TryFrom<String> for BundleRelativePath {
type Error = BundlePathError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::from_str(&value)
}
}
impl From<BundleRelativePath> for String {
fn from(value: BundleRelativePath) -> Self {
value.to_string()
}
}
impl fmt::Display for BundleRelativePath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0.to_string_lossy())
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BundlePathError {
#[error("invalid bundle-relative path: {0}")]
Invalid(String),
}