use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::io;
use std::num::NonZeroU32;
use std::str::FromStr;
#[derive(Debug, thiserror::Error)]
#[error("{}", .0)]
pub struct ParseError(pub String);
impl From<ParseError> for io::Error {
fn from(value: ParseError) -> Self {
Self::new(io::ErrorKind::InvalidInput, value.0)
}
}
macro_rules! mkerror {
($($arg:tt)*) => ({
ParseError(format!($($arg)*))
})
}
type Result<T> = std::result::Result<T, ParseError>;
#[derive(Debug, PartialEq)]
pub enum Resolution {
FullScreenDesktop,
FullScreen((NonZeroU32, NonZeroU32)),
Windowed((NonZeroU32, NonZeroU32)),
}
fn parse_resolution(resolution: &str) -> Result<Resolution> {
if resolution == "fs" {
return Ok(Resolution::FullScreenDesktop);
}
let (dimensions, fullscreen) = match resolution.strip_suffix("fs") {
Some(prefix) => (prefix, true),
None => (resolution, false),
};
match dimensions.split_once('x') {
Some((width, height)) => {
let width = NonZeroU32::from_str(width).map_err(|e| {
mkerror!("Invalid width {} in resolution {}: {}", width, resolution, e)
})?;
let height = NonZeroU32::from_str(height).map_err(|e| {
mkerror!("Invalid height {} in resolution {}: {}", height, resolution, e)
})?;
if fullscreen {
Ok(Resolution::FullScreen((width, height)))
} else {
Ok(Resolution::Windowed((width, height)))
}
}
_ => Err(mkerror!(
"Invalid resolution {}: must be of the form [WIDTHxHEIGHT][fs]",
resolution
)),
}
}
impl FromStr for Resolution {
type Err = ParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
parse_resolution(s)
}
}
pub struct ConsoleSpec<'a> {
pub driver: &'a str,
flags: HashSet<&'a str>,
keyed_flags: HashMap<&'a str, &'a str>,
}
impl<'a> ConsoleSpec<'a> {
pub fn init(s: &'a str) -> Self {
assert!(!s.is_empty());
let (driver, rest) = s.split_once(':').unwrap_or((s, ""));
let mut flags = HashSet::default();
let mut keyed_flags = HashMap::default();
for pair in rest.split(',') {
if pair.is_empty() {
continue;
}
match pair.split_once('=') {
None => {
let _exists = flags.insert(pair);
}
Some((k, v)) => {
let _old = keyed_flags.insert(k, v);
}
}
}
Self { driver, flags, keyed_flags }
}
pub fn take_flag(&mut self, flag: &str) -> bool {
self.flags.remove(flag)
}
pub fn take_keyed_flag_str(&mut self, key: &str) -> Option<&str> {
self.keyed_flags.remove(key)
}
pub fn take_keyed_flag<V>(&mut self, key: &str) -> Result<Option<V>>
where
V: FromStr,
V::Err: Display,
{
match self.take_keyed_flag_str(key) {
Some(v) => V::from_str(v)
.map(|v| Some(v))
.map_err(|e| mkerror!("Invalid console flag {}: {}", key, e)),
None => Ok(None),
}
}
pub fn finish(self) -> Result<()> {
if self.flags.is_empty() && self.keyed_flags.is_empty() {
Ok(())
} else {
let flags_iter = self.flags.into_iter();
let keyed_iter = self.keyed_flags.into_keys();
let mut unknown = flags_iter.chain(keyed_iter).collect::<Vec<&'a str>>();
unknown.sort();
Err(mkerror!(
"Console driver {} does not recognize flags: {}",
self.driver,
unknown.join(", ")
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolution_ok() -> Result<()> {
let nz100 = NonZeroU32::new(100).unwrap();
let nz200 = NonZeroU32::new(200).unwrap();
for (s, exp_resolution) in [
("100x200", Resolution::Windowed((nz100, nz200))),
("100x200fs", Resolution::FullScreen((nz100, nz200))),
("fs", Resolution::FullScreenDesktop),
] {
assert_eq!(exp_resolution, Resolution::from_str(s)?);
}
Ok(())
}
#[test]
fn test_resolution_errors() -> Result<()> {
for (s, exp_error) in [
("100", "Invalid resolution 100: must be of the form [WIDTHxHEIGHT][fs]"),
("100fs", "Invalid resolution 100fs: must be of the form [WIDTHxHEIGHT][fs]"),
(
"100x200x300",
"Invalid height 200x300 in resolution 100x200x300: invalid digit found in string",
),
("100x", "Invalid height in resolution 100x: cannot parse integer from empty string"),
("x200", "Invalid width in resolution x200: cannot parse integer from empty string"),
("0x2", "Invalid width 0 in resolution 0x2: number would be zero for non-zero type"),
("1x0", "Invalid height 0 in resolution 1x0: number would be zero for non-zero type"),
] {
match Resolution::from_str(s) {
Ok(_) => panic!("Invalid resolution {} not raised as an error", s),
Err(e) => assert_eq!(exp_error, e.0),
}
}
Ok(())
}
#[test]
fn test_console_spec_just_driver() -> Result<()> {
let spec = ConsoleSpec::init("default");
assert_eq!("default", spec.driver);
spec.finish()
}
#[test]
fn test_console_spec_driver_no_opts() -> Result<()> {
let spec = ConsoleSpec::init("default:");
assert_eq!("default", spec.driver);
spec.finish()
}
#[test]
fn test_console_spec_flags() -> Result<()> {
let mut spec = ConsoleSpec::init("default:foo,baz");
assert_eq!("default", spec.driver);
assert!(spec.take_flag("foo"));
assert!(!spec.take_flag("bar"));
assert!(spec.take_flag("baz"));
spec.finish()
}
#[test]
fn test_console_spec_keyed_flags() -> Result<()> {
let mut spec = ConsoleSpec::init("default:a=b=c,foo=bar");
assert_eq!("default", spec.driver);
assert_eq!(Some("b=c"), spec.take_keyed_flag_str("a"));
assert_eq!(Some("bar"), spec.take_keyed_flag_str("foo"));
assert_eq!(None, spec.take_keyed_flag_str("baz"));
spec.finish()
}
#[test]
fn test_console_spec_keyed_flags_last_wins() -> Result<()> {
let mut spec = ConsoleSpec::init("default:x=1,y=2,x=3");
assert_eq!("default", spec.driver);
assert_eq!(Some("3"), spec.take_keyed_flag_str("x"));
assert_eq!(Some("2"), spec.take_keyed_flag_str("y"));
spec.finish()
}
#[test]
fn test_console_spec_keyed_flags_typed_ok() -> Result<()> {
let mut spec = ConsoleSpec::init("default:x=1");
assert_eq!("default", spec.driver);
assert_eq!(Some(1_i32), spec.take_keyed_flag("x")?);
assert_eq!(None as Option<i32>, spec.take_keyed_flag("x")?);
spec.finish()
}
#[test]
fn test_console_spec_keyed_flags_typed_err() -> Result<()> {
let mut spec = ConsoleSpec::init("default:x=0");
assert_eq!("default", spec.driver);
assert_eq!(
"Invalid console flag x: number would be zero for non-zero type",
spec.take_keyed_flag::<NonZeroU32>("x").unwrap_err().0
);
spec.finish()
}
#[test]
fn test_console_spec_residue_errors() -> Result<()> {
let mut spec = ConsoleSpec::init("abc:foo,y,x=z,bar=baz");
assert!(spec.take_flag("foo"));
assert!(!spec.take_flag("x"));
assert_eq!(Some("baz"), spec.take_keyed_flag_str("bar"));
assert_eq!(None, spec.take_keyed_flag_str("y"));
match spec.finish() {
Ok(()) => panic!("Residual flags not detected"),
Err(e) => {
assert_eq!("Console driver abc does not recognize flags: x, y", e.0);
Ok(())
}
}
}
}