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#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
20pub struct ImageState {
21 #[serde(default)]
23 pub image: u32,
24 pub slot: u32,
26 pub version: String,
28 #[serde(serialize_with = "serialize_option_hex")] pub hash: Option<Vec<u8>>,
35 #[serde(default)]
37 pub bootable: bool,
38 #[serde(default)]
40 pub pending: bool,
41 #[serde(default)]
43 pub confirmed: bool,
44 #[serde(default)]
46 pub active: bool,
47 #[serde(default)]
49 pub permanent: bool,
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct GetImageState;
55impl_serialize_as_empty_map!(GetImageState);
56
57#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
59pub struct ImageStateResponse {
60 pub images: Vec<ImageState>,
62 }
65
66#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
68pub struct SetImageState<'a> {
69 #[serde(skip_serializing_if = "Option::is_none")]
77 #[serde(with = "serde_bytes")]
78 pub hash: Option<&'a [u8]>,
79 pub confirm: bool,
84}
85
86#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
88pub struct ImageUpload<'a, 'b> {
89 #[serde(skip_serializing_if = "Option::is_none")]
93 pub image: Option<u32>,
94 #[serde(skip_serializing_if = "Option::is_none")]
98 pub len: Option<u64>,
99 pub off: u64,
101 #[serde(skip_serializing_if = "Option::is_none")]
108 #[serde(with = "serde_bytes")]
109 pub sha: Option<&'a [u8; 32]>,
110 #[serde(with = "serde_bytes")]
112 pub data: &'b [u8],
113 #[serde(skip_serializing_if = "Option::is_none")]
118 pub upgrade: Option<bool>,
119}
120
121#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
123pub struct ImageUploadResponse {
124 pub off: u64,
126 pub r#match: Option<bool>,
128}
129
130pub 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; 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 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#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
192pub struct ImageErase {
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub slot: Option<u32>,
196}
197
198#[derive(Clone, Default, Debug, Eq, PartialEq)]
200pub struct ImageEraseResponse;
201impl_deserialize_from_empty_map_and_into_unit!(ImageEraseResponse);
202
203#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct SlotInfo;
206impl_serialize_as_empty_map!(SlotInfo);
207
208#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
210pub struct SlotInfoImage {
211 pub image: u32,
213 pub slots: Vec<SlotInfoImageSlot>,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub max_image_size: Option<u64>,
218}
219
220#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
222pub struct SlotInfoImageSlot {
223 pub slot: u32,
225 pub size: u64,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub upload_image_id: Option<u32>,
230}
231
232#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
234pub struct SlotInfoResponse {
235 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 ;
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 ;
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}