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    pub fn parse_string(s: &str) -> Result<Self, Error> {
97        if s.is_empty() {
98            return Ok(Flags::new());
99        }
100        s.trim_end().split('\n').map(Flag::try_from).collect()
101    }
102
103    pub fn parse_file(f: File) -> Result<Self, Error> {
104        // If read as stream, we'd have to deal with the trailing newline
105        // It's just the flags, so read it all and have parse_string handle it
106        let s = io::read_to_string(&f)?;
107        Flags::parse_string(&s)
108    }
109
110    pub fn write_string(&self) -> String {
111        let mut result = String::new();
112        for flag in &self.0 {
113            result.push_str(&format!("{}\n", flag));
114        }
115        result
116    }
117
118    #[allow(clippy::write_with_newline)]
119    pub fn write_file(&self, f: &mut impl Write) -> io::Result<()> {
120        for flag in &self.0 {
121            write!(f, "{}\n", flag)?;
122        }
123        f.flush()
124    }
125}
126
127impl Default for Flags {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Deref for Flags {
134    type Target = BTreeSet<Flag>;
135    fn deref(&self) -> &Self::Target {
136        &self.0
137    }
138}
139
140impl DerefMut for Flags {
141    fn deref_mut(&mut self) -> &mut Self::Target {
142        &mut self.0
143    }
144}
145
146impl<const N: usize> std::convert::From<[Flag; N]> for Flags {
147    fn from(arr: [Flag; N]) -> Self {
148        Flags(BTreeSet::from(arr))
149    }
150}
151
152impl std::convert::From<&BTreeSet<Flag>> for Flags {
153    fn from(set: &BTreeSet<Flag>) -> Self {
154        let mut c = BTreeSet::new();
155        for i in set {
156            c.insert(i.clone());
157        }
158        Flags(c)
159    }
160}
161
162impl FromIterator<Flag> for Flags {
163    fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
164        let mut set = BTreeSet::new();
165        for i in iter {
166            set.insert(i);
167        }
168        Flags(set)
169    }
170}
171
172impl IntoIterator for Flags {
173    type Item = Flag;
174    type IntoIter = <BTreeSet<Flag> as IntoIterator>::IntoIter;
175    fn into_iter(self) -> Self::IntoIter {
176        self.0.into_iter()
177    }
178}
179
180impl fmt::Display for Flags {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        let mut first = true;
183
184        write!(f, "(")?;
185        for flag in &self.0 {
186            if !first {
187                write!(f, ", {}", flag)?;
188            } else {
189                write!(f, "{}", flag)?;
190                first = false;
191            }
192        }
193        write!(f, ")")?;
194        Ok(())
195    }
196}
197
198pub(crate) fn flags_path_for(dir: impl AsRef<Path>, id: &str) -> PathBuf {
199    PathBuf::from_iter([
200        dir.as_ref().as_os_str(),
201        &OsString::from(".meta"),
202        &OsString::from(format!("{}.flags", id)),
203    ])
204}
205
206#[cfg(test)]
207mod tests {
208    use tempfile::NamedTempFile;
209
210    use super::*;
211
212    #[test]
213    fn test_flag() {
214        assert_eq!(Flag::try_from("$seen").unwrap(), Flag::Seen);
215        assert_eq!(Flag::try_from("$Forwarded").unwrap(), Flag::Forwarded);
216        assert_eq!(
217            Flag::try_from("myflag").unwrap(),
218            Flag::Custom(String::from("myflag"))
219        );
220        assert!(Flag::try_from("my\nflag").is_err());
221        assert!(Flag::try_from("").is_err());
222    }
223
224    #[test]
225    fn test_flags() {
226        let set = Flags::from([Flag::Answered]);
227        assert_eq!(set.to_string(), String::from("($answered)"));
228
229        let set = Flags::from([Flag::Forwarded, Flag::Answered]);
230        assert_eq!(set.to_string(), String::from("($answered, $Forwarded)"));
231
232        let set = Flags::from([
233            Flag::Custom(String::from("myflag")),
234            Flag::Forwarded,
235            Flag::Answered,
236        ]);
237        assert_eq!(
238            set.to_string(),
239            String::from("($answered, $Forwarded, myflag)")
240        );
241    }
242
243    #[test]
244    fn test_serialize_string() {
245        let set = Flags::from([
246            Flag::Custom(String::from("myflag")),
247            Flag::Forwarded,
248            Flag::Answered,
249        ]);
250        assert_eq!(
251            set.to_string(),
252            String::from("($answered, $Forwarded, myflag)")
253        );
254
255        let ser = set.write_string();
256        let set2 = Flags::parse_string(&ser).unwrap();
257        assert_eq!(set, set2);
258    }
259
260    #[test]
261    fn test_serialize_file() {
262        let set = Flags::from([
263            Flag::Custom(String::from("myflag")),
264            Flag::Forwarded,
265            Flag::Answered,
266        ]);
267        assert_eq!(
268            set.to_string(),
269            String::from("($answered, $Forwarded, myflag)")
270        );
271
272        let mut f = NamedTempFile::new().unwrap();
273
274        set.write_file(&mut f).unwrap();
275
276        let r = File::open(f.path()).unwrap();
277
278        let set2 = Flags::parse_file(r).unwrap();
279        assert_eq!(set, set2);
280    }
281}