Skip to main content

io_m2dir/
m2dir.rs

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