Skip to main content

mcumgr_toolkit/commands/
fs.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_repr::Deserialize_repr;
5use strum::Display;
6
7use crate::commands::{
8    CountingWriter, data_too_large_error,
9    macros::{impl_deserialize_from_empty_map_and_into_unit, impl_serialize_as_empty_map},
10};
11
12use super::is_default;
13
14/// [File Download](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_8.html#file-download) command
15#[derive(Debug, Serialize, Eq, PartialEq)]
16pub struct FileDownload<'a> {
17    /// offset to start download at
18    pub off: u64,
19    /// absolute path to a file
20    pub name: &'a str,
21}
22
23/// Response for [`FileDownload`] command
24#[derive(Debug, Deserialize, Eq, PartialEq)]
25pub struct FileDownloadResponse {
26    /// offset the response is for
27    pub off: u64,
28    /// chunk of data read from file
29    pub data: Vec<u8>,
30    /// length of file, this field is only mandatory when “off” is 0
31    pub len: Option<u64>,
32}
33
34/// Computes how large [`FileUpload::data`] is allowed to be.
35///
36/// # Arguments
37///
38/// * `smp_frame_size`  - The max allowed size of an SMP frame.
39/// * `filename`        - The filename we transfer to.
40pub fn file_upload_max_data_chunk_size(
41    smp_frame_size: usize,
42    filename: &str,
43) -> std::io::Result<usize> {
44    const MGMT_HDR_SIZE: usize = 8; // Size of SMP header
45
46    let mut size_counter = CountingWriter::new();
47    ciborium::into_writer(
48        &FileUpload {
49            off: u64::MAX,
50            name: filename,
51            data: &[0u8],
52            len: Some(u64::MAX),
53        },
54        &mut size_counter,
55    )
56    .map_err(|_| data_too_large_error())?;
57
58    let size_with_one_byte = size_counter.bytes_written;
59    let size_without_data = size_with_one_byte - 1;
60
61    let estimated_data_size = smp_frame_size
62        .checked_sub(MGMT_HDR_SIZE)
63        .ok_or_else(data_too_large_error)?
64        .checked_sub(size_without_data)
65        .ok_or_else(data_too_large_error)?;
66
67    let data_length_bytes = if estimated_data_size == 0 {
68        return Err(data_too_large_error());
69    } else if estimated_data_size <= u8::MAX as usize {
70        1
71    } else if estimated_data_size <= u16::MAX as usize {
72        2
73    } else if estimated_data_size <= u32::MAX as usize {
74        4
75    } else {
76        8
77    };
78
79    // Remove data length entry from estimated data size
80    let actual_data_size = estimated_data_size
81        .checked_sub(data_length_bytes as usize)
82        .ok_or_else(data_too_large_error)?;
83
84    if actual_data_size == 0 {
85        return Err(data_too_large_error());
86    }
87
88    Ok(actual_data_size)
89}
90
91/// [File Upload](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_8.html#file-upload) command
92#[derive(Debug, Serialize, Eq, PartialEq)]
93pub struct FileUpload<'a, 'b> {
94    /// offset to start/continue upload at
95    pub off: u64,
96    /// chunk of data to write to the file
97    #[serde(with = "serde_bytes")]
98    pub data: &'a [u8],
99    /// absolute path to a file
100    pub name: &'b str,
101    /// length of file, this field is only mandatory when “off” is 0
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub len: Option<u64>,
104}
105
106/// Response for [`FileUpload`] command
107#[derive(Debug, Deserialize, Eq, PartialEq)]
108pub struct FileUploadResponse {
109    /// offset of last successfully written data
110    pub off: u64,
111}
112
113/// [File Status](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_8.html#file-status) command
114#[derive(Debug, Serialize, Eq, PartialEq)]
115pub struct FileStatus<'a> {
116    /// absolute path to a file
117    pub name: &'a str,
118}
119
120/// Response for [`FileStatus`] command
121#[derive(Debug, Deserialize, Eq, PartialEq)]
122pub struct FileStatusResponse {
123    /// length of file (in bytes)
124    pub len: u64,
125}
126
127/// [File Hash/Checksum](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_8.html#file-hash-checksum) command
128#[derive(Debug, Serialize, Eq, PartialEq)]
129pub struct FileChecksum<'a, 'b> {
130    /// absolute path to a file
131    pub name: &'a str,
132    /// type of hash/checksum to perform or None to use default
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub r#type: Option<&'b str>,
135    /// offset to start hash/checksum calculation at
136    #[serde(default, skip_serializing_if = "is_default")]
137    pub off: u64,
138    /// maximum length of data to read from file to generate hash/checksum with (optional, full file size if None)
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub len: Option<u64>,
141}
142
143/// Response for [`FileChecksum`] command
144#[derive(Debug, Deserialize, Eq, PartialEq)]
145pub struct FileChecksumResponse {
146    /// type of hash/checksum that was performed
147    pub r#type: String,
148    /// offset that hash/checksum calculation started at
149    #[serde(default, skip_serializing_if = "is_default")]
150    pub off: u64,
151    /// length of input data used for hash/checksum generation (in bytes)
152    pub len: u64,
153    /// output hash/checksum
154    pub output: FileChecksumData,
155}
156
157/// Hash data of [`FileChecksumResponse`]
158#[derive(Debug, Deserialize, Eq, PartialEq)]
159#[serde(untagged)]
160pub enum FileChecksumData {
161    /// hash bytes
162    #[serde(with = "serde_bytes")]
163    Hash(Box<[u8]>),
164    /// checksum integer
165    Checksum(u32),
166}
167
168impl FileChecksumData {
169    /// Convert to hex string
170    pub fn hex(&self) -> String {
171        match self {
172            FileChecksumData::Hash(data) => hex::encode(data),
173            FileChecksumData::Checksum(value) => format!("{value:08x}"),
174        }
175    }
176}
177
178/// [Supported file hash/checksum types](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_8.html#supported-file-hash-checksum-types) command
179#[derive(Debug, Eq, PartialEq)]
180pub struct SupportedFileChecksumTypes;
181impl_serialize_as_empty_map!(SupportedFileChecksumTypes);
182
183/// Response for [`SupportedFileChecksumTypes`] command
184#[derive(Debug, Deserialize, Eq, PartialEq)]
185pub struct SupportedFileChecksumTypesResponse {
186    /// names and properties of the hash/checksum types
187    pub r#types: HashMap<String, FileChecksumProperties>,
188}
189
190/// Data format of the hash/checksum type
191#[derive(Display, Deserialize_repr, Debug, Copy, Clone, PartialEq, Eq)]
192#[repr(u8)]
193#[allow(non_camel_case_types)]
194pub enum FileChecksumDataFormat {
195    /// Data is a number
196    Numerical = 0,
197    /// Data is a bytes array
198    ByteArray = 1,
199}
200
201/// Properties of a hash/checksum algorithm
202#[derive(Debug, Deserialize, Eq, PartialEq)]
203pub struct FileChecksumProperties {
204    /// format that the hash/checksum returns
205    pub format: FileChecksumDataFormat,
206    /// size (in bytes) of output hash/checksum response
207    pub size: u32,
208}
209
210/// [File Close](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_8.html#file-close) command
211#[derive(Debug, Eq, PartialEq)]
212pub struct FileClose;
213impl_serialize_as_empty_map!(FileClose);
214
215/// Response for [`FileClose`] command
216#[derive(Default, Debug, Eq, PartialEq)]
217pub struct FileCloseResponse;
218impl_deserialize_from_empty_map_and_into_unit!(FileCloseResponse);
219
220#[cfg(test)]
221mod tests {
222    use super::super::macros::command_encode_decode_test;
223    use super::*;
224    use ciborium::cbor;
225
226    #[test]
227    fn file_upload_max_data_chunk_size() {
228        for smp_frame_size in 57..100000 {
229            let smp_payload_size = smp_frame_size - 8 /* SMP frame header */;
230
231            let filename = "test.txt";
232            let max_data_size =
233                super::file_upload_max_data_chunk_size(smp_frame_size, filename).unwrap();
234
235            let cmd = FileUpload {
236                off: u64::MAX,
237                data: &vec![0; max_data_size],
238                name: filename,
239                len: Some(u64::MAX),
240            };
241
242            let mut cbor_data = vec![];
243            ciborium::into_writer(&cmd, &mut cbor_data).unwrap();
244
245            assert!(
246                smp_payload_size - 2 <= cbor_data.len() && cbor_data.len() <= smp_payload_size,
247                "Failed at frame size {}: actual={}, max={}",
248                smp_frame_size,
249                cbor_data.len(),
250                smp_payload_size,
251            );
252        }
253    }
254
255    #[test]
256    fn file_upload_max_data_chunk_size_too_small() {
257        for smp_frame_size in 0..57 {
258            let filename = "test.txt";
259            let max_data_size = super::file_upload_max_data_chunk_size(smp_frame_size, filename);
260
261            assert!(max_data_size.is_err());
262        }
263    }
264
265    command_encode_decode_test! {
266        file_download_with_len,
267        (0, 8, 0),
268        FileDownload{
269            off: 42,
270            name: "foo.txt",
271        },
272        cbor!({
273            "off" => 42,
274            "name" => "foo.txt",
275        }),
276        cbor!({
277            "off" => 42,
278            "data" => ciborium::Value::Bytes(vec![1,2,3,4,5]),
279            "len" => 100,
280        }),
281        FileDownloadResponse{
282            off: 42,
283            data: vec![1,2,3,4,5],
284            len: Some(100),
285        },
286    }
287
288    command_encode_decode_test! {
289        file_download_without_len,
290        (0, 8, 0),
291        FileDownload{
292            off: 69,
293            name: "bla.txt",
294        },
295        cbor!({
296            "off" => 69,
297            "name" => "bla.txt",
298        }),
299        cbor!({
300            "off" => 50,
301            "data" => ciborium::Value::Bytes(vec![10]),
302        }),
303        FileDownloadResponse{
304            off: 50,
305            data: vec![10],
306            len: None,
307        },
308    }
309
310    command_encode_decode_test! {
311        file_upload_with_len,
312        (2, 8, 0),
313        FileUpload{off: 0, data: &[1,2,3,4,5], name: "foo.bar", len: Some(123)},
314        cbor!({
315            "off" => 0,
316            "data" => ciborium::Value::Bytes(vec![1,2,3,4,5]),
317            "name" => "foo.bar",
318            "len" => 123,
319        }),
320        cbor!({
321            "off" => 58,
322        }),
323        FileUploadResponse{
324            off: 58
325        }
326    }
327
328    command_encode_decode_test! {
329        file_upload_without_len,
330        (2, 8, 0),
331        FileUpload{off: 10, data: &[40], name: "a.xy", len: None},
332        cbor!({
333            "off" => 10,
334            "data" => ciborium::Value::Bytes(vec![40]),
335            "name" => "a.xy",
336        }),
337        cbor!({
338            "off" => 0,
339        }),
340        FileUploadResponse{
341            off: 0
342        }
343    }
344
345    command_encode_decode_test! {
346        file_status,
347        (0, 8, 1),
348        FileStatus{name: "a.xy"},
349        cbor!({
350            "name" => "a.xy",
351        }),
352        cbor!({
353            "len" => 123,
354        }),
355        FileStatusResponse{
356            len: 123,
357        }
358    }
359
360    command_encode_decode_test! {
361        file_checksum_full_with_checksum,
362        (0, 8, 2),
363        FileChecksum{
364            name: "file.txt",
365            r#type: Some("sha256"),
366            off: 42,
367            len: Some(16),
368        },
369        cbor!({
370            "name" => "file.txt",
371            "type" => "sha256",
372            "off"  => 42,
373            "len"  => 16,
374        }),
375        cbor!({
376            "type"   => "foo",
377            "off"    => 69,
378            "len"    => 42,
379            "output" => 100000,
380        }),
381        FileChecksumResponse{
382            r#type: "foo".to_string(),
383            off: 69,
384            len: 42,
385            output: FileChecksumData::Checksum(100000),
386        }
387    }
388
389    command_encode_decode_test! {
390        file_checksum_empty_with_hash,
391        (0, 8, 2),
392        FileChecksum{
393            name: "file.txt",
394            r#type: None,
395            off: 0,
396            len: None,
397        },
398        cbor!({
399            "name" => "file.txt",
400        }),
401        cbor!({
402            "type"   => "foo",
403            "len"    => 42,
404            "output" => ciborium::Value::Bytes(vec![1,2,3,4]),
405        }),
406        FileChecksumResponse{
407            r#type: "foo".to_string(),
408            off: 0,
409            len: 42,
410            output: FileChecksumData::Hash(vec![1,2,3,4].into_boxed_slice()),
411        }
412    }
413
414    command_encode_decode_test! {
415        supported_checksum_types,
416        (0, 8, 3),
417        SupportedFileChecksumTypes,
418        cbor!({}),
419        cbor!({
420            "types" => {
421                "sha256" => {
422                    "format" => 1,
423                    "size" => 32,
424                },
425                "crc32" => {
426                    "format" => 0,
427                    "size" => 4
428                },
429            },
430        }),
431        SupportedFileChecksumTypesResponse{
432            types: HashMap::from([
433                (
434                    "crc32".to_string(),
435                    FileChecksumProperties{
436                        format: FileChecksumDataFormat::Numerical,
437                        size: 4,
438                    }
439                ),
440                (
441                    "sha256".to_string(),
442                    FileChecksumProperties{
443                        format: FileChecksumDataFormat::ByteArray,
444                        size: 32,
445                    }
446                ),
447            ])
448        }
449    }
450
451    command_encode_decode_test! {
452        file_close,
453        (2, 8, 4),
454        FileClose,
455        cbor!({}),
456        cbor!({}),
457        FileCloseResponse,
458    }
459}