evobench_tools/serde_types/
proper_filename.rs

1use std::str::FromStr;
2
3use serde::de::Visitor;
4
5/// A unicode file name, not path, i.e. not contain '/', '\n', or '\0'
6/// and must not be ".", "..", or "".
7#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
8pub struct ProperFilename(String);
9
10impl ProperFilename {
11    pub fn as_str(&self) -> &str {
12        &self.0
13    }
14}
15
16impl AsRef<str> for ProperFilename {
17    fn as_ref(&self) -> &str {
18        self.as_str()
19    }
20}
21
22impl<'t> From<&'t ProperFilename> for &'t str {
23    fn from(value: &'t ProperFilename) -> Self {
24        value.as_str()
25    }
26}
27
28pub fn is_proper_filename(v: &str) -> bool {
29    !(v == ""
30        || v == "."
31        || v == ".."
32        || v.contains('/')
33        || v.contains('\n')
34        || v.contains('\0')
35        || v.as_bytes().len() > 255)
36}
37
38const ERR_MSG: &str = "a file name (not path), must not contain '/', '\\n', '\\0', \
39     and must not be \".\", \"..\", the empty string, or longer than 255 bytes";
40// XX Windows will be different than "bytes" and 255.
41
42impl FromStr for ProperFilename {
43    type Err = &'static str;
44
45    fn from_str(v: &str) -> Result<Self, Self::Err> {
46        if !is_proper_filename(v) {
47            return Err(ERR_MSG);
48        }
49        Ok(ProperFilename(v.to_owned()))
50    }
51}
52
53struct FilenameVisitor;
54impl<'de> Visitor<'de> for FilenameVisitor {
55    type Value = ProperFilename;
56
57    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
58        formatter.write_str(ERR_MSG)
59    }
60
61    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
62    where
63        E: serde::de::Error,
64    {
65        v.parse().map_err(E::custom)
66    }
67}
68
69impl<'de> serde::Deserialize<'de> for ProperFilename {
70    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
71        deserializer.deserialize_str(FilenameVisitor)
72    }
73}
74
75impl serde::Serialize for ProperFilename {
76    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
77    where
78        S: serde::Serializer,
79    {
80        serializer.serialize_str(&self.0)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    // Returns Some when actual do, None when it wasn't a proper
89    // filename.
90    fn t_round_trip_json(s: &str) -> Option<()> {
91        let pfn = ProperFilename::from_str(s).ok()?;
92        assert_eq!(pfn.as_str(), s);
93        let v = serde_json::to_string(&pfn).expect("doesn't fail");
94        dbg!(&v);
95        let pfn2: ProperFilename = serde_json::from_str(&v).expect("doesn't fail either");
96        assert_eq!(pfn.as_str(), pfn2.as_str());
97        Some(())
98    }
99
100    fn t_round_trip_ron(s: &str) -> Option<()> {
101        let pfn = ProperFilename::from_str(s).ok()?;
102        assert_eq!(pfn.as_str(), s);
103        let v = ron::to_string(&pfn).expect("doesn't fail");
104        dbg!(&v);
105        let pfn2: ProperFilename = ron::from_str(&v).expect("doesn't fail either");
106        assert_eq!(pfn.as_str(), pfn2.as_str());
107        Some(())
108    }
109
110    fn t_round_trip(s: &str) -> Option<()> {
111        t_round_trip_json(s)?;
112        t_round_trip_ron(s)
113    }
114
115    #[test]
116    fn t_proper_filename() {
117        let t = t_round_trip;
118        assert!(t("foo").is_some());
119        assert!(t("<bar>").is_some());
120        assert!(t(" baz .. bla").is_some());
121        assert!(t(" baz/ .. bla").is_none());
122    }
123}