use core::{
fmt::{self, Display, Formatter, Write as _},
str::FromStr,
};
use cbor_smol::{cbor_deserialize, cbor_serialize_to};
use heapless::{string::StringView, VecView};
use littlefs2_core::{path, Path};
use serde::{de::DeserializeOwned, Serialize};
use trussed::store::Filestore;
use trussed_core::{
reset_signal::ResetSignalAllocation,
try_syscall,
types::{Location, Message},
FilesystemClient,
};
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,
String,
}
#[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)]
#[non_exhaustive]
pub enum ConfigValueMut<'a> {
Bool(&'a mut bool),
U8(&'a mut u8),
String(&'a mut StringView),
}
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),
Self::String(r) => {
if value.len() > r.capacity() {
return Err(ConfigError::DataTooLong);
}
r.clear();
r.push_str(value).map_err(|_| ConfigError::DataTooLong)
}
}
}
}
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}"),
Self::String(value) => f.write_str(value),
}
}
}
macro_rules! enum_u8 {
(
$(#[$outer:meta])*
$vis:vis enum $name:ident {
$($(#[$attr:meta])* $var:ident = $num:expr),+
$(,)*
}
) => {
$(#[$outer])*
#[repr(u8)]
$vis enum $name {
$(
$(#[$attr])*
$var = $num,
)*
}
impl $name {
const fn from_repr(val: u8) -> Option<$name> {
match val {
$(
$num => Some($name::$var),
)*
_ => None,
}
}
}
}
}
enum_u8!(
#[derive(Debug)]
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")
);
}
#[test]
fn field_type_ids() {
for (ty, id) in [
(FieldType::Bool, hex!("00").as_slice()),
(FieldType::U8, hex!("01").as_slice()),
(FieldType::String, hex!("02").as_slice()),
] {
let mut bytes: heapless::Vec<u8, 8> = Default::default();
cbor_smol::cbor_serialize_to(&ty, &mut bytes).unwrap();
assert_eq!(bytes.as_slice(), id);
}
}
#[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]
struct TestConfig {
label: heapless::String<8>,
}
impl Config for TestConfig {
fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>> {
match key {
"label" => Some(ConfigValueMut::String(self.label.as_mut_view())),
_ => None,
}
}
fn migration_version(&self) -> Option<u32> {
None
}
fn set_migration_version(&mut self, _version: u32) -> bool {
false
}
fn list_available_fields(&self) -> &'static [ConfigField] {
&[]
}
}
fn get_field(config: &mut TestConfig, key: &str) -> Result<heapless::String<32>, ConfigError> {
let mut response: heapless::Vec<u8, 32> = Default::default();
get(config, key, response.as_mut_view())?;
Ok(core::str::from_utf8(&response).unwrap().try_into().unwrap())
}
#[test]
fn string_field() {
let mut config = TestConfig::default();
assert_eq!(get_field(&mut config, "label").unwrap(), "");
set(&mut config, "label", "Backup").unwrap();
assert_eq!(config.label, "Backup");
assert_eq!(get_field(&mut config, "label").unwrap(), "Backup");
set(&mut config, "label", "12345678").unwrap();
assert_eq!(config.label, "12345678");
set(&mut config, "label", "").unwrap();
assert_eq!(config.label, "");
}
#[test]
fn string_field_too_long() {
let mut config = TestConfig::default();
set(&mut config, "label", "old").unwrap();
let error = set(&mut config, "label", "123456789").unwrap_err();
assert!(matches!(error, ConfigError::DataTooLong), "{error:?}");
assert_eq!(config.label, "old");
}
#[test]
fn string_field_arbitrary_utf8() {
let mut config = TestConfig::default();
for value in ["a\nb", "\x1b[2J", "\u{202e}", "\u{2028}", "Χ’ΧΧ¨"] {
set(&mut config, "label", value).unwrap_or_else(|e| panic!("{value:?}: {e:?}"));
assert_eq!(config.label, value);
}
}
#[test]
fn string_field_multibyte() {
let mut config = TestConfig::default();
set(&mut config, "label", "ππ").unwrap();
assert_eq!(config.label, "ππ");
assert_eq!(config.label.len(), 8);
let error = set(&mut config, "label", "δΈδΈδΈ").unwrap_err();
assert!(matches!(error, ConfigError::DataTooLong), "{error:?}");
assert_eq!(config.label, "ππ");
}
}