Skip to main content

vomit_m2dir/
flags.rs

1use std::collections::BTreeSet;
2use std::ffi::OsString;
3use std::fmt::{self, Display};
4use std::fs::File;
5use std::io::{self, Write};
6use std::ops::{Deref, DerefMut};
7use std::path::{Path, PathBuf};
8
9/// Errors that can occur while handling flags.
10#[derive(thiserror::Error, Debug)]
11pub enum Error {
12    #[error("invalid flag name: {0}")]
13    InvalidFlagName(String),
14    #[error("I/O error: {0}")]
15    IOError(#[from] std::io::Error),
16}
17
18/// Represents a single flag.
19///
20/// Can be either one of a set of spec-defined flags, or a custom value.
21///
22/// Instances of custom flags should always be created via [`Flag::try_from`],
23/// to ensure that the flag does not contain invalid characters (newline) and is
24/// not the empty string.
25#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
26pub enum Flag {
27    Seen,
28    Answered,
29    Forwarded,
30    Flagged,
31    Deleted,
32    Draft,
33    Important,
34    MDNSent,
35    Junk,
36    NotJunk,
37    Phishing,
38    Custom(String),
39}
40
41impl TryFrom<&str> for Flag {
42    type Error = Error;
43
44    fn try_from(flag: &str) -> Result<Self, Error> {
45        Ok(match flag {
46            "$seen" => Flag::Seen,
47            "$answered" => Flag::Answered,
48            "$Forwarded" => Flag::Forwarded,
49            "$flagged" => Flag::Flagged,
50            "$Deleted" => Flag::Deleted,
51            "$draft" => Flag::Draft,
52            "$Important" => Flag::Important,
53            "$MDNSent" => Flag::MDNSent,
54            "$Junk" => Flag::Junk,
55            "$NotJunk" => Flag::NotJunk,
56            "$Phishing" => Flag::Phishing,
57            _ => {
58                if flag.is_empty() || flag.contains('\n') {
59                    return Err(Error::InvalidFlagName(flag.to_string()));
60                }
61                Flag::Custom(flag.to_string())
62            }
63        })
64    }
65}
66
67impl Display for Flag {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        let s = match self {
70            Flag::Seen => "$seen",
71            Flag::Answered => "$answered",
72            Flag::Forwarded => "$Forwarded",
73            Flag::Flagged => "$flagged",
74            Flag::Deleted => "$Deleted",
75            Flag::Draft => "$draft",
76            Flag::Important => "$Important",
77            Flag::MDNSent => "$MDNSent",
78            Flag::Junk => "$Junk",
79            Flag::NotJunk => "$NotJunk",
80            Flag::Phishing => "$Phishing",
81            Flag::Custom(c) => c,
82        };
83        write!(f, "{}", s)
84    }
85}
86
87/// A set of [`Flag`]s.
88#[derive(Debug, Eq, PartialEq, PartialOrd)]
89pub struct Flags(BTreeSet<Flag>);
90
91impl Flags {
92    pub fn new() -> Self {
93        Flags(BTreeSet::new())
94    }
95
96    /// Parse a set of flags from a string.
97    ///
98    /// The flags must be newline-separated.
99    pub fn parse_string(s: &str) -> Result<Self, Error> {
100        if s.is_empty() {
101            return Ok(Flags::new());
102        }
103        s.trim_end().split('\n').map(Flag::try_from).collect()
104    }
105
106    /// Parse a set of flags from a file.
107    ///
108    /// The file must be a valid m2dir [flags] file.
109    ///
110    /// [flags]: https://man.sr.ht/~bitfehler/m2dir/#flags
111    pub fn parse_file(f: File) -> Result<Self, Error> {
112        // If read as stream, we'd have to deal with the trailing newline
113        // It's just the flags, so read it all and have parse_string handle it
114        let s = io::read_to_string(&f)?;
115        Flags::parse_string(&s)
116    }
117
118    /// Write a set of flags to a string.
119    ///
120    /// The flags will be newline-separated, meaning the output is suitable to
121    /// write a valid m2dir [flags] file. For display purposes, use the
122    /// implementation of [`Display`] instead.
123    ///
124    /// [flags]: https://man.sr.ht/~bitfehler/m2dir/#flags
125    pub fn write_string(&self) -> String {
126        let mut result = String::new();
127        for flag in &self.0 {
128            result.push_str(&format!("{}\n", flag));
129        }
130        result
131    }
132
133    /// Write a set of flags to a file.
134    ///
135    /// The file will be a valid m2dir [flags] file.
136    ///
137    /// [flags]: https://man.sr.ht/~bitfehler/m2dir/#flags
138    #[allow(clippy::write_with_newline)]
139    pub fn write_file(&self, f: &mut impl Write) -> io::Result<()> {
140        for flag in &self.0 {
141            write!(f, "{}\n", flag)?;
142        }
143        f.flush()
144    }
145}
146
147impl Default for Flags {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl Deref for Flags {
154    type Target = BTreeSet<Flag>;
155    fn deref(&self) -> &Self::Target {
156        &self.0
157    }
158}
159
160impl DerefMut for Flags {
161    fn deref_mut(&mut self) -> &mut Self::Target {
162        &mut self.0
163    }
164}
165
166impl<const N: usize> std::convert::From<[Flag; N]> for Flags {
167    fn from(arr: [Flag; N]) -> Self {
168        Flags(BTreeSet::from(arr))
169    }
170}
171
172impl std::convert::From<&BTreeSet<Flag>> for Flags {
173    fn from(set: &BTreeSet<Flag>) -> Self {
174        let mut c = BTreeSet::new();
175        for i in set {
176            c.insert(i.clone());
177        }
178        Flags(c)
179    }
180}
181
182impl FromIterator<Flag> for Flags {
183    fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
184        let mut set = BTreeSet::new();
185        for i in iter {
186            set.insert(i);
187        }
188        Flags(set)
189    }
190}
191
192impl IntoIterator for Flags {
193    type Item = Flag;
194    type IntoIter = <BTreeSet<Flag> as IntoIterator>::IntoIter;
195    fn into_iter(self) -> Self::IntoIter {
196        self.0.into_iter()
197    }
198}
199
200impl fmt::Display for Flags {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        let mut first = true;
203
204        write!(f, "(")?;
205        for flag in &self.0 {
206            if !first {
207                write!(f, ", {}", flag)?;
208            } else {
209                write!(f, "{}", flag)?;
210                first = false;
211            }
212        }
213        write!(f, ")")?;
214        Ok(())
215    }
216}
217
218pub(crate) fn flags_path_for(dir: impl AsRef<Path>, id: &str) -> PathBuf {
219    PathBuf::from_iter([
220        dir.as_ref().as_os_str(),
221        &OsString::from(".meta"),
222        &OsString::from(format!("{}.flags", id)),
223    ])
224}
225
226#[cfg(test)]
227mod tests {
228    use tempfile::NamedTempFile;
229
230    use super::*;
231
232    #[test]
233    fn test_flag() {
234        assert_eq!(Flag::try_from("$seen").unwrap(), Flag::Seen);
235        assert_eq!(Flag::try_from("$Forwarded").unwrap(), Flag::Forwarded);
236        assert_eq!(
237            Flag::try_from("myflag").unwrap(),
238            Flag::Custom(String::from("myflag"))
239        );
240        assert!(Flag::try_from("my\nflag").is_err());
241        assert!(Flag::try_from("").is_err());
242    }
243
244    #[test]
245    fn test_flags() {
246        let set = Flags::from([Flag::Answered]);
247        assert_eq!(set.to_string(), String::from("($answered)"));
248
249        let set = Flags::from([Flag::Forwarded, Flag::Answered]);
250        assert_eq!(set.to_string(), String::from("($answered, $Forwarded)"));
251
252        let set = Flags::from([
253            Flag::Custom(String::from("myflag")),
254            Flag::Forwarded,
255            Flag::Answered,
256        ]);
257        assert_eq!(
258            set.to_string(),
259            String::from("($answered, $Forwarded, myflag)")
260        );
261    }
262
263    #[test]
264    fn test_serialize_string() {
265        let set = Flags::from([
266            Flag::Custom(String::from("myflag")),
267            Flag::Forwarded,
268            Flag::Answered,
269        ]);
270        assert_eq!(
271            set.to_string(),
272            String::from("($answered, $Forwarded, myflag)")
273        );
274
275        let ser = set.write_string();
276        let set2 = Flags::parse_string(&ser).unwrap();
277        assert_eq!(set, set2);
278    }
279
280    #[test]
281    fn test_serialize_file() {
282        let set = Flags::from([
283            Flag::Custom(String::from("myflag")),
284            Flag::Forwarded,
285            Flag::Answered,
286        ]);
287        assert_eq!(
288            set.to_string(),
289            String::from("($answered, $Forwarded, myflag)")
290        );
291
292        let mut f = NamedTempFile::new().unwrap();
293
294        set.write_file(&mut f).unwrap();
295
296        let r = File::open(f.path()).unwrap();
297
298        let set2 = Flags::parse_file(r).unwrap();
299        assert_eq!(set, set2);
300    }
301}