From 0bdf4be077cd7284ecf8a7dc12c94f3e004cc3d0 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Thu, 31 Jul 2025 16:55:41 +0800 Subject: [PATCH 1/4] pldm-platform: Add AsciiString This is a fixed length null terminated string for encoding/decoding PDRs, wrapping the underlying VecWrap bytes. Signed-off-by: Matt Johnston --- pldm-file/examples/host.rs | 7 +-- pldm-platform/src/proto.rs | 99 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/pldm-file/examples/host.rs b/pldm-file/examples/host.rs index 1ff0c51..5df0758 100644 --- a/pldm-file/examples/host.rs +++ b/pldm-file/examples/host.rs @@ -173,11 +173,6 @@ fn handle_get_pdr( .try_into() .context("File size > u32")?; - // null terminated filename - let mut file_name = FILENAME.as_bytes().to_vec(); - file_name.push(0x00); - let file_name = pldm_platform::Vec::from_slice(&file_name).unwrap(); - let pdr_resp = GetPDRResp::new_single( PDR_HANDLE, PdrRecord::FileDescriptor(FileDescriptorPdr { @@ -196,7 +191,7 @@ fn handle_get_pdr( file_max_size, // TODO file_max_desc_count: 1, - file_name: file_name.into(), + file_name: FILENAME.try_into().expect("Filename too long"), oem_file_name: Default::default(), }), )?; diff --git a/pldm-platform/src/proto.rs b/pldm-platform/src/proto.rs index 7808b13..bbaeef2 100644 --- a/pldm-platform/src/proto.rs +++ b/pldm-platform/src/proto.rs @@ -260,6 +260,97 @@ where } } +/// A null terminated ascii string. +#[derive(DekuWrite, Default, Clone)] +pub struct AsciiString(pub VecWrap); + +impl AsciiString { + /// Return the length of the string + /// + /// Null terminator is included. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether `len() == 0`. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return the underlying bytes. + /// + /// This includes any null terminator. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl core::fmt::Debug for AsciiString { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "\"")?; + for c in self.0.escape_ascii() { + write!(f, "{}", char::from(c))?; + } + write!(f, "\"")?; + if !self.0.is_empty() && !self.0.ends_with(&[0x00]) { + write!(f, " (missing null terminator)")?; + } + Ok(()) + } +} + +impl TryFrom<&[u8]> for AsciiString { + type Error = (); + + /// Convert from a byte slice. + /// + /// No null terminating byte is added, it should be provided by the caller. + fn try_from(v: &[u8]) -> Result { + Ok(Self(VecWrap(heapless::Vec::from_slice(v)?))) + } +} + +impl TryFrom<&str> for AsciiString { + type Error = (); + + /// Convert from a `str`. + /// + /// A null terminating byte is added. + fn try_from(v: &str) -> Result { + let mut h = heapless::Vec::from_slice(v.as_bytes())?; + h.push(0x00).map_err(|_| ())?; + Ok(Self(VecWrap(h))) + } +} + +impl<'a, Predicate, const N: usize> DekuReader<'a, (Limit, ())> + for AsciiString +where + Predicate: FnMut(&u8) -> bool, +{ + fn from_reader_with_ctx< + R: deku::no_std_io::Read + deku::no_std_io::Seek, + >( + reader: &mut deku::reader::Reader, + (limit, _ctx): (Limit, ()), + ) -> core::result::Result { + let Limit::Count(count) = limit else { + return Err(DekuError::Assertion( + "Only count implemented for heapless::Vec".into(), + )); + }; + + let mut v = heapless::Vec::new(); + for _ in 0..count { + v.push(u8::from_reader_with_ctx(reader, ())?).map_err(|_| { + DekuError::InvalidParam("Too many elements".into()) + })? + } + + Ok(AsciiString(VecWrap(v))) + } +} + #[derive(Debug, DekuRead, DekuWrite, PartialEq, Eq, Clone, Copy)] #[deku(endian = "little")] pub struct SensorId(pub u16); @@ -659,17 +750,15 @@ pub struct FileDescriptorPdr { /// File name. /// /// A null terminated string. - // TODO: null terminated string type // TODO: max length #[deku(count = "file_name_len")] - pub file_name: VecWrap, + pub file_name: AsciiString, #[deku(temp, temp_value = "self.oem_file_name.len() as u8")] pub oem_file_name_len: u8, /// OEM file name. /// - /// A null terminated string. - // TODO: null terminated string type + /// A null terminated string. Must be empty if `oem_file_classification == 0`. #[deku(count = "oem_file_name_len")] - pub oem_file_name: VecWrap, + pub oem_file_name: AsciiString, } From cc70959854f18133d96653cb8050e8bebc7c7c8f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 1 Aug 2025 11:00:35 +0800 Subject: [PATCH 2/4] pldm-platform: Add more standard derive impls This adds Eq comparison and other common traits to the protocol structs. Signed-off-by: Matt Johnston --- pldm-platform/src/proto.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pldm-platform/src/proto.rs b/pldm-platform/src/proto.rs index bbaeef2..7c80c77 100644 --- a/pldm-platform/src/proto.rs +++ b/pldm-platform/src/proto.rs @@ -192,7 +192,7 @@ pub enum SensorState { UpperFatal, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct VecWrap(pub heapless::Vec); impl From> for VecWrap { @@ -261,7 +261,7 @@ where } /// A null terminated ascii string. -#[derive(DekuWrite, Default, Clone)] +#[derive(DekuWrite, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct AsciiString(pub VecWrap); impl AsciiString { @@ -373,7 +373,7 @@ pub struct GetSensorReadingReq { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GetSensorReadingResp { #[deku(temp, temp_value = "reading.deku_id().unwrap()")] data_size: u8, @@ -387,7 +387,7 @@ pub struct GetSensorReadingResp { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GetStateSensorReadingsReq { pub sensor: SensorId, pub rearm: u8, @@ -395,7 +395,7 @@ pub struct GetStateSensorReadingsReq { rsvd: u8, } -#[derive(Debug, DekuRead, DekuWrite, Clone)] +#[derive(Debug, DekuRead, DekuWrite, Clone, PartialEq, Eq)] pub struct StateField { pub op_state: SensorOperationalState, pub present_state: u8, @@ -487,7 +487,7 @@ impl Debug for StateFieldDebug<'_> { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GetStateSensorReadingsResp { #[deku(temp, temp_value = "self.fields.len() as u8")] pub composite_sensor_count: u8, @@ -495,21 +495,21 @@ pub struct GetStateSensorReadingsResp { pub fields: VecWrap, } -#[derive(Debug, DekuRead, DekuWrite, Clone)] +#[derive(Debug, DekuRead, DekuWrite, Clone, PartialEq, Eq)] pub struct SetNumericSensorEnableReq { pub sensor: SensorId, pub set_op_state: SetSensorOperationalState, pub event_enable: SensorEventMessageEnable, } -#[derive(Debug, DekuRead, DekuWrite, Clone)] +#[derive(Debug, DekuRead, DekuWrite, Clone, PartialEq, Eq)] pub struct SetEnableField { pub set_op_state: SetSensorOperationalState, pub event_enable: SensorEventMessageEnable, } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SetStateSensorEnablesReq { pub sensor: SensorId, @@ -534,7 +534,7 @@ pub enum PDRRepositoryState { pub type Timestamp104 = [u8; 13]; #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GetPDRRepositoryInfoResp { pub state: PDRRepositoryState, pub update_time: Timestamp104, @@ -546,7 +546,7 @@ pub struct GetPDRRepositoryInfoResp { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[deku(id_type = "u8")] #[repr(u8)] pub enum TransferOperationFlag { @@ -555,7 +555,7 @@ pub enum TransferOperationFlag { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GetPDRReq { pub record_handle: u32, pub data_transfer_handle: u32, @@ -567,7 +567,7 @@ pub struct GetPDRReq { const MAX_PDR_TRANSFER: usize = 100; #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GetPDRResp { pub next_record_handle: u32, pub next_data_transfer_handle: u32, @@ -665,7 +665,7 @@ impl deku::no_std_io::Seek for &mut Length { pub const PDR_VERSION_1: u8 = 1; #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Pdr { pub record_handle: u32, pub pdr_header_version: u8, @@ -681,7 +681,7 @@ pub struct Pdr { #[non_exhaustive] #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] #[deku(ctx = "pdr_type: u8", id = "pdr_type")] pub enum PdrRecord { #[deku(id = 30)] @@ -697,7 +697,7 @@ impl PdrRecord { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] #[deku(id_type = "u8")] #[repr(u8)] pub enum FileClassification { @@ -730,7 +730,7 @@ pub mod file_capabilities { } #[deku_derive(DekuRead, DekuWrite)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FileDescriptorPdr { pub terminus_handle: u16, pub file_identifier: u16, From f9b4937aa5e668026cc7eb79821982df09d49d9f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 1 Aug 2025 13:08:14 +0800 Subject: [PATCH 3/4] pldm-platform: Fix oem_file_classification_name The length field is now omitted entirely when oem_classification is set to 0. Previously in that case length was included and set to zero, which is not the correct message format. The name field is renamed from oem_file_name to oem_file_classification_name, and is now an Option. Signed-off-by: Matt Johnston --- pldm-file/examples/host.rs | 2 +- pldm-platform/src/proto.rs | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pldm-file/examples/host.rs b/pldm-file/examples/host.rs index 5df0758..3b45273 100644 --- a/pldm-file/examples/host.rs +++ b/pldm-file/examples/host.rs @@ -192,7 +192,7 @@ fn handle_get_pdr( // TODO file_max_desc_count: 1, file_name: FILENAME.try_into().expect("Filename too long"), - oem_file_name: Default::default(), + oem_file_classification_name: Default::default(), }), )?; let enc = pdr_resp.to_bytes().context("Encoding failed")?; diff --git a/pldm-platform/src/proto.rs b/pldm-platform/src/proto.rs index 7c80c77..be10788 100644 --- a/pldm-platform/src/proto.rs +++ b/pldm-platform/src/proto.rs @@ -739,6 +739,10 @@ pub struct FileDescriptorPdr { pub container_id: u16, pub superior_directory: u16, pub file_classification: FileClassification, + /// OEM File Classification + /// + /// `oem_file_classification_name` must be `Some` if this is + /// non-zero (may be an empty string). pub oem_file_classification: u8, pub capabilities: u16, pub file_version: u32, @@ -754,11 +758,20 @@ pub struct FileDescriptorPdr { #[deku(count = "file_name_len")] pub file_name: AsciiString, - #[deku(temp, temp_value = "self.oem_file_name.len() as u8")] + #[deku(skip, cond = "*oem_file_classification == 0")] + #[deku( + temp, + temp_value = "self.oem_file_classification_name.as_ref().map(|f| f.len()).unwrap_or(0) as u8" + )] pub oem_file_name_len: u8, - /// OEM file name. + + /// OEM file classification name. /// - /// A null terminated string. Must be empty if `oem_file_classification == 0`. + /// A null terminated string. Must be `None` if `oem_file_classification == 0`. + #[deku(skip, cond = "*oem_file_classification == 0")] + #[deku( + assert = "(*oem_file_classification > 0) == oem_file_classification_name.is_some()" + )] #[deku(count = "oem_file_name_len")] - pub oem_file_name: AsciiString, + pub oem_file_classification_name: Option>, } From 29a41d34d77c4f6e654f9047c8668dcc2bb76941 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 1 Aug 2025 15:56:14 +0800 Subject: [PATCH 4/4] pldm-platform: Add chrono conversion for Timestamp104 The PLDM file host example now sets updateTime to the local time for testing purposes. Signed-off-by: Matt Johnston --- Cargo.lock | 2 ++ pldm-file/Cargo.toml | 1 + pldm-file/examples/host.rs | 8 +++-- pldm-platform/Cargo.toml | 1 + pldm-platform/src/proto.rs | 74 +++++++++++++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2ec2ea..4f6fe73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,6 +1067,7 @@ name = "pldm-file" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "crc", "deku", "enumset", @@ -1118,6 +1119,7 @@ dependencies = [ name = "pldm-platform" version = "0.1.0" dependencies = [ + "chrono", "deku", "heapless", "log", diff --git a/pldm-file/Cargo.toml b/pldm-file/Cargo.toml index afc9ba5..72f24bb 100644 --- a/pldm-file/Cargo.toml +++ b/pldm-file/Cargo.toml @@ -19,6 +19,7 @@ pldm = { workspace = true } [dev-dependencies] anyhow = "1.0" +chrono = { workspace = true } mctp-linux = { workspace = true } pldm-platform = { workspace = true, features = ["alloc"] } smol = "2.0" diff --git a/pldm-file/examples/host.rs b/pldm-file/examples/host.rs index 3b45273..a44c61e 100644 --- a/pldm-file/examples/host.rs +++ b/pldm-file/examples/host.rs @@ -105,12 +105,16 @@ async fn handle_platform( let mut resp = req.response(); + let update_time = + Timestamp104::try_from(&chrono::Local::now().fixed_offset()) + .unwrap_or_default(); + resp.cc = match Cmd::try_from(req.cmd)? { Cmd::GetPDRRepositoryInfo => { let pdrinfo = GetPDRRepositoryInfoResp { state: PDRRepositoryState::Available, - update_time: [0u8; 13], - oem_update_time: [0u8; 13], + update_time, + oem_update_time: Default::default(), record_count: 1, // TODO. "An implementation is allowed to round this number up to the nearest kilobyte (1024 bytes)." repository_size: 1024, diff --git a/pldm-platform/Cargo.toml b/pldm-platform/Cargo.toml index 035d65b..98107bc 100644 --- a/pldm-platform/Cargo.toml +++ b/pldm-platform/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true categories = ["network-programming", "embedded", "hardware-support"] [dependencies] +chrono = { workspace = true } deku = { workspace = true } heapless = { workspace = true } log = { workspace = true } diff --git a/pldm-platform/src/proto.rs b/pldm-platform/src/proto.rs index be10788..986a68d 100644 --- a/pldm-platform/src/proto.rs +++ b/pldm-platform/src/proto.rs @@ -13,6 +13,8 @@ use deku::{ DekuReader, DekuUpdate, DekuWrite, DekuWriter, }; +use chrono::{DateTime, Datelike, FixedOffset, TimeDelta, TimeZone, Timelike}; + use pldm::control::xfer_flag; use pldm::{proto_error, PldmError, PldmResult}; @@ -531,7 +533,77 @@ pub enum PDRRepositoryState { } // TODO -pub type Timestamp104 = [u8; 13]; +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Clone, PartialEq, Eq, Default)] +pub struct Timestamp104(pub [u8; 13]); + +impl core::fmt::Debug for Timestamp104 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if let Ok(dt) = DateTime::::try_from(self) { + write!(f, "Timestamp104({dt:?})") + } else { + write!(f, "Timestamp104(invalid {:?})", self.0) + } + } +} + +impl TryFrom<&Timestamp104> for DateTime { + type Error = (); + + fn try_from(t: &Timestamp104) -> Result { + let t = &t.0; + let tz = i16::from_le_bytes(t[..=1].try_into().unwrap()); + let tz = FixedOffset::east_opt(tz as i32 * 60).ok_or_else(|| { + trace!("Bad timezone {tz}"); + })?; + let year = u16::from_le_bytes(t[10..=11].try_into().unwrap()); + let dt = tz + .with_ymd_and_hms( + year as i32, + t[9] as u32, + t[8] as u32, + t[7] as u32, + t[6] as u32, + t[5] as u32, + ) + .earliest() + .ok_or_else(|| { + trace!("Bad timestamp"); + })?; + // read a u32 and mask to 24 bit + let micros = + u32::from_le_bytes(t[2..=5].try_into().unwrap()) & 0xffffff; + let dt = dt + TimeDelta::microseconds(micros as i64); + Ok(dt) + } +} + +impl TryFrom<&DateTime> for Timestamp104 { + type Error = (); + + fn try_from(dt: &DateTime) -> Result { + let mut t = [0; 13]; + let off = dt.offset().local_minus_utc() as u16; + t[0..=1].copy_from_slice(&off.to_le_bytes()); + + let date = dt.date_naive(); + let time = dt.time(); + // can be > 1e9 for leap seconds, discard that. + let micros = (time.nanosecond() % 1_000_000_000) / 1000; + t[2..=4].copy_from_slice(µs.to_le_bytes()[..3]); + t[5] = time.second() as u8; + t[6] = time.minute() as u8; + t[7] = time.hour() as u8; + t[8] = date.day() as u8; + t[9] = date.month() as u8; + let year: u16 = date.year().try_into().map_err(|_| { + trace!("Year out of range"); + })?; + t[10..=11].copy_from_slice(&year.to_le_bytes()); + + Ok(Timestamp104(t)) + } +} #[deku_derive(DekuRead, DekuWrite)] #[derive(Debug, Clone, PartialEq, Eq)]