Skip to main content

mcumgr_toolkit/commands/
image.rs

1use serde::{Deserialize, Serialize};
2
3use crate::commands::{
4    CountingWriter, data_too_large_error,
5    macros::{impl_deserialize_from_empty_map_and_into_unit, impl_serialize_as_empty_map},
6};
7
8fn serialize_option_hex<S, T>(data: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
9where
10    S: serde::Serializer,
11    T: hex::ToHex,
12{
13    data.as_ref()
14        .map(|val| val.encode_hex::<String>())
15        .serialize(serializer)
16}
17
18/// The state of an image slot
19#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
20pub struct ImageState {
21    /// image number
22    #[serde(default)]
23    pub image: u32,
24    /// slot number within “image”
25    pub slot: u32,
26    /// string representing image version, as set with `imgtool`
27    pub version: String,
28    /// SHA256 hash of the image header and body
29    ///
30    /// Note that this will not be the same as the SHA256 of the whole file, it is the field in the
31    /// MCUboot TLV section that contains a hash of the data which is used for signature
32    /// verification purposes.
33    #[serde(serialize_with = "serialize_option_hex")] // For JSON (cli)
34    pub hash: Option<[u8; 32]>,
35    /// true if image has bootable flag set
36    #[serde(default)]
37    pub bootable: bool,
38    /// true if image is set for next swap
39    #[serde(default)]
40    pub pending: bool,
41    /// true if image has been confirmed
42    #[serde(default)]
43    pub confirmed: bool,
44    /// true if image is currently active application
45    #[serde(default)]
46    pub active: bool,
47    /// true if image is to stay in primary slot after the next boot
48    #[serde(default)]
49    pub permanent: bool,
50}
51
52/// [Get Image State](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#get-state-of-images-request) command
53#[derive(Debug, Eq, PartialEq)]
54pub struct GetImageState;
55impl_serialize_as_empty_map!(GetImageState);
56
57/// Response for [`GetImageState`] and [`SetImageState`] commands
58#[derive(Debug, Deserialize, Eq, PartialEq)]
59pub struct ImageStateResponse {
60    /// List of all images and their state
61    pub images: Vec<ImageState>,
62    // splitStatus field is missing
63    // because it is unused by Zephyr
64}
65
66/// [Set Image State](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#set-state-of-image-request) command
67#[derive(Debug, Serialize, Eq, PartialEq)]
68pub struct SetImageState<'a> {
69    /// SHA256 hash of the image header and body
70    ///
71    /// If `confirm` is `true` this can be omitted, which will select the currently running image.
72    ///
73    /// Note that this will not be the same as the SHA256 of the whole file, it is the field in the
74    /// MCUboot TLV section that contains a hash of the data which is used for signature
75    /// verification purposes.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    #[serde(with = "serde_bytes")]
78    pub hash: Option<&'a [u8; 32]>,
79    /// If true, mark the given image as 'confirmed'.
80    ///
81    /// If false, perform a test boot with the given image
82    /// and revert upon hard reset.
83    pub confirm: bool,
84}
85
86/// [Image Upload](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#image-upload) command
87#[derive(Debug, Serialize, Eq, PartialEq)]
88pub struct ImageUpload<'a, 'b> {
89    /// optional image number, it does not have to appear in request at all, in which case it is assumed to be 0.
90    ///
91    /// Should only be present when “off” is 0.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub image: Option<u32>,
94    /// optional length of an image.
95    ///
96    /// Must appear when “off” is 0.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub len: Option<u64>,
99    /// offset of image chunk the request carries.
100    pub off: u64,
101    /// SHA256 hash of an upload; this is used to identify an upload session
102    /// (e.g. to allow MCUmgr to continue a broken session), and for image verification purposes.
103    /// This must be a full SHA256 hash of the whole image being uploaded, or not included if the hash
104    /// is not available (in which case, upload session continuation and image verification functionality will be unavailable).
105    ///
106    /// Should only be present when “off” is 0.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    #[serde(with = "serde_bytes")]
109    pub sha: Option<&'a [u8; 32]>,
110    /// image data to write at provided offset.
111    #[serde(with = "serde_bytes")]
112    pub data: &'b [u8],
113    /// optional flag that states that only upgrade should be allowed, so if the version of uploaded software
114    /// is not higher than already on a device, the image upload will be rejected.
115    ///
116    /// Should only be present when “off” is 0.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub upgrade: Option<bool>,
119}
120
121/// Response for [`ImageUpload`] command
122#[derive(Debug, Deserialize, Eq, PartialEq)]
123pub struct ImageUploadResponse {
124    /// offset of last successfully written byte of update.
125    pub off: u64,
126    /// indicates if the uploaded data successfully matches the provided SHA256 hash or not
127    pub r#match: Option<bool>,
128}
129
130/// Computes how large [`ImageUpload::data`] is allowed to be.
131///
132/// # Arguments
133///
134/// * `smp_frame_size`  - The max allowed size of an SMP frame.
135///
136pub fn image_upload_max_data_chunk_size(smp_frame_size: usize) -> std::io::Result<usize> {
137    const MGMT_HDR_SIZE: usize = 8; // Size of SMP header
138
139    let mut size_counter = CountingWriter::new();
140    ciborium::into_writer(
141        &ImageUpload {
142            off: u64::MAX,
143            data: &[0u8],
144            len: Some(u64::MAX),
145            image: Some(u32::MAX),
146            sha: Some(&[42; 32]),
147            upgrade: Some(true),
148        },
149        &mut size_counter,
150    )
151    .map_err(|_| data_too_large_error())?;
152
153    let size_with_one_byte = size_counter.bytes_written;
154    let size_without_data = size_with_one_byte - 1;
155
156    let estimated_data_size = smp_frame_size
157        .checked_sub(MGMT_HDR_SIZE)
158        .ok_or_else(data_too_large_error)?
159        .checked_sub(size_without_data)
160        .ok_or_else(data_too_large_error)?;
161
162    let data_length_bytes = if estimated_data_size == 0 {
163        return Err(data_too_large_error());
164    } else if estimated_data_size <= u8::MAX as usize {
165        1
166    } else if estimated_data_size <= u16::MAX as usize {
167        2
168    } else if estimated_data_size <= u32::MAX as usize {
169        4
170    } else {
171        8
172    };
173
174    // Remove data length entry from estimated data size
175    let actual_data_size = estimated_data_size
176        .checked_sub(data_length_bytes as usize)
177        .ok_or_else(data_too_large_error)?;
178
179    if actual_data_size == 0 {
180        return Err(data_too_large_error());
181    }
182
183    Ok(actual_data_size)
184}
185
186/// [Image Erase](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#image-erase) command
187#[derive(Debug, Serialize, Eq, PartialEq)]
188pub struct ImageErase {
189    /// slot number; it does not have to appear in the request at all, in which case it is assumed to be 1
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub slot: Option<u32>,
192}
193
194/// Response for [`ImageErase`] command
195#[derive(Default, Debug, Eq, PartialEq)]
196pub struct ImageEraseResponse;
197impl_deserialize_from_empty_map_and_into_unit!(ImageEraseResponse);
198
199/// [Slot Info](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#slot-info) command
200#[derive(Debug, Eq, PartialEq)]
201pub struct SlotInfo;
202impl_serialize_as_empty_map!(SlotInfo);
203
204/// Information about a firmware image type returned by [`SlotInfo`]
205#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
206pub struct SlotInfoImage {
207    /// The number of the image
208    pub image: u32,
209    /// Slots available for the image
210    pub slots: Vec<SlotInfoImageSlot>,
211    /// Maximum size of an application that can be uploaded to that image number
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub max_image_size: Option<u64>,
214}
215
216/// Information about a slot that can hold a firmware image
217#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
218pub struct SlotInfoImageSlot {
219    /// The slot inside the image being enumerated
220    pub slot: u32,
221    /// The size of the slot
222    pub size: u64,
223    /// Specifies the image ID that can be used by external tools to upload an image to that slot
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub upload_image_id: Option<u32>,
226}
227
228/// Response for [`SlotInfo`] command
229#[derive(Debug, Deserialize, Eq, PartialEq)]
230pub struct SlotInfoResponse {
231    /// List of all image slot collections on the device
232    pub images: Vec<SlotInfoImage>,
233}
234
235#[cfg(test)]
236mod tests {
237    use super::super::macros::command_encode_decode_test;
238    use super::*;
239    use ciborium::cbor;
240
241    command_encode_decode_test! {
242        get_image_state,
243        (0, 1, 0),
244        GetImageState,
245        cbor!({}),
246        cbor!({
247            "images" => [
248                {
249                    "image" => 3,
250                    "slot" => 5,
251                    "version" => "v1.2.3",
252                    "hash" => ciborium::Value::Bytes(vec![1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]),
253                    "bootable" => true,
254                    "pending" => true,
255                    "confirmed" => true,
256                    "active" => true,
257                    "permanent" => true,
258                },
259                {
260                    "image" => 4,
261                    "slot" => 6,
262                    "version" => "v5.5.5",
263                    "bootable" => false,
264                    "pending" => false,
265                    "confirmed" => false,
266                    "active" => false,
267                    "permanent" => false,
268                },
269                {
270                    "slot" => 9,
271                    "version" => "8.6.4",
272                },
273            ],
274            "splitStatus" => 42,
275        }),
276        ImageStateResponse{
277            images: vec![
278                ImageState{
279                    image: 3,
280                    slot: 5,
281                    version: "v1.2.3".to_string(),
282                    hash: Some([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]),
283                    bootable: true,
284                    pending: true,
285                    confirmed: true,
286                    active: true,
287                    permanent: true,
288                },
289                ImageState{
290                    image: 4,
291                    slot: 6,
292                    version: "v5.5.5".to_string(),
293                    hash: None,
294                    bootable: false,
295                    pending: false,
296                    confirmed: false,
297                    active: false,
298                    permanent: false,
299                },
300                ImageState{
301                    image: 0,
302                    slot: 9,
303                    version: "8.6.4".to_string(),
304                    hash: None,
305                    bootable: false,
306                    pending: false,
307                    confirmed: false,
308                    active: false,
309                    permanent: false,
310                }
311            ],
312        },
313    }
314
315    command_encode_decode_test! {
316        set_image_state_temp,
317        (2, 1, 0),
318        SetImageState {
319            confirm: false,
320            hash: Some(&[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]),
321        },
322        cbor!({
323            "hash" => ciborium::Value::Bytes(vec![1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]),
324            "confirm" => false,
325        }),
326        cbor!({
327            "images" => [],
328        }),
329        ImageStateResponse{
330            images: vec![],
331        },
332    }
333
334    command_encode_decode_test! {
335        set_image_state_perm,
336        (2, 1, 0),
337        SetImageState {
338            confirm: true,
339            hash: None,
340        },
341        cbor!({
342            "confirm" => true,
343        }),
344        cbor!({
345            "images" => [],
346        }),
347        ImageStateResponse{
348            images: vec![],
349        },
350    }
351
352    command_encode_decode_test! {
353        upload_image_first,
354        (2, 1, 1),
355        ImageUpload{
356            image: Some(2),
357            len: Some(123456789123),
358            off: 0,
359            sha: Some(&[0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1]),
360            data: &[5,6,7,8],
361            upgrade: Some(false),
362        },
363        cbor!({
364            "image" => 2,
365            "len" => 123456789123u64,
366            "off" => 0,
367            "sha" => ciborium::Value::Bytes(vec![0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1]),
368            "data" => ciborium::Value::Bytes(vec![5,6,7,8]),
369            "upgrade" => false,
370        }),
371        cbor!({
372            "off" => 4,
373        }),
374        ImageUploadResponse {
375            off: 4,
376            r#match: None,
377        },
378    }
379
380    command_encode_decode_test! {
381        upload_image_last,
382        (2, 1, 1),
383        ImageUpload{
384            image: None,
385            len: None,
386            off: 123456789118,
387            sha: None,
388            data: &[100, 101, 102, 103, 104],
389            upgrade: None,
390        },
391        cbor!({
392            "off" => 123456789118u64,
393            "data" => ciborium::Value::Bytes(vec![100, 101, 102, 103, 104]),
394        }),
395        cbor!({
396            "off" => 123456789123u64,
397            "match" => false,
398        }),
399        ImageUploadResponse {
400            off: 123456789123,
401            r#match: Some(false),
402        },
403    }
404
405    command_encode_decode_test! {
406        image_erase,
407        (2, 1, 5),
408        ImageErase{
409            slot: None
410        },
411        cbor!({}),
412        cbor!({}),
413        ImageEraseResponse,
414    }
415
416    command_encode_decode_test! {
417        image_erase_with_slot_number,
418        (2, 1, 5),
419        ImageErase{
420            slot: Some(42)
421        },
422        cbor!({
423            "slot" => 42,
424        }),
425        cbor!({}),
426        ImageEraseResponse,
427    }
428
429    command_encode_decode_test! {
430        slot_info,
431        (0, 1, 6),
432        SlotInfo,
433        cbor!({}),
434        cbor!({
435            "images" => [
436                {
437                    "image" => 0,
438                    "slots" => [
439                        {
440                            "slot" => 0,
441                            "size" => 42,
442                            "upload_image_id" => 2,
443                        },
444                        {
445                            "slot" => 1,
446                            "size" => 123456789012u64,
447                        },
448                    ],
449                    "max_image_size" => 123456789987u64,
450                },
451                {
452                    "image" => 1,
453                    "slots" => [
454                    ],
455                },
456            ],
457        }),
458        SlotInfoResponse{
459            images: vec![
460                SlotInfoImage {
461                    image: 0,
462                    slots: vec![
463                        SlotInfoImageSlot {
464                            slot: 0,
465                            size: 42,
466                            upload_image_id: Some(2),
467                        },
468                        SlotInfoImageSlot {
469                            slot: 1,
470                            size: 123456789012,
471                            upload_image_id: None,
472                        }
473                    ],
474                    max_image_size: Some(123456789987)
475                },
476                SlotInfoImage {
477                    image: 1,
478                    slots: vec![],
479                    max_image_size: None,
480                }
481            ],
482        },
483    }
484
485    #[test]
486    fn image_upload_max_data_chunk_size() {
487        for smp_frame_size in 101..100000 {
488            let smp_payload_size = smp_frame_size - 8 /* SMP frame header */;
489
490            let max_data_size = super::image_upload_max_data_chunk_size(smp_frame_size).unwrap();
491
492            let cmd = ImageUpload {
493                off: u64::MAX,
494                data: &vec![0; max_data_size],
495                len: Some(u64::MAX),
496                image: Some(u32::MAX),
497                sha: Some(&[u8::MAX; 32]),
498                upgrade: Some(true),
499            };
500
501            let mut cbor_data = vec![];
502            ciborium::into_writer(&cmd, &mut cbor_data).unwrap();
503
504            assert!(
505                smp_payload_size - 2 <= cbor_data.len() && cbor_data.len() <= smp_payload_size,
506                "Failed at frame size {}: actual={}, max={}",
507                smp_frame_size,
508                cbor_data.len(),
509                smp_payload_size,
510            );
511        }
512    }
513
514    #[test]
515    fn image_upload_max_data_chunk_size_too_small() {
516        for smp_frame_size in 0..101 {
517            let max_data_size = super::image_upload_max_data_chunk_size(smp_frame_size);
518
519            assert!(max_data_size.is_err());
520        }
521    }
522}