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(Clone, 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    /// 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<Vec<u8>>,
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(Clone, Debug, Eq, PartialEq)]
54pub struct GetImageState;
55impl_serialize_as_empty_map!(GetImageState);
56
57/// Response for [`GetImageState`] and [`SetImageState`] commands
58#[derive(Clone, 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(Clone, Debug, Serialize, Eq, PartialEq)]
68pub struct SetImageState<'a> {
69    /// 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]>,
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(Clone, 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(Clone, 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/// * `first_chunk`     - Whether this is the first chunk. It carries several optional fields that can be omitted afterwards.
136///
137pub fn image_upload_max_data_chunk_size(
138    smp_frame_size: usize,
139    first_chunk: bool,
140) -> std::io::Result<usize> {
141    const MGMT_HDR_SIZE: usize = 8; // Size of SMP header
142
143    let mut size_counter = CountingWriter::new();
144    ciborium::into_writer(
145        &ImageUpload {
146            off: u64::MAX,
147            data: &[0u8],
148            len: first_chunk.then_some(u64::MAX),
149            image: first_chunk.then_some(u32::MAX),
150            sha: first_chunk.then_some(&[42; 32]),
151            upgrade: first_chunk.then_some(true),
152        },
153        &mut size_counter,
154    )
155    .map_err(|_| data_too_large_error())?;
156
157    let size_with_one_byte = size_counter.bytes_written;
158    let size_without_data = size_with_one_byte - 1;
159
160    let estimated_data_size = smp_frame_size
161        .checked_sub(MGMT_HDR_SIZE)
162        .ok_or_else(data_too_large_error)?
163        .checked_sub(size_without_data)
164        .ok_or_else(data_too_large_error)?;
165
166    let data_length_bytes = if estimated_data_size == 0 {
167        return Err(data_too_large_error());
168    } else if estimated_data_size <= u8::MAX as usize {
169        1
170    } else if estimated_data_size <= u16::MAX as usize {
171        2
172    } else if estimated_data_size <= u32::MAX as usize {
173        4
174    } else {
175        8
176    };
177
178    // Remove data length entry from estimated data size
179    let actual_data_size = estimated_data_size
180        .checked_sub(data_length_bytes as usize)
181        .ok_or_else(data_too_large_error)?;
182
183    if actual_data_size == 0 {
184        return Err(data_too_large_error());
185    }
186
187    Ok(actual_data_size)
188}
189
190/// [Image Erase](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#image-erase) command
191#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
192pub struct ImageErase {
193    /// slot number; it does not have to appear in the request at all, in which case it is assumed to be 1
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub slot: Option<u32>,
196}
197
198/// Response for [`ImageErase`] command
199#[derive(Clone, Default, Debug, Eq, PartialEq)]
200pub struct ImageEraseResponse;
201impl_deserialize_from_empty_map_and_into_unit!(ImageEraseResponse);
202
203/// [Slot Info](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_1.html#slot-info) command
204#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct SlotInfo;
206impl_serialize_as_empty_map!(SlotInfo);
207
208/// Information about a firmware image type returned by [`SlotInfo`]
209#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
210pub struct SlotInfoImage {
211    /// The number of the image
212    pub image: u32,
213    /// Slots available for the image
214    pub slots: Vec<SlotInfoImageSlot>,
215    /// Maximum size of an application that can be uploaded to that image number
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub max_image_size: Option<u64>,
218}
219
220/// Information about a slot that can hold a firmware image
221#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
222pub struct SlotInfoImageSlot {
223    /// The slot inside the image being enumerated
224    pub slot: u32,
225    /// The size of the slot
226    pub size: u64,
227    /// Specifies the image ID that can be used by external tools to upload an image to that slot
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub upload_image_id: Option<u32>,
230}
231
232/// Response for [`SlotInfo`] command
233#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
234pub struct SlotInfoResponse {
235    /// List of all image slot collections on the device
236    pub images: Vec<SlotInfoImage>,
237}
238
239#[cfg(test)]
240mod tests {
241    use super::super::macros::command_encode_decode_test;
242    use super::*;
243    use ciborium::cbor;
244
245    command_encode_decode_test! {
246        get_image_state,
247        (0, 1, 0),
248        GetImageState,
249        cbor!({}),
250        cbor!({
251            "images" => [
252                {
253                    "image" => 3,
254                    "slot" => 5,
255                    "version" => "v1.2.3",
256                    "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]),
257                    "bootable" => true,
258                    "pending" => true,
259                    "confirmed" => true,
260                    "active" => true,
261                    "permanent" => true,
262                },
263                {
264                    "image" => 4,
265                    "slot" => 6,
266                    "version" => "v5.5.5",
267                    "bootable" => false,
268                    "pending" => false,
269                    "confirmed" => false,
270                    "active" => false,
271                    "permanent" => false,
272                },
273                {
274                    "slot" => 9,
275                    "version" => "8.6.4",
276                },
277            ],
278            "splitStatus" => 42,
279        }),
280        ImageStateResponse{
281            images: vec![
282                ImageState{
283                    image: 3,
284                    slot: 5,
285                    version: "v1.2.3".to_string(),
286                    hash: Some(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]),
287                    bootable: true,
288                    pending: true,
289                    confirmed: true,
290                    active: true,
291                    permanent: true,
292                },
293                ImageState{
294                    image: 4,
295                    slot: 6,
296                    version: "v5.5.5".to_string(),
297                    hash: None,
298                    bootable: false,
299                    pending: false,
300                    confirmed: false,
301                    active: false,
302                    permanent: false,
303                },
304                ImageState{
305                    image: 0,
306                    slot: 9,
307                    version: "8.6.4".to_string(),
308                    hash: None,
309                    bootable: false,
310                    pending: false,
311                    confirmed: false,
312                    active: false,
313                    permanent: false,
314                }
315            ],
316        },
317    }
318
319    command_encode_decode_test! {
320        set_image_state_temp,
321        (2, 1, 0),
322        SetImageState {
323            confirm: false,
324            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]),
325        },
326        cbor!({
327            "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]),
328            "confirm" => false,
329        }),
330        cbor!({
331            "images" => [],
332        }),
333        ImageStateResponse{
334            images: vec![],
335        },
336    }
337
338    command_encode_decode_test! {
339        set_image_state_perm,
340        (2, 1, 0),
341        SetImageState {
342            confirm: true,
343            hash: None,
344        },
345        cbor!({
346            "confirm" => true,
347        }),
348        cbor!({
349            "images" => [],
350        }),
351        ImageStateResponse{
352            images: vec![],
353        },
354    }
355
356    command_encode_decode_test! {
357        upload_image_first,
358        (2, 1, 1),
359        ImageUpload{
360            image: Some(2),
361            len: Some(123456789123),
362            off: 0,
363            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]),
364            data: &[5,6,7,8],
365            upgrade: Some(false),
366        },
367        cbor!({
368            "image" => 2,
369            "len" => 123456789123u64,
370            "off" => 0,
371            "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]),
372            "data" => ciborium::Value::Bytes(vec![5,6,7,8]),
373            "upgrade" => false,
374        }),
375        cbor!({
376            "off" => 4,
377        }),
378        ImageUploadResponse {
379            off: 4,
380            r#match: None,
381        },
382    }
383
384    command_encode_decode_test! {
385        upload_image_last,
386        (2, 1, 1),
387        ImageUpload{
388            image: None,
389            len: None,
390            off: 123456789118,
391            sha: None,
392            data: &[100, 101, 102, 103, 104],
393            upgrade: None,
394        },
395        cbor!({
396            "off" => 123456789118u64,
397            "data" => ciborium::Value::Bytes(vec![100, 101, 102, 103, 104]),
398        }),
399        cbor!({
400            "off" => 123456789123u64,
401            "match" => false,
402        }),
403        ImageUploadResponse {
404            off: 123456789123,
405            r#match: Some(false),
406        },
407    }
408
409    command_encode_decode_test! {
410        image_erase,
411        (2, 1, 5),
412        ImageErase{
413            slot: None
414        },
415        cbor!({}),
416        cbor!({}),
417        ImageEraseResponse,
418    }
419
420    command_encode_decode_test! {
421        image_erase_with_slot_number,
422        (2, 1, 5),
423        ImageErase{
424            slot: Some(42)
425        },
426        cbor!({
427            "slot" => 42,
428        }),
429        cbor!({}),
430        ImageEraseResponse,
431    }
432
433    command_encode_decode_test! {
434        slot_info,
435        (0, 1, 6),
436        SlotInfo,
437        cbor!({}),
438        cbor!({
439            "images" => [
440                {
441                    "image" => 0,
442                    "slots" => [
443                        {
444                            "slot" => 0,
445                            "size" => 42,
446                            "upload_image_id" => 2,
447                        },
448                        {
449                            "slot" => 1,
450                            "size" => 123456789012u64,
451                        },
452                    ],
453                    "max_image_size" => 123456789987u64,
454                },
455                {
456                    "image" => 1,
457                    "slots" => [
458                    ],
459                },
460            ],
461        }),
462        SlotInfoResponse{
463            images: vec![
464                SlotInfoImage {
465                    image: 0,
466                    slots: vec![
467                        SlotInfoImageSlot {
468                            slot: 0,
469                            size: 42,
470                            upload_image_id: Some(2),
471                        },
472                        SlotInfoImageSlot {
473                            slot: 1,
474                            size: 123456789012,
475                            upload_image_id: None,
476                        }
477                    ],
478                    max_image_size: Some(123456789987)
479                },
480                SlotInfoImage {
481                    image: 1,
482                    slots: vec![],
483                    max_image_size: None,
484                }
485            ],
486        },
487    }
488
489    #[test]
490    fn image_upload_max_first_data_chunk_size() {
491        for smp_frame_size in 101..100000 {
492            let smp_payload_size = smp_frame_size - 8 /* SMP frame header */;
493
494            let max_data_size =
495                super::image_upload_max_data_chunk_size(smp_frame_size, true).unwrap();
496
497            let cmd = ImageUpload {
498                off: u64::MAX,
499                data: &vec![0; max_data_size],
500                len: Some(u64::MAX),
501                image: Some(u32::MAX),
502                sha: Some(&[u8::MAX; 32]),
503                upgrade: Some(true),
504            };
505
506            let mut cbor_data = vec![];
507            ciborium::into_writer(&cmd, &mut cbor_data).unwrap();
508
509            assert!(
510                smp_payload_size - 2 <= cbor_data.len() && cbor_data.len() <= smp_payload_size,
511                "Failed at frame size {}: actual={}, max={}",
512                smp_frame_size,
513                cbor_data.len(),
514                smp_payload_size,
515            );
516        }
517    }
518
519    #[test]
520    fn image_upload_max_first_data_chunk_size_too_small() {
521        for smp_frame_size in 0..101 {
522            let max_data_size = super::image_upload_max_data_chunk_size(smp_frame_size, true);
523
524            assert!(max_data_size.is_err());
525        }
526    }
527
528    #[test]
529    fn image_upload_max_data_chunk_size() {
530        for smp_frame_size in 30..100000 {
531            let smp_payload_size = smp_frame_size - 8 /* SMP frame header */;
532
533            let max_data_size =
534                super::image_upload_max_data_chunk_size(smp_frame_size, false).unwrap();
535
536            let cmd = ImageUpload {
537                off: u64::MAX,
538                data: &vec![0; max_data_size],
539                len: None,
540                image: None,
541                sha: None,
542                upgrade: None,
543            };
544
545            let mut cbor_data = vec![];
546            ciborium::into_writer(&cmd, &mut cbor_data).unwrap();
547
548            assert!(
549                smp_payload_size - 2 <= cbor_data.len() && cbor_data.len() <= smp_payload_size,
550                "Failed at frame size {}: actual={}, max={}",
551                smp_frame_size,
552                cbor_data.len(),
553                smp_payload_size,
554            );
555        }
556    }
557
558    #[test]
559    fn image_upload_max_data_chunk_size_too_small() {
560        for smp_frame_size in 0..30 {
561            let max_data_size = super::image_upload_max_data_chunk_size(smp_frame_size, false);
562
563            assert!(max_data_size.is_err());
564        }
565    }
566}