diff --git a/Cargo.lock b/Cargo.lock index 6db135b..a2ec2ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,6 +281,18 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blocking" version = "1.6.1" @@ -404,6 +416,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", + "strsim", "syn", ] @@ -450,6 +463,29 @@ dependencies = [ "thiserror 2.0.11", ] +[[package]] +name = "deku" +version = "0.19.1" +source = "git+https://github.com/CodeConstruct/deku.git?tag=cc%2Fdeku-v0.19.1%2Fno-alloc-3#d68915c71e1b3ac76726328803eeffc773fb9871" +dependencies = [ + "bitvec", + "deku_derive", + "log", + "no_std_io2", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.19.1" +source = "git+https://github.com/CodeConstruct/deku.git?tag=cc%2Fdeku-v0.19.1%2Fno-alloc-3#d68915c71e1b3ac76726328803eeffc773fb9871" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -638,6 +674,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -834,9 +876,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.21" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mctp" @@ -917,6 +959,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "no_std_io2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2b9acd47481ab557a89a5665891be79e43cce8a29ad77aa9419d7be5a7c06a" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -1002,9 +1053,32 @@ dependencies = [ name = "pldm" version = "0.2.0" dependencies = [ + "crc", + "deku", + "heapless", + "log", + "mctp", + "num-derive", + "num-traits", +] + +[[package]] +name = "pldm-file" +version = "0.1.0" +dependencies = [ + "anyhow", + "crc", + "deku", + "enumset", + "env_logger", + "log", "mctp", + "mctp-linux", "num-derive", "num-traits", + "pldm", + "pldm-platform", + "smol", ] [[package]] @@ -1040,6 +1114,34 @@ dependencies = [ "pldm-fw", ] +[[package]] +name = "pldm-platform" +version = "0.1.0" +dependencies = [ + "deku", + "heapless", + "log", + "mctp", + "num-derive", + "num-traits", + "pldm", +] + +[[package]] +name = "pldm-platform-util" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "deku", + "env_logger", + "log", + "mctp", + "mctp-linux", + "pldm-platform", + "smol", +] + [[package]] name = "polling" version = "3.7.2" @@ -1136,6 +1238,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1217,6 +1325,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "rusty-fork" version = "0.3.0" @@ -1310,6 +1424,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "2.0.98" @@ -1321,6 +1441,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.11.0" @@ -1612,6 +1738,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index b367fa8..3981216 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "mctp-usb-embassy", "pldm-fw-cli", "standalone" ] +members = [ "mctp-usb-embassy", "pldm-file", "pldm-fw-cli", "pldm-platform", "pldm-platform-util", "standalone" ] resolver = "2" [workspace.package] @@ -13,6 +13,7 @@ argh = "0.1.12" chrono = { version = "0.4", default-features = false } crc = "3.0" defmt = "0.3" +deku = { git = "https://github.com/CodeConstruct/deku.git", tag = "cc/deku-v0.19.1/no-alloc-3", default-features = false } embedded-io-adapters = { version = "0.6", features = ["std", "futures-03"] } embedded-io-async = "0.6" enumset = "1.1" @@ -25,6 +26,7 @@ mctp = { version = "0.2", path = "mctp", default-features = false } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false } pldm-fw = { version = "0.2", path = "pldm-fw", default-features = false } +pldm-platform = { path = "pldm-platform", default-features = false } pldm = { version = "0.2", path = "pldm", default-features = false } proptest = "1.0.0" simplelog = "0.12" diff --git a/ci/runtests.sh b/ci/runtests.sh index 9513526..5aa9934 100755 --- a/ci/runtests.sh +++ b/ci/runtests.sh @@ -22,11 +22,18 @@ cargo build --release cargo test # stable, no_std -NOSTD_CRATES="mctp pldm pldm-fw" +NOSTD_CRATES="mctp pldm pldm-fw pldm-platform pldm-file" for c in $NOSTD_CRATES; do ( cd $c - cargo build --target thumbv7em-none-eabihf --no-default-features --release + cargo build --target thumbv7em-none-eabihf --no-default-features + ) +done +ALLOC_CRATES="pldm pldm-platform pldm-file" +for c in $ALLOC_CRATES; do + ( + cd $c + cargo build --target thumbv7em-none-eabihf --no-default-features --features alloc ) done diff --git a/mctp-estack/src/control.rs b/mctp-estack/src/control.rs index a5805c5..c40024f 100644 --- a/mctp-estack/src/control.rs +++ b/mctp-estack/src/control.rs @@ -515,7 +515,7 @@ impl<'a> MctpControl<'a> { let res = self.router.set_eid(eid).await; let present_eid = self.router.get_eid().await; - if res.is_ok() && old != present_eid { + if res.is_ok() { event = Some(ControlEvent::SetEndpointId { old, new: present_eid, @@ -550,9 +550,12 @@ impl<'a> MctpControl<'a> { /// An MCTP control handler event pub enum ControlEvent { - /// Set Endpoint ID changed EID + /// Set Endpoint ID received. + /// + /// Note that the EID may be unchanged. SetEndpointId { - /// Previous EID + /// Previous EID. + /// old: Eid, /// New EID new: Eid, diff --git a/mctp-estack/src/lib.rs b/mctp-estack/src/lib.rs index cbf1c8a..9d8f345 100644 --- a/mctp-estack/src/lib.rs +++ b/mctp-estack/src/lib.rs @@ -25,7 +25,7 @@ //! `mctp-estack` uses fixed sizes to be suitable on no-alloc platforms. //! These can be configured at build time, see [`config`] -#![cfg_attr(not(any(feature = "std", test)), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] #![forbid(unsafe_code)] #![allow(clippy::int_plus_one)] #![allow(clippy::too_many_arguments)] @@ -33,6 +33,10 @@ // those reworked when using the log crate either. #![allow(clippy::uninlined_format_args)] +#[cfg(test)] +#[macro_use] +extern crate std; + /// Re-exported so that callers can use the same `heapless` version. /// /// `heapless::Vec` is currently an argument of `send_fill()` in transports. @@ -581,16 +585,21 @@ impl Stack { trace!("set flow {}", peer); if let Some(tv) = tag { + if flow_expires { + trace!("Can't specify a tag with tag_expires"); + return Err(Error::BadArgument); + } + + // Compare with any existing flow if let Some(f) = self.flows.get_mut(&(peer, tv)) { - if f.expiry_stamp.is_some() { - // An Owned tag given to start_send() must have been initially created - // tag_expires=false. - trace!("Can't specify an owned tag that didn't have tag_expires=false"); + if flow_expires != f.expiry_stamp.is_some() { + trace!("varying slow_expires for flow"); return Err(Error::BadArgument); } if f.cookie != cookie { - trace!("varying app for flow"); + trace!("varying cookie for flow"); + return Err(Error::BadArgument); } return Ok(tv); } diff --git a/mctp-estack/src/router.rs b/mctp-estack/src/router.rs index e60ad8f..9438ab2 100644 --- a/mctp-estack/src/router.rs +++ b/mctp-estack/src/router.rs @@ -316,7 +316,18 @@ impl Default for WakerPool { impl WakerPool { fn wake(&self, cookie: AppCookie) { - self.inner.lock(|i| i.borrow_mut().pool[&cookie].wake()) + self.inner.lock(|i| { + let mut i = i.borrow_mut(); + if let Some(w) = i.pool.get_mut(&cookie) { + w.wake() + } else { + // This can currently happen if a ReqChannel is dropped but the core stack + // subsequently receives a response message corresponding to that cookie. + // TODO fix expiring from the stack (?) and make this a debug_assertion. + // We can't expire in the ReqChannel drop handler since it needs async + // for locking. + } + }) } fn wake_all(&self) { @@ -697,8 +708,12 @@ impl<'r> Router<'r> { /// A request channel. pub struct RouterAsyncReqChannel<'r> { + /// Destination EID eid: Eid, - sent_tag: Option, + /// Tag from the last `send()`. + /// + /// Cleared upon receiving a response, except in the case of !tag_expires. + last_tag: Option, router: &'r Router<'r>, tag_expires: bool, cookie: Option, @@ -708,7 +723,7 @@ impl<'r> RouterAsyncReqChannel<'r> { fn new(eid: Eid, router: &'r Router<'r>) -> Self { RouterAsyncReqChannel { eid, - sent_tag: None, + last_tag: None, tag_expires: true, router, cookie: None, @@ -718,8 +733,12 @@ impl<'r> RouterAsyncReqChannel<'r> { /// Set the tag to not expire. That allows multiple calls to `send()`. /// /// `async_drop` must be called prior to drop. + /// + /// Should be called prior to any `send()` and may only be called once + /// for a `RouterAsyncReqChannel`. + /// This can also be called after the `recv()` has completed. pub fn tag_noexpire(&mut self) -> Result<()> { - if self.sent_tag.is_some() { + if self.last_tag.is_some() { return Err(Error::BadArgument); } self.tag_expires = false; @@ -734,7 +753,7 @@ impl<'r> RouterAsyncReqChannel<'r> { /// pub async fn async_drop(mut self) { if !self.tag_expires { - if let Some(tag) = self.sent_tag.take() { + if let Some(tag) = self.last_tag.take() { self.router.app_release_tag(self.eid, tag).await; } } @@ -743,7 +762,7 @@ impl<'r> RouterAsyncReqChannel<'r> { impl Drop for RouterAsyncReqChannel<'_> { fn drop(&mut self) { - if !self.tag_expires && self.sent_tag.is_some() { + if !self.tag_expires && self.last_tag.is_some() { warn!("Didn't call async_drop()"); } if let Some(c) = self.cookie { @@ -762,40 +781,58 @@ impl mctp::AsyncReqChannel for RouterAsyncReqChannel<'_> { /// Note that it will return failure immediately if the MCTP stack has no available tags, /// that behaviour may need changing in future. /// - /// Subsequent calls will fail unless tag_noexpire() was performed. + /// A `RouterAsyncReqChannel` can only receive responses for its + /// most recent `send()`, unless unless `tag_noexpire()` was set. async fn send_vectored( &mut self, typ: MsgType, integrity_check: MsgIC, bufs: &[&[u8]], ) -> Result<()> { - // For the first call, we pass a None tag, get an Owned one allocated. - // Subsequent calls will fail unless tag_noexpire() was performed. + let send_tag = if self.tag_expires { + // Expiring (normal) case. Pass a None tag, and use a new cookie. + // An allocated tag is returned from app_send_message(). + if let Some(c) = self.cookie.take() { + self.router.recv_wakers.remove(c); + } + None + } else { + // Non-expiring case, allocate a tag and cookie the + // first time then reuse it. + self.last_tag + }; + + if self.cookie.is_none() { + self.cookie = Some(self.router.recv_wakers.alloc()?); + } + let tag = self .router .app_send_message( self.eid, typ, - self.sent_tag, + send_tag, self.tag_expires, integrity_check, bufs, - None, + self.cookie, ) .await?; debug_assert!(matches!(tag, Tag::Owned(_))); - self.sent_tag = Some(tag); - if self.cookie.is_none() { - self.cookie = Some(self.router.recv_wakers.alloc()?); - } + self.last_tag = Some(tag); Ok(()) } + /// Receive a message. + /// + /// In the normal case, this will only receive responses to the + /// most recent `send()`. Responses to earlier `send()`s will be dropped. + /// When `tag_noexpire()` is set, this can receive multiple responses. async fn recv<'f>( &mut self, buf: &'f mut [u8], ) -> Result<(MsgType, MsgIC, &'f mut [u8])> { - let Some(Tag::Owned(tv)) = self.sent_tag else { + let Some(Tag::Owned(tv)) = self.last_tag else { debug!("recv without send"); return Err(Error::BadArgument); }; @@ -807,6 +844,17 @@ impl mctp::AsyncReqChannel for RouterAsyncReqChannel<'_> { self.router.app_recv(cookie, buf).await?; debug_assert_eq!(tag, recv_tag); debug_assert_eq!(eid, self.eid); + + if self.tag_expires { + self.last_tag = None; + + // Remove the cookie. It would get cleared up anyway + // by drop() or a later send(), but this means it won't + // be taking up a slot in the interim. + if let Some(c) = self.cookie.take() { + self.router.recv_wakers.remove(c); + } + } Ok((typ, ic, buf)) } diff --git a/mctp-linux/src/lib.rs b/mctp-linux/src/lib.rs index 3a64275..f490e2c 100644 --- a/mctp-linux/src/lib.rs +++ b/mctp-linux/src/lib.rs @@ -163,6 +163,7 @@ impl MctpSocket { // Inner recvfrom, returning an io::Error on failure. This can be // used with async wrappers. + // This uses MSG_TRUNC so the returned length may be larger than buf.len() fn io_recvfrom( &self, buf: &mut [u8], @@ -174,7 +175,14 @@ impl MctpSocket { let fd = self.as_raw_fd(); let rc = unsafe { - libc::recvfrom(fd, buf_ptr, buf_len, 0, addr_ptr, &mut addr_len) + libc::recvfrom( + fd, + buf_ptr, + buf_len, + libc::MSG_TRUNC, + addr_ptr, + &mut addr_len, + ) }; if rc < 0 { @@ -190,7 +198,11 @@ impl MctpSocket { /// Essentially a wrapper around [libc::recvfrom], using MCTP-specific /// addressing. pub fn recvfrom(&self, buf: &mut [u8]) -> Result<(usize, MctpSockAddr)> { - self.io_recvfrom(buf).map_err(mctp::Error::Io) + let (len, addr) = self.io_recvfrom(buf).map_err(mctp::Error::Io)?; + if len > buf.len() { + return Err(mctp::Error::NoSpace); + } + Ok((len, addr)) } fn io_sendto( @@ -346,10 +358,15 @@ impl MctpSocketAsync { &self, buf: &mut [u8], ) -> Result<(usize, MctpSockAddr)> { - self.0 + let (len, addr) = self + .0 .read_with(|io| io.io_recvfrom(buf)) .await - .map_err(mctp::Error::Io) + .map_err(mctp::Error::Io)?; + if len > buf.len() { + return Err(mctp::Error::NoSpace); + } + Ok((len, addr)) } /// Send a message to a given address diff --git a/mctp/src/lib.rs b/mctp/src/lib.rs index 027f239..0b49061 100644 --- a/mctp/src/lib.rs +++ b/mctp/src/lib.rs @@ -5,8 +5,7 @@ * Copyright (c) 2024 Code Construct */ -// Tests may use std -#![cfg_attr(not(any(feature = "std", test)), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] #![forbid(unsafe_code)] #![warn(missing_docs)] diff --git a/pldm-file/Cargo.toml b/pldm-file/Cargo.toml new file mode 100644 index 0000000..afc9ba5 --- /dev/null +++ b/pldm-file/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "pldm-file" +description = "Platform Level Data Model (PLDM) for File Transfer" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +categories = ["network-programming", "embedded", "hardware-support"] + +[dependencies] +crc = { workspace = true } +deku = { workspace = true } +enumset = { workspace = true } +log = { workspace = true } +mctp = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +pldm = { workspace = true } + +[dev-dependencies] +anyhow = "1.0" +mctp-linux = { workspace = true } +pldm-platform = { workspace = true, features = ["alloc"] } +smol = "2.0" +env_logger = "0.11.3" + +[features] +default = ["std"] +alloc = ["pldm/alloc", "deku/alloc"] +std = ["alloc", "pldm/std", "mctp/std"] + +[[example]] +name = "pldm-file-host" +path = "examples/host.rs" + +[[example]] +name = "pldm-file-client" +path = "examples/client.rs" diff --git a/pldm-file/examples/client.rs b/pldm-file/examples/client.rs new file mode 100644 index 0000000..d34e7a8 --- /dev/null +++ b/pldm-file/examples/client.rs @@ -0,0 +1,64 @@ +use anyhow::{Context, Result}; +use mctp::Eid; +use mctp_linux::MctpLinuxAsyncReq; +use pldm::{control::requester::negotiate_transfer_parameters, PldmError}; +use pldm_file::{ + client::{df_close, df_open, df_properties, df_read_with}, + proto::{DfCloseAttributes, DfOpenAttributes, DfProperty, FileIdentifier}, +}; + +const EID: Eid = Eid(8); + +fn main() -> Result<()> { + let mut req = MctpLinuxAsyncReq::new(EID, None)?; + + smol::block_on(async { + let mcm_prop = + df_properties(&mut req, DfProperty::MaxConcurrentMedium).await; + let fds_prop = + df_properties(&mut req, DfProperty::MaxFileDescriptors).await; + println!("Max Concurrent Medium: {mcm_prop:?}, Max FDs: {fds_prop:?}"); + + let req_types = [pldm_file::PLDM_TYPE_FILE_TRANSFER]; + let mut buf = [0u8]; + + let (size, neg_types) = + negotiate_transfer_parameters(&mut req, &req_types, &mut buf, 512) + .await?; + + println!("Negotiated multipart size {size} for types {neg_types:?}"); + + let id = FileIdentifier(0); + let attrs = DfOpenAttributes::empty(); + let fd = df_open(&mut req, id, attrs) + .await + .context("DfOpen failed")?; + + println!("Open: {fd:?}"); + + let mut buf = Vec::new(); + let req_len = 4096; + + println!("Reading..."); + let res = df_read_with(&mut req, fd, 0, req_len, |part| { + println!(" {} bytes", part.len()); + if buf.len() + part.len() > req_len { + println!(" data overflow!"); + Err(PldmError::NoSpace) + } else { + buf.extend_from_slice(part); + Ok(()) + } + }) + .await; + + println!("Read: {res:?}"); + + let attrs = DfCloseAttributes::empty(); + let res = df_close(&mut req, fd, attrs).await; + + println!("Close: {res:?}"); + + Ok(()) + }) +} diff --git a/pldm-file/examples/host.rs b/pldm-file/examples/host.rs new file mode 100644 index 0000000..5b185c5 --- /dev/null +++ b/pldm-file/examples/host.rs @@ -0,0 +1,208 @@ +use anyhow::{bail, Context, Result}; +use deku::DekuContainerRead; +#[allow(unused_imports)] +use log::{debug, info, trace, warn}; + +use mctp::{AsyncListener, AsyncRespChannel}; +use mctp_linux::MctpLinuxAsyncListener; +use pldm::{PldmRequest, PldmResponse}; +use std::cell::RefCell; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::os::unix::fs::MetadataExt; + +struct Host { + file: RefCell, +} + +const FILENAME: &str = "pldm-file-host.bin"; +const PDR_HANDLE: u32 = 1; + +impl Host { + fn new() -> Result { + Ok(Self { + file: RefCell::new(File::open(FILENAME).with_context(|| { + format!("cannot open input file {FILENAME}") + })?), + }) + } +} + +impl pldm_file::host::Host for Host { + fn read(&self, buf: &mut [u8], offset: usize) -> std::io::Result { + let mut file = self.file.borrow_mut(); + file.seek(SeekFrom::Start(offset as u64))?; + file.read(buf) + } +} + +type FileResponder = pldm_file::host::Responder<1>; + +fn main() -> Result<()> { + env_logger::init(); + + let mut listener = MctpLinuxAsyncListener::new(mctp::MCTP_TYPE_PLDM, None)?; + let mut pldm_ctrl = pldm::control::responder::Responder::<2>::new(); + let mut pldm_file = FileResponder::new(); + let mut host = Host::new().context("unable to create file host")?; + + FileResponder::register(&mut pldm_ctrl)?; + + let mut buf = [0u8; 4096]; + + smol::block_on(async { + loop { + let (typ, _ic, buf, chan) = listener.recv(&mut buf).await?; + + if typ != mctp::MCTP_TYPE_PLDM { + warn!("unexpected MCTP type {typ}"); + continue; + } + + let req = PldmRequest::from_buf_borrowed(buf) + .context("invalid PLDM message")?; + + const MP_RECV: u8 = pldm::control::Cmd::MultipartReceive as u8; + + match (req.typ, req.cmd) { + // we pass all of the multipart receive commands over to + // the pldm-file handler + (pldm::control::PLDM_TYPE_CONTROL, MP_RECV) => pldm_file + .multipart_request_in(chan, &req, &pldm_ctrl, &mut host) + .await + .context("PLDM file multipart handler failed")?, + (pldm::control::PLDM_TYPE_CONTROL, _) => pldm_ctrl + .handle_async(&req, chan) + .await + .context("PLDM control handler failed")?, + (pldm_file::PLDM_TYPE_FILE_TRANSFER, _) => pldm_file + .request_in(chan, &req, &mut host) + .await + .context("PLDM file handler failed")?, + (pldm_platform::PLDM_TYPE_PLATFORM, _) => { + handle_platform(chan, &req, &host) + .await + .context("PLDM platform handler failed")? + } + (t, _) => { + Err(anyhow::anyhow!("unexpected PLDM type {t}"))?; + } + } + } + }) +} + +async fn handle_platform( + mut comm: R, + req: &PldmRequest<'_>, + host: &Host, +) -> Result<()> { + use pldm_platform::deku::DekuContainerWrite; + use pldm_platform::proto::*; + + assert_eq!(req.typ, pldm_platform::PLDM_TYPE_PLATFORM); + + let mut resp = req.response(); + + 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], + record_count: 1, + // TODO. "An implementation is allowed to round this number up to the nearest kilobyte (1024 bytes)." + repository_size: 1024, + // TODO + largest_record_size: 128, + // No Timeout + data_transfer_handle_timeout: 0x00, + }; + resp.set_data(pdrinfo.to_bytes().context("Encoding failed")?); + Ok(pldm::CCode::SUCCESS as u8) + } + Cmd::GetPDR => handle_get_pdr(req, &mut resp, host), + other => { + warn!("Unsupported PLDM platform command {other:?}"); + Ok(pldm::CCode::ERROR_UNSUPPORTED_PLDM_CMD as u8) + } + }?; + + pldm::pldm_tx_resp_async(&mut comm, &resp) + .await + .context("Sending response failed") +} + +fn handle_get_pdr( + req: &PldmRequest<'_>, + resp: &mut PldmResponse, + host: &Host, +) -> Result { + use pldm_platform::deku::DekuContainerWrite; + use pldm_platform::proto::*; + + let ((rest, _), pdr_req) = GetPDRReq::from_bytes((&req.data, 0))?; + if !rest.is_empty() { + bail!("Extra Get PDR Request bytes"); + } + + if pdr_req.record_handle != PDR_HANDLE { + warn!("Wrong PDR handle"); + return Ok(plat_codes::INVALID_RECORD_HANDLE); + } + if pdr_req.data_transfer_handle != 0 { + warn!("Don't support multipart PDR"); + return Ok(plat_codes::INVALID_DATA_TRANSFER_HANDLE); + } + if pdr_req.transfer_operation_flag != TransferOperationFlag::FirstPart { + warn!("Don't support multipart PDR"); + return Ok(plat_codes::INVALID_TRANSFER_OPERATION_FLAG); + } + if pdr_req.record_change_number != 0 { + warn!("Don't support multipart PDR"); + return Ok(plat_codes::INVALID_RECORD_CHANGE_NUMBER); + } + + let file_max_size = host + .file + .borrow() + .metadata() + .context("Metadata failed")? + .size() + .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_req.record_handle, + PdrRecord::FileDescriptor(FileDescriptorPdr { + terminus_handle: 0, + file_identifier: 0, + // Management Controller Firmware + // TODO + entity_type: entity_type::LOGICAL | 36, + entity_instance: 0, + container_id: 0, + superior_directory: 0, + file_classification: FileClassification::OtherFile, + oem_file_classification: 0, + capabilities: file_capabilities::EX_READ_OPEN, + file_version: 0xFFFFFFFF, + file_max_size, + // TODO + file_max_desc_count: 1, + file_name_length: file_name.len() as u8, + file_name: file_name.into(), + oem_file_name: Default::default(), + }), + )?; + let enc = pdr_resp.to_bytes().context("Encoding failed")?; + trace!("enc {enc:02x?}"); + resp.set_data(enc); + + Ok(pldm::CCode::SUCCESS as u8) +} diff --git a/pldm-file/src/client.rs b/pldm-file/src/client.rs new file mode 100644 index 0000000..884f823 --- /dev/null +++ b/pldm-file/src/client.rs @@ -0,0 +1,228 @@ +use deku::{DekuContainerRead, DekuContainerWrite}; +use log::trace; +use pldm::control::{MultipartReceiveReq, MultipartReceiveResp}; +use pldm::{ + pldm_xfer_buf_async, proto_error, PldmError, PldmRequest, PldmResult, + Result, +}; + +use crate::proto::*; +use crate::PLDM_TYPE_FILE_TRANSFER; + +pub async fn df_properties( + comm: &mut impl mctp::AsyncReqChannel, + property: DfProperty, +) -> Result { + let req = DfPropertiesReq { + property: property as u32, + }; + + let mut buf = [0; 10]; + let l = req.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_FILE_TRANSFER, + Cmd::DfProperties as u8, + buf, + ); + + let mut rx = [0; 30]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + let ((rest, _), ret) = DfPropertiesResp::from_bytes((&resp.data, 0)) + .map_err(|e| { + trace!("DfProperties parse error {e}"); + proto_error!("Bad DfProperties response") + })?; + + if !rest.is_empty() { + return Err(proto_error!("Extra response")); + } + + Ok(ret.value) +} + +pub async fn df_open( + comm: &mut impl mctp::AsyncReqChannel, + id: FileIdentifier, + attributes: DfOpenAttributes, +) -> Result { + let req = DfOpenReq { + file_identifier: id.0, + attributes: attributes.as_u16(), + }; + + let mut buf = [0; 10]; + let l = req.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_FILE_TRANSFER, + Cmd::DfOpen as u8, + buf, + ); + + let mut rx = [0; 10]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + let ((rest, _), ret) = + DfOpenResp::from_bytes((&resp.data, 0)).map_err(|e| { + trace!("DfOpen parse error {e}"); + proto_error!("Bad DfOpen response") + })?; + + if !rest.is_empty() { + return Err(proto_error!("Extra response")); + } + + Ok(FileDescriptor(ret.file_descriptor)) +} + +pub async fn df_close( + comm: &mut impl mctp::AsyncReqChannel, + fd: FileDescriptor, + attributes: DfCloseAttributes, +) -> Result<()> { + let req = DfCloseReq { + file_descriptor: fd.0, + attributes: attributes.as_u16(), + }; + + let mut buf = [0; 4]; + let l = req.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_FILE_TRANSFER, + Cmd::DfClose as u8, + buf, + ); + + let mut rx = [0; 10]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + if resp.cc != 0 { + return Err(proto_error!("DfClose failed")); + } + + Ok(()) +} + +const PART_SIZE: usize = 1024; + +/// Read a file from the host. +/// +/// The total file size read is returned. +pub async fn df_read( + comm: &mut impl mctp::AsyncReqChannel, + file: FileDescriptor, + offset: usize, + buf: &mut [u8], +) -> Result { + let len = buf.len(); + let mut v = pldm::util::SliceWriter::new(buf); + + df_read_with(comm, file, offset, len, |b| { + let r = v.extend(b); + debug_assert!(r.is_some(), "provided data should be <= len"); + Ok(()) + }) + .await +} + +/// Read a file into a provided closure. +/// +/// The `out` closure will be called repeatedly with sequential buffer chunks +/// sent from the host. Any error returned by `out` will be returned from `df_read_with`. +pub async fn df_read_with( + comm: &mut impl mctp::AsyncReqChannel, + file: FileDescriptor, + offset: usize, + len: usize, + mut out: F, +) -> Result +where + F: FnMut(&[u8]) -> PldmResult<()>, +{ + let req_offset = u32::try_from(offset).map_err(|_| { + trace!("invalid offset"); + PldmError::InvalidArgument + })?; + let req_length = u32::try_from(len).map_err(|_| { + trace!("invalid len"); + PldmError::InvalidArgument + })?; + req_length.checked_add(req_offset).ok_or_else(|| { + trace!("invalid len"); + PldmError::InvalidArgument + })?; + + let mut part_offset = 0usize; + let mut req = MultipartReceiveReq { + pldm_type: PLDM_TYPE_FILE_TRANSFER, + xfer_op: pldm::control::xfer_op::FIRST_PART, + xfer_context: file.0 as u32, + xfer_handle: 0, + req_offset, + req_length, + }; + loop { + let mut tx_buf = [0; 18]; + let l = req.to_slice(&mut tx_buf).map_err(|_| PldmError::NoSpace)?; + let tx_buf = &tx_buf[..l]; + + let pldm_req = PldmRequest::new_borrowed( + pldm::control::PLDM_TYPE_CONTROL, + pldm::control::Cmd::MultipartReceive as u8, + tx_buf, + ); + + // todo: negotiated length + let mut rx_buf = [0u8; 14 + PART_SIZE + 4]; + let resp = pldm_xfer_buf_async(comm, pldm_req, &mut rx_buf).await?; + + let ((rest, _), read_resp) = + MultipartReceiveResp::from_bytes((&resp.data, 0)).map_err(|e| { + trace!("DfRead parse error {e}"); + proto_error!("Bad DfOpen response") + })?; + + let resp_data_len = read_resp.len as usize; + + if rest.len() != resp_data_len + 4 { + return Err(proto_error!("invalid resonse data length")); + } + + let (resp_data, resp_cs) = rest.split_at(resp_data_len); + + let crc32 = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + let calc_cs = crc32.checksum(resp_data); + // unwrap: we have asserted the lengths above + let cs = u32::from_le_bytes(resp_cs.try_into().unwrap()); + + if calc_cs != cs { + return Err(proto_error!("data checksum mismatch")); + } + + // Ensure the host doesn't send more data than requested + let total_len = part_offset + .checked_add(resp_data_len) + .filter(|l| *l <= (req_length as usize)) + .ok_or(proto_error!("excess data received"))?; + + // provide data to the callback + out(resp_data)?; + + if read_resp.xfer_flag & pldm::control::xfer_flag::END != 0 { + break Ok(total_len); + } + + part_offset = total_len; + req.xfer_op = pldm::control::xfer_op::NEXT_PART; + req.xfer_context = 0; + req.xfer_handle = read_resp.next_handle; + req.req_offset = 0; + req.req_length = 0; + } +} diff --git a/pldm-file/src/host.rs b/pldm-file/src/host.rs new file mode 100644 index 0000000..1243489 --- /dev/null +++ b/pldm-file/src/host.rs @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use deku::{DekuContainerRead, DekuContainerWrite, DekuError}; +use log::{debug, trace}; +use mctp::{AsyncRespChannel, Eid}; +use num_traits::FromPrimitive; +use pldm::{ + self, pldm_tx_resp_async, proto_error, CCode, PldmError, PldmRequest, + PldmResponse, +}; + +use crate::proto::file_ccode; +use crate::proto::*; +use crate::PLDM_TYPE_FILE_TRANSFER; + +const FILE_ID: FileIdentifier = FileIdentifier(0); + +const MAX_PART_SIZE: u16 = 1024; + +pub trait Host { + /// Returns number of bytes read + // just a single-file implementation + fn read(&self, buf: &mut [u8], offset: usize) -> std::io::Result; +} + +// Created at the first stage (XFER_FIRST_PART) of a MultpartReceive, +// where we have the offset and size. +struct FileTransferContext { + buf: Vec, + offset: usize, +} + +// Created on DfOpen +struct FileContext { + xfer_ctx: Option, +} + +pub struct Responder { + files: [Option; N], +} + +#[derive(Debug)] +struct PldmFileError(u8); + +impl From for PldmFileError { + fn from(cc: CCode) -> Self { + Self(cc as u8) + } +} + +impl From for PldmFileError { + fn from(cc: u8) -> Self { + Self(cc) + } +} + +impl From for PldmFileError { + fn from(_: DekuError) -> Self { + CCode::ERROR_INVALID_DATA.into() + } +} + +impl From for PldmFileError { + fn from(_: PldmError) -> Self { + CCode::ERROR.into() + } +} + +impl From for u8 { + fn from(err: PldmFileError) -> Self { + err.0 + } +} +impl std::fmt::Display for PldmFileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CC: {}", self.0) + } +} + +type Result = std::result::Result; + +impl Responder { + pub fn new() -> Self { + Self { + files: [const { None }; N], + } + } + + pub fn register( + pldm: &mut pldm::control::responder::Responder, + ) -> pldm::Result<()> { + pldm.register_type( + PLDM_TYPE_FILE_TRANSFER, + 0xf1f0f000, + Some(MAX_PART_SIZE), + &[ + Cmd::DfProperties as u8, + Cmd::DfOpen as u8, + Cmd::DfClose as u8, + Cmd::DfRead as u8, + ], + ) + } + + pub async fn request_in( + &mut self, + mut comm: R, + req: &PldmRequest<'_>, + host: &mut impl Host, + ) -> pldm::Result<()> { + if req.typ != PLDM_TYPE_FILE_TRANSFER { + trace!("pldm-fw non-pldm-fw request {req:?}"); + return Err(proto_error!("Unexpected pldm-fw request")); + } + + let Some(cmd) = Cmd::from_u8(req.cmd) else { + let _ = self + .reply_error( + req, + &mut comm, + CCode::ERROR_UNSUPPORTED_PLDM_CMD as u8, + ) + .await; + return Ok(()); + }; + + let r = match cmd { + Cmd::DfProperties => self.cmd_dfproperties(req, host), + Cmd::DfOpen => self.cmd_dfopen(req, host), + Cmd::DfClose => self.cmd_dfclose(req, host), + _ => { + trace!("unhandled command {cmd:?}"); + Err(CCode::ERROR_UNSUPPORTED_PLDM_CMD.into()) + } + }; + + let res = match r { + Ok(resp) => pldm_tx_resp_async(&mut comm, &resp) + .await + .map_err(|e| ("command", e)), + Err(e) => { + debug!("Error handling {cmd:?}: {e:?}"); + self.reply_error(req, &mut comm, e.into()) + .await + .map_err(|e| ("failure", e)) + } + }; + + if let Err((typ, e)) = res { + debug!("error sending {typ} response. {e:?}"); + return Err(e); + } + Ok(()) + } + + // We may want to move this to the general request_in() path? + pub async fn multipart_request_in( + &mut self, + mut comm: R, + req: &PldmRequest<'_>, + pldm_ctrl: &pldm::control::responder::Responder, + host: &mut impl Host, + ) -> pldm::Result<()> { + let Ok(cmd) = pldm::control::Cmd::try_from(req.cmd) else { + return self + .reply_error( + req, + &mut comm, + CCode::ERROR_UNSUPPORTED_PLDM_CMD as u8, + ) + .await; + }; + + let res = match cmd { + pldm::control::Cmd::MultipartReceive => self.cmd_multipart_receive( + req, + comm.remote_eid(), + pldm_ctrl, + host, + ), + _ => { + trace!("unhandled multipart request"); + Err(CCode::ERROR_UNSUPPORTED_PLDM_CMD.into()) + } + }; + + match res { + Ok(resp) => pldm_tx_resp_async(&mut comm, &resp).await, + Err(e) => { + debug!("Error handling multipart command: {e:?}"); + self.reply_error(req, &mut comm, e.into()).await + } + } + } + + async fn reply_error( + &self, + req: &PldmRequest<'_>, + comm: &mut R, + cc: u8, + ) -> std::result::Result<(), PldmError> { + let mut resp = req.response(); + resp.cc = cc; + pldm_tx_resp_async(comm, &resp).await + } + + fn cmd_dfproperties<'a>( + &mut self, + req: &'a PldmRequest<'a>, + _host: &mut impl Host, + ) -> Result> { + let (rest, dfp) = DfPropertiesReq::from_bytes((&req.data, 0))?; + + if !rest.0.is_empty() { + Err(CCode::ERROR_INVALID_DATA)?; + } + + let prop = DfProperty::try_from(dfp.property) + .map_err(|_| file_ccode::INVALID_DF_ATTRIBUTE)?; + + // fixed properties at present... + let value = match prop { + DfProperty::MaxConcurrentMedium => 1u32, + DfProperty::MaxFileDescriptors => 1u32, + }; + + let prop_resp = DfPropertiesResp { value }; + let mut resp = req.response(); + resp.set_data(prop_resp.to_bytes()?); + + Ok(resp) + } + + fn cmd_dfopen<'a>( + &mut self, + req: &'a PldmRequest<'a>, + _host: &mut impl Host, + ) -> Result> { + let (rest, dfo) = DfOpenReq::from_bytes((&req.data, 0))?; + + if !rest.0.is_empty() { + Err(CCode::ERROR_INVALID_LENGTH)?; + } + + if dfo.file_identifier != FILE_ID.0 { + Err(file_ccode::INVALID_FILE_IDENTIFIER)?; + } + + // todo: attributes + + // single file implementation, requires no file-specific context + let file_ctx = FileContext { xfer_ctx: None }; + + let id = self + .files + .iter() + .enumerate() + .find_map(|(n, e)| if e.is_none() { Some(n) } else { None }) + .ok_or(file_ccode::MAX_NUM_FDS_EXCEEDED)?; + + self.files[id].replace(file_ctx); + + let dfo_resp = DfOpenResp { + file_descriptor: id as u16, + }; + + let mut resp = req.response(); + resp.set_data(dfo_resp.to_bytes()?); + + Ok(resp) + } + + fn cmd_dfclose<'a>( + &mut self, + req: &'a PldmRequest<'a>, + _host: &mut impl Host, + ) -> Result> { + let (rest, dfc) = DfCloseReq::from_bytes((&req.data, 0))?; + + if !rest.0.is_empty() { + Err(CCode::ERROR_INVALID_LENGTH)?; + } + + if dfc.attributes != 0 { + Err(file_ccode::ZEROLENGTH_NOT_ALLOWED)?; + } + + self.files + .get_mut(dfc.file_descriptor as usize) + .ok_or(file_ccode::INVALID_FILE_DESCRIPTOR)? // valid? + .take() + .ok_or(file_ccode::INVALID_FILE_DESCRIPTOR)?; // open? + + Ok(req.response()) + } + + fn cmd_multipart_receive<'a, const T: usize>( + &mut self, + req: &'a PldmRequest<'a>, + eid: Eid, + ctrl: &pldm::control::responder::Responder, + host: &mut impl Host, + ) -> Result> { + let (rest, cmd) = pldm::control::MultipartReceiveReq::from_bytes(( + req.data.as_ref(), + 0, + ))?; + + if !rest.0.is_empty() { + Err(CCode::ERROR_INVALID_LENGTH)?; + } + + // MultipartRead context is 32bits, but file descriptors are 16... + if cmd.xfer_context > u16::MAX as u32 { + Err(CCode::ERROR_INVALID_TRANSFER_CONTEXT)?; + } + + let part_size = ctrl + .negotiated_xfer_size(eid, PLDM_TYPE_FILE_TRANSFER) + .ok_or(pldm::control::control_ccode::NEGOTIATION_INCOMPLETE)? + as usize; + + let fd = FileDescriptor(cmd.xfer_context as u16); + + let file_ctx = self + .files + .get_mut(fd.0 as usize) + .ok_or(CCode::ERROR_INVALID_TRANSFER_CONTEXT)? // valid? + .as_mut() + .ok_or(CCode::ERROR_INVALID_TRANSFER_CONTEXT)?; // open? + + // handle termination + if cmd.xfer_op == pldm::control::xfer_op::ABORT + || cmd.xfer_op == pldm::control::xfer_op::COMPLETE + { + file_ctx.xfer_ctx.take(); + let dfread_resp = pldm::control::MultipartReceiveResp { + xfer_flag: pldm::control::xfer_flag::ACKNOWLEDGE_COMPLETION, + next_handle: 0, + len: 0, + }; + let mut resp = req.response(); + resp.set_data(dfread_resp.to_bytes()?); + return Ok(resp); + } + + // Set new transfer context + if cmd.xfer_op == pldm::control::xfer_op::FIRST_PART { + if let Some(ctx) = file_ctx.xfer_ctx.as_mut() { + ctx.offset = 0; + } else { + let new_ctx = Self::init_read(&cmd, host)?; + // a repeated FIRST_PART is valid, and restarts the transfer + file_ctx.xfer_ctx.replace(new_ctx); + }; + } + + let xfer_ctx = file_ctx.xfer_ctx.as_mut().ok_or(CCode::ERROR)?; + let full_len = xfer_ctx.buf.len(); + + let offset = match cmd.xfer_op { + pldm::control::xfer_op::FIRST_PART + | pldm::control::xfer_op::CURRENT_PART => xfer_ctx.offset, + pldm::control::xfer_op::NEXT_PART => xfer_ctx.offset + part_size, + _ => Err(CCode::ERROR_INVALID_DATA)?, + }; + + if offset >= xfer_ctx.buf.len() { + Err(CCode::ERROR_INVALID_DATA)?; + } + + let start = offset == 0; + let (len, end) = if offset + part_size >= full_len { + (full_len - offset, true) + } else { + (part_size, false) + }; + + let mut flags = 0; + if start { + flags |= pldm::control::xfer_flag::START + } + if end { + flags |= pldm::control::xfer_flag::END + } + // spec defines useless flags :( + if flags == 0 { + flags |= pldm::control::xfer_flag::MIDDLE + } + + let mut resp = req.response(); + let dfread_resp = pldm::control::MultipartReceiveResp { + xfer_flag: flags, + next_handle: 0, + len: len as u32, + }; + + let mut resp_data = Vec::new(); + resp_data.extend_from_slice(&dfread_resp.to_bytes()?); + + let data = &xfer_ctx.buf[offset..offset + len]; + let crc32 = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + let cs = crc32.checksum(data); + + resp_data.extend_from_slice(data); + resp_data.extend_from_slice(&cs.to_le_bytes()); + + xfer_ctx.offset = offset; + resp.set_data(resp_data); + + Ok(resp) + } + + fn init_read( + req: &pldm::control::MultipartReceiveReq, + host: &mut impl Host, + ) -> Result { + let offset = req.req_offset; + let len = req.req_length; + + let mut buf = vec![0; len as usize]; + let read_len = + host.read(&mut buf, offset as usize).or(Err(CCode::ERROR))?; + + buf.truncate(read_len); + + Ok(FileTransferContext { buf, offset: 0 }) + } +} + +impl Default for Responder { + fn default() -> Self { + Self::new() + } +} diff --git a/pldm-file/src/lib.rs b/pldm-file/src/lib.rs new file mode 100644 index 0000000..5ef85ce --- /dev/null +++ b/pldm-file/src/lib.rs @@ -0,0 +1,12 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +pub const PLDM_TYPE_FILE_TRANSFER: u8 = 7; + +pub mod client; +#[cfg(feature = "std")] +pub mod host; +pub mod proto; diff --git a/pldm-file/src/proto.rs b/pldm-file/src/proto.rs new file mode 100644 index 0000000..12fa2ad --- /dev/null +++ b/pldm-file/src/proto.rs @@ -0,0 +1,107 @@ +use num_derive::FromPrimitive; + +use deku::{DekuRead, DekuWrite}; +use enumset::{EnumSet, EnumSetType}; + +/// PLDM for File Transfer commands +#[allow(missing_docs)] +#[non_exhaustive] +#[derive(Debug, FromPrimitive)] +#[repr(u8)] +pub enum Cmd { + DfOpen = 0x01, + DfClose = 0x02, + DfHeartbeat = 0x03, + DfProperties = 0x10, + DfGetFileAttribute = 0x11, + DfSetFileAttribute = 0x12, + // the following two are implemented as type-0 multipart commands, but + // we need command identifiers for GetPLDMCommands. + DfRead = 0x20, + DfFifoSend = 0x21, +} + +#[allow(missing_docs)] +pub mod file_ccode { + pub const INVALID_FILE_DESCRIPTOR: u8 = 0x80; + pub const INVALID_DF_ATTRIBUTE: u8 = 0x81; + pub const ZEROLENGTH_NOT_ALLOWED: u8 = 0x82; + pub const EXCLUSIVE_OWNERSHIP_NOT_ESTABLISHED: u8 = 0x83; + pub const EXCLUSIVE_OWNERSHIP_NOT_ALLOWED: u8 = 0x84; + pub const EXCLUSIVE_OWNERSHIP_NOT_AVAILABLE: u8 = 0x85; + pub const INVALID_FILE_IDENTIFIER: u8 = 0x86; + pub const DFOPEN_DIR_NOT_ALLOWED: u8 = 0x87; + pub const MAX_NUM_FDS_EXCEEDED: u8 = 0x88; + pub const FILE_OPEN: u8 = 0x89; + pub const UNABLE_TO_OPEN_FILE: u8 = 0x8a; +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct FileIdentifier(pub u16); + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct FileDescriptor(pub u16); + +// These are represented as their encoding in the DfProperties command; +// a bitmask, where only one value is permitted. +#[repr(u32)] +pub enum DfProperty { + MaxConcurrentMedium = 0x01, + MaxFileDescriptors = 0x02, +} + +#[derive(EnumSetType, Debug)] +pub enum DfOpenAttribute { + DfOpenWrite = 0, + DfOpenExclusive = 1, + DfOpenFifo = 2, + DfOpenPushed = 3, +} + +pub type DfOpenAttributes = EnumSet; + +#[derive(EnumSetType, Debug)] +pub enum DfCloseAttribute { + DfCloseZeroLength = 0, +} + +pub type DfCloseAttributes = EnumSet; + +impl TryFrom for DfProperty { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0x01 => Ok(Self::MaxConcurrentMedium), + 0x02 => Ok(Self::MaxFileDescriptors), + _ => Err(()), + } + } +} + +#[derive(DekuRead, DekuWrite)] +pub struct DfPropertiesReq { + pub property: u32, +} + +#[derive(DekuRead, DekuWrite)] +pub struct DfPropertiesResp { + pub value: u32, +} + +#[derive(DekuRead, DekuWrite)] +pub struct DfOpenReq { + pub file_identifier: u16, + pub attributes: u16, +} + +#[derive(DekuRead, DekuWrite)] +pub struct DfOpenResp { + pub file_descriptor: u16, +} + +#[derive(DekuRead, DekuWrite)] +pub struct DfCloseReq { + pub file_descriptor: u16, + pub attributes: u16, +} diff --git a/pldm-fw/src/lib.rs b/pldm-fw/src/lib.rs index 2907411..2e26db8 100644 --- a/pldm-fw/src/lib.rs +++ b/pldm-fw/src/lib.rs @@ -4,7 +4,7 @@ * * Copyright (c) 2023 Code Construct */ -#![cfg_attr(not(any(feature = "std", test)), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] #![forbid(unsafe_code)] // #![warn(missing_docs)] @@ -30,7 +30,8 @@ use nom::{ #[cfg(feature = "alloc")] use nom::multi::{count, length_count}; -extern crate pldm; +#[cfg(feature = "alloc")] +extern crate alloc; /// Firmware Device specific pub mod fd; diff --git a/pldm-platform-util/Cargo.toml b/pldm-platform-util/Cargo.toml new file mode 100644 index 0000000..a49f817 --- /dev/null +++ b/pldm-platform-util/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pldm-platform-util" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +anyhow = { workspace = true } +argh = { workspace = true } +deku = { workspace = true } +env_logger = { workspace = true } +log = { workspace = true } +mctp = { workspace = true } +mctp-linux = { workspace = true } +pldm-platform = { workspace = true, features = ["std"] } +smol = { workspace = true } + +[features] +deku-debug = ["deku/logging"] diff --git a/pldm-platform-util/build.rs b/pldm-platform-util/build.rs new file mode 100644 index 0000000..bbb8fd4 --- /dev/null +++ b/pldm-platform-util/build.rs @@ -0,0 +1,21 @@ +use std::process::Command; + +fn main() { + let version = Command::new("git") + .args(["describe", "--always", "--dirty"]) + .output() + .map(|o| String::from_utf8(o.stdout).unwrap().trim().to_string()) + .unwrap_or("(unknown)".to_string()); + + let path_res = Command::new("git") + .args(["rev-parse", "--path-format=relative", "--git-dir"]) + .output() + .map(|o| String::from_utf8(o.stdout).unwrap().trim().to_string()); + + println!("cargo:rustc-env=VERSION={version}"); + if let Ok(path) = path_res { + println!("cargo:rerun-if-changed={path}/HEAD"); + // default rerun paths get lost once any have been added. + println!("cargo:rerun-if-changed=."); + } +} diff --git a/pldm-platform-util/src/bin/pldm-platform.rs b/pldm-platform-util/src/bin/pldm-platform.rs new file mode 100644 index 0000000..cf6ae73 --- /dev/null +++ b/pldm-platform-util/src/bin/pldm-platform.rs @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +/* + * PLDM platform utility. + * + * Copyright (c) 2025 Code Construct + */ +#[allow(unused)] +use log::{debug, error, info, trace, warn}; + +use anyhow::{bail, Result}; + +use argh::FromArgs; +use mctp_linux::MctpAddr; + +use pldm_platform::proto::{SensorId, SetSensorOperationalState}; +use pldm_platform::requester::*; + +#[derive(FromArgs, Debug)] +#[argh(description = "PLDM platform requester")] +struct Args { + #[argh(switch, short = 'd')] + /// debug logging + debug: bool, + + #[argh(switch)] + /// trace logging + trace: bool, + + /// MCTP net/EID of device + #[argh(positional)] + addr: MctpAddr, + + #[argh(subcommand)] + command: Command, +} + +#[derive(FromArgs, Debug)] +#[argh(subcommand)] +enum Command { + NumericSensor(NumericSensorCommand), + StateSensor(StateSensorCommand), + NumericEnable(NumericEnableCommand), + StateEnable(StateEnableCommand), + PdrRepository(PdrRepositoryCommand), + GetPdr(GetPdrCommand), + Version(VersionCommand), +} + +#[derive(FromArgs, Debug)] +#[argh(subcommand, name = "version", description = "Print version")] +struct VersionCommand {} + +#[derive(FromArgs, Debug)] +#[argh( + subcommand, + name = "numeric-sensor", + description = "Get Numeric Sensor Reading" +)] +struct NumericSensorCommand { + /// sensor ID + #[argh(positional)] + sensor: SensorId, +} + +#[derive(FromArgs, Debug)] +#[argh( + subcommand, + name = "state-sensor", + description = "Get State Sensor Reading. Only simple sensors supported." +)] +struct StateSensorCommand { + /// sensor ID + #[argh(positional)] + sensor: SensorId, + + /// state set + #[argh(option)] + state_set: Option, +} + +#[derive(FromArgs, Debug)] +#[argh( + subcommand, + name = "numeric-enable", + description = "Set Numeric Sensor Enable" +)] +struct NumericEnableCommand { + /// event enable. disable, enable, op-only, state-only + #[argh(option)] + event: Option, + + /// sensor ID + #[argh(positional)] + sensor: SensorId, + + /// operational state. enable, disable, unavailable + // TODO: could use enums once newer argh is released + #[argh(positional)] + op_state: String, +} + +#[derive(FromArgs, Debug)] +#[argh( + subcommand, + name = "state-enable", + description = "Set State Sensor Enable. Only simple sensors supported." +)] +struct StateEnableCommand { + /// event enable. disable, enable, op-only, state-only + #[argh(option)] + event: Option, + + /// sensor ID + #[argh(positional)] + sensor: SensorId, + + /// operational state. enable, disable, unavailable + // TODO: could use enums once newer argh is released + #[argh(positional)] + op_state: String, +} + +#[derive(FromArgs, Debug)] +#[argh(subcommand, name = "pdr-repo", description = "Get PDR Repsitory Info")] +struct PdrRepositoryCommand {} + +#[derive(FromArgs, Debug)] +#[argh(subcommand, name = "get-pdr", description = "Get PDR")] +struct GetPdrCommand { + /// PDR record + #[argh(positional)] + record: u32, +} + +fn enable_command_op(op_state: &str) -> Result { + Ok(if op_state.starts_with("en") { + SetSensorOperationalState::Enabled + } else if op_state.starts_with("dis") { + SetSensorOperationalState::Disabled + } else if op_state.starts_with("un") { + SetSensorOperationalState::Unavailable + } else { + bail!("Bad operational state '{}'", op_state); + }) +} + +fn enable_command_op_event_enable(event: &Option) -> Result { + Ok(if let Some(e) = event { + if e.starts_with("en") || e == "op-only" { + true + } else if e.starts_with("dis") || e == "state-only" { + false + } else { + bail!("Bad --event argument"); + } + } else { + false + }) +} + +fn enable_command_state_event_enable(event: &Option) -> Result { + Ok(if let Some(e) = event { + if e.starts_with("en") || e == "state-only" { + true + } else if e.starts_with("dis") || e == "op-only" { + false + } else { + bail!("Bad --event argument"); + } + } else { + false + }) +} + +fn main() -> anyhow::Result<()> { + smol::block_on(async_main()) +} + +async fn async_main() -> anyhow::Result<()> { + let args: Args = argh::from_env(); + + let level = if args.trace { + log::LevelFilter::Trace + } else if args.debug { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }; + + env_logger::Builder::new() + .filter_level(level) + .format_timestamp(None) + .init(); + + match args.command { + Command::Version(_) => info!("pldm-platform {}", env!("VERSION")), + Command::NumericSensor(s) => { + let mut ep = args.addr.create_req_async()?; + let reading = get_sensor_reading(&mut ep, s.sensor, false).await?; + println!("Sensor {} {:?}", s.sensor.0, reading); + } + Command::StateSensor(s) => { + let mut ep = args.addr.create_req_async()?; + let reading = + get_simple_state_sensor_reading(&mut ep, s.sensor, false) + .await?; + if let Some(state_set) = s.state_set { + println!( + "Sensor {} {:?}", + s.sensor.0, + reading.debug_state_set(state_set) + ); + } else { + println!("Sensor {} {:?}", s.sensor.0, reading); + } + } + Command::NumericEnable(s) => { + let mut ep = args.addr.create_req_async()?; + set_numeric_sensor_enable( + &mut ep, + s.sensor, + enable_command_op(&s.op_state)?, + s.event.is_none(), + enable_command_op_event_enable(&s.event)?, + enable_command_state_event_enable(&s.event)?, + ) + .await?; + } + Command::StateEnable(s) => { + let mut ep = args.addr.create_req_async()?; + set_simple_state_sensor_enables( + &mut ep, + s.sensor, + enable_command_op(&s.op_state)?, + s.event.is_none(), + enable_command_op_event_enable(&s.event)?, + enable_command_state_event_enable(&s.event)?, + ) + .await?; + } + Command::PdrRepository(_) => { + let mut ep = args.addr.create_req_async()?; + let pdr_info = get_pdr_repository_info(&mut ep).await?; + println!("PDR Repository Info: {pdr_info:#x?}"); + } + Command::GetPdr(s) => { + let mut ep = args.addr.create_req_async()?; + let pdr = get_pdr(&mut ep, s.record).await?; + println!("PDR: {pdr:#x?}"); + } + } + Ok(()) +} diff --git a/pldm-platform/Cargo.toml b/pldm-platform/Cargo.toml new file mode 100644 index 0000000..035d65b --- /dev/null +++ b/pldm-platform/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pldm-platform" +description = "Platform Level Data Model (PLDM) Platform Monitoring and Control" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +categories = ["network-programming", "embedded", "hardware-support"] + +[dependencies] +deku = { workspace = true } +heapless = { workspace = true } +log = { workspace = true } +mctp = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +pldm = { workspace = true } + +[features] +default = ["std"] +alloc = ["pldm/alloc", "deku/alloc"] +std = ["alloc", "pldm/std", "mctp/std"] diff --git a/pldm-platform/README.md b/pldm-platform/README.md new file mode 100644 index 0000000..2514faf --- /dev/null +++ b/pldm-platform/README.md @@ -0,0 +1,15 @@ +# PLDM Platform + +This crate implements PLDM Platform ("type 2") handling. + +Currently only a subset of commands are implemented. +PLDM type 2 is defined by DMTF DSP0248 and DSP0249 (state sets). + +At the moment the crate requires `alloc`, that requirement will be relaxed later. + +[`pldm-platform-util`](../pldm-platform-util) crate provides a PLDM +requester program to run on Linux. + + + + diff --git a/pldm-platform/src/lib.rs b/pldm-platform/src/lib.rs new file mode 100644 index 0000000..370f9db --- /dev/null +++ b/pldm-platform/src/lib.rs @@ -0,0 +1,18 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +pub mod proto; +pub mod requester; +pub mod state_sets; + +pub const PLDM_TYPE_PLATFORM: u8 = 2; + +/// Re-export of `deku`. +/// +/// Traits allow encoding/decoding [`pldm_platform::proto`](proto) data structures. +pub use deku; +/// Re-export of `heapless::Vec` +pub use heapless::Vec; diff --git a/pldm-platform/src/proto.rs b/pldm-platform/src/proto.rs new file mode 100644 index 0000000..b4f7382 --- /dev/null +++ b/pldm-platform/src/proto.rs @@ -0,0 +1,676 @@ +use core::{marker::PhantomData, num::ParseIntError, str::FromStr}; + +use deku::DekuContainerWrite; +#[allow(unused)] +use log::{debug, error, info, trace, warn}; + +use core::fmt::Debug; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +use deku::{ + ctx::Limit, deku_derive, writer::Writer, DekuEnumExt, DekuError, DekuRead, + DekuReader, DekuUpdate, DekuWrite, DekuWriter, +}; + +use pldm::control::xfer_flag; +use pldm::{proto_error, PldmError, PldmResult}; + +pub mod entity_type { + pub const PHYSICAL: u16 = 0b00000000_00000000; + pub const LOGICAL: u16 = 0b10000000_00000000; +} + +/// PLDM Platform Commands +#[allow(missing_docs)] +#[derive(FromPrimitive, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(u8)] +pub enum Cmd { + GetTerminusUID = 0x03, + SetEventReceiver = 0x04, + GetEventReceiver = 0x05, + PlatformEventMessage = 0x0A, + PollForPlatformEventMessage = 0x0B, + EventMessageSupported = 0x0C, + EventMessageBufferSize = 0x0D, + SetNumericSensorEnable = 0x10, + GetSensorReading = 0x11, + GetSensorThresholds = 0x12, + SetSensorThresholds = 0x13, + RestoreSensorThresholds = 0x14, + GetSensorHysteresis = 0x15, + SetSensorHysteresis = 0x16, + InitNumericSensor = 0x17, + SetStateSensorEnables = 0x20, + GetStateSensorReadings = 0x21, + InitStateSensor = 0x22, + SetNumericEffecterEnable = 0x30, + SetNumericEffecterValue = 0x31, + GetNumericEffecterValue = 0x32, + SetStateEffecterEnables = 0x38, + SetStateEffecterStates = 0x39, + GetStateEffecterStates = 0x3A, + GetPLDMEventLogInfo = 0x40, + EnablePLDMEventLogging = 0x41, + ClearPLDMEventLog = 0x42, + GetPLDMEventLogTimestamp = 0x43, + SetPLDMEventLogTimestamp = 0x44, + ReadPLDMEventLog = 0x45, + GetPLDMEventLogPolicyInfo = 0x46, + SetPLDMEventLogPolicy = 0x47, + FindPLDMEventLogEntry = 0x48, + GetPDRRepositoryInfo = 0x50, + GetPDR = 0x51, + FindPDR = 0x52, + RunInitAgent = 0x58, + GetPDRRepositorySignature = 0x53, +} + +impl TryFrom for Cmd { + type Error = PldmError; + + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + proto_error!("Unknown PLDM platform command", "{value:02x}") + }) + } +} + +// TODO: PlatformError type? + +/// PLDM platform response codes +#[allow(missing_docs)] +pub mod plat_codes { + pub const INVALID_SENSOR_ID: u8 = 0x80; + pub const EVENT_GENERATION_NOT_SUPPORTED: u8 = 0x82; + + // Get PDR + pub const INVALID_DATA_TRANSFER_HANDLE: u8 = 0x80; + pub const INVALID_TRANSFER_OPERATION_FLAG: u8 = 0x81; + pub const INVALID_RECORD_HANDLE: u8 = 0x82; + pub const INVALID_RECORD_CHANGE_NUMBER: u8 = 0x83; + pub const TRANSFER_TIMEOUT: u8 = 0x84; + pub const REPOSITORY_UPDATE_IN_PROGRESS: u8 = 0x85; +} + +pub use plat_codes::*; + +// repr(u8) doesn't work with with field-less variants for Deku +#[derive(Debug, Eq, PartialEq, Hash, Clone, DekuWrite, DekuRead)] +#[deku(endian = "little", ctx = "data_size: u8", id = "data_size")] +pub enum SensorData { + #[deku(id = 0)] + U8(u8), + #[deku(id = 1)] + I8(i8), + #[deku(id = 2)] + U16(u16), + #[deku(id = 3)] + I16(i16), + #[deku(id = 4)] + U32(u32), + #[deku(id = 5)] + I32(i32), + #[deku(id = 6)] + U64(u64), + #[deku(id = 7)] + I64(i64), +} + +#[allow(missing_docs)] +#[derive( + FromPrimitive, Debug, PartialEq, Eq, Copy, Clone, DekuRead, DekuWrite, +)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum SensorOperationalState { + Enabled = 0, + Disabled, + Unavailable, + StatusUnknown, + Failed, + Initializing, + ShuttingDown, + InTest, +} + +#[allow(missing_docs)] +#[derive( + FromPrimitive, Debug, PartialEq, Eq, Copy, Clone, DekuRead, DekuWrite, +)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum SetSensorOperationalState { + Enabled = 0, + Disabled, + Unavailable, +} + +#[allow(missing_docs)] +#[derive( + FromPrimitive, Debug, PartialEq, Eq, Copy, Clone, DekuRead, DekuWrite, +)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum SensorEventMessageEnable { + /// NoEventGeneration for GetSensor, NoChange for SetSensorEnable + NoEventGeneration = 0, + EventsDisabled, + EventsEnabled, + OpEventsOnlyEnabled, + StateEventsOnlyEnabled, +} + +impl SensorEventMessageEnable { + pub fn new(op_enable: bool, state_enable: bool) -> Self { + match (op_enable, state_enable) { + (true, true) => Self::EventsEnabled, + (false, false) => Self::EventsDisabled, + (true, false) => Self::OpEventsOnlyEnabled, + (false, true) => Self::StateEventsOnlyEnabled, + } + } +} + +#[allow(missing_docs)] +#[derive( + FromPrimitive, Debug, PartialEq, Eq, Copy, Clone, DekuRead, DekuWrite, +)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum SensorState { + Unknown = 0, + Normal, + Warning, + Critical, + Fatal, + LowerWarning, + LowerCritical, + LowerFatal, + UpperWarning, + UpperCritical, + UpperFatal, +} + +#[derive(Debug, Clone, Default)] +pub struct VecWrap(pub heapless::Vec); + +impl From> for VecWrap { + fn from(value: heapless::Vec) -> Self { + Self(value) + } +} + +impl core::ops::Deref for VecWrap { + type Target = heapless::Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::DerefMut for VecWrap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a, T, Predicate, Ctx, const N: usize> + DekuReader<'a, (Limit, Ctx)> for VecWrap +where + Predicate: FnMut(&T) -> bool, + Ctx: Copy, + T: DekuReader<'a, Ctx>, +{ + fn from_reader_with_ctx< + R: deku::no_std_io::Read + deku::no_std_io::Seek, + >( + reader: &mut deku::reader::Reader, + (limit, ctx): (Limit, Ctx), + ) -> 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(T::from_reader_with_ctx(reader, ctx)?).map_err(|_| { + DekuError::InvalidParam("Too many elements".into()) + })? + } + + Ok(VecWrap(v)) + } +} + +impl DekuWriter for VecWrap +where + T: DekuWriter, + Ctx: Copy, +{ + // Required method + fn to_writer( + &self, + writer: &mut Writer, + ctx: Ctx, + ) -> core::result::Result<(), DekuError> { + self.0.to_writer(writer, ctx) + } +} + +#[derive(Debug, DekuRead, DekuWrite, PartialEq, Eq, Clone, Copy)] +#[deku(endian = "little")] +pub struct SensorId(pub u16); + +impl FromStr for SensorId { + type Err = ParseIntError; + fn from_str(s: &str) -> core::result::Result { + Ok(Self(if let Some(s) = s.strip_prefix("0x") { + u16::from_str_radix(s, 16) + } else { + s.parse() + }?)) + } +} + +#[derive(Debug, DekuRead, DekuWrite, PartialEq, Eq, Clone)] +pub struct GetSensorReadingReq { + pub sensor: SensorId, + pub rearm: bool, +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct GetSensorReadingResp { + #[deku(temp, temp_value = "reading.deku_id().unwrap()")] + data_size: u8, + pub op_state: SensorOperationalState, + pub event_enable: SensorEventMessageEnable, + pub present_state: SensorState, + pub previous_state: SensorState, + pub event_state: SensorState, + #[deku(ctx = "*data_size")] + pub reading: SensorData, +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct GetStateSensorReadingsReq { + pub sensor: SensorId, + pub rearm: u8, + #[deku(temp, temp_value = "0")] + rsvd: u8, +} + +#[derive(Debug, DekuRead, DekuWrite, Clone)] +pub struct StateField { + pub op_state: SensorOperationalState, + pub present_state: u8, + pub previous_state: u8, + pub event_state: u8, +} + +impl StateField { + pub fn debug_state_set(&self, state_set: u16) -> StateFieldDebug<'_> { + StateFieldDebug { + inner: self, + state_set, + } + } +} + +pub struct StateDebug { + state: u8, + state_set: PhantomData, +} + +impl StateDebug { + fn new(state: u8) -> Self { + Self { + state, + state_set: PhantomData, + } + } +} + +impl Debug for StateDebug { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if let Some(v) = T::from_u8(self.state) { + write!(f, "{} {:?}", self.state, &v) + } else { + write!(f, "{} (unrecognised state)", self.state) + } + } +} + +/// Print debug formatting with a given StateSet value `T`. +/// +/// Will print u8 version for unknown state set. +pub struct StateFieldDebug<'a> { + inner: &'a StateField, + state_set: u16, +} + +impl StateFieldDebug<'_> { + pub fn debug_from_u8( + &self, + f: &mut core::fmt::Formatter, + ) -> core::fmt::Result { + f.debug_struct("StateField") + .field("op_state", &self.inner.op_state) + .field( + "present_state", + &StateDebug::::new(self.inner.present_state), + ) + .field( + "previous_state", + &StateDebug::::new(self.inner.previous_state), + ) + .field( + "event_state", + &StateDebug::::new(self.inner.present_state), + ) + .finish() + } +} + +impl Debug for StateFieldDebug<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use crate::state_sets::*; + match self.state_set { + OperationFaultStatus::ID => { + self.debug_from_u8::(f) + } + DeviceInitialization::ID => { + self.debug_from_u8::(f) + } + HardwareSecurity::ID => self.debug_from_u8::(f), + _ => { + debug!("Unrecognised state set {:#04x}", self.state_set); + write!(f, "{:?}", self.inner) + } + } + } +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct GetStateSensorReadingsResp { + #[deku(temp, temp_value = "self.fields.len() as u8")] + pub composite_sensor_count: u8, + #[deku(count = "composite_sensor_count")] + pub fields: VecWrap, +} + +#[derive(Debug, DekuRead, DekuWrite, Clone)] +pub struct SetNumericSensorEnableReq { + pub sensor: SensorId, + pub set_op_state: SetSensorOperationalState, + pub event_enable: SensorEventMessageEnable, +} + +#[derive(Debug, DekuRead, DekuWrite, Clone)] +pub struct SetEnableField { + pub set_op_state: SetSensorOperationalState, + pub event_enable: SensorEventMessageEnable, +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct SetStateSensorEnablesReq { + pub sensor: SensorId, + + #[deku(temp, temp_value = "self.fields.len() as u8")] + pub composite_sensor_count: u8, + + #[deku(count = "composite_sensor_count")] + pub fields: VecWrap, +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone, Eq, PartialEq)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum PDRRepositoryState { + Available = 0, + UpdateInProgress, + Failed, +} + +// TODO +pub type Timestamp104 = [u8; 13]; + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct GetPDRRepositoryInfoResp { + pub state: PDRRepositoryState, + pub update_time: Timestamp104, + pub oem_update_time: Timestamp104, + pub record_count: u32, + pub repository_size: u32, + pub largest_record_size: u32, + pub data_transfer_handle_timeout: u8, +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone, Eq, PartialEq)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum TransferOperationFlag { + NextPart = 0, + FirstPart = 1, +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct GetPDRReq { + pub record_handle: u32, + pub data_transfer_handle: u32, + pub transfer_operation_flag: TransferOperationFlag, + pub request_count: u16, + pub record_change_number: u16, +} + +const MAX_PDR_TRANSFER: usize = 100; + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct GetPDRResp { + pub next_record_handle: u32, + pub next_data_transfer_handle: u32, + pub transfer_flag: u8, + + #[deku(temp, temp_value = "self.record_data.len() as u16")] + response_count: u16, + #[deku(count = "response_count")] + pub record_data: VecWrap, + + /// CRC over entire PDR, when transfer_flag == end + // TODO + #[deku(cond = "*transfer_flag & xfer_flag::END != 0")] + pub crc: Option, +} + +impl GetPDRResp { + pub fn new_single( + record_handle: u32, + record: PdrRecord, + ) -> PldmResult { + let mut pdr = Pdr { + record_handle, + pdr_header_version: PDR_VERSION_1, + record_change_number: 0, + data_length: 0, + record, + }; + pdr.update()?; + info!("pdr after update {pdr:#x?}"); + + let mut s = GetPDRResp { + next_record_handle: 0, + next_data_transfer_handle: 0, + transfer_flag: xfer_flag::START_AND_END, + record_data: Default::default(), + // TODO crc + crc: Some(0), + }; + let cap = s.record_data.capacity(); + s.record_data.resize_default(cap).unwrap(); + let len = pdr.to_slice(&mut s.record_data)?; + s.record_data.truncate(len); + Ok(s) + } +} + +#[derive(Default)] +struct Length { + pos: i64, + len: u64, +} + +impl Length { + fn len(c: &impl DekuWriter, ctx: CTX) -> Result { + let mut l = Length::default(); + let mut w = deku::writer::Writer::new(&mut l); + c.to_writer(&mut w, ctx)?; + info!("len {}", l.len); + Ok(l.len) + } +} + +impl deku::no_std_io::Write for &mut Length { + fn write(&mut self, buf: &[u8]) -> deku::no_std_io::Result { + info!("wr {}", buf.len()); + self.pos += buf.len() as i64; + self.len = self.len.max(self.pos as u64); + Ok(buf.len()) + } + fn flush(&mut self) -> deku::no_std_io::Result<()> { + Ok(()) + } +} + +impl deku::no_std_io::Seek for &mut Length { + fn seek( + &mut self, + pos: deku::no_std_io::SeekFrom, + ) -> deku::no_std_io::Result { + use deku::no_std_io; + match pos { + no_std_io::SeekFrom::Start(p) => self.pos = p as i64, + no_std_io::SeekFrom::End(_p) => { + return Err(no_std_io::Error::from( + no_std_io::ErrorKind::UnexpectedEof, + )); + } + no_std_io::SeekFrom::Current(p) => self.pos += p, + } + Ok(self.pos as u64) + } +} + +pub const PDR_VERSION_1: u8 = 1; + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct Pdr { + pub record_handle: u32, + pub pdr_header_version: u8, + #[deku(temp, temp_value = "self.record.pdr_type()")] + pdr_type: u8, + pub record_change_number: u16, + #[deku(update = "Length::len(&self.record, self.record.pdr_type())?")] + pub data_length: u16, + + #[deku(ctx = "*pdr_type")] + pub record: PdrRecord, +} + +#[non_exhaustive] +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +#[deku(ctx = "pdr_type: u8", id = "pdr_type")] +pub enum PdrRecord { + #[deku(id = 30)] + FileDescriptor(FileDescriptorPdr), +} + +impl PdrRecord { + fn pdr_type(&self) -> u8 { + match self { + Self::FileDescriptor(_) => 30, + } + } +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +#[deku(id_type = "u8")] +#[repr(u8)] +pub enum FileClassification { + OEM = 0, + BootLog, + SerialTxFIFO, + SerialRxFIFO, + DiagnosticLog, + CrashDumpFile, + SecurityLog, + FRUDataFile, + TelemetryDataFile, + TelemetryDataLog, + OtherLog = 0xFD, + OtherFile = 0xFE, + FileDirectory = 0xFF, +} + +pub mod file_capabilities { + pub const EX_READ_OPEN: u16 = 1 << 0; + pub const EX_WRITE_OPEN: u16 = 1 << 1; + pub const FILE_TRUNC: u16 = 1 << 2; + pub const DATA_TYPE: u16 = 1 << 3; + pub const POLLED: u16 = 1 << 4; + pub const PUSHED: u16 = 1 << 5; + pub const DATA_VOLATILITY: u16 = 1 << 6; + pub const FILE_MODIFY: u16 = 1 << 7; + pub const FC_ZERO_LENGTH_PERMITTED: u16 = 1 << 8; + pub const FC_WRITES_PERMITTED: u16 = 1 << 9; +} + +#[deku_derive(DekuRead, DekuWrite)] +#[derive(Debug, Clone)] +pub struct FileDescriptorPdr { + pub terminus_handle: u16, + pub file_identifier: u16, + pub entity_type: u16, + pub entity_instance: u16, + pub container_id: u16, + pub superior_directory: u16, + pub file_classification: FileClassification, + pub oem_file_classification: u8, + pub capabilities: u16, + pub file_version: u32, + pub file_max_size: u32, + pub file_max_desc_count: u8, + pub file_name_length: u8, + + #[deku(temp, temp_value = "self.file_name.len() as u8")] + pub file_name_len: u8, + /// File name. + /// + /// A null terminated string. + // TODO: null terminated string type + // TODO: max length + #[deku(count = "file_name_len")] + pub file_name: VecWrap, + + #[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 + #[deku(count = "oem_file_name_len")] + pub oem_file_name: VecWrap, +} diff --git a/pldm-platform/src/requester.rs b/pldm-platform/src/requester.rs new file mode 100644 index 0000000..1a52658 --- /dev/null +++ b/pldm-platform/src/requester.rs @@ -0,0 +1,304 @@ +#[allow(unused)] +use log::{debug, error, info, trace, warn}; + +use num_traits::FromPrimitive; + +use crate::proto::*; +use crate::PLDM_TYPE_PLATFORM; +use pldm::{ + control::xfer_flag, pldm_xfer_buf_async, proto_error, CCode, PldmError, + PldmRequest, Result, +}; + +use deku::{DekuContainerRead, DekuContainerWrite}; + +use heapless::Vec; + +/// Reads a numeric sensor. +pub async fn get_sensor_reading( + comm: &mut impl mctp::AsyncReqChannel, + sensor: SensorId, + rearm: bool, +) -> Result { + let r = GetSensorReadingReq { sensor, rearm }; + + let mut buf = [0; 10]; + let l = r.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_PLATFORM, + Cmd::GetSensorReading as u8, + buf, + ); + + let mut rx = [0; 30]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + let ((rest, _), ret) = GetSensorReadingResp::from_bytes((&resp.data, 0)) + .map_err(|e| { + trace!("GetSensorReading parse error {e}"); + proto_error!("Bad GetSensorReading response") + })?; + + if !rest.is_empty() { + return Err(proto_error!("Extra response")); + } + + Ok(ret) +} + +/// Reads a simple state sensor. +/// +/// Reads sensor offset 0. +pub async fn get_simple_state_sensor_reading( + comm: &mut impl mctp::AsyncReqChannel, + sensor: SensorId, + rearm: bool, +) -> Result { + let r = GetStateSensorReadingsReq { + sensor, + rearm: rearm as u8, + }; + + let mut buf = [0; 10]; + let l = r.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_PLATFORM, + Cmd::GetStateSensorReadings as u8, + buf, + ); + + let mut rx = [0; 50]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + match CCode::from_u8(resp.cc) { + Some(CCode::SUCCESS) => (), + Some(e) => { + return Err(proto_error!("Error response", "{e:?}")); + } + None if resp.cc == INVALID_SENSOR_ID => { + return Err(proto_error!("Invalid Sensor ID")); + } + None => return Err(proto_error!("Error", "{}", resp.cc)), + } + + let ((rest, _), mut ret) = GetStateSensorReadingsResp::from_bytes(( + &resp.data, 0, + )) + .map_err(|e| { + trace!("GetStateSensorReadings parse error {e}"); + proto_error!("Bad GetStateSensorReadings response") + })?; + + if !rest.is_empty() { + return Err(proto_error!("Extra response")); + } + + if ret.fields.len() != 1 { + return Err(proto_error!("Incorrect sensor count")); + } + + Ok(ret.fields.swap_remove(0)) +} + +/// SetNumericSensorEnable +/// +/// `op_event_enable` and `state_event_enable` are ignored if `event_no_change` is set. +pub async fn set_numeric_sensor_enable( + comm: &mut impl mctp::AsyncReqChannel, + sensor: SensorId, + set_op_state: SetSensorOperationalState, + event_no_change: bool, + op_event_enable: bool, + state_event_enable: bool, +) -> Result<()> { + let event_enable = if event_no_change { + SensorEventMessageEnable::NoEventGeneration + } else { + SensorEventMessageEnable::new(op_event_enable, state_event_enable) + }; + + let r = SetNumericSensorEnableReq { + sensor, + set_op_state, + event_enable, + }; + + let mut buf = [0; 10]; + let l = r.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_PLATFORM, + Cmd::SetNumericSensorEnable as u8, + buf, + ); + + let mut rx = [0; 50]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + match CCode::from_u8(resp.cc) { + Some(CCode::SUCCESS) => (), + Some(e) => { + return Err(proto_error!("Error response", "{e:?}")); + } + None if resp.cc == INVALID_SENSOR_ID => { + return Err(proto_error!("Invalid Sensor ID")); + } + None if resp.cc == EVENT_GENERATION_NOT_SUPPORTED => { + return Err(proto_error!("Event generation not supported")); + } + None => return Err(proto_error!("Error", "{}", resp.cc)), + } + + Ok(()) +} + +/// SetStateSensorEnables +/// +/// Sets field 0 of a sensor. +/// `op_event_enable` and `state_event_enable` are ignored if `event_no_change` is set. +pub async fn set_simple_state_sensor_enables( + comm: &mut impl mctp::AsyncReqChannel, + sensor: SensorId, + set_op_state: SetSensorOperationalState, + event_no_change: bool, + op_event_enable: bool, + state_event_enable: bool, +) -> Result<()> { + let event_enable = if event_no_change { + SensorEventMessageEnable::NoEventGeneration + } else { + SensorEventMessageEnable::new(op_event_enable, state_event_enable) + }; + + let f = SetEnableField { + set_op_state, + event_enable, + }; + let fields = Vec::from_slice(&[f]).unwrap().into(); + let r = SetStateSensorEnablesReq { sensor, fields }; + + let mut buf = [0; 10]; + let l = r.to_slice(&mut buf).map_err(|_| PldmError::NoSpace)?; + let buf = &buf[..l]; + + let req = PldmRequest::new_borrowed( + PLDM_TYPE_PLATFORM, + Cmd::SetStateSensorEnables as u8, + buf, + ); + + let mut rx = [0; 50]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + match CCode::from_u8(resp.cc) { + Some(CCode::SUCCESS) => (), + Some(e) => { + return Err(proto_error!("Error response", "{e:?}")); + } + None if resp.cc == INVALID_SENSOR_ID => { + return Err(proto_error!("Invalid Sensor ID")); + } + None if resp.cc == EVENT_GENERATION_NOT_SUPPORTED => { + return Err(proto_error!("Event generation not supported")); + } + None => return Err(proto_error!("Error", "{}", resp.cc)), + } + + Ok(()) +} + +pub async fn get_pdr_repository_info( + comm: &mut impl mctp::AsyncReqChannel, +) -> Result { + let req = PldmRequest::new_borrowed( + PLDM_TYPE_PLATFORM, + Cmd::GetPDRRepositoryInfo as u8, + &[], + ); + + let mut rx = [0; 50]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx).await?; + + let ((rest, _), ret) = + GetPDRRepositoryInfoResp::from_bytes((&resp.data, 0)).map_err(|e| { + trace!("GetPDRRepositoryInfo parse error {e}"); + proto_error!("Bad GetPDRRepositoryInfo response") + })?; + + if !rest.is_empty() { + return Err(proto_error!("Extra response")); + } + + Ok(ret) +} + +pub async fn get_pdr( + comm: &mut impl mctp::AsyncReqChannel, + record_handle: u32, +) -> Result { + // TODO: callers pass a buffer? might be nice to + // reuse between tx/rx. + let mut rxbuf = [0; 200]; + + let getpdr = GetPDRReq { + record_handle, + data_transfer_handle: 0, + transfer_operation_flag: TransferOperationFlag::FirstPart, + // subtract 4 bytes pldm header, 12 bytes PDR header/crc + request_count: (rxbuf.len() - 4 - 12) as u16, + record_change_number: 0, + }; + let mut txdata = [0; 50]; + let l = getpdr.to_slice(&mut txdata)?; + let txdata = &txdata[..l]; + let req = PldmRequest::new_borrowed( + PLDM_TYPE_PLATFORM, + Cmd::GetPDR as u8, + txdata, + ); + + let resp = pldm_xfer_buf_async(comm, req, &mut rxbuf).await?; + let ((rest, _), pdrrsp) = + GetPDRResp::from_bytes((&resp.data, 0)).map_err(|e| { + trace!("GetPDR parse error {e:?}"); + proto_error!("Bad GetPDR response") + })?; + if !rest.is_empty() { + return Err(proto_error!("Extra response")); + } + + if pdrrsp.transfer_flag != xfer_flag::START_AND_END { + return Err(proto_error!("Can't handle multipart")); + } + + let ((rest, _), pdr) = + Pdr::from_bytes((&pdrrsp.record_data, 0)).map_err(|e| { + trace!("GetSensorReading parse error {e}"); + proto_error!("Bad GetSensorReading response") + })?; + if !rest.is_empty() { + return Err(proto_error!("Extra PDR response")); + } + + if pdr.record_handle != record_handle { + return Err(proto_error!("PDR record handle mismatch")); + } + if pdr.pdr_header_version != PDR_VERSION_1 { + return Err(proto_error!("PDR unknown version")); + } + + let expect_len = pdrrsp.record_data.len() - 10; + if pdr.data_length as usize != expect_len { + warn!( + "Incorrect PDR data_length, got {}, expect {}", + pdr.data_length, expect_len + ); + } + + Ok(pdr.record) +} diff --git a/pldm-platform/src/state_sets.rs b/pldm-platform/src/state_sets.rs new file mode 100644 index 0000000..a2dced1 --- /dev/null +++ b/pldm-platform/src/state_sets.rs @@ -0,0 +1,45 @@ +/// PLDM State Set definitions. +/// From DSP0249. Only contains a subset at present. +use num_derive::FromPrimitive; + +#[allow(missing_docs)] +#[derive(FromPrimitive, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum OperationFaultStatus { + Unknown = 0, + Normal, + Error, + NonRecoverableError, +} + +impl OperationFaultStatus { + pub const ID: u16 = 10; +} + +#[allow(missing_docs)] +#[derive(FromPrimitive, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum DeviceInitialization { + Unknown = 0, + Normal, + InitializationInProgress, + InitializationHung, + InitializationFailed, +} + +impl DeviceInitialization { + pub const ID: u16 = 20; +} + +#[allow(missing_docs)] +#[derive(FromPrimitive, Debug, PartialEq, Eq, Copy, Clone)] +#[repr(u8)] +pub enum HardwareSecurity { + Unknown = 0, + HardwareSecurityVerified, + HardwareSecurityUnverified, +} + +impl HardwareSecurity { + pub const ID: u16 = 99; +} diff --git a/pldm/Cargo.toml b/pldm/Cargo.toml index 4de9017..3265ade 100644 --- a/pldm/Cargo.toml +++ b/pldm/Cargo.toml @@ -8,10 +8,15 @@ repository.workspace = true categories = ["network-programming", "embedded", "hardware-support"] [dependencies] +crc = "3.0" +deku = { workspace = true } +heapless = { workspace = true } mctp = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } +log = "0.4" + [features] default = ["std"] alloc = [] diff --git a/pldm/src/control.rs b/pldm/src/control.rs new file mode 100644 index 0000000..604a13e --- /dev/null +++ b/pldm/src/control.rs @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +/* + * PLDM Messaging Control and Discovery ("PLDM Control") definitions. + * + * Copyright (c) 2025 Code Construct + */ + +//! PLDM Messaging Control and Discovery ("PLDM Control" / type 0) messaging +//! support. +//! +//! This module provides definitions for PLDM control requests and responses. + +use deku::{DekuRead, DekuWrite}; + +use crate::{proto_error, PldmError, Result}; + +pub mod requester; +pub mod responder; + +/// PLDM Messaging Control and Discovery type +pub const PLDM_TYPE_CONTROL: u8 = 0; + +/// PLDM Control command codes +#[derive(Debug)] +#[allow(missing_docs)] +#[repr(u8)] +#[non_exhaustive] +pub enum Cmd { + SetTID = 0x01, + GetTID = 0x02, + GetPLDMVersion = 0x03, + GetPLDMTypes = 0x04, + GetPLDMCommands = 0x05, + SelectPLDMVersion = 0x06, + NegotiateTransferParameters = 0x07, + MultipartSend = 0x08, + MultipartReceive = 0x09, +} + +/// Completion codes for the PLDM Base (subtype 0) commands +#[allow(missing_docs)] +pub mod control_ccode { + pub const INVALID_DATA_TRANSFER_HANDLE: u8 = 0x80; + pub const INVALID_TRANSFER_OPERATION_FLAG: u8 = 0x81; + pub const INVALID_PLDM_TYPE_IN_REQUEST_DATA: u8 = 0x83; + pub const INVALID_PLDM_VERSION_IN_REQUEST_DATA: u8 = 0x84; + pub const NEGOTIATION_INCOMPLETE: u8 = 0x83; +} + +/// Multipart transfer operation values +#[allow(missing_docs)] +#[allow(non_camel_case_types)] +pub mod xfer_op { + pub const FIRST_PART: u8 = 0; + pub const NEXT_PART: u8 = 1; + pub const ABORT: u8 = 2; + pub const COMPLETE: u8 = 3; + pub const CURRENT_PART: u8 = 4; +} + +/// Transfer flag values +#[allow(missing_docs)] +#[allow(non_camel_case_types)] +pub mod xfer_flag { + pub const START: u8 = 1; + pub const MIDDLE: u8 = 2; + pub const END: u8 = 4; + // Provided by the spec.... + pub const START_AND_END: u8 = START | END; + pub const ACKNOWLEDGE_COMPLETION: u8 = 8; +} + +impl TryFrom for Cmd { + type Error = PldmError; + + fn try_from(value: u8) -> Result { + let c = match value { + 0x01 => Self::SetTID, + 0x02 => Self::GetTID, + 0x03 => Self::GetPLDMVersion, + 0x04 => Self::GetPLDMTypes, + 0x05 => Self::GetPLDMCommands, + 0x06 => Self::SelectPLDMVersion, + 0x07 => Self::NegotiateTransferParameters, + 0x08 => Self::MultipartSend, + 0x09 => Self::MultipartReceive, + v => { + let _ = v; + return Err(proto_error!( + "Unknown PLDM base command", + "{v:02x}" + )); + } + }; + Ok(c) + } +} + +/// Set TID request +#[derive(Debug, DekuRead, DekuWrite)] +pub struct SetTIDReq { + /// TID + pub tid: u8, +} + +/// Get TID response +#[derive(Debug, DekuRead, DekuWrite)] +pub struct GetTIDResp { + /// TID + pub tid: u8, +} + +/// Get PLDM Version request +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct GetPLDMVersionReq { + /// DataTransferHandle + pub xfer_handle: u32, + /// TransferOperationFlag + pub xfer_op: u8, + /// PLDMType + pub pldm_type: u8, +} + +/// Get PLDM Version response +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct GetPLDMVersionResp { + /// NextDataTransferHandle + pub next_handle: u32, + /// TransferFlag + pub xfer_flag: u8, + /// Version. We only support one version in our responses at present. + pub version: u32, + /// Checksum, over the version data. + pub crc: u32, +} + +/// Get PLDM Types response +#[derive(Debug, DekuRead, DekuWrite)] +pub struct GetPLDMTypesResp { + /// PLDM Types bitmask + pub types: [u8; 8], +} + +/// Get PLDM Commands request +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct GetPLDMCommandsReq { + /// PLDMType + pub pldm_type: u8, + /// Version + pub version: u32, +} + +/// Get PLDM Commands response +#[derive(Debug, DekuRead, DekuWrite)] +pub struct GetPLDMCommandsResp { + /// PLDM Types bitmask + pub commands: [u8; 32], +} + +/// Negotiate Transfer Parameters request +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct NegotiateTransferParametersReq { + /// RequesterPartSize. + /// + /// Maximum transfer size supported by the requester + pub part_size: u16, + + /// Requester Protocol Support + /// + /// Bitmask of PLDM protocols implementing multipart transfer + pub protocols: [u8; 8], +} + +/// Negotiate Transfer Parameters response +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct NegotiateTransferParametersResp { + /// ResponderPartSize. + /// + /// Negotiated transfer size + pub part_size: u16, + + /// Responder Protocol Support + /// + /// Bitmask of negotiated PLDM protocols implementing multipart transfer + pub protocols: [u8; 8], +} + +/// Multipart Receive request +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct MultipartReceiveReq { + /// PLDM type + pub pldm_type: u8, + /// Transfer operation + pub xfer_op: u8, + /// Transfer context identifier + pub xfer_context: u32, + /// Transfer handle for this receive request + pub xfer_handle: u32, + /// Requested offset + pub req_offset: u32, + /// Requested length + pub req_length: u32, +} + +/// Multipart Receive response +#[derive(Debug, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct MultipartReceiveResp { + /// Transfer flag + pub xfer_flag: u8, + /// Next transfer handle + pub next_handle: u32, + /// Data length + pub len: u32, +} diff --git a/pldm/src/control/requester.rs b/pldm/src/control/requester.rs new file mode 100644 index 0000000..7410f72 --- /dev/null +++ b/pldm/src/control/requester.rs @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +/* + * PLDM base responder implementation. + * + * Copyright (c) 2025 Code Construct + */ + +//! PLDM base protocol requester support +#[allow(unused_imports)] +use log::{debug, error, info, trace, warn}; + +use mctp::AsyncReqChannel; + +use deku::prelude::*; + +use crate::{ + ccode_result, control, pldm_xfer_buf_async, proto_error, util::SliceWriter, + PldmError, PldmRequest, PldmResult, +}; + +use super::xfer_flag; + +/// Perform a Set TID request. +pub async fn set_tid( + comm: &mut impl AsyncReqChannel, + tid: u8, +) -> PldmResult<()> { + let mut buf = [0u8; 1]; + let msg = control::SetTIDReq { tid }; + let l = msg.to_slice(&mut buf)?; + let req = PldmRequest::new_borrowed( + control::PLDM_TYPE_CONTROL, + control::Cmd::SetTID as u8, + &buf[..l], + ); + + let mut rx_buf = [0u8; 4]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx_buf).await?; + + ccode_result(resp.cc) +} + +/// Perform a Get TID request. +pub async fn get_tid(comm: &mut impl AsyncReqChannel) -> PldmResult { + let req = PldmRequest::new_borrowed( + control::PLDM_TYPE_CONTROL, + control::Cmd::GetTID as u8, + &[], + ); + + let mut rx_buf = [0u8; 5]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx_buf).await?; + + ccode_result(resp.cc)?; + + let ((rest, _), tidrsp) = control::GetTIDResp::from_bytes((&resp.data, 0))?; + + if !rest.is_empty() { + // TODO + warn!("Extra get TID response"); + } + + Ok(tidrsp.tid) +} + +/// Perform a Get PLDM Version request. +pub async fn get_pldm_version<'f>( + comm: &mut impl AsyncReqChannel, + pldm_type: u8, + ret_buf: &'f mut [u32], +) -> PldmResult<&'f [u32]> { + let mut buf = [0u8; 6]; + let msg = control::GetPLDMVersionReq { + xfer_handle: 0, + // Get first part + xfer_op: 1, + pldm_type, + }; + let l = msg.to_slice(&mut buf)?; + let req = PldmRequest::new_borrowed( + control::PLDM_TYPE_CONTROL, + control::Cmd::GetPLDMVersion as u8, + &buf[..l], + ); + + let mut rx_buf = [0u8; 50]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx_buf).await?; + + ccode_result(resp.cc)?; + + let ((rest, _), vrsp) = + control::GetPLDMVersionResp::from_bytes((&resp.data, 0))?; + + if !rest.is_empty() { + // TODO + warn!("Extra get version response"); + } + + // Require StartAndEnd + if vrsp.xfer_flag != xfer_flag::START_AND_END { + // TODO + return Err(proto_error!("Can't handle parts")); + } + + let Some(r) = ret_buf.get_mut(..1) else { + return Err(proto_error!("Short ret_buf")); + }; + r[0] = vrsp.version; + Ok(r) +} + +/// Perform a Get PLDM Types request. +pub async fn get_pldm_types<'f>( + comm: &mut impl AsyncReqChannel, + ret_buf: &'f mut [u8], +) -> PldmResult<&'f [u8]> { + let req = PldmRequest::new_borrowed( + control::PLDM_TYPE_CONTROL, + control::Cmd::GetPLDMTypes as u8, + &[], + ); + + let mut rx_buf = [0u8; 70]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx_buf).await?; + + ccode_result(resp.cc)?; + + let ((rest, _), vty) = + control::GetPLDMTypesResp::from_bytes((&resp.data, 0))?; + + if !rest.is_empty() { + // TODO + warn!("Extra get types response"); + } + + let mut ret = SliceWriter::new(ret_buf); + for t in 0..64 { + if vty.types[t / 8] & (1 << (t % 8)) != 0 { + ret.push_le(t as u8).ok_or(PldmError::NoSpace)?; + } + } + + Ok(ret.done()) +} + +/// Perform a Get PLDM Commands request. +pub async fn get_pldm_commands<'f>( + comm: &mut impl AsyncReqChannel, + pldm_type: u8, + version: u32, + ret_buf: &'f mut [u8], +) -> PldmResult<&'f [u8]> { + let mut buf = [0u8; 5]; + let msg = control::GetPLDMCommandsReq { pldm_type, version }; + let l = msg.to_slice(&mut buf)?; + let req = PldmRequest::new_borrowed( + control::PLDM_TYPE_CONTROL, + control::Cmd::GetPLDMCommands as u8, + &buf[..l], + ); + + let mut rx_buf = [0u8; 36]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx_buf).await?; + + ccode_result(resp.cc)?; + + let ((rest, _), cmdrsp) = + control::GetPLDMCommandsResp::from_bytes((&resp.data, 0))?; + + if !rest.is_empty() { + // TODO + warn!("Extra get commands response"); + } + + let mut ret = SliceWriter::new(ret_buf); + for t in 0..256 { + if cmdrsp.commands[t / 8] & (1 << (t % 8)) != 0 { + ret.push_le(t as u8).ok_or(PldmError::NoSpace)?; + } + } + + Ok(ret.done()) +} + +/// Negotiate Transfer Parameters. +/// +/// This sends a Negotiate Transfer Parameters command with the given PLDM +/// type set and requested size. Returns the negotiated size and type set +/// from the responder. +pub async fn negotiate_transfer_parameters<'f>( + comm: &mut impl AsyncReqChannel, + req_types: &[u8], + neg_types_buf: &'f mut [u8], + part_size: u16, +) -> PldmResult<(u16, &'f [u8])> { + let mut buf = [0u8; 10]; + + let req_types = req_types.iter().fold(0u64, |x, typ| x | 1 << typ); + + let req = control::NegotiateTransferParametersReq { + part_size, + protocols: req_types.to_le_bytes(), + }; + + let len = req.to_slice(&mut buf)?; + let req = PldmRequest::new_borrowed( + control::PLDM_TYPE_CONTROL, + control::Cmd::NegotiateTransferParameters as u8, + &buf[..len], + ); + + let mut rx_buf = [0u8; 16]; + let resp = pldm_xfer_buf_async(comm, req, &mut rx_buf).await?; + + ccode_result(resp.cc)?; + + let ((rest, _), cmdrsp) = + control::NegotiateTransferParametersReq::from_bytes((&resp.data, 0))?; + + if !rest.is_empty() { + warn!("Extra negotiate transfer parameters response"); + } + + let neg_types = u64::from_le_bytes(cmdrsp.protocols); + let mut neg_types_ret = SliceWriter::new(neg_types_buf); + for t in 0..64 { + let mask = 1u64 << t; + if neg_types & mask != 0 { + neg_types_ret.push_le(t as u8).ok_or(PldmError::NoSpace)?; + } + } + + Ok((cmdrsp.part_size, neg_types_ret.done())) +} diff --git a/pldm/src/control/responder.rs b/pldm/src/control/responder.rs new file mode 100644 index 0000000..d9a2fd2 --- /dev/null +++ b/pldm/src/control/responder.rs @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +/* + * PLDM base responder implementation. + * + * Copyright (c) 2025 Code Construct + */ + +//! Platform Level Data Model (PLDM) base protocol responder support +//! +//! Using structures from the core PLDM support, implement a simple +//! responder that handles base commands, maintaining core PLDM state, mostly +//! for enumerating subtype responders on this endpoint. + +use deku::{DekuContainerRead, DekuContainerWrite, DekuError}; +use heapless::Vec; +use mctp::{AsyncRespChannel, Eid}; + +use crate::control::{self, control_ccode, Cmd, PLDM_TYPE_CONTROL}; +use crate::{ + pldm_tx_resp_async, proto_error, CCode, PldmError, PldmRequest, + PldmResponse, Result, +}; + +/// Unassigned terminus ID +pub const TID_UNASSIGNED: u8 = 0x00; + +struct TypeData { + id: u8, + version: u32, + // stored as log2 + multipart_size: Option, + commands: [u8; 32], +} + +impl TypeData { + fn new( + id: u8, + version: u32, + multipart_size: Option, + commands: &[u8], + ) -> Self { + let mut t = Self { + id, + version, + multipart_size, + commands: [0u8; 32], + }; + for c in commands { + let idx = *c as usize / 8; + let offs = c % 8; + + t.commands[idx] |= 1 << offs; + } + t + } +} + +// number of peers that we track negotiated parameters against. +const N_PEERS: usize = 8; + +// per-peer data on Negotiate Transfer Parameters results. +// +// The current implementation is spec-volatingly basic: we don't handle per-type +// data, but just update our negotiated size across all types. +struct NegotiatedTransfer { + eid: Eid, + // log2(size) + size: u8, +} + +/// Responder object for PLDM Messaging Control and Discovery (type 0) commands. +/// +/// The N const represents the number of PLDM subtype slots that we support. +/// The type-0 handler itself consumes one slot. +pub struct Responder { + tid: u8, + types: Vec, + // we can get our base PLDM responses into a single MCTP BTU + buf: [u8; 64], + + // negotiated sizes for each peer + negotiations: Vec, +} + +struct PldmCommandError(u8); + +impl From for PldmCommandError { + fn from(value: CCode) -> Self { + Self(value as u8) + } +} + +impl From for PldmCommandError { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl From for PldmCommandError { + fn from(err: DekuError) -> Self { + let cc = match err { + DekuError::Incomplete(_) => CCode::ERROR_INVALID_LENGTH, + DekuError::Parse(_) => CCode::ERROR_INVALID_DATA, + _ => CCode::ERROR, + }; + Self(cc as u8) + } +} + +type PldmCommandResult = core::result::Result; + +impl Responder { + /// Create a new responder. + pub fn new() -> Self { + let mut r = Self { + tid: TID_UNASSIGNED, + types: Vec::new(), + buf: [0u8; 64], + negotiations: Vec::new(), + }; + let t = TypeData::new( + 0, + 0xf1f1f000, + None, + &[Cmd::SetTID as u8, Cmd::GetTID as u8], + ); + let _ = r.types.push(t); + r + } + + /// Resgister a new PLDM type with this responder. + /// + /// This populates data returned by the base Get PLDM Types, Get PLDM + /// Versions and Get PLDM Commands responses. + pub fn register_type( + &mut self, + id: u8, + version: u32, + multipart_size: Option, + commands: &[u8], + ) -> Result<()> { + if id >= 64 { + Err(PldmError::InvalidArgument)?; + } + let log2_sz = match multipart_size { + Some(sz) => { + if sz < 256 || !sz.is_power_of_two() { + Err(PldmError::InvalidArgument)?; + } + Some(sz.ilog2() as u8) + } + None => None, + }; + let typ = TypeData::new(id, version, log2_sz, commands); + self.types.push(typ).map_err(|_| PldmError::NoSpace)?; + Ok(()) + } + + /// Query the previously-negotiated transfer size for the given EID + /// and PLDM type + /// + /// Returns None if no negotiation has occurred + pub fn negotiated_xfer_size( + &self, + peer: Eid, + _pldm_type: u8, + ) -> Option { + self.negotiations + .iter() + .find(|n| n.eid == peer) + .map(|n| 1 << n.size) + } + + /// Handle an incoming PLDM Messaging Control and Discovery request + pub async fn handle_async( + &mut self, + req: &PldmRequest<'_>, + mut resp_chan: impl AsyncRespChannel, + ) -> Result<()> { + if req.typ != PLDM_TYPE_CONTROL { + return Err(proto_error!("Unexpected pldm control request")); + } + + let eid = resp_chan.remote_eid(); + + let res = match Cmd::try_from(req.cmd) { + Ok(Cmd::SetTID) => self.cmd_set_tid(req), + Ok(Cmd::GetTID) => self.cmd_get_tid(req), + Ok(Cmd::GetPLDMVersion) => self.cmd_get_version(req), + Ok(Cmd::GetPLDMTypes) => self.cmd_get_types(req), + Ok(Cmd::GetPLDMCommands) => self.cmd_get_commands(req), + Ok(Cmd::NegotiateTransferParameters) => { + self.cmd_negotiate_transfer_parameters(req, eid) + } + _ => Err(CCode::ERROR_UNSUPPORTED_PLDM_CMD.into()), + }; + + let resp = res.unwrap_or_else(|e| { + let mut r = req.response_borrowed(&[]); + r.cc = e.0; + r + }); + + pldm_tx_resp_async(&mut resp_chan, &resp).await + } + + fn cmd_set_tid( + &mut self, + req: &PldmRequest, + ) -> PldmCommandResult> { + let data = &req.data; + let (_rest, sreq) = control::SetTIDReq::from_bytes((data, 0))?; + self.tid = sreq.tid; + + let resp = req.response_borrowed(&[]); + Ok(resp) + } + + fn cmd_get_tid( + &mut self, + req: &PldmRequest, + ) -> PldmCommandResult> { + let resp = control::GetTIDResp { tid: self.tid }; + + let len = resp.to_slice(&mut self.buf)?; + let resp = req.response_borrowed(&self.buf[0..len]); + Ok(resp) + } + + fn cmd_get_version( + &mut self, + req: &PldmRequest, + ) -> PldmCommandResult> { + let data = &req.data; + let (_rest, vreq) = control::GetPLDMVersionReq::from_bytes((data, 0))?; + + // Get First Part? + if vreq.xfer_op != 1 { + Err(control_ccode::INVALID_TRANSFER_OPERATION_FLAG)?; + } + + let typ = self + .types + .iter() + .find(|t| t.id == vreq.pldm_type) + .ok_or(control_ccode::INVALID_PLDM_TYPE_IN_REQUEST_DATA)?; + + let tmp = typ.version.to_le_bytes(); + let crc32 = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + let crc = crc32.checksum(&tmp); + + let resp = control::GetPLDMVersionResp { + next_handle: 0, + xfer_flag: 0x05, /* Start and End */ + version: typ.version, + crc, + }; + + let len = resp.to_slice(&mut self.buf)?; + let resp = req.response_borrowed(&self.buf[0..len]); + Ok(resp) + } + + fn cmd_get_types( + &mut self, + req: &PldmRequest, + ) -> PldmCommandResult> { + let mut resp = control::GetPLDMTypesResp { types: [0u8; 8] }; + + for typ in &self.types { + let idx = typ.id as usize / 8; + let offs = typ.id % 8; + if idx >= 8 { + continue; + } + resp.types[idx] |= 1 << offs; + } + + let len = resp.to_slice(&mut self.buf)?; + let resp = req.response_borrowed(&self.buf[0..len]); + Ok(resp) + } + + fn cmd_get_commands( + &mut self, + req: &PldmRequest, + ) -> PldmCommandResult> { + let data = &req.data; + let (_rest, creq) = control::GetPLDMCommandsReq::from_bytes((data, 0))?; + + let typ = self + .types + .iter() + .find(|t| t.id == creq.pldm_type) + .ok_or(control_ccode::INVALID_PLDM_TYPE_IN_REQUEST_DATA)?; + + if typ.version != creq.version { + return Err( + control_ccode::INVALID_PLDM_VERSION_IN_REQUEST_DATA.into() + ); + } + + let resp = control::GetPLDMCommandsResp { + commands: typ.commands, + }; + + let len = resp.to_slice(&mut self.buf)?; + let resp = req.response_borrowed(&self.buf[0..len]); + Ok(resp) + } + + fn cmd_negotiate_transfer_parameters( + &mut self, + req: &PldmRequest, + eid: Eid, + ) -> PldmCommandResult> { + let data = &req.data; + let (_rest, nreq) = + control::NegotiateTransferParametersReq::from_bytes((data, 0))?; + + let req_size = nreq.part_size; + if !req_size.is_power_of_two() || req_size < 256 { + Err(CCode::ERROR_INVALID_DATA)?; + } + + let req_size = req_size.ilog2() as u8; + let mut neg_size = req_size; + + let req_types = u64::from_le_bytes(nreq.protocols); + let mut neg_types = 0u64; + + for t in &self.types { + if t.id >= 64 { + continue; + } + let mask = 1 << t.id; + if req_types & mask == 0 { + continue; + } + if let Some(sz) = t.multipart_size { + neg_types |= mask; + neg_size = neg_size.min(sz); + } + } + + let negotiation = self.negotiations.iter_mut().find(|n| n.eid == eid); + + match negotiation { + Some(n) => n.size = neg_size, + None => { + let n = NegotiatedTransfer { + eid, + size: neg_size, + }; + self.negotiations.push(n).map_err(|_| CCode::ERROR)?; + } + } + + let resp = control::NegotiateTransferParametersResp { + part_size: 1u16 << req_size, + protocols: neg_types.to_le_bytes(), + }; + + let len = resp.to_slice(&mut self.buf)?; + let resp = req.response_borrowed(&self.buf[0..len]); + Ok(resp) + } +} + +impl Default for Responder { + fn default() -> Self { + Self::new() + } +} diff --git a/pldm/src/lib.rs b/pldm/src/lib.rs index 3bf1acc..a224186 100644 --- a/pldm/src/lib.rs +++ b/pldm/src/lib.rs @@ -4,7 +4,7 @@ * * Copyright (c) 2023 Code Construct */ -#![cfg_attr(not(any(feature = "std", test)), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] #![forbid(unsafe_code)] #![warn(missing_docs)] @@ -13,11 +13,22 @@ //! This crate implements some base communication primitives for PLDM, //! used to construct higher-level PLDM messaging applications. +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "alloc")] +use alloc::{string::String, vec::Vec}; + +#[allow(unused)] +use log::{debug, error, info, trace, warn}; + use core::fmt::{self, Debug}; +use deku::{no_std_io::ErrorKind, DekuError}; use num_derive::FromPrimitive; use mctp::MsgIC; +pub mod control; pub mod util; use util::*; @@ -37,6 +48,8 @@ pub enum PldmError { InvalidArgument, /// No buffer space available NoSpace, + /// Error completion code received + CompletionCode(u8), } impl core::fmt::Display for PldmError { @@ -46,6 +59,9 @@ impl core::fmt::Display for PldmError { Self::Mctp(s) => write!(f, "MCTP error: {s}"), Self::InvalidArgument => write!(f, "Invalid Argument"), Self::NoSpace => write!(f, "Insufficient buffer space available"), + Self::CompletionCode(c) => { + write!(f, "Error completion code {c:#02x} received") + } } } } @@ -65,6 +81,35 @@ impl From for PldmError { } } +impl From for PldmError { + fn from(err: DekuError) -> Self { + match err { + DekuError::Incomplete(_) => proto_error!("Incomplete input"), + DekuError::Io(ErrorKind::WriteZero) => { + trace!("Deku no space"); + PldmError::NoSpace + } + _ => { + trace!("Deku error: {err:?}"); + proto_error!("Deku error", "{err}") + } + } + } +} + +/// Convert a PLDM Completion code into a `PldmResult`. +/// +/// Success completion code returns `Ok`. +pub fn ccode_result(ccode: u8) -> PldmResult<()> { + match ccode { + 0 => Ok(()), + e => Err(PldmError::CompletionCode(e)), + } +} + +/// PLDM result type +pub type PldmResult = core::result::Result; + #[cfg(feature = "alloc")] type ErrStr = String; #[cfg(not(feature = "alloc"))] @@ -77,20 +122,24 @@ type ErrStr = &'static str; /// Example /// /// ``` +/// # extern crate alloc; /// # let iid = 1; /// # let actual_iid = 2; +/// # let buf = b"123"; /// use pldm::proto_error; /// proto_error!("Mismatching IID", "Expected {iid:02x}, received {actual_iid:02x}"); /// proto_error!("Rq bit wasn't expected"); +/// proto_error!("Short response", "{} bytes", buf.len()); /// ``` #[macro_export] #[cfg(feature = "alloc")] macro_rules! proto_error { - ($msg: expr, $desc_str: expr) => { - $crate::PldmError::Protocol(format!("{}. {}", $msg, $desc_str)) + ($msg:expr, $($desc_fmt:expr),* $(,)?) => { + $crate::PldmError::Protocol(alloc::format!("{}. {}", $msg, + alloc::format!($($desc_fmt),*))) }; - ($msg: expr) => { - $crate::PldmError::Protocol(format!("{}.", $msg)) + ($msg:expr) => { + $crate::PldmError::Protocol(alloc::format!("{}.", $msg)) }; } @@ -103,17 +152,23 @@ macro_rules! proto_error { /// ``` /// # let iid = 1; /// # let actual_iid = 2; +/// # let buf = b"123"; /// use pldm::proto_error; /// proto_error!("Mismatching IID", "Expected {iid:02x}, received {actual_iid:02x}"); /// proto_error!("Rq bit wasn't expected"); +/// proto_error!("Short response", "{} bytes", buf.len()); /// ``` #[macro_export] #[cfg(not(feature = "alloc"))] macro_rules! proto_error { - ($msg: expr, $desc_str: expr) => { - $crate::PldmError::Protocol($msg) + ($msg:expr, $($desc_fmt:expr),* $(,)?) => { + { + // Avoid unused variable warning + let _ = format_args!($($desc_fmt),*); + $crate::PldmError::Protocol($msg) + } }; - ($msg: expr) => { + ($msg:expr) => { $crate::PldmError::Protocol($msg) }; } @@ -133,6 +188,7 @@ pub enum CCode { ERROR_NOT_READY = 4, ERROR_UNSUPPORTED_PLDM_CMD = 5, ERROR_INVALID_PLDM_TYPE = 32, + ERROR_INVALID_TRANSFER_CONTEXT = 33, } /// Base PLDM request type @@ -185,7 +241,7 @@ impl<'a> PldmRequest<'a> { /// /// Convert this request to a response, using the instance, type and command /// from the original request. - pub fn response(&self) -> PldmResponse<'_> { + pub fn response(&self) -> PldmResponse<'static> { PldmResponse { iid: self.iid, typ: self.typ, @@ -240,10 +296,7 @@ impl<'a> PldmRequest<'a> { /// May fail if the message data is not parsable as a PLDM message. pub fn from_buf_borrowed(data: &'a [u8]) -> Result> { if data.len() < 3 { - return Err(proto_error!( - "Short request", - format!("{} bytes", data.len()) - )); + return Err(proto_error!("Short request", "{} bytes", data.len())); } let rq = (data[0] & 0x80) != 0; @@ -336,7 +389,8 @@ impl<'a> PldmResponse<'a> { if rx_buf.len() < 4 { return Err(proto_error!( "Short response", - format!("{} bytes", rx_buf.len()) + "{} bytes", + rx_buf.len() )); } @@ -362,6 +416,37 @@ impl<'a> PldmResponse<'a> { } } +fn check_req_resp_match(req: &PldmRequest, rsp: &PldmResponse) -> Result<()> { + if rsp.iid != req.iid { + return Err(proto_error!( + "Incorrect instance ID in reply", + "Expected 0x{:02x} got 0x{:02x}", + req.iid, + rsp.iid + )); + } + + if rsp.typ != req.typ { + return Err(proto_error!( + "Incorrect PLDM type in reply", + "Expected 0x{:02x} got 0x{:02x}", + req.typ, + rsp.typ + )); + } + + if rsp.cmd != req.cmd { + return Err(proto_error!( + "Incorrect PLDM command in reply", + "Expected 0x{:02x} got 0x{:02x}", + req.cmd, + rsp.cmd, + )); + } + + Ok(()) +} + /// Represents either a PLDM request or response message #[derive(Debug)] pub enum PldmMessage<'a> { @@ -398,28 +483,32 @@ pub fn pldm_xfer_buf<'buf>( pldm_tx_req(ep, &mut req)?; let rsp = pldm_rx_resp_borrowed(ep, rx_buf)?; + check_req_resp_match(&req, &rsp)?; - if rsp.iid != req.iid { - return Err(proto_error!( - "Incorrect instance ID in reply", - format!("Expected 0x{:02x} got 0x{:02x}", req.iid, rsp.iid) - )); - } + Ok(rsp) +} - if rsp.typ != req.typ { - return Err(proto_error!( - "Incorrect PLDM type in reply", - format!("Expected 0x{:02x} got 0x{:02x}", req.typ, rsp.typ) - )); - } +/// Async PLDM transfer +/// +/// Sends a Request, and waits for a response. This is generally +/// used by PLDM Requesters, which issue commands to Responders. +/// +/// This function requires an external `rx_buf`. +pub async fn pldm_xfer_buf_async<'buf>( + ep: &mut impl mctp::AsyncReqChannel, + mut req: PldmRequest<'_>, + rx_buf: &'buf mut [u8], +) -> Result> { + pldm_tx_req_async(ep, &mut req).await?; - if rsp.cmd != req.cmd { - return Err(proto_error!( - "Incorrect PLDM command in reply", - format!("Expected 0x{:02x} got 0x{:02x}", req.cmd, rsp.cmd) - )); + let (_typ, ic, rx_buf) = ep.recv(rx_buf).await?; + if ic.0 { + return Err(proto_error!("IC bit set")); } + let rsp = PldmResponse::from_buf_borrowed(rx_buf)?; + check_req_resp_match(&req, &rsp)?; + ccode_result(rsp.cc)?; Ok(rsp) } @@ -514,3 +603,33 @@ pub fn pldm_tx_req( ep.send_vectored(mctp::MCTP_TYPE_PLDM, MsgIC(false), txs)?; Ok(()) } + +/// Transmit an outgoing PLDM response, asynchronously +pub async fn pldm_tx_resp_async( + ep: &mut impl mctp::AsyncRespChannel, + resp: &PldmResponse<'_>, +) -> Result<()> { + let tx_buf = [resp.iid, resp.typ, resp.cmd, resp.cc]; + let txs = &[&tx_buf, resp.data.as_ref()]; + ep.send_vectored(MsgIC(false), txs).await?; + Ok(()) +} + +/// Transmit an outgoing PLDM response, asynchronously +/// +/// The iid will be updated in `req`. +pub async fn pldm_tx_req_async( + ep: &mut impl mctp::AsyncReqChannel, + req: &mut PldmRequest<'_>, +) -> Result<()> { + // TODO IID allocation + const REQ_IID: u8 = 0; + req.iid = REQ_IID; + + let tx_buf = [1 << 7 | req.iid, req.typ & 0x3f, req.cmd]; + + let txs = &[&tx_buf, req.data.as_ref()]; + ep.send_vectored(mctp::MCTP_TYPE_PLDM, MsgIC(false), txs) + .await?; + Ok(()) +} diff --git a/pldm/src/util.rs b/pldm/src/util.rs index 01cd531..dd80d97 100644 --- a/pldm/src/util.rs +++ b/pldm/src/util.rs @@ -6,6 +6,9 @@ //! Helper functions use core::mem::size_of; +#[cfg(feature = "alloc")] +use alloc::vec::Vec; + /// Holds either an allocated `Vec` or borrowed slice. /// /// Can be constructed using `.into()` on a `Vec` (`std` feature) or `&[u8]` (always) @@ -75,7 +78,7 @@ impl<'a> SliceWriter<'a> { } /// Returns the written buffer - pub fn done(&mut self) -> &mut [u8] { + pub fn done(self) -> &'a mut [u8] { &mut self.s[..self.pos] } @@ -88,7 +91,11 @@ impl<'a> SliceWriter<'a> { Some(s.len()) } - fn push_le(&mut self, v: S) -> Option + /// Pushes a value into the output buffer, little endian. + /// + /// Returns the length written or `None` on insufficient space. + #[must_use] + pub fn push_le(&mut self, v: S) -> Option where S: num_traits::ToBytes, { @@ -144,6 +151,19 @@ impl<'a> SliceWriter<'a> { Some(l) } + /// Pushes slice into the output buffer, little endian. + /// + /// Returns the number of elements written or `None` on insufficient space. + #[must_use] + pub fn extend(&mut self, v: &[u8]) -> Option { + self.push_with(|s| { + s.get_mut(..v.len()).map(|s| { + s.copy_from_slice(v); + s.len() + }) + }) + } + /// Pushes data into the output buffer provided by a function, with a length prefix. /// /// The `write` closure writes into the output buffer (with prefix space left), @@ -278,5 +298,16 @@ mod tests { }); assert_eq!(r, Some(2)); assert_eq!(x, [3, 4, 99u8]); + + let mut x = [0u8; 5]; + let mut w = SliceWriter::new(&mut x); + let r = w.extend(&[1, 2, 3]); + assert_eq!(r, Some(3)); + assert_eq!(x, [1, 2, 3, 0, 0u8]); + + let mut x = [0u8; 2]; + let mut w = SliceWriter::new(&mut x); + let r = w.extend(&[1, 2, 3]); + assert_eq!(r, None); } }