printobserver_types/
file_name.rs1use core::fmt;
37use core::str::FromStr;
38use std::borrow::Cow;
39
40use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
41use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
42
43pub const SEPARATORS: [char; 2] = ['/', '\\'];
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum FileNameRefusal {
52 Empty,
54 Nul,
56 LeadingSeparator,
58 Separator,
60 DotSegment,
62 DrivePrefix,
64}
65
66impl FileNameRefusal {
67 pub const ALL: [Self; 6] = [
72 Self::Empty,
73 Self::Nul,
74 Self::LeadingSeparator,
75 Self::Separator,
76 Self::DotSegment,
77 Self::DrivePrefix,
78 ];
79
80 #[must_use]
82 pub const fn detail(self) -> &'static str {
83 match self {
84 Self::Empty => "it is empty",
85 Self::Nul => "it contains a NUL byte",
86 Self::LeadingSeparator => "it begins with a path separator",
87 Self::Separator => "it contains a path separator",
88 Self::DotSegment => "it carries a `.` or `..` segment",
89 Self::DrivePrefix => "it carries a drive prefix",
90 }
91 }
92}
93
94impl fmt::Display for FileNameRefusal {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 formatter.write_str(self.detail())
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FileNameError {
103 refusal: FileNameRefusal,
105 value: String,
107}
108
109impl FileNameError {
110 #[must_use]
112 pub const fn refusal(&self) -> FileNameRefusal {
113 self.refusal
114 }
115
116 #[must_use]
118 pub fn value(&self) -> &str {
119 &self.value
120 }
121}
122
123impl fmt::Display for FileNameError {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 write!(
126 formatter,
127 "{:?} is no file name: {}",
128 self.value, self.refusal
129 )
130 }
131}
132
133impl core::error::Error for FileNameError {}
134
135fn has_drive_prefix(value: &str) -> bool {
137 let mut characters = value.chars();
138 let Some(first) = characters.next() else {
139 return false;
140 };
141 first.is_ascii_alphabetic() && characters.next() == Some(':')
142}
143
144fn refusal_for(value: &str) -> Option<FileNameRefusal> {
146 if value.is_empty() {
147 return Some(FileNameRefusal::Empty);
148 }
149 if value.contains('\0') {
150 return Some(FileNameRefusal::Nul);
151 }
152 if value.starts_with(SEPARATORS) {
153 return Some(FileNameRefusal::LeadingSeparator);
154 }
155 if value.contains(SEPARATORS) {
156 return Some(FileNameRefusal::Separator);
157 }
158 if value
159 .split(SEPARATORS)
160 .any(|segment| segment == "." || segment == "..")
161 {
162 return Some(FileNameRefusal::DotSegment);
163 }
164 if has_drive_prefix(value) {
165 return Some(FileNameRefusal::DrivePrefix);
166 }
167 None
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
175pub struct FileName(String);
176
177impl FileName {
178 pub fn new(value: impl Into<String>) -> Result<Self, FileNameError> {
184 let value = value.into();
185 match refusal_for(&value) {
186 Some(refusal) => Err(FileNameError { refusal, value }),
187 None => Ok(Self(value)),
188 }
189 }
190
191 #[must_use]
193 pub fn as_str(&self) -> &str {
194 &self.0
195 }
196}
197
198impl fmt::Display for FileName {
199 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200 formatter.write_str(&self.0)
201 }
202}
203
204impl FromStr for FileName {
205 type Err = FileNameError;
206
207 fn from_str(text: &str) -> Result<Self, Self::Err> {
208 Self::new(text)
209 }
210}
211
212impl Serialize for FileName {
213 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
214 serializer.serialize_str(&self.0)
215 }
216}
217
218impl<'de> Deserialize<'de> for FileName {
219 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
220 let text = String::deserialize(deserializer)?;
221 Self::new(text).map_err(D::Error::custom)
222 }
223}
224
225impl JsonSchema for FileName {
226 fn schema_name() -> Cow<'static, str> {
227 Cow::Borrowed("FileName")
228 }
229
230 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
231 json_schema!({
232 "type": "string",
233 "title": "FileName",
234 "description": "A file name a printer's own file API can be asked for: no path \
235 separator, no NUL byte, no `.` or `..` segment, no drive prefix, \
236 and not empty.",
237 "minLength": 1,
238 "pattern": "^[^/\\\\\u{0}]+$",
239 "not": { "pattern": "^([.]{1,2}$|[A-Za-z]:)" }
240 })
241 }
242}