From 5592708271bd2f9edc5e315729aad1e2e371e9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Hlusi=C4=8Dka?= Date: Sat, 31 Jan 2026 15:24:36 +0100 Subject: [PATCH] UI code cleanup --- firmware/acid-firmware/build.rs | 3 +- firmware/acid-firmware/src/ui/messages.rs | 33 + firmware/acid-firmware/src/ui/mod.rs | 733 +++++++++------------- firmware/acid-firmware/src/ui/storage.rs | 109 ++++ 4 files changed, 448 insertions(+), 430 deletions(-) create mode 100644 firmware/acid-firmware/src/ui/messages.rs create mode 100644 firmware/acid-firmware/src/ui/storage.rs diff --git a/firmware/acid-firmware/build.rs b/firmware/acid-firmware/build.rs index 382f391..6eac8d0 100644 --- a/firmware/acid-firmware/build.rs +++ b/firmware/acid-firmware/build.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use const_gen::*; use embuild::cmd; -use indoc::{formatdoc, writedoc}; +use indoc::writedoc; use json::JsonValue; use slint_build::{CompilerConfiguration, EmbedResourcesKind}; use xz2::read::XzEncoder; @@ -63,6 +63,7 @@ fn main() { #[derive(Debug)] struct NotBuilt { + #[allow(unused)] lib_build_dir: String, } diff --git a/firmware/acid-firmware/src/ui/messages.rs b/firmware/acid-firmware/src/ui/messages.rs new file mode 100644 index 0000000..018cd28 --- /dev/null +++ b/firmware/acid-firmware/src/ui/messages.rs @@ -0,0 +1,33 @@ +use slint::SharedString; + +pub enum CallbackMessage { + /// The escape key was pressed. + Escape, + Login(CallbackMessageLogin), + Users(CallbackMessageUsers), + UserEdit(CallbackMessageUserEdit), + UserSites(CallbackMessageUserSites), +} + +pub enum CallbackMessageLogin { + PwAccepted { + username: SharedString, + password: SharedString, + }, +} + +pub enum CallbackMessageUsers { + EditUser { username: SharedString, new: bool }, +} + +pub enum CallbackMessageUserEdit { + ComputeIdenticon { + encrypted_key: SharedString, + password: SharedString, + }, + Confirm { + encrypted_key: SharedString, + }, +} + +pub enum CallbackMessageUserSites {} diff --git a/firmware/acid-firmware/src/ui/mod.rs b/firmware/acid-firmware/src/ui/mod.rs index 3840e42..31a51ce 100644 --- a/firmware/acid-firmware/src/ui/mod.rs +++ b/firmware/acid-firmware/src/ui/mod.rs @@ -46,120 +46,23 @@ use crate::{ PartitionAcid, ReadTransactionExt, }, ffi::alloc::__spre_free, - ui::backend::SlintBackend, + ui::{ + backend::SlintBackend, + messages::{ + CallbackMessage, CallbackMessageLogin, CallbackMessageUserEdit, CallbackMessageUsers, + }, + storage::SpectreUsersConfig, + }, util::DurationExt, }; pub mod backend; +pub mod messages; +pub mod storage; pub mod window_adapter; slint::include_modules!(); -#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)] -struct SpectreUsersConfig { - users: Vec, -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -struct SpectreUserConfig { - username: String, - #[serde(with = "serde_bytes")] - encrypted_key: Key, - #[serde(with = "serde_bytes")] - key_id: [u8; 32], -} - -trait ReprConvert: Copy { - type Repr: Copy; - - fn into_repr(self) -> Self::Repr; - fn from_repr(repr: Self::Repr) -> Self; -} - -impl ReprConvert for SpectreAlgorithm { - type Repr = u32; - - fn from_repr(repr: Self::Repr) -> Self { - unsafe { core::mem::transmute::(repr) } - } - - fn into_repr(self) -> Self::Repr { - self as Self::Repr - } -} - -impl ReprConvert for SpectreResultType { - type Repr = u32; - - fn from_repr(repr: Self::Repr) -> Self { - unsafe { core::mem::transmute::(repr) } - } - - fn into_repr(self) -> Self::Repr { - self as Self::Repr - } -} - -mod with_repr { - use serde::{Deserialize, Deserializer, Serializer}; - - use super::ReprConvert; - - pub fn serialize(value: &T, serializer: S) -> Result - where - T: ReprConvert, - S: Serializer, - { - serializer.serialize_u32(value.into_repr()) - } - - pub fn deserialize<'de, T, D>(deserializer: D) -> Result - where - T: ReprConvert, - D: Deserializer<'de>, - { - ::deserialize(deserializer).map(T::from_repr) - } -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -struct SpectreSiteConfig { - #[serde(with = "with_repr")] - algorithm: SpectreAlgorithm, - counter: SpectreCounter::Type, - #[serde(rename = "type")] - #[serde(with = "with_repr")] - result_type: SpectreResultType, - password: Option, - #[serde(with = "with_repr")] - login_type: SpectreResultType, - login_name: Option, - uses: u32, - last_used: NaiveDateTime, -} - -impl Default for SpectreSiteConfig { - fn default() -> Self { - Self { - algorithm: SpectreAlgorithm::Current, - counter: SpectreCounter::Default, - result_type: SpectreResultType::SpectreResultDefaultResult, - password: None, - login_type: SpectreResultType::SpectreResultDefaultLogin, - login_name: None, - uses: 0, - last_used: Default::default(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct SpectreSite { - username: String, - site_name: String, - config: SpectreSiteConfig, -} - fn spectre_derive_user_key(username: &CStr, password: &CStr) -> SpectreUserKey { let user_key_start = Instant::now(); @@ -282,334 +185,306 @@ pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: Partition .unwrap(); slint::platform::set_platform(Box::new(backend)).expect("backend already initialized"); - struct State { - window: AppWindow, - db: AcidDatabase, - users: SpectreUsersConfig, - /// Currently active view. - view: AppState, - // Retained state for each view. - state_login: StateLogin, - state_users: StateUsers, - state_user_edit: StateUserEdit, - state_user_sites: Option, - } - - impl State { - fn process_callback_message(&mut self, message: CallbackMessage) { - match self.view { - AppState::Login => StateLogin::process_callback_message(self, message), - AppState::Users => StateUsers::process_callback_message(self, message), - AppState::UserEdit => StateUserEdit::process_callback_message(self, message), - AppState::UserSites => StateUserSites::process_callback_message(self, message), - } - } - - fn set_view(&mut self, view: AppState, reset: bool) { - self.view = view; - self.window.set_app_state(view); - - if reset { - match self.view { - AppState::Login => self.state_login = Default::default(), - AppState::Users => self.state_users = Default::default(), - AppState::UserEdit => self.state_user_edit = Default::default(), - AppState::UserSites => self.state_user_sites = Default::default(), - } - } - } - } - - trait AppViewTrait { - fn process_callback_message(state: &mut State, message: CallbackMessage) {} - } - - #[derive(Default)] - struct StateLogin {} - - impl AppViewTrait for StateLogin { - fn process_callback_message(state: &mut State, message: CallbackMessage) { - match message { - CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) => { - let username_c = CString::new(&*username) - .expect("Username cannot be converted to a C string."); - let password_c = CString::new(&*password) - .expect("Password cannot be converted to a C string."); - let user_key = spectre_derive_user_key(&username_c, &password_c); - - // let site_key_start = Instant::now(); - // let site_key = &*spectre_api_sys::spectre_site_key( - // user_key as *const SpectreUserKey, - // c"example.org".as_ptr(), - // SpectreCounter::Initial, - // SpectreKeyPurpose::Authentication, - // c"".as_ptr(), - // ); - // let site_key_duration = Instant::now().duration_since(site_key_start); - // warn!( - // "Site key derived in {} seconds:\n{site_key:02x?}", - // site_key_duration.display_as_secs() - // ); - // TODO: Erase memory before freeing - // __spre_free(site_key as *const _ as *mut _); - let Some(user) = state - .users - .users - .iter() - .find(|user| &*user.username == &*username) - else { - return; - }; - - if user.key_id == user_key.keyID.bytes { - info!("Correct password entered for user {username:?}."); - state.state_user_sites = Some(StateUserSites { username, user_key }); - state.set_view(AppState::UserSites, true); - } else { - warn!("Incorrect password entered for user {username:?}."); - // TODO: Clear the input - } - } - _ => (), - } - } - } - - #[derive(Default)] - struct StateUsers {} - - impl AppViewTrait for StateUsers { - fn process_callback_message(state: &mut State, message: CallbackMessage) { - match message { - CallbackMessage::Escape => { - state.set_view(AppState::Login, false); - } - CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => { - state.state_user_edit = StateUserEdit { - username: username.clone(), - new, - hashed: None, - }; - state.window.set_user_edit_username(username); - state.set_view(AppState::UserEdit, false); - } - _ => (), - } - } - } - - #[derive(Default)] - struct StateUserEdit { - username: SharedString, - new: bool, - hashed: Option, - } - - impl AppViewTrait for StateUserEdit { - fn process_callback_message(state: &mut State, message: CallbackMessage) { - match message { - CallbackMessage::Escape => { - state.set_view(AppState::Users, false); - } - CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { - encrypted_key, - password, - }) => { - let username_c = CString::new(&*state.state_user_edit.username) - .expect("Username cannot be converted to a C string."); - let password_c = CString::new(&*password) - .expect("Password cannot be converted to a C string."); - // let user_key = spectre_derive_user_key(&username_c, &password_c); - let identicon: SharedString = unsafe { - let identicon = spectre_api_sys::spectre_identicon( - username_c.as_ptr(), - password_c.as_ptr(), - ); - // TODO: identicon.color - format!( - "{}{}{}{}", - CStr::from_ptr(identicon.leftArm).to_str().unwrap(), - CStr::from_ptr(identicon.body).to_str().unwrap(), - CStr::from_ptr(identicon.rightArm).to_str().unwrap(), - CStr::from_ptr(identicon.accessory).to_str().unwrap() - ) - .into() - }; - - warn!("Identicon: {identicon} ({identicon:?})"); - state.window.set_user_edit_identicon(identicon.clone()); - state.state_user_edit.hashed = Some(ProposedPassword { - encrypted_key, - identicon, - }) - } - CallbackMessage::UserEdit(CallbackMessageUserEdit::Confirm { encrypted_key }) => { - // TODO - } - _ => (), - } - } - } - - struct ProposedPassword { - encrypted_key: SharedString, - identicon: SharedString, - } - - struct StateUserSites { - username: SharedString, - user_key: SpectreUserKey, - } - - impl AppViewTrait for StateUserSites {} - - enum CallbackMessage { - /// The escape key was pressed. - Escape, - Login(CallbackMessageLogin), - Users(CallbackMessageUsers), - UserEdit(CallbackMessageUserEdit), - UserSites(CallbackMessageUserSites), - } - - enum CallbackMessageLogin { - PwAccepted { - username: SharedString, - password: SharedString, - }, - } - - enum CallbackMessageUsers { - EditUser { username: SharedString, new: bool }, - } - - enum CallbackMessageUserEdit { - ComputeIdenticon { - encrypted_key: SharedString, - password: SharedString, - }, - Confirm { - encrypted_key: SharedString, - }, - } - - enum CallbackMessageUserSites {} - let main = AppWindow::new().unwrap(); + let state = State::new(db, main).await; - let state = Rc::new(RefCell::new(State { - window: main.clone_strong(), - users: { - let read = db.read_transaction().await; - let mut buffer = vec![0_u8; 128]; - match read - .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer) - .await - { - Ok(bytes) => postcard::from_bytes::(&bytes).unwrap(), - Err(ekv::ReadError::KeyNotFound) => Default::default(), - Err(error) => panic!("Failed to read the users config: {error:?}"), - } - }, - db, - view: AppState::Login, - state_login: Default::default(), - state_users: Default::default(), - state_user_edit: Default::default(), - state_user_sites: Default::default(), - })); - - main.on_enter_view({ - let state = state.clone(); - move |view| { - state.borrow_mut().set_view(view, true); - } - }); - - main.on_escape({ - let state = state.clone(); - move || { - state - .borrow_mut() - .process_callback_message(CallbackMessage::Escape); - } - }); - - main.on_login_pw_accepted({ - let state = state.clone(); - move |username, password| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::Login( - CallbackMessageLogin::PwAccepted { username, password }, - )); - } - }); - - main.on_users_edit_user({ - let state = state.clone(); - move |username, new| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::Users(CallbackMessageUsers::EditUser { - username, - new, - })); - } - }); - - main.on_user_edit_compute_identicon({ - let state = state.clone(); - move |encrypted_key, password| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::UserEdit( - CallbackMessageUserEdit::ComputeIdenticon { - encrypted_key, - password, - }, - )); - } - }); - - main.on_user_edit_confirm({ - let state = state.clone(); - move |encrypted_key| { - state - .borrow_mut() - .process_callback_message(CallbackMessage::UserEdit( - CallbackMessageUserEdit::Confirm { encrypted_key }, - )); - } - }); - - // let sites = Rc::new(VecModel::default()); - // sites.push("First".into()); - // sites.push("Second".into()); - // main.set_sites(ModelRc::new(ModelRc::new(sites.clone()).map( - // |mut site: StandardListViewItem| { - // site.text += "10"; - // site - // }, - // ))); - - run_event_loop(main).await; + state.borrow().run_event_loop().await; } -/// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve -/// async by having only one iteration of the loop run, and `await`ing here. -/// The following block is analogous to `main.run()`. -async fn run_event_loop(main: AppWindow) { - main.show().unwrap(); +struct State { + window: AppWindow, + db: AcidDatabase, + users: SpectreUsersConfig, + /// Currently active view. + view: AppState, + // Retained state for each view. + state_login: StateLogin, + state_users: StateUsers, + state_user_edit: StateUserEdit, + state_user_sites: Option, +} - loop { - slint::run_event_loop().unwrap(); - SIGNAL_LCD_SUBMIT.signal(()); - #[cfg(feature = "limit-fps")] - embassy_time::Timer::after(FRAME_DURATION_MIN).await; - SIGNAL_UI_RENDER.wait().await; +impl State { + async fn new(db: AcidDatabase, main: AppWindow) -> Rc> { + let state = Rc::new(RefCell::new(State { + window: main.clone_strong(), + users: { + let read = db.read_transaction().await; + let mut buffer = vec![0_u8; 128]; + match read + .read_to_vec(&DbKey::new(DbPathSpectreUsers), &mut buffer) + .await + { + Ok(bytes) => postcard::from_bytes::(&bytes).unwrap(), + Err(ekv::ReadError::KeyNotFound) => Default::default(), + Err(error) => panic!("Failed to read the users config: {error:?}"), + } + }, + db, + view: AppState::Login, + state_login: Default::default(), + state_users: Default::default(), + state_user_edit: Default::default(), + state_user_sites: Default::default(), + })); + + main.on_enter_view({ + let state = state.clone(); + move |view| { + state.borrow_mut().set_view(view, true); + } + }); + + main.on_escape({ + let state = state.clone(); + move || { + state + .borrow_mut() + .process_callback_message(CallbackMessage::Escape); + } + }); + + main.on_login_pw_accepted({ + let state = state.clone(); + move |username, password| { + state + .borrow_mut() + .process_callback_message(CallbackMessage::Login( + CallbackMessageLogin::PwAccepted { username, password }, + )); + } + }); + + main.on_users_edit_user({ + let state = state.clone(); + move |username, new| { + state + .borrow_mut() + .process_callback_message(CallbackMessage::Users( + CallbackMessageUsers::EditUser { username, new }, + )); + } + }); + + main.on_user_edit_compute_identicon({ + let state = state.clone(); + move |encrypted_key, password| { + state + .borrow_mut() + .process_callback_message(CallbackMessage::UserEdit( + CallbackMessageUserEdit::ComputeIdenticon { + encrypted_key, + password, + }, + )); + } + }); + + main.on_user_edit_confirm({ + let state = state.clone(); + move |encrypted_key| { + state + .borrow_mut() + .process_callback_message(CallbackMessage::UserEdit( + CallbackMessageUserEdit::Confirm { encrypted_key }, + )); + } + }); + + // let sites = Rc::new(VecModel::default()); + // sites.push("First".into()); + // sites.push("Second".into()); + // main.set_sites(ModelRc::new(ModelRc::new(sites.clone()).map( + // |mut site: StandardListViewItem| { + // site.text += "10"; + // site + // }, + // ))); + + state } - #[expect(unreachable_code)] - main.hide().unwrap(); + fn process_callback_message(&mut self, message: CallbackMessage) { + match self.view { + AppState::Login => StateLogin::process_callback_message(self, message), + AppState::Users => StateUsers::process_callback_message(self, message), + AppState::UserEdit => StateUserEdit::process_callback_message(self, message), + AppState::UserSites => StateUserSites::process_callback_message(self, message), + } + } + + fn set_view(&mut self, view: AppState, reset: bool) { + self.view = view; + self.window.set_app_state(view); + + if reset { + match self.view { + AppState::Login => self.state_login = Default::default(), + AppState::Users => self.state_users = Default::default(), + AppState::UserEdit => self.state_user_edit = Default::default(), + AppState::UserSites => self.state_user_sites = Default::default(), + } + } + } + + /// Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve + /// async by having only one iteration of the loop run, and `await`ing here. + /// The following block is analogous to `main.run()`. + async fn run_event_loop(&self) -> ! { + self.window.show().unwrap(); + + loop { + slint::run_event_loop().unwrap(); + SIGNAL_LCD_SUBMIT.signal(()); + #[cfg(feature = "limit-fps")] + embassy_time::Timer::after(FRAME_DURATION_MIN).await; + SIGNAL_UI_RENDER.wait().await; + } + + #[expect(unreachable_code)] + self.window.hide().unwrap(); + } } + +trait AppViewTrait { + fn process_callback_message(state: &mut State, message: CallbackMessage) {} +} + +#[derive(Default)] +struct StateLogin {} + +impl AppViewTrait for StateLogin { + fn process_callback_message(state: &mut State, message: CallbackMessage) { + match message { + CallbackMessage::Login(CallbackMessageLogin::PwAccepted { username, password }) => { + let username_c = + CString::new(&*username).expect("Username cannot be converted to a C string."); + let password_c = + CString::new(&*password).expect("Password cannot be converted to a C string."); + let user_key = spectre_derive_user_key(&username_c, &password_c); + + // let site_key_start = Instant::now(); + // let site_key = &*spectre_api_sys::spectre_site_key( + // user_key as *const SpectreUserKey, + // c"example.org".as_ptr(), + // SpectreCounter::Initial, + // SpectreKeyPurpose::Authentication, + // c"".as_ptr(), + // ); + // let site_key_duration = Instant::now().duration_since(site_key_start); + // warn!( + // "Site key derived in {} seconds:\n{site_key:02x?}", + // site_key_duration.display_as_secs() + // ); + // TODO: Erase memory before freeing + // __spre_free(site_key as *const _ as *mut _); + let Some(user) = state + .users + .users + .iter() + .find(|user| &*user.username == &*username) + else { + return; + }; + + if user.key_id == user_key.keyID.bytes { + info!("Correct password entered for user {username:?}."); + state.state_user_sites = Some(StateUserSites { username, user_key }); + state.set_view(AppState::UserSites, true); + } else { + warn!("Incorrect password entered for user {username:?}."); + // TODO: Clear the input + } + } + _ => (), + } + } +} + +#[derive(Default)] +struct StateUsers {} + +impl AppViewTrait for StateUsers { + fn process_callback_message(state: &mut State, message: CallbackMessage) { + match message { + CallbackMessage::Escape => { + state.set_view(AppState::Login, false); + } + CallbackMessage::Users(CallbackMessageUsers::EditUser { username, new }) => { + state.state_user_edit = StateUserEdit { + username: username.clone(), + new, + hashed: None, + }; + state.window.set_user_edit_username(username); + state.set_view(AppState::UserEdit, false); + } + _ => (), + } + } +} + +#[derive(Default)] +struct StateUserEdit { + username: SharedString, + new: bool, + hashed: Option, +} + +impl AppViewTrait for StateUserEdit { + fn process_callback_message(state: &mut State, message: CallbackMessage) { + match message { + CallbackMessage::Escape => { + state.set_view(AppState::Users, false); + } + CallbackMessage::UserEdit(CallbackMessageUserEdit::ComputeIdenticon { + encrypted_key, + password, + }) => { + let username_c = CString::new(&*state.state_user_edit.username) + .expect("Username cannot be converted to a C string."); + let password_c = + CString::new(&*password).expect("Password cannot be converted to a C string."); + // let user_key = spectre_derive_user_key(&username_c, &password_c); + let identicon: SharedString = unsafe { + let identicon = spectre_api_sys::spectre_identicon( + username_c.as_ptr(), + password_c.as_ptr(), + ); + // TODO: identicon.color + format!( + "{}{}{}{}", + CStr::from_ptr(identicon.leftArm).to_str().unwrap(), + CStr::from_ptr(identicon.body).to_str().unwrap(), + CStr::from_ptr(identicon.rightArm).to_str().unwrap(), + CStr::from_ptr(identicon.accessory).to_str().unwrap() + ) + .into() + }; + + warn!("Identicon: {identicon} ({identicon:?})"); + state.window.set_user_edit_identicon(identicon.clone()); + state.state_user_edit.hashed = Some(ProposedPassword { + encrypted_key, + identicon, + }) + } + CallbackMessage::UserEdit(CallbackMessageUserEdit::Confirm { encrypted_key }) => { + // TODO + } + _ => (), + } + } +} + +struct ProposedPassword { + encrypted_key: SharedString, + identicon: SharedString, +} + +struct StateUserSites { + username: SharedString, + user_key: SpectreUserKey, +} + +impl AppViewTrait for StateUserSites {} diff --git a/firmware/acid-firmware/src/ui/storage.rs b/firmware/acid-firmware/src/ui/storage.rs new file mode 100644 index 0000000..259e2e6 --- /dev/null +++ b/firmware/acid-firmware/src/ui/storage.rs @@ -0,0 +1,109 @@ +use alloc::{string::String, vec::Vec}; +use chrono::NaiveDateTime; +use password_hash::Key; +use serde::{Deserialize, Serialize}; +use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreResultType}; + +#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)] +pub struct SpectreUsersConfig { + pub users: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +pub struct SpectreUserConfig { + pub username: String, + #[serde(with = "serde_bytes")] + pub encrypted_key: Key, + #[serde(with = "serde_bytes")] + pub key_id: [u8; 32], +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +pub struct SpectreSiteConfig { + #[serde(with = "with_repr")] + pub algorithm: SpectreAlgorithm, + pub counter: SpectreCounter::Type, + #[serde(rename = "type")] + #[serde(with = "with_repr")] + pub result_type: SpectreResultType, + pub password: Option, + #[serde(with = "with_repr")] + pub login_type: SpectreResultType, + pub login_name: Option, + pub uses: u32, + pub last_used: NaiveDateTime, +} + +impl Default for SpectreSiteConfig { + fn default() -> Self { + Self { + algorithm: SpectreAlgorithm::Current, + counter: SpectreCounter::Default, + result_type: SpectreResultType::SpectreResultDefaultResult, + password: None, + login_type: SpectreResultType::SpectreResultDefaultLogin, + login_name: None, + uses: 0, + last_used: Default::default(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct SpectreSite { + pub username: String, + pub site_name: String, + pub config: SpectreSiteConfig, +} + +mod with_repr { + use serde::{Deserialize, Deserializer, Serializer}; + use spectre_api_sys::{SpectreAlgorithm, SpectreResultType}; + + pub trait ReprConvert: Copy { + type Repr: Copy; + + fn into_repr(self) -> Self::Repr; + fn from_repr(repr: Self::Repr) -> Self; + } + + impl ReprConvert for SpectreAlgorithm { + type Repr = u32; + + fn from_repr(repr: Self::Repr) -> Self { + unsafe { core::mem::transmute::(repr) } + } + + fn into_repr(self) -> Self::Repr { + self as Self::Repr + } + } + + impl ReprConvert for SpectreResultType { + type Repr = u32; + + fn from_repr(repr: Self::Repr) -> Self { + unsafe { core::mem::transmute::(repr) } + } + + fn into_repr(self) -> Self::Repr { + self as Self::Repr + } + } + + pub fn serialize(value: &T, serializer: S) -> Result + where + T: ReprConvert, + S: Serializer, + { + serializer.serialize_u32(value.into_repr()) + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: ReprConvert, + D: Deserializer<'de>, + { + ::deserialize(deserializer).map(T::from_repr) + } +}