From 01ef419505681c5e67f07d7ea5e378e101609938 Mon Sep 17 00:00:00 2001 From: Emi Simpson Date: Thu, 21 Oct 2021 18:33:38 -0400 Subject: [PATCH] Added v0 parsing --- src/lib.rs | 9 +-- src/user_preferences/error.rs | 105 ++++++++++++++++++++++++++++++++++ src/user_preferences/mod.rs | 13 +++-- src/user_preferences/v0.rs | 90 +++++++++++++++++++++++++++-- 4 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 src/user_preferences/error.rs diff --git a/src/lib.rs b/src/lib.rs index 9c1a4fb..250949d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ //! ``` //! use pronouns_today::InstanceSettings; //! -//! let instance_settings = InstanceSettings::default() // Or load from a config file +//! let instance_settings = InstanceSettings::default(); // Or load from a config file //! //! // When you receive a request //! let user_name = Some("Emi"); @@ -70,9 +70,10 @@ impl InstanceSettings { /// This is shorthand for /// /// ``` - /// # settings = InstanceSettings::default(); - /// # name = String::from("Sashanora"); - /// # prefstring = String::from("todo"); + /// # use pronouns_today::InstanceSettings; + /// # let settings = InstanceSettings::default(); + /// # let name = String::from("Sashanora"); + /// # let prefstring = String::from("todo"); /// let pronouns = settings.parse_prefstring(Some(&prefstring)).select_pronouns(Some(&name)); /// # assert_eq!(pronouns, settings.select_pronouns(Some(&name), Some(&prefstring))); /// ``` diff --git a/src/user_preferences/error.rs b/src/user_preferences/error.rs new file mode 100644 index 0000000..10b0e38 --- /dev/null +++ b/src/user_preferences/error.rs @@ -0,0 +1,105 @@ +//! Errors that might occur when parsing or rendering prefstrings +//! +//! There are several possible points at which an error might occur when working with user +//! preference string. Each of these points are documented here, as well as +//! human-readable error messages and explainations of each. + +use std::{ + fmt, + ops::Range, +}; + +use data_encoding::DecodeError; + +/// An error occured while trying to parse a user's prefstring +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ParseError { + + /// The provided prefstring wasn't properly formatted base32 text + /// + /// This could be because an implementation generated text with a different alphabet, + /// or the provided text isn't base32 at all. The [`DecodeError`] in the error is the + /// error that caused this exception. + Base32Error(DecodeError), + + /// The version of the prefstring didn't match the version of the parser + /// + /// This could occur when the prefstring is of a newer version than the parser, or the + /// parser is meant for a different version of the prefstring than expected. + /// + /// Parsing typically works by having a master parser which supports all implemented + /// versions of prefstrings, and which delegates to several sub parsers. Each sub + /// parser handles one and only one version of the spec. This error could occur if, + /// for example, a v0 prefstring was fed to the v1 parser. + /// + /// The `expected_version` and `expected_variant` properties denote the accepted + /// version or range of accepted versions that the parser supported. The `actual_` + /// properties denote what was received. These can be `None` if a version wasn't + /// specified. + /// + /// If `actual_version` is [`Some`] then so must be `actual_variant`, and vice versa. + VersionMismatch { + expected_version: Range, + expected_variant: Range, + actual_version_byte: u8, + }, + + /// The `content` part of the prefstring was malformed + /// + /// Whatever version of the specification the prefstring was being parsed with does + /// not allow whatever is going on in the prefstring. More details are provided in + /// human-readable form in the attached string. + MalformedContent(String), + + /// The passed prefstring was zero-length + /// + /// The minimum length for a prefstring is one byte, but the version that was passed + /// was zero bytes long. Often, this means that the developer should simply use the + /// instance default preferences. + ZeroLengthPrefstring, +} + +impl fmt::Display for ParseError { + + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseError::Base32Error(e) => { + write!(f, + "A problem occured while deserializing the preference string as base32: {}", + e + ) + }, + ParseError::VersionMismatch { + expected_version, + expected_variant, + actual_version_byte, + } => { + write!(f, + "A parser meant to handle prefstring version {} through {} and + variants {} throught {} was given a prefstring of version {} and + variant {}.", + expected_version.start, + expected_version.end - 1, + expected_variant.start, + expected_variant.end - 1, + actual_version_byte >> 3, + actual_version_byte & 0b00000111, + ) + }, + ParseError::MalformedContent(msg) => { + write!(f,"The prefstring's content was malformed: {}", msg) + }, + ParseError::ZeroLengthPrefstring => { + write!(f, "Tried to parse a zero-length prefstring") + } + } + } + +} + +impl From for ParseError { + fn from(e: DecodeError) -> Self { + ParseError::Base32Error(e) + } +} diff --git a/src/user_preferences/mod.rs b/src/user_preferences/mod.rs index ec8bfd4..e81cbfc 100644 --- a/src/user_preferences/mod.rs +++ b/src/user_preferences/mod.rs @@ -5,10 +5,12 @@ //! module houses the implementations for each of these versions. pub mod v0; +mod error; +pub use error::ParseError; use crate::{InstanceSettings, WeightedTable}; -use data_encoding::{BASE32_NOPAD, DecodeError}; +use data_encoding::BASE32_NOPAD; /// A user's preferences for the probabilities of certain pronouns /// @@ -45,7 +47,7 @@ pub trait Preference { /// format. Prefstrings will already have been parsed from base64 before being passed to the /// implementation. Users looking to turn a b64 prefstring into a `Preference` should use /// [`Preference::from_prefstring()`] - fn from_prefstring_bytes(bytes: &[u8]) -> Self; + fn from_prefstring_bytes(bytes: &[u8]) -> Result where Self: Sized; /// Serialize these preferences into as few bytes as possible /// @@ -60,9 +62,10 @@ pub trait Preference { /// /// This is the primary method of creating a `Preference` object from a prefstring. The /// default implementation calls the underlying [`Preference::from_prefstring_bytes()`] method. - fn from_prefstring(prefstring: &str) -> Result where Self: Sized { + fn from_prefstring(prefstring: &str) -> Result where Self: Sized { BASE32_NOPAD.decode(prefstring.as_ref()) - .map(|ps| Self::from_prefstring_bytes(&ps)) + .map_err(ParseError::Base32Error) + .and_then(|ps| Self::from_prefstring_bytes(&ps)) } /// Serialize into a base64 prefstring @@ -81,7 +84,7 @@ impl Preference for UserPreferences { }.into_weighted_table(settings) } - fn from_prefstring_bytes(bytes: &[u8]) -> Self { + fn from_prefstring_bytes(bytes: &[u8]) -> Result where Self: Sized { todo!() } diff --git a/src/user_preferences/v0.rs b/src/user_preferences/v0.rs index 669f0d8..f71be40 100644 --- a/src/user_preferences/v0.rs +++ b/src/user_preferences/v0.rs @@ -2,7 +2,7 @@ use crate::{ InstanceSettings, - user_preferences::Preference, + user_preferences::{Preference, ParseError}, WeightedTable, }; @@ -12,8 +12,9 @@ use crate::{ /// /// [1]: https://fem.mint.lgbt/Emi/PronounsToday/raw/branch/main/doc/User-Preference-String-Spec.txt pub struct UserPreferencesV0 { - default_weight: u8, - default_enabled: bool, + pub default_weight: u8, + pub default_enabled: bool, + pub commands: Vec, } impl Preference for UserPreferencesV0 { @@ -21,11 +22,90 @@ impl Preference for UserPreferencesV0 { todo!() } - fn from_prefstring_bytes(bytes: &[u8]) -> Self { - todo!() + /// ``` + /// # use pronouns_today::user_preferences::{Preference, v0::{Command, UserPreferencesV0}}; + /// let pbytes = &[ 0x00, 0x81, 0x08, 0xcf, 0x80 ][..]; + /// let prefs = UserPreferencesV0::from_prefstring_bytes(&pbytes).unwrap(); + /// assert_eq!(prefs.default_weight, 1); + /// assert!(prefs.default_enabled); + /// assert_eq!(prefs.commands, vec![ + /// Command::SetWeight(8), + /// Command::Move { toggle_enabled: true, distance: 15 }, + /// Command::Move { toggle_enabled: false, distance: 0 }, + /// ]); + /// ``` + fn from_prefstring_bytes(pbytes: &[u8]) -> Result { + + // Some simple error checks + if pbytes.len() == 0 { + return Err(ParseError::ZeroLengthPrefstring); + } else if pbytes[0] != 00 { + return Err(ParseError::VersionMismatch { + expected_version: 0..1, + expected_variant: 0..1, + actual_version_byte: pbytes[0], + }) + } else if pbytes.len() == 1 { + return Err( + ParseError::MalformedContent( + "Version 0 prefstrings must be at least two bytes long, but this one + was just one byte long".into() + ) + ) + } + + // Extract the defaults from byte 1 + Ok(UserPreferencesV0 { + default_enabled: pbytes[1] & 0b10000000 > 0, + default_weight: pbytes[1] & 0b01111111, + commands: pbytes[2..].iter().map(|b| Command::from(*b)).collect(), + }) } fn into_prefstring_bytes(&self) -> Vec { todo!() } } + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Command { + SetWeight(u8), + Move { + toggle_enabled: bool, + distance: u8, + } +} + +impl From for Command { + /// ``` + /// use pronouns_today::user_preferences::v0::Command; + /// assert_eq!( + /// Command::from(0b01001000), + /// Command::SetWeight(0b01001000) + /// ); + /// assert_eq!( + /// Command::from(0b11000000), + /// Command::Move { + /// toggle_enabled: true, + /// distance: 0b00000000 + /// } + /// ); + /// assert_eq!( + /// Command::from(0b10101000), + /// Command::Move { + /// toggle_enabled: false, + /// distance: 0b00101000 + /// } + /// ); + /// ``` + fn from(b: u8) -> Self { + if b & 0b10000000 > 0 { + Command::Move { + toggle_enabled: b & 0b01000000 > 0, + distance: b & 0b00111111, + } + } else { + Command::SetWeight(b) + } + } +}