Skip to main content

io_m2dir/
store.rs

1//! Root m2store directory containing one or more m2dirs.
2
3use 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
10/// Bytes percent-encoded by m2dir folder names: the path separators
11/// (`/`, `\`), the escape char (`%`), plus all control bytes; non-ASCII
12/// codepoints are always encoded by `utf8_percent_encode`.
13const M2DIR_PCT: &AsciiSet = &CONTROLS.add(b'%').add(b'/').add(b'\\');
14
15/// Marker filename written at the root of every m2store.
16pub const DOT_M2STORE: &str = ".m2store";
17
18/// Filename or symlink at the m2store root identifying the delivery
19/// target m2dir.
20pub const DOT_DELIVERY: &str = ".delivery";
21
22/// Errors that can occur while operating on an m2store.
23#[derive(Clone, Debug, Error)]
24pub enum M2dirStoreError {
25    /// The given path is not a directory.
26    #[error("path {0} is not a directory")]
27    NotDir(M2dirPath),
28    /// The given directory does not contain the `.m2store` marker.
29    #[error("no valid `.m2store` marker found in directory {0}")]
30    NoDotM2store(M2dirPath),
31    /// The given folder name resolves to an absolute path.
32    #[error("folder path {0} must be relative")]
33    AbsolutePath(String),
34    /// The given folder name contains components that fall outside
35    /// the m2store root (such as `..`).
36    #[error("folder path {0} escapes m2store root")]
37    EscapesRoot(String),
38}
39
40/// Root m2store directory holding one or more [`crate::m2dir::types::M2dir`]s.
41#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
42pub struct M2dirStore {
43    path: M2dirPath,
44}
45
46impl M2dirStore {
47    /// Builds an [`M2dirStore`] from a path without checking the marker.
48    pub fn from_path(path: impl Into<M2dirPath>) -> Self {
49        Self { path: path.into() }
50    }
51
52    /// Returns the path to the m2store root directory.
53    pub fn path(&self) -> &M2dirPath {
54        &self.path
55    }
56
57    /// Returns the path to the `.m2store` marker file.
58    pub fn marker_path(&self) -> M2dirPath {
59        self.path.join(DOT_M2STORE)
60    }
61
62    /// Returns the path to the `.delivery` entry.
63    pub fn delivery_path(&self) -> M2dirPath {
64        self.path.join(DOT_DELIVERY)
65    }
66
67    /// Resolves a folder name (relative path, components percent
68    /// encoded per the m2dir specification) to its on-disk path
69    /// inside this store.
70    ///
71    /// Returns an error if `name` is absolute or escapes the store
72    /// root.
73    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    /// Decodes a path inside the store back to its UTF-8 folder name.
97    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}