1use alloc::string::{String, ToString};
4
5use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
6use thiserror::Error;
7
8use crate::path::M2dirPath;
9
10const M2DIR_PCT: &AsciiSet = &CONTROLS.add(b'%').add(b'/').add(b'\\');
14
15pub const DOT_M2STORE: &str = ".m2store";
17
18pub const DOT_DELIVERY: &str = ".delivery";
21
22#[derive(Clone, Debug, Error)]
24pub enum M2dirStoreError {
25 #[error("path {0} is not a directory")]
27 NotDir(M2dirPath),
28 #[error("no valid `.m2store` marker found in directory {0}")]
30 NoDotM2store(M2dirPath),
31 #[error("folder path {0} must be relative")]
33 AbsolutePath(String),
34 #[error("folder path {0} escapes m2store root")]
37 EscapesRoot(String),
38}
39
40#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
42pub struct M2dirStore {
43 path: M2dirPath,
44}
45
46impl M2dirStore {
47 pub fn from_path(path: impl Into<M2dirPath>) -> Self {
49 Self { path: path.into() }
50 }
51
52 pub fn path(&self) -> &M2dirPath {
54 &self.path
55 }
56
57 pub fn marker_path(&self) -> M2dirPath {
59 self.path.join(DOT_M2STORE)
60 }
61
62 pub fn delivery_path(&self) -> M2dirPath {
64 self.path.join(DOT_DELIVERY)
65 }
66
67 pub fn resolve_folder_path(&self, name: &str) -> Result<M2dirPath, M2dirStoreError> {
74 if name.starts_with('/') || name.starts_with('\\') {
75 return Err(M2dirStoreError::AbsolutePath(name.to_string()));
76 }
77
78 let mut resolved = self.path.clone();
79
80 for raw in name.split(|c| c == '/' || c == '\\') {
81 match raw {
82 "" | "." => {}
83 ".." => {
84 return Err(M2dirStoreError::EscapesRoot(name.to_string()));
85 }
86 part => {
87 let encoded = utf8_percent_encode(part, M2DIR_PCT).to_string();
88 resolved.push(&encoded);
89 }
90 }
91 }
92
93 Ok(resolved)
94 }
95
96 pub fn decode_folder_name(&self, path: &M2dirPath) -> Option<String> {
98 let rel = path.strip_prefix(&self.path)?;
99 percent_decode_str(rel)
100 .decode_utf8()
101 .ok()
102 .map(|s| s.into_owned())
103 }
104}
105
106impl AsRef<M2dirPath> for M2dirStore {
107 fn as_ref(&self) -> &M2dirPath {
108 &self.path
109 }
110}
111
112impl AsRef<str> for M2dirStore {
113 fn as_ref(&self) -> &str {
114 self.path.as_str()
115 }
116}
117
118impl From<M2dirPath> for M2dirStore {
119 fn from(path: M2dirPath) -> Self {
120 Self { path }
121 }
122}