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#[derive(Debug, Serialize, Eq, PartialEq)]
16pub struct FileDownload<'a> {
17 pub off: u64,
19 pub name: &'a str,
21}
22
23#[derive(Debug, Deserialize, Eq, PartialEq)]
25pub struct FileDownloadResponse {
26 pub off: u64,
28 pub data: Vec<u8>,
30 pub len: Option<u64>,
32}
33
34pub 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; 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 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#[derive(Debug, Serialize, Eq, PartialEq)]
93pub struct FileUpload<'a, 'b> {
94 pub off: u64,
96 #[serde(with = "serde_bytes")]
98 pub data: &'a [u8],
99 pub name: &'b str,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub len: Option<u64>,
104}
105
106#[derive(Debug, Deserialize, Eq, PartialEq)]
108pub struct FileUploadResponse {
109 pub off: u64,
111}
112
113#[derive(Debug, Serialize, Eq, PartialEq)]
115pub struct FileStatus<'a> {
116 pub name: &'a str,
118}
119
120#[derive(Debug, Deserialize, Eq, PartialEq)]
122pub struct FileStatusResponse {
123 pub len: u64,
125}
126
127#[derive(Debug, Serialize, Eq, PartialEq)]
129pub struct FileChecksum<'a, 'b> {
130 pub name: &'a str,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub r#type: Option<&'b str>,
135 #[serde(default, skip_serializing_if = "is_default")]
137 pub off: u64,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub len: Option<u64>,
141}
142
143#[derive(Debug, Deserialize, Eq, PartialEq)]
145pub struct FileChecksumResponse {
146 pub r#type: String,
148 #[serde(default, skip_serializing_if = "is_default")]
150 pub off: u64,
151 pub len: u64,
153 pub output: FileChecksumData,
155}
156
157#[derive(Debug, Deserialize, Eq, PartialEq)]
159#[serde(untagged)]
160pub enum FileChecksumData {
161 #[serde(with = "serde_bytes")]
163 Hash(Box<[u8]>),
164 Checksum(u32),
166}
167
168impl FileChecksumData {
169 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#[derive(Debug, Eq, PartialEq)]
180pub struct SupportedFileChecksumTypes;
181impl_serialize_as_empty_map!(SupportedFileChecksumTypes);
182
183#[derive(Debug, Deserialize, Eq, PartialEq)]
185pub struct SupportedFileChecksumTypesResponse {
186 pub r#types: HashMap<String, FileChecksumProperties>,
188}
189
190#[derive(Display, Deserialize_repr, Debug, Copy, Clone, PartialEq, Eq)]
192#[repr(u8)]
193#[allow(non_camel_case_types)]
194pub enum FileChecksumDataFormat {
195 Numerical = 0,
197 ByteArray = 1,
199}
200
201#[derive(Debug, Deserialize, Eq, PartialEq)]
203pub struct FileChecksumProperties {
204 pub format: FileChecksumDataFormat,
206 pub size: u32,
208}
209
210#[derive(Debug, Eq, PartialEq)]
212pub struct FileClose;
213impl_serialize_as_empty_map!(FileClose);
214
215#[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 ;
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}