1use std::collections::{HashMap, HashSet};
19use std::fmt::Display;
20use std::io;
21use std::num::NonZeroU32;
22use std::str::FromStr;
23
24#[derive(Debug, thiserror::Error)]
26#[error("{}", .0)]
27pub struct ParseError(pub String);
28
29impl From<ParseError> for io::Error {
30 fn from(value: ParseError) -> Self {
31 Self::new(io::ErrorKind::InvalidInput, value.0)
32 }
33}
34
35macro_rules! mkerror {
37 ($($arg:tt)*) => ({
38 ParseError(format!($($arg)*))
39 })
40}
41
42type Result<T> = std::result::Result<T, ParseError>;
44
45#[derive(Debug, PartialEq)]
47pub enum Resolution {
48 FullScreenDesktop,
50
51 FullScreen((NonZeroU32, NonZeroU32)),
53
54 Windowed((NonZeroU32, NonZeroU32)),
56}
57
58fn parse_resolution(resolution: &str) -> Result<Resolution> {
60 if resolution == "fs" {
61 return Ok(Resolution::FullScreenDesktop);
62 }
63
64 let (dimensions, fullscreen) = match resolution.strip_suffix("fs") {
65 Some(prefix) => (prefix, true),
66 None => (resolution, false),
67 };
68
69 match dimensions.split_once('x') {
70 Some((width, height)) => {
71 let width = NonZeroU32::from_str(width).map_err(|e| {
72 mkerror!("Invalid width {} in resolution {}: {}", width, resolution, e)
73 })?;
74 let height = NonZeroU32::from_str(height).map_err(|e| {
75 mkerror!("Invalid height {} in resolution {}: {}", height, resolution, e)
76 })?;
77
78 if fullscreen {
79 Ok(Resolution::FullScreen((width, height)))
80 } else {
81 Ok(Resolution::Windowed((width, height)))
82 }
83 }
84 _ => Err(mkerror!(
85 "Invalid resolution {}: must be of the form [WIDTHxHEIGHT][fs]",
86 resolution
87 )),
88 }
89}
90
91impl FromStr for Resolution {
92 type Err = ParseError;
93
94 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
95 parse_resolution(s)
96 }
97}
98
99pub struct ConsoleSpec<'a> {
109 pub driver: &'a str,
111
112 flags: HashSet<&'a str>,
115
116 keyed_flags: HashMap<&'a str, &'a str>,
119}
120
121impl<'a> ConsoleSpec<'a> {
122 pub fn init(s: &'a str) -> Self {
127 assert!(!s.is_empty());
128
129 let (driver, rest) = s.split_once(':').unwrap_or((s, ""));
130
131 let mut flags = HashSet::default();
132 let mut keyed_flags = HashMap::default();
133 for pair in rest.split(',') {
134 if pair.is_empty() {
135 continue;
136 }
137
138 match pair.split_once('=') {
139 None => {
140 let _exists = flags.insert(pair);
141 }
142 Some((k, v)) => {
143 let _old = keyed_flags.insert(k, v);
144 }
145 }
146 }
147
148 Self { driver, flags, keyed_flags }
149 }
150
151 pub fn take_flag(&mut self, flag: &str) -> bool {
155 self.flags.remove(flag)
156 }
157
158 pub fn take_keyed_flag_str(&mut self, key: &str) -> Option<&str> {
161 self.keyed_flags.remove(key)
162 }
163
164 pub fn take_keyed_flag<V>(&mut self, key: &str) -> Result<Option<V>>
169 where
170 V: FromStr,
171 V::Err: Display,
172 {
173 match self.take_keyed_flag_str(key) {
174 Some(v) => V::from_str(v)
175 .map(|v| Some(v))
176 .map_err(|e| mkerror!("Invalid console flag {}: {}", key, e)),
177 None => Ok(None),
178 }
179 }
180
181 pub fn finish(self) -> Result<()> {
183 if self.flags.is_empty() && self.keyed_flags.is_empty() {
184 Ok(())
185 } else {
186 let flags_iter = self.flags.into_iter();
187 let keyed_iter = self.keyed_flags.into_keys();
188 let mut unknown = flags_iter.chain(keyed_iter).collect::<Vec<&'a str>>();
189 unknown.sort();
190 Err(mkerror!(
191 "Console driver {} does not recognize flags: {}",
192 self.driver,
193 unknown.join(", ")
194 ))
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_resolution_ok() -> Result<()> {
205 let nz100 = NonZeroU32::new(100).unwrap();
206 let nz200 = NonZeroU32::new(200).unwrap();
207 for (s, exp_resolution) in [
208 ("100x200", Resolution::Windowed((nz100, nz200))),
209 ("100x200fs", Resolution::FullScreen((nz100, nz200))),
210 ("fs", Resolution::FullScreenDesktop),
211 ] {
212 assert_eq!(exp_resolution, Resolution::from_str(s)?);
213 }
214 Ok(())
215 }
216
217 #[test]
218 fn test_resolution_errors() -> Result<()> {
219 for (s, exp_error) in [
220 ("100", "Invalid resolution 100: must be of the form [WIDTHxHEIGHT][fs]"),
221 ("100fs", "Invalid resolution 100fs: must be of the form [WIDTHxHEIGHT][fs]"),
222 (
223 "100x200x300",
224 "Invalid height 200x300 in resolution 100x200x300: invalid digit found in string",
225 ),
226 ("100x", "Invalid height in resolution 100x: cannot parse integer from empty string"),
227 ("x200", "Invalid width in resolution x200: cannot parse integer from empty string"),
228 ("0x2", "Invalid width 0 in resolution 0x2: number would be zero for non-zero type"),
229 ("1x0", "Invalid height 0 in resolution 1x0: number would be zero for non-zero type"),
230 ] {
231 match Resolution::from_str(s) {
232 Ok(_) => panic!("Invalid resolution {} not raised as an error", s),
233 Err(e) => assert_eq!(exp_error, e.0),
234 }
235 }
236 Ok(())
237 }
238
239 #[test]
240 fn test_console_spec_just_driver() -> Result<()> {
241 let spec = ConsoleSpec::init("default");
242 assert_eq!("default", spec.driver);
243 spec.finish()
244 }
245
246 #[test]
247 fn test_console_spec_driver_no_opts() -> Result<()> {
248 let spec = ConsoleSpec::init("default:");
249 assert_eq!("default", spec.driver);
250 spec.finish()
251 }
252
253 #[test]
254 fn test_console_spec_flags() -> Result<()> {
255 let mut spec = ConsoleSpec::init("default:foo,baz");
256 assert_eq!("default", spec.driver);
257 assert!(spec.take_flag("foo"));
258 assert!(!spec.take_flag("bar"));
259 assert!(spec.take_flag("baz"));
260 spec.finish()
261 }
262
263 #[test]
264 fn test_console_spec_keyed_flags() -> Result<()> {
265 let mut spec = ConsoleSpec::init("default:a=b=c,foo=bar");
266 assert_eq!("default", spec.driver);
267 assert_eq!(Some("b=c"), spec.take_keyed_flag_str("a"));
268 assert_eq!(Some("bar"), spec.take_keyed_flag_str("foo"));
269 assert_eq!(None, spec.take_keyed_flag_str("baz"));
270 spec.finish()
271 }
272
273 #[test]
274 fn test_console_spec_keyed_flags_last_wins() -> Result<()> {
275 let mut spec = ConsoleSpec::init("default:x=1,y=2,x=3");
276 assert_eq!("default", spec.driver);
277 assert_eq!(Some("3"), spec.take_keyed_flag_str("x"));
278 assert_eq!(Some("2"), spec.take_keyed_flag_str("y"));
279 spec.finish()
280 }
281
282 #[test]
283 fn test_console_spec_keyed_flags_typed_ok() -> Result<()> {
284 let mut spec = ConsoleSpec::init("default:x=1");
285 assert_eq!("default", spec.driver);
286 assert_eq!(Some(1_i32), spec.take_keyed_flag("x")?);
287 assert_eq!(None as Option<i32>, spec.take_keyed_flag("x")?);
288 spec.finish()
289 }
290
291 #[test]
292 fn test_console_spec_keyed_flags_typed_err() -> Result<()> {
293 let mut spec = ConsoleSpec::init("default:x=0");
294 assert_eq!("default", spec.driver);
295 assert_eq!(
296 "Invalid console flag x: number would be zero for non-zero type",
297 spec.take_keyed_flag::<NonZeroU32>("x").unwrap_err().0
298 );
299 spec.finish()
300 }
301
302 #[test]
303 fn test_console_spec_residue_errors() -> Result<()> {
304 let mut spec = ConsoleSpec::init("abc:foo,y,x=z,bar=baz");
305 assert!(spec.take_flag("foo"));
306 assert!(!spec.take_flag("x"));
307 assert_eq!(Some("baz"), spec.take_keyed_flag_str("bar"));
308 assert_eq!(None, spec.take_keyed_flag_str("y"));
309 match spec.finish() {
310 Ok(()) => panic!("Residual flags not detected"),
311 Err(e) => {
312 assert_eq!("Console driver abc does not recognize flags: x, y", e.0);
313 Ok(())
314 }
315 }
316 }
317}