use core::{
fmt::{self, Display, Formatter, Write as _},
str::FromStr,
sync::atomic::{AtomicU8, Ordering},
};
use cbor_smol::{cbor_deserialize, cbor_serialize_to};
use heapless::VecView;
use littlefs2_core::{path, Path};
use serde::{de::DeserializeOwned, Serialize};
use strum_macros::FromRepr;
use trussed::store::Filestore;
use trussed_core::{
try_syscall,
types::{Location, Message},
FilesystemClient,
};
#[derive(Debug)]
pub struct ResetSignalAllocation(AtomicU8);
impl Default for ResetSignalAllocation {
fn default() -> Self {
Self::new()
}
}
impl ResetSignalAllocation {
pub const fn new() -> Self {
Self(AtomicU8::new(ResetSignal::None as u8))
}
pub fn load(&self) -> ResetSignal {
let v = self.0.load(Ordering::Relaxed);
ResetSignal::from_repr(v).expect("A reset signal value")
}
pub fn set_factory_reset(&self) -> bool {
self.0
.compare_exchange(
ResetSignal::None as u8,
ResetSignal::FactoryReset as u8,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok()
}
pub fn set_config_changed(&self) {
self.0
.store(ResetSignal::ConfigChanged as u8, Ordering::Relaxed)
}
pub fn ack_factory_reset(&self) -> bool {
self.0
.compare_exchange(
ResetSignal::FactoryReset as u8,
ResetSignal::None as u8,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok()
}
}
#[derive(Debug, FromRepr, Default)]
#[repr(u8)]
pub enum ResetSignal {
#[default]
None,
FactoryReset,
ConfigChanged,
}
const LOCATION: Location = Location::Internal;
const FILENAME: &Path = path!("config");
#[derive(Debug, Clone, Copy)]
pub enum ResetConfigResult {
Changed,
Unchanged,
WrongKey,
}
impl ResetConfigResult {
pub fn is_changed(&self) -> bool {
matches!(self, Self::Changed)
}
pub fn is_unchanged(&self) -> bool {
matches!(self, Self::Unchanged)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::WrongKey)
}
}
pub trait Config: Default + PartialEq + DeserializeOwned + Serialize {
fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>>;
fn reset_client_id(
&self,
_key: &str,
) -> Option<(&'static Path, &'static ResetSignalAllocation)> {
None
}
fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
ResetConfigResult::WrongKey
}
fn migration_version(&self) -> Option<u32>;
fn set_migration_version(&mut self, _version: u32) -> bool;
fn list_available_fields(&self) -> &'static [ConfigField];
}
#[derive(Serialize)]
#[non_exhaustive]
pub enum FieldType {
Bool,
U8,
}
#[derive(Serialize)]
pub struct ConfigField {
#[serde(rename = "n")]
pub name: &'static str,
#[serde(rename = "c")]
pub requires_touch_confirmation: bool,
#[serde(rename = "r")]
pub requires_reboot: bool,
#[serde(rename = "d")]
pub destructive: bool,
#[serde(rename = "t")]
pub ty: FieldType,
}
impl Config for () {
fn field(&mut self, _key: &str) -> Option<ConfigValueMut<'_>> {
None
}
fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
ResetConfigResult::WrongKey
}
fn migration_version(&self) -> Option<u32> {
None
}
fn set_migration_version(&mut self, _version: u32) -> bool {
false
}
fn list_available_fields(&self) -> &'static [ConfigField] {
&[]
}
}
#[derive(Debug, Serialize)]
pub enum ConfigValueMut<'a> {
Bool(&'a mut bool),
U8(&'a mut u8),
}
impl<'a> ConfigValueMut<'a> {
fn set(&mut self, value: &str) -> Result<(), ConfigError> {
fn set_value<T: FromStr>(target: &mut T, s: &str) -> Result<(), ConfigError> {
*target = s.parse().map_err(|_| ConfigError::InvalidValue)?;
Ok(())
}
match self {
Self::Bool(r) => set_value(*r, value),
Self::U8(r) => set_value(*r, value),
}
}
}
impl<'a> Display for ConfigValueMut<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Bool(value) => write!(f, "{value}"),
Self::U8(value) => write!(f, "{value}"),
}
}
}
#[derive(Debug, FromRepr)]
#[repr(u8)]
pub enum ConfigError {
ReadFailed = 1,
WriteFailed = 2,
DeserializationFailed = 3,
SerializationFailed = 4,
InvalidKey = 5,
InvalidValue = 6,
DataTooLong = 7,
NotConfirmed = 8,
}
const _: () = assert!(
ConfigError::from_repr(0).is_none(),
"ConfigError may not have a variant with discriminant zero as zero indicates success.",
);
impl From<ConfigError> for u8 {
fn from(error: ConfigError) -> u8 {
error as _
}
}
pub fn get<C: Config>(
config: &mut C,
key: &str,
response: &mut VecView<u8>,
) -> Result<(), ConfigError> {
let field = config.field(key).ok_or(ConfigError::InvalidKey)?;
write!(response, "{field}").map_err(|_| ConfigError::DataTooLong)
}
pub fn set<C: Config>(config: &mut C, key: &str, value: &str) -> Result<(), ConfigError> {
config
.field(key)
.ok_or(ConfigError::InvalidKey)?
.set(value)?;
Ok(())
}
pub fn load<F: Filestore, C: Config>(store: &mut F) -> Result<C, ConfigError> {
let Some(data) = load_if_exists(store, LOCATION, FILENAME)? else {
return Ok(Default::default());
};
cbor_deserialize(&data).map_err(|_| ConfigError::DeserializationFailed)
}
pub fn save_filestore<F: Filestore, C: Config>(
store: &mut F,
config: &C,
) -> Result<(), ConfigError> {
if config == &C::default() {
if store.exists(FILENAME, LOCATION) {
store
.remove_file(FILENAME, LOCATION)
.map_err(|_| ConfigError::WriteFailed)?;
}
} else {
let mut data = Message::new();
cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
store
.write(FILENAME, LOCATION, &data)
.map_err(|_| ConfigError::SerializationFailed)?;
}
Ok(())
}
pub fn save<T: FilesystemClient, C: Config>(client: &mut T, config: &C) -> Result<(), ConfigError> {
if config == &Default::default() {
if exists(client, LOCATION, FILENAME)? {
try_syscall!(client.remove_file(LOCATION, FILENAME.into()))
.map_err(|_| ConfigError::WriteFailed)?;
}
} else {
let mut data = Message::new();
cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
try_syscall!(client.write_file(LOCATION, FILENAME.into(), data, None))
.map_err(|_| ConfigError::WriteFailed)?;
}
Ok(())
}
fn exists<T: FilesystemClient>(
client: &mut T,
location: Location,
path: &Path,
) -> Result<bool, ConfigError> {
try_syscall!(client.entry_metadata(location, path.into()))
.map(|r| r.metadata.is_some())
.map_err(|_| ConfigError::ReadFailed)
}
fn load_if_exists<F: Filestore>(
store: &mut F,
location: Location,
path: &Path,
) -> Result<Option<Message>, ConfigError> {
store.read(path, location).map(Some).or_else(|_| {
if store.exists(path, location) {
Err(ConfigError::ReadFailed)
} else {
Ok(None)
}
})
}
#[cfg(test)]
mod tests {
use hex_literal::hex;
use super::*;
#[test]
fn config_field() {
let fields = &[ConfigField {
name: "test_name",
requires_touch_confirmation: true,
requires_reboot: false,
destructive: true,
ty: FieldType::Bool,
}];
let mut bytes: heapless::Vec<u8, 100> = Default::default();
cbor_smol::cbor_serialize_to(fields, &mut bytes).unwrap();
assert_eq!(
&bytes,
&hex!("81A5616E69746573745F6E616D656163F56172F46164F5617400")
);
}
}