use alloc::string::{String, ToString};
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
use thiserror::Error;
use crate::path::M2dirPath;
const M2DIR_PCT: &AsciiSet = &CONTROLS.add(b'%').add(b'/').add(b'\\');
pub const DOT_M2STORE: &str = ".m2store";
pub const DOT_DELIVERY: &str = ".delivery";
#[derive(Clone, Debug, Error)]
pub enum M2dirStoreError {
#[error("path {0} is not a directory")]
NotDir(M2dirPath),
#[error("no valid `.m2store` marker found in directory {0}")]
NoDotM2store(M2dirPath),
#[error("folder path {0} must be relative")]
AbsolutePath(String),
#[error("folder path {0} escapes m2store root")]
EscapesRoot(String),
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct M2dirStore {
path: M2dirPath,
}
impl M2dirStore {
pub fn from_path(path: impl Into<M2dirPath>) -> Self {
Self { path: path.into() }
}
pub fn path(&self) -> &M2dirPath {
&self.path
}
pub fn marker_path(&self) -> M2dirPath {
self.path.join(DOT_M2STORE)
}
pub fn delivery_path(&self) -> M2dirPath {
self.path.join(DOT_DELIVERY)
}
pub fn resolve_folder_path(&self, name: &str) -> Result<M2dirPath, M2dirStoreError> {
if name.starts_with('/') || name.starts_with('\\') {
return Err(M2dirStoreError::AbsolutePath(name.to_string()));
}
let mut resolved = self.path.clone();
for raw in name.split(|c| c == '/' || c == '\\') {
match raw {
"" | "." => {}
".." => {
return Err(M2dirStoreError::EscapesRoot(name.to_string()));
}
part => {
let encoded = utf8_percent_encode(part, M2DIR_PCT).to_string();
resolved.push(&encoded);
}
}
}
Ok(resolved)
}
pub fn decode_folder_name(&self, path: &M2dirPath) -> Option<String> {
let rel = path.strip_prefix(&self.path)?;
percent_decode_str(rel)
.decode_utf8()
.ok()
.map(|s| s.into_owned())
}
}
impl AsRef<M2dirPath> for M2dirStore {
fn as_ref(&self) -> &M2dirPath {
&self.path
}
}
impl AsRef<str> for M2dirStore {
fn as_ref(&self) -> &str {
self.path.as_str()
}
}
impl From<M2dirPath> for M2dirStore {
fn from(path: M2dirPath) -> Self {
Self { path }
}
}