use serde::{Deserialize, Serialize};
use crate::commands::{
CountingWriter, data_too_large_error,
macros::{impl_deserialize_from_empty_map_and_into_unit, impl_serialize_as_empty_map},
};
fn serialize_option_hex<S, T>(data: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: hex::ToHex,
{
data.as_ref()
.map(|val| val.encode_hex::<String>())
.serialize(serializer)
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct ImageState {
#[serde(default)]
pub image: u32,
pub slot: u32,
pub version: String,
#[serde(serialize_with = "serialize_option_hex")] pub hash: Option<Vec<u8>>,
#[serde(default)]
pub bootable: bool,
#[serde(default)]
pub pending: bool,
#[serde(default)]
pub confirmed: bool,
#[serde(default)]
pub active: bool,
#[serde(default)]
pub permanent: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetImageState;
impl_serialize_as_empty_map!(GetImageState);
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct ImageStateResponse {
pub images: Vec<ImageState>,
}
#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
pub struct SetImageState<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "serde_bytes")]
pub hash: Option<&'a [u8]>,
pub confirm: bool,
}
#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
pub struct ImageUpload<'a, 'b> {
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub len: Option<u64>,
pub off: u64,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "serde_bytes")]
pub sha: Option<&'a [u8; 32]>,
#[serde(with = "serde_bytes")]
pub data: &'b [u8],
#[serde(skip_serializing_if = "Option::is_none")]
pub upgrade: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct ImageUploadResponse {
pub off: u64,
pub r#match: Option<bool>,
}
pub fn image_upload_max_data_chunk_size(smp_frame_size: usize) -> std::io::Result<usize> {
const MGMT_HDR_SIZE: usize = 8;
let mut size_counter = CountingWriter::new();
ciborium::into_writer(
&ImageUpload {
off: u64::MAX,
data: &[0u8],
len: Some(u64::MAX),
image: Some(u32::MAX),
sha: Some(&[42; 32]),
upgrade: Some(true),
},
&mut size_counter,
)
.map_err(|_| data_too_large_error())?;
let size_with_one_byte = size_counter.bytes_written;
let size_without_data = size_with_one_byte - 1;
let estimated_data_size = smp_frame_size
.checked_sub(MGMT_HDR_SIZE)
.ok_or_else(data_too_large_error)?
.checked_sub(size_without_data)
.ok_or_else(data_too_large_error)?;
let data_length_bytes = if estimated_data_size == 0 {
return Err(data_too_large_error());
} else if estimated_data_size <= u8::MAX as usize {
1
} else if estimated_data_size <= u16::MAX as usize {
2
} else if estimated_data_size <= u32::MAX as usize {
4
} else {
8
};
let actual_data_size = estimated_data_size
.checked_sub(data_length_bytes as usize)
.ok_or_else(data_too_large_error)?;
if actual_data_size == 0 {
return Err(data_too_large_error());
}
Ok(actual_data_size)
}
#[derive(Clone, Debug, Serialize, Eq, PartialEq)]
pub struct ImageErase {
#[serde(skip_serializing_if = "Option::is_none")]
pub slot: Option<u32>,
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct ImageEraseResponse;
impl_deserialize_from_empty_map_and_into_unit!(ImageEraseResponse);
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SlotInfo;
impl_serialize_as_empty_map!(SlotInfo);
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct SlotInfoImage {
pub image: u32,
pub slots: Vec<SlotInfoImageSlot>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_image_size: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct SlotInfoImageSlot {
pub slot: u32,
pub size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub upload_image_id: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct SlotInfoResponse {
pub images: Vec<SlotInfoImage>,
}
#[cfg(test)]
mod tests {
use super::super::macros::command_encode_decode_test;
use super::*;
use ciborium::cbor;
command_encode_decode_test! {
get_image_state,
(0, 1, 0),
GetImageState,
cbor!({}),
cbor!({
"images" => [
{
"image" => 3,
"slot" => 5,
"version" => "v1.2.3",
"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]),
"bootable" => true,
"pending" => true,
"confirmed" => true,
"active" => true,
"permanent" => true,
},
{
"image" => 4,
"slot" => 6,
"version" => "v5.5.5",
"bootable" => false,
"pending" => false,
"confirmed" => false,
"active" => false,
"permanent" => false,
},
{
"slot" => 9,
"version" => "8.6.4",
},
],
"splitStatus" => 42,
}),
ImageStateResponse{
images: vec![
ImageState{
image: 3,
slot: 5,
version: "v1.2.3".to_string(),
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]),
bootable: true,
pending: true,
confirmed: true,
active: true,
permanent: true,
},
ImageState{
image: 4,
slot: 6,
version: "v5.5.5".to_string(),
hash: None,
bootable: false,
pending: false,
confirmed: false,
active: false,
permanent: false,
},
ImageState{
image: 0,
slot: 9,
version: "8.6.4".to_string(),
hash: None,
bootable: false,
pending: false,
confirmed: false,
active: false,
permanent: false,
}
],
},
}
command_encode_decode_test! {
set_image_state_temp,
(2, 1, 0),
SetImageState {
confirm: false,
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]),
},
cbor!({
"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]),
"confirm" => false,
}),
cbor!({
"images" => [],
}),
ImageStateResponse{
images: vec![],
},
}
command_encode_decode_test! {
set_image_state_perm,
(2, 1, 0),
SetImageState {
confirm: true,
hash: None,
},
cbor!({
"confirm" => true,
}),
cbor!({
"images" => [],
}),
ImageStateResponse{
images: vec![],
},
}
command_encode_decode_test! {
upload_image_first,
(2, 1, 1),
ImageUpload{
image: Some(2),
len: Some(123456789123),
off: 0,
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]),
data: &[5,6,7,8],
upgrade: Some(false),
},
cbor!({
"image" => 2,
"len" => 123456789123u64,
"off" => 0,
"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]),
"data" => ciborium::Value::Bytes(vec![5,6,7,8]),
"upgrade" => false,
}),
cbor!({
"off" => 4,
}),
ImageUploadResponse {
off: 4,
r#match: None,
},
}
command_encode_decode_test! {
upload_image_last,
(2, 1, 1),
ImageUpload{
image: None,
len: None,
off: 123456789118,
sha: None,
data: &[100, 101, 102, 103, 104],
upgrade: None,
},
cbor!({
"off" => 123456789118u64,
"data" => ciborium::Value::Bytes(vec![100, 101, 102, 103, 104]),
}),
cbor!({
"off" => 123456789123u64,
"match" => false,
}),
ImageUploadResponse {
off: 123456789123,
r#match: Some(false),
},
}
command_encode_decode_test! {
image_erase,
(2, 1, 5),
ImageErase{
slot: None
},
cbor!({}),
cbor!({}),
ImageEraseResponse,
}
command_encode_decode_test! {
image_erase_with_slot_number,
(2, 1, 5),
ImageErase{
slot: Some(42)
},
cbor!({
"slot" => 42,
}),
cbor!({}),
ImageEraseResponse,
}
command_encode_decode_test! {
slot_info,
(0, 1, 6),
SlotInfo,
cbor!({}),
cbor!({
"images" => [
{
"image" => 0,
"slots" => [
{
"slot" => 0,
"size" => 42,
"upload_image_id" => 2,
},
{
"slot" => 1,
"size" => 123456789012u64,
},
],
"max_image_size" => 123456789987u64,
},
{
"image" => 1,
"slots" => [
],
},
],
}),
SlotInfoResponse{
images: vec![
SlotInfoImage {
image: 0,
slots: vec![
SlotInfoImageSlot {
slot: 0,
size: 42,
upload_image_id: Some(2),
},
SlotInfoImageSlot {
slot: 1,
size: 123456789012,
upload_image_id: None,
}
],
max_image_size: Some(123456789987)
},
SlotInfoImage {
image: 1,
slots: vec![],
max_image_size: None,
}
],
},
}
#[test]
fn image_upload_max_data_chunk_size() {
for smp_frame_size in 101..100000 {
let smp_payload_size = smp_frame_size - 8 ;
let max_data_size = super::image_upload_max_data_chunk_size(smp_frame_size).unwrap();
let cmd = ImageUpload {
off: u64::MAX,
data: &vec![0; max_data_size],
len: Some(u64::MAX),
image: Some(u32::MAX),
sha: Some(&[u8::MAX; 32]),
upgrade: Some(true),
};
let mut cbor_data = vec![];
ciborium::into_writer(&cmd, &mut cbor_data).unwrap();
assert!(
smp_payload_size - 2 <= cbor_data.len() && cbor_data.len() <= smp_payload_size,
"Failed at frame size {}: actual={}, max={}",
smp_frame_size,
cbor_data.len(),
smp_payload_size,
);
}
}
#[test]
fn image_upload_max_data_chunk_size_too_small() {
for smp_frame_size in 0..101 {
let max_data_size = super::image_upload_max_data_chunk_size(smp_frame_size);
assert!(max_data_size.is_err());
}
}
}