Skip to main content

admin_app/
config.rs

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    /// The config was changed as a result of the reset to default
24    Changed,
25    /// The config was at the default value
26    Unchanged,
27    /// The key does not correspond to any application that can be reset
28    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    /// Client ID to factory-reset if the associated configuration option is changed
47    ///
48    /// # If the Request is for a `client_id`:
49    ///
50    /// - MUST return `Some` to indicate that the client can be factory reset by the admin app,
51    ///   In that case, the path is the clientid that must be reset, and the allocation must point to a
52    ///   signal that id checked by the application.
53    /// - MUST return None otherwise.
54    fn reset_client_id(
55        &self,
56        _key: &str,
57    ) -> Option<(&'static Path, &'static ResetSignalAllocation)> {
58        None
59    }
60
61    /// Reset the config of a client to its default value
62    ///
63    /// Returns `true` if the config has been changed as a result
64    fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
65        ResetConfigResult::WrongKey
66    }
67
68    /// The migration version
69    ///
70    /// Return None if the configuration does not support storing the migration version
71    fn migration_version(&self) -> Option<u32>;
72
73    /// Set the migration version
74    ///
75    /// Return false if the configuration does not support storing the migration version
76    fn set_migration_version(&mut self, _version: u32) -> bool;
77
78    fn list_available_fields(&self) -> &'static [ConfigField];
79}
80
81// No need to rename, cbor-smol already packs enum using ids
82//
83// As the variants are serialized as their index, new variants may only be appended
84#[derive(Serialize)]
85#[non_exhaustive]
86pub enum FieldType {
87    Bool,
88    U8,
89    /// A UTF-8 string
90    ///
91    /// The maximum length is defined by the config struct holding the value, not by this type
92    String,
93}
94
95#[derive(Serialize)]
96pub struct ConfigField {
97    #[serde(rename = "n")]
98    pub name: &'static str,
99    /// Changing the config field requires a touch
100    #[serde(rename = "c")]
101    pub requires_touch_confirmation: bool,
102    /// Changing the config field requires a power cycle
103    #[serde(rename = "r")]
104    pub requires_reboot: bool,
105    /// Changing the config field deletes data
106    #[serde(rename = "d")]
107    pub destructive: bool,
108    /// The type of data stored in this field
109    #[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    /// A string of any capacity, obtained from a `heapless::String<N>` with `as_mut_view`
141    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                // Check the capacity before clearing so that a rejected value leaves the stored
156                // one intact
157                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    // The field types are parsed as integers by the hosts, so their values may never change
338    #[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        // A rejected value must not destroy the stored one
406        assert_eq!(config.label, "old");
407    }
408
409    // The firmware stores arbitrary UTF-8, clients sanitize the value before displaying it
410    #[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    // Values are bounded by their length in bytes, not in characters
421    #[test]
422    fn string_field_multibyte() {
423        let mut config = TestConfig::default();
424
425        // The capacity is 8 bytes: two 4-byte characters fit, three 3-byte ones do not
426        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}