blob: a9eb34c676d4f226bee6e364f1dec48104a1bc9b [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! MCTP transport adapter for PLDM messages.
//!
//! [`MctpPldmTransport`] wraps a [`Stack`] (backed by any [`MctpClient`]) and
//! provides PLDM-specific send/receive helpers that manage the MCTP framing
//! byte automatically.
//!
//! ## Buffer layout
//!
//! All methods in this module use the same flat-buffer convention as the rest
//! of this crate:
//!
//! ```text
//! buf[0] : MCTP message-type (0x01) – written/verified by this layer
//! buf[1..] : PLDM message (header + data)
//! ```
//!
//! Callers only deal with PLDM bytes; the framing byte is inserted or stripped
//! transparently.
use openprot_mctp_api::stack::StackListener;
use openprot_mctp_api::{MctpClient, MctpListener, MctpReqChannel, MctpRespChannel, Stack};
use pldm_common::util::mctp_transport::MCTP_PLDM_MSG_TYPE;
use crate::error::{PldmMemError, PldmServiceError};
/// MCTP transport adapter for PLDM messages.
///
/// Wraps a [`Stack`] backed by any [`MctpClient`] implementation and provides
/// PLDM-specific send/receive helpers that manage the MCTP framing byte
/// (`buf[0]` = `0x01`) automatically.
///
/// # Example
///
/// ```rust,ignore
/// use openprot_pldm_service::transport::MctpPldmTransport;
/// use openprot_mctp_client::IpcMctpClient;
///
/// let transport = MctpPldmTransport::new(IpcMctpClient::new(handle::MCTP));
///
/// // Use the underlying stack directly if needed.
/// transport.stack().set_eid(8).unwrap();
///
/// // Send a PLDM request; the caller fills buf[1..1+pldm_len] first.
/// let pldm_resp_len = transport
/// .send_request(remote_eid, pldm_len, &mut buf, 5_000)
/// .unwrap();
///
/// // Receive and respond to one inbound PLDM request via a handler closure.
/// transport
/// .recv_and_respond(&mut buf, 5_000, |framed_buf, _req_total_len, _source_eid| {
/// // framed_buf[0] == 0x01, framed_buf[1..] is the PLDM payload.
/// // Process and write the response in-place; return total bytes.
/// my_cmd_interface.handle_responder_msg(framed_buf)
/// .map_err(PldmServiceError::MsgHandler)
/// })
/// .unwrap();
/// ```
pub struct MctpPldmTransport<C: MctpClient> {
stack: Stack<C>,
}
impl<C: MctpClient> MctpPldmTransport<C> {
/// Create a new PLDM transport wrapping the given [`MctpClient`].
pub fn new(client: C) -> Self {
MctpPldmTransport {
stack: Stack::new(client),
}
}
/// Get a reference to the underlying MCTP [`Stack`].
///
/// Useful when the raw stack is needed (e.g. to set the local EID).
pub fn stack(&self) -> &Stack<C> {
&self.stack
}
/// Send a PLDM request to `remote_eid` and receive the response.
///
/// The caller must place the PLDM request bytes in `buf[1..1+pldm_len]`
/// before calling this method. `buf[0]` is overwritten with the MCTP
/// PLDM message-type byte (`0x01`).
///
/// On success, the PLDM response bytes are written into `buf[1..]` and
/// the number of response bytes is returned.
///
/// A `timeout_millis` of `0` blocks indefinitely.
///
/// # Errors
///
/// Returns [`PldmServiceError::PldmMem`] if `buf` is too small to hold
/// the request or if an arithmetic overflow would occur.
/// Returns [`PldmServiceError::Mctp`] on any MCTP transport error.
pub fn send_request(
&self,
remote_eid: u8,
pldm_len: usize,
buf: &mut [u8],
timeout_millis: u32,
) -> Result<usize, PldmServiceError> {
// Stamp the framing byte even though the MCTP layer manages it; this
// keeps buf consistent for callers that inspect buf[0] afterward.
match buf.first_mut() {
Some(b) => *b = MCTP_PLDM_MSG_TYPE,
None => return Err(PldmServiceError::PldmMem(PldmMemError::MalformedBuffer)),
}
// Open an outbound request channel.
let mut req_channel = self
.stack
.req(remote_eid, timeout_millis)
.map_err(PldmServiceError::Mctp)?;
// Send the PLDM payload (buf[1..1+pldm_len]). The MCTP layer adds
// its own framing, so we exclude buf[0].
let req_end = pldm_len
.checked_add(1)
.ok_or(PldmServiceError::PldmMem(PldmMemError::OverflowMaxSize))?;
let req_payload = buf
.get(1..req_end)
.ok_or(PldmServiceError::PldmMem(PldmMemError::BufferTooSmall))?;
req_channel
.send(MCTP_PLDM_MSG_TYPE, req_payload)
.map_err(PldmServiceError::Mctp)?;
// Receive the PLDM response into buf[1..].
let recv_buf = buf
.get_mut(1..)
.ok_or(PldmServiceError::PldmMem(PldmMemError::BufferTooSmall))?;
let (meta, _) = req_channel.recv(recv_buf).map_err(PldmServiceError::Mctp)?;
Ok(meta.payload_size)
}
/// Receive one incoming PLDM request, frame it in `buf`, and invoke
/// `handler` to produce a response.
///
/// The PLDM payload is received into `buf[1..]`; `buf[0]` is set to the
/// MCTP PLDM type byte (`0x01`). `handler` receives the entire `buf`
/// (with the request framed at `buf[..1+payload_size]`) so it has room to
/// write a response that is larger than the request, processing it in
/// place. `handler` is called with the full framed buffer, the total
/// request length (including `buf[0]`), and the source EID of the
/// request (`RecvMetadata::remote_eid`), so callers can filter requests
/// by sender. It must return the total number of bytes written for the
/// response (including the type byte at `buf[0]`), or `0` to indicate no
/// response should be sent (e.g. the request came from an unexpected
/// EID). Bytes `buf[1..resp_total_len]` are sent back to the requester.
///
/// A `timeout_millis` of `0` blocks indefinitely.
///
/// This registers and drops a fresh listener on every call. A caller that
/// invokes this repeatedly in a tight loop with no other MCTP traffic in
/// between is unaffected, but a caller that interleaves other MCTP
/// activity on the same endpoint between calls (e.g.
/// [`FirmwareDevice::run_terminus`](crate::firmware_device::FirmwareDevice::run_terminus),
/// which interleaves FD-initiated requests) should use
/// [`responder_listener`](Self::responder_listener) and
/// [`respond_once`](Self::respond_once) instead, to avoid a window with no
/// listener registered in which an inbound request could be dropped by
/// the underlying MCTP stack.
///
/// # Errors
///
/// Returns [`PldmServiceError::PldmMem(PldmMemError::BufferTooSmall)`] if `buf` is too small.
/// Returns [`PldmServiceError::Mctp`] on any MCTP transport error.
/// Propagates any error returned by `handler`.
pub fn recv_and_respond<F>(
&self,
buf: &mut [u8],
timeout_millis: u32,
handler: F,
) -> Result<(), PldmServiceError>
where
F: FnOnce(&mut [u8], usize, u8) -> Result<usize, PldmServiceError>,
{
let mut listener = self.responder_listener(timeout_millis)?;
self.respond_once(&mut listener, buf, handler)
}
/// Register a persistent listener for inbound PLDM requests.
///
/// Unlike [`recv_and_respond`](Self::recv_and_respond), which registers
/// and drops a listener handle on every call, the returned listener can
/// be reused across many [`respond_once`](Self::respond_once) calls
/// (adjusting its timeout with [`StackListener::set_timeout`] as needed).
/// This matters because the underlying MCTP stack requires an active
/// listener registration to accept an inbound request of a given
/// message type; a caller that interleaves other MCTP traffic (e.g.
/// FD-initiated requests) between polls would otherwise have a window
/// with no listener registered in which an inbound UA command could be
/// silently dropped.
///
/// A `timeout_millis` of `0` blocks indefinitely.
pub fn responder_listener(
&self,
timeout_millis: u32,
) -> Result<StackListener<'_, C>, PldmServiceError> {
self.stack
.listener(MCTP_PLDM_MSG_TYPE, timeout_millis)
.map_err(PldmServiceError::Mctp)
}
/// Receive one incoming PLDM request on an already-registered `listener`
/// (obtained via [`responder_listener`](Self::responder_listener)), frame
/// it in `buf`, and invoke `handler` to produce a response.
///
/// See [`recv_and_respond`](Self::recv_and_respond) for the buffer and
/// handler contract; this method has the same semantics but reuses an
/// existing listener instead of registering a new one.
///
/// # Errors
///
/// Returns [`PldmServiceError::PldmMem(PldmMemError::BufferTooSmall)`] if `buf` is too small.
/// Returns [`PldmServiceError::Mctp`] on any MCTP transport error.
/// Propagates any error returned by `handler`.
pub fn respond_once<F>(
&self,
listener: &mut StackListener<'_, C>,
buf: &mut [u8],
handler: F,
) -> Result<(), PldmServiceError>
where
F: FnOnce(&mut [u8], usize, u8) -> Result<usize, PldmServiceError>,
{
// Receive into buf[1..]; discard the payload sub-slice to end the
// mutable borrow before we touch buf[0].
let recv_buf = buf
.get_mut(1..)
.ok_or(PldmServiceError::PldmMem(PldmMemError::BufferTooSmall))?;
let (meta, mut resp_channel) = listener
.recv(recv_buf)
.map(|(m, _, r)| (m, r))
.map_err(PldmServiceError::Mctp)?;
let payload_size = meta.payload_size;
// Stamp the framing byte.
match buf.first_mut() {
Some(b) => *b = MCTP_PLDM_MSG_TYPE,
None => return Err(PldmServiceError::PldmMem(PldmMemError::MalformedBuffer)),
}
// Ensure the buffer is at least large enough to hold the framed
// request before handing it off.
let req_total_len = payload_size
.checked_add(1)
.filter(|&total_len| total_len <= buf.len())
.ok_or(PldmServiceError::PldmMem(PldmMemError::BufferTooSmall))?;
// Invoke the handler to process the request in-place. The handler is
// given the whole buffer so the response may exceed the request size.
let resp_total_len = handler(buf, req_total_len, meta.remote_eid)?;
// A handler returning 0 means it deliberately chose not to respond
// (e.g. the request came from an EID it does not serve).
if resp_total_len == 0 {
return Ok(());
}
// Send the response, excluding the MCTP type byte that the transport
// layer manages separately.
let resp_payload = buf
.get(1..resp_total_len)
.ok_or(PldmServiceError::PldmMem(PldmMemError::BufferTooSmall))?;
resp_channel
.send(resp_payload)
.map_err(PldmServiceError::Mctp)
}
}