diff --git a/.gitignore b/.gitignore index 44e7887..52cb662 100644 --- a/.gitignore +++ b/.gitignore @@ -328,7 +328,8 @@ fuzz/corpus/ fuzz/artifacts/ .omniscient/ - +omniscient.toml .DS_Store +.superpowers/ omniscient.toml diff --git a/src/dtc/ext_data.rs b/src/dtc/ext_data.rs index 5da7922..2192b92 100644 --- a/src/dtc/ext_data.rs +++ b/src/dtc/ext_data.rs @@ -49,7 +49,7 @@ impl DTCExtDataRecordNumber { /// Return the raw `u8` value of this record number. #[must_use] #[allow(clippy::match_same_arms)] - pub fn value(&self) -> u8 { + pub const fn value(&self) -> u8 { match self { Self::ISOSAEReserved(value) => *value, Self::VehicleManufacturer(value) => *value, diff --git a/src/dtc/snapshot.rs b/src/dtc/snapshot.rs index 087bf28..251c162 100644 --- a/src/dtc/snapshot.rs +++ b/src/dtc/snapshot.rs @@ -32,7 +32,7 @@ impl DTCSnapshotRecordNumber { /// Return the raw `u8` value of this snapshot record number. #[must_use] #[allow(clippy::match_same_arms)] - pub fn value(&self) -> u8 { + pub const fn value(&self) -> u8 { match self { DTCSnapshotRecordNumber::Reserved(value) => *value, DTCSnapshotRecordNumber::Number(value) => *value, diff --git a/src/dtc/status.rs b/src/dtc/status.rs index 113fe31..b001a46 100644 --- a/src/dtc/status.rs +++ b/src/dtc/status.rs @@ -131,7 +131,7 @@ impl<'a> Decode<'a> for DTCStatusMask { #[allow(non_camel_case_types)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] #[repr(u8)] pub enum DTCFormatIdentifier { @@ -299,7 +299,7 @@ pub enum FunctionalGroupIdentifier { impl FunctionalGroupIdentifier { /// Return the raw `u8` value of this functional group identifier. #[must_use] - pub fn value(&self) -> u8 { + pub const fn value(&self) -> u8 { match self { FunctionalGroupIdentifier::EmissionsSystemGroup => 0x33, FunctionalGroupIdentifier::SafetySystemGroup => 0xD0, diff --git a/src/lib.rs b/src/lib.rs index bfb11ab..ad42d2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,19 +37,20 @@ pub use service::UdsServiceType; mod services; pub use services::{ - ClearDiagnosticInfoRequest, CommunicationControlRequest, CommunicationControlResponse, - CommunicationControlType, CommunicationType, ControlDTCSettingsRequest, - ControlDTCSettingsResponse, DiagnosticSessionControlRequest, DiagnosticSessionControlResponse, - DiagnosticSessionType, DirSizePayload, DtcAndStatusIter, DtcFaultDetectionIter, DtcSettings, - DtcSeverityAndStatusIter, EcuResetRequest, EcuResetResponse, FileOperationMode, - FileSizePayload, NamePayload, NegativeResponse, PositionPayload, ReadDTCInfoRequest, - ReadDTCInfoResponse, ReadDTCInfoSubFunction, ReadDataByIdentifierRequest, - RequestDownloadRequest, RequestDownloadResponse, RequestFileTransferRequest, - RequestFileTransferResponse, RequestTransferExitRequest, RequestTransferExitResponse, - ResetType, RoutineControlRequest, RoutineControlResponse, RoutineControlSubFunction, - SecurityAccessLevel, SecurityAccessRequest, SecurityAccessResponse, SecurityAccessType, - SentDataPayload, SizePayload, TesterPresentRequest, TesterPresentResponse, TransferDataRequest, - TransferDataResponse, WriteDataByIdentifierRequest, WriteDataByIdentifierResponse, + ClearDiagnosticInfoRequest, ClearDiagnosticInfoResponse, CommunicationControlRequest, + CommunicationControlResponse, CommunicationControlType, CommunicationType, + ControlDTCSettingsRequest, ControlDTCSettingsResponse, DiagnosticSessionControlRequest, + DiagnosticSessionControlResponse, DiagnosticSessionType, DirSizePayload, DtcAndStatusIter, + DtcFaultDetectionIter, DtcSettings, DtcSeverityAndStatusIter, EcuResetRequest, + EcuResetResponse, FileOperationMode, FileSizePayload, NamePayload, NegativeResponse, + PositionPayload, ReadDTCInfoRequest, ReadDTCInfoResponse, ReadDTCInfoSubFunction, + ReadDataByIdentifierRequest, ReadDataByIdentifierResponse, RequestDownloadRequest, + RequestDownloadResponse, RequestFileTransferRequest, RequestFileTransferResponse, + RequestTransferExitRequest, RequestTransferExitResponse, ResetType, RoutineControlRequest, + RoutineControlResponse, RoutineControlSubFunction, SecurityAccessLevel, SecurityAccessRequest, + SecurityAccessResponse, SecurityAccessType, SentDataPayload, SizePayload, TesterPresentRequest, + TesterPresentResponse, TransferDataRequest, TransferDataResponse, WriteDataByIdentifierRequest, + WriteDataByIdentifierResponse, }; #[cfg(test)] diff --git a/src/response.rs b/src/response.rs index da4c2f5..1819330 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,9 +1,10 @@ use crate::{ - CommunicationControlResponse, ControlDTCSettingsResponse, Decode, + ClearDiagnosticInfoResponse, CommunicationControlResponse, ControlDTCSettingsResponse, Decode, DiagnosticSessionControlResponse, EcuResetResponse, Encode, Error, NegativeResponse, - ReadDTCInfoResponse, RequestDownloadResponse, RequestFileTransferResponse, - RequestTransferExitResponse, RoutineControlResponse, SecurityAccessResponse, - TesterPresentResponse, TransferDataResponse, UdsServiceType, WriteDataByIdentifierResponse, + ReadDTCInfoResponse, ReadDataByIdentifierResponse, RequestDownloadResponse, + RequestFileTransferResponse, RequestTransferExitResponse, RoutineControlResponse, + SecurityAccessResponse, TesterPresentResponse, TransferDataResponse, UdsServiceType, + WriteDataByIdentifierResponse, }; /// Parsed zero-copy UDS response. Borrows from the wire buffer. @@ -14,7 +15,7 @@ use crate::{ #[non_exhaustive] pub enum Response<'a> { /// Positive response to `ClearDiagnosticInfo`. - ClearDiagnosticInfo, + ClearDiagnosticInfo(ClearDiagnosticInfoResponse), /// Positive response to `CommunicationControl`. CommunicationControl(CommunicationControlResponse), /// Positive response to `ControlDTCSettings`. @@ -34,7 +35,7 @@ pub enum Response<'a> { /// cannot split it into `(DID, value)` pairs. Parse it caller-side once you know each /// DID's record length — read the 2-byte big-endian DID, take the application-defined /// number of data bytes, then repeat on the remainder. - ReadDataByIdentifier(&'a [u8]), + ReadDataByIdentifier(ReadDataByIdentifierResponse<'a>), /// Positive response to `ReadDTCInformation` with lazy iterators. ReadDTCInfo(ReadDTCInfoResponse<'a>), /// Positive response to `RequestDownload`. @@ -74,7 +75,9 @@ impl<'a> Decode<'a> for Response<'a> { let payload = &buf[1..]; let response = match service { - UdsServiceType::ClearDiagnosticInfo => Self::ClearDiagnosticInfo, + UdsServiceType::ClearDiagnosticInfo => Self::ClearDiagnosticInfo( + ::decode_exact(payload)?, + ), UdsServiceType::CommunicationControl => Self::CommunicationControl( ::decode_exact(payload)?, ), @@ -90,7 +93,9 @@ impl<'a> Decode<'a> for Response<'a> { UdsServiceType::NegativeResponse => { Self::NegativeResponse(::decode_exact(payload)?) } - UdsServiceType::ReadDataByIdentifier => Self::ReadDataByIdentifier(payload), + UdsServiceType::ReadDataByIdentifier => { + Self::ReadDataByIdentifier(ReadDataByIdentifierResponse::new(payload)) + } UdsServiceType::ReadDTCInfo => { Self::ReadDTCInfo(::decode_exact(payload)?) } @@ -143,7 +148,7 @@ impl Response<'_> { /// Returns the response service-ID byte that frames this response on the wire. fn response_sid(&self) -> u8 { match self { - Self::ClearDiagnosticInfo => UdsServiceType::ClearDiagnosticInfo.response_to_byte(), + Self::ClearDiagnosticInfo(_) => UdsServiceType::ClearDiagnosticInfo.response_to_byte(), Self::CommunicationControl(_) => { UdsServiceType::CommunicationControl.response_to_byte() } @@ -175,7 +180,7 @@ impl Response<'_> { impl Encode for Response<'_> { fn encoded_size(&self) -> usize { let payload = match self { - Self::ClearDiagnosticInfo => 0, + Self::ClearDiagnosticInfo(resp) => resp.encoded_size(), Self::RequestTransferExit(resp) => resp.encoded_size(), Self::Other { data, .. } => data.len(), Self::CommunicationControl(resp) => resp.encoded_size(), @@ -183,7 +188,7 @@ impl Encode for Response<'_> { Self::DiagnosticSessionControl(resp) => resp.encoded_size(), Self::EcuReset(resp) => resp.encoded_size(), Self::NegativeResponse(resp) => resp.encoded_size(), - Self::ReadDataByIdentifier(bytes) => bytes.len(), + Self::ReadDataByIdentifier(resp) => resp.encoded_size(), Self::WriteDataByIdentifier(resp) => resp.encoded_size(), Self::ReadDTCInfo(resp) => resp.encoded_size(), Self::RequestDownload(resp) => resp.encoded_size(), @@ -201,17 +206,14 @@ impl Encode for Response<'_> { .write_all(&[self.response_sid()]) .map_err(Error::io)?; let payload = match self { - Self::ClearDiagnosticInfo => 0, + Self::ClearDiagnosticInfo(resp) => resp.encode(writer)?, Self::RequestTransferExit(resp) => resp.encode(writer)?, Self::CommunicationControl(resp) => resp.encode(writer)?, Self::ControlDTCSettings(resp) => resp.encode(writer)?, Self::DiagnosticSessionControl(resp) => resp.encode(writer)?, Self::EcuReset(resp) => resp.encode(writer)?, Self::NegativeResponse(resp) => resp.encode(writer)?, - Self::ReadDataByIdentifier(bytes) => { - writer.write_all(bytes).map_err(Error::io)?; - bytes.len() - } + Self::ReadDataByIdentifier(resp) => resp.encode(writer)?, Self::WriteDataByIdentifier(resp) => resp.encode(writer)?, Self::ReadDTCInfo(resp) => resp.encode(writer)?, Self::RequestDownload(resp) => resp.encode(writer)?, @@ -284,4 +286,17 @@ mod tests { let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); assert_eq!(&buf[..written], &frame); // previously became 0x7F (NegativeResponse) } + + #[test] + fn clear_diagnostic_info_response_rejects_trailing_bytes() { + // Bare SID round-trips; a conformant ClearDiagnosticInfo positive response is [0x54]. + let (resp, remaining) = Response::decode(&[0x54]).unwrap(); + assert!(remaining.is_empty()); + assert!(matches!(resp, Response::ClearDiagnosticInfo(_))); + // Trailing bytes after the SID are now rejected (matches every other response arm). + assert!(matches!( + Response::decode(&[0x54, 0xAA]), + Err(crate::Error::IncorrectMessageLengthOrInvalidFormat) + )); + } } diff --git a/src/services/clear_dtc_information.rs b/src/services/clear_dtc_information.rs index 19ba945..be48d87 100644 --- a/src/services/clear_dtc_information.rs +++ b/src/services/clear_dtc_information.rs @@ -1,6 +1,38 @@ //! `ClearDiagnosticInformation` (0x14) service implementation use crate::{CLEAR_ALL_DTCS, DTCRecord, Decode, Encode, NegativeResponseCode}; +/// Positive response to `ClearDiagnosticInformation`. Carries no payload. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[non_exhaustive] +pub struct ClearDiagnosticInfoResponse; + +impl ClearDiagnosticInfoResponse { + /// Create a `ClearDiagnosticInfoResponse`. + #[must_use] + pub const fn new() -> Self { + Self + } +} + +impl Encode for ClearDiagnosticInfoResponse { + fn encoded_size(&self) -> usize { + 0 + } + fn encode(&self, _writer: &mut impl embedded_io::Write) -> Result { + Ok(0) + } +} + +impl<'a> Decode<'a> for ClearDiagnosticInfoResponse { + /// Consumes zero bytes and returns the full buffer as the remainder. + /// `decode_exact` at the call site (in `Response::decode`) enforces that no trailing bytes follow the SID. + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), crate::Error> { + Ok((Self, buf)) + } +} + /// Negative response codes const CLEAR_DIAG_INFO_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, @@ -108,3 +140,21 @@ mod request { assert_eq!(all, compare); } } + +#[cfg(test)] +mod response { + use super::*; + use crate::{Decode, Encode}; + + #[test] + fn clear_dtc_response_roundtrips_empty() { + let resp = ClearDiagnosticInfoResponse::new(); + let mut buf = [0u8; 4]; + let n = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(n, 0); + let (decoded, remaining) = + ::decode(&buf[..0]).unwrap(); + assert_eq!(decoded, resp); + assert!(remaining.is_empty()); + } +} diff --git a/src/services/control_dtc_settings.rs b/src/services/control_dtc_settings.rs index 5a4c45d..5ae997a 100644 --- a/src/services/control_dtc_settings.rs +++ b/src/services/control_dtc_settings.rs @@ -1,6 +1,6 @@ //! `ControlDTCSetting` (0x85) service implementation use crate::shared::SuppressablePositiveResponse; -use crate::{Decode, Encode, Error}; +use crate::{Decode, Encode, Error, NegativeResponseCode}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -49,6 +49,13 @@ pub struct ControlDTCSettingsRequest { pub setting: DtcSettings, } +const CONTROL_DTC_SETTINGS_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ + NegativeResponseCode::SubFunctionNotSupported, + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::ConditionsNotCorrect, + NegativeResponseCode::RequestOutOfRange, +]; + impl ControlDTCSettingsRequest { /// Create a new `ControlDTCSettingsRequest`. #[must_use] @@ -58,6 +65,12 @@ impl ControlDTCSettingsRequest { setting, } } + + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &CONTROL_DTC_SETTINGS_NEGATIVE_RESPONSE_CODES + } } impl Encode for ControlDTCSettingsRequest { @@ -96,7 +109,7 @@ impl<'a> Decode<'a> for ControlDTCSettingsRequest { /// The ECU will respond with a `ControlDTCSettingsResponse` if the request was successful. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct ControlDTCSettingsResponse { /// The DTC logging setting that was set in the request @@ -137,7 +150,7 @@ impl<'a> Decode<'a> for ControlDTCSettingsResponse { #[cfg(test)] mod request { use super::*; - use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + use crate::{Decode, Encode, NegativeResponseCode, test_util::assert_encode_size_agrees}; #[cfg(feature = "alloc")] use alloc::{vec, vec::Vec}; @@ -164,6 +177,15 @@ mod request { let err = ::decode(&[0x09]).unwrap_err(); assert!(matches!(err, Error::InvalidDtcSetting(0x09))); } + + #[test] + fn exposes_allowed_nack_codes() { + assert!(!ControlDTCSettingsRequest::allowed_nack_codes().is_empty()); + assert!( + ControlDTCSettingsRequest::allowed_nack_codes() + .contains(&NegativeResponseCode::RequestOutOfRange) + ); + } } #[cfg(test)] @@ -187,4 +209,9 @@ mod response { assert_eq!(parsed.setting, DtcSettings::On); assert_encode_size_agrees(&req); } + + #[test] + fn response_is_eq() { + crate::test_util::assert_impl_eq::(); + } } diff --git a/src/services/mod.rs b/src/services/mod.rs index d79de5f..d87d06e 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,5 +1,5 @@ mod clear_dtc_information; -pub use clear_dtc_information::ClearDiagnosticInfoRequest; +pub use clear_dtc_information::{ClearDiagnosticInfoRequest, ClearDiagnosticInfoResponse}; mod communication_control; pub use communication_control::{ @@ -24,7 +24,7 @@ mod negative_response; pub use negative_response::NegativeResponse; mod read_data_by_identifier; -pub use read_data_by_identifier::ReadDataByIdentifierRequest; +pub use read_data_by_identifier::{ReadDataByIdentifierRequest, ReadDataByIdentifierResponse}; mod read_dtc_information; pub use read_dtc_information::{ diff --git a/src/services/read_data_by_identifier.rs b/src/services/read_data_by_identifier.rs index c73b67a..6815696 100644 --- a/src/services/read_data_by_identifier.rs +++ b/src/services/read_data_by_identifier.rs @@ -1,6 +1,52 @@ //! `ReadDataByIdentifier` (0x22) service implementation use crate::{Decode, Encode, Error, NegativeResponseCode}; +/// Positive response to `ReadDataByIdentifier`: raw `[DID][data record]…` bytes. +/// +/// Left opaque **by design**: each data record's length is defined by the ECU's +/// configuration for that DID and is not present on the wire, so the library cannot +/// split it into `(DID, value)` pairs. Read the 2-byte big-endian DID, take the +/// application-defined number of data bytes via [`records`](Self::records), then repeat. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct ReadDataByIdentifierResponse<'a> { + #[cfg_attr(feature = "serde", serde(borrow))] + records: &'a [u8], +} + +impl<'a> ReadDataByIdentifierResponse<'a> { + /// Wrap the raw record bytes of a positive `ReadDataByIdentifier` response. + #[must_use] + pub const fn new(records: &'a [u8]) -> Self { + Self { records } + } + + /// The raw `[DID][data record]…` bytes, to be parsed caller-side. + #[must_use] + pub const fn records(&self) -> &'a [u8] { + self.records + } +} + +impl Encode for ReadDataByIdentifierResponse<'_> { + fn encoded_size(&self) -> usize { + self.records.len() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(self.records).map_err(Error::io)?; + Ok(self.records.len()) + } +} + +impl<'a> Decode<'a> for ReadDataByIdentifierResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + Ok((Self { records: buf }, &[])) + } +} + const READ_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, NegativeResponseCode::ResponseTooLong, @@ -11,7 +57,18 @@ const READ_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ /// Read-DID request: a list of 16-bit Data Identifiers. Built from native `&[u16]` /// or borrowed from the wire as big-endian bytes; `dids()` yields `u16` either way. +/// +/// # serde / utoipa carve-out +/// +/// serde derives are omitted: the zero-copy `Native(&[u16])` backing has no borrowed +/// `Deserialize` impl in serde (zero-copy borrowing via `#[serde(borrow)]` only works +/// for `&[u8]` and `&str`, not `&[u16]`), so `Dids::Native(&[u16])` cannot be +/// deserialized without an owned allocation. +/// +/// utoipa derives are omitted: `utoipa::ToSchema` cannot be derived on +/// `ReadDataByIdentifierRequest` without also deriving it on the private `Dids` enum. #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] pub struct ReadDataByIdentifierRequest<'d> { dids: Dids<'d>, } @@ -108,7 +165,13 @@ impl<'a> Decode<'a> for ReadDataByIdentifierRequest<'a> { #[cfg(test)] mod test { use super::*; - use crate::test_util::assert_encode_size_agrees; + use crate::test_util::{assert_encode_size_agrees, assert_impl_eq}; + + #[test] + fn derive_contract() { + assert_impl_eq::>(); + // serde: omitted — see struct-level doc comment for rationale + } #[test] fn rdbi_native_encodes_be() { @@ -150,6 +213,18 @@ mod test { assert!(::decode(&[0xF1, 0x90, 0xF1]).is_err()); } + #[test] + fn rdbi_response_wraps_and_roundtrips() { + use crate::{Decode, Encode}; + let raw = [0xF1, 0x90, 0x01, 0x02]; + let (resp, remaining) = ::decode(&raw).unwrap(); + assert!(remaining.is_empty()); + assert_eq!(resp.records(), &raw); + let mut buf = [0u8; 8]; + let n = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..n], &raw); + } + #[test] fn encode_read_did_request_tx() { let ids = [0xF180u16, 0xF186u16]; diff --git a/src/services/read_dtc_information.rs b/src/services/read_dtc_information.rs index c7b92ec..23ba69a 100644 --- a/src/services/read_dtc_information.rs +++ b/src/services/read_dtc_information.rs @@ -3,7 +3,7 @@ use crate::{ DTCExtDataRecordNumber, DTCFormatIdentifier, DTCRecord, DTCSeverityMask, DTCSnapshotRecordNumber, DTCStatusMask, DTCStoredDataRecordNumber, Decode, Encode, Error, - FunctionalGroupIdentifier, + FunctionalGroupIdentifier, NegativeResponseCode, }; /// Used for non-emissions related servers @@ -11,10 +11,16 @@ type DTCFaultDetectionCounter = u8; /// Used to address the respective user-defined DTC memory when retrieving DTCs type MemorySelection = u8; +const READ_DTC_INFO_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 3] = [ + NegativeResponseCode::SubFunctionNotSupported, + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::RequestOutOfRange, +]; + /// Request for the server to report diagnostic trouble code information #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct ReadDTCInfoRequest { /// The sub-function specifying what DTC information to report. @@ -24,9 +30,15 @@ pub struct ReadDTCInfoRequest { impl ReadDTCInfoRequest { /// Create a new `ReadDTCInfoRequest`. #[must_use] - pub fn new(dtc_subfunction: ReadDTCInfoSubFunction) -> Self { + pub const fn new(dtc_subfunction: ReadDTCInfoSubFunction) -> Self { Self { dtc_subfunction } } + + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &READ_DTC_INFO_NEGATIVE_RESPONSE_CODES + } } impl Encode for ReadDTCInfoRequest { @@ -150,7 +162,7 @@ impl<'a> Decode<'a> for ReadDTCInfoRequest { #[cfg(test)] mod read_dtc_info_request_encode_tests { use super::*; - use crate::test_util::assert_encode_size_agrees; + use crate::{NegativeResponseCode, test_util::assert_encode_size_agrees}; #[test] fn encode_no_param_subfunction() { @@ -224,12 +236,21 @@ mod read_dtc_info_request_encode_tests { assert_eq!(decoded, req); } } + + #[test] + fn exposes_allowed_nack_codes() { + assert!(!ReadDTCInfoRequest::allowed_nack_codes().is_empty()); + assert!( + ReadDTCInfoRequest::allowed_nack_codes() + .contains(&NegativeResponseCode::RequestOutOfRange) + ); + } } /// A DTC paired with its fault detection counter value #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DTCFaultDetectionCounterRecord { pub dtc_record: DTCRecord, pub dtc_fault_detection_counter: DTCFaultDetectionCounter, @@ -364,7 +385,7 @@ pub enum ReadDTCInfoSubFunction { impl ReadDTCInfoSubFunction { /// Return the raw `u8` sub-function byte. #[must_use] - pub fn value(&self) -> u8 { + pub const fn value(&self) -> u8 { match self { Self::ReportNumberOfDTC_ByStatusMask(_) => 0x01, Self::ReportDTC_ByStatusMask(_) => 0x02, @@ -693,7 +714,9 @@ impl Iterator for DtcSeverityAndStatusIter<'_> { /// `ReadDTCInformation` defines further sub-functions that are **not yet modeled**; /// [`decode`](Self::decode) returns [`Error::InvalidDtcSubfunctionType`] for those. See the /// support table in the crate README. This is the "Partial" coverage noted there, not a bug. -#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[derive(Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum ReadDTCInfoResponse<'a> { /// Sub-functions 0x01, 0x07: count of DTCs matching a mask. @@ -712,11 +735,13 @@ pub enum ReadDTCInfoResponse<'a> { /// DTC status availability mask. status_availability_mask: DTCStatusAvailabilityMask, /// Raw record bytes — use [`DtcAndStatusIter`] to iterate. + #[cfg_attr(feature = "serde", serde(borrow))] raw_records: &'a [u8], }, /// Sub-function 0x14: list of DTC fault detection counter records. DTCFaultDetectionCounterList { /// Raw record bytes — use [`DtcFaultDetectionIter`] to iterate. + #[cfg_attr(feature = "serde", serde(borrow))] raw_records: &'a [u8], }, /// Sub-functions 0x08, 0x09: list of DTC severity records. @@ -729,6 +754,7 @@ pub enum ReadDTCInfoResponse<'a> { /// 3-byte DTC + status). These differ from the 5-byte WWH-OBD records, so /// [`DtcSeverityAndStatusIter`] does **not** apply here; no severity-list iterator is /// wired yet, so parse these bytes caller-side until one is added. + #[cfg_attr(feature = "serde", serde(borrow))] raw_records: &'a [u8], }, /// Sub-function 0x42: WWH-OBD DTC by mask with severity info. @@ -742,6 +768,7 @@ pub enum ReadDTCInfoResponse<'a> { /// DTC format identifier. format_identifier: DTCFormatIdentifier, /// Raw record bytes (5 bytes per record) — use [`DtcSeverityAndStatusIter`]. + #[cfg_attr(feature = "serde", serde(borrow))] raw_records: &'a [u8], }, } @@ -928,6 +955,31 @@ impl Encode for ReadDTCInfoResponse<'_> { } } +#[cfg(test)] +mod derive_contract { + use super::*; + use crate::test_util::assert_impl_eq; + #[cfg(feature = "serde")] + use crate::test_util::assert_impl_serde; + + const _: ReadDTCInfoRequest = ReadDTCInfoRequest::new( + ReadDTCInfoSubFunction::ReportDTC_ByStatusMask(DTCStatusMask::TestFailed), + ); + + #[test] + fn eq_impls() { + assert_impl_eq::(); + assert_impl_eq::(); + assert_impl_eq::>(); + } + + #[cfg(feature = "serde")] + #[test] + fn serde_impls() { + assert_impl_serde::>(); + } +} + #[cfg(test)] mod iter_tests { use super::*; diff --git a/src/services/request_download.rs b/src/services/request_download.rs index 5d690c3..0806280 100644 --- a/src/services/request_download.rs +++ b/src/services/request_download.rs @@ -24,7 +24,7 @@ const REQUEST_DOWNLOAD_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 6] = [ /// See ISO-14229-1:2020, Table H.1 for format information #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct RequestDownloadRequest { /// compression method (high nibble) and encrypting method (low nibble). 0x00 is no compression or encryption @@ -147,12 +147,16 @@ impl<'a> Decode<'a> for RequestDownloadRequest { /// Zero-alloc response for request download. Borrows from the caller. /// /// Positive response to a [`RequestDownloadRequest`] indicating the server is ready to receive data. -#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] pub struct RequestDownloadResponse<'d> { /// Maximum number of bytes per [`TransferDataRequest`](crate::TransferDataRequest). /// /// The on-wire `lengthFormatIdentifier` nibble is derived from this slice's length /// at encode time, so the declared length can never disagree with the bytes present. + #[cfg_attr(feature = "serde", serde(borrow))] pub max_number_of_block_length: &'d [u8], } @@ -313,4 +317,17 @@ mod tests { let resp = RequestDownloadResponse::new(&block); assert_encode_size_agrees(&resp); } + + #[test] + fn derive_contract() { + use crate::test_util::assert_impl_eq; + assert_impl_eq::(); + assert_impl_eq::>(); + #[cfg(feature = "serde")] + { + use crate::test_util::assert_impl_serde; + assert_impl_serde::(); + assert_impl_serde::>(); + } + } } diff --git a/src/services/request_file_transfer.rs b/src/services/request_file_transfer.rs index fda8890..5d1fb6d 100644 --- a/src/services/request_file_transfer.rs +++ b/src/services/request_file_transfer.rs @@ -1,7 +1,7 @@ //! `RequestFileTransfer` (0x38) service implementation use crate::shared::{DataFormatIdentifier, read_be_uint, write_be_uint}; -use crate::{Decode, Encode, Error, param_length_u128}; +use crate::{Decode, Encode, Error, NegativeResponseCode, param_length_u128}; /// Minimum byte-width (clamped to at least 1) needed to hold the larger of two size /// values. Used to derive the on-wire `parameterLength` prefix from the data itself, @@ -87,7 +87,7 @@ impl TryFrom for FileOperationMode { #[allow(clippy::struct_field_names)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SizePayload { /// Specifies the size of the uncompressed file in bytes. /// @@ -136,7 +136,7 @@ impl SizePayload { /// [Request]: RequestFileTransferRequest #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct NamePayload<'a> { /// 0x01 - 0x06, the type of operation to be applied to the file or directory specified in `file_path_and_name` pub mode_of_operation: FileOperationMode, @@ -175,7 +175,7 @@ impl<'a> NamePayload<'a> { /// there is no need to use the `TransferData` or [`crate::UdsServiceType::RequestTransferExit`] services. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum RequestFileTransferRequest<'a> { /// Add a file to the server @@ -214,6 +214,21 @@ pub enum RequestFileTransferRequest<'a> { ), } +const REQUEST_FILE_TRANSFER_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::ConditionsNotCorrect, + NegativeResponseCode::RequestOutOfRange, + NegativeResponseCode::UploadDownloadNotAccepted, +]; + +impl RequestFileTransferRequest<'_> { + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &REQUEST_FILE_TRANSFER_NEGATIVE_RESPONSE_CODES + } +} + ///////////////////////////////////////// - Response - /////////////////////////////////////////////////// /// Sent by the server to inform the client of the maximum number of bytes to include in each `TransferData` request message @@ -231,7 +246,7 @@ pub enum RequestFileTransferRequest<'a> { /// [Response]: RequestFileTransferResponse #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SentDataPayload<'a> { /// This parameter is used by the requestFileTransfer positive response message to inform the client how many /// data bytes (maxNumberOfBlockLength) to include in each `TransferData` request message from the client or how @@ -279,7 +294,7 @@ impl<'a> SentDataPayload<'a> { #[allow(clippy::struct_field_names)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct FileSizePayload { /// Size of the uncompressed file in bytes. pub file_size_uncompressed: u128, @@ -319,7 +334,7 @@ impl FileSizePayload { /// [Response]: RequestFileTransferResponse #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DirSizePayload { /// Total size of the directory information in bytes. pub dir_info_length: u128, @@ -354,7 +369,7 @@ impl DirSizePayload { /// [Response]: RequestFileTransferResponse #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct PositionPayload { /// Specifies the byte position within the file at which the Tester will resume downloading after an initial download is suspended /// A download is suspended when the ECU stops receiving [`crate::TransferDataRequest`] requests and does not receive the @@ -381,7 +396,7 @@ impl PositionPayload { /// `DataFormatIdentifier` - Echoes the value of the request #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum RequestFileTransferResponse<'a> { /// Positive response to an [`AddFile`](FileOperationMode::AddFile) request. @@ -820,8 +835,15 @@ impl<'a> Decode<'a> for RequestFileTransferResponse<'a> { #[cfg(test)] mod request_tests { use super::*; + use crate::NegativeResponseCode; use crate::test_util::assert_encode_size_agrees; + #[test] + fn test_allowed_nack_codes() { + let codes = RequestFileTransferRequest::allowed_nack_codes(); + assert!(codes.contains(&NegativeResponseCode::UploadDownloadNotAccepted)); + } + #[test] fn test_file_operation_mode() { use FileOperationMode::*; @@ -1049,4 +1071,20 @@ mod response_tests { assert_eq!(decoded, resp); assert_encode_size_agrees(&resp); } + + #[test] + fn derive_contract() { + use crate::test_util::assert_impl_eq; + + // Verify all nine types implement Eq + assert_impl_eq::(); + assert_impl_eq::(); + assert_impl_eq::>(); + assert_impl_eq::>(); + assert_impl_eq::>(); + assert_impl_eq::(); + assert_impl_eq::(); + assert_impl_eq::(); + assert_impl_eq::>(); + } } diff --git a/src/services/request_transfer_exit.rs b/src/services/request_transfer_exit.rs index 2b17e5f..2bc3c18 100644 --- a/src/services/request_transfer_exit.rs +++ b/src/services/request_transfer_exit.rs @@ -1,5 +1,5 @@ //! `RequestTransferExit` (0x37 / 0x77) service implementation. -use crate::{Decode, Encode, Error}; +use crate::{Decode, Encode, Error, NegativeResponseCode}; macro_rules! transfer_exit_descriptor { ($name:ident, $doc:literal) => { @@ -48,10 +48,31 @@ transfer_exit_descriptor!( "Positive response to `RequestTransferExit`, carrying an optional parameter record." ); +const REQUEST_TRANSFER_EXIT_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::RequestSequenceError, + NegativeResponseCode::RequestOutOfRange, + NegativeResponseCode::GeneralProgrammingFailure, +]; + +impl RequestTransferExitRequest<'_> { + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &REQUEST_TRANSFER_EXIT_NEGATIVE_RESPONSE_CODES + } +} + #[cfg(test)] mod tests { use super::*; - use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + use crate::{Decode, Encode, NegativeResponseCode, test_util::assert_encode_size_agrees}; + + #[test] + fn test_allowed_nack_codes() { + let codes = RequestTransferExitRequest::allowed_nack_codes(); + assert!(codes.contains(&NegativeResponseCode::RequestSequenceError)); + } #[test] fn rte_request_round_trips_with_and_without_record() { diff --git a/src/services/routine_control.rs b/src/services/routine_control.rs index 6c46b20..0682f58 100644 --- a/src/services/routine_control.rs +++ b/src/services/routine_control.rs @@ -3,7 +3,7 @@ //! It can also be used to check the ECU's health, erase memory, or other custom manufacturer/supplier routines. //! However, some routines may have side effects or require certain preconditions to be met. use crate::shared::SuppressablePositiveResponse; -use crate::{Decode, Encode, Error}; +use crate::{Decode, Encode, Error, NegativeResponseCode}; /// What type of routine control to perform for a [`RoutineControlRequest`]. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -49,10 +49,22 @@ impl TryFrom for RoutineControlSubFunction { } } +const ROUTINE_CONTROL_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 7] = [ + NegativeResponseCode::SubFunctionNotSupported, + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::ConditionsNotCorrect, + NegativeResponseCode::RequestSequenceError, + NegativeResponseCode::RequestOutOfRange, + NegativeResponseCode::SecurityAccessDenied, + NegativeResponseCode::GeneralProgrammingFailure, +]; + /// Used by a client to execute a defined sequence of events and obtain any relevant results. /// /// The 2-byte big-endian routine identifier is decoded into a typed `u16`, followed by /// optional routine input parameters in `option_record`. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct RoutineControlRequest<'d> { @@ -63,6 +75,7 @@ pub struct RoutineControlRequest<'d> { /// The 16-bit routine identifier. pub routine_id: u16, /// Optional routine input parameters (may be empty). + #[cfg_attr(feature = "serde", serde(borrow))] pub option_record: &'d [u8], } @@ -82,6 +95,12 @@ impl<'d> RoutineControlRequest<'d> { option_record, } } + + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &ROUTINE_CONTROL_NEGATIVE_RESPONSE_CODES + } } impl Encode for RoutineControlRequest<'_> { @@ -127,6 +146,8 @@ impl<'a> Decode<'a> for RoutineControlRequest<'a> { /// /// The 2-byte big-endian routine identifier echo is decoded into a typed `u16`, followed /// by optional routine status bytes in `status_record`. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct RoutineControlResponse<'d> { @@ -135,6 +156,7 @@ pub struct RoutineControlResponse<'d> { /// The 16-bit routine identifier echoed from the request. pub routine_id: u16, /// Optional routine status bytes (may be empty). + #[cfg_attr(feature = "serde", serde(borrow))] pub status_record: &'d [u8], } @@ -193,8 +215,21 @@ impl<'a> Decode<'a> for RoutineControlResponse<'a> { #[cfg(test)] mod test { use super::*; - use crate::Decode; - use crate::test_util::assert_encode_size_agrees; + use crate::test_util::{assert_encode_size_agrees, assert_impl_eq}; + use crate::{Decode, NegativeResponseCode}; + + #[test] + fn derive_contract() { + assert_impl_eq::>(); + assert_impl_eq::>(); + + #[cfg(feature = "serde")] + { + use crate::test_util::assert_impl_serde; + assert_impl_serde::>(); + assert_impl_serde::>(); + } + } #[test] fn rc_request_round_trips_with_suppress() { @@ -242,4 +277,13 @@ mod test { let bytes = [0x7F, 0xFF, 0x00]; assert!(::decode(&bytes).is_err()); } + + #[test] + fn exposes_allowed_nack_codes() { + assert!(!RoutineControlRequest::allowed_nack_codes().is_empty()); + assert!( + RoutineControlRequest::allowed_nack_codes() + .contains(&NegativeResponseCode::SecurityAccessDenied) + ); + } } diff --git a/src/services/security_access.rs b/src/services/security_access.rs index 11d823d..64ec9c0 100644 --- a/src/services/security_access.rs +++ b/src/services/security_access.rs @@ -28,7 +28,7 @@ impl SecurityAccessLevel { /// The raw level byte (always `0x00..=0x7F`). #[must_use] - pub const fn get(self) -> u8 { + pub const fn value(self) -> u8 { self.0 } } @@ -75,8 +75,8 @@ impl From for u8 { fn from(value: SecurityAccessType) -> Self { match value { SecurityAccessType::ISOSAEReserved(val) => val, - SecurityAccessType::RequestSeed(level) => level.get(), - SecurityAccessType::SendKey(level) => level.get(), + SecurityAccessType::RequestSeed(level) => level.value(), + SecurityAccessType::SendKey(level) => level.value(), SecurityAccessType::ISO26021_2Values => 0x5F, SecurityAccessType::ISO26021_2SendKeyValues => 0x60, SecurityAccessType::SystemSupplierSpecific(val) => val, @@ -172,11 +172,20 @@ mod security_access_type_tests { } } + #[test] + fn level_value_is_const_accessor() { + const V: u8 = match SecurityAccessLevel::new(0x03) { + Ok(l) => l.value(), + Err(_) => 0xFF, + }; + assert_eq!(V, 0x03); + } + #[test] fn security_access_level_rejects_sprmib_colliding_values() { // 0x00..=0x7F are constructible; 0x80..=0xFF (SPRMIB bit set) are rejected. for value in 0x00..=0x7F { - assert_eq!(SecurityAccessLevel::new(value).unwrap().get(), value); + assert_eq!(SecurityAccessLevel::new(value).unwrap().value(), value); } for value in 0x80..=0xFF { assert!(matches!( @@ -232,6 +241,8 @@ const SECURITY_ACCESS_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 8] = [ /// Suppressing a positive response to this request is allowed. /// /// Zero-alloc request for security access. Borrows from the caller. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct SecurityAccessRequest<'d> { @@ -240,6 +251,7 @@ pub struct SecurityAccessRequest<'d> { /// The requested [`SecurityAccessType`]. pub access_type: SecurityAccessType, /// The implementation-defined request data (seed data record or key bytes). + #[cfg_attr(feature = "serde", serde(borrow))] pub request_data: &'d [u8], } @@ -299,12 +311,15 @@ impl<'a> Decode<'a> for SecurityAccessRequest<'a> { } /// Zero-alloc response for security access. Borrows from the caller. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct SecurityAccessResponse<'d> { /// The security access type echoed from the request. pub access_type: SecurityAccessType, /// The security seed bytes (empty for a `SendKey` positive response). + #[cfg_attr(feature = "serde", serde(borrow))] pub security_seed: &'d [u8], } @@ -352,10 +367,22 @@ impl<'a> Decode<'a> for SecurityAccessResponse<'a> { #[cfg(test)] mod request { use super::*; - use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees, test_util::assert_impl_eq}; #[cfg(feature = "alloc")] use alloc::vec::Vec; + #[test] + fn derive_contract() { + assert_impl_eq::>(); + assert_impl_eq::>(); + #[cfg(feature = "serde")] + { + use crate::test_util::assert_impl_serde; + assert_impl_serde::>(); + assert_impl_serde::>(); + } + } + #[cfg(feature = "alloc")] #[test] fn request_seed() { diff --git a/src/services/transfer_data.rs b/src/services/transfer_data.rs index d06e0f8..89a20c4 100644 --- a/src/services/transfer_data.rs +++ b/src/services/transfer_data.rs @@ -1,6 +1,15 @@ //! `TransferData` (0x36) service implementation -use crate::{Decode, Encode, Error}; +use crate::{Decode, Encode, Error, NegativeResponseCode}; + +const TRANSFER_DATA_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 6] = [ + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::RequestSequenceError, + NegativeResponseCode::RequestOutOfRange, + NegativeResponseCode::TransferDataSuspended, + NegativeResponseCode::GeneralProgrammingFailure, + NegativeResponseCode::WrongBlockSequenceCounter, +]; /// A request to the server to transfer data (either upload or download) /// @@ -22,11 +31,15 @@ use crate::{Decode, Encode, Error}; /// Step 3 Response: The server sends a [`crate::UdsServiceType::RequestTransferExit`] response message to the client (RID 0x77) /// /// Zero-alloc request to transfer data. Borrows from the caller. -#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] pub struct TransferDataRequest<'d> { /// Block sequence counter (wraps 0xFF → 0x00). pub block_sequence_counter: u8, /// The data to be transferred. + #[cfg_attr(feature = "serde", serde(borrow))] pub data: &'d [u8], } @@ -39,6 +52,12 @@ impl<'d> TransferDataRequest<'d> { data, } } + + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &TRANSFER_DATA_NEGATIVE_RESPONSE_CODES + } } impl Encode for TransferDataRequest<'_> { @@ -71,11 +90,15 @@ impl<'a> Decode<'a> for TransferDataRequest<'a> { } /// Zero-alloc response for transfer data. Borrows from the caller. -#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] pub struct TransferDataResponse<'d> { /// Echo of the block sequence counter. pub block_sequence_counter: u8, /// Response data (vendor-specific). + #[cfg_attr(feature = "serde", serde(borrow))] pub data: &'d [u8], } @@ -122,10 +145,29 @@ impl<'a> Decode<'a> for TransferDataResponse<'a> { #[cfg(test)] mod request { use super::*; - use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + use crate::{Decode, Encode, NegativeResponseCode, test_util::assert_encode_size_agrees}; #[cfg(feature = "alloc")] use alloc::vec::Vec; + #[test] + fn test_allowed_nack_codes() { + let codes = TransferDataRequest::allowed_nack_codes(); + assert!(codes.contains(&NegativeResponseCode::WrongBlockSequenceCounter)); + } + + #[test] + fn derive_contract() { + use crate::test_util::assert_impl_eq; + assert_impl_eq::>(); + assert_impl_eq::>(); + #[cfg(feature = "serde")] + { + use crate::test_util::assert_impl_serde; + assert_impl_serde::>(); + assert_impl_serde::>(); + } + } + #[test] fn test_transfer_data_request() { let data = [0x01, 0x02, 0x03, 0x04]; diff --git a/src/services/write_data_by_identifier.rs b/src/services/write_data_by_identifier.rs index a2a5348..59e6b8d 100644 --- a/src/services/write_data_by_identifier.rs +++ b/src/services/write_data_by_identifier.rs @@ -15,6 +15,8 @@ const WRITE_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ /// data record that follows it on the wire. /// /// See ISO-14229-1:2020, Section 11.7.2.1 +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct WriteDataByIdentifierRequest<'d> { @@ -22,6 +24,7 @@ pub struct WriteDataByIdentifierRequest<'d> { /// the big-endian encoding is handled on the wire. pub identifier: u16, /// The opaque data record written after the identifier. + #[cfg_attr(feature = "serde", serde(borrow))] pub data: &'d [u8], } @@ -72,6 +75,8 @@ impl<'a> Decode<'a> for WriteDataByIdentifierRequest<'a> { /// Positive response to `WriteDataByIdentifier`: echoes the DID that was written. /// /// See ISO-14229-1:2020, Section 11.7.3.1 +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub struct WriteDataByIdentifierResponse { @@ -115,6 +120,19 @@ mod test { use super::*; use crate::test_util::assert_encode_size_agrees; + #[test] + fn derive_contract() { + use crate::test_util::assert_impl_eq; + assert_impl_eq::>(); + assert_impl_eq::(); + #[cfg(feature = "serde")] + { + use crate::test_util::assert_impl_serde; + assert_impl_serde::>(); + assert_impl_serde::(); + } + } + #[test] fn test_write_response_encode() { let response = WriteDataByIdentifierResponse::new(0xBEEF); diff --git a/src/test_util.rs b/src/test_util.rs index 640014a..73130b2 100644 --- a/src/test_util.rs +++ b/src/test_util.rs @@ -28,3 +28,13 @@ pub(crate) fn assert_encode_size_agrees(value: &T) { "encode consumed {consumed} bytes, encoded_size() is {size}" ); } + +/// Compile-time assertion that `T: Eq`. Never called at runtime; instantiating it +/// in a test forces a compile error until the type derives `Eq`. +#[allow(dead_code)] +pub(crate) const fn assert_impl_eq() {} + +/// Compile-time assertion that `T` round-trips serde (borrowed deserialize allowed). +#[cfg(feature = "serde")] +#[allow(dead_code)] +pub(crate) const fn assert_impl_serde<'de, T: serde::Serialize + serde::Deserialize<'de>>() {}