| // Licensed under the Apache-2.0 license |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| //! PLDM IPC server: the FD side of the IPC channel. |
| //! |
| //! Decodes orchestrator requests, dispatches to an `FdIpcHandler` |
| //! implementation, and encodes responses. `FdIpcServer` is the |
| //! `util_service::Dispatch` impl, so the same server answers behind a |
| //! kernel channel in production and inside `util_service::Loopback` in |
| //! host tests. |
| |
| #![no_std] |
| |
| use pldm_api::wire::{self, PldmOp}; |
| use pldm_api::{FdStatus, RejectReason, ResponseCode, WireError}; |
| use util_service::{Dispatch, DispatchError}; |
| |
| /// What the FD does in response to each orchestrator operation. |
| /// |
| /// One method per opcode. All return `Result<(), ResponseCode>` except |
| /// `query_status`, which returns the FD's current condition. The server |
| /// encodes the result into the response buffer; the handler never |
| /// touches wire bytes. |
| /// |
| /// Every method must return immediately. Record the decision, flip |
| /// state, return. The FD shares its loop with MCTP traffic from the |
| /// UA, so a handler that blocks stalls UA traffic. |
| pub trait FdIpcHandler { |
| fn accept_offer(&mut self, staging_base: u32) -> Result<(), ResponseCode>; |
| fn reject_offer(&mut self, reason: RejectReason) -> Result<(), ResponseCode>; |
| fn perform_verify(&mut self) -> Result<(), ResponseCode>; |
| fn reject_verify(&mut self, reason: RejectReason) -> Result<(), ResponseCode>; |
| fn perform_apply(&mut self) -> Result<(), ResponseCode>; |
| fn reject_apply(&mut self, reason: RejectReason) -> Result<(), ResponseCode>; |
| fn query_status(&mut self) -> Result<FdStatus, ResponseCode>; |
| fn perform_activate(&mut self) -> Result<(), ResponseCode>; |
| fn reject_activate(&mut self, reason: RejectReason) -> Result<(), ResponseCode>; |
| fn ack_cancel(&mut self) -> Result<(), ResponseCode>; |
| fn perform_svn_commit(&mut self) -> Result<(), ResponseCode>; |
| fn reject_svn_commit(&mut self, reason: RejectReason) -> Result<(), ResponseCode>; |
| } |
| |
| /// Decode one request, call the handler, encode the response. |
| /// |
| /// Returns the number of bytes written to `response`. An unknown opcode |
| /// answers `InvalidOp` and a request frame that does not decode answers |
| /// `MalformedRequest`, so a caller error is never reported as a fault in |
| /// the FD. `InternalError` is left for the one server-side case: the |
| /// response does not fit the buffer the caller gave, but an error frame |
| /// does. A `response` too small for even that error frame has nothing to |
| /// send back. |
| pub fn dispatch<F: FdIpcHandler>( |
| handler: &mut F, |
| request: &[u8], |
| response: &mut [u8], |
| ) -> Result<usize, DispatchError> { |
| let code = match dispatch_inner(handler, request, response) { |
| Ok(n) => return Ok(n), |
| Err(WireError::InvalidOpcode(_)) => ResponseCode::InvalidOp, |
| Err(WireError::Truncated | WireError::PayloadTooLarge | WireError::InvalidValue(_)) => { |
| ResponseCode::MalformedRequest |
| } |
| Err(WireError::BufferTooSmall) => ResponseCode::InternalError, |
| }; |
| wire::encode_error_response(response, code).map_err(|_| DispatchError::ResponseTooLarge) |
| } |
| |
| /// An `FdIpcHandler` as a server the shared transports can drive. |
| /// |
| /// The newtype exists because `Dispatch` is a foreign trait: it cannot |
| /// be implemented for every `F: FdIpcHandler` directly. |
| pub struct FdIpcServer<F> { |
| handler: F, |
| } |
| |
| impl<F> FdIpcServer<F> { |
| pub const fn new(handler: F) -> Self { |
| Self { handler } |
| } |
| |
| /// The handler, to assert on what the requests did to it. |
| pub fn handler(&self) -> &F { |
| &self.handler |
| } |
| } |
| |
| impl<F: FdIpcHandler> Dispatch for FdIpcServer<F> { |
| fn dispatch(&mut self, request: &[u8], response: &mut [u8]) -> Result<usize, DispatchError> { |
| dispatch(&mut self.handler, request, response) |
| } |
| } |
| |
| fn dispatch_inner<F: FdIpcHandler>( |
| handler: &mut F, |
| request: &[u8], |
| response: &mut [u8], |
| ) -> Result<usize, WireError> { |
| let header = wire::decode_request_header(request)?; |
| let op = header |
| .operation() |
| .ok_or(WireError::InvalidOpcode(header.op))?; |
| if request.len() > wire::expected_request_len(op) { |
| return Err(WireError::PayloadTooLarge); |
| } |
| let args = wire::get_request_args(request); |
| |
| match op { |
| PldmOp::AcceptOffer => { |
| let base = wire::get_accept_offer_base(args)?; |
| encode_unit_result(response, handler.accept_offer(base)) |
| } |
| PldmOp::RejectOffer => { |
| let reason = wire::get_reject_reason(args)?; |
| encode_unit_result(response, handler.reject_offer(reason)) |
| } |
| PldmOp::PerformVerify => encode_unit_result(response, handler.perform_verify()), |
| PldmOp::RejectVerify => { |
| let reason = wire::get_reject_reason(args)?; |
| encode_unit_result(response, handler.reject_verify(reason)) |
| } |
| PldmOp::PerformApply => encode_unit_result(response, handler.perform_apply()), |
| PldmOp::RejectApply => { |
| let reason = wire::get_reject_reason(args)?; |
| encode_unit_result(response, handler.reject_apply(reason)) |
| } |
| PldmOp::QueryStatus => match handler.query_status() { |
| Ok(status) => wire::encode_status_response(response, &status), |
| Err(code) => wire::encode_error_response(response, code), |
| }, |
| PldmOp::PerformActivate => encode_unit_result(response, handler.perform_activate()), |
| PldmOp::RejectActivate => { |
| let reason = wire::get_reject_reason(args)?; |
| encode_unit_result(response, handler.reject_activate(reason)) |
| } |
| PldmOp::AckCancel => encode_unit_result(response, handler.ack_cancel()), |
| PldmOp::PerformSvnCommit => encode_unit_result(response, handler.perform_svn_commit()), |
| PldmOp::RejectSvnCommit => { |
| let reason = wire::get_reject_reason(args)?; |
| encode_unit_result(response, handler.reject_svn_commit(reason)) |
| } |
| } |
| } |
| |
| fn encode_unit_result( |
| response: &mut [u8], |
| result: Result<(), ResponseCode>, |
| ) -> Result<usize, WireError> { |
| match result { |
| Ok(()) => wire::encode_success_response(response), |
| Err(code) => wire::encode_error_response(response, code), |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use pldm_api::status::TransferMode; |
| use pldm_api::wire::{RequestHeader, MAX_RESPONSE_SIZE}; |
| |
| /// Mock handler that records calls and returns canned responses. |
| struct MockFd { |
| last_op: Option<&'static str>, |
| status: FdStatus, |
| next_error: Option<ResponseCode>, |
| } |
| |
| impl MockFd { |
| fn new() -> Self { |
| Self { |
| last_op: None, |
| status: FdStatus::Idle { reason: 0 }, |
| next_error: None, |
| } |
| } |
| |
| fn returning_error(code: ResponseCode) -> Self { |
| Self { |
| last_op: None, |
| status: FdStatus::Idle { reason: 0 }, |
| next_error: Some(code), |
| } |
| } |
| |
| fn check(&mut self, name: &'static str) -> Result<(), ResponseCode> { |
| self.last_op = Some(name); |
| match self.next_error.take() { |
| Some(code) => Err(code), |
| None => Ok(()), |
| } |
| } |
| } |
| |
| impl FdIpcHandler for MockFd { |
| fn accept_offer(&mut self, _base: u32) -> Result<(), ResponseCode> { |
| self.check("accept_offer") |
| } |
| fn reject_offer(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| self.check("reject_offer") |
| } |
| fn perform_verify(&mut self) -> Result<(), ResponseCode> { |
| self.check("perform_verify") |
| } |
| fn reject_verify(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| self.check("reject_verify") |
| } |
| fn perform_apply(&mut self) -> Result<(), ResponseCode> { |
| self.check("perform_apply") |
| } |
| fn reject_apply(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| self.check("reject_apply") |
| } |
| fn query_status(&mut self) -> Result<FdStatus, ResponseCode> { |
| self.last_op = Some("query_status"); |
| match self.next_error.take() { |
| Some(code) => Err(code), |
| None => Ok(self.status), |
| } |
| } |
| fn perform_activate(&mut self) -> Result<(), ResponseCode> { |
| self.check("perform_activate") |
| } |
| fn reject_activate(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| self.check("reject_activate") |
| } |
| fn ack_cancel(&mut self) -> Result<(), ResponseCode> { |
| self.check("ack_cancel") |
| } |
| fn perform_svn_commit(&mut self) -> Result<(), ResponseCode> { |
| self.check("perform_svn_commit") |
| } |
| fn reject_svn_commit(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| self.check("reject_svn_commit") |
| } |
| } |
| |
| fn roundtrip_success( |
| encode: impl FnOnce(&mut [u8]) -> Result<usize, WireError>, |
| expected_op: &'static str, |
| ) { |
| let mut req = [0u8; 16]; |
| let req_len = encode(&mut req).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp).unwrap(); |
| assert_eq!(fd.last_op, Some(expected_op)); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success(), "expected success for {expected_op}"); |
| } |
| |
| #[test] |
| fn accept_offer_dispatches() { |
| roundtrip_success( |
| |buf| wire::encode_accept_offer(buf, 0x2000_0000), |
| "accept_offer", |
| ); |
| } |
| |
| #[test] |
| fn reject_offer_dispatches() { |
| roundtrip_success( |
| |buf| wire::encode_reject_offer(buf, RejectReason::PolicyViolation), |
| "reject_offer", |
| ); |
| } |
| |
| #[test] |
| fn perform_verify_dispatches() { |
| roundtrip_success(wire::encode_perform_verify, "perform_verify"); |
| } |
| |
| #[test] |
| fn reject_verify_dispatches() { |
| roundtrip_success( |
| |buf| wire::encode_reject_verify(buf, RejectReason::Isolated), |
| "reject_verify", |
| ); |
| } |
| |
| #[test] |
| fn perform_apply_dispatches() { |
| roundtrip_success(wire::encode_perform_apply, "perform_apply"); |
| } |
| |
| #[test] |
| fn reject_apply_dispatches() { |
| roundtrip_success( |
| |buf| wire::encode_reject_apply(buf, RejectReason::PolicyViolation), |
| "reject_apply", |
| ); |
| } |
| |
| #[test] |
| fn perform_activate_dispatches() { |
| roundtrip_success(wire::encode_perform_activate, "perform_activate"); |
| } |
| |
| #[test] |
| fn reject_activate_dispatches() { |
| roundtrip_success( |
| |buf| wire::encode_reject_activate(buf, RejectReason::Busy), |
| "reject_activate", |
| ); |
| } |
| |
| #[test] |
| fn ack_cancel_dispatches() { |
| roundtrip_success(wire::encode_ack_cancel, "ack_cancel"); |
| } |
| |
| #[test] |
| fn perform_svn_commit_dispatches() { |
| roundtrip_success(wire::encode_perform_svn_commit, "perform_svn_commit"); |
| } |
| |
| #[test] |
| fn reject_svn_commit_dispatches() { |
| roundtrip_success( |
| |buf| wire::encode_reject_svn_commit(buf, RejectReason::PolicyViolation), |
| "reject_svn_commit", |
| ); |
| } |
| |
| #[test] |
| fn query_status_returns_fd_state() { |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| fd.status = FdStatus::OfferPending { |
| target: 0x0001, |
| total: 0x0010_0000, |
| mode: TransferMode::InTransport, |
| svn_delayed: true, |
| }; |
| let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp).unwrap(); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success()); |
| let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); |
| let status = FdStatus::decode(payload).unwrap(); |
| assert_eq!(status, fd.status); |
| } |
| |
| #[test] |
| fn handler_error_becomes_error_response() { |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_perform_verify(&mut req).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::returning_error(ResponseCode::WrongPhase); |
| let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp).unwrap(); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(!h.is_success()); |
| assert_eq!(h.response_code(), ResponseCode::WrongPhase); |
| } |
| |
| #[test] |
| fn unknown_opcode_returns_invalid_op() { |
| let mut req = [0u8; 16]; |
| let h = RequestHeader { |
| op: 0xFF, |
| flags: 0, |
| generation: 0, |
| }; |
| req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp).unwrap(); |
| let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(!rh.is_success()); |
| assert_eq!(rh.response_code(), ResponseCode::InvalidOp); |
| assert_eq!(fd.last_op, None); |
| } |
| |
| #[test] |
| fn truncated_request_returns_malformed_request() { |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &[0u8; 2], &mut resp).unwrap(); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert_eq!(h.response_code(), ResponseCode::MalformedRequest); |
| } |
| |
| #[test] |
| fn reject_verify_missing_reason_returns_malformed_request() { |
| let mut req = [0u8; 16]; |
| let h = RequestHeader { |
| op: PldmOp::RejectVerify as u8, |
| flags: 0, |
| generation: 0, |
| }; |
| req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp).unwrap(); |
| let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert_eq!(rh.response_code(), ResponseCode::MalformedRequest); |
| assert_eq!(fd.last_op, None); |
| } |
| |
| // RejectOffer used to be header-only, so a frame with no reason is a |
| // frame from the old format, not just a truncated one. |
| #[test] |
| fn reject_offer_missing_reason_returns_malformed_request() { |
| let mut req = [0u8; 16]; |
| let h = RequestHeader { |
| op: PldmOp::RejectOffer as u8, |
| flags: 0, |
| generation: 0, |
| }; |
| req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp).unwrap(); |
| let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert_eq!(rh.response_code(), ResponseCode::MalformedRequest); |
| assert_eq!(fd.last_op, None); |
| } |
| |
| #[test] |
| fn reject_verify_bad_reason_returns_malformed_request() { |
| let mut req = [0u8; 16]; |
| let h = RequestHeader { |
| op: PldmOp::RejectVerify as u8, |
| flags: 0, |
| generation: 0, |
| }; |
| req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); |
| req[RequestHeader::SIZE] = 0xFF; |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp).unwrap(); |
| let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert_eq!(rh.response_code(), ResponseCode::MalformedRequest); |
| assert_eq!(fd.last_op, None); |
| } |
| |
| #[test] |
| fn accept_offer_truncated_args_returns_malformed_request() { |
| let mut req = [0u8; 16]; |
| let h = RequestHeader { |
| op: PldmOp::AcceptOffer as u8, |
| flags: 0, |
| generation: 0, |
| }; |
| req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); |
| // Only 1 byte of args instead of the 4 needed for the base address. |
| req[RequestHeader::SIZE] = 0x20; |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp).unwrap(); |
| let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert_eq!(rh.response_code(), ResponseCode::MalformedRequest); |
| assert_eq!(fd.last_op, None); |
| } |
| |
| #[test] |
| fn a_query_status_frame_with_trailing_slack_is_rejected() { |
| // 12 bytes is MAX_REQUEST_SIZE, so the old length gate let this |
| // through and the decoder ignored the four extra bytes. |
| let mut req = [0u8; wire::MAX_REQUEST_SIZE]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| assert!(req_len < wire::MAX_REQUEST_SIZE); |
| |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let mut fd = MockFd::new(); |
| let resp_len = dispatch(&mut fd, &req, &mut resp).unwrap(); |
| |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(!h.is_success()); |
| assert_eq!(h.response_code(), ResponseCode::MalformedRequest); |
| assert_eq!(fd.last_op, None); |
| } |
| } |
| |
| #[cfg(test)] |
| mod loopback_tests { |
| use super::*; |
| use pldm_api::wire::MAX_REQUEST_SIZE; |
| use util_service::{AsyncTransport, Loopback, TransportError}; |
| |
| /// Start one request and poll the response out, as the client layer |
| /// does; the loopback always has it ready on the first poll. |
| fn round_trip<F: FdIpcHandler>( |
| transport: &mut Loopback<FdIpcServer<F>, MAX_RESPONSE_SIZE>, |
| req: &[u8], |
| resp: &mut [u8], |
| ) -> usize { |
| transport.start(req).unwrap(); |
| transport.poll(resp).unwrap().unwrap() |
| } |
| use pldm_api::status::TransferMode; |
| use pldm_api::wire::{self, MAX_RESPONSE_SIZE}; |
| use pldm_api::{FdStatus, RejectReason, ResponseCode}; |
| |
| /// Minimal handler for loopback tests. |
| struct StubFd { |
| status: FdStatus, |
| } |
| |
| impl StubFd { |
| fn idle() -> Self { |
| Self { |
| status: FdStatus::Idle { reason: 0 }, |
| } |
| } |
| |
| fn with_offer() -> Self { |
| Self { |
| status: FdStatus::OfferPending { |
| target: 0x0001, |
| total: 0x0010_0000, |
| mode: TransferMode::InTransport, |
| svn_delayed: false, |
| }, |
| } |
| } |
| |
| fn at(status: FdStatus) -> Self { |
| Self { status } |
| } |
| } |
| |
| impl FdIpcHandler for StubFd { |
| fn accept_offer(&mut self, _base: u32) -> Result<(), ResponseCode> { |
| self.status = FdStatus::ReadyXfer; |
| Ok(()) |
| } |
| fn reject_offer(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| self.status = FdStatus::Idle { reason: 0 }; |
| Ok(()) |
| } |
| fn perform_verify(&mut self) -> Result<(), ResponseCode> { |
| self.status = FdStatus::ApplyPending; |
| Ok(()) |
| } |
| fn reject_verify(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| Ok(()) |
| } |
| fn perform_apply(&mut self) -> Result<(), ResponseCode> { |
| self.status = FdStatus::ActivationPending; |
| Ok(()) |
| } |
| fn reject_apply(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| Ok(()) |
| } |
| fn query_status(&mut self) -> Result<FdStatus, ResponseCode> { |
| Ok(self.status) |
| } |
| fn perform_activate(&mut self) -> Result<(), ResponseCode> { |
| self.status = FdStatus::Idle { reason: 0 }; |
| Ok(()) |
| } |
| fn reject_activate(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| Ok(()) |
| } |
| fn ack_cancel(&mut self) -> Result<(), ResponseCode> { |
| self.status = FdStatus::Idle { reason: 0 }; |
| Ok(()) |
| } |
| fn perform_svn_commit(&mut self) -> Result<(), ResponseCode> { |
| self.status = FdStatus::Idle { reason: 0 }; |
| Ok(()) |
| } |
| fn reject_svn_commit(&mut self, _reason: RejectReason) -> Result<(), ResponseCode> { |
| Ok(()) |
| } |
| } |
| |
| #[test] |
| fn query_status_through_loopback() { |
| let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::idle())); |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success()); |
| let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); |
| let status = FdStatus::decode(payload).unwrap(); |
| assert_eq!(status, FdStatus::Idle { reason: 0 }); |
| } |
| |
| #[test] |
| fn accept_offer_then_query_shows_ready_xfer() { |
| let mut transport = |
| Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::with_offer())); |
| |
| // Accept the offer. |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_accept_offer(&mut req, 0x2000_0000).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success()); |
| |
| // Query status: should now be ReadyXfer. |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); |
| let status = FdStatus::decode(payload).unwrap(); |
| assert_eq!(status, FdStatus::ReadyXfer); |
| } |
| |
| #[test] |
| fn reject_offer_then_query_shows_idle() { |
| let mut transport = |
| Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::with_offer())); |
| |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_reject_offer(&mut req, RejectReason::PolicyViolation).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success()); |
| |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); |
| let status = FdStatus::decode(payload).unwrap(); |
| assert_eq!(status, FdStatus::Idle { reason: 0 }); |
| } |
| |
| #[test] |
| fn start_while_pending_is_wrong_state() { |
| let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::idle())); |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| |
| transport.start(&req[..req_len]).unwrap(); |
| assert_eq!( |
| transport.start(&req[..req_len]), |
| Err(TransportError::WrongState) |
| ); |
| } |
| |
| #[test] |
| fn poll_without_start_is_wrong_state() { |
| let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::idle())); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); |
| } |
| |
| #[test] |
| fn cancel_releases_the_round_trip() { |
| let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::idle())); |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| |
| transport.start(&req[..req_len]).unwrap(); |
| transport.cancel().unwrap(); |
| assert_eq!(transport.cancel(), Err(TransportError::WrongState)); |
| |
| // The transport is usable again after a cancel. |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); |
| assert!(wire::decode_response_header(&resp[..resp_len]) |
| .unwrap() |
| .is_success()); |
| } |
| |
| #[test] |
| fn poll_into_a_short_buffer_is_too_large() { |
| let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::idle())); |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| |
| transport.start(&req[..req_len]).unwrap(); |
| let mut resp = [0u8; 1]; |
| assert_eq!(transport.poll(&mut resp), Err(TransportError::TooLarge)); |
| // The failed poll ended the round-trip. |
| assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); |
| } |
| |
| /// Send a request and assert the response is success. |
| fn send_ok<F: FdIpcHandler>( |
| transport: &mut Loopback<FdIpcServer<F>, MAX_RESPONSE_SIZE>, |
| encode: impl FnOnce(&mut [u8]) -> Result<usize, WireError>, |
| ) { |
| let mut req = [0u8; 16]; |
| let req_len = encode(&mut req).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let resp_len = round_trip(transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success()); |
| } |
| |
| /// Send QueryStatus and return the decoded FdStatus. |
| fn query_status<F: FdIpcHandler>( |
| transport: &mut Loopback<FdIpcServer<F>, MAX_RESPONSE_SIZE>, |
| ) -> FdStatus { |
| let mut req = [0u8; 16]; |
| let req_len = wire::encode_query_status(&mut req).unwrap(); |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| let resp_len = round_trip(transport, &req[..req_len], &mut resp); |
| let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); |
| assert!(h.is_success()); |
| let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); |
| FdStatus::decode(payload).unwrap() |
| } |
| |
| #[test] |
| fn offer_accept_reaches_ready_xfer() { |
| let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::with_offer())); |
| |
| assert_eq!( |
| query_status(&mut t), |
| FdStatus::OfferPending { |
| target: 0x0001, |
| total: 0x0010_0000, |
| mode: TransferMode::InTransport, |
| svn_delayed: false, |
| } |
| ); |
| |
| send_ok(&mut t, |b| wire::encode_accept_offer(b, 0x2000_0000)); |
| assert_eq!(query_status(&mut t), FdStatus::ReadyXfer); |
| } |
| |
| // Transfer is UA-driven (no IPC op). The FD enters VerifyPending |
| // when the transfer completes, so the command sequence starts there. |
| #[test] |
| fn command_sequence_verify_through_idle() { |
| let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::at( |
| FdStatus::VerifyPending, |
| ))); |
| |
| send_ok(&mut t, wire::encode_perform_verify); |
| assert_eq!(query_status(&mut t), FdStatus::ApplyPending); |
| |
| send_ok(&mut t, wire::encode_perform_apply); |
| assert_eq!(query_status(&mut t), FdStatus::ActivationPending); |
| |
| send_ok(&mut t, wire::encode_perform_activate); |
| assert_eq!(query_status(&mut t), FdStatus::Idle { reason: 0 }); |
| } |
| |
| #[test] |
| fn svn_commit_after_activation() { |
| let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::at( |
| FdStatus::SvnCommitPending { component: 1 }, |
| ))); |
| |
| send_ok(&mut t, wire::encode_perform_svn_commit); |
| assert_eq!(query_status(&mut t), FdStatus::Idle { reason: 0 }); |
| } |
| |
| #[test] |
| fn ack_cancel_returns_to_idle() { |
| let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::at( |
| FdStatus::Cancelled, |
| ))); |
| |
| send_ok(&mut t, wire::encode_ack_cancel); |
| assert_eq!(query_status(&mut t), FdStatus::Idle { reason: 0 }); |
| } |
| |
| #[test] |
| fn an_oversized_request_is_answered_by_dispatch_not_the_transport() { |
| // A loopback has no request buffer to overflow, so nothing caps the |
| // request at the transport. The decoder rejects it instead and the |
| // caller gets an error frame, the same answer a malformed request of |
| // any length gets. |
| let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdIpcServer::new(StubFd::idle())); |
| let req = [0u8; MAX_REQUEST_SIZE + 1]; |
| let mut resp = [0u8; MAX_RESPONSE_SIZE]; |
| |
| transport.start(&req).unwrap(); |
| let len = transport.poll(&mut resp).unwrap().unwrap(); |
| |
| let h = wire::decode_response_header(&resp[..len]).unwrap(); |
| assert!(!h.is_success()); |
| } |
| } |