From 13a487ea913c949a1e6b8c374df601dd73a597b7 Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:08:10 -0700 Subject: [PATCH 1/8] firmware: add the `FirmwareHeader` type Adds the `FirmwareHeader` type to represent the header block in an ITL firmware file. The header block contains information about the rest of the firmware file, including the magic bytes, RAM block size, and firmware file code. --- src/error.rs | 2 + src/types.rs | 2 + src/types/firmware.rs | 3 + src/types/firmware/header.rs | 270 +++++++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+) create mode 100644 src/types/firmware.rs create mode 100644 src/types/firmware/header.rs diff --git a/src/error.rs b/src/error.rs index f01920f..6e7c02a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -38,6 +38,7 @@ pub enum Error { JsonRpc(String), Event(String), Enum(String), + Firmware(String), } impl fmt::Display for Error { @@ -97,6 +98,7 @@ impl fmt::Display for Error { Error::JsonRpc(err) => write!(f, "Failed processing JSON-RPC message(s): {err}"), Error::Event(err) => write!(f, "Failed processing event message(s): {err}"), Error::Enum(err) => write!(f, "Enum error: {err}"), + Error::Firmware(err) => write!(f, "Firmware error: {err}"), } } } diff --git a/src/types.rs b/src/types.rs index 55eb485..240c688 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,6 +9,7 @@ pub(crate) mod country_code; pub(crate) mod device_status; pub(crate) mod encryption; pub(crate) mod events; +pub(crate) mod firmware; pub(crate) mod inhibit; pub(crate) mod last_reject_code; pub(crate) mod message_type; @@ -26,6 +27,7 @@ pub use country_code::*; pub use device_status::*; pub use encryption::*; pub use events::*; +pub use firmware::*; pub use inhibit::*; pub use last_reject_code::*; pub use message_type::*; diff --git a/src/types/firmware.rs b/src/types/firmware.rs new file mode 100644 index 0000000..d166d14 --- /dev/null +++ b/src/types/firmware.rs @@ -0,0 +1,3 @@ +pub(crate) mod header; + +pub use header::*; diff --git a/src/types/firmware/header.rs b/src/types/firmware/header.rs new file mode 100644 index 0000000..14ad902 --- /dev/null +++ b/src/types/firmware/header.rs @@ -0,0 +1,270 @@ +use core::mem; + +use crate::{Error, Result}; + +/// Length of the firmware header magic bytes. +pub const FIRMWARE_MAGIC_LEN: usize = 3; +/// Firmware header magic bytes (`b"ITL"`). +pub const FIRMWARE_MAGIC: [u8; FIRMWARE_MAGIC_LEN] = [b'I', b'T', b'L']; +/// Firmware header magic bytes (integer representation). +pub const FIRMWARE_MAGIC_NUM: u32 = 0x49544c; +/// Firmware header data length. +/// +/// The firmware header is defined to be 128 bytes long, but only the first eleven bytes are given +/// meaning in the specification. The remaining bytes are reserved as `data`, as they may still +/// contain meaningful information. +pub const FIRMWARE_HDR_DATA_LEN: usize = 117; +/// Length of the firmware file header. +pub const FIRMWARE_HEADER_LEN: usize = 128; +/// Reserved field of unspecified firmware header data. +pub const FIRMWARE_RESERVED_LEN: usize = 3; + +/// Represents the header of a firmware file used to upgrade devices over SSP. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FirmwareHeader { + magic: [u8; FIRMWARE_MAGIC_LEN], + _reserved: [u8; FIRMWARE_RESERVED_LEN], + file_code: u8, + ram_len: u32, + data: [u8; FIRMWARE_HDR_DATA_LEN], +} + +impl FirmwareHeader { + /// Creates a new [FirmwareHeader]. + pub const fn new() -> Self { + Self { + magic: FIRMWARE_MAGIC, + _reserved: [0; FIRMWARE_RESERVED_LEN], + file_code: 0, + ram_len: 0, + data: [0; FIRMWARE_HDR_DATA_LEN], + } + } + + /// Gets the [FirmwareHeader] magic bytes. + pub fn magic(&self) -> u32 { + u32::from_be_bytes([0, self.magic[0], self.magic[1], self.magic[2]]) + } + + /// Gets the [FirmwareHeader] file code for the firmware data. + pub const fn file_code(&self) -> u8 { + self.file_code + } + + /// Gets the length of the RAM code. + pub const fn ram_len(&self) -> u32 { + self.ram_len + } + + /// Gets the [FirmwareHeader] data section. + pub const fn data(&self) -> &[u8] { + &self.data + } + + /// Converts the [FirmwareHeader] to a byte buffer. + /// + /// **NOTE**: `buf` must be at least [FIRMWARE_HEADER_LEN] bytes. + pub fn to_bytes(&self, buf: &mut [u8]) -> Result<()> { + let len = buf.len(); + if len < FIRMWARE_HEADER_LEN { + Err(Error::Firmware(format!("invalid header destination buffer length, have: {len}, expected: {FIRMWARE_HEADER_LEN}"))) + } else { + let mut idx = 0; + buf[..FIRMWARE_MAGIC_LEN].copy_from_slice(&self.magic); + + idx += FIRMWARE_MAGIC_LEN; + let res_end = idx + FIRMWARE_RESERVED_LEN; + buf[idx..res_end].copy_from_slice(&self._reserved); + + idx = res_end; + + buf[idx] = self.file_code; + idx += mem::size_of::(); + + let ram_end = idx + mem::size_of::(); + buf[idx..ram_end].copy_from_slice(self.ram_len.to_be_bytes().as_ref()); + + idx = ram_end; + let data_end = idx + FIRMWARE_HDR_DATA_LEN; + + buf[idx..data_end].copy_from_slice(&self.data); + + Ok(()) + } + } +} + +impl TryFrom<&FirmwareHeader> for [u8; FIRMWARE_HEADER_LEN] { + type Error = Error; + + fn try_from(val: &FirmwareHeader) -> Result { + let mut buf = [0u8; FIRMWARE_HEADER_LEN]; + val.to_bytes(&mut buf)?; + Ok(buf) + } +} + +impl TryFrom for [u8; FIRMWARE_HEADER_LEN] { + type Error = Error; + + fn try_from(val: FirmwareHeader) -> Result { + (&val).try_into() + } +} + +impl TryFrom<&[u8]> for FirmwareHeader { + type Error = Error; + + fn try_from(val: &[u8]) -> Result { + let len = val.len(); + if len < FIRMWARE_HEADER_LEN { + Err(Error::Firmware(format!( + "invalid header length, have: {len}, expected: {FIRMWARE_HEADER_LEN}" + ))) + } else if val[..FIRMWARE_MAGIC_LEN] != FIRMWARE_MAGIC { + let bad_magic = &val[..FIRMWARE_MAGIC_LEN]; + let magic = &FIRMWARE_MAGIC; + Err(Error::Firmware(format!( + "bad magic bytes, have: {bad_magic:x?}, expected: {magic:x?}" + ))) + } else { + let mut idx = FIRMWARE_MAGIC_LEN; + let magic = FIRMWARE_MAGIC; + + // these conversions to fixed-length arrays should never fail, but just in case... + let res_end = idx.saturating_add(FIRMWARE_RESERVED_LEN); + let _reserved: [u8; FIRMWARE_RESERVED_LEN] = val[idx..res_end] + .try_into() + .map_err(|err| Error::Firmware(format!("invalid reserved data: {err}")))?; + + idx = res_end; + + let file_code = val[idx]; + + idx += mem::size_of::(); + let ram_len = u32::from_be_bytes([val[idx], val[idx + 1], val[idx + 2], val[idx + 3]]); + + idx += mem::size_of::(); + let data_end = idx + FIRMWARE_HDR_DATA_LEN; + // these conversions to fixed-length arrays should never fail, but just in case... + let data: [u8; FIRMWARE_HDR_DATA_LEN] = val[idx..data_end] + .try_into() + .map_err(|err| Error::Firmware(format!("invalid firmware data: {err}")))?; + + Ok(Self { + magic, + _reserved, + file_code, + ram_len, + data, + }) + } + } +} + +impl TryFrom<&[u8; N]> for FirmwareHeader { + type Error = Error; + + fn try_from(val: &[u8; N]) -> Result { + val.as_ref().try_into() + } +} + +impl TryFrom<[u8; N]> for FirmwareHeader { + type Error = Error; + + fn try_from(val: [u8; N]) -> Result { + val.as_ref().try_into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[rustfmt::skip] + fn test_header_parse() -> Result<()> { + let hdr_buf = [ + // magic + b'I', b'T', b'L', + // reserved + 0, 0, 0, + // file code + 0x03, + // RAM length + 0x01, 0x02, 0x03, 0x04, + // data + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + + let exp_hdr = FirmwareHeader { + magic: FIRMWARE_MAGIC, + _reserved: [0u8; FIRMWARE_RESERVED_LEN], + file_code: 0x03, + ram_len: 0x01020304, + data: [0u8; FIRMWARE_HDR_DATA_LEN], + }; + + assert_eq!(FirmwareHeader::try_from(&hdr_buf)?, exp_hdr); + assert_eq!(<[u8; FIRMWARE_HEADER_LEN]>::try_from(&exp_hdr)?, hdr_buf); + + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_header_bad_magic() -> Result<()> { + let hdr_buf = [ + // magic + b'b', b'4', b'd', + // reserved + 0, 0, 0, + // file code + 0x03, + // RAM length + 0x01, 0x02, 0x03, 0x04, + // data + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + + assert!(FirmwareHeader::try_from(&hdr_buf).is_err()); + + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_header_short_buf() -> Result<()> { + let mut hdr_buf = [ + // magic + b'b', b'4', b'd', + // reserved + 0, 0, 0, + // file code + 0x03, + // RAM length + 0x01, 0x02, 0x03, 0x04, + ]; + + assert!(FirmwareHeader::try_from(&hdr_buf).is_err()); + assert!(FirmwareHeader::new().to_bytes(&mut hdr_buf).is_err()); + + Ok(()) + } +} From 75b2ea55b7208ee2441176f38a70d226113ed77f Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:30:21 -0700 Subject: [PATCH 2/8] firmware: add `FirmwareRam` type Adds the `FirmwareRam` type to represent the RAM block in an ITL firmware file. The RAM block contains code executed on the machine during the firmware update process. --- src/types/firmware.rs | 2 + src/types/firmware/ram.rs | 175 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 src/types/firmware/ram.rs diff --git a/src/types/firmware.rs b/src/types/firmware.rs index d166d14..36b5510 100644 --- a/src/types/firmware.rs +++ b/src/types/firmware.rs @@ -1,3 +1,5 @@ pub(crate) mod header; +pub(crate) mod ram; pub use header::*; +pub use ram::*; diff --git a/src/types/firmware/ram.rs b/src/types/firmware/ram.rs new file mode 100644 index 0000000..cd9c975 --- /dev/null +++ b/src/types/firmware/ram.rs @@ -0,0 +1,175 @@ +use alloc::vec::Vec; + +use crate::{Error, Result}; + +/// The maximum length of a [FirmwareRam] block. +pub const FIRMWARE_RAM_MAX: usize = u32::MAX as usize; +/// Length of a [FirmwareRam] section. +pub const FIRMWARE_RAM_SECTION_LEN: usize = 128; + +/// Represents the Firmware RAM block in the ITL firmware file. +/// +/// The RAM block immediately follows the [FirmwareHeader](super::FirmwareHeader), and has a +/// variable length. +/// +/// Typical size is 4096 bytes. +/// +/// The RAM block is sent to the device in 128-byte sections. +/// +/// An XOR-checksum is calculated over each byte in the [FirmwareRam] block. At the end of +/// transmission, the device will return its XOR-checksum byte, which should be checked against +/// the local checksum. If there is a mismatch, abort the firmware update, and retry. +pub struct FirmwareRam { + block: Vec, + index: usize, +} + +impl FirmwareRam { + /// Creates a new [FirmwareRam]. + pub const fn new() -> Self { + Self { + block: Vec::new(), + index: 0, + } + } + + /// Creates a new [FirmwareRam] from the provided buffer. + /// + /// Returns: + /// - `Ok(FirmwareRam)` on success + /// - `Err(Error)` if `val` length exceeds [FIRMWARE_RAM_MAX]. + // TODO: should we do checks for valid machine instructions? Are there specifications available + // for the ITL instruction set used on their devices? + pub fn create(val: &[u8]) -> Result { + let len = val.len(); + if len > FIRMWARE_RAM_MAX { + Err(Error::Firmware(format!( + "invalid firmware RAM length, have: {len}, max: {FIRMWARE_RAM_MAX}" + ))) + } else { + Ok(Self { + block: val.into(), + index: 0, + }) + } + } + + /// Gets the next [FIRMWARE_SECTION_LEN] bytes of the [FirmwareRam], and advances the `index` to + /// the end of the returned section. + /// + /// If there are no more bytes, `None` is returned. + /// + /// If [FirmwareRam] does not divide into even [FIRMWARE_SECTION_LEN] segments, the remaining + /// bytes will be returned in the final section. + /// + /// Example: + /// + /// ```no_run + /// use ssp::FirmwareRam; + /// + /// let mut ram_block = FirmwareRam::try_from([0xff; 4096]).expect("invalid RAM block size"); + /// while let Some(section) = ram_block.next_section() { + /// // socket.write_all(section).expect("failure writing to socket"); + /// } + /// ``` + pub fn next_section(&mut self) -> Option<&[u8]> { + if self.index >= self.block.len() { + None + } else if self.block.len() - self.index < FIRMWARE_RAM_SECTION_LEN { + let start = self.index; + self.index = self.block.len(); + Some(&self.block[start..]) + } else { + let start = self.index; + let end = start + FIRMWARE_RAM_SECTION_LEN; + self.index = end; + Some(&self.block[start..end]) + } + } + + /// Gets the current index in the [FirmwareRam] block. + pub const fn index(&self) -> usize { + self.index + } +} + +impl TryFrom<&[u8]> for FirmwareRam { + type Error = Error; + + fn try_from(val: &[u8]) -> Result { + Self::create(val) + } +} + +impl TryFrom<&[u8; N]> for FirmwareRam { + type Error = Error; + + fn try_from(val: &[u8; N]) -> Result { + val.as_ref().try_into() + } +} + +impl TryFrom<[u8; N]> for FirmwareRam { + type Error = Error; + + fn try_from(val: [u8; N]) -> Result { + val.as_ref().try_into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_ram_sections() -> Result<()> { + let exp_ram_buf = [0xff; 4096]; + let mut sections = 0; + let mut index = 0; + let exp_sections = 32; + + let mut ram_block = FirmwareRam::create(&exp_ram_buf)?; + + while let Some(section) = ram_block.next_section() { + assert_eq!(section, &exp_ram_buf[index..index + section.len()]); + assert!(section.len() <= FIRMWARE_RAM_SECTION_LEN); + index += section.len(); + sections += 1; + } + + assert_eq!(sections, exp_sections); + + Ok(()) + } + + #[test] + fn test_ram_uneven_sections() -> Result<()> { + let exp_ram_buf = [0xff; 192]; + let mut sections = 0; + let mut index = 0; + let exp_sections = 2; + + let mut ram_block = FirmwareRam::create(&exp_ram_buf)?; + + while let Some(section) = ram_block.next_section() { + assert_eq!(section, &exp_ram_buf[index..index + section.len()]); + assert!(section.len() <= FIRMWARE_RAM_SECTION_LEN); + index += section.len(); + sections += 1; + } + + assert_eq!(sections, exp_sections); + + Ok(()) + } + + #[test] + fn test_invalid_ram_block() { + let bad_ram_buf = vec![0xff; FIRMWARE_RAM_MAX + 1]; + let bad_slice: &[u8] = bad_ram_buf.as_ref(); + + assert!(FirmwareRam::create(bad_slice).is_err()); + assert!(FirmwareRam::try_from(bad_slice).is_err()); + } +} From 7218c7b76169f6f571d500471d773798b39105e7 Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:32:57 -0700 Subject: [PATCH 3/8] firmware: add `FirmwareData` type Adds the `FirmwareData` type to represent the dataset block in an ITL firmware file. The dataset block contains currency set information, and other data needed to run the validator device. --- src/types/firmware.rs | 2 + src/types/firmware/dataset.rs | 175 ++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 src/types/firmware/dataset.rs diff --git a/src/types/firmware.rs b/src/types/firmware.rs index 36b5510..5ac7ff0 100644 --- a/src/types/firmware.rs +++ b/src/types/firmware.rs @@ -1,5 +1,7 @@ +pub(crate) mod dataset; pub(crate) mod header; pub(crate) mod ram; +pub use dataset::*; pub use header::*; pub use ram::*; diff --git a/src/types/firmware/dataset.rs b/src/types/firmware/dataset.rs new file mode 100644 index 0000000..93ecd2b --- /dev/null +++ b/src/types/firmware/dataset.rs @@ -0,0 +1,175 @@ +use alloc::vec::Vec; + +use crate::{Error, Result}; + +/// The maximum length of a [FirmwareData] block. +pub const FIRMWARE_DATA_MAX: usize = u32::MAX as usize; +/// Length of a [FirmwareData] section. +pub const FIRMWARE_DATA_SECTION_LEN: usize = 128; + +/// Represents the Firmware DATA block in the ITL firmware file. +/// +/// The DATA block immediately follows the [FirmwareRam](super::FirmwareRam), and has a +/// variable length. +/// +/// The DATA block is sent to the device in 128-byte sections. +/// +/// An XOR-checksum is calculated over each byte in the [FirmwareData] block. At the end of +/// transmission of each section, the host will send the device the calculated XOR-checksum byte. +/// The device responds with its calculated XOR-checksum byte. +/// +/// If there is a mismatch, abort the firmware update, and retry. +pub struct FirmwareData { + block: Vec, + index: usize, +} + +impl FirmwareData { + /// Creates a new [FirmwareData]. + pub const fn new() -> Self { + Self { + block: Vec::new(), + index: 0, + } + } + + /// Creates a new [FirmwareData] from the provided buffer. + /// + /// Returns: + /// - `Ok(FirmwareData)` on success + /// - `Err(Error)` if `val` length exceeds [FIRMWARE_DATA_MAX]. + // TODO: should we do checks for valid machine instructions? Are there specifications available + // for the ITL instruction set used on their devices? + pub fn create(val: &[u8]) -> Result { + let len = val.len(); + if len > FIRMWARE_DATA_MAX { + Err(Error::Firmware(format!( + "invalid firmware dataset length, have: {len}, max: {FIRMWARE_DATA_MAX}" + ))) + } else { + Ok(Self { + block: val.into(), + index: 0, + }) + } + } + + /// Gets the next [FIRMWARE_SECTION_LEN] bytes of the [FirmwareData], and advances the `index` to + /// the end of the returned section. + /// + /// If there are no more bytes, `None` is returned. + /// + /// If [FirmwareData] does not divide into even [FIRMWARE_SECTION_LEN] segments, the remaining + /// bytes will be returned in the final section. + /// + /// Example: + /// + /// ```no_run + /// use ssp::FirmwareData; + /// + /// let mut ram_block = FirmwareData::try_from([0xff; 4096]).expect("invalid DATA block size"); + /// while let Some(section) = ram_block.next_section() { + /// // socket.write_all(section).expect("failure writing to socket"); + /// } + /// ``` + pub fn next_section(&mut self) -> Option<&[u8]> { + if self.index >= self.block.len() { + None + } else if self.block.len() - self.index < FIRMWARE_DATA_SECTION_LEN { + let start = self.index; + self.index = self.block.len(); + Some(&self.block[start..]) + } else { + let start = self.index; + let end = start + FIRMWARE_DATA_SECTION_LEN; + self.index = end; + Some(&self.block[start..end]) + } + } + + /// Gets the current index in the [FirmwareData] block. + pub const fn index(&self) -> usize { + self.index + } +} + +impl TryFrom<&[u8]> for FirmwareData { + type Error = Error; + + fn try_from(val: &[u8]) -> Result { + Self::create(val) + } +} + +impl TryFrom<&[u8; N]> for FirmwareData { + type Error = Error; + + fn try_from(val: &[u8; N]) -> Result { + val.as_ref().try_into() + } +} + +impl TryFrom<[u8; N]> for FirmwareData { + type Error = Error; + + fn try_from(val: [u8; N]) -> Result { + val.as_ref().try_into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + #[test] + fn test_data_sections() -> Result<()> { + let exp_data_buf = [0xff; 4096]; + let mut sections = 0; + let mut index = 0; + let exp_sections = 32; + + let mut data_block = FirmwareData::create(&exp_data_buf)?; + + while let Some(section) = data_block.next_section() { + assert_eq!(section, &exp_data_buf[index..index + section.len()]); + assert!(section.len() <= FIRMWARE_DATA_SECTION_LEN); + index += section.len(); + sections += 1; + } + + assert_eq!(sections, exp_sections); + + Ok(()) + } + + #[test] + fn test_data_uneven_sections() -> Result<()> { + let exp_data_buf = [0xff; 192]; + let mut sections = 0; + let mut index = 0; + let exp_sections = 2; + + let mut data_block = FirmwareData::create(&exp_data_buf)?; + + while let Some(section) = data_block.next_section() { + assert_eq!(section, &exp_data_buf[index..index + section.len()]); + assert!(section.len() <= FIRMWARE_DATA_SECTION_LEN); + index += section.len(); + sections += 1; + } + + assert_eq!(sections, exp_sections); + + Ok(()) + } + + #[test] + fn test_invalid_data_block() { + let bad_data_buf = vec![0xff; FIRMWARE_DATA_MAX + 1]; + let bad_slice: &[u8] = bad_data_buf.as_ref(); + + assert!(FirmwareData::create(bad_slice).is_err()); + assert!(FirmwareData::try_from(bad_slice).is_err()); + } +} From f6d9237fbc55bbb478dad87e8363d44f680cb7d3 Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:18:47 -0700 Subject: [PATCH 4/8] firmware: add `parse_firmware_file` function Adds the `parse_firmware_file` helper function to parse `FirmwareHeader`, `FirmwareRam`, and `FirmwareData` structures from an ITL firmware file. --- src/types/firmware.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/types/firmware.rs b/src/types/firmware.rs index 5ac7ff0..038527f 100644 --- a/src/types/firmware.rs +++ b/src/types/firmware.rs @@ -5,3 +5,42 @@ pub(crate) mod ram; pub use dataset::*; pub use header::*; pub use ram::*; + +/// Parses an ITL firmware file into [FirmwareHeader], [FirmwareRam], and [FirmwareData] +/// structures. +#[cfg(feature = "std")] +pub fn parse_firmware_file( + file_path: &str, +) -> crate::Result<(FirmwareHeader, FirmwareRam, FirmwareData)> { + use crate::Error; + use std::{fs, io::Read, mem}; + + let md = fs::metadata(file_path) + .map_err(|err| Error::Firmware(format!("error getting firmware file metadata: {err}")))?; + + let mut f = fs::File::open(file_path) + .map_err(|err| Error::Firmware(format!("error opening firmware file: {err}")))?; + + let file_len = md.len() as usize; + let mut file_buf = Vec::with_capacity(file_len); + + f.read_to_end(&mut file_buf) + .map_err(|err| Error::Firmware(format!("error reading firmware file: {err}")))?; + + let file_slice: &[u8] = file_buf.as_ref(); + let header = FirmwareHeader::try_from(file_slice)?; + + let ram_start = mem::size_of::(); + let ram_end = ram_start + header.ram_len() as usize; + + if ram_end > file_len { + Err(Error::Firmware(format!( + "invalid RAM block length, have: {ram_end}, max: {file_len}" + ))) + } else { + let ram = FirmwareRam::try_from(file_buf[ram_start..ram_end].as_ref())?; + let data = FirmwareData::try_from(file_buf[ram_end..].as_ref())?; + + Ok((header, ram, data)) + } +} From 238fdeee4032c98929ece6d3dbc5b1e427a797d8 Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:33:41 -0700 Subject: [PATCH 5/8] firmware: add `ProgramFirmware` messages Adds the `ProgramFirmwareCommand` and `ProgramFirmwareResponse` messages for use programming unit firmware. --- src/firmware.rs | 5 ++ src/firmware/command.rs | 101 +++++++++++++++++++++++++++++++++++++++ src/firmware/response.rs | 64 +++++++++++++++++++++++++ src/len.rs | 4 ++ src/lib.rs | 2 + 5 files changed, 176 insertions(+) create mode 100644 src/firmware.rs create mode 100644 src/firmware/command.rs create mode 100644 src/firmware/response.rs diff --git a/src/firmware.rs b/src/firmware.rs new file mode 100644 index 0000000..472782a --- /dev/null +++ b/src/firmware.rs @@ -0,0 +1,5 @@ +pub(crate) mod command; +pub(crate) mod response; + +pub use command::*; +pub use response::*; diff --git a/src/firmware/command.rs b/src/firmware/command.rs new file mode 100644 index 0000000..ed88999 --- /dev/null +++ b/src/firmware/command.rs @@ -0,0 +1,101 @@ +use crate::{ + impl_command_display, impl_command_ops, impl_default, impl_message_from_buf, impl_message_ops, + len::PROGRAM_FIRMWARE_COMMAND, CommandOps, MessageOps, MessageType, +}; + +mod index { + pub const FIRMWARE_CODE: usize = 4; +} + +/// Represents the type of programming the unit expects. +// FIXME: there is currently only one code described in public documentation. +// Are there others still in use? +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProgramFirmwareCode { + Ram = 0x03, + Reserved(u8), +} + +impl From for ProgramFirmwareCode { + fn from(val: u8) -> Self { + match val { + 0x03 => Self::Ram, + _ => Self::Reserved(val), + } + } +} + +impl From<&ProgramFirmwareCode> for u8 { + fn from(val: &ProgramFirmwareCode) -> Self { + match val { + ProgramFirmwareCode::Ram => 0x03, + ProgramFirmwareCode::Reserved(code) => *code, + } + } +} + +impl From for u8 { + fn from(val: ProgramFirmwareCode) -> Self { + (&val).into() + } +} + +/// ProgramFirmware - Command (0x0B) +/// +/// This two byte command prepares the unit for firmware programming. +/// +/// The `FirmwareCode` field defines the type of programming. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ProgramFirmwareCommand { + buf: [u8; PROGRAM_FIRMWARE_COMMAND], +} + +impl ProgramFirmwareCommand { + /// Creates a new [SyncCommand] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; PROGRAM_FIRMWARE_COMMAND], + }; + + msg.init(); + msg.set_command(MessageType::ProgramFirmware); + msg.set_firmware_code(ProgramFirmwareCode::Ram); + + msg + } + + /// Gets the [FirmwareCode] for the type of programming the unit expects. + /// + /// Example: + /// + /// ``` + /// # use ssp; + /// let fw_cmd = ssp::ProgramFirmwareCommand::new(); + /// assert_eq!(fw_cmd.firmware_code(), ssp::ProgramFirmwareCode::Ram); + /// ``` + pub fn firmware_code(&self) -> ProgramFirmwareCode { + self.buf[index::FIRMWARE_CODE].into() + } + + /// Sets the [FirmwareCode] for the type of programming the unit expects. + /// + /// Example: + /// + /// ``` + /// # use ssp; + /// let mut fw_cmd = ssp::ProgramFirmwareCommand::new(); + /// fw_cmd.set_firmware_code(ssp::ProgramFirmwareCode::Ram); + /// assert_eq!(fw_cmd.firmware_code(), ssp::ProgramFirmwareCode::Ram); + /// ``` + pub fn set_firmware_code(&mut self, code: ProgramFirmwareCode) { + self.buf[index::FIRMWARE_CODE] = code.into(); + } +} + +impl_default!(ProgramFirmwareCommand); +impl_command_display!(ProgramFirmwareCommand); +impl_message_from_buf!(ProgramFirmwareCommand); +impl_message_ops!(ProgramFirmwareCommand); +impl_command_ops!(ProgramFirmwareCommand); diff --git a/src/firmware/response.rs b/src/firmware/response.rs new file mode 100644 index 0000000..0b3e470 --- /dev/null +++ b/src/firmware/response.rs @@ -0,0 +1,64 @@ +use crate::{ + impl_default, impl_message_from_buf, impl_message_ops, impl_response_display, + impl_response_ops, len::PROGRAM_FIRMWARE_RESPONSE, message::MessageOps, MessageType, +}; + +mod index { + pub const BLOCK_LEN: usize = 4; + pub const BLOCK_LEN_END: usize = 5; +} + +/// ProgramFirmware - Response (0x0B) +/// +/// Represents a response to an [ProgramFirmwareCommand](crate::ProgramFirmwareCommand) message. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ProgramFirmwareResponse { + buf: [u8; PROGRAM_FIRMWARE_RESPONSE], +} + +impl ProgramFirmwareResponse { + /// Creates a new [ProgramFirmwareResponse] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; PROGRAM_FIRMWARE_RESPONSE], + }; + + msg.init(); + + msg + } + + /// Gets the expected block length from the device. + /// + /// Example: + /// + /// ``` + /// # use ssp; + /// let fw_res = ssp::ProgramFirmwareResponse::new(); + /// assert_eq!(fw_res.block_len(), 0); + /// ``` + pub fn block_len(&self) -> u16 { + u16::from_le_bytes([self.buf[index::BLOCK_LEN], self.buf[index::BLOCK_LEN_END]]) + } + + /// Sets the expected block length from the device. + /// + /// Example: + /// + /// ``` + /// # use ssp; + /// let mut fw_res = ssp::ProgramFirmwareResponse::new(); + /// fw_res.set_block_len(128); + /// assert_eq!(fw_res.block_len(), 128); + /// ``` + pub fn set_block_len(&mut self, len: u16) { + self.buf[index::BLOCK_LEN..=index::BLOCK_LEN_END].copy_from_slice(&len.to_le_bytes()); + } +} + +impl_default!(ProgramFirmwareResponse); +impl_message_from_buf!(ProgramFirmwareResponse); +impl_message_ops!(ProgramFirmwareResponse, MessageType::ProgramFirmware); +impl_response_ops!(ProgramFirmwareResponse); +impl_response_display!(ProgramFirmwareResponse); diff --git a/src/len.rs b/src/len.rs index 07ef277..2cd3f20 100644 --- a/src/len.rs +++ b/src/len.rs @@ -162,6 +162,10 @@ pub const ENABLE_PAYOUT_RESPONSE: usize = 6; pub const DISABLE_PAYOUT_COMMAND: usize = 6; /// DisablePayout Response full message length. pub const DISABLE_PAYOUT_RESPONSE: usize = 6; +/// ProgramFirmware Command full message length. +pub const PROGRAM_FIRMWARE_COMMAND: usize = 7; +/// ProgramFirmware Response full message length. +pub const PROGRAM_FIRMWARE_RESPONSE: usize = 8; /// Encrypted Command full message length. /// /// Because encrypted messages have variable lengths, set the static length to maximum diff --git a/src/lib.rs b/src/lib.rs index cbcae19..58b2a72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,7 @@ pub mod encrypted; pub mod encryption_reset; pub mod error; pub mod event_ack; +pub mod firmware; pub mod get_barcode_data; pub mod get_barcode_inhibit; pub mod get_barcode_reader_configuration; @@ -93,6 +94,7 @@ pub use encrypted::*; pub use encryption_reset::*; pub use error::*; pub use event_ack::*; +pub use firmware::*; pub use get_barcode_data::*; pub use get_barcode_inhibit::*; pub use get_barcode_reader_configuration::*; From 5fe09505bdfb9db28b888212a848275456449874 Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:35:32 -0700 Subject: [PATCH 6/8] fixup: encrypted: silence compiler warnings Uses conditional compilation to not create a `rng` variable when in `test` mode. Clears compiler warnings about unused variable. --- src/encrypted/command.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/encrypted/command.rs b/src/encrypted/command.rs index 4d303f5..4469841 100644 --- a/src/encrypted/command.rs +++ b/src/encrypted/command.rs @@ -174,6 +174,7 @@ impl EncryptedCommand { return; } + #[cfg(not(test))] let mut rng = rand::thread_rng(); let start = self.packing_start(); From 80c65c7058b9f8e376f52b53033900197b36b1d4 Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Fri, 15 Dec 2023 16:29:57 -0700 Subject: [PATCH 7/8] firmware: add `FirmwareHeader` messages Adds the `FirmwareHeaderCommand` and `FirmwareHeaderResponse` messages for sending the ITL firmware file header to the validator unit. --- src/firmware.rs | 4 ++ src/firmware/header_command.rs | 80 +++++++++++++++++++++++++++++++++ src/firmware/header_response.rs | 32 +++++++++++++ src/len.rs | 4 ++ 4 files changed, 120 insertions(+) create mode 100644 src/firmware/header_command.rs create mode 100644 src/firmware/header_response.rs diff --git a/src/firmware.rs b/src/firmware.rs index 472782a..fe16857 100644 --- a/src/firmware.rs +++ b/src/firmware.rs @@ -1,5 +1,9 @@ pub(crate) mod command; +pub(crate) mod header_command; +pub(crate) mod header_response; pub(crate) mod response; pub use command::*; +pub use header_command::*; +pub use header_response::*; pub use response::*; diff --git a/src/firmware/header_command.rs b/src/firmware/header_command.rs new file mode 100644 index 0000000..7b5af34 --- /dev/null +++ b/src/firmware/header_command.rs @@ -0,0 +1,80 @@ +use crate::{ + impl_command_display, impl_command_ops, impl_default, impl_message_from_buf, impl_message_ops, + len::FIRMWARE_HEADER_COMMAND, CommandOps, FirmwareHeader, MessageOps, Result, +}; + +mod index { + pub const FIRMWARE_HEADER: usize = 3; + pub const FIRMWARE_HEADER_END: usize = 131; +} + +/// ProgramFirmware - Command (0x0B) +/// +/// This two byte command prepares the unit for firmware programming. +/// +/// The `FirmwareCode` field defines the type of programming. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FirmwareHeaderCommand { + buf: [u8; FIRMWARE_HEADER_COMMAND], +} + +impl FirmwareHeaderCommand { + /// Creates a new [FirmwareHeaderCommand] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FIRMWARE_HEADER_COMMAND], + }; + + msg.init(); + msg.set_firmware_header(&FirmwareHeader::new()).ok(); + + msg + } + + /// Creates a new [FirmwareHeaderCommand] message. + pub fn create(header: &FirmwareHeader) -> Result { + let mut msg = Self { + buf: [0u8; FIRMWARE_HEADER_COMMAND], + }; + + msg.init(); + msg.set_firmware_header(header)?; + + Ok(msg) + } + + /// Gets the [FirmwareCode] for the type of programming the unit expects. + /// + /// Example: + /// + /// ``` + /// # use ssp; + /// let fw_cmd = ssp::FirmwareHeaderCommand::new(); + /// assert_eq!(fw_cmd.firmware_header().unwrap(), ssp::FirmwareHeader::new()); + /// ``` + pub fn firmware_header(&self) -> Result { + FirmwareHeader::try_from(&self.buf[index::FIRMWARE_HEADER..index::FIRMWARE_HEADER_END]) + } + + /// Sets the [FirmwareHeader] for the type of programming the unit expects. + /// + /// Example: + /// + /// ``` + /// # use ssp; + /// let mut fw_cmd = ssp::FirmwareHeaderCommand::new(); + /// let header = ssp::FirmwareHeader::new(); + /// fw_cmd.set_firmware_header(&header); + /// assert_eq!(fw_cmd.firmware_header().unwrap(), header); + /// ``` + pub fn set_firmware_header(&mut self, header: &FirmwareHeader) -> Result<()> { + header.to_bytes(&mut self.buf[index::FIRMWARE_HEADER..index::FIRMWARE_HEADER_END]) + } +} + +impl_default!(FirmwareHeaderCommand); +impl_command_display!(FirmwareHeaderCommand); +impl_message_from_buf!(FirmwareHeaderCommand); +impl_message_ops!(FirmwareHeaderCommand); +impl_command_ops!(FirmwareHeaderCommand); diff --git a/src/firmware/header_response.rs b/src/firmware/header_response.rs new file mode 100644 index 0000000..7c77069 --- /dev/null +++ b/src/firmware/header_response.rs @@ -0,0 +1,32 @@ +use crate::{ + impl_default, impl_message_from_buf, impl_message_ops, impl_response_display, + impl_response_ops, len::FIRMWARE_HEADER_RESPONSE, message::MessageOps, MessageType, +}; + +/// FirmwareHeader - Response (0x0B) +/// +/// Represents a response to an [FirmwareHeaderCommand](crate::FirmwareHeaderCommand) message. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FirmwareHeaderResponse { + buf: [u8; FIRMWARE_HEADER_RESPONSE], +} + +impl FirmwareHeaderResponse { + /// Creates a new [ProgramFirmwareResponse] message. + pub fn new() -> Self { + let mut msg = Self { + buf: [0u8; FIRMWARE_HEADER_RESPONSE], + }; + + msg.init(); + + msg + } +} + +impl_default!(FirmwareHeaderResponse); +impl_message_from_buf!(FirmwareHeaderResponse); +impl_message_ops!(FirmwareHeaderResponse, MessageType::ProgramFirmware); +impl_response_ops!(FirmwareHeaderResponse); +impl_response_display!(FirmwareHeaderResponse); diff --git a/src/len.rs b/src/len.rs index 2cd3f20..906b5c1 100644 --- a/src/len.rs +++ b/src/len.rs @@ -166,6 +166,10 @@ pub const DISABLE_PAYOUT_RESPONSE: usize = 6; pub const PROGRAM_FIRMWARE_COMMAND: usize = 7; /// ProgramFirmware Response full message length. pub const PROGRAM_FIRMWARE_RESPONSE: usize = 8; +/// FirmwareHeader Command full message length. +pub const FIRMWARE_HEADER_COMMAND: usize = 133; +/// FirmwareHeader Response full message length. +pub const FIRMWARE_HEADER_RESPONSE: usize = 6; /// Encrypted Command full message length. /// /// Because encrypted messages have variable lengths, set the static length to maximum From bfa9595d842e5d64971e0b3d7e5cd58d396cd2ec Mon Sep 17 00:00:00 2001 From: SSP Rust Developers <135766782+ssp-rs@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:28:08 -0700 Subject: [PATCH 8/8] firmware: clean up and add `FIrmwareHeader` formatter Cleans up some code, and adds a formatter implementation for `FirmwareHeader`. --- src/types/firmware.rs | 2 ++ src/types/firmware/dataset.rs | 41 +++++++++++++++++++++++++++++------ src/types/firmware/header.rs | 37 ++++++++++++++++++++++++++++++- src/types/firmware/ram.rs | 2 ++ 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/types/firmware.rs b/src/types/firmware.rs index 038527f..dc37b5e 100644 --- a/src/types/firmware.rs +++ b/src/types/firmware.rs @@ -6,6 +6,8 @@ pub use dataset::*; pub use header::*; pub use ram::*; +pub const FIRMWARE_ACK: u8 = 0x32; + /// Parses an ITL firmware file into [FirmwareHeader], [FirmwareRam], and [FirmwareData] /// structures. #[cfg(feature = "std")] diff --git a/src/types/firmware/dataset.rs b/src/types/firmware/dataset.rs index 93ecd2b..7a83e43 100644 --- a/src/types/firmware/dataset.rs +++ b/src/types/firmware/dataset.rs @@ -6,6 +6,10 @@ use crate::{Error, Result}; pub const FIRMWARE_DATA_MAX: usize = u32::MAX as usize; /// Length of a [FirmwareData] section. pub const FIRMWARE_DATA_SECTION_LEN: usize = 128; +/// Default length of a [FirmwareData] block. +/// +/// A block consists of one or more sections. +pub const DEFAULT_DATA_BLOCK_LEN: usize = 128; /// Represents the Firmware DATA block in the ITL firmware file. /// @@ -19,9 +23,12 @@ pub const FIRMWARE_DATA_SECTION_LEN: usize = 128; /// The device responds with its calculated XOR-checksum byte. /// /// If there is a mismatch, abort the firmware update, and retry. +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] pub struct FirmwareData { block: Vec, index: usize, + block_len: usize, } impl FirmwareData { @@ -30,6 +37,7 @@ impl FirmwareData { Self { block: Vec::new(), index: 0, + block_len: DEFAULT_DATA_BLOCK_LEN, } } @@ -38,9 +46,7 @@ impl FirmwareData { /// Returns: /// - `Ok(FirmwareData)` on success /// - `Err(Error)` if `val` length exceeds [FIRMWARE_DATA_MAX]. - // TODO: should we do checks for valid machine instructions? Are there specifications available - // for the ITL instruction set used on their devices? - pub fn create(val: &[u8]) -> Result { + pub fn create(val: &[u8], block_len: usize) -> Result { let len = val.len(); if len > FIRMWARE_DATA_MAX { Err(Error::Firmware(format!( @@ -50,6 +56,7 @@ impl FirmwareData { Ok(Self { block: val.into(), index: 0, + block_len, }) } } @@ -87,17 +94,37 @@ impl FirmwareData { } } + /// Gets the block length returned by the RAM programming command. + pub fn block_len(&self) -> usize { + self.block_len + } + + /// Sets the block length returned by the RAM programming command. + pub fn set_block_len(&mut self, len: u16) { + self.block_len = len as usize; + } + /// Gets the current index in the [FirmwareData] block. pub const fn index(&self) -> usize { self.index } + + /// Gets the length of the [FirmwareData] buffer. + pub fn len(&self) -> usize { + self.block.len() + } + + /// Gets whether the [FirmwareData] buffer is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } impl TryFrom<&[u8]> for FirmwareData { type Error = Error; fn try_from(val: &[u8]) -> Result { - Self::create(val) + Self::create(val, DEFAULT_DATA_BLOCK_LEN) } } @@ -129,7 +156,7 @@ mod tests { let mut index = 0; let exp_sections = 32; - let mut data_block = FirmwareData::create(&exp_data_buf)?; + let mut data_block = FirmwareData::create(&exp_data_buf, FIRMWARE_DATA_SECTION_LEN)?; while let Some(section) = data_block.next_section() { assert_eq!(section, &exp_data_buf[index..index + section.len()]); @@ -150,7 +177,7 @@ mod tests { let mut index = 0; let exp_sections = 2; - let mut data_block = FirmwareData::create(&exp_data_buf)?; + let mut data_block = FirmwareData::create(&exp_data_buf, FIRMWARE_DATA_SECTION_LEN)?; while let Some(section) = data_block.next_section() { assert_eq!(section, &exp_data_buf[index..index + section.len()]); @@ -169,7 +196,7 @@ mod tests { let bad_data_buf = vec![0xff; FIRMWARE_DATA_MAX + 1]; let bad_slice: &[u8] = bad_data_buf.as_ref(); - assert!(FirmwareData::create(bad_slice).is_err()); + assert!(FirmwareData::create(bad_slice, FIRMWARE_DATA_SECTION_LEN).is_err()); assert!(FirmwareData::try_from(bad_slice).is_err()); } } diff --git a/src/types/firmware/header.rs b/src/types/firmware/header.rs index 14ad902..4d0023e 100644 --- a/src/types/firmware/header.rs +++ b/src/types/firmware/header.rs @@ -1,4 +1,4 @@ -use core::mem; +use core::{fmt, mem}; use crate::{Error, Result}; @@ -47,6 +47,16 @@ impl FirmwareHeader { u32::from_be_bytes([0, self.magic[0], self.magic[1], self.magic[2]]) } + /// Gets the [FirmwareHeader] magic bytes as a string. + pub fn magic_str(&self) -> &str { + core::str::from_utf8(self.magic.as_ref()).unwrap_or("") + } + + /// Gets the [FirmwareHeader] reserved bytes. + pub(crate) fn reserved(&self) -> u32 { + u32::from_be_bytes([0, self._reserved[0], self._reserved[1], self._reserved[2]]) + } + /// Gets the [FirmwareHeader] file code for the firmware data. pub const fn file_code(&self) -> u8 { self.file_code @@ -179,6 +189,31 @@ impl TryFrom<[u8; N]> for FirmwareHeader { } } +impl fmt::Display for FirmwareHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let magic = self.magic_str(); + let reserved = self.reserved(); + let file_code = self.file_code(); + let ram_len = self.ram_len(); + + write!(f, "{{")?; + write!(f, r#""magic": "{magic}", "#)?; + write!(f, r#""reserved": {reserved}, "#)?; + write!(f, r#""file_code": {file_code}, "#)?; + write!(f, r#""ram_len": {ram_len}, "#)?; + write!(f, r#""data": ["#)?; + + for (i, d) in self.data().iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{d}")?; + } + + write!(f, "]}}") + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/types/firmware/ram.rs b/src/types/firmware/ram.rs index cd9c975..422d3f8 100644 --- a/src/types/firmware/ram.rs +++ b/src/types/firmware/ram.rs @@ -19,6 +19,8 @@ pub const FIRMWARE_RAM_SECTION_LEN: usize = 128; /// An XOR-checksum is calculated over each byte in the [FirmwareRam] block. At the end of /// transmission, the device will return its XOR-checksum byte, which should be checked against /// the local checksum. If there is a mismatch, abort the firmware update, and retry. +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] pub struct FirmwareRam { block: Vec, index: usize,