evobench_tools/serde_types/
proper_filename.rs1use std::str::FromStr;
2
3use serde::de::Visitor;
4
5#[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";
40impl 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 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}