1use core::{
2 fmt::{self, Display, Formatter, Write as _},
3 str::FromStr,
4};
5
6use cbor_smol::{cbor_deserialize, cbor_serialize_to};
7use heapless::{string::StringView, VecView};
8use littlefs2_core::{path, Path};
9use serde::{de::DeserializeOwned, Serialize};
10use trussed::store::Filestore;
11use trussed_core::{
12 reset_signal::ResetSignalAllocation,
13 try_syscall,
14 types::{Location, Message},
15 FilesystemClient,
16};
17
18const LOCATION: Location = Location::Internal;
19const FILENAME: &Path = path!("config");
20
21#[derive(Debug, Clone, Copy)]
22pub enum ResetConfigResult {
23 Changed,
25 Unchanged,
27 WrongKey,
29}
30
31impl ResetConfigResult {
32 pub fn is_changed(&self) -> bool {
33 matches!(self, Self::Changed)
34 }
35 pub fn is_unchanged(&self) -> bool {
36 matches!(self, Self::Unchanged)
37 }
38 pub fn is_error(&self) -> bool {
39 matches!(self, Self::WrongKey)
40 }
41}
42
43pub trait Config: Default + PartialEq + DeserializeOwned + Serialize {
44 fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>>;
45
46 fn reset_client_id(
55 &self,
56 _key: &str,
57 ) -> Option<(&'static Path, &'static ResetSignalAllocation)> {
58 None
59 }
60
61 fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
65 ResetConfigResult::WrongKey
66 }
67
68 fn migration_version(&self) -> Option<u32>;
72
73 fn set_migration_version(&mut self, _version: u32) -> bool;
77
78 fn list_available_fields(&self) -> &'static [ConfigField];
79}
80
81#[derive(Serialize)]
85#[non_exhaustive]
86pub enum FieldType {
87 Bool,
88 U8,
89 String,
93}
94
95#[derive(Serialize)]
96pub struct ConfigField {
97 #[serde(rename = "n")]
98 pub name: &'static str,
99 #[serde(rename = "c")]
101 pub requires_touch_confirmation: bool,
102 #[serde(rename = "r")]
104 pub requires_reboot: bool,
105 #[serde(rename = "d")]
107 pub destructive: bool,
108 #[serde(rename = "t")]
110 pub ty: FieldType,
111}
112
113impl Config for () {
114 fn field(&mut self, _key: &str) -> Option<ConfigValueMut<'_>> {
115 None
116 }
117
118 fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
119 ResetConfigResult::WrongKey
120 }
121
122 fn migration_version(&self) -> Option<u32> {
123 None
124 }
125
126 fn set_migration_version(&mut self, _version: u32) -> bool {
127 false
128 }
129
130 fn list_available_fields(&self) -> &'static [ConfigField] {
131 &[]
132 }
133}
134
135#[derive(Debug, Serialize)]
136#[non_exhaustive]
137pub enum ConfigValueMut<'a> {
138 Bool(&'a mut bool),
139 U8(&'a mut u8),
140 String(&'a mut StringView),
142}
143
144impl<'a> ConfigValueMut<'a> {
145 fn set(&mut self, value: &str) -> Result<(), ConfigError> {
146 fn set_value<T: FromStr>(target: &mut T, s: &str) -> Result<(), ConfigError> {
147 *target = s.parse().map_err(|_| ConfigError::InvalidValue)?;
148 Ok(())
149 }
150
151 match self {
152 Self::Bool(r) => set_value(*r, value),
153 Self::U8(r) => set_value(*r, value),
154 Self::String(r) => {
155 if value.len() > r.capacity() {
158 return Err(ConfigError::DataTooLong);
159 }
160 r.clear();
161 r.push_str(value).map_err(|_| ConfigError::DataTooLong)
162 }
163 }
164 }
165}
166
167impl<'a> Display for ConfigValueMut<'a> {
168 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
169 match self {
170 Self::Bool(value) => write!(f, "{value}"),
171 Self::U8(value) => write!(f, "{value}"),
172 Self::String(value) => f.write_str(value),
173 }
174 }
175}
176
177macro_rules! enum_u8 {
178 (
179 $(#[$outer:meta])*
180 $vis:vis enum $name:ident {
181 $($(#[$attr:meta])* $var:ident = $num:expr),+
182 $(,)*
183 }
184 ) => {
185 $(#[$outer])*
186 #[repr(u8)]
187 $vis enum $name {
188 $(
189 $(#[$attr])*
190 $var = $num,
191 )*
192 }
193
194 impl $name {
195 const fn from_repr(val: u8) -> Option<$name> {
196 match val {
197 $(
198 $num => Some($name::$var),
199 )*
200 _ => None,
201 }
202 }
203 }
204 }
205}
206
207enum_u8!(
208 #[derive(Debug)]
209 pub enum ConfigError {
210 ReadFailed = 1,
211 WriteFailed = 2,
212 DeserializationFailed = 3,
213 SerializationFailed = 4,
214 InvalidKey = 5,
215 InvalidValue = 6,
216 DataTooLong = 7,
217 NotConfirmed = 8,
218 }
219);
220const _: () = assert!(
221 ConfigError::from_repr(0).is_none(),
222 "ConfigError may not have a variant with discriminant zero as zero indicates success.",
223);
224
225impl From<ConfigError> for u8 {
226 fn from(error: ConfigError) -> u8 {
227 error as _
228 }
229}
230
231pub fn get<C: Config>(
232 config: &mut C,
233 key: &str,
234 response: &mut VecView<u8>,
235) -> Result<(), ConfigError> {
236 let field = config.field(key).ok_or(ConfigError::InvalidKey)?;
237 write!(response, "{field}").map_err(|_| ConfigError::DataTooLong)
238}
239
240pub fn set<C: Config>(config: &mut C, key: &str, value: &str) -> Result<(), ConfigError> {
241 config
242 .field(key)
243 .ok_or(ConfigError::InvalidKey)?
244 .set(value)?;
245 Ok(())
246}
247
248pub fn load<F: Filestore, C: Config>(store: &mut F) -> Result<C, ConfigError> {
249 let Some(data) = load_if_exists(store, LOCATION, FILENAME)? else {
250 return Ok(Default::default());
251 };
252 cbor_deserialize(&data).map_err(|_| ConfigError::DeserializationFailed)
253}
254
255pub fn save_filestore<F: Filestore, C: Config>(
256 store: &mut F,
257 config: &C,
258) -> Result<(), ConfigError> {
259 if config == &C::default() {
260 if store.exists(FILENAME, LOCATION) {
261 store
262 .remove_file(FILENAME, LOCATION)
263 .map_err(|_| ConfigError::WriteFailed)?;
264 }
265 } else {
266 let mut data = Message::new();
267 cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
268 store
269 .write(FILENAME, LOCATION, &data)
270 .map_err(|_| ConfigError::SerializationFailed)?;
271 }
272 Ok(())
273}
274
275pub fn save<T: FilesystemClient, C: Config>(client: &mut T, config: &C) -> Result<(), ConfigError> {
276 if config == &Default::default() {
277 if exists(client, LOCATION, FILENAME)? {
278 try_syscall!(client.remove_file(LOCATION, FILENAME.into()))
279 .map_err(|_| ConfigError::WriteFailed)?;
280 }
281 } else {
282 let mut data = Message::new();
283 cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
284 try_syscall!(client.write_file(LOCATION, FILENAME.into(), data, None))
285 .map_err(|_| ConfigError::WriteFailed)?;
286 }
287 Ok(())
288}
289
290fn exists<T: FilesystemClient>(
291 client: &mut T,
292 location: Location,
293 path: &Path,
294) -> Result<bool, ConfigError> {
295 try_syscall!(client.entry_metadata(location, path.into()))
296 .map(|r| r.metadata.is_some())
297 .map_err(|_| ConfigError::ReadFailed)
298}
299
300fn load_if_exists<F: Filestore>(
301 store: &mut F,
302 location: Location,
303 path: &Path,
304) -> Result<Option<Message>, ConfigError> {
305 store.read(path, location).map(Some).or_else(|_| {
306 if store.exists(path, location) {
307 Err(ConfigError::ReadFailed)
308 } else {
309 Ok(None)
310 }
311 })
312}
313
314#[cfg(test)]
315mod tests {
316 use hex_literal::hex;
317
318 use super::*;
319
320 #[test]
321 fn config_field() {
322 let fields = &[ConfigField {
323 name: "test_name",
324 requires_touch_confirmation: true,
325 requires_reboot: false,
326 destructive: true,
327 ty: FieldType::Bool,
328 }];
329 let mut bytes: heapless::Vec<u8, 100> = Default::default();
330 cbor_smol::cbor_serialize_to(fields, &mut bytes).unwrap();
331 assert_eq!(
332 &bytes,
333 &hex!("81A5616E69746573745F6E616D656163F56172F46164F5617400")
334 );
335 }
336
337 #[test]
339 fn field_type_ids() {
340 for (ty, id) in [
341 (FieldType::Bool, hex!("00").as_slice()),
342 (FieldType::U8, hex!("01").as_slice()),
343 (FieldType::String, hex!("02").as_slice()),
344 ] {
345 let mut bytes: heapless::Vec<u8, 8> = Default::default();
346 cbor_smol::cbor_serialize_to(&ty, &mut bytes).unwrap();
347 assert_eq!(bytes.as_slice(), id);
348 }
349 }
350
351 #[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]
352 struct TestConfig {
353 label: heapless::String<8>,
354 }
355
356 impl Config for TestConfig {
357 fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>> {
358 match key {
359 "label" => Some(ConfigValueMut::String(self.label.as_mut_view())),
360 _ => None,
361 }
362 }
363
364 fn migration_version(&self) -> Option<u32> {
365 None
366 }
367
368 fn set_migration_version(&mut self, _version: u32) -> bool {
369 false
370 }
371
372 fn list_available_fields(&self) -> &'static [ConfigField] {
373 &[]
374 }
375 }
376
377 fn get_field(config: &mut TestConfig, key: &str) -> Result<heapless::String<32>, ConfigError> {
378 let mut response: heapless::Vec<u8, 32> = Default::default();
379 get(config, key, response.as_mut_view())?;
380 Ok(core::str::from_utf8(&response).unwrap().try_into().unwrap())
381 }
382
383 #[test]
384 fn string_field() {
385 let mut config = TestConfig::default();
386 assert_eq!(get_field(&mut config, "label").unwrap(), "");
387
388 set(&mut config, "label", "Backup").unwrap();
389 assert_eq!(config.label, "Backup");
390 assert_eq!(get_field(&mut config, "label").unwrap(), "Backup");
391
392 set(&mut config, "label", "12345678").unwrap();
393 assert_eq!(config.label, "12345678");
394 set(&mut config, "label", "").unwrap();
395 assert_eq!(config.label, "");
396 }
397
398 #[test]
399 fn string_field_too_long() {
400 let mut config = TestConfig::default();
401 set(&mut config, "label", "old").unwrap();
402
403 let error = set(&mut config, "label", "123456789").unwrap_err();
404 assert!(matches!(error, ConfigError::DataTooLong), "{error:?}");
405 assert_eq!(config.label, "old");
407 }
408
409 #[test]
411 fn string_field_arbitrary_utf8() {
412 let mut config = TestConfig::default();
413
414 for value in ["a\nb", "\x1b[2J", "\u{202e}", "\u{2028}", "Χ’ΧΧ¨"] {
415 set(&mut config, "label", value).unwrap_or_else(|e| panic!("{value:?}: {e:?}"));
416 assert_eq!(config.label, value);
417 }
418 }
419
420 #[test]
422 fn string_field_multibyte() {
423 let mut config = TestConfig::default();
424
425 set(&mut config, "label", "ππ").unwrap();
427 assert_eq!(config.label, "ππ");
428 assert_eq!(config.label.len(), 8);
429
430 let error = set(&mut config, "label", "δΈδΈδΈ").unwrap_err();
431 assert!(matches!(error, ConfigError::DataTooLong), "{error:?}");
432 assert_eq!(config.label, "ππ");
433 }
434}