Skip to main content

admin_app/
config.rs

1use core::{
2    fmt::{self, Display, Formatter, Write as _},
3    str::FromStr,
4    sync::atomic::{AtomicU8, Ordering},
5};
6
7use cbor_smol::{cbor_deserialize, cbor_serialize_to};
8use heapless::{string::StringView, VecView};
9use littlefs2_core::{path, Path};
10use serde::{de::DeserializeOwned, Serialize};
11use strum_macros::FromRepr;
12use trussed::store::Filestore;
13use trussed_core::{
14    try_syscall,
15    types::{Location, Message},
16    FilesystemClient,
17};
18
19#[derive(Debug)]
20/// Structure meant to be stored in  a `static` to signal applications that they have been factory-resetted by the admin app
21///
22/// It is expected to have one such structure for each application supporting factory-reset by the admin-app
23///
24/// ```rust,ignore
25///# use admin_app::{ResetSignalAllocation, ConfigValueMut};
26///# use littlefs2::{path::Path, path};
27/// #[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]
28/// struct Config {
29///    use_new_backend: bool,
30///};
31/// static OPCARD_RESET: ResetSignalAllocation = ResetSignalAllocation::new();
32/// impl admin_app::Config for Config {
33///     fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>> {
34///         match key {
35///             "opcard.use_new_backend" => Some(ConfigValueMut::Bool(&mut self.use_new_backend)),
36///             _ => None,
37///         }
38///     }
39///     /// Client ID to factory-reset if the associated configuration option is changed
40///     fn reset_client_id(&self, key: &str) -> Option<(&'static Path, &'static ResetSignalAllocation)> {
41///         match key {
42///             "opcard" => Some((path!("opcard"), &OPCARD_RESET)),
43///             "opcard.use_new_backend" =>Some((path!("opcard"), &OPCARD_RESET)),
44///             _ => None,
45///         }
46///     }
47/// }
48/// ```
49pub struct ResetSignalAllocation(AtomicU8);
50
51impl Default for ResetSignalAllocation {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl ResetSignalAllocation {
58    pub const fn new() -> Self {
59        Self(AtomicU8::new(ResetSignal::None as u8))
60    }
61
62    pub fn load(&self) -> ResetSignal {
63        let v = self.0.load(Ordering::Relaxed);
64        ResetSignal::from_repr(v).expect("A reset signal value")
65    }
66
67    pub fn set_factory_reset(&self) -> bool {
68        self.0
69            .compare_exchange(
70                ResetSignal::None as u8,
71                ResetSignal::FactoryReset as u8,
72                Ordering::Relaxed,
73                Ordering::Relaxed,
74            )
75            .is_ok()
76    }
77
78    pub fn set_config_changed(&self) {
79        self.0
80            .store(ResetSignal::ConfigChanged as u8, Ordering::Relaxed)
81    }
82
83    /// Factory reset can be acknowledged so that the application can restart working
84    ///
85    /// A configuration change cannot be acknowledged as it requires a power cycle to be taken into account.
86    pub fn ack_factory_reset(&self) -> bool {
87        self.0
88            .compare_exchange(
89                ResetSignal::FactoryReset as u8,
90                ResetSignal::None as u8,
91                Ordering::Relaxed,
92                Ordering::Relaxed,
93            )
94            .is_ok()
95    }
96}
97
98#[derive(Debug, FromRepr, Default)]
99#[repr(u8)]
100pub enum ResetSignal {
101    #[default]
102    /// The App can continue operating
103    None,
104    /// The app has had it state factory reseted by the admin app
105    ///
106    /// It should delete any runtime state it is currently holding, then [`acknowledge`](ResetSignalAllocation::ack_factory_reset) the reset and continue working.
107    FactoryReset,
108    /// A configuration relevant to the application has been changed.
109    ///
110    /// The application must reject all incoming request and store no persistent state until a power cycle.
111    ConfigChanged,
112}
113
114const LOCATION: Location = Location::Internal;
115const FILENAME: &Path = path!("config");
116
117#[derive(Debug, Clone, Copy)]
118pub enum ResetConfigResult {
119    /// The config was changed as a result of the reset to default
120    Changed,
121    /// The config was at the default value
122    Unchanged,
123    /// The key does not correspond to any application that can be reset
124    WrongKey,
125}
126
127impl ResetConfigResult {
128    pub fn is_changed(&self) -> bool {
129        matches!(self, Self::Changed)
130    }
131    pub fn is_unchanged(&self) -> bool {
132        matches!(self, Self::Unchanged)
133    }
134    pub fn is_error(&self) -> bool {
135        matches!(self, Self::WrongKey)
136    }
137}
138
139pub trait Config: Default + PartialEq + DeserializeOwned + Serialize {
140    fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>>;
141
142    /// Client ID to factory-reset if the associated configuration option is changed
143    ///
144    /// # If the Request is for a `client_id`:
145    ///
146    /// - MUST return `Some` to indicate that the client can be factory reset by the admin app,
147    ///   In that case, the path is the clientid that must be reset, and the allocation must point to a
148    ///   signal that id checked by the application.
149    /// - MUST return None otherwise.
150    fn reset_client_id(
151        &self,
152        _key: &str,
153    ) -> Option<(&'static Path, &'static ResetSignalAllocation)> {
154        None
155    }
156
157    /// Reset the config of a client to its default value
158    ///
159    /// Returns `true` if the config has been changed as a result
160    fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
161        ResetConfigResult::WrongKey
162    }
163
164    /// The migration version
165    ///
166    /// Return None if the configuration does not support storing the migration version
167    fn migration_version(&self) -> Option<u32>;
168
169    /// Set the migration version
170    ///
171    /// Return false if the configuration does not support storing the migration version
172    fn set_migration_version(&mut self, _version: u32) -> bool;
173
174    fn list_available_fields(&self) -> &'static [ConfigField];
175}
176
177// No need to rename, cbor-smol already packs enum using ids
178//
179// As the variants are serialized as their index, new variants may only be appended
180#[derive(Serialize)]
181#[non_exhaustive]
182pub enum FieldType {
183    Bool,
184    U8,
185    /// A UTF-8 string
186    ///
187    /// The maximum length is defined by the config struct holding the value, not by this type
188    String,
189}
190
191#[derive(Serialize)]
192pub struct ConfigField {
193    #[serde(rename = "n")]
194    pub name: &'static str,
195    /// Changing the config field requires a touch
196    #[serde(rename = "c")]
197    pub requires_touch_confirmation: bool,
198    /// Changing the config field requires a power cycle
199    #[serde(rename = "r")]
200    pub requires_reboot: bool,
201    /// Changing the config field deletes data
202    #[serde(rename = "d")]
203    pub destructive: bool,
204    /// The type of data stored in this field
205    #[serde(rename = "t")]
206    pub ty: FieldType,
207}
208
209impl Config for () {
210    fn field(&mut self, _key: &str) -> Option<ConfigValueMut<'_>> {
211        None
212    }
213
214    fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
215        ResetConfigResult::WrongKey
216    }
217
218    fn migration_version(&self) -> Option<u32> {
219        None
220    }
221
222    fn set_migration_version(&mut self, _version: u32) -> bool {
223        false
224    }
225
226    fn list_available_fields(&self) -> &'static [ConfigField] {
227        &[]
228    }
229}
230
231#[derive(Debug, Serialize)]
232#[non_exhaustive]
233pub enum ConfigValueMut<'a> {
234    Bool(&'a mut bool),
235    U8(&'a mut u8),
236    /// A string of any capacity, obtained from a `heapless::String<N>` with `as_mut_view`
237    String(&'a mut StringView),
238}
239
240impl<'a> ConfigValueMut<'a> {
241    fn set(&mut self, value: &str) -> Result<(), ConfigError> {
242        fn set_value<T: FromStr>(target: &mut T, s: &str) -> Result<(), ConfigError> {
243            *target = s.parse().map_err(|_| ConfigError::InvalidValue)?;
244            Ok(())
245        }
246
247        match self {
248            Self::Bool(r) => set_value(*r, value),
249            Self::U8(r) => set_value(*r, value),
250            Self::String(r) => {
251                // Check the capacity before clearing so that a rejected value leaves the stored
252                // one intact
253                if value.len() > r.capacity() {
254                    return Err(ConfigError::DataTooLong);
255                }
256                r.clear();
257                r.push_str(value).map_err(|_| ConfigError::DataTooLong)
258            }
259        }
260    }
261}
262
263impl<'a> Display for ConfigValueMut<'a> {
264    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
265        match self {
266            Self::Bool(value) => write!(f, "{value}"),
267            Self::U8(value) => write!(f, "{value}"),
268            Self::String(value) => f.write_str(value),
269        }
270    }
271}
272
273#[derive(Debug, FromRepr)]
274#[repr(u8)]
275pub enum ConfigError {
276    ReadFailed = 1,
277    WriteFailed = 2,
278    DeserializationFailed = 3,
279    SerializationFailed = 4,
280    InvalidKey = 5,
281    InvalidValue = 6,
282    DataTooLong = 7,
283    NotConfirmed = 8,
284}
285
286const _: () = assert!(
287    ConfigError::from_repr(0).is_none(),
288    "ConfigError may not have a variant with discriminant zero as zero indicates success.",
289);
290
291impl From<ConfigError> for u8 {
292    fn from(error: ConfigError) -> u8 {
293        error as _
294    }
295}
296
297pub fn get<C: Config>(
298    config: &mut C,
299    key: &str,
300    response: &mut VecView<u8>,
301) -> Result<(), ConfigError> {
302    let field = config.field(key).ok_or(ConfigError::InvalidKey)?;
303    write!(response, "{field}").map_err(|_| ConfigError::DataTooLong)
304}
305
306pub fn set<C: Config>(config: &mut C, key: &str, value: &str) -> Result<(), ConfigError> {
307    config
308        .field(key)
309        .ok_or(ConfigError::InvalidKey)?
310        .set(value)?;
311    Ok(())
312}
313
314pub fn load<F: Filestore, C: Config>(store: &mut F) -> Result<C, ConfigError> {
315    let Some(data) = load_if_exists(store, LOCATION, FILENAME)? else {
316        return Ok(Default::default());
317    };
318    cbor_deserialize(&data).map_err(|_| ConfigError::DeserializationFailed)
319}
320
321pub fn save_filestore<F: Filestore, C: Config>(
322    store: &mut F,
323    config: &C,
324) -> Result<(), ConfigError> {
325    if config == &C::default() {
326        if store.exists(FILENAME, LOCATION) {
327            store
328                .remove_file(FILENAME, LOCATION)
329                .map_err(|_| ConfigError::WriteFailed)?;
330        }
331    } else {
332        let mut data = Message::new();
333        cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
334        store
335            .write(FILENAME, LOCATION, &data)
336            .map_err(|_| ConfigError::SerializationFailed)?;
337    }
338    Ok(())
339}
340
341pub fn save<T: FilesystemClient, C: Config>(client: &mut T, config: &C) -> Result<(), ConfigError> {
342    if config == &Default::default() {
343        if exists(client, LOCATION, FILENAME)? {
344            try_syscall!(client.remove_file(LOCATION, FILENAME.into()))
345                .map_err(|_| ConfigError::WriteFailed)?;
346        }
347    } else {
348        let mut data = Message::new();
349        cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
350        try_syscall!(client.write_file(LOCATION, FILENAME.into(), data, None))
351            .map_err(|_| ConfigError::WriteFailed)?;
352    }
353    Ok(())
354}
355
356fn exists<T: FilesystemClient>(
357    client: &mut T,
358    location: Location,
359    path: &Path,
360) -> Result<bool, ConfigError> {
361    try_syscall!(client.entry_metadata(location, path.into()))
362        .map(|r| r.metadata.is_some())
363        .map_err(|_| ConfigError::ReadFailed)
364}
365
366fn load_if_exists<F: Filestore>(
367    store: &mut F,
368    location: Location,
369    path: &Path,
370) -> Result<Option<Message>, ConfigError> {
371    store.read(path, location).map(Some).or_else(|_| {
372        if store.exists(path, location) {
373            Err(ConfigError::ReadFailed)
374        } else {
375            Ok(None)
376        }
377    })
378}
379
380#[cfg(test)]
381mod tests {
382    use hex_literal::hex;
383
384    use super::*;
385
386    #[test]
387    fn config_field() {
388        let fields = &[ConfigField {
389            name: "test_name",
390            requires_touch_confirmation: true,
391            requires_reboot: false,
392            destructive: true,
393            ty: FieldType::Bool,
394        }];
395        let mut bytes: heapless::Vec<u8, 100> = Default::default();
396        cbor_smol::cbor_serialize_to(fields, &mut bytes).unwrap();
397        assert_eq!(
398            &bytes,
399            &hex!("81A5616E69746573745F6E616D656163F56172F46164F5617400")
400        );
401    }
402
403    // The field types are parsed as integers by the hosts, so their values may never change
404    #[test]
405    fn field_type_ids() {
406        for (ty, id) in [
407            (FieldType::Bool, hex!("00").as_slice()),
408            (FieldType::U8, hex!("01").as_slice()),
409            (FieldType::String, hex!("02").as_slice()),
410        ] {
411            let mut bytes: heapless::Vec<u8, 8> = Default::default();
412            cbor_smol::cbor_serialize_to(&ty, &mut bytes).unwrap();
413            assert_eq!(bytes.as_slice(), id);
414        }
415    }
416
417    #[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]
418    struct TestConfig {
419        label: heapless::String<8>,
420    }
421
422    impl Config for TestConfig {
423        fn field(&mut self, key: &str) -> Option<ConfigValueMut<'_>> {
424            match key {
425                "label" => Some(ConfigValueMut::String(self.label.as_mut_view())),
426                _ => None,
427            }
428        }
429
430        fn migration_version(&self) -> Option<u32> {
431            None
432        }
433
434        fn set_migration_version(&mut self, _version: u32) -> bool {
435            false
436        }
437
438        fn list_available_fields(&self) -> &'static [ConfigField] {
439            &[]
440        }
441    }
442
443    fn get_field(config: &mut TestConfig, key: &str) -> Result<heapless::String<32>, ConfigError> {
444        let mut response: heapless::Vec<u8, 32> = Default::default();
445        get(config, key, response.as_mut_view())?;
446        Ok(core::str::from_utf8(&response).unwrap().try_into().unwrap())
447    }
448
449    #[test]
450    fn string_field() {
451        let mut config = TestConfig::default();
452        assert_eq!(get_field(&mut config, "label").unwrap(), "");
453
454        set(&mut config, "label", "Backup").unwrap();
455        assert_eq!(config.label, "Backup");
456        assert_eq!(get_field(&mut config, "label").unwrap(), "Backup");
457
458        set(&mut config, "label", "12345678").unwrap();
459        assert_eq!(config.label, "12345678");
460        set(&mut config, "label", "").unwrap();
461        assert_eq!(config.label, "");
462    }
463
464    #[test]
465    fn string_field_too_long() {
466        let mut config = TestConfig::default();
467        set(&mut config, "label", "old").unwrap();
468
469        let error = set(&mut config, "label", "123456789").unwrap_err();
470        assert!(matches!(error, ConfigError::DataTooLong), "{error:?}");
471        // A rejected value must not destroy the stored one
472        assert_eq!(config.label, "old");
473    }
474
475    // The firmware stores arbitrary UTF-8, clients sanitize the value before displaying it
476    #[test]
477    fn string_field_arbitrary_utf8() {
478        let mut config = TestConfig::default();
479
480        for value in ["a\nb", "\x1b[2J", "\u{202e}", "\u{2028}", "Χ’Χ‘Χ¨"] {
481            set(&mut config, "label", value).unwrap_or_else(|e| panic!("{value:?}: {e:?}"));
482            assert_eq!(config.label, value);
483        }
484    }
485
486    // Values are bounded by their length in bytes, not in characters
487    #[test]
488    fn string_field_multibyte() {
489        let mut config = TestConfig::default();
490
491        // The capacity is 8 bytes: two 4-byte characters fit, three 3-byte ones do not
492        set(&mut config, "label", "πŸ”‘πŸ”‘").unwrap();
493        assert_eq!(config.label, "πŸ”‘πŸ”‘");
494        assert_eq!(config.label.len(), 8);
495
496        let error = set(&mut config, "label", "δΈ­δΈ­δΈ­").unwrap_err();
497        assert!(matches!(error, ConfigError::DataTooLong), "{error:?}");
498        assert_eq!(config.label, "πŸ”‘πŸ”‘");
499    }
500}