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::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#[derive(Serialize)]
179#[non_exhaustive]
180pub enum FieldType {
181    Bool,
182    U8,
183}
184
185#[derive(Serialize)]
186pub struct ConfigField {
187    #[serde(rename = "n")]
188    pub name: &'static str,
189    /// Changing the config field requires a touch
190    #[serde(rename = "c")]
191    pub requires_touch_confirmation: bool,
192    /// Changing the config field requires a power cycle
193    #[serde(rename = "r")]
194    pub requires_reboot: bool,
195    /// Changing the config field deletes data
196    #[serde(rename = "d")]
197    pub destructive: bool,
198    /// The type of data stored in this field
199    #[serde(rename = "t")]
200    pub ty: FieldType,
201}
202
203impl Config for () {
204    fn field(&mut self, _key: &str) -> Option<ConfigValueMut<'_>> {
205        None
206    }
207
208    fn reset_client_config(&mut self, _key: &str) -> ResetConfigResult {
209        ResetConfigResult::WrongKey
210    }
211
212    fn migration_version(&self) -> Option<u32> {
213        None
214    }
215
216    fn set_migration_version(&mut self, _version: u32) -> bool {
217        false
218    }
219
220    fn list_available_fields(&self) -> &'static [ConfigField] {
221        &[]
222    }
223}
224
225#[derive(Debug, Serialize)]
226pub enum ConfigValueMut<'a> {
227    Bool(&'a mut bool),
228    U8(&'a mut u8),
229}
230
231impl<'a> ConfigValueMut<'a> {
232    fn set(&mut self, value: &str) -> Result<(), ConfigError> {
233        fn set_value<T: FromStr>(target: &mut T, s: &str) -> Result<(), ConfigError> {
234            *target = s.parse().map_err(|_| ConfigError::InvalidValue)?;
235            Ok(())
236        }
237
238        match self {
239            Self::Bool(r) => set_value(*r, value),
240            Self::U8(r) => set_value(*r, value),
241        }
242    }
243}
244
245impl<'a> Display for ConfigValueMut<'a> {
246    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
247        match self {
248            Self::Bool(value) => write!(f, "{value}"),
249            Self::U8(value) => write!(f, "{value}"),
250        }
251    }
252}
253
254#[derive(Debug, FromRepr)]
255#[repr(u8)]
256pub enum ConfigError {
257    ReadFailed = 1,
258    WriteFailed = 2,
259    DeserializationFailed = 3,
260    SerializationFailed = 4,
261    InvalidKey = 5,
262    InvalidValue = 6,
263    DataTooLong = 7,
264    NotConfirmed = 8,
265}
266
267const _: () = assert!(
268    ConfigError::from_repr(0).is_none(),
269    "ConfigError may not have a variant with discriminant zero as zero indicates success.",
270);
271
272impl From<ConfigError> for u8 {
273    fn from(error: ConfigError) -> u8 {
274        error as _
275    }
276}
277
278pub fn get<C: Config>(
279    config: &mut C,
280    key: &str,
281    response: &mut VecView<u8>,
282) -> Result<(), ConfigError> {
283    let field = config.field(key).ok_or(ConfigError::InvalidKey)?;
284    write!(response, "{field}").map_err(|_| ConfigError::DataTooLong)
285}
286
287pub fn set<C: Config>(config: &mut C, key: &str, value: &str) -> Result<(), ConfigError> {
288    config
289        .field(key)
290        .ok_or(ConfigError::InvalidKey)?
291        .set(value)?;
292    Ok(())
293}
294
295pub fn load<F: Filestore, C: Config>(store: &mut F) -> Result<C, ConfigError> {
296    let Some(data) = load_if_exists(store, LOCATION, FILENAME)? else {
297        return Ok(Default::default());
298    };
299    cbor_deserialize(&data).map_err(|_| ConfigError::DeserializationFailed)
300}
301
302pub fn save_filestore<F: Filestore, C: Config>(
303    store: &mut F,
304    config: &C,
305) -> Result<(), ConfigError> {
306    if config == &C::default() {
307        if store.exists(FILENAME, LOCATION) {
308            store
309                .remove_file(FILENAME, LOCATION)
310                .map_err(|_| ConfigError::WriteFailed)?;
311        }
312    } else {
313        let mut data = Message::new();
314        cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
315        store
316            .write(FILENAME, LOCATION, &data)
317            .map_err(|_| ConfigError::SerializationFailed)?;
318    }
319    Ok(())
320}
321
322pub fn save<T: FilesystemClient, C: Config>(client: &mut T, config: &C) -> Result<(), ConfigError> {
323    if config == &Default::default() {
324        if exists(client, LOCATION, FILENAME)? {
325            try_syscall!(client.remove_file(LOCATION, FILENAME.into()))
326                .map_err(|_| ConfigError::WriteFailed)?;
327        }
328    } else {
329        let mut data = Message::new();
330        cbor_serialize_to(config, &mut data).map_err(|_| ConfigError::SerializationFailed)?;
331        try_syscall!(client.write_file(LOCATION, FILENAME.into(), data, None))
332            .map_err(|_| ConfigError::WriteFailed)?;
333    }
334    Ok(())
335}
336
337fn exists<T: FilesystemClient>(
338    client: &mut T,
339    location: Location,
340    path: &Path,
341) -> Result<bool, ConfigError> {
342    try_syscall!(client.entry_metadata(location, path.into()))
343        .map(|r| r.metadata.is_some())
344        .map_err(|_| ConfigError::ReadFailed)
345}
346
347fn load_if_exists<F: Filestore>(
348    store: &mut F,
349    location: Location,
350    path: &Path,
351) -> Result<Option<Message>, ConfigError> {
352    store.read(path, location).map(Some).or_else(|_| {
353        if store.exists(path, location) {
354            Err(ConfigError::ReadFailed)
355        } else {
356            Ok(None)
357        }
358    })
359}
360
361#[cfg(test)]
362mod tests {
363    use hex_literal::hex;
364
365    use super::*;
366
367    #[test]
368    fn config_field() {
369        let fields = &[ConfigField {
370            name: "test_name",
371            requires_touch_confirmation: true,
372            requires_reboot: false,
373            destructive: true,
374            ty: FieldType::Bool,
375        }];
376        let mut bytes: heapless::Vec<u8, 100> = Default::default();
377        cbor_smol::cbor_serialize_to(fields, &mut bytes).unwrap();
378        assert_eq!(
379            &bytes,
380            &hex!("81A5616E69746573745F6E616D656163F56172F46164F5617400")
381        );
382    }
383}