Skip to main content

io_m2dir/m2dir/
types.rs

1//! Single m2dir directory on the filesystem.
2
3use core::hash::{Hash, Hasher};
4
5use alloc::{
6    format,
7    string::{String, ToString},
8};
9
10use base64::{Engine, engine::general_purpose::URL_SAFE};
11use thiserror::Error;
12
13use crate::{entry::utils::write_checksum, m2dir::utils::extract_date, path::M2dirPath};
14
15/// Epoch timestamp used as the filename date prefix when the message
16/// has no parseable `Date:` header. Matches the original behaviour of
17/// falling back to `Datetime::default()` when extraction failed.
18const EPOCH_DATE: &str = "1970-01-01T00:00:00Z";
19
20/// Marker filename written into every m2dir.
21pub const DOT_M2DIR: &str = ".m2dir";
22
23/// Metadata subdirectory inside an m2dir.
24pub const META: &str = ".meta";
25
26/// Errors that can occur while opening an existing m2dir.
27#[derive(Clone, Debug, Error)]
28pub enum LoadM2dirError {
29    /// The given path is not a directory.
30    #[error("path {0} is not a directory")]
31    NotDir(M2dirPath),
32
33    /// The given directory does not contain the `.m2dir` marker.
34    #[error("no valid `.m2dir` marker found in directory {0}")]
35    NoDotM2dir(M2dirPath),
36}
37
38/// A single m2dir directory on the filesystem.
39///
40/// Holds the root path and provides helpers to derive entry paths,
41/// metadata paths, and a new filename for a delivery.
42#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
43pub struct M2dir {
44    path: M2dirPath,
45}
46
47impl M2dir {
48    /// Builds an [`M2dir`] from a path without checking the marker.
49    pub fn from_path(path: impl Into<M2dirPath>) -> Self {
50        Self { path: path.into() }
51    }
52
53    /// Returns the path to the m2dir directory.
54    pub fn path(&self) -> &M2dirPath {
55        &self.path
56    }
57
58    /// Returns the path to the `.m2dir` marker file.
59    pub fn marker_path(&self) -> M2dirPath {
60        self.path.join(DOT_M2DIR)
61    }
62
63    /// Returns the path to the `.meta` directory.
64    pub fn meta_dir(&self) -> M2dirPath {
65        self.path.join(META)
66    }
67
68    /// Returns the path to the `.flags` metadata file for the given
69    /// entry id.
70    pub fn flags_path(&self, id: &str) -> M2dirPath {
71        self.meta_dir().join(&format!("{id}.flags"))
72    }
73
74    /// Computes the filename and final on-disk path for a new entry
75    /// holding `bytes`. The filename is `<date>,<checksum>.<nonce>`
76    /// per the m2dir specification.
77    ///
78    /// `nonce_bytes` should be 4 freshly-generated random bytes
79    /// supplied by the caller.
80    pub fn entry_path(&self, bytes: &[u8], nonce_bytes: &[u8]) -> (String, M2dirPath) {
81        let mut checksum = String::new();
82        write_checksum(bytes, &mut checksum).expect("base64 encoding to a string is always valid");
83
84        let dt = extract_date(bytes).unwrap_or_else(|| EPOCH_DATE.to_string());
85
86        let nonce = URL_SAFE.encode(nonce_bytes);
87
88        let id = format!("{checksum}.{nonce}");
89        let filename = format!("{dt},{id}");
90        let path = self.path.join(&filename);
91
92        (id, path)
93    }
94
95    /// Returns the path of a temporary file inside this m2dir, used
96    /// during the write-then-rename delivery sequence.
97    pub fn tmp_path(&self, pid: u32, counter: u32) -> M2dirPath {
98        self.path.join(&format!(".m2dir.tmp.{pid:x}{counter:x}"))
99    }
100
101    /// Splits a filename into its `<checksum>.<nonce>` tail (used as
102    /// the entry id).
103    pub fn parse_filename_id(filename: &str) -> Option<&str> {
104        let (_, id) = filename.rsplit_once(',')?;
105        Some(id)
106    }
107}
108
109impl Hash for M2dir {
110    fn hash<H: Hasher>(&self, state: &mut H) {
111        self.path.hash(state);
112    }
113}
114
115impl AsRef<M2dirPath> for M2dir {
116    fn as_ref(&self) -> &M2dirPath {
117        &self.path
118    }
119}
120
121impl AsRef<str> for M2dir {
122    fn as_ref(&self) -> &str {
123        self.path.as_str()
124    }
125}
126
127impl From<M2dirPath> for M2dir {
128    fn from(path: M2dirPath) -> Self {
129        Self { path }
130    }
131}
132
133impl From<String> for M2dir {
134    fn from(path: String) -> Self {
135        Self { path: path.into() }
136    }
137}
138
139impl From<&str> for M2dir {
140    fn from(path: &str) -> Self {
141        Self {
142            path: path.to_string().into(),
143        }
144    }
145}