From 904d4278e5b16941fd4453b6bae0c29d08ee0ba3 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Fri, 30 Jan 2026 11:19:02 +0530 Subject: [PATCH 01/10] feat(doip): implement header parsing with tokio codec --- Cargo.toml | 34 ++-- src/doip/hearder_parser.rs | 378 +++++++++++++++++++++++++++++++++++++ src/doip/mod.rs | 10 + src/lib.rs | 1 + 4 files changed, 401 insertions(+), 22 deletions(-) create mode 100644 src/doip/hearder_parser.rs create mode 100644 src/doip/mod.rs create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 529569d..495c93c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,39 +8,29 @@ # terms of the Apache License Version 2.0 which is available at # https://www.apache.org/licenses/LICENSE-2.0 -[package] -name = "uds2sovd-proxy" -version = "0.1.0" -edition = "2024" -license = "Apache-2.0" -homepage = "https://github.com/eclipse-opensovd/uds2sovd-proxy" +[workspace] +members = ["doip-server"] +resolver = "2" -[lints] -workspace = true - -# Lints are aligned with shared-lints.toml from: -# https://github.com/eclipse-opensovd/cicd-workflows/tree/main/shared-lints [workspace.lints.clippy] # enable pedantic pedantic = { level = "warn", priority = -1 } ## exclude some too pedantic lints for now similar_names = "allow" +module_name_repetitions = "allow" ## lints related to runtime panic behavior -# enforce only checked access to slices to avoid runtime panics -indexing_slicing = "deny" -# disallow any unwraps in the production code -# (unwrap in test code is explicitly allowed) unwrap_used = "deny" -# enforce that arithmetic operations that can produce side effects always use -# either checked or explicit versions of the operations. eg. `.checked_add(...)` -# or `.saturating_sub(...)` to avoid unexpected runtime behavior or panics. +indexing_slicing = "deny" arithmetic_side_effects = "deny" ## lints related to readability of code -# enforce that references are cloned via eg. `Arc::clone` instead of `.clone()` -# making it explicit that a reference is cloned here and not the underlying data. clone_on_ref_ptr = "warn" -# enforce that the type suffix of a literal is always appended directly -# eg. 12u8 instead of 12_u8 separated_literal_suffix = "deny" + +[profile.release] +opt-level = 3 + +[profile.release-with-debug] +inherits = "release" +debug = true diff --git a/src/doip/hearder_parser.rs b/src/doip/hearder_parser.rs new file mode 100644 index 0000000..4697534 --- /dev/null +++ b/src/doip/hearder_parser.rs @@ -0,0 +1,378 @@ +//! DoIP Header Parser + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use std::io; +use tokio_util::codec::{Decoder, Encoder}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum GenericNackCode { + IncorrectPatternFormat = 0x00, + UnknownPayloadType = 0x01, + MessageTooLarge = 0x02, + OutOfMemory = 0x03, + InvalidPayloadLength = 0x04, +} + +#[derive(Debug)] +pub enum ParseError { + InvalidHeader(String), + Io(io::Error), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidHeader(msg) => write!(f, "Invalid header: {}", msg), + Self::Io(e) => write!(f, "IO error: {}", e), + } + } +} + +impl std::error::Error for ParseError {} + +impl From for ParseError { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +pub type Result = std::result::Result; + +pub const DEFAULT_PROTOCOL_VERSION: u8 = 0x02; +pub const DEFAULT_PROTOCOL_VERSION_INV: u8 = 0xFD; +pub const DOIP_HEADER_LENGTH: usize = 8; +pub const MAX_DOIP_MESSAGE_SIZE: u32 = 0x0FFF_FFFF; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum PayloadType { + GenericNack = 0x0000, + VehicleIdentificationRequest = 0x0001, + VehicleIdentificationRequestWithEid = 0x0002, + VehicleIdentificationRequestWithVin = 0x0003, + VehicleIdentificationResponse = 0x0004, + RoutingActivationRequest = 0x0005, + RoutingActivationResponse = 0x0006, + AliveCheckRequest = 0x0007, + AliveCheckResponse = 0x0008, + DoipEntityStatusRequest = 0x4001, + DoipEntityStatusResponse = 0x4002, + DiagnosticPowerModeRequest = 0x4003, + DiagnosticPowerModeResponse = 0x4004, + DiagnosticMessage = 0x8001, + DiagnosticMessagePositiveAck = 0x8002, + DiagnosticMessageNegativeAck = 0x8003, +} + +impl PayloadType { + pub fn from_u16(value: u16) -> Option { + match value { + 0x0000 => Some(Self::GenericNack), + 0x0001 => Some(Self::VehicleIdentificationRequest), + 0x0002 => Some(Self::VehicleIdentificationRequestWithEid), + 0x0003 => Some(Self::VehicleIdentificationRequestWithVin), + 0x0004 => Some(Self::VehicleIdentificationResponse), + 0x0005 => Some(Self::RoutingActivationRequest), + 0x0006 => Some(Self::RoutingActivationResponse), + 0x0007 => Some(Self::AliveCheckRequest), + 0x0008 => Some(Self::AliveCheckResponse), + 0x4001 => Some(Self::DoipEntityStatusRequest), + 0x4002 => Some(Self::DoipEntityStatusResponse), + 0x4003 => Some(Self::DiagnosticPowerModeRequest), + 0x4004 => Some(Self::DiagnosticPowerModeResponse), + 0x8001 => Some(Self::DiagnosticMessage), + 0x8002 => Some(Self::DiagnosticMessagePositiveAck), + 0x8003 => Some(Self::DiagnosticMessageNegativeAck), + _ => None, + } + } + + pub const fn min_payload_length(self) -> usize { + match self { + Self::GenericNack => 1, + Self::VehicleIdentificationRequest => 0, + Self::VehicleIdentificationRequestWithEid => 6, + Self::VehicleIdentificationRequestWithVin => 17, + Self::VehicleIdentificationResponse => 32, + Self::RoutingActivationRequest => 7, + Self::RoutingActivationResponse => 9, + Self::AliveCheckRequest => 0, + Self::AliveCheckResponse => 2, + Self::DoipEntityStatusRequest => 0, + Self::DoipEntityStatusResponse => 3, + Self::DiagnosticPowerModeRequest => 0, + Self::DiagnosticPowerModeResponse => 1, + Self::DiagnosticMessage => 5, + Self::DiagnosticMessagePositiveAck => 5, + Self::DiagnosticMessageNegativeAck => 5, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DoipHeader { + pub version: u8, + pub inverse_version: u8, + pub payload_type: u16, + pub payload_length: u32, +} + +impl DoipHeader { + pub fn parse(data: &[u8]) -> Result { + if data.len() < DOIP_HEADER_LENGTH { + return Err(ParseError::InvalidHeader(format!( + "header too short: expected {}, got {}", + DOIP_HEADER_LENGTH, + data.len() + ))); + } + Ok(Self { + version: data[0], + inverse_version: data[1], + payload_type: u16::from_be_bytes([data[2], data[3]]), + payload_length: u32::from_be_bytes([data[4], data[5], data[6], data[7]]), + }) + } + + pub fn parse_from_buf(buf: &mut Bytes) -> Result { + if buf.len() < DOIP_HEADER_LENGTH { + return Err(ParseError::InvalidHeader(format!( + "header too short: expected {}, got {}", + DOIP_HEADER_LENGTH, + buf.len() + ))); + } + Ok(Self { + version: buf.get_u8(), + inverse_version: buf.get_u8(), + payload_type: buf.get_u16(), + payload_length: buf.get_u32(), + }) + } + + pub fn validate(&self) -> Option { + if self.version != DEFAULT_PROTOCOL_VERSION { + return Some(GenericNackCode::IncorrectPatternFormat); + } + if self.inverse_version != DEFAULT_PROTOCOL_VERSION_INV { + return Some(GenericNackCode::IncorrectPatternFormat); + } + if self.version ^ self.inverse_version != 0xFF { + return Some(GenericNackCode::IncorrectPatternFormat); + } + + let payload_type = match PayloadType::from_u16(self.payload_type) { + Some(pt) => pt, + None => return Some(GenericNackCode::UnknownPayloadType), + }; + + if self.payload_length > MAX_DOIP_MESSAGE_SIZE { + return Some(GenericNackCode::MessageTooLarge); + } + if (self.payload_length as usize) < payload_type.min_payload_length() { + return Some(GenericNackCode::InvalidPayloadLength); + } + None + } + + pub fn is_valid(&self) -> bool { + self.validate().is_none() + } + + pub const fn total_length(&self) -> usize { + DOIP_HEADER_LENGTH + self.payload_length as usize + } + + pub fn to_bytes(&self) -> Bytes { + let mut buf = BytesMut::with_capacity(DOIP_HEADER_LENGTH); + buf.put_u8(self.version); + buf.put_u8(self.inverse_version); + buf.put_u16(self.payload_type); + buf.put_u32(self.payload_length); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u8(self.version); + buf.put_u8(self.inverse_version); + buf.put_u16(self.payload_type); + buf.put_u32(self.payload_length); + } +} + +impl Default for DoipHeader { + fn default() -> Self { + Self { + version: DEFAULT_PROTOCOL_VERSION, + inverse_version: DEFAULT_PROTOCOL_VERSION_INV, + payload_type: 0, + payload_length: 0, + } + } +} + +impl std::fmt::Display for DoipHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let payload_name = PayloadType::from_u16(self.payload_type) + .map(|pt| format!("{:?}", pt)) + .unwrap_or_else(|| format!("Unknown(0x{:04X})", self.payload_type)); + write!( + f, + "DoipHeader {{ version: 0x{:02X}, type: {}, length: {} }}", + self.version, payload_name, self.payload_length + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DoipMessage { + pub header: DoipHeader, + pub payload: Bytes, +} + +impl DoipMessage { + pub fn new(payload_type: PayloadType, payload: Bytes) -> Self { + Self { + header: DoipHeader { + version: DEFAULT_PROTOCOL_VERSION, + inverse_version: DEFAULT_PROTOCOL_VERSION_INV, + payload_type: payload_type as u16, + payload_length: payload.len() as u32, + }, + payload, + } + } + + pub fn with_raw_type(payload_type: u16, payload: Bytes) -> Self { + Self { + header: DoipHeader { + version: DEFAULT_PROTOCOL_VERSION, + inverse_version: DEFAULT_PROTOCOL_VERSION_INV, + payload_type, + payload_length: payload.len() as u32, + }, + payload, + } + } + + pub fn payload_type(&self) -> Option { + PayloadType::from_u16(self.header.payload_type) + } + + pub fn total_length(&self) -> usize { + DOIP_HEADER_LENGTH + self.payload.len() + } + + pub fn to_bytes(&self) -> Bytes { + let mut buf = BytesMut::with_capacity(self.total_length()); + self.header.write_to(&mut buf); + buf.extend_from_slice(&self.payload); + buf.freeze() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DecodeState { + Header, + Payload(DoipHeader), +} + +#[derive(Debug)] +pub struct DoipCodec { + state: DecodeState, + max_payload_size: u32, +} + +impl DoipCodec { + pub fn new() -> Self { + Self { + state: DecodeState::Header, + max_payload_size: MAX_DOIP_MESSAGE_SIZE, + } + } + + pub fn with_max_payload_size(max_size: u32) -> Self { + Self { + state: DecodeState::Header, + max_payload_size: max_size, + } + } +} + +impl Default for DoipCodec { + fn default() -> Self { + Self::new() + } +} + +impl Decoder for DoipCodec { + type Item = DoipMessage; + type Error = io::Error; + + fn decode( + &mut self, + src: &mut BytesMut, + ) -> std::result::Result, Self::Error> { + loop { + match self.state { + DecodeState::Header => { + if src.len() < DOIP_HEADER_LENGTH { + src.reserve(DOIP_HEADER_LENGTH); + return Ok(None); + } + + let header = DoipHeader::parse(&src[..DOIP_HEADER_LENGTH]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + if let Some(nack_code) = header.validate() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("validation failed: {:?}", nack_code), + )); + } + + if header.payload_length > self.max_payload_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "payload too large: {} > {}", + header.payload_length, self.max_payload_size + ), + )); + } + + src.reserve(header.total_length()); + self.state = DecodeState::Payload(header); + } + + DecodeState::Payload(header) => { + if src.len() < header.total_length() { + return Ok(None); + } + + let _ = src.split_to(DOIP_HEADER_LENGTH); + let payload = src.split_to(header.payload_length as usize).freeze(); + + self.state = DecodeState::Header; + return Ok(Some(DoipMessage { header, payload })); + } + } + } + } +} + +impl Encoder for DoipCodec { + type Error = io::Error; + + fn encode( + &mut self, + item: DoipMessage, + dst: &mut BytesMut, + ) -> std::result::Result<(), Self::Error> { + dst.reserve(item.total_length()); + item.header.write_to(dst); + dst.extend_from_slice(&item.payload); + Ok(()) + } +} diff --git a/src/doip/mod.rs b/src/doip/mod.rs new file mode 100644 index 0000000..943705e --- /dev/null +++ b/src/doip/mod.rs @@ -0,0 +1,10 @@ +//! DoIP Protocol Implementation (ISO 13400-2) + +pub mod hearder_parser; + +// Re-export commonly used types +pub use hearder_parser::{ + DoipCodec, DoipHeader, DoipMessage, GenericNackCode, ParseError, PayloadType, Result, + DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, + MAX_DOIP_MESSAGE_SIZE, +}; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..16e1767 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod doip; From fb4a095c5572d5200d5e0c3c1f0c99d35205c7e4 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Mon, 2 Feb 2026 18:44:19 +0530 Subject: [PATCH 02/10] test(doip): add unit tests for header_parser - Renamed hearder_parser.rs to header_parser.rs (typo fix) - Added unit tests for header parsing - Updated MAX_DOIP_MESSAGE_SIZE to 4MB --- src/doip/alive_check.rs | 120 ++++++ src/doip/diagnostic_message.rs | 420 ++++++++++++++++++ src/doip/header_parser.rs | 768 +++++++++++++++++++++++++++++++++ src/doip/mod.rs | 13 +- src/doip/routing_activation.rs | 373 ++++++++++++++++ src/doip/vehicle_id.rs | 331 ++++++++++++++ 6 files changed, 2021 insertions(+), 4 deletions(-) create mode 100644 src/doip/alive_check.rs create mode 100644 src/doip/diagnostic_message.rs create mode 100644 src/doip/header_parser.rs create mode 100644 src/doip/routing_activation.rs create mode 100644 src/doip/vehicle_id.rs diff --git a/src/doip/alive_check.rs b/src/doip/alive_check.rs new file mode 100644 index 0000000..aed545a --- /dev/null +++ b/src/doip/alive_check.rs @@ -0,0 +1,120 @@ +//! Alive Check handlers (ISO 13400-2) + +use bytes::{BufMut, Bytes, BytesMut}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + PayloadTooShort { expected: usize, actual: usize }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PayloadTooShort { expected, actual } => { + write!(f, "payload too short: need {} bytes, got {}", expected, actual) + } + } + } +} + +impl std::error::Error for Error {} + +// Alive Check Request (0x0007) - no payload +// Server sends this to check if tester is still connected +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Request; + +impl Request { + pub fn parse(_payload: &[u8]) -> Result { + Ok(Self) + } + + pub fn to_bytes(&self) -> Bytes { + Bytes::new() + } +} + +// Alive Check Response (0x0008) - 2 byte source address +// Tester responds with its logical address +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Response { + pub source_address: u16, +} + +impl Response { + pub const LEN: usize = 2; + + pub fn new(source_address: u16) -> Self { + Self { source_address } + } + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::LEN { + return Err(Error::PayloadTooShort { + expected: Self::LEN, + actual: payload.len(), + }); + } + + let source_address = u16::from_be_bytes([payload[0], payload[1]]); + Ok(Self { source_address }) + } + + pub fn to_bytes(&self) -> Bytes { + let mut buf = BytesMut::with_capacity(Self::LEN); + self.write_to(&mut buf); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.source_address); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_request() { + let req = Request::parse(&[]).unwrap(); + assert_eq!(req, Request); + } + + #[test] + fn request_empty_payload() { + let req = Request; + let bytes = req.to_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn parse_response() { + let payload = [0x0E, 0x80]; + let resp = Response::parse(&payload).unwrap(); + assert_eq!(resp.source_address, 0x0E80); + } + + #[test] + fn reject_short_response() { + let short = [0x0E]; + assert!(Response::parse(&short).is_err()); + } + + #[test] + fn build_response() { + let resp = Response::new(0x0E80); + let bytes = resp.to_bytes(); + + assert_eq!(bytes.len(), 2); + assert_eq!(&bytes[..], &[0x0E, 0x80]); + } + + #[test] + fn roundtrip_response() { + let original = Response::new(0x0F00); + let bytes = original.to_bytes(); + let parsed = Response::parse(&bytes).unwrap(); + assert_eq!(original, parsed); + } +} diff --git a/src/doip/diagnostic_message.rs b/src/doip/diagnostic_message.rs new file mode 100644 index 0000000..5f31aa1 --- /dev/null +++ b/src/doip/diagnostic_message.rs @@ -0,0 +1,420 @@ +//! Diagnostic Message handlers (ISO 13400-2) + +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +// Diagnostic message positive ack codes per ISO 13400-2 Table 27 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum AckCode { + Acknowledged = 0x00, +} + +// Diagnostic message negative ack codes per ISO 13400-2 Table 28 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum NackCode { + InvalidSourceAddress = 0x02, + UnknownTargetAddress = 0x03, + DiagnosticMessageTooLarge = 0x04, + OutOfMemory = 0x05, + TargetUnreachable = 0x06, + UnknownNetwork = 0x07, + TransportProtocolError = 0x08, +} + +impl NackCode { + pub fn from_u8(value: u8) -> Option { + match value { + 0x02 => Some(Self::InvalidSourceAddress), + 0x03 => Some(Self::UnknownTargetAddress), + 0x04 => Some(Self::DiagnosticMessageTooLarge), + 0x05 => Some(Self::OutOfMemory), + 0x06 => Some(Self::TargetUnreachable), + 0x07 => Some(Self::UnknownNetwork), + 0x08 => Some(Self::TransportProtocolError), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + PayloadTooShort { expected: usize, actual: usize }, + EmptyUserData, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PayloadTooShort { expected, actual } => { + write!(f, "payload too short: need {} bytes, got {}", expected, actual) + } + Self::EmptyUserData => write!(f, "diagnostic message has no user data"), + } + } +} + +impl std::error::Error for Error {} + +// Diagnostic Message - carries UDS data between tester and ECU +// Payload: SA(2) + TA(2) + user_data(1+) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Message { + pub source_address: u16, + pub target_address: u16, + pub user_data: Bytes, +} + +impl Message { + pub const MIN_LEN: usize = 5; // SA + TA + at least 1 byte UDS + + pub fn new(source: u16, target: u16, data: Bytes) -> Self { + Self { + source_address: source, + target_address: target, + user_data: data, + } + } + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: payload.len(), + }); + } + + let source_address = u16::from_be_bytes([payload[0], payload[1]]); + let target_address = u16::from_be_bytes([payload[2], payload[3]]); + let user_data = Bytes::copy_from_slice(&payload[4..]); + + if user_data.is_empty() { + return Err(Error::EmptyUserData); + } + + Ok(Self { + source_address, + target_address, + user_data, + }) + } + + pub fn parse_buf(buf: &mut Bytes) -> Result { + if buf.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: buf.len(), + }); + } + + let source_address = buf.get_u16(); + let target_address = buf.get_u16(); + let user_data = buf.split_off(0); + + if user_data.is_empty() { + return Err(Error::EmptyUserData); + } + + Ok(Self { + source_address, + target_address, + user_data, + }) + } + + pub fn to_bytes(&self) -> Bytes { + let mut buf = BytesMut::with_capacity(4 + self.user_data.len()); + self.write_to(&mut buf); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.source_address); + buf.put_u16(self.target_address); + buf.extend_from_slice(&self.user_data); + } + + // UDS service ID is first byte of user_data + pub fn service_id(&self) -> Option { + self.user_data.first().copied() + } +} + +// Diagnostic Message Positive Ack (0x8002) +// Payload: SA(2) + TA(2) + ack_code(1) + optional previous_diag_data +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PositiveAck { + pub source_address: u16, + pub target_address: u16, + pub ack_code: AckCode, + pub previous_data: Option, +} + +impl PositiveAck { + pub const MIN_LEN: usize = 5; + + pub fn new(source: u16, target: u16) -> Self { + Self { + source_address: source, + target_address: target, + ack_code: AckCode::Acknowledged, + previous_data: None, + } + } + + pub fn with_previous_data(source: u16, target: u16, data: Bytes) -> Self { + Self { + source_address: source, + target_address: target, + ack_code: AckCode::Acknowledged, + previous_data: Some(data), + } + } + + pub fn to_bytes(&self) -> Bytes { + let extra = self.previous_data.as_ref().map(|d| d.len()).unwrap_or(0); + let mut buf = BytesMut::with_capacity(Self::MIN_LEN + extra); + self.write_to(&mut buf); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.source_address); + buf.put_u16(self.target_address); + buf.put_u8(self.ack_code as u8); + if let Some(ref data) = self.previous_data { + buf.extend_from_slice(data); + } + } + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: payload.len(), + }); + } + + let source_address = u16::from_be_bytes([payload[0], payload[1]]); + let target_address = u16::from_be_bytes([payload[2], payload[3]]); + let ack_code = AckCode::Acknowledged; // only one value + + let previous_data = if payload.len() > Self::MIN_LEN { + Some(Bytes::copy_from_slice(&payload[5..])) + } else { + None + }; + + Ok(Self { + source_address, + target_address, + ack_code, + previous_data, + }) + } +} + +// Diagnostic Message Negative Ack (0x8003) +// Payload: SA(2) + TA(2) + nack_code(1) + optional previous_diag_data +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NegativeAck { + pub source_address: u16, + pub target_address: u16, + pub nack_code: NackCode, + pub previous_data: Option, +} + +impl NegativeAck { + pub const MIN_LEN: usize = 5; + + pub fn new(source: u16, target: u16, code: NackCode) -> Self { + Self { + source_address: source, + target_address: target, + nack_code: code, + previous_data: None, + } + } + + pub fn with_previous_data(source: u16, target: u16, code: NackCode, data: Bytes) -> Self { + Self { + source_address: source, + target_address: target, + nack_code: code, + previous_data: Some(data), + } + } + + pub fn to_bytes(&self) -> Bytes { + let extra = self.previous_data.as_ref().map(|d| d.len()).unwrap_or(0); + let mut buf = BytesMut::with_capacity(Self::MIN_LEN + extra); + self.write_to(&mut buf); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.source_address); + buf.put_u16(self.target_address); + buf.put_u8(self.nack_code as u8); + if let Some(ref data) = self.previous_data { + buf.extend_from_slice(data); + } + } + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: payload.len(), + }); + } + + let source_address = u16::from_be_bytes([payload[0], payload[1]]); + let target_address = u16::from_be_bytes([payload[2], payload[3]]); + let nack_code = NackCode::from_u8(payload[4]) + .unwrap_or(NackCode::TransportProtocolError); + + let previous_data = if payload.len() > Self::MIN_LEN { + Some(Bytes::copy_from_slice(&payload[5..])) + } else { + None + }; + + Ok(Self { + source_address, + target_address, + nack_code, + previous_data, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nack_code_values() { + assert_eq!(NackCode::InvalidSourceAddress as u8, 0x02); + assert_eq!(NackCode::UnknownTargetAddress as u8, 0x03); + assert_eq!(NackCode::TargetUnreachable as u8, 0x06); + } + + #[test] + fn parse_diagnostic_message() { + // SA=0x0E80, TA=0x1000, UDS=0x22 0xF1 0x90 (ReadDataByID) + let payload = [0x0E, 0x80, 0x10, 0x00, 0x22, 0xF1, 0x90]; + let msg = Message::parse(&payload).unwrap(); + + assert_eq!(msg.source_address, 0x0E80); + assert_eq!(msg.target_address, 0x1000); + assert_eq!(msg.user_data.as_ref(), &[0x22, 0xF1, 0x90]); + assert_eq!(msg.service_id(), Some(0x22)); + } + + #[test] + fn parse_tester_present() { + // TesterPresent service + let payload = [0x0E, 0x80, 0x10, 0x00, 0x3E, 0x00]; + let msg = Message::parse(&payload).unwrap(); + + assert_eq!(msg.service_id(), Some(0x3E)); + assert_eq!(msg.user_data.len(), 2); + } + + #[test] + fn reject_short_message() { + let short = [0x0E, 0x80, 0x10, 0x00]; // no user data + assert!(Message::parse(&short).is_err()); + } + + #[test] + fn build_diagnostic_message() { + let uds = Bytes::from_static(&[0x22, 0xF1, 0x90]); + let msg = Message::new(0x0E80, 0x1000, uds); + let bytes = msg.to_bytes(); + + assert_eq!(&bytes[0..2], &[0x0E, 0x80]); + assert_eq!(&bytes[2..4], &[0x10, 0x00]); + assert_eq!(&bytes[4..], &[0x22, 0xF1, 0x90]); + } + + #[test] + fn build_positive_ack() { + let ack = PositiveAck::new(0x1000, 0x0E80); + let bytes = ack.to_bytes(); + + assert_eq!(bytes.len(), 5); + assert_eq!(&bytes[0..2], &[0x10, 0x00]); // source (ECU) + assert_eq!(&bytes[2..4], &[0x0E, 0x80]); // target (tester) + assert_eq!(bytes[4], 0x00); // ack code + } + + #[test] + fn build_positive_ack_with_prev_data() { + let prev = Bytes::from_static(&[0x22, 0xF1, 0x90]); + let ack = PositiveAck::with_previous_data(0x1000, 0x0E80, prev); + let bytes = ack.to_bytes(); + + assert_eq!(bytes.len(), 8); + assert_eq!(&bytes[5..], &[0x22, 0xF1, 0x90]); + } + + #[test] + fn build_negative_ack() { + let nack = NegativeAck::new(0x1000, 0x0E80, NackCode::UnknownTargetAddress); + let bytes = nack.to_bytes(); + + assert_eq!(bytes.len(), 5); + assert_eq!(bytes[4], 0x03); + } + + #[test] + fn build_negative_ack_target_unreachable() { + let nack = NegativeAck::new(0x1000, 0x0E80, NackCode::TargetUnreachable); + let bytes = nack.to_bytes(); + assert_eq!(bytes[4], 0x06); + } + + #[test] + fn parse_positive_ack() { + let payload = [0x10, 0x00, 0x0E, 0x80, 0x00]; + let ack = PositiveAck::parse(&payload).unwrap(); + + assert_eq!(ack.source_address, 0x1000); + assert_eq!(ack.target_address, 0x0E80); + assert!(ack.previous_data.is_none()); + } + + #[test] + fn parse_negative_ack() { + let payload = [0x10, 0x00, 0x0E, 0x80, 0x03]; + let nack = NegativeAck::parse(&payload).unwrap(); + + assert_eq!(nack.nack_code, NackCode::UnknownTargetAddress); + } + + #[test] + fn roundtrip_message() { + let original = Message::new(0x0E80, 0x1000, Bytes::from_static(&[0x10, 0x01])); + let bytes = original.to_bytes(); + let parsed = Message::parse(&bytes).unwrap(); + assert_eq!(original, parsed); + } + + #[test] + fn roundtrip_positive_ack() { + let original = PositiveAck::new(0x1000, 0x0E80); + let bytes = original.to_bytes(); + let parsed = PositiveAck::parse(&bytes).unwrap(); + assert_eq!(original, parsed); + } + + #[test] + fn roundtrip_negative_ack() { + let original = NegativeAck::new(0x1000, 0x0E80, NackCode::OutOfMemory); + let bytes = original.to_bytes(); + let parsed = NegativeAck::parse(&bytes).unwrap(); + assert_eq!(original, parsed); + } +} diff --git a/src/doip/header_parser.rs b/src/doip/header_parser.rs new file mode 100644 index 0000000..f489da3 --- /dev/null +++ b/src/doip/header_parser.rs @@ -0,0 +1,768 @@ +//! DoIP Header Parser + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use std::io; +use tokio_util::codec::{Decoder, Encoder}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum GenericNackCode { + IncorrectPatternFormat = 0x00, + UnknownPayloadType = 0x01, + MessageTooLarge = 0x02, + OutOfMemory = 0x03, + InvalidPayloadLength = 0x04, +} + +#[derive(Debug)] +pub enum ParseError { + InvalidHeader(String), + Io(io::Error), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidHeader(msg) => write!(f, "Invalid header: {}", msg), + Self::Io(e) => write!(f, "IO error: {}", e), + } + } +} + +impl std::error::Error for ParseError {} + +impl From for ParseError { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} + +pub type Result = std::result::Result; + +pub const DEFAULT_PROTOCOL_VERSION: u8 = 0x02; +pub const DEFAULT_PROTOCOL_VERSION_INV: u8 = 0xFD; +pub const DOIP_HEADER_LENGTH: usize = 8; +/// Maximum DoIP message size (4MB) - provides DoS protection while allowing +/// large diagnostic data transfers. Can be customized via DoipCodec::with_max_payload_size(). +pub const MAX_DOIP_MESSAGE_SIZE: u32 = 0x0040_0000; // 4MB + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum PayloadType { + GenericNack = 0x0000, + VehicleIdentificationRequest = 0x0001, + VehicleIdentificationRequestWithEid = 0x0002, + VehicleIdentificationRequestWithVin = 0x0003, + VehicleIdentificationResponse = 0x0004, + RoutingActivationRequest = 0x0005, + RoutingActivationResponse = 0x0006, + AliveCheckRequest = 0x0007, + AliveCheckResponse = 0x0008, + DoipEntityStatusRequest = 0x4001, + DoipEntityStatusResponse = 0x4002, + DiagnosticPowerModeRequest = 0x4003, + DiagnosticPowerModeResponse = 0x4004, + DiagnosticMessage = 0x8001, + DiagnosticMessagePositiveAck = 0x8002, + DiagnosticMessageNegativeAck = 0x8003, +} + +impl PayloadType { + pub fn from_u16(value: u16) -> Option { + match value { + 0x0000 => Some(Self::GenericNack), + 0x0001 => Some(Self::VehicleIdentificationRequest), + 0x0002 => Some(Self::VehicleIdentificationRequestWithEid), + 0x0003 => Some(Self::VehicleIdentificationRequestWithVin), + 0x0004 => Some(Self::VehicleIdentificationResponse), + 0x0005 => Some(Self::RoutingActivationRequest), + 0x0006 => Some(Self::RoutingActivationResponse), + 0x0007 => Some(Self::AliveCheckRequest), + 0x0008 => Some(Self::AliveCheckResponse), + 0x4001 => Some(Self::DoipEntityStatusRequest), + 0x4002 => Some(Self::DoipEntityStatusResponse), + 0x4003 => Some(Self::DiagnosticPowerModeRequest), + 0x4004 => Some(Self::DiagnosticPowerModeResponse), + 0x8001 => Some(Self::DiagnosticMessage), + 0x8002 => Some(Self::DiagnosticMessagePositiveAck), + 0x8003 => Some(Self::DiagnosticMessageNegativeAck), + _ => None, + } + } + + pub const fn min_payload_length(self) -> usize { + match self { + Self::GenericNack => 1, + Self::VehicleIdentificationRequest => 0, + Self::VehicleIdentificationRequestWithEid => 6, + Self::VehicleIdentificationRequestWithVin => 17, + Self::VehicleIdentificationResponse => 32, + Self::RoutingActivationRequest => 7, + Self::RoutingActivationResponse => 9, + Self::AliveCheckRequest => 0, + Self::AliveCheckResponse => 2, + Self::DoipEntityStatusRequest => 0, + Self::DoipEntityStatusResponse => 3, + Self::DiagnosticPowerModeRequest => 0, + Self::DiagnosticPowerModeResponse => 1, + Self::DiagnosticMessage => 5, + Self::DiagnosticMessagePositiveAck => 5, + Self::DiagnosticMessageNegativeAck => 5, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DoipHeader { + pub version: u8, + pub inverse_version: u8, + pub payload_type: u16, + pub payload_length: u32, +} + +impl DoipHeader { + pub fn parse(data: &[u8]) -> Result { + if data.len() < DOIP_HEADER_LENGTH { + return Err(ParseError::InvalidHeader(format!( + "header too short: expected {}, got {}", + DOIP_HEADER_LENGTH, + data.len() + ))); + } + Ok(Self { + version: data[0], + inverse_version: data[1], + payload_type: u16::from_be_bytes([data[2], data[3]]), + payload_length: u32::from_be_bytes([data[4], data[5], data[6], data[7]]), + }) + } + + pub fn parse_from_buf(buf: &mut Bytes) -> Result { + if buf.len() < DOIP_HEADER_LENGTH { + return Err(ParseError::InvalidHeader(format!( + "header too short: expected {}, got {}", + DOIP_HEADER_LENGTH, + buf.len() + ))); + } + Ok(Self { + version: buf.get_u8(), + inverse_version: buf.get_u8(), + payload_type: buf.get_u16(), + payload_length: buf.get_u32(), + }) + } + + pub fn validate(&self) -> Option { + if self.version != DEFAULT_PROTOCOL_VERSION { + return Some(GenericNackCode::IncorrectPatternFormat); + } + if self.inverse_version != DEFAULT_PROTOCOL_VERSION_INV { + return Some(GenericNackCode::IncorrectPatternFormat); + } + if self.version ^ self.inverse_version != 0xFF { + return Some(GenericNackCode::IncorrectPatternFormat); + } + + let payload_type = match PayloadType::from_u16(self.payload_type) { + Some(pt) => pt, + None => return Some(GenericNackCode::UnknownPayloadType), + }; + + if self.payload_length > MAX_DOIP_MESSAGE_SIZE { + return Some(GenericNackCode::MessageTooLarge); + } + if (self.payload_length as usize) < payload_type.min_payload_length() { + return Some(GenericNackCode::InvalidPayloadLength); + } + None + } + + pub fn is_valid(&self) -> bool { + self.validate().is_none() + } + + pub const fn total_length(&self) -> usize { + DOIP_HEADER_LENGTH + self.payload_length as usize + } + + pub fn to_bytes(&self) -> Bytes { + let mut buf = BytesMut::with_capacity(DOIP_HEADER_LENGTH); + buf.put_u8(self.version); + buf.put_u8(self.inverse_version); + buf.put_u16(self.payload_type); + buf.put_u32(self.payload_length); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u8(self.version); + buf.put_u8(self.inverse_version); + buf.put_u16(self.payload_type); + buf.put_u32(self.payload_length); + } +} + +impl Default for DoipHeader { + fn default() -> Self { + Self { + version: DEFAULT_PROTOCOL_VERSION, + inverse_version: DEFAULT_PROTOCOL_VERSION_INV, + payload_type: 0, + payload_length: 0, + } + } +} + +impl std::fmt::Display for DoipHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let payload_name = PayloadType::from_u16(self.payload_type) + .map(|pt| format!("{:?}", pt)) + .unwrap_or_else(|| format!("Unknown(0x{:04X})", self.payload_type)); + write!( + f, + "DoipHeader {{ version: 0x{:02X}, type: {}, length: {} }}", + self.version, payload_name, self.payload_length + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DoipMessage { + pub header: DoipHeader, + pub payload: Bytes, +} + +impl DoipMessage { + pub fn new(payload_type: PayloadType, payload: Bytes) -> Self { + Self { + header: DoipHeader { + version: DEFAULT_PROTOCOL_VERSION, + inverse_version: DEFAULT_PROTOCOL_VERSION_INV, + payload_type: payload_type as u16, + payload_length: payload.len() as u32, + }, + payload, + } + } + + pub fn with_raw_type(payload_type: u16, payload: Bytes) -> Self { + Self { + header: DoipHeader { + version: DEFAULT_PROTOCOL_VERSION, + inverse_version: DEFAULT_PROTOCOL_VERSION_INV, + payload_type, + payload_length: payload.len() as u32, + }, + payload, + } + } + + pub fn payload_type(&self) -> Option { + PayloadType::from_u16(self.header.payload_type) + } + + pub fn total_length(&self) -> usize { + DOIP_HEADER_LENGTH + self.payload.len() + } + + pub fn to_bytes(&self) -> Bytes { + let mut buf = BytesMut::with_capacity(self.total_length()); + self.header.write_to(&mut buf); + buf.extend_from_slice(&self.payload); + buf.freeze() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DecodeState { + Header, + Payload(DoipHeader), +} + +#[derive(Debug)] +pub struct DoipCodec { + state: DecodeState, + max_payload_size: u32, +} + +impl DoipCodec { + pub fn new() -> Self { + Self { + state: DecodeState::Header, + max_payload_size: MAX_DOIP_MESSAGE_SIZE, + } + } + + pub fn with_max_payload_size(max_size: u32) -> Self { + Self { + state: DecodeState::Header, + max_payload_size: max_size, + } + } +} + +impl Default for DoipCodec { + fn default() -> Self { + Self::new() + } +} + +impl Decoder for DoipCodec { + type Item = DoipMessage; + type Error = io::Error; + + fn decode( + &mut self, + src: &mut BytesMut, + ) -> std::result::Result, Self::Error> { + loop { + match self.state { + DecodeState::Header => { + if src.len() < DOIP_HEADER_LENGTH { + src.reserve(DOIP_HEADER_LENGTH); + return Ok(None); + } + + let header = DoipHeader::parse(&src[..DOIP_HEADER_LENGTH]) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + if let Some(nack_code) = header.validate() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("validation failed: {:?}", nack_code), + )); + } + + if header.payload_length > self.max_payload_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "payload too large: {} > {}", + header.payload_length, self.max_payload_size + ), + )); + } + + src.reserve(header.total_length()); + self.state = DecodeState::Payload(header); + } + + DecodeState::Payload(header) => { + if src.len() < header.total_length() { + return Ok(None); + } + + let _ = src.split_to(DOIP_HEADER_LENGTH); + let payload = src.split_to(header.payload_length as usize).freeze(); + + self.state = DecodeState::Header; + return Ok(Some(DoipMessage { header, payload })); + } + } + } + } +} + +impl Encoder for DoipCodec { + type Error = io::Error; + + fn encode( + &mut self, + item: DoipMessage, + dst: &mut BytesMut, + ) -> std::result::Result<(), Self::Error> { + dst.reserve(item.total_length()); + item.header.write_to(dst); + dst.extend_from_slice(&item.payload); + Ok(()) + } +} + +// ============================================================================ +// Unit Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + // --- Helper to build a valid DoIP header quickly --- + fn make_header(payload_type: u16, payload_len: u32) -> DoipHeader { + DoipHeader { + version: 0x02, + inverse_version: 0xFD, + payload_type, + payload_length: payload_len, + } + } + + // ------------------------------------------------------------------------- + // Basic header parsing - the bread and butter + // ------------------------------------------------------------------------- + + #[test] + fn parse_vehicle_id_request_from_tester() { + // Real-world: tester broadcasts "who's there?" on UDP + let raw = [0x02, 0xFD, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]; + let hdr = DoipHeader::parse(&raw).unwrap(); + + assert_eq!(hdr.payload_type, 0x0001); + assert_eq!(hdr.payload_length, 0); + assert!(hdr.is_valid()); + } + + #[test] + fn parse_diagnostic_message_with_uds_payload() { + // Tester sends UDS request: SA=0x0E80, TA=0x1001, SID=0x22 (ReadDataByID) + let raw = [0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x07]; + let hdr = DoipHeader::parse(&raw).unwrap(); + + assert_eq!(hdr.payload_type, 0x8001); // DiagnosticMessage + assert_eq!(hdr.payload_length, 7); // 2+2+3 = SA+TA+UDS + } + + #[test] + fn parse_routing_activation_request() { + // Tester wants to start a diagnostic session + let raw = [0x02, 0xFD, 0x00, 0x05, 0x00, 0x00, 0x00, 0x07]; + let hdr = DoipHeader::parse(&raw).unwrap(); + + assert_eq!(hdr.payload_type, 0x0005); + assert!(hdr.is_valid()); + } + + #[test] + fn reject_truncated_header() { + // Only 4 bytes arrived - not enough + let partial = [0x02, 0xFD, 0x00, 0x01]; + assert!(DoipHeader::parse(&partial).is_err()); + } + + #[test] + fn extra_bytes_after_header_are_ignored() { + // Header + some payload bytes mixed in + let raw = [0x02, 0xFD, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD]; + let hdr = DoipHeader::parse(&raw).unwrap(); + assert_eq!(hdr.payload_length, 0); // Parses only header + } + + // ------------------------------------------------------------------------- + // Validation - reject bad packets before processing + // ------------------------------------------------------------------------- + + #[test] + fn reject_wrong_protocol_version() { + // Someone sends version 0x03 - we only support 0x02 + let hdr = DoipHeader { + version: 0x03, + inverse_version: 0xFC, + payload_type: 0x0001, + payload_length: 0, + }; + assert_eq!(hdr.validate(), Some(GenericNackCode::IncorrectPatternFormat)); + } + + #[test] + fn reject_corrupted_inverse_version() { + // Inverse should be 0xFD for version 0x02, but we got 0xFC + let hdr = DoipHeader { + version: 0x02, + inverse_version: 0xFC, // Wrong! + payload_type: 0x0001, + payload_length: 0, + }; + assert_eq!(hdr.validate(), Some(GenericNackCode::IncorrectPatternFormat)); + } + + #[test] + fn reject_unknown_payload_type() { + // 0x1234 is not a valid DoIP payload type + let hdr = make_header(0x1234, 0); + assert_eq!(hdr.validate(), Some(GenericNackCode::UnknownPayloadType)); + } + + #[test] + fn reject_oversized_message() { + // Payload claims to be 256MB - way too big + let hdr = make_header(0x8001, MAX_DOIP_MESSAGE_SIZE + 1); + assert_eq!(hdr.validate(), Some(GenericNackCode::MessageTooLarge)); + } + + #[test] + fn reject_diagnostic_msg_with_too_small_payload() { + // DiagnosticMessage needs at least 5 bytes (SA + TA + 1 UDS byte) + let hdr = make_header(0x8001, 3); + assert_eq!(hdr.validate(), Some(GenericNackCode::InvalidPayloadLength)); + } + + #[test] + fn accept_max_allowed_payload_size() { + let hdr = make_header(0x8001, MAX_DOIP_MESSAGE_SIZE); + assert!(hdr.is_valid()); + } + + // ------------------------------------------------------------------------- + // PayloadType enum - mapping values correctly + // ------------------------------------------------------------------------- + + #[test] + fn payload_type_lookup_works() { + assert_eq!(PayloadType::from_u16(0x0001), Some(PayloadType::VehicleIdentificationRequest)); + assert_eq!(PayloadType::from_u16(0x0005), Some(PayloadType::RoutingActivationRequest)); + assert_eq!(PayloadType::from_u16(0x8001), Some(PayloadType::DiagnosticMessage)); + assert_eq!(PayloadType::from_u16(0x8002), Some(PayloadType::DiagnosticMessagePositiveAck)); + } + + #[test] + fn payload_type_gaps_return_none() { + // These are in gaps between valid ranges + assert!(PayloadType::from_u16(0x0009).is_none()); + assert!(PayloadType::from_u16(0x4000).is_none()); + assert!(PayloadType::from_u16(0x8000).is_none()); + assert!(PayloadType::from_u16(0xFFFF).is_none()); + } + + #[test] + fn minimum_payload_lengths_per_spec() { + // ISO 13400-2 requirements + assert_eq!(PayloadType::VehicleIdentificationRequest.min_payload_length(), 0); + assert_eq!(PayloadType::RoutingActivationRequest.min_payload_length(), 7); + assert_eq!(PayloadType::DiagnosticMessage.min_payload_length(), 5); + assert_eq!(PayloadType::AliveCheckResponse.min_payload_length(), 2); + } + + // ------------------------------------------------------------------------- + // DoipMessage - wrapping header + payload together + // ------------------------------------------------------------------------- + + #[test] + fn create_tester_present_message() { + // UDS TesterPresent: 0x3E 0x00 + let uds = Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x3E, 0x00]); + let msg = DoipMessage::new(PayloadType::DiagnosticMessage, uds); + + assert_eq!(msg.header.payload_type, 0x8001); + assert_eq!(msg.header.payload_length, 6); + assert_eq!(msg.payload_type(), Some(PayloadType::DiagnosticMessage)); + } + + #[test] + fn create_vehicle_id_broadcast() { + // Empty payload for discovery + let msg = DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()); + + assert_eq!(msg.header.payload_length, 0); + assert_eq!(msg.total_length(), 8); // Just the header + } + + #[test] + fn message_with_unknown_type() { + // For testing/fuzzing - create msg with invalid type + let msg = DoipMessage::with_raw_type(0xBEEF, Bytes::new()); + assert_eq!(msg.payload_type(), None); + } + + #[test] + fn serialize_message_to_wire_format() { + let msg = DoipMessage::new( + PayloadType::AliveCheckRequest, + Bytes::new() + ); + let wire = msg.to_bytes(); + + assert_eq!(wire.len(), 8); + assert_eq!(&wire[..4], &[0x02, 0xFD, 0x00, 0x07]); // Version + type + assert_eq!(&wire[4..8], &[0x00, 0x00, 0x00, 0x00]); // Length = 0 + } + + // ------------------------------------------------------------------------- + // Codec - TCP stream framing + // ------------------------------------------------------------------------- + + #[test] + fn decode_complete_alive_check_response() { + let mut codec = DoipCodec::new(); + // AliveCheckResponse with source address 0x0E80 + let mut buf = BytesMut::from(&[ + 0x02, 0xFD, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, + 0x0E, 0x80 + ][..]); + + let msg = codec.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.header.payload_type, 0x0008); + assert_eq!(msg.payload.as_ref(), &[0x0E, 0x80]); + assert!(buf.is_empty()); // Consumed everything + } + + #[test] + fn wait_for_more_data_when_header_incomplete() { + let mut codec = DoipCodec::new(); + let mut buf = BytesMut::from(&[0x02, 0xFD, 0x00][..]); // Only 3 bytes + + assert!(codec.decode(&mut buf).unwrap().is_none()); + assert_eq!(buf.len(), 3); // Nothing consumed + } + + #[test] + fn wait_for_more_data_when_payload_incomplete() { + let mut codec = DoipCodec::new(); + // Header says 5 bytes payload, but only 2 arrived + let mut buf = BytesMut::from(&[ + 0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x05, + 0x0E, 0x80 + ][..]); + + assert!(codec.decode(&mut buf).unwrap().is_none()); + } + + #[test] + fn decode_back_to_back_messages() { + let mut codec = DoipCodec::new(); + let mut buf = BytesMut::from(&[ + // Msg 1: AliveCheckRequest + 0x02, 0xFD, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + // Msg 2: AliveCheckResponse + 0x02, 0xFD, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x0E, 0x80, + ][..]); + + let m1 = codec.decode(&mut buf).unwrap().unwrap(); + let m2 = codec.decode(&mut buf).unwrap().unwrap(); + + assert_eq!(m1.header.payload_type, 0x0007); + assert_eq!(m2.header.payload_type, 0x0008); + assert!(buf.is_empty()); + } + + #[test] + fn reject_invalid_header_in_stream() { + let mut codec = DoipCodec::new(); + // Bad version + let mut buf = BytesMut::from(&[ + 0x01, 0xFE, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 + ][..]); + + assert!(codec.decode(&mut buf).is_err()); + } + + #[test] + fn respect_custom_max_payload_size() { + let mut codec = DoipCodec::with_max_payload_size(100); + // Payload length = 101, over our limit + let mut buf = BytesMut::from(&[ + 0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x65 + ][..]); + + assert!(codec.decode(&mut buf).is_err()); + } + + #[test] + fn encode_diagnostic_message() { + let mut codec = DoipCodec::new(); + let payload = Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x3E]); + let msg = DoipMessage::new(PayloadType::DiagnosticMessage, payload); + + let mut buf = BytesMut::new(); + codec.encode(msg, &mut buf).unwrap(); + + assert_eq!(&buf[..4], &[0x02, 0xFD, 0x80, 0x01]); + assert_eq!(&buf[4..8], &[0x00, 0x00, 0x00, 0x05]); + assert_eq!(&buf[8..], &[0x0E, 0x80, 0x10, 0x01, 0x3E]); + } + + // ------------------------------------------------------------------------- + // Round-trip: encode then decode should give same data + // ------------------------------------------------------------------------- + + #[test] + fn roundtrip_diagnostic_message() { + let mut codec = DoipCodec::new(); + let original = DoipMessage::new( + PayloadType::DiagnosticMessage, + Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x22, 0xF1, 0x90]) + ); + + let mut buf = BytesMut::new(); + codec.encode(original.clone(), &mut buf).unwrap(); + let decoded = codec.decode(&mut buf).unwrap().unwrap(); + + assert_eq!(original.header, decoded.header); + assert_eq!(original.payload, decoded.payload); + } + + #[test] + fn roundtrip_header_only() { + let original = make_header(0x8001, 42); + let bytes = original.to_bytes(); + let parsed = DoipHeader::parse(&bytes).unwrap(); + + assert_eq!(original, parsed); + } + + // ------------------------------------------------------------------------- + // Edge cases and error handling + // ------------------------------------------------------------------------- + + #[test] + fn handle_all_zeros_gracefully() { + let garbage = [0x00; 8]; + let hdr = DoipHeader::parse(&garbage).unwrap(); + assert!(!hdr.is_valid()); // Wrong version, but doesn't panic + } + + #[test] + fn handle_all_ones_gracefully() { + let garbage = [0xFF; 8]; + let hdr = DoipHeader::parse(&garbage).unwrap(); + assert!(!hdr.is_valid()); + } + + #[test] + fn parse_error_shows_useful_message() { + let err = ParseError::InvalidHeader("buffer too short".into()); + let msg = format!("{}", err); + assert!(msg.contains("Invalid header")); + assert!(msg.contains("buffer too short")); + } + + #[test] + fn io_errors_convert_to_parse_errors() { + let io_err = io::Error::new(io::ErrorKind::UnexpectedEof, "connection lost"); + let parse_err: ParseError = io_err.into(); + match parse_err { + ParseError::Io(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof), + _ => panic!("expected Io variant"), + } + } + + #[test] + fn nack_codes_have_correct_values() { + // Per ISO 13400-2 Table 17 + assert_eq!(GenericNackCode::IncorrectPatternFormat as u8, 0x00); + assert_eq!(GenericNackCode::UnknownPayloadType as u8, 0x01); + assert_eq!(GenericNackCode::MessageTooLarge as u8, 0x02); + assert_eq!(GenericNackCode::OutOfMemory as u8, 0x03); + assert_eq!(GenericNackCode::InvalidPayloadLength as u8, 0x04); + } + + #[test] + fn protocol_version_inverse_relationship() { + // Version XOR inverse must equal 0xFF (per spec) + assert_eq!(DEFAULT_PROTOCOL_VERSION ^ DEFAULT_PROTOCOL_VERSION_INV, 0xFF); + } + + #[test] + fn header_display_shows_readable_info() { + let hdr = make_header(0x8001, 10); + let s = format!("{}", hdr); + assert!(s.contains("DiagnosticMessage")); + assert!(s.contains("10")); // payload length + } + + #[test] + fn default_header_has_correct_version() { + let hdr = DoipHeader::default(); + assert_eq!(hdr.version, 0x02); + assert_eq!(hdr.inverse_version, 0xFD); + } +} diff --git a/src/doip/mod.rs b/src/doip/mod.rs index 943705e..dfebf91 100644 --- a/src/doip/mod.rs +++ b/src/doip/mod.rs @@ -1,9 +1,14 @@ -//! DoIP Protocol Implementation (ISO 13400-2) +//! DoIP Protocol Implementation +//! +//! This module provides the core DoIP protocol types and codec for TCP/UDP communication. -pub mod hearder_parser; +pub mod header_parser; +pub mod routing_activation; +pub mod diagnostic_message; +pub mod vehicle_id; +pub mod alive_check; -// Re-export commonly used types -pub use hearder_parser::{ +pub use header_parser::{ DoipCodec, DoipHeader, DoipMessage, GenericNackCode, ParseError, PayloadType, Result, DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, MAX_DOIP_MESSAGE_SIZE, diff --git a/src/doip/routing_activation.rs b/src/doip/routing_activation.rs new file mode 100644 index 0000000..1628b86 --- /dev/null +++ b/src/doip/routing_activation.rs @@ -0,0 +1,373 @@ +//! Routing Activation handlers (ISO 13400-2) + +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +// Response codes per ISO 13400-2 Table 25 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ResponseCode { + UnknownSourceAddress = 0x00, + AllSocketsRegistered = 0x01, + DifferentSourceAddress = 0x02, + SourceAddressAlreadyActive = 0x03, + MissingAuthentication = 0x04, + RejectedConfirmation = 0x05, + UnsupportedActivationType = 0x06, + TlsRequired = 0x07, + SuccessfullyActivated = 0x10, + ConfirmationRequired = 0x11, +} + +impl ResponseCode { + pub fn from_u8(value: u8) -> Option { + match value { + 0x00 => Some(Self::UnknownSourceAddress), + 0x01 => Some(Self::AllSocketsRegistered), + 0x02 => Some(Self::DifferentSourceAddress), + 0x03 => Some(Self::SourceAddressAlreadyActive), + 0x04 => Some(Self::MissingAuthentication), + 0x05 => Some(Self::RejectedConfirmation), + 0x06 => Some(Self::UnsupportedActivationType), + 0x07 => Some(Self::TlsRequired), + 0x10 => Some(Self::SuccessfullyActivated), + 0x11 => Some(Self::ConfirmationRequired), + _ => None, + } + } + + pub fn is_success(self) -> bool { + matches!(self, Self::SuccessfullyActivated | Self::ConfirmationRequired) + } +} + +// Activation types per ISO 13400-2 Table 24 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ActivationType { + Default = 0x00, + WwhObd = 0x01, + CentralSecurity = 0xE0, +} + +impl ActivationType { + pub fn from_u8(value: u8) -> Option { + match value { + 0x00 => Some(Self::Default), + 0x01 => Some(Self::WwhObd), + 0xE0 => Some(Self::CentralSecurity), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + PayloadTooShort { expected: usize, actual: usize }, + UnknownResponseCode(u8), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PayloadTooShort { expected, actual } => { + write!(f, "payload too short: need {} bytes, got {}", expected, actual) + } + Self::UnknownResponseCode(code) => write!(f, "unknown response code: 0x{:02X}", code), + } + } +} + +impl std::error::Error for Error {} + +// Routing Activation Request - payload is 7 bytes min, 11 with OEM data +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Request { + pub source_address: u16, + pub activation_type: u8, + pub reserved: u32, + pub oem_specific: Option, +} + +impl Request { + pub const MIN_LEN: usize = 7; + pub const MAX_LEN: usize = 11; + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: payload.len(), + }); + } + + let source_address = u16::from_be_bytes([payload[0], payload[1]]); + let activation_type = payload[2]; + let reserved = u32::from_be_bytes([payload[3], payload[4], payload[5], payload[6]]); + + let oem_specific = if payload.len() >= Self::MAX_LEN { + Some(u32::from_be_bytes([payload[7], payload[8], payload[9], payload[10]])) + } else { + None + }; + + Ok(Self { + source_address, + activation_type, + reserved, + oem_specific, + }) + } + + pub fn parse_buf(buf: &mut Bytes) -> Result { + if buf.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: buf.len(), + }); + } + + let source_address = buf.get_u16(); + let activation_type = buf.get_u8(); + let reserved = buf.get_u32(); + let oem_specific = if buf.remaining() >= 4 { Some(buf.get_u32()) } else { None }; + + Ok(Self { + source_address, + activation_type, + reserved, + oem_specific, + }) + } + + pub fn activation_type_enum(&self) -> Option { + ActivationType::from_u8(self.activation_type) + } + + pub fn validate(&self) -> Option { + if ActivationType::from_u8(self.activation_type).is_none() { + return Some(ResponseCode::UnsupportedActivationType); + } + None + } +} + +// Routing Activation Response - 9 bytes min, 13 with OEM data +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Response { + pub tester_address: u16, + pub entity_address: u16, + pub response_code: ResponseCode, + pub reserved: u32, + pub oem_specific: Option, +} + +impl Response { + pub const MIN_LEN: usize = 9; + pub const MAX_LEN: usize = 13; + + pub fn success(tester_address: u16, entity_address: u16) -> Self { + Self { + tester_address, + entity_address, + response_code: ResponseCode::SuccessfullyActivated, + reserved: 0, + oem_specific: None, + } + } + + pub fn denial(tester_address: u16, entity_address: u16, code: ResponseCode) -> Self { + Self { + tester_address, + entity_address, + response_code: code, + reserved: 0, + oem_specific: None, + } + } + + pub fn to_bytes(&self) -> Bytes { + let len = if self.oem_specific.is_some() { Self::MAX_LEN } else { Self::MIN_LEN }; + let mut buf = BytesMut::with_capacity(len); + self.write_to(&mut buf); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.tester_address); + buf.put_u16(self.entity_address); + buf.put_u8(self.response_code as u8); + buf.put_u32(self.reserved); + if let Some(oem) = self.oem_specific { + buf.put_u32(oem); + } + } + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: payload.len(), + }); + } + + let tester_address = u16::from_be_bytes([payload[0], payload[1]]); + let entity_address = u16::from_be_bytes([payload[2], payload[3]]); + let response_code = ResponseCode::from_u8(payload[4]) + .ok_or(Error::UnknownResponseCode(payload[4]))?; + let reserved = u32::from_be_bytes([payload[5], payload[6], payload[7], payload[8]]); + + let oem_specific = if payload.len() >= Self::MAX_LEN { + Some(u32::from_be_bytes([payload[9], payload[10], payload[11], payload[12]])) + } else { + None + }; + + Ok(Self { + tester_address, + entity_address, + response_code, + reserved, + oem_specific, + }) + } + + pub fn is_success(&self) -> bool { + self.response_code.is_success() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_code_success_check() { + assert!(ResponseCode::SuccessfullyActivated.is_success()); + assert!(ResponseCode::ConfirmationRequired.is_success()); + assert!(!ResponseCode::UnknownSourceAddress.is_success()); + assert!(!ResponseCode::TlsRequired.is_success()); + } + + #[test] + fn response_code_values() { + assert_eq!(ResponseCode::UnknownSourceAddress as u8, 0x00); + assert_eq!(ResponseCode::SuccessfullyActivated as u8, 0x10); + assert_eq!(ResponseCode::ConfirmationRequired as u8, 0x11); + } + + #[test] + fn parse_minimal_request() { + let payload = [0x0E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00]; + let req = Request::parse(&payload).unwrap(); + + assert_eq!(req.source_address, 0x0E80); + assert_eq!(req.activation_type, 0x00); + assert_eq!(req.reserved, 0); + assert!(req.oem_specific.is_none()); + } + + #[test] + fn parse_request_with_oem() { + let payload = [ + 0x0E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xDE, 0xAD, 0xBE, 0xEF, + ]; + let req = Request::parse(&payload).unwrap(); + assert_eq!(req.oem_specific, Some(0xDEADBEEF)); + } + + #[test] + fn parse_wwh_obd_request() { + let payload = [0x0F, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]; + let req = Request::parse(&payload).unwrap(); + assert_eq!(req.activation_type_enum(), Some(ActivationType::WwhObd)); + } + + #[test] + fn reject_short_request() { + let short = [0x0E, 0x80, 0x00, 0x00]; + assert!(Request::parse(&short).is_err()); + } + + #[test] + fn validate_bad_activation_type() { + let payload = [0x0E, 0x80, 0x99, 0x00, 0x00, 0x00, 0x00]; + let req = Request::parse(&payload).unwrap(); + assert_eq!(req.validate(), Some(ResponseCode::UnsupportedActivationType)); + } + + #[test] + fn build_success_response() { + let resp = Response::success(0x0E80, 0x1000); + assert_eq!(resp.tester_address, 0x0E80); + assert_eq!(resp.entity_address, 0x1000); + assert!(resp.is_success()); + } + + #[test] + fn build_denial_response() { + let resp = Response::denial(0x0E80, 0x1000, ResponseCode::AllSocketsRegistered); + assert!(!resp.is_success()); + } + + #[test] + fn serialize_response() { + let resp = Response::success(0x0E80, 0x1000); + let bytes = resp.to_bytes(); + + assert_eq!(bytes.len(), 9); + assert_eq!(&bytes[0..2], &[0x0E, 0x80]); + assert_eq!(&bytes[2..4], &[0x10, 0x00]); + assert_eq!(bytes[4], 0x10); + } + + #[test] + fn serialize_response_with_oem() { + let mut resp = Response::success(0x0E80, 0x1000); + resp.oem_specific = Some(0x12345678); + let bytes = resp.to_bytes(); + + assert_eq!(bytes.len(), 13); + assert_eq!(&bytes[9..13], &[0x12, 0x34, 0x56, 0x78]); + } + + #[test] + fn parse_success_response() { + let payload = [ + 0x0E, 0x80, 0x10, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, + ]; + let resp = Response::parse(&payload).unwrap(); + assert!(resp.is_success()); + assert_eq!(resp.tester_address, 0x0E80); + assert_eq!(resp.entity_address, 0x1000); + } + + #[test] + fn parse_denial_response() { + let payload = [ + 0x0E, 0x80, 0x10, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + ]; + let resp = Response::parse(&payload).unwrap(); + assert!(!resp.is_success()); + assert_eq!(resp.response_code, ResponseCode::AllSocketsRegistered); + } + + #[test] + fn roundtrip_response() { + let original = Response::success(0x0E80, 0x1000); + let bytes = original.to_bytes(); + let parsed = Response::parse(&bytes).unwrap(); + assert_eq!(original, parsed); + } + + #[test] + fn roundtrip_response_with_oem() { + let mut original = Response::denial(0x0F00, 0x2000, ResponseCode::MissingAuthentication); + original.oem_specific = Some(0xCAFEBABE); + let bytes = original.to_bytes(); + let parsed = Response::parse(&bytes).unwrap(); + assert_eq!(original, parsed); + } +} diff --git a/src/doip/vehicle_id.rs b/src/doip/vehicle_id.rs new file mode 100644 index 0000000..101e382 --- /dev/null +++ b/src/doip/vehicle_id.rs @@ -0,0 +1,331 @@ +//! Vehicle Identification handlers (ISO 13400-2) + +use bytes::{BufMut, Bytes, BytesMut}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + PayloadTooShort { expected: usize, actual: usize }, + InvalidVinLength(usize), + InvalidEidLength(usize), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PayloadTooShort { expected, actual } => { + write!(f, "payload too short: need {} bytes, got {}", expected, actual) + } + Self::InvalidVinLength(len) => write!(f, "VIN must be 17 bytes, got {}", len), + Self::InvalidEidLength(len) => write!(f, "EID must be 6 bytes, got {}", len), + } + } +} + +impl std::error::Error for Error {} + +// Vehicle Identification Request (0x0001) - no payload +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Request; + +impl Request { + pub fn parse(_payload: &[u8]) -> Result { + Ok(Self) + } +} + +// Vehicle Identification Request with EID (0x0002) - 6 byte EID +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RequestWithEid { + pub eid: [u8; 6], +} + +impl RequestWithEid { + pub const LEN: usize = 6; + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::LEN { + return Err(Error::PayloadTooShort { + expected: Self::LEN, + actual: payload.len(), + }); + } + + let mut eid = [0u8; 6]; + eid.copy_from_slice(&payload[0..6]); + Ok(Self { eid }) + } + + pub fn new(eid: [u8; 6]) -> Self { + Self { eid } + } +} + +// Vehicle Identification Request with VIN (0x0003) - 17 byte VIN +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RequestWithVin { + pub vin: [u8; 17], +} + +impl RequestWithVin { + pub const LEN: usize = 17; + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::LEN { + return Err(Error::PayloadTooShort { + expected: Self::LEN, + actual: payload.len(), + }); + } + + let mut vin = [0u8; 17]; + vin.copy_from_slice(&payload[0..17]); + Ok(Self { vin }) + } + + pub fn new(vin: [u8; 17]) -> Self { + Self { vin } + } + + pub fn vin_string(&self) -> String { + String::from_utf8_lossy(&self.vin).to_string() + } +} + +// Further action codes for vehicle identification response +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum FurtherAction { + NoFurtherAction = 0x00, + RoutingActivationRequired = 0x10, +} + +// Sync status for vehicle identification response +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SyncStatus { + Synchronized = 0x00, + NotSynchronized = 0x10, +} + +// Vehicle Identification Response (0x0004) +// VIN(17) + LogicalAddr(2) + EID(6) + GID(6) + FurtherAction(1) = 32 bytes min +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Response { + pub vin: [u8; 17], + pub logical_address: u16, + pub eid: [u8; 6], + pub gid: [u8; 6], + pub further_action: FurtherAction, + pub sync_status: Option, +} + +impl Response { + pub const MIN_LEN: usize = 32; // without sync status + pub const MAX_LEN: usize = 33; // with sync status + + pub fn new(vin: [u8; 17], logical_address: u16, eid: [u8; 6], gid: [u8; 6]) -> Self { + Self { + vin, + logical_address, + eid, + gid, + further_action: FurtherAction::NoFurtherAction, + sync_status: None, + } + } + + pub fn with_routing_required(mut self) -> Self { + self.further_action = FurtherAction::RoutingActivationRequired; + self + } + + pub fn with_sync_status(mut self, status: SyncStatus) -> Self { + self.sync_status = Some(status); + self + } + + pub fn to_bytes(&self) -> Bytes { + let len = if self.sync_status.is_some() { Self::MAX_LEN } else { Self::MIN_LEN }; + let mut buf = BytesMut::with_capacity(len); + self.write_to(&mut buf); + buf.freeze() + } + + pub fn write_to(&self, buf: &mut BytesMut) { + buf.extend_from_slice(&self.vin); + buf.put_u16(self.logical_address); + buf.extend_from_slice(&self.eid); + buf.extend_from_slice(&self.gid); + buf.put_u8(self.further_action as u8); + if let Some(status) = self.sync_status { + buf.put_u8(status as u8); + } + } + + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < Self::MIN_LEN { + return Err(Error::PayloadTooShort { + expected: Self::MIN_LEN, + actual: payload.len(), + }); + } + + let mut vin = [0u8; 17]; + vin.copy_from_slice(&payload[0..17]); + + let logical_address = u16::from_be_bytes([payload[17], payload[18]]); + + let mut eid = [0u8; 6]; + eid.copy_from_slice(&payload[19..25]); + + let mut gid = [0u8; 6]; + gid.copy_from_slice(&payload[25..31]); + + let further_action = match payload[31] { + 0x10 => FurtherAction::RoutingActivationRequired, + _ => FurtherAction::NoFurtherAction, + }; + + let sync_status = if payload.len() >= Self::MAX_LEN { + Some(match payload[32] { + 0x10 => SyncStatus::NotSynchronized, + _ => SyncStatus::Synchronized, + }) + } else { + None + }; + + Ok(Self { + vin, + logical_address, + eid, + gid, + further_action, + sync_status, + }) + } + + pub fn vin_string(&self) -> String { + String::from_utf8_lossy(&self.vin).to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_basic_request() { + let req = Request::parse(&[]).unwrap(); + assert_eq!(req, Request); + } + + #[test] + fn parse_request_with_eid() { + let payload = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; + let req = RequestWithEid::parse(&payload).unwrap(); + assert_eq!(req.eid, [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]); + } + + #[test] + fn reject_short_eid_request() { + let short = [0x00, 0x1A, 0x2B]; + assert!(RequestWithEid::parse(&short).is_err()); + } + + #[test] + fn parse_request_with_vin() { + let vin = b"WVWZZZ3CZWE123456"; + let req = RequestWithVin::parse(vin).unwrap(); + assert_eq!(req.vin_string(), "WVWZZZ3CZWE123456"); + } + + #[test] + fn reject_short_vin_request() { + let short = b"WVWZZZ"; + assert!(RequestWithVin::parse(short).is_err()); + } + + #[test] + fn build_basic_response() { + let vin = *b"WVWZZZ3CZWE123456"; + let eid = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; + let gid = [0x00, 0x00, 0x00, 0x00, 0x00, 0x01]; + + let resp = Response::new(vin, 0x1000, eid, gid); + + assert_eq!(resp.logical_address, 0x1000); + assert_eq!(resp.further_action, FurtherAction::NoFurtherAction); + assert!(resp.sync_status.is_none()); + } + + #[test] + fn build_response_with_routing_required() { + let vin = *b"WVWZZZ3CZWE123456"; + let eid = [0; 6]; + let gid = [0; 6]; + + let resp = Response::new(vin, 0x1000, eid, gid).with_routing_required(); + assert_eq!(resp.further_action, FurtherAction::RoutingActivationRequired); + } + + #[test] + fn serialize_response_minimal() { + let vin = *b"WVWZZZ3CZWE123456"; + let eid = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; + let gid = [0x00, 0x00, 0x00, 0x00, 0x00, 0x01]; + + let resp = Response::new(vin, 0x1000, eid, gid); + let bytes = resp.to_bytes(); + + assert_eq!(bytes.len(), 32); + assert_eq!(&bytes[0..17], b"WVWZZZ3CZWE123456"); + assert_eq!(&bytes[17..19], &[0x10, 0x00]); // logical address + } + + #[test] + fn serialize_response_with_sync() { + let vin = *b"WVWZZZ3CZWE123456"; + let eid = [0; 6]; + let gid = [0; 6]; + + let resp = Response::new(vin, 0x1000, eid, gid) + .with_sync_status(SyncStatus::Synchronized); + let bytes = resp.to_bytes(); + + assert_eq!(bytes.len(), 33); + assert_eq!(bytes[32], 0x00); // sync status + } + + #[test] + fn parse_response() { + let vin = *b"WVWZZZ3CZWE123456"; + let eid = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; + let gid = [0x00, 0x00, 0x00, 0x00, 0x00, 0x01]; + + let original = Response::new(vin, 0x1000, eid, gid); + let bytes = original.to_bytes(); + let parsed = Response::parse(&bytes).unwrap(); + + assert_eq!(parsed.vin, vin); + assert_eq!(parsed.logical_address, 0x1000); + assert_eq!(parsed.eid, eid); + assert_eq!(parsed.gid, gid); + } + + #[test] + fn roundtrip_response() { + let vin = *b"WVWZZZ3CZWE123456"; + let eid = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; + let gid = [0x00, 0x00, 0x00, 0x00, 0x00, 0x01]; + + let original = Response::new(vin, 0x1000, eid, gid) + .with_routing_required() + .with_sync_status(SyncStatus::NotSynchronized); + + let bytes = original.to_bytes(); + let parsed = Response::parse(&bytes).unwrap(); + + assert_eq!(original, parsed); + } +} From 7f962158f0a2c3e28ffa11ebf20edac9aebcd09d Mon Sep 17 00:00:00 2001 From: vinayrs Date: Mon, 2 Feb 2026 18:51:00 +0530 Subject: [PATCH 03/10] feat(error): add DoIP error types and NRC codes --- src/error.rs | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..58ec31f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,139 @@ +//! Error Types for DoIP Server (ISO 13400-2 & ISO 14229) + +use std::io; +use thiserror::Error; + +/// Result type alias +pub type Result = std::result::Result; + +/// Main DoIP Error type +#[derive(Error, Debug)] +pub enum DoipError { + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("Invalid protocol version: expected 0x{expected:02X}, got 0x{actual:02X}")] + InvalidProtocolVersion { expected: u8, actual: u8 }, + + #[error("Invalid DoIP header: {0}")] + InvalidHeader(String), + + #[error("Unknown payload type: 0x{0:04X}")] + UnknownPayloadType(u16), + + #[error("Message too large: {size} bytes (max: {max})")] + MessageTooLarge { size: usize, max: usize }, + + #[error("Routing activation failed: {message}")] + RoutingActivationFailed { code: u8, message: String }, + + #[error("Session not found")] + SessionNotFound, + + #[error("Session closed")] + SessionClosed, + + #[error("Timeout: {0}")] + Timeout(String), + + #[error("UDS error: service 0x{service:02X}, NRC 0x{nrc:02X}")] + UdsError { service: u8, nrc: u8 }, +} + +/// Generic Header NACK codes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum GenericNackCode { + IncorrectPatternFormat = 0x00, + UnknownPayloadType = 0x01, + MessageTooLarge = 0x02, + OutOfMemory = 0x03, + InvalidPayloadLength = 0x04, +} + +impl GenericNackCode { + pub const fn as_u8(self) -> u8 { self as u8 } +} + +/// Routing Activation Response codes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum RoutingActivationCode { + UnknownSourceAddress = 0x00, + AllSocketsRegistered = 0x01, + DifferentSourceAddress = 0x02, + SourceAddressAlreadyActive = 0x03, + MissingAuthentication = 0x04, + RejectedConfirmation = 0x05, + UnsupportedActivationType = 0x06, + SuccessfullyActivated = 0x10, + ConfirmationRequired = 0x11, +} + +impl RoutingActivationCode { + pub const fn as_u8(self) -> u8 { self as u8 } + pub const fn is_success(self) -> bool { + matches!(self, Self::SuccessfullyActivated | Self::ConfirmationRequired) + } +} + +/// Diagnostic Message NACK codes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum DiagnosticNackCode { + InvalidSourceAddress = 0x02, + UnknownTargetAddress = 0x03, + DiagnosticMessageTooLarge = 0x04, + OutOfMemory = 0x05, + TargetUnreachable = 0x06, + UnknownNetwork = 0x07, + TransportProtocolError = 0x08, +} + +impl DiagnosticNackCode { + pub const fn as_u8(self) -> u8 { self as u8 } +} + +/// UDS Negative Response Codes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum UdsNrc { + GeneralReject = 0x10, + ServiceNotSupported = 0x11, + SubFunctionNotSupported = 0x12, + IncorrectMessageLength = 0x13, + BusyRepeatRequest = 0x21, + ConditionsNotCorrect = 0x22, + RequestSequenceError = 0x24, + RequestOutOfRange = 0x31, + SecurityAccessDenied = 0x33, + InvalidKey = 0x35, + ExceededNumberOfAttempts = 0x36, + RequiredTimeDelayNotExpired = 0x37, + ResponsePending = 0x78, + ServiceNotSupportedInActiveSession = 0x7F, +} + +impl UdsNrc { + pub const fn as_u8(self) -> u8 { self as u8 } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_routing_activation_success() { + assert!(RoutingActivationCode::SuccessfullyActivated.is_success()); + assert!(!RoutingActivationCode::UnknownSourceAddress.is_success()); + } + + #[test] + fn test_nrc_values() { + assert_eq!(UdsNrc::ServiceNotSupported.as_u8(), 0x11); + assert_eq!(DiagnosticNackCode::UnknownTargetAddress.as_u8(), 0x03); + } +} \ No newline at end of file From 5a2cdbe7f90df84136418728c8c9bd59d2cb5e93 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Mon, 2 Feb 2026 18:51:13 +0530 Subject: [PATCH 04/10] feat(uds): add UdsHandler trait for pluggable backends --- src/uds/handler.rs | 128 +++++++++++++++++++++++++++++++++++++++++++++ src/uds/mod.rs | 7 +++ 2 files changed, 135 insertions(+) create mode 100644 src/uds/handler.rs create mode 100644 src/uds/mod.rs diff --git a/src/uds/handler.rs b/src/uds/handler.rs new file mode 100644 index 0000000..3d4f50f --- /dev/null +++ b/src/uds/handler.rs @@ -0,0 +1,128 @@ +//! UDS Handler Trait +//! +//! Defines the interface between DoIP transport and UDS processing. +//! The DoIP server extracts UDS bytes from DoIP frames and delegates +//! to the handler. The handler returns UDS response bytes. + +use bytes::Bytes; + +/// UDS request extracted from DoIP diagnostic message +#[derive(Debug, Clone)] +pub struct UdsRequest { + pub source_address: u16, + pub target_address: u16, + pub data: Bytes, +} + +impl UdsRequest { + pub fn new(source: u16, target: u16, data: Bytes) -> Self { + Self { + source_address: source, + target_address: target, + data, + } + } + + pub fn service_id(&self) -> Option { + self.data.first().copied() + } +} + +/// UDS response to be wrapped in DoIP diagnostic message +#[derive(Debug, Clone)] +pub struct UdsResponse { + pub source_address: u16, + pub target_address: u16, + pub data: Bytes, +} + +impl UdsResponse { + pub fn new(source: u16, target: u16, data: Bytes) -> Self { + Self { + source_address: source, + target_address: target, + data, + } + } +} + +/// Trait for handling UDS requests +/// +/// Implement this trait to connect the DoIP server to a UDS backend +/// (e.g., UDS2SOVD converter, ODX/MDD handler, or ECU simulator) +pub trait UdsHandler: Send + Sync { + fn handle(&self, request: UdsRequest) -> UdsResponse; +} + +/// Stub handler that returns NRC 0x11 (Service Not Supported) for all requests +#[derive(Debug, Default, Clone)] +pub struct StubHandler; + +impl UdsHandler for StubHandler { + fn handle(&self, request: UdsRequest) -> UdsResponse { + let sid = request.service_id().unwrap_or(0); + // Negative response: 0x7F + SID + NRC + let nrc_service_not_supported = 0x11; + let data = Bytes::from(vec![0x7F, sid, nrc_service_not_supported]); + + UdsResponse::new( + request.target_address, + request.source_address, + data, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stub_handler_returns_nrc() { + let handler = StubHandler; + let request = UdsRequest::new( + 0x0E00, + 0x1000, + Bytes::from(vec![0x22, 0xF1, 0x90]), // ReadDataByIdentifier + ); + + let response = handler.handle(request); + + assert_eq!(response.source_address, 0x1000); + assert_eq!(response.target_address, 0x0E00); + assert_eq!(response.data.as_ref(), &[0x7F, 0x22, 0x11]); + } + + #[test] + fn stub_handler_handles_empty_request() { + let handler = StubHandler; + let request = UdsRequest::new(0x0E00, 0x1000, Bytes::new()); + + let response = handler.handle(request); + + assert_eq!(response.data.as_ref(), &[0x7F, 0x00, 0x11]); + } + + #[test] + fn stub_handler_tester_present() { + let handler = StubHandler; + let request = UdsRequest::new( + 0x0E00, + 0x1000, + Bytes::from(vec![0x3E, 0x00]), // TesterPresent + ); + + let response = handler.handle(request); + + assert_eq!(response.data.as_ref(), &[0x7F, 0x3E, 0x11]); + } + + #[test] + fn uds_request_service_id() { + let request = UdsRequest::new(0x0E00, 0x1000, Bytes::from(vec![0x10, 0x01])); + assert_eq!(request.service_id(), Some(0x10)); + + let empty = UdsRequest::new(0x0E00, 0x1000, Bytes::new()); + assert_eq!(empty.service_id(), None); + } +} diff --git a/src/uds/mod.rs b/src/uds/mod.rs new file mode 100644 index 0000000..be783e7 --- /dev/null +++ b/src/uds/mod.rs @@ -0,0 +1,7 @@ +//! UDS Module +//! +//! Provides the interface between DoIP transport and UDS processing. + +pub mod handler; + +pub use handler::{UdsHandler, UdsRequest, UdsResponse, StubHandler}; From 6bea1b9be7216c459e7ac8842ddf6dc6f84b0610 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Wed, 25 Feb 2026 18:15:43 +0530 Subject: [PATCH 05/10] refactor(doip): address PR review comments and Improvement - Consolidate errors into DoipError; add TryFrom on all enums - Strongly type activation_type field (ActivationType, not u8) - Add DoipParseable / DoipSerializable traits + DoipPayload dispatch enum - Override serialized_len() on all impls; fix array size magic numbers - Make ParseError pub(crate); add serde(default), tracing --- Cargo.lock | 872 ++++++++++++++++++++++++++++++++- src/doip/alive_check.rs | 94 ++-- src/doip/diagnostic_message.rs | 462 ++++++++++------- src/doip/header_parser.rs | 517 +++++++++++++------ src/doip/hearder_parser.rs | 378 -------------- src/doip/mod.rs | 87 +++- src/doip/payload.rs | 253 ++++++++++ src/doip/routing_activation.rs | 309 ++++++------ src/doip/vehicle_id.rs | 278 ++++++----- src/error.rs | 219 ++++++--- src/lib.rs | 16 + src/server/config.rs | 317 ++++++++++++ src/server/session.rs | 186 +++++++ src/uds/handler.rs | 130 +++-- src/uds/mod.rs | 21 +- 15 files changed, 2986 insertions(+), 1153 deletions(-) delete mode 100644 src/doip/hearder_parser.rs create mode 100644 src/doip/payload.rs create mode 100644 src/server/config.rs create mode 100644 src/server/session.rs diff --git a/Cargo.lock b/Cargo.lock index 55e3925..d705ef4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,875 @@ version = 4 [[package]] -name = "uds2sovd-proxy" +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "doip-server" version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "clap", + "dashmap", + "parking_lot", + "serde", + "thiserror", + "tokio", + "tokio-util", + "toml", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" diff --git a/src/doip/alive_check.rs b/src/doip/alive_check.rs index aed545a..358834a 100644 --- a/src/doip/alive_check.rs +++ b/src/doip/alive_check.rs @@ -1,37 +1,39 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ //! Alive Check handlers (ISO 13400-2) -use bytes::{BufMut, Bytes, BytesMut}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Error { - PayloadTooShort { expected: usize, actual: usize }, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::PayloadTooShort { expected, actual } => { - write!(f, "payload too short: need {} bytes, got {}", expected, actual) - } - } - } -} - -impl std::error::Error for Error {} +use bytes::{BufMut, BytesMut}; +use tracing::warn; +use crate::DoipError; +use super::{DoipParseable, DoipSerializable}; // Alive Check Request (0x0007) - no payload // Server sends this to check if tester is still connected #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Request; -impl Request { - pub fn parse(_payload: &[u8]) -> Result { +impl DoipParseable for Request { + fn parse(_payload: &[u8]) -> std::result::Result { Ok(Self) } +} - pub fn to_bytes(&self) -> Bytes { - Bytes::new() +impl DoipSerializable for Request { + fn serialized_len(&self) -> Option { + Some(0) } + + fn write_to(&self, _buf: &mut BytesMut) {} } // Alive Check Response (0x0008) - 2 byte source address @@ -41,39 +43,45 @@ pub struct Response { pub source_address: u16, } -impl Response { - pub const LEN: usize = 2; - - pub fn new(source_address: u16) -> Self { - Self { source_address } - } - - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::LEN { - return Err(Error::PayloadTooShort { - expected: Self::LEN, - actual: payload.len(), - }); - } - - let source_address = u16::from_be_bytes([payload[0], payload[1]]); +impl DoipParseable for Response { + fn parse(payload: &[u8]) -> std::result::Result { + let bytes: [u8; 2] = payload + .get(..Self::LEN) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = DoipError::PayloadTooShort { expected: Self::LEN, actual: payload.len() }; + warn!("AliveCheck Response parse failed: {}", e); + e + })?; + + let source_address = u16::from_be_bytes(bytes); Ok(Self { source_address }) } +} - pub fn to_bytes(&self) -> Bytes { - let mut buf = BytesMut::with_capacity(Self::LEN); - self.write_to(&mut buf); - buf.freeze() +impl DoipSerializable for Response { + fn serialized_len(&self) -> Option { + Some(Self::LEN) } - pub fn write_to(&self, buf: &mut BytesMut) { + fn write_to(&self, buf: &mut BytesMut) { buf.put_u16(self.source_address); } } +impl Response { + pub const LEN: usize = 2; + + #[must_use] + pub fn new(source_address: u16) -> Self { + Self { source_address } + } +} + #[cfg(test)] mod tests { use super::*; + use crate::doip::{DoipParseable, DoipSerializable}; #[test] fn parse_request() { diff --git a/src/doip/diagnostic_message.rs b/src/doip/diagnostic_message.rs index 5f31aa1..2ed0988 100644 --- a/src/doip/diagnostic_message.rs +++ b/src/doip/diagnostic_message.rs @@ -1,15 +1,35 @@ -//! Diagnostic Message handlers (ISO 13400-2) +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +//! Diagnostic Message handlers (ISO 13400-2:2019) use bytes::{Buf, BufMut, Bytes, BytesMut}; +use tracing::warn; +use crate::DoipError; +use super::{DoipParseable, DoipSerializable, too_short, check_min_len}; -// Diagnostic message positive ack codes per ISO 13400-2 Table 27 +const ADDRESS_BYTES: usize = 2; +const HEADER_BYTES: usize = ADDRESS_BYTES * 2; +const ACK_CODE_BYTES: usize = 1; +const MIN_USER_DATA_BYTES: usize = 1; + +// Diagnostic message positive ack codes per ISO 13400-2:2019 Table 27 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum AckCode { Acknowledged = 0x00, } -// Diagnostic message negative ack codes per ISO 13400-2 Table 28 +// Diagnostic message negative ack codes per ISO 13400-2:2019 Table 28 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum NackCode { @@ -22,52 +42,47 @@ pub enum NackCode { TransportProtocolError = 0x08, } -impl NackCode { - pub fn from_u8(value: u8) -> Option { - match value { - 0x02 => Some(Self::InvalidSourceAddress), - 0x03 => Some(Self::UnknownTargetAddress), - 0x04 => Some(Self::DiagnosticMessageTooLarge), - 0x05 => Some(Self::OutOfMemory), - 0x06 => Some(Self::TargetUnreachable), - 0x07 => Some(Self::UnknownNetwork), - 0x08 => Some(Self::TransportProtocolError), - _ => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Error { - PayloadTooShort { expected: usize, actual: usize }, - EmptyUserData, -} +impl TryFrom for NackCode { + type Error = DoipError; -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::PayloadTooShort { expected, actual } => { - write!(f, "payload too short: need {} bytes, got {}", expected, actual) - } - Self::EmptyUserData => write!(f, "diagnostic message has no user data"), + fn try_from(value: u8) -> std::result::Result { + match value { + 0x02 => Ok(Self::InvalidSourceAddress), + 0x03 => Ok(Self::UnknownTargetAddress), + 0x04 => Ok(Self::DiagnosticMessageTooLarge), + 0x05 => Ok(Self::OutOfMemory), + 0x06 => Ok(Self::TargetUnreachable), + 0x07 => Ok(Self::UnknownNetwork), + 0x08 => Ok(Self::TransportProtocolError), + other => Err(DoipError::UnknownNackCode(other)), } } } -impl std::error::Error for Error {} - -// Diagnostic Message - carries UDS data between tester and ECU -// Payload: SA(2) + TA(2) + user_data(1+) +/// Diagnostic Message - carries UDS data between tester and ECU +/// +/// Represents a DoIP diagnostic message as defined in ISO 13400-2:2019. +/// The message contains source/target addresses and UDS payload data. +/// +/// # Wire Format +/// Payload: SA(2) + TA(2) + user_data(1+) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Message { - pub source_address: u16, - pub target_address: u16, - pub user_data: Bytes, + source_address: u16, + target_address: u16, + user_data: Bytes, } impl Message { - pub const MIN_LEN: usize = 5; // SA + TA + at least 1 byte UDS - + /// Minimum message length in bytes (SA + TA + at least 1 byte UDS data) + pub const MIN_LEN: usize = HEADER_BYTES + MIN_USER_DATA_BYTES; + + /// Create a new diagnostic message + /// + /// # Arguments + /// * `source` - Source address (tester or ECU) + /// * `target` - Target address (tester or ECU) + /// * `data` - UDS payload data pub fn new(source: u16, target: u16, data: Bytes) -> Self { Self { source_address: source, @@ -76,83 +91,56 @@ impl Message { } } - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: payload.len(), - }); - } - - let source_address = u16::from_be_bytes([payload[0], payload[1]]); - let target_address = u16::from_be_bytes([payload[2], payload[3]]); - let user_data = Bytes::copy_from_slice(&payload[4..]); - - if user_data.is_empty() { - return Err(Error::EmptyUserData); - } - - Ok(Self { - source_address, - target_address, - user_data, - }) + /// Get the source address + pub fn source_address(&self) -> u16 { + self.source_address } - pub fn parse_buf(buf: &mut Bytes) -> Result { - if buf.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: buf.len(), - }); - } - - let source_address = buf.get_u16(); - let target_address = buf.get_u16(); - let user_data = buf.split_off(0); - - if user_data.is_empty() { - return Err(Error::EmptyUserData); - } - - Ok(Self { - source_address, - target_address, - user_data, - }) + /// Get the target address + pub fn target_address(&self) -> u16 { + self.target_address } - pub fn to_bytes(&self) -> Bytes { - let mut buf = BytesMut::with_capacity(4 + self.user_data.len()); - self.write_to(&mut buf); - buf.freeze() + /// Get the UDS user data + pub fn user_data(&self) -> &Bytes { + &self.user_data } - pub fn write_to(&self, buf: &mut BytesMut) { - buf.put_u16(self.source_address); - buf.put_u16(self.target_address); - buf.extend_from_slice(&self.user_data); + /// Parse a Diagnostic Message from a mutable Bytes buffer + /// + /// # Errors + /// Returns [`DoipError::PayloadTooShort`] if buffer is less than 5 bytes + /// Returns [`DoipError::EmptyUserData`] if no UDS data is present + pub fn parse_buf(buf: &mut Bytes) -> std::result::Result { + let msg = ::parse(buf)?; + buf.advance(buf.len()); + Ok(msg) } - // UDS service ID is first byte of user_data pub fn service_id(&self) -> Option { self.user_data.first().copied() } } -// Diagnostic Message Positive Ack (0x8002) -// Payload: SA(2) + TA(2) + ack_code(1) + optional previous_diag_data +/// Diagnostic Message Positive Acknowledgment (message type 0x8002) +/// +/// Sent by a DoIP entity to acknowledge receipt of a diagnostic message. +/// +/// # Wire Format +/// Payload: SA(2) + TA(2) + ack_code(1) + optional previous_diag_data #[derive(Debug, Clone, PartialEq, Eq)] pub struct PositiveAck { - pub source_address: u16, - pub target_address: u16, - pub ack_code: AckCode, - pub previous_data: Option, + source_address: u16, + target_address: u16, + ack_code: AckCode, + previous_data: Option, } impl PositiveAck { - pub const MIN_LEN: usize = 5; + /// Minimum positive ack length in bytes + pub const MIN_LEN: usize = HEADER_BYTES + ACK_CODE_BYTES; + /// Create a new positive acknowledgment pub fn new(source: u16, target: u16) -> Self { Self { source_address: source, @@ -162,71 +150,58 @@ impl PositiveAck { } } + /// Create a positive acknowledgment with previous diagnostic data pub fn with_previous_data(source: u16, target: u16, data: Bytes) -> Self { Self { source_address: source, target_address: target, ack_code: AckCode::Acknowledged, - previous_data: Some(data), + // ISO 13400-2:2019: previous diagnostic data is optional; treat empty as absent + previous_data: if data.is_empty() { None } else { Some(data) }, } } - pub fn to_bytes(&self) -> Bytes { - let extra = self.previous_data.as_ref().map(|d| d.len()).unwrap_or(0); - let mut buf = BytesMut::with_capacity(Self::MIN_LEN + extra); - self.write_to(&mut buf); - buf.freeze() + /// Get the source address + pub fn source_address(&self) -> u16 { + self.source_address } - pub fn write_to(&self, buf: &mut BytesMut) { - buf.put_u16(self.source_address); - buf.put_u16(self.target_address); - buf.put_u8(self.ack_code as u8); - if let Some(ref data) = self.previous_data { - buf.extend_from_slice(data); - } + /// Get the target address + pub fn target_address(&self) -> u16 { + self.target_address } - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: payload.len(), - }); - } - - let source_address = u16::from_be_bytes([payload[0], payload[1]]); - let target_address = u16::from_be_bytes([payload[2], payload[3]]); - let ack_code = AckCode::Acknowledged; // only one value - - let previous_data = if payload.len() > Self::MIN_LEN { - Some(Bytes::copy_from_slice(&payload[5..])) - } else { - None - }; + /// Get the acknowledgment code + pub fn ack_code(&self) -> AckCode { + self.ack_code + } - Ok(Self { - source_address, - target_address, - ack_code, - previous_data, - }) + /// Get the previous diagnostic data + pub fn previous_data(&self) -> Option<&Bytes> { + self.previous_data.as_ref() } } -// Diagnostic Message Negative Ack (0x8003) -// Payload: SA(2) + TA(2) + nack_code(1) + optional previous_diag_data +/// Diagnostic Message Negative Acknowledgment (message type 0x8003) +/// +/// Sent by a DoIP entity to indicate rejection of a diagnostic message. +/// NACK codes are defined in ISO 13400-2:2019 Table 28. +/// +/// # Wire Format +/// Payload: SA(2) + TA(2) + nack_code(1) + optional previous_diag_data #[derive(Debug, Clone, PartialEq, Eq)] pub struct NegativeAck { - pub source_address: u16, - pub target_address: u16, - pub nack_code: NackCode, - pub previous_data: Option, + source_address: u16, + target_address: u16, + nack_code: NackCode, + previous_data: Option, } impl NegativeAck { - pub const MIN_LEN: usize = 5; + /// Minimum negative ack length in bytes + pub const MIN_LEN: usize = HEADER_BYTES + ACK_CODE_BYTES; + /// Create a new negative acknowledgment pub fn new(source: u16, target: u16, code: NackCode) -> Self { Self { source_address: source, @@ -236,49 +211,155 @@ impl NegativeAck { } } + /// Get the source address + pub fn source_address(&self) -> u16 { + self.source_address + } + + /// Get the target address + pub fn target_address(&self) -> u16 { + self.target_address + } + + /// Get the negative acknowledgment code + pub fn nack_code(&self) -> NackCode { + self.nack_code + } + + /// Get the previous diagnostic data + pub fn previous_data(&self) -> Option<&Bytes> { + self.previous_data.as_ref() + } + + /// Create a negative acknowledgment with previous diagnostic data pub fn with_previous_data(source: u16, target: u16, code: NackCode, data: Bytes) -> Self { Self { source_address: source, target_address: target, nack_code: code, - previous_data: Some(data), + // ISO 13400-2:2019: previous diagnostic data is optional; treat empty as absent + previous_data: if data.is_empty() { None } else { Some(data) }, } } +} + +impl DoipParseable for Message { + fn parse(payload: &[u8]) -> std::result::Result { + let header: [u8; HEADER_BYTES] = payload + .get(..HEADER_BYTES) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = too_short(payload, Self::MIN_LEN); + warn!("DiagnosticMessage parse failed: {}", e); + e + })?; + + let source_address = u16::from_be_bytes([header[0], header[1]]); + let target_address = u16::from_be_bytes([header[2], header[3]]); + + let user_data = payload + .get(HEADER_BYTES..) + .map(Bytes::copy_from_slice) + .ok_or_else(|| { + let e = too_short(payload, Self::MIN_LEN); + warn!("DiagnosticMessage parse failed: {}", e); + e + })?; - pub fn to_bytes(&self) -> Bytes { - let extra = self.previous_data.as_ref().map(|d| d.len()).unwrap_or(0); - let mut buf = BytesMut::with_capacity(Self::MIN_LEN + extra); - self.write_to(&mut buf); - buf.freeze() + if user_data.is_empty() { + warn!("DiagnosticMessage parse failed: empty user data"); + return Err(DoipError::EmptyUserData); + } + + Ok(Self { + source_address, + target_address, + user_data, + }) + } +} + +impl DoipSerializable for Message { + fn serialized_len(&self) -> Option { + Some(HEADER_BYTES + self.user_data.len()) } - pub fn write_to(&self, buf: &mut BytesMut) { + fn write_to(&self, buf: &mut BytesMut) { buf.put_u16(self.source_address); buf.put_u16(self.target_address); - buf.put_u8(self.nack_code as u8); + buf.extend_from_slice(&self.user_data); + } +} + +impl DoipParseable for PositiveAck { + fn parse(payload: &[u8]) -> std::result::Result { + if let Err(e) = check_min_len(payload, Self::MIN_LEN) { + warn!("DiagnosticPositiveAck parse failed: {}", e); + return Err(e); + } + + let header: [u8; HEADER_BYTES] = payload + .get(..HEADER_BYTES) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + + let source_address = u16::from_be_bytes([header[0], header[1]]); + let target_address = u16::from_be_bytes([header[2], header[3]]); + let ack_code = AckCode::Acknowledged; + + let previous_data = payload + .get(Self::MIN_LEN..) + .filter(|d| !d.is_empty()) + .map(Bytes::copy_from_slice); + + Ok(Self { + source_address, + target_address, + ack_code, + previous_data, + }) + } +} + +impl DoipSerializable for PositiveAck { + fn serialized_len(&self) -> Option { + Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, |d| d.len())) + } + + fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.source_address); + buf.put_u16(self.target_address); + buf.put_u8(self.ack_code as u8); if let Some(ref data) = self.previous_data { buf.extend_from_slice(data); } } +} - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: payload.len(), - }); +impl DoipParseable for NegativeAck { + fn parse(payload: &[u8]) -> std::result::Result { + if let Err(e) = check_min_len(payload, Self::MIN_LEN) { + warn!("DiagnosticNegativeAck parse failed: {}", e); + return Err(e); } - let source_address = u16::from_be_bytes([payload[0], payload[1]]); - let target_address = u16::from_be_bytes([payload[2], payload[3]]); - let nack_code = NackCode::from_u8(payload[4]) + let header: [u8; HEADER_BYTES] = payload + .get(..HEADER_BYTES) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + + let source_address = u16::from_be_bytes([header[0], header[1]]); + let target_address = u16::from_be_bytes([header[2], header[3]]); + let nack_code = payload + .get(HEADER_BYTES) + .copied() + .and_then(|b| NackCode::try_from(b).ok()) .unwrap_or(NackCode::TransportProtocolError); - let previous_data = if payload.len() > Self::MIN_LEN { - Some(Bytes::copy_from_slice(&payload[5..])) - } else { - None - }; + let previous_data = payload + .get(Self::MIN_LEN..) + .filter(|d| !d.is_empty()) + .map(Bytes::copy_from_slice); Ok(Self { source_address, @@ -289,9 +370,25 @@ impl NegativeAck { } } +impl DoipSerializable for NegativeAck { + fn serialized_len(&self) -> Option { + Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, |d| d.len())) + } + + fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.source_address); + buf.put_u16(self.target_address); + buf.put_u8(self.nack_code as u8); + if let Some(ref data) = self.previous_data { + buf.extend_from_slice(data); + } + } +} + #[cfg(test)] mod tests { use super::*; + use crate::doip::{DoipParseable, DoipSerializable}; #[test] fn nack_code_values() { @@ -306,9 +403,9 @@ mod tests { let payload = [0x0E, 0x80, 0x10, 0x00, 0x22, 0xF1, 0x90]; let msg = Message::parse(&payload).unwrap(); - assert_eq!(msg.source_address, 0x0E80); - assert_eq!(msg.target_address, 0x1000); - assert_eq!(msg.user_data.as_ref(), &[0x22, 0xF1, 0x90]); + assert_eq!(msg.source_address(), 0x0E80); + assert_eq!(msg.target_address(), 0x1000); + assert_eq!(msg.user_data().as_ref(), &[0x22, 0xF1, 0x90]); assert_eq!(msg.service_id(), Some(0x22)); } @@ -319,7 +416,7 @@ mod tests { let msg = Message::parse(&payload).unwrap(); assert_eq!(msg.service_id(), Some(0x3E)); - assert_eq!(msg.user_data.len(), 2); + assert_eq!(msg.user_data().len(), 2); } #[test] @@ -334,9 +431,9 @@ mod tests { let msg = Message::new(0x0E80, 0x1000, uds); let bytes = msg.to_bytes(); - assert_eq!(&bytes[0..2], &[0x0E, 0x80]); - assert_eq!(&bytes[2..4], &[0x10, 0x00]); - assert_eq!(&bytes[4..], &[0x22, 0xF1, 0x90]); + assert_eq!(&bytes[..ADDRESS_BYTES], &[0x0E, 0x80]); + assert_eq!(&bytes[ADDRESS_BYTES..HEADER_BYTES], &[0x10, 0x00]); + assert_eq!(&bytes[HEADER_BYTES..], &[0x22, 0xF1, 0x90]); } #[test] @@ -344,20 +441,21 @@ mod tests { let ack = PositiveAck::new(0x1000, 0x0E80); let bytes = ack.to_bytes(); - assert_eq!(bytes.len(), 5); - assert_eq!(&bytes[0..2], &[0x10, 0x00]); // source (ECU) - assert_eq!(&bytes[2..4], &[0x0E, 0x80]); // target (tester) - assert_eq!(bytes[4], 0x00); // ack code + assert_eq!(bytes.len(), PositiveAck::MIN_LEN); + assert_eq!(&bytes[..ADDRESS_BYTES], &[0x10, 0x00]); // source (ECU) + assert_eq!(&bytes[ADDRESS_BYTES..HEADER_BYTES], &[0x0E, 0x80]); // target (tester) + assert_eq!(bytes[HEADER_BYTES], 0x00); // ack code } #[test] fn build_positive_ack_with_prev_data() { let prev = Bytes::from_static(&[0x22, 0xF1, 0x90]); + let expected_len = PositiveAck::MIN_LEN + prev.len(); let ack = PositiveAck::with_previous_data(0x1000, 0x0E80, prev); let bytes = ack.to_bytes(); - assert_eq!(bytes.len(), 8); - assert_eq!(&bytes[5..], &[0x22, 0xF1, 0x90]); + assert_eq!(bytes.len(), expected_len); + assert_eq!(&bytes[PositiveAck::MIN_LEN..], &[0x22, 0xF1, 0x90]); } #[test] @@ -365,15 +463,15 @@ mod tests { let nack = NegativeAck::new(0x1000, 0x0E80, NackCode::UnknownTargetAddress); let bytes = nack.to_bytes(); - assert_eq!(bytes.len(), 5); - assert_eq!(bytes[4], 0x03); + assert_eq!(bytes.len(), NegativeAck::MIN_LEN); + assert_eq!(bytes[HEADER_BYTES], 0x03); } #[test] fn build_negative_ack_target_unreachable() { let nack = NegativeAck::new(0x1000, 0x0E80, NackCode::TargetUnreachable); let bytes = nack.to_bytes(); - assert_eq!(bytes[4], 0x06); + assert_eq!(bytes[HEADER_BYTES], 0x06); } #[test] @@ -381,9 +479,9 @@ mod tests { let payload = [0x10, 0x00, 0x0E, 0x80, 0x00]; let ack = PositiveAck::parse(&payload).unwrap(); - assert_eq!(ack.source_address, 0x1000); - assert_eq!(ack.target_address, 0x0E80); - assert!(ack.previous_data.is_none()); + assert_eq!(ack.source_address(), 0x1000); + assert_eq!(ack.target_address(), 0x0E80); + assert!(ack.previous_data().is_none()); } #[test] @@ -391,7 +489,7 @@ mod tests { let payload = [0x10, 0x00, 0x0E, 0x80, 0x03]; let nack = NegativeAck::parse(&payload).unwrap(); - assert_eq!(nack.nack_code, NackCode::UnknownTargetAddress); + assert_eq!(nack.nack_code(), NackCode::UnknownTargetAddress); } #[test] diff --git a/src/doip/header_parser.rs b/src/doip/header_parser.rs index f489da3..1da76b2 100644 --- a/src/doip/header_parser.rs +++ b/src/doip/header_parser.rs @@ -1,9 +1,43 @@ -//! DoIP Header Parser +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! `DoIP` Header Parser and Codec +//! +//! This module implements the core `DoIP` protocol header parsing and message framing +//! according to ISO 13400-2:2019. It provides: +//! +//! - **Header Validation**: Checks protocol version, payload types, and message sizes +//! - **Message Framing**: Tokio codec for TCP stream processing with proper buffering +//! - **Type Safety**: Strongly-typed payload types and error codes per ISO specification +//! - **`DoS` Protection**: Configurable max message size limits (default 4MB) +//! +//! The `DoIP` header consists of 8 bytes: +//! - Protocol version (1 byte) + inverse version (1 byte) +//! - Payload type (2 bytes, big-endian) +//! - Payload length (4 bytes, big-endian) +//! +//! This module accepts protocol versions 0x01, 0x02, 0x03, and 0xFF to support +//! both ISO 13400-2:2012 and ISO 13400-2:2019 specifications. use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::io; use tokio_util::codec::{Decoder, Encoder}; +use tracing::{debug, warn}; +/// Generic Header NACK codes (ISO 13400-2:2019 Table 17) +/// +/// Negative acknowledgment codes sent in Generic `DoIP` Header NACK (payload type 0x0000) +/// when the `DoIP` entity cannot process a received message due to header-level issues. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum GenericNackCode { @@ -14,38 +48,59 @@ pub enum GenericNackCode { InvalidPayloadLength = 0x04, } -#[derive(Debug)] -pub enum ParseError { - InvalidHeader(String), - Io(io::Error), -} +impl TryFrom for GenericNackCode { + type Error = u8; -impl std::fmt::Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidHeader(msg) => write!(f, "Invalid header: {}", msg), - Self::Io(e) => write!(f, "IO error: {}", e), + fn try_from(value: u8) -> std::result::Result { + match value { + 0x00 => Ok(Self::IncorrectPatternFormat), + 0x01 => Ok(Self::UnknownPayloadType), + 0x02 => Ok(Self::MessageTooLarge), + 0x03 => Ok(Self::OutOfMemory), + 0x04 => Ok(Self::InvalidPayloadLength), + other => Err(other), } } } -impl std::error::Error for ParseError {} - -impl From for ParseError { - fn from(e: io::Error) -> Self { - Self::Io(e) - } +#[derive(thiserror::Error, Debug)] +pub(crate) enum ParseError { + #[error("Invalid header: {0}")] + InvalidHeader(String), + /// Payload length exceeds `u32::MAX` (DoIP protocol limit is 4 MB) + #[error("DoIP payload exceeds u32::MAX (protocol limit is 4 MB)")] + PayloadTooLarge, + #[error("IO error: {0}")] + IoError(#[from] io::Error), } -pub type Result = std::result::Result; +pub(crate) type Result = std::result::Result; +/// `DoIP` protocol version 0x01 (legacy, pre-ISO 13400-2:2012) +pub const PROTOCOL_VERSION_V1: u8 = 0x01; +/// Default `DoIP` protocol version 0x02 (ISO 13400-2:2012 / 2019) pub const DEFAULT_PROTOCOL_VERSION: u8 = 0x02; -pub const DEFAULT_PROTOCOL_VERSION_INV: u8 = 0xFD; +/// `DoIP` protocol version 0x03 (ISO 13400-2:2019 update) +pub const PROTOCOL_VERSION_V3: u8 = 0x03; +/// Wildcard/default protocol version (ISO 13400-2:2019 – accept any version) +pub const DOIP_VERSION_DEFAULT: u8 = 0xFF; +/// XOR mask used to compute and verify the inverse version byte in the `DoIP` header. +/// The header requires `version XOR inverse_version == 0xFF`. +pub const DOIP_HEADER_VERSION_MASK: u8 = 0xFF; +/// Inverse of default protocol version for header validation +pub const DEFAULT_PROTOCOL_VERSION_INV: u8 = DEFAULT_PROTOCOL_VERSION ^ DOIP_HEADER_VERSION_MASK; +/// Size of `DoIP` header in bytes (ISO 13400-2:2019 Section 6) pub const DOIP_HEADER_LENGTH: usize = 8; -/// Maximum DoIP message size (4MB) - provides DoS protection while allowing -/// large diagnostic data transfers. Can be customized via DoipCodec::with_max_payload_size(). +/// Maximum `DoIP` message size (4MB) - provides `DoS` protection while allowing +/// large diagnostic data transfers. Can be customized via `DoipCodec::with_max_payload_size()`. pub const MAX_DOIP_MESSAGE_SIZE: u32 = 0x0040_0000; // 4MB +/// `DoIP` Payload Types (ISO 13400-2:2019 Table 16) +/// +/// Identifies the type of message carried in the `DoIP` payload. +/// Supports diagnostic messages (0x8001), routing activation (0x0005-0x0006), +/// vehicle identification (0x0001-0x0004), alive check (0x0007-0x0008), +/// and diagnostic power mode requests (0x4003-0x4004). #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum PayloadType { @@ -67,51 +122,69 @@ pub enum PayloadType { DiagnosticMessageNegativeAck = 0x8003, } -impl PayloadType { - pub fn from_u16(value: u16) -> Option { +impl TryFrom for PayloadType { + type Error = u16; + + fn try_from(value: u16) -> std::result::Result { match value { - 0x0000 => Some(Self::GenericNack), - 0x0001 => Some(Self::VehicleIdentificationRequest), - 0x0002 => Some(Self::VehicleIdentificationRequestWithEid), - 0x0003 => Some(Self::VehicleIdentificationRequestWithVin), - 0x0004 => Some(Self::VehicleIdentificationResponse), - 0x0005 => Some(Self::RoutingActivationRequest), - 0x0006 => Some(Self::RoutingActivationResponse), - 0x0007 => Some(Self::AliveCheckRequest), - 0x0008 => Some(Self::AliveCheckResponse), - 0x4001 => Some(Self::DoipEntityStatusRequest), - 0x4002 => Some(Self::DoipEntityStatusResponse), - 0x4003 => Some(Self::DiagnosticPowerModeRequest), - 0x4004 => Some(Self::DiagnosticPowerModeResponse), - 0x8001 => Some(Self::DiagnosticMessage), - 0x8002 => Some(Self::DiagnosticMessagePositiveAck), - 0x8003 => Some(Self::DiagnosticMessageNegativeAck), - _ => None, + 0x0000 => Ok(Self::GenericNack), + 0x0001 => Ok(Self::VehicleIdentificationRequest), + 0x0002 => Ok(Self::VehicleIdentificationRequestWithEid), + 0x0003 => Ok(Self::VehicleIdentificationRequestWithVin), + 0x0004 => Ok(Self::VehicleIdentificationResponse), + 0x0005 => Ok(Self::RoutingActivationRequest), + 0x0006 => Ok(Self::RoutingActivationResponse), + 0x0007 => Ok(Self::AliveCheckRequest), + 0x0008 => Ok(Self::AliveCheckResponse), + 0x4001 => Ok(Self::DoipEntityStatusRequest), + 0x4002 => Ok(Self::DoipEntityStatusResponse), + 0x4003 => Ok(Self::DiagnosticPowerModeRequest), + 0x4004 => Ok(Self::DiagnosticPowerModeResponse), + 0x8001 => Ok(Self::DiagnosticMessage), + 0x8002 => Ok(Self::DiagnosticMessagePositiveAck), + 0x8003 => Ok(Self::DiagnosticMessageNegativeAck), + other => Err(other), } } +} +impl PayloadType { + /// Returns minimum payload length for this payload type (ISO 13400-2:2019 Section 7) + /// + /// Each payload type has a defined minimum length to be considered valid. + /// For example, `DiagnosticMessage` requires at least 5 bytes (source address, + /// target address, and at least 1 UDS data byte). + #[must_use] pub const fn min_payload_length(self) -> usize { match self { - Self::GenericNack => 1, - Self::VehicleIdentificationRequest => 0, + Self::VehicleIdentificationRequest + | Self::AliveCheckRequest + | Self::DoipEntityStatusRequest + | Self::DiagnosticPowerModeRequest => 0, + Self::GenericNack | Self::DiagnosticPowerModeResponse => 1, + Self::AliveCheckResponse => 2, + Self::DoipEntityStatusResponse => 3, + Self::DiagnosticMessage + | Self::DiagnosticMessagePositiveAck + | Self::DiagnosticMessageNegativeAck => 5, Self::VehicleIdentificationRequestWithEid => 6, - Self::VehicleIdentificationRequestWithVin => 17, - Self::VehicleIdentificationResponse => 32, Self::RoutingActivationRequest => 7, Self::RoutingActivationResponse => 9, - Self::AliveCheckRequest => 0, - Self::AliveCheckResponse => 2, - Self::DoipEntityStatusRequest => 0, - Self::DoipEntityStatusResponse => 3, - Self::DiagnosticPowerModeRequest => 0, - Self::DiagnosticPowerModeResponse => 1, - Self::DiagnosticMessage => 5, - Self::DiagnosticMessagePositiveAck => 5, - Self::DiagnosticMessageNegativeAck => 5, + Self::VehicleIdentificationRequestWithVin => 17, + Self::VehicleIdentificationResponse => 32, } } } +/// `DoIP` Header Structure (ISO 13400-2:2019 Section 6) +/// +/// Represents the 8-byte generic `DoIP` header that precedes every `DoIP` message. +/// The header uses big-endian byte order for multi-byte fields. +/// +/// The `payload_type` field is stored as `u16` rather than `PayloadType` enum to allow +/// receiving and processing unknown/future payload types. This follows the robustness +/// principle: be liberal in what you accept. Unknown types trigger `GenericNackCode` +/// during validation but don't cause parsing failures. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DoipHeader { pub version: u8, @@ -121,26 +194,43 @@ pub struct DoipHeader { } impl DoipHeader { - pub fn parse(data: &[u8]) -> Result { - if data.len() < DOIP_HEADER_LENGTH { - return Err(ParseError::InvalidHeader(format!( - "header too short: expected {}, got {}", - DOIP_HEADER_LENGTH, - data.len() - ))); - } + /// Parse a `DoIP` header from a byte slice + /// + /// # Errors + /// Returns `ParseError::InvalidHeader` if data is less than 8 bytes + pub(crate) fn parse(data: &[u8]) -> Result { + let header: [u8; DOIP_HEADER_LENGTH] = data + .get(..DOIP_HEADER_LENGTH) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + ParseError::InvalidHeader(format!( + "DoIP header too short: expected {}, got {}", + DOIP_HEADER_LENGTH, + data.len() + )) + })?; + Ok(Self { - version: data[0], - inverse_version: data[1], - payload_type: u16::from_be_bytes([data[2], data[3]]), - payload_length: u32::from_be_bytes([data[4], data[5], data[6], data[7]]), + version: header[0], + inverse_version: header[1], + payload_type: u16::from_be_bytes([header[2], header[3]]), + payload_length: u32::from_be_bytes([header[4], header[5], header[6], header[7]]), }) } - pub fn parse_from_buf(buf: &mut Bytes) -> Result { + /// Parse a `DoIP` header from a mutable Bytes buffer + /// + /// # Errors + /// Returns `ParseError::InvalidHeader` if buffer is less than 8 bytes + // + // Used by the streaming decode path in the TCP/UDP server handlers + // (feature/doip-async-server). Not yet called in this branch because + // the server handler layer has not been ported here yet. + #[allow(dead_code)] + pub(crate) fn parse_from_buf(buf: &mut Bytes) -> Result { if buf.len() < DOIP_HEADER_LENGTH { return Err(ParseError::InvalidHeader(format!( - "header too short: expected {}, got {}", + "DoIP header too short: expected {}, got {}", DOIP_HEADER_LENGTH, buf.len() ))); @@ -154,44 +244,75 @@ impl DoipHeader { } pub fn validate(&self) -> Option { - if self.version != DEFAULT_PROTOCOL_VERSION { - return Some(GenericNackCode::IncorrectPatternFormat); - } - if self.inverse_version != DEFAULT_PROTOCOL_VERSION_INV { + debug!( + "Validating DoIP header: version=0x{:02X}, inverse=0x{:02X}, type=0x{:04X}, len={}", + self.version, self.inverse_version, self.payload_type, self.payload_length + ); + + // Accept DoIP protocol versions V1, V2, V3 (and wildcard 0xFF for default/any) + // ISO 13400-2:2012 uses V2, ISO 13400-2:2019 uses V3 + let valid_version = matches!( + self.version, + PROTOCOL_VERSION_V1 + | DEFAULT_PROTOCOL_VERSION + | PROTOCOL_VERSION_V3 + | DOIP_VERSION_DEFAULT + ); + if !valid_version { + warn!("Invalid DoIP version: 0x{:02X}", self.version); return Some(GenericNackCode::IncorrectPatternFormat); } - if self.version ^ self.inverse_version != 0xFF { + + // Check version XOR inverse_version == DOIP_HEADER_VERSION_MASK (0xFF) + if self.version ^ self.inverse_version != DOIP_HEADER_VERSION_MASK { + warn!( + "Version/inverse mismatch: 0x{:02X} ^ 0x{:02X} = 0x{:02X} (expected 0x{:02X})", + self.version, + self.inverse_version, + self.version ^ self.inverse_version, + DOIP_HEADER_VERSION_MASK, + ); return Some(GenericNackCode::IncorrectPatternFormat); } - let payload_type = match PayloadType::from_u16(self.payload_type) { - Some(pt) => pt, - None => return Some(GenericNackCode::UnknownPayloadType), + let Some(payload_type) = PayloadType::try_from(self.payload_type).ok() else { + warn!("Unknown payload type: 0x{:04X}", self.payload_type); + return Some(GenericNackCode::UnknownPayloadType); }; if self.payload_length > MAX_DOIP_MESSAGE_SIZE { + warn!("Message too large: {} bytes", self.payload_length); return Some(GenericNackCode::MessageTooLarge); } if (self.payload_length as usize) < payload_type.min_payload_length() { + warn!( + "Payload too short for {:?}: {} < {}", + payload_type, + self.payload_length, + payload_type.min_payload_length() + ); return Some(GenericNackCode::InvalidPayloadLength); } + None } + #[must_use] pub fn is_valid(&self) -> bool { self.validate().is_none() } - pub const fn total_length(&self) -> usize { - DOIP_HEADER_LENGTH + self.payload_length as usize + /// Returns the total message length (header + payload) + #[must_use] + pub fn message_length(&self) -> usize { + DOIP_HEADER_LENGTH.saturating_add(self.payload_length as usize) } + /// Serialize header to bytes + #[must_use] pub fn to_bytes(&self) -> Bytes { let mut buf = BytesMut::with_capacity(DOIP_HEADER_LENGTH); - buf.put_u8(self.version); - buf.put_u8(self.inverse_version); - buf.put_u16(self.payload_type); - buf.put_u32(self.payload_length); + self.write_to(&mut buf); buf.freeze() } @@ -216,9 +337,10 @@ impl Default for DoipHeader { impl std::fmt::Display for DoipHeader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let payload_name = PayloadType::from_u16(self.payload_type) - .map(|pt| format!("{:?}", pt)) - .unwrap_or_else(|| format!("Unknown(0x{:04X})", self.payload_type)); + let payload_name = PayloadType::try_from(self.payload_type).ok().map_or_else( + || format!("Unknown(0x{:04X})", self.payload_type), + |pt| format!("{pt:?}"), + ); write!( f, "DoipHeader {{ version: 0x{:02X}, type: {}, length: {} }}", @@ -227,6 +349,10 @@ impl std::fmt::Display for DoipHeader { } } +/// `DoIP` Message (ISO 13400-2 Section 6) +/// +/// Complete `DoIP` message consisting of an 8-byte header and variable-length payload. +/// ISO 13400-2 uses the term "message" throughout the specification. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DoipMessage { pub header: DoipHeader, @@ -234,24 +360,69 @@ pub struct DoipMessage { } impl DoipMessage { - pub fn new(payload_type: PayloadType, payload: Bytes) -> Self { - Self { + /// Create a new `DoIP` message with the default protocol version. + /// + /// # Errors + /// + /// Returns [`ParseError::PayloadTooLarge`] if `payload.len()` exceeds `u32::MAX`. + /// In practice this cannot occur because the `DoIP` codec enforces a 4 MB message limit. + // + // Called by server handlers to build outgoing response messages. + // No production callers exist in this branch yet — server handlers live + // on feature/doip-async-server and have not been ported here. + #[allow(dead_code)] + pub(crate) fn new(payload_type: PayloadType, payload: Bytes) -> Result { + Ok(Self { header: DoipHeader { version: DEFAULT_PROTOCOL_VERSION, inverse_version: DEFAULT_PROTOCOL_VERSION_INV, payload_type: payload_type as u16, - payload_length: payload.len() as u32, + payload_length: u32::try_from(payload.len()) + .map_err(|_| ParseError::PayloadTooLarge)?, }, payload, - } + }) } - pub fn with_raw_type(payload_type: u16, payload: Bytes) -> Self { + /// Create a `DoIP` message with a specific protocol version. + /// + /// This mirrors the version from the incoming request. + /// + /// # Errors + /// + /// Returns [`ParseError::PayloadTooLarge`] if `payload.len()` exceeds `u32::MAX`. + /// In practice this cannot occur because the `DoIP` codec enforces a 4 MB message limit. + // + // Required by ISO 13400-2:2019: the server must mirror the client's protocol + // version in every response. No production callers exist in this branch yet — + // server handlers live on feature/doip-async-server and have not been ported here. + #[allow(dead_code)] + pub(crate) fn with_version(version: u8, payload_type: PayloadType, payload: Bytes) -> Result { + Ok(Self { + header: DoipHeader { + version, + inverse_version: version ^ DOIP_HEADER_VERSION_MASK, // Compute inverse + payload_type: payload_type as u16, + payload_length: u32::try_from(payload.len()) + .map_err(|_| ParseError::PayloadTooLarge)?, + }, + payload, + }) + } + + /// Create a DoIP message with a raw (unparsed) payload type + /// + /// This is primarily used for testing unknown/invalid payload types. + /// Production code should use `new()` or `with_version()` instead. + #[cfg(test)] + pub fn with_raw_payload_type(payload_type: u16, payload: Bytes) -> Self { Self { header: DoipHeader { version: DEFAULT_PROTOCOL_VERSION, inverse_version: DEFAULT_PROTOCOL_VERSION_INV, payload_type, + // Safe cast: DoIP messages are limited to MAX_DOIP_MESSAGE_SIZE (4MB) + // which fits in u32. The codec enforces this limit during decode. payload_length: payload.len() as u32, }, payload, @@ -259,15 +430,17 @@ impl DoipMessage { } pub fn payload_type(&self) -> Option { - PayloadType::from_u16(self.header.payload_type) + PayloadType::try_from(self.header.payload_type).ok() } - pub fn total_length(&self) -> usize { - DOIP_HEADER_LENGTH + self.payload.len() + /// Returns the total message length (header + payload) + #[must_use] + pub fn message_length(&self) -> usize { + DOIP_HEADER_LENGTH.saturating_add(self.payload.len()) } pub fn to_bytes(&self) -> Bytes { - let mut buf = BytesMut::with_capacity(self.total_length()); + let mut buf = BytesMut::with_capacity(self.message_length()); self.header.write_to(&mut buf); buf.extend_from_slice(&self.payload); buf.freeze() @@ -287,6 +460,7 @@ pub struct DoipCodec { } impl DoipCodec { + #[must_use] pub fn new() -> Self { Self { state: DecodeState::Header, @@ -294,6 +468,11 @@ impl DoipCodec { } } + /// Create codec with custom max payload size limit + /// + /// The size is u32 to match the `DoIP` header `payload_length` field (4 bytes). + /// This provides `DoS` protection by rejecting oversized messages early. + #[must_use] pub fn with_max_payload_size(max_size: u32) -> Self { Self { state: DecodeState::Header, @@ -312,6 +491,8 @@ impl Decoder for DoipCodec { type Item = DoipMessage; type Error = io::Error; + // Using fully qualified std::result::Result because the module-level Result + // type alias uses ParseError, but the Decoder trait requires io::Error. fn decode( &mut self, src: &mut BytesMut, @@ -320,17 +501,28 @@ impl Decoder for DoipCodec { match self.state { DecodeState::Header => { if src.len() < DOIP_HEADER_LENGTH { + // Reserve space to reduce reallocations when more data arrives src.reserve(DOIP_HEADER_LENGTH); return Ok(None); } - let header = DoipHeader::parse(&src[..DOIP_HEADER_LENGTH]) + // Log raw bytes for debugging + let header_slice = src.get(..DOIP_HEADER_LENGTH).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "buffer too short") + })?; + debug!("Received raw header bytes: {:02X?}", header_slice); + + let header = DoipHeader::parse(header_slice) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; if let Some(nack_code) = header.validate() { + warn!( + "Header validation failed: {:?} - raw bytes: {:02X?}", + nack_code, header_slice + ); return Err(io::Error::new( io::ErrorKind::InvalidData, - format!("validation failed: {:?}", nack_code), + format!("validation failed: {nack_code:?}"), )); } @@ -344,12 +536,16 @@ impl Decoder for DoipCodec { )); } - src.reserve(header.total_length()); + // Pre-allocate buffer for the complete message + src.reserve(header.message_length()); self.state = DecodeState::Payload(header); } DecodeState::Payload(header) => { - if src.len() < header.total_length() { + let total_len = header.message_length(); + if src.len() < total_len { + // Still waiting for complete payload - this is normal for large messages + // or when data arrives in multiple TCP packets return Ok(None); } @@ -372,7 +568,7 @@ impl Encoder for DoipCodec { item: DoipMessage, dst: &mut BytesMut, ) -> std::result::Result<(), Self::Error> { - dst.reserve(item.total_length()); + dst.reserve(item.message_length()); item.header.write_to(dst); dst.extend_from_slice(&item.payload); Ok(()) @@ -419,7 +615,7 @@ mod tests { let hdr = DoipHeader::parse(&raw).unwrap(); assert_eq!(hdr.payload_type, 0x8001); // DiagnosticMessage - assert_eq!(hdr.payload_length, 7); // 2+2+3 = SA+TA+UDS + assert_eq!(hdr.payload_length, 7); // 2+2+3 = SA+TA+UDS } #[test] @@ -453,14 +649,17 @@ mod tests { #[test] fn reject_wrong_protocol_version() { - // Someone sends version 0x03 - we only support 0x02 + // Someone sends version 0x04 - we only support 0x01, 0x02, 0x03, 0xFF let hdr = DoipHeader { - version: 0x03, - inverse_version: 0xFC, + version: 0x04, + inverse_version: 0xFB, payload_type: 0x0001, payload_length: 0, }; - assert_eq!(hdr.validate(), Some(GenericNackCode::IncorrectPatternFormat)); + assert_eq!( + hdr.validate(), + Some(GenericNackCode::IncorrectPatternFormat) + ); } #[test] @@ -472,7 +671,10 @@ mod tests { payload_type: 0x0001, payload_length: 0, }; - assert_eq!(hdr.validate(), Some(GenericNackCode::IncorrectPatternFormat)); + assert_eq!( + hdr.validate(), + Some(GenericNackCode::IncorrectPatternFormat) + ); } #[test] @@ -508,26 +710,44 @@ mod tests { #[test] fn payload_type_lookup_works() { - assert_eq!(PayloadType::from_u16(0x0001), Some(PayloadType::VehicleIdentificationRequest)); - assert_eq!(PayloadType::from_u16(0x0005), Some(PayloadType::RoutingActivationRequest)); - assert_eq!(PayloadType::from_u16(0x8001), Some(PayloadType::DiagnosticMessage)); - assert_eq!(PayloadType::from_u16(0x8002), Some(PayloadType::DiagnosticMessagePositiveAck)); + assert_eq!( + PayloadType::try_from(0x0001).ok(), + Some(PayloadType::VehicleIdentificationRequest) + ); + assert_eq!( + PayloadType::try_from(0x0005).ok(), + Some(PayloadType::RoutingActivationRequest) + ); + assert_eq!( + PayloadType::try_from(0x8001).ok(), + Some(PayloadType::DiagnosticMessage) + ); + assert_eq!( + PayloadType::try_from(0x8002).ok(), + Some(PayloadType::DiagnosticMessagePositiveAck) + ); } #[test] fn payload_type_gaps_return_none() { // These are in gaps between valid ranges - assert!(PayloadType::from_u16(0x0009).is_none()); - assert!(PayloadType::from_u16(0x4000).is_none()); - assert!(PayloadType::from_u16(0x8000).is_none()); - assert!(PayloadType::from_u16(0xFFFF).is_none()); + assert!(PayloadType::try_from(0x0009_u16).is_err()); + assert!(PayloadType::try_from(0x4000_u16).is_err()); + assert!(PayloadType::try_from(0x8000_u16).is_err()); + assert!(PayloadType::try_from(0xFFFF_u16).is_err()); } #[test] fn minimum_payload_lengths_per_spec() { // ISO 13400-2 requirements - assert_eq!(PayloadType::VehicleIdentificationRequest.min_payload_length(), 0); - assert_eq!(PayloadType::RoutingActivationRequest.min_payload_length(), 7); + assert_eq!( + PayloadType::VehicleIdentificationRequest.min_payload_length(), + 0 + ); + assert_eq!( + PayloadType::RoutingActivationRequest.min_payload_length(), + 7 + ); assert_eq!(PayloadType::DiagnosticMessage.min_payload_length(), 5); assert_eq!(PayloadType::AliveCheckResponse.min_payload_length(), 2); } @@ -540,7 +760,7 @@ mod tests { fn create_tester_present_message() { // UDS TesterPresent: 0x3E 0x00 let uds = Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x3E, 0x00]); - let msg = DoipMessage::new(PayloadType::DiagnosticMessage, uds); + let msg = DoipMessage::new(PayloadType::DiagnosticMessage, uds).unwrap(); assert_eq!(msg.header.payload_type, 0x8001); assert_eq!(msg.header.payload_length, 6); @@ -550,25 +770,22 @@ mod tests { #[test] fn create_vehicle_id_broadcast() { // Empty payload for discovery - let msg = DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()); + let msg = DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()).unwrap(); assert_eq!(msg.header.payload_length, 0); - assert_eq!(msg.total_length(), 8); // Just the header + assert_eq!(msg.message_length(), 8); // Just the header } #[test] fn message_with_unknown_type() { // For testing/fuzzing - create msg with invalid type - let msg = DoipMessage::with_raw_type(0xBEEF, Bytes::new()); + let msg = DoipMessage::with_raw_payload_type(0xBEEF, Bytes::new()); assert_eq!(msg.payload_type(), None); } #[test] fn serialize_message_to_wire_format() { - let msg = DoipMessage::new( - PayloadType::AliveCheckRequest, - Bytes::new() - ); + let msg = DoipMessage::new(PayloadType::AliveCheckRequest, Bytes::new()).unwrap(); let wire = msg.to_bytes(); assert_eq!(wire.len(), 8); @@ -584,10 +801,8 @@ mod tests { fn decode_complete_alive_check_response() { let mut codec = DoipCodec::new(); // AliveCheckResponse with source address 0x0E80 - let mut buf = BytesMut::from(&[ - 0x02, 0xFD, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, - 0x0E, 0x80 - ][..]); + let mut buf = + BytesMut::from(&[0x02, 0xFD, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x0E, 0x80][..]); let msg = codec.decode(&mut buf).unwrap().unwrap(); assert_eq!(msg.header.payload_type, 0x0008); @@ -608,10 +823,8 @@ mod tests { fn wait_for_more_data_when_payload_incomplete() { let mut codec = DoipCodec::new(); // Header says 5 bytes payload, but only 2 arrived - let mut buf = BytesMut::from(&[ - 0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x05, - 0x0E, 0x80 - ][..]); + let mut buf = + BytesMut::from(&[0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x05, 0x0E, 0x80][..]); assert!(codec.decode(&mut buf).unwrap().is_none()); } @@ -619,12 +832,14 @@ mod tests { #[test] fn decode_back_to_back_messages() { let mut codec = DoipCodec::new(); - let mut buf = BytesMut::from(&[ - // Msg 1: AliveCheckRequest - 0x02, 0xFD, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, - // Msg 2: AliveCheckResponse - 0x02, 0xFD, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x0E, 0x80, - ][..]); + let mut buf = BytesMut::from( + &[ + // Msg 1: AliveCheckRequest + 0x02, 0xFD, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + // Msg 2: AliveCheckResponse + 0x02, 0xFD, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x0E, 0x80, + ][..], + ); let m1 = codec.decode(&mut buf).unwrap().unwrap(); let m2 = codec.decode(&mut buf).unwrap().unwrap(); @@ -637,10 +852,8 @@ mod tests { #[test] fn reject_invalid_header_in_stream() { let mut codec = DoipCodec::new(); - // Bad version - let mut buf = BytesMut::from(&[ - 0x01, 0xFE, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 - ][..]); + // Bad version (0x04 is not valid - only 0x01, 0x02, 0x03, 0xFF are accepted) + let mut buf = BytesMut::from(&[0x04, 0xFB, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00][..]); assert!(codec.decode(&mut buf).is_err()); } @@ -649,9 +862,7 @@ mod tests { fn respect_custom_max_payload_size() { let mut codec = DoipCodec::with_max_payload_size(100); // Payload length = 101, over our limit - let mut buf = BytesMut::from(&[ - 0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x65 - ][..]); + let mut buf = BytesMut::from(&[0x02, 0xFD, 0x80, 0x01, 0x00, 0x00, 0x00, 0x65][..]); assert!(codec.decode(&mut buf).is_err()); } @@ -660,7 +871,7 @@ mod tests { fn encode_diagnostic_message() { let mut codec = DoipCodec::new(); let payload = Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x3E]); - let msg = DoipMessage::new(PayloadType::DiagnosticMessage, payload); + let msg = DoipMessage::new(PayloadType::DiagnosticMessage, payload).unwrap(); let mut buf = BytesMut::new(); codec.encode(msg, &mut buf).unwrap(); @@ -679,8 +890,9 @@ mod tests { let mut codec = DoipCodec::new(); let original = DoipMessage::new( PayloadType::DiagnosticMessage, - Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x22, 0xF1, 0x90]) - ); + Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x22, 0xF1, 0x90]), + ) + .unwrap(); let mut buf = BytesMut::new(); codec.encode(original.clone(), &mut buf).unwrap(); @@ -720,7 +932,7 @@ mod tests { #[test] fn parse_error_shows_useful_message() { let err = ParseError::InvalidHeader("buffer too short".into()); - let msg = format!("{}", err); + let msg = format!("{err}"); assert!(msg.contains("Invalid header")); assert!(msg.contains("buffer too short")); } @@ -730,7 +942,7 @@ mod tests { let io_err = io::Error::new(io::ErrorKind::UnexpectedEof, "connection lost"); let parse_err: ParseError = io_err.into(); match parse_err { - ParseError::Io(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof), + ParseError::IoError(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof), _ => panic!("expected Io variant"), } } @@ -747,14 +959,17 @@ mod tests { #[test] fn protocol_version_inverse_relationship() { - // Version XOR inverse must equal 0xFF (per spec) - assert_eq!(DEFAULT_PROTOCOL_VERSION ^ DEFAULT_PROTOCOL_VERSION_INV, 0xFF); + // Version XOR inverse must equal DOIP_HEADER_VERSION_MASK (0xFF, per ISO 13400-2:2019 spec) + assert_eq!( + DEFAULT_PROTOCOL_VERSION ^ DEFAULT_PROTOCOL_VERSION_INV, + DOIP_HEADER_VERSION_MASK + ); } #[test] fn header_display_shows_readable_info() { let hdr = make_header(0x8001, 10); - let s = format!("{}", hdr); + let s = format!("{hdr}"); assert!(s.contains("DiagnosticMessage")); assert!(s.contains("10")); // payload length } diff --git a/src/doip/hearder_parser.rs b/src/doip/hearder_parser.rs deleted file mode 100644 index 4697534..0000000 --- a/src/doip/hearder_parser.rs +++ /dev/null @@ -1,378 +0,0 @@ -//! DoIP Header Parser - -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use std::io; -use tokio_util::codec::{Decoder, Encoder}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum GenericNackCode { - IncorrectPatternFormat = 0x00, - UnknownPayloadType = 0x01, - MessageTooLarge = 0x02, - OutOfMemory = 0x03, - InvalidPayloadLength = 0x04, -} - -#[derive(Debug)] -pub enum ParseError { - InvalidHeader(String), - Io(io::Error), -} - -impl std::fmt::Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidHeader(msg) => write!(f, "Invalid header: {}", msg), - Self::Io(e) => write!(f, "IO error: {}", e), - } - } -} - -impl std::error::Error for ParseError {} - -impl From for ParseError { - fn from(e: io::Error) -> Self { - Self::Io(e) - } -} - -pub type Result = std::result::Result; - -pub const DEFAULT_PROTOCOL_VERSION: u8 = 0x02; -pub const DEFAULT_PROTOCOL_VERSION_INV: u8 = 0xFD; -pub const DOIP_HEADER_LENGTH: usize = 8; -pub const MAX_DOIP_MESSAGE_SIZE: u32 = 0x0FFF_FFFF; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u16)] -pub enum PayloadType { - GenericNack = 0x0000, - VehicleIdentificationRequest = 0x0001, - VehicleIdentificationRequestWithEid = 0x0002, - VehicleIdentificationRequestWithVin = 0x0003, - VehicleIdentificationResponse = 0x0004, - RoutingActivationRequest = 0x0005, - RoutingActivationResponse = 0x0006, - AliveCheckRequest = 0x0007, - AliveCheckResponse = 0x0008, - DoipEntityStatusRequest = 0x4001, - DoipEntityStatusResponse = 0x4002, - DiagnosticPowerModeRequest = 0x4003, - DiagnosticPowerModeResponse = 0x4004, - DiagnosticMessage = 0x8001, - DiagnosticMessagePositiveAck = 0x8002, - DiagnosticMessageNegativeAck = 0x8003, -} - -impl PayloadType { - pub fn from_u16(value: u16) -> Option { - match value { - 0x0000 => Some(Self::GenericNack), - 0x0001 => Some(Self::VehicleIdentificationRequest), - 0x0002 => Some(Self::VehicleIdentificationRequestWithEid), - 0x0003 => Some(Self::VehicleIdentificationRequestWithVin), - 0x0004 => Some(Self::VehicleIdentificationResponse), - 0x0005 => Some(Self::RoutingActivationRequest), - 0x0006 => Some(Self::RoutingActivationResponse), - 0x0007 => Some(Self::AliveCheckRequest), - 0x0008 => Some(Self::AliveCheckResponse), - 0x4001 => Some(Self::DoipEntityStatusRequest), - 0x4002 => Some(Self::DoipEntityStatusResponse), - 0x4003 => Some(Self::DiagnosticPowerModeRequest), - 0x4004 => Some(Self::DiagnosticPowerModeResponse), - 0x8001 => Some(Self::DiagnosticMessage), - 0x8002 => Some(Self::DiagnosticMessagePositiveAck), - 0x8003 => Some(Self::DiagnosticMessageNegativeAck), - _ => None, - } - } - - pub const fn min_payload_length(self) -> usize { - match self { - Self::GenericNack => 1, - Self::VehicleIdentificationRequest => 0, - Self::VehicleIdentificationRequestWithEid => 6, - Self::VehicleIdentificationRequestWithVin => 17, - Self::VehicleIdentificationResponse => 32, - Self::RoutingActivationRequest => 7, - Self::RoutingActivationResponse => 9, - Self::AliveCheckRequest => 0, - Self::AliveCheckResponse => 2, - Self::DoipEntityStatusRequest => 0, - Self::DoipEntityStatusResponse => 3, - Self::DiagnosticPowerModeRequest => 0, - Self::DiagnosticPowerModeResponse => 1, - Self::DiagnosticMessage => 5, - Self::DiagnosticMessagePositiveAck => 5, - Self::DiagnosticMessageNegativeAck => 5, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DoipHeader { - pub version: u8, - pub inverse_version: u8, - pub payload_type: u16, - pub payload_length: u32, -} - -impl DoipHeader { - pub fn parse(data: &[u8]) -> Result { - if data.len() < DOIP_HEADER_LENGTH { - return Err(ParseError::InvalidHeader(format!( - "header too short: expected {}, got {}", - DOIP_HEADER_LENGTH, - data.len() - ))); - } - Ok(Self { - version: data[0], - inverse_version: data[1], - payload_type: u16::from_be_bytes([data[2], data[3]]), - payload_length: u32::from_be_bytes([data[4], data[5], data[6], data[7]]), - }) - } - - pub fn parse_from_buf(buf: &mut Bytes) -> Result { - if buf.len() < DOIP_HEADER_LENGTH { - return Err(ParseError::InvalidHeader(format!( - "header too short: expected {}, got {}", - DOIP_HEADER_LENGTH, - buf.len() - ))); - } - Ok(Self { - version: buf.get_u8(), - inverse_version: buf.get_u8(), - payload_type: buf.get_u16(), - payload_length: buf.get_u32(), - }) - } - - pub fn validate(&self) -> Option { - if self.version != DEFAULT_PROTOCOL_VERSION { - return Some(GenericNackCode::IncorrectPatternFormat); - } - if self.inverse_version != DEFAULT_PROTOCOL_VERSION_INV { - return Some(GenericNackCode::IncorrectPatternFormat); - } - if self.version ^ self.inverse_version != 0xFF { - return Some(GenericNackCode::IncorrectPatternFormat); - } - - let payload_type = match PayloadType::from_u16(self.payload_type) { - Some(pt) => pt, - None => return Some(GenericNackCode::UnknownPayloadType), - }; - - if self.payload_length > MAX_DOIP_MESSAGE_SIZE { - return Some(GenericNackCode::MessageTooLarge); - } - if (self.payload_length as usize) < payload_type.min_payload_length() { - return Some(GenericNackCode::InvalidPayloadLength); - } - None - } - - pub fn is_valid(&self) -> bool { - self.validate().is_none() - } - - pub const fn total_length(&self) -> usize { - DOIP_HEADER_LENGTH + self.payload_length as usize - } - - pub fn to_bytes(&self) -> Bytes { - let mut buf = BytesMut::with_capacity(DOIP_HEADER_LENGTH); - buf.put_u8(self.version); - buf.put_u8(self.inverse_version); - buf.put_u16(self.payload_type); - buf.put_u32(self.payload_length); - buf.freeze() - } - - pub fn write_to(&self, buf: &mut BytesMut) { - buf.put_u8(self.version); - buf.put_u8(self.inverse_version); - buf.put_u16(self.payload_type); - buf.put_u32(self.payload_length); - } -} - -impl Default for DoipHeader { - fn default() -> Self { - Self { - version: DEFAULT_PROTOCOL_VERSION, - inverse_version: DEFAULT_PROTOCOL_VERSION_INV, - payload_type: 0, - payload_length: 0, - } - } -} - -impl std::fmt::Display for DoipHeader { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let payload_name = PayloadType::from_u16(self.payload_type) - .map(|pt| format!("{:?}", pt)) - .unwrap_or_else(|| format!("Unknown(0x{:04X})", self.payload_type)); - write!( - f, - "DoipHeader {{ version: 0x{:02X}, type: {}, length: {} }}", - self.version, payload_name, self.payload_length - ) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DoipMessage { - pub header: DoipHeader, - pub payload: Bytes, -} - -impl DoipMessage { - pub fn new(payload_type: PayloadType, payload: Bytes) -> Self { - Self { - header: DoipHeader { - version: DEFAULT_PROTOCOL_VERSION, - inverse_version: DEFAULT_PROTOCOL_VERSION_INV, - payload_type: payload_type as u16, - payload_length: payload.len() as u32, - }, - payload, - } - } - - pub fn with_raw_type(payload_type: u16, payload: Bytes) -> Self { - Self { - header: DoipHeader { - version: DEFAULT_PROTOCOL_VERSION, - inverse_version: DEFAULT_PROTOCOL_VERSION_INV, - payload_type, - payload_length: payload.len() as u32, - }, - payload, - } - } - - pub fn payload_type(&self) -> Option { - PayloadType::from_u16(self.header.payload_type) - } - - pub fn total_length(&self) -> usize { - DOIP_HEADER_LENGTH + self.payload.len() - } - - pub fn to_bytes(&self) -> Bytes { - let mut buf = BytesMut::with_capacity(self.total_length()); - self.header.write_to(&mut buf); - buf.extend_from_slice(&self.payload); - buf.freeze() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DecodeState { - Header, - Payload(DoipHeader), -} - -#[derive(Debug)] -pub struct DoipCodec { - state: DecodeState, - max_payload_size: u32, -} - -impl DoipCodec { - pub fn new() -> Self { - Self { - state: DecodeState::Header, - max_payload_size: MAX_DOIP_MESSAGE_SIZE, - } - } - - pub fn with_max_payload_size(max_size: u32) -> Self { - Self { - state: DecodeState::Header, - max_payload_size: max_size, - } - } -} - -impl Default for DoipCodec { - fn default() -> Self { - Self::new() - } -} - -impl Decoder for DoipCodec { - type Item = DoipMessage; - type Error = io::Error; - - fn decode( - &mut self, - src: &mut BytesMut, - ) -> std::result::Result, Self::Error> { - loop { - match self.state { - DecodeState::Header => { - if src.len() < DOIP_HEADER_LENGTH { - src.reserve(DOIP_HEADER_LENGTH); - return Ok(None); - } - - let header = DoipHeader::parse(&src[..DOIP_HEADER_LENGTH]) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - if let Some(nack_code) = header.validate() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("validation failed: {:?}", nack_code), - )); - } - - if header.payload_length > self.max_payload_size { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "payload too large: {} > {}", - header.payload_length, self.max_payload_size - ), - )); - } - - src.reserve(header.total_length()); - self.state = DecodeState::Payload(header); - } - - DecodeState::Payload(header) => { - if src.len() < header.total_length() { - return Ok(None); - } - - let _ = src.split_to(DOIP_HEADER_LENGTH); - let payload = src.split_to(header.payload_length as usize).freeze(); - - self.state = DecodeState::Header; - return Ok(Some(DoipMessage { header, payload })); - } - } - } - } -} - -impl Encoder for DoipCodec { - type Error = io::Error; - - fn encode( - &mut self, - item: DoipMessage, - dst: &mut BytesMut, - ) -> std::result::Result<(), Self::Error> { - dst.reserve(item.total_length()); - item.header.write_to(dst); - dst.extend_from_slice(&item.payload); - Ok(()) - } -} diff --git a/src/doip/mod.rs b/src/doip/mod.rs index dfebf91..950fd16 100644 --- a/src/doip/mod.rs +++ b/src/doip/mod.rs @@ -1,15 +1,90 @@ -//! DoIP Protocol Implementation +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ //! -//! This module provides the core DoIP protocol types and codec for TCP/UDP communication. +//! This module provides the core `DoIP` protocol types and codec for TCP/UDP communication. +pub mod alive_check; +pub mod diagnostic_message; pub mod header_parser; +pub mod payload; pub mod routing_activation; -pub mod diagnostic_message; pub mod vehicle_id; -pub mod alive_check; +use bytes::{Bytes, BytesMut}; +use crate::DoipError; + +/// Trait for DoIP message types that can be parsed from a raw payload slice. +/// +/// Implement this for every message struct so callers can decode incoming +/// DoIP frames through a uniform interface. +pub trait DoipParseable: Sized { + /// Parse a DoIP message from a raw payload byte slice. + /// + /// # Errors + /// Returns [`DoipError`] if the payload is malformed or too short. + fn parse(payload: &[u8]) -> std::result::Result; +} + +/// Trait for DoIP message types that can be serialized to a [`Bytes`] buffer. +/// +/// Implement [`write_to`] with the wire-format logic. The default [`to_bytes`] +/// wraps it in a `BytesMut` and calls `freeze()`, so you never write that +/// boilerplate again. +pub trait DoipSerializable { + /// Write the serialized wire-format bytes into `buf`. + fn write_to(&self, buf: &mut BytesMut); + + /// Return the exact serialized byte count, if known without encoding. + /// + /// Override this to enable pre-allocated buffers in [`to_bytes`], avoiding + /// incremental `BytesMut` reallocations for large messages. + fn serialized_len(&self) -> Option { + None + } + + /// Serialize this message into a [`Bytes`] buffer. + /// + /// Pre-allocates the buffer when [`serialized_len`] returns `Some`. + fn to_bytes(&self) -> Bytes { + let mut buf = match self.serialized_len() { + Some(n) => BytesMut::with_capacity(n), + None => BytesMut::new(), + }; + self.write_to(&mut buf); + buf.freeze() + } +} + +/// Build a [`DoipError::PayloadTooShort`] from the given slice and expected length. +pub(crate) fn too_short(payload: &[u8], expected: usize) -> DoipError { + DoipError::PayloadTooShort { expected, actual: payload.len() } +} + +/// Return `Err` if `payload` is shorter than `expected` bytes. +pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> std::result::Result<(), DoipError> { + if payload.len() < expected { + Err(too_short(payload, expected)) + } else { + Ok(()) + } +} + +// Re-export core types and constants for convenient access. +// Constants are exported to allow external testing and custom DoIP message construction. pub use header_parser::{ - DoipCodec, DoipHeader, DoipMessage, GenericNackCode, ParseError, PayloadType, Result, + DoipCodec, DoipHeader, DoipMessage, GenericNackCode, PayloadType, DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, - MAX_DOIP_MESSAGE_SIZE, + MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, + DOIP_VERSION_DEFAULT, DOIP_HEADER_VERSION_MASK, }; +pub use payload::DoipPayload; diff --git a/src/doip/payload.rs b/src/doip/payload.rs new file mode 100644 index 0000000..cc27854 --- /dev/null +++ b/src/doip/payload.rs @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +//! Typed dispatch envelope for DoIP message payloads (ISO 13400-2:2019). +//! +//! [`DoipPayload`] is a strongly-typed enum that wraps every concrete payload +//! struct. Use [`DoipPayload::parse`] to decode a raw [`DoipMessage`] into +//! the correct variant in one step, instead of manually matching on +//! [`PayloadType`] throughout the codebase. +//! +//! # Example +//! ```ignore +//! let payload = DoipPayload::parse(&msg)?; +//! match payload { +//! DoipPayload::DiagnosticMessage(m) => handle_diag(m), +//! DoipPayload::AliveCheckRequest(_) => send_alive_response(), +//! _ => {} +//! } +//! ``` + +use crate::DoipError; +use super::{ + DoipMessage, DoipParseable, GenericNackCode, PayloadType, + alive_check, diagnostic_message, routing_activation, vehicle_id, +}; + +/// A fully-parsed DoIP message payload. +/// +/// Each variant corresponds to one [`PayloadType`] and wraps the concrete +/// struct returned by its [`DoipParseable`] impl. +#[derive(Debug, Clone)] +pub enum DoipPayload { + /// `0x0007` – Alive Check Request (zero-length payload) + AliveCheckRequest(alive_check::Request), + /// `0x0008` – Alive Check Response + AliveCheckResponse(alive_check::Response), + /// `0x0005` – Routing Activation Request + RoutingActivationRequest(routing_activation::Request), + /// `0x0006` – Routing Activation Response + RoutingActivationResponse(routing_activation::Response), + /// `0x8001` – Diagnostic Message (UDS data) + DiagnosticMessage(diagnostic_message::Message), + /// `0x8002` – Diagnostic Message Positive Acknowledgement + DiagnosticMessagePositiveAck(diagnostic_message::PositiveAck), + /// `0x8003` – Diagnostic Message Negative Acknowledgement + DiagnosticMessageNegativeAck(diagnostic_message::NegativeAck), + /// `0x0001` – Vehicle Identification Request (no filter) + VehicleIdentificationRequest(vehicle_id::Request), + /// `0x0002` – Vehicle Identification Request filtered by EID + VehicleIdentificationRequestWithEid(vehicle_id::RequestWithEid), + /// `0x0003` – Vehicle Identification Request filtered by VIN + VehicleIdentificationRequestWithVin(vehicle_id::RequestWithVin), + /// `0x0004` – Vehicle Identification Response / Announce + VehicleIdentificationResponse(vehicle_id::Response), + /// `0x0000` – Generic DoIP Header Negative Acknowledgement + GenericNack(GenericNackCode), +} + +impl DoipPayload { + /// Decode the payload of a [`DoipMessage`] into a typed [`DoipPayload`] variant. + /// + /// # Errors + /// Returns [`DoipError::UnknownPayloadType`] when the `payload_type` field + /// in the header does not map to a known [`PayloadType`] variant, or when + /// the DoIP payload byte is not a recognized `GenericNackCode`. + /// + /// Returns a more specific [`DoipError`] (e.g. [`DoipError::PayloadTooShort`]) + /// when the payload bytes are present but malformed. + pub fn parse(msg: &DoipMessage) -> std::result::Result { + let payload = msg.payload.as_ref(); + + let payload_type = msg + .payload_type() + .ok_or(DoipError::UnknownPayloadType(msg.header.payload_type))?; + + match payload_type { + PayloadType::AliveCheckRequest => Ok(Self::AliveCheckRequest( + alive_check::Request::parse(payload)?, + )), + PayloadType::AliveCheckResponse => Ok(Self::AliveCheckResponse( + alive_check::Response::parse(payload)?, + )), + PayloadType::RoutingActivationRequest => Ok(Self::RoutingActivationRequest( + routing_activation::Request::parse(payload)?, + )), + PayloadType::RoutingActivationResponse => Ok(Self::RoutingActivationResponse( + routing_activation::Response::parse(payload)?, + )), + PayloadType::DiagnosticMessage => Ok(Self::DiagnosticMessage( + diagnostic_message::Message::parse(payload)?, + )), + PayloadType::DiagnosticMessagePositiveAck => Ok(Self::DiagnosticMessagePositiveAck( + diagnostic_message::PositiveAck::parse(payload)?, + )), + PayloadType::DiagnosticMessageNegativeAck => Ok(Self::DiagnosticMessageNegativeAck( + diagnostic_message::NegativeAck::parse(payload)?, + )), + PayloadType::VehicleIdentificationRequest => Ok(Self::VehicleIdentificationRequest( + vehicle_id::Request::parse(payload)?, + )), + PayloadType::VehicleIdentificationRequestWithEid => { + Ok(Self::VehicleIdentificationRequestWithEid( + vehicle_id::RequestWithEid::parse(payload)?, + )) + } + PayloadType::VehicleIdentificationRequestWithVin => { + Ok(Self::VehicleIdentificationRequestWithVin( + vehicle_id::RequestWithVin::parse(payload)?, + )) + } + PayloadType::VehicleIdentificationResponse => Ok(Self::VehicleIdentificationResponse( + vehicle_id::Response::parse(payload)?, + )), + PayloadType::GenericNack => { + let byte = payload + .first() + .copied() + .ok_or(DoipError::PayloadTooShort { expected: 1, actual: 0 })?; + let code = GenericNackCode::try_from(byte) + .map_err(|b| DoipError::UnknownPayloadType(u16::from(b)))?; + Ok(Self::GenericNack(code)) + } + PayloadType::DoipEntityStatusRequest + | PayloadType::DoipEntityStatusResponse + | PayloadType::DiagnosticPowerModeRequest + | PayloadType::DiagnosticPowerModeResponse => { + Err(DoipError::UnknownPayloadType(payload_type as u16)) + } + } + } + + /// Return the [`PayloadType`] that corresponds to this payload variant. + #[must_use] + pub fn payload_type(&self) -> PayloadType { + match self { + Self::AliveCheckRequest(_) => PayloadType::AliveCheckRequest, + Self::AliveCheckResponse(_) => PayloadType::AliveCheckResponse, + Self::RoutingActivationRequest(_) => PayloadType::RoutingActivationRequest, + Self::RoutingActivationResponse(_) => PayloadType::RoutingActivationResponse, + Self::DiagnosticMessage(_) => PayloadType::DiagnosticMessage, + Self::DiagnosticMessagePositiveAck(_) => PayloadType::DiagnosticMessagePositiveAck, + Self::DiagnosticMessageNegativeAck(_) => PayloadType::DiagnosticMessageNegativeAck, + Self::VehicleIdentificationRequest(_) => PayloadType::VehicleIdentificationRequest, + Self::VehicleIdentificationRequestWithEid(_) => { + PayloadType::VehicleIdentificationRequestWithEid + } + Self::VehicleIdentificationRequestWithVin(_) => { + PayloadType::VehicleIdentificationRequestWithVin + } + Self::VehicleIdentificationResponse(_) => PayloadType::VehicleIdentificationResponse, + Self::GenericNack(_) => PayloadType::GenericNack, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::Bytes; + use crate::doip::{DoipMessage, PayloadType}; + + fn make_msg(payload_type: PayloadType, payload: impl Into) -> DoipMessage { + DoipMessage { + header: crate::doip::DoipHeader { + version: crate::doip::DEFAULT_PROTOCOL_VERSION, + inverse_version: crate::doip::DEFAULT_PROTOCOL_VERSION_INV, + payload_type: payload_type as u16, + payload_length: 0, + }, + payload: payload.into(), + } + } + + #[test] + fn alive_check_request_roundtrip() { + let msg = make_msg(PayloadType::AliveCheckRequest, vec![]); + let parsed = DoipPayload::parse(&msg).unwrap(); + assert!(matches!(parsed, DoipPayload::AliveCheckRequest(_))); + assert_eq!(parsed.payload_type(), PayloadType::AliveCheckRequest); + } + + #[test] + fn alive_check_response_roundtrip() { + let msg = make_msg(PayloadType::AliveCheckResponse, vec![0x0E, 0x80]); + let parsed = DoipPayload::parse(&msg).unwrap(); + assert!(matches!(parsed, DoipPayload::AliveCheckResponse(_))); + } + + #[test] + fn routing_activation_request_roundtrip() { + let payload = vec![0x0E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00]; + let msg = make_msg(PayloadType::RoutingActivationRequest, payload); + let parsed = DoipPayload::parse(&msg).unwrap(); + assert!(matches!(parsed, DoipPayload::RoutingActivationRequest(_))); + } + + #[test] + fn routing_activation_response_roundtrip() { + use crate::doip::DoipSerializable; + let resp = routing_activation::Response::success(0x0E80, 0x1000); + let msg = make_msg(PayloadType::RoutingActivationResponse, resp.to_bytes()); + let parsed = DoipPayload::parse(&msg).unwrap(); + assert!(matches!(parsed, DoipPayload::RoutingActivationResponse(_))); + } + + #[test] + fn generic_nack_roundtrip() { + let msg = make_msg(PayloadType::GenericNack, vec![0x02]); + let parsed = DoipPayload::parse(&msg).unwrap(); + assert!(matches!( + parsed, + DoipPayload::GenericNack(GenericNackCode::MessageTooLarge) + )); + } + + #[test] + fn unknown_payload_type_error() { + let msg = DoipMessage { + header: crate::doip::DoipHeader { + version: crate::doip::DEFAULT_PROTOCOL_VERSION, + inverse_version: crate::doip::DEFAULT_PROTOCOL_VERSION_INV, + payload_type: 0xFFFF, + payload_length: 0, + }, + payload: Bytes::new(), + }; + let err = DoipPayload::parse(&msg).unwrap_err(); + assert!(matches!(err, crate::DoipError::UnknownPayloadType(0xFFFF))); + } + + #[test] + fn missing_alive_check_response_data_errors() { + let msg = make_msg(PayloadType::AliveCheckResponse, vec![]); + assert!(DoipPayload::parse(&msg).is_err()); + } + + #[test] + fn payload_type_round_trips() { + assert_eq!( + DoipPayload::GenericNack(GenericNackCode::MessageTooLarge).payload_type(), + PayloadType::GenericNack, + ); + } +} diff --git a/src/doip/routing_activation.rs b/src/doip/routing_activation.rs index 1628b86..f8282ed 100644 --- a/src/doip/routing_activation.rs +++ b/src/doip/routing_activation.rs @@ -1,8 +1,23 @@ -//! Routing Activation handlers (ISO 13400-2) +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +//! Routing Activation handlers (ISO 13400-2:2019) use bytes::{Buf, BufMut, Bytes, BytesMut}; +use tracing::warn; +use crate::DoipError; +use super::{DoipParseable, DoipSerializable, too_short, check_min_len}; -// Response codes per ISO 13400-2 Table 25 +// Response codes per ISO 13400-2:2019 Table 25 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ResponseCode { @@ -18,29 +33,37 @@ pub enum ResponseCode { ConfirmationRequired = 0x11, } -impl ResponseCode { - pub fn from_u8(value: u8) -> Option { +impl TryFrom for ResponseCode { + type Error = DoipError; + + fn try_from(value: u8) -> std::result::Result { match value { - 0x00 => Some(Self::UnknownSourceAddress), - 0x01 => Some(Self::AllSocketsRegistered), - 0x02 => Some(Self::DifferentSourceAddress), - 0x03 => Some(Self::SourceAddressAlreadyActive), - 0x04 => Some(Self::MissingAuthentication), - 0x05 => Some(Self::RejectedConfirmation), - 0x06 => Some(Self::UnsupportedActivationType), - 0x07 => Some(Self::TlsRequired), - 0x10 => Some(Self::SuccessfullyActivated), - 0x11 => Some(Self::ConfirmationRequired), - _ => None, + 0x00 => Ok(Self::UnknownSourceAddress), + 0x01 => Ok(Self::AllSocketsRegistered), + 0x02 => Ok(Self::DifferentSourceAddress), + 0x03 => Ok(Self::SourceAddressAlreadyActive), + 0x04 => Ok(Self::MissingAuthentication), + 0x05 => Ok(Self::RejectedConfirmation), + 0x06 => Ok(Self::UnsupportedActivationType), + 0x07 => Ok(Self::TlsRequired), + 0x10 => Ok(Self::SuccessfullyActivated), + 0x11 => Ok(Self::ConfirmationRequired), + other => Err(DoipError::UnknownRoutingActivationResponseCode(other)), } } +} +impl ResponseCode { + #[must_use] pub fn is_success(self) -> bool { - matches!(self, Self::SuccessfullyActivated | Self::ConfirmationRequired) + matches!( + self, + Self::SuccessfullyActivated | Self::ConfirmationRequired + ) } } -// Activation types per ISO 13400-2 Table 24 +// Activation types per ISO 13400-2:2019 Table 24 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ActivationType { @@ -49,41 +72,24 @@ pub enum ActivationType { CentralSecurity = 0xE0, } -impl ActivationType { - pub fn from_u8(value: u8) -> Option { - match value { - 0x00 => Some(Self::Default), - 0x01 => Some(Self::WwhObd), - 0xE0 => Some(Self::CentralSecurity), - _ => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Error { - PayloadTooShort { expected: usize, actual: usize }, - UnknownResponseCode(u8), -} +impl TryFrom for ActivationType { + type Error = DoipError; -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::PayloadTooShort { expected, actual } => { - write!(f, "payload too short: need {} bytes, got {}", expected, actual) - } - Self::UnknownResponseCode(code) => write!(f, "unknown response code: 0x{:02X}", code), + fn try_from(value: u8) -> std::result::Result { + match value { + 0x00 => Ok(Self::Default), + 0x01 => Ok(Self::WwhObd), + 0xE0 => Ok(Self::CentralSecurity), + other => Err(DoipError::UnknownActivationType(other)), } } } -impl std::error::Error for Error {} - // Routing Activation Request - payload is 7 bytes min, 11 with OEM data #[derive(Debug, Clone, PartialEq, Eq)] pub struct Request { pub source_address: u16, - pub activation_type: u8, + pub activation_type: ActivationType, pub reserved: u32, pub oem_specific: Option, } @@ -92,20 +98,22 @@ impl Request { pub const MIN_LEN: usize = 7; pub const MAX_LEN: usize = 11; - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: payload.len(), - }); - } - - let source_address = u16::from_be_bytes([payload[0], payload[1]]); - let activation_type = payload[2]; - let reserved = u32::from_be_bytes([payload[3], payload[4], payload[5], payload[6]]); + /// Parse routing activation request from buffer + /// + /// # Errors + /// + /// Returns an error if the buffer is too short or contains invalid data. + pub fn parse_buf(buf: &mut Bytes) -> std::result::Result { + check_min_len(buf.as_ref(), Self::MIN_LEN)?; - let oem_specific = if payload.len() >= Self::MAX_LEN { - Some(u32::from_be_bytes([payload[7], payload[8], payload[9], payload[10]])) + let source_address = buf.get_u16(); + let activation_type = ActivationType::try_from(buf.get_u8()).map_err(|e| { + warn!("RoutingActivation Request parse_buf: {}", e); + e + })?; + let reserved = buf.get_u32(); + let oem_specific = if buf.remaining() >= 4 { + Some(buf.get_u32()) } else { None }; @@ -117,38 +125,6 @@ impl Request { oem_specific, }) } - - pub fn parse_buf(buf: &mut Bytes) -> Result { - if buf.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: buf.len(), - }); - } - - let source_address = buf.get_u16(); - let activation_type = buf.get_u8(); - let reserved = buf.get_u32(); - let oem_specific = if buf.remaining() >= 4 { Some(buf.get_u32()) } else { None }; - - Ok(Self { - source_address, - activation_type, - reserved, - oem_specific, - }) - } - - pub fn activation_type_enum(&self) -> Option { - ActivationType::from_u8(self.activation_type) - } - - pub fn validate(&self) -> Option { - if ActivationType::from_u8(self.activation_type).is_none() { - return Some(ResponseCode::UnsupportedActivationType); - } - None - } } // Routing Activation Response - 9 bytes min, 13 with OEM data @@ -165,6 +141,7 @@ impl Response { pub const MIN_LEN: usize = 9; pub const MAX_LEN: usize = 13; + #[must_use] pub fn success(tester_address: u16, entity_address: u16) -> Self { Self { tester_address, @@ -175,6 +152,7 @@ impl Response { } } + #[must_use] pub fn denial(tester_address: u16, entity_address: u16, code: ResponseCode) -> Self { Self { tester_address, @@ -185,42 +163,67 @@ impl Response { } } - pub fn to_bytes(&self) -> Bytes { - let len = if self.oem_specific.is_some() { Self::MAX_LEN } else { Self::MIN_LEN }; - let mut buf = BytesMut::with_capacity(len); - self.write_to(&mut buf); - buf.freeze() - } - - pub fn write_to(&self, buf: &mut BytesMut) { - buf.put_u16(self.tester_address); - buf.put_u16(self.entity_address); - buf.put_u8(self.response_code as u8); - buf.put_u32(self.reserved); - if let Some(oem) = self.oem_specific { - buf.put_u32(oem); - } + #[must_use] + pub fn is_success(&self) -> bool { + self.response_code.is_success() } +} - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: payload.len(), - }); - } +impl DoipParseable for Request { + fn parse(payload: &[u8]) -> std::result::Result { + let header: [u8; Self::MIN_LEN] = payload + .get(..Self::MIN_LEN) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = too_short(payload, Self::MIN_LEN); + warn!("RoutingActivation Request parse failed: {}", e); + e + })?; + + let source_address = u16::from_be_bytes([header[0], header[1]]); + let activation_type = ActivationType::try_from(header[2]).map_err(|e| { + warn!("RoutingActivation Request parse failed: {}", e); + e + })?; + let reserved = u32::from_be_bytes([header[3], header[4], header[5], header[6]]); + + let oem_specific = payload + .get(Self::MIN_LEN..Self::MAX_LEN) + .and_then(|s| <[u8; 4]>::try_from(s).ok()) + .map(u32::from_be_bytes); - let tester_address = u16::from_be_bytes([payload[0], payload[1]]); - let entity_address = u16::from_be_bytes([payload[2], payload[3]]); - let response_code = ResponseCode::from_u8(payload[4]) - .ok_or(Error::UnknownResponseCode(payload[4]))?; - let reserved = u32::from_be_bytes([payload[5], payload[6], payload[7], payload[8]]); + Ok(Self { + source_address, + activation_type, + reserved, + oem_specific, + }) + } +} - let oem_specific = if payload.len() >= Self::MAX_LEN { - Some(u32::from_be_bytes([payload[9], payload[10], payload[11], payload[12]])) - } else { - None - }; +impl DoipParseable for Response { + fn parse(payload: &[u8]) -> std::result::Result { + let header: [u8; Self::MIN_LEN] = payload + .get(..Self::MIN_LEN) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = too_short(payload, Self::MIN_LEN); + warn!("RoutingActivation Response parse failed: {}", e); + e + })?; + + let tester_address = u16::from_be_bytes([header[0], header[1]]); + let entity_address = u16::from_be_bytes([header[2], header[3]]); + let response_code = ResponseCode::try_from(header[4]).map_err(|e| { + warn!("RoutingActivation Response parse failed: {}", e); + e + })?; + let reserved = u32::from_be_bytes([header[5], header[6], header[7], header[8]]); + + let oem_specific = payload + .get(Self::MIN_LEN..Self::MAX_LEN) + .and_then(|s| <[u8; 4]>::try_from(s).ok()) + .map(u32::from_be_bytes); Ok(Self { tester_address, @@ -230,15 +233,36 @@ impl Response { oem_specific, }) } +} - pub fn is_success(&self) -> bool { - self.response_code.is_success() +impl DoipSerializable for Response { + fn serialized_len(&self) -> Option { + Some(Self::MIN_LEN + if self.oem_specific.is_some() { 4 } else { 0 }) + } + + fn write_to(&self, buf: &mut BytesMut) { + buf.put_u16(self.tester_address); + buf.put_u16(self.entity_address); + buf.put_u8(self.response_code as u8); + buf.put_u32(self.reserved); + if let Some(oem) = self.oem_specific { + buf.put_u32(oem); + } } } #[cfg(test)] mod tests { use super::*; + use crate::doip::{DoipParseable, DoipSerializable}; + + // Wire-format byte offsets for RoutingActivation Response + // Layout: TesterAddr(2) + EntityAddr(2) + ResponseCode(1) + Reserved(4) + OEM(4 optional) + const TESTER_ADDR_END: usize = 2; + const ENTITY_ADDR_END: usize = 4; + const RESP_CODE_IDX: usize = 4; + const OEM_DATA_START: usize = Response::MIN_LEN; // 9 + const OEM_DATA_END: usize = Response::MAX_LEN; // 13 #[test] fn response_code_success_check() { @@ -261,7 +285,7 @@ mod tests { let req = Request::parse(&payload).unwrap(); assert_eq!(req.source_address, 0x0E80); - assert_eq!(req.activation_type, 0x00); + assert_eq!(req.activation_type, ActivationType::Default); assert_eq!(req.reserved, 0); assert!(req.oem_specific.is_none()); } @@ -269,18 +293,17 @@ mod tests { #[test] fn parse_request_with_oem() { let payload = [ - 0x0E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xDE, 0xAD, 0xBE, 0xEF, + 0x0E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, ]; let req = Request::parse(&payload).unwrap(); - assert_eq!(req.oem_specific, Some(0xDEADBEEF)); + assert_eq!(req.oem_specific, Some(0xDEAD_BEEF)); } #[test] fn parse_wwh_obd_request() { let payload = [0x0F, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]; let req = Request::parse(&payload).unwrap(); - assert_eq!(req.activation_type_enum(), Some(ActivationType::WwhObd)); + assert_eq!(req.activation_type, ActivationType::WwhObd); } #[test] @@ -290,10 +313,10 @@ mod tests { } #[test] - fn validate_bad_activation_type() { + fn reject_unknown_activation_type() { + // 0x99 is not a valid ActivationType — parse must fail, not silently accept let payload = [0x0E, 0x80, 0x99, 0x00, 0x00, 0x00, 0x00]; - let req = Request::parse(&payload).unwrap(); - assert_eq!(req.validate(), Some(ResponseCode::UnsupportedActivationType)); + assert!(Request::parse(&payload).is_err()); } #[test] @@ -315,28 +338,25 @@ mod tests { let resp = Response::success(0x0E80, 0x1000); let bytes = resp.to_bytes(); - assert_eq!(bytes.len(), 9); - assert_eq!(&bytes[0..2], &[0x0E, 0x80]); - assert_eq!(&bytes[2..4], &[0x10, 0x00]); - assert_eq!(bytes[4], 0x10); + assert_eq!(bytes.len(), Response::MIN_LEN); + assert_eq!(&bytes[..TESTER_ADDR_END], &[0x0E, 0x80]); + assert_eq!(&bytes[TESTER_ADDR_END..ENTITY_ADDR_END], &[0x10, 0x00]); + assert_eq!(bytes[RESP_CODE_IDX], ResponseCode::SuccessfullyActivated as u8); } #[test] fn serialize_response_with_oem() { let mut resp = Response::success(0x0E80, 0x1000); - resp.oem_specific = Some(0x12345678); + resp.oem_specific = Some(0x1234_5678); let bytes = resp.to_bytes(); - assert_eq!(bytes.len(), 13); - assert_eq!(&bytes[9..13], &[0x12, 0x34, 0x56, 0x78]); + assert_eq!(bytes.len(), Response::MAX_LEN); + assert_eq!(&bytes[OEM_DATA_START..OEM_DATA_END], &[0x12, 0x34, 0x56, 0x78]); } #[test] fn parse_success_response() { - let payload = [ - 0x0E, 0x80, 0x10, 0x00, 0x10, - 0x00, 0x00, 0x00, 0x00, - ]; + let payload = [0x0E, 0x80, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00]; let resp = Response::parse(&payload).unwrap(); assert!(resp.is_success()); assert_eq!(resp.tester_address, 0x0E80); @@ -345,10 +365,7 @@ mod tests { #[test] fn parse_denial_response() { - let payload = [ - 0x0E, 0x80, 0x10, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, - ]; + let payload = [0x0E, 0x80, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]; let resp = Response::parse(&payload).unwrap(); assert!(!resp.is_success()); assert_eq!(resp.response_code, ResponseCode::AllSocketsRegistered); @@ -365,7 +382,7 @@ mod tests { #[test] fn roundtrip_response_with_oem() { let mut original = Response::denial(0x0F00, 0x2000, ResponseCode::MissingAuthentication); - original.oem_specific = Some(0xCAFEBABE); + original.oem_specific = Some(0xCAFE_BABE); let bytes = original.to_bytes(); let parsed = Response::parse(&bytes).unwrap(); assert_eq!(original, parsed); diff --git a/src/doip/vehicle_id.rs b/src/doip/vehicle_id.rs index 101e382..2c9f2ba 100644 --- a/src/doip/vehicle_id.rs +++ b/src/doip/vehicle_id.rs @@ -1,38 +1,45 @@ -//! Vehicle Identification handlers (ISO 13400-2) - -use bytes::{BufMut, Bytes, BytesMut}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Error { - PayloadTooShort { expected: usize, actual: usize }, - InvalidVinLength(usize), - InvalidEidLength(usize), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::PayloadTooShort { expected, actual } => { - write!(f, "payload too short: need {} bytes, got {}", expected, actual) - } - Self::InvalidVinLength(len) => write!(f, "VIN must be 17 bytes, got {}", len), - Self::InvalidEidLength(len) => write!(f, "EID must be 6 bytes, got {}", len), - } - } -} - -impl std::error::Error for Error {} +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Vehicle Identification handlers (ISO 13400-2:2019) + +use bytes::{BufMut, BytesMut}; +use tracing::warn; +use crate::DoipError; +use super::{DoipParseable, DoipSerializable, too_short, check_min_len}; + +// Wire-format field lengths for VehicleIdentificationResponse (ISO 13400-2:2019) +const VIN_LEN: usize = 17; +const LOGICAL_ADDR_LEN: usize = 2; +const EID_LEN: usize = 6; +const GID_LEN: usize = 6; +const FURTHER_ACTION_LEN: usize = 1; + +// Pre-computed byte offsets derived from field layout +const VIN_END: usize = VIN_LEN; // 17 +const ADDR_START: usize = VIN_END; // 17 +const ADDR_END: usize = ADDR_START + LOGICAL_ADDR_LEN; // 19 +const EID_START: usize = ADDR_END; // 19 +const EID_END: usize = EID_START + EID_LEN; // 25 +const GID_START: usize = EID_END; // 25 +const GID_END: usize = GID_START + GID_LEN; // 31 +const FURTHER_ACTION_IDX: usize = GID_END; // 31 +const SYNC_STATUS_IDX: usize = FURTHER_ACTION_IDX + FURTHER_ACTION_LEN; // 32 // Vehicle Identification Request (0x0001) - no payload #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Request; -impl Request { - pub fn parse(_payload: &[u8]) -> Result { - Ok(Self) - } -} - // Vehicle Identification Request with EID (0x0002) - 6 byte EID #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestWithEid { @@ -42,19 +49,7 @@ pub struct RequestWithEid { impl RequestWithEid { pub const LEN: usize = 6; - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::LEN { - return Err(Error::PayloadTooShort { - expected: Self::LEN, - actual: payload.len(), - }); - } - - let mut eid = [0u8; 6]; - eid.copy_from_slice(&payload[0..6]); - Ok(Self { eid }) - } - + #[must_use] pub fn new(eid: [u8; 6]) -> Self { Self { eid } } @@ -69,29 +64,18 @@ pub struct RequestWithVin { impl RequestWithVin { pub const LEN: usize = 17; - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::LEN { - return Err(Error::PayloadTooShort { - expected: Self::LEN, - actual: payload.len(), - }); - } - - let mut vin = [0u8; 17]; - vin.copy_from_slice(&payload[0..17]); - Ok(Self { vin }) - } - + #[must_use] pub fn new(vin: [u8; 17]) -> Self { Self { vin } } + #[must_use] pub fn vin_string(&self) -> String { String::from_utf8_lossy(&self.vin).to_string() } } -// Further action codes for vehicle identification response +// Further action codes per ISO 13400-2:2019 Table 23 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum FurtherAction { @@ -99,7 +83,18 @@ pub enum FurtherAction { RoutingActivationRequired = 0x10, } -// Sync status for vehicle identification response +impl TryFrom for FurtherAction { + type Error = u8; + fn try_from(value: u8) -> std::result::Result { + match value { + 0x00 => Ok(Self::NoFurtherAction), + 0x10 => Ok(Self::RoutingActivationRequired), + other => Err(other), + } + } +} + +// Synchronization status per ISO 13400-2:2019 Table 22 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum SyncStatus { @@ -107,6 +102,17 @@ pub enum SyncStatus { NotSynchronized = 0x10, } +impl TryFrom for SyncStatus { + type Error = u8; + fn try_from(value: u8) -> std::result::Result { + match value { + 0x00 => Ok(Self::Synchronized), + 0x10 => Ok(Self::NotSynchronized), + other => Err(other), + } + } +} + // Vehicle Identification Response (0x0004) // VIN(17) + LogicalAddr(2) + EID(6) + GID(6) + FurtherAction(1) = 32 bytes min #[derive(Debug, Clone, PartialEq, Eq)] @@ -120,9 +126,10 @@ pub struct Response { } impl Response { - pub const MIN_LEN: usize = 32; // without sync status - pub const MAX_LEN: usize = 33; // with sync status + pub const MIN_LEN: usize = SYNC_STATUS_IDX; // 32: VIN(17) + Addr(2) + EID(6) + GID(6) + FurtherAction(1) + pub const MAX_LEN: usize = SYNC_STATUS_IDX + 1; // 33: adds optional SyncStatus(1) + #[must_use] pub fn new(vin: [u8; 17], logical_address: u16, eid: [u8; 6], gid: [u8; 6]) -> Self { Self { vin, @@ -134,66 +141,101 @@ impl Response { } } + #[must_use] pub fn with_routing_required(mut self) -> Self { self.further_action = FurtherAction::RoutingActivationRequired; self } + #[must_use] pub fn with_sync_status(mut self, status: SyncStatus) -> Self { self.sync_status = Some(status); self } - pub fn to_bytes(&self) -> Bytes { - let len = if self.sync_status.is_some() { Self::MAX_LEN } else { Self::MIN_LEN }; - let mut buf = BytesMut::with_capacity(len); - self.write_to(&mut buf); - buf.freeze() + #[must_use] + pub fn vin_string(&self) -> String { + String::from_utf8_lossy(&self.vin).to_string() } +} - pub fn write_to(&self, buf: &mut BytesMut) { - buf.extend_from_slice(&self.vin); - buf.put_u16(self.logical_address); - buf.extend_from_slice(&self.eid); - buf.extend_from_slice(&self.gid); - buf.put_u8(self.further_action as u8); - if let Some(status) = self.sync_status { - buf.put_u8(status as u8); - } +impl DoipParseable for Request { + fn parse(_payload: &[u8]) -> std::result::Result { + Ok(Self) } +} - pub fn parse(payload: &[u8]) -> Result { - if payload.len() < Self::MIN_LEN { - return Err(Error::PayloadTooShort { - expected: Self::MIN_LEN, - actual: payload.len(), - }); - } - - let mut vin = [0u8; 17]; - vin.copy_from_slice(&payload[0..17]); +impl DoipParseable for RequestWithEid { + fn parse(payload: &[u8]) -> std::result::Result { + let eid: [u8; 6] = payload + .get(..Self::LEN) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = too_short(payload, Self::LEN); + warn!("VehicleId RequestWithEid parse failed: {}", e); + e + })?; - let logical_address = u16::from_be_bytes([payload[17], payload[18]]); + Ok(Self { eid }) + } +} - let mut eid = [0u8; 6]; - eid.copy_from_slice(&payload[19..25]); +impl DoipParseable for RequestWithVin { + fn parse(payload: &[u8]) -> std::result::Result { + let vin: [u8; 17] = payload + .get(..Self::LEN) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = too_short(payload, Self::LEN); + warn!("VehicleId RequestWithVin parse failed: {}", e); + e + })?; - let mut gid = [0u8; 6]; - gid.copy_from_slice(&payload[25..31]); + Ok(Self { vin }) + } +} - let further_action = match payload[31] { - 0x10 => FurtherAction::RoutingActivationRequired, - _ => FurtherAction::NoFurtherAction, +impl DoipParseable for Response { + fn parse(payload: &[u8]) -> std::result::Result { + if let Err(e) = check_min_len(payload, Self::MIN_LEN) { + warn!("VehicleId Response parse failed: {}", e); + return Err(e); }; - let sync_status = if payload.len() >= Self::MAX_LEN { - Some(match payload[32] { - 0x10 => SyncStatus::NotSynchronized, - _ => SyncStatus::Synchronized, - }) - } else { - None - }; + let vin: [u8; VIN_LEN] = + payload + .get(..VIN_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + + let addr_bytes: [u8; LOGICAL_ADDR_LEN] = + payload + .get(ADDR_START..ADDR_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let logical_address = u16::from_be_bytes(addr_bytes); + + let eid: [u8; EID_LEN] = + payload + .get(EID_START..EID_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + + let gid: [u8; GID_LEN] = + payload + .get(GID_START..GID_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + + let further_action_byte = payload + .get(FURTHER_ACTION_IDX) + .copied() + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let further_action = FurtherAction::try_from(further_action_byte) + .unwrap_or(FurtherAction::NoFurtherAction); + + let sync_status = payload.get(SYNC_STATUS_IDX) + .map(|&b| SyncStatus::try_from(b).unwrap_or(SyncStatus::Synchronized)); Ok(Self { vin, @@ -204,15 +246,29 @@ impl Response { sync_status, }) } +} + +impl DoipSerializable for Response { + fn serialized_len(&self) -> Option { + Some(Self::MIN_LEN + if self.sync_status.is_some() { 1 } else { 0 }) + } - pub fn vin_string(&self) -> String { - String::from_utf8_lossy(&self.vin).to_string() + fn write_to(&self, buf: &mut BytesMut) { + buf.extend_from_slice(&self.vin); + buf.put_u16(self.logical_address); + buf.extend_from_slice(&self.eid); + buf.extend_from_slice(&self.gid); + buf.put_u8(self.further_action as u8); + if let Some(status) = self.sync_status { + buf.put_u8(status as u8); + } } } #[cfg(test)] mod tests { use super::*; + use crate::doip::{DoipParseable, DoipSerializable}; #[test] fn parse_basic_request() { @@ -266,7 +322,10 @@ mod tests { let gid = [0; 6]; let resp = Response::new(vin, 0x1000, eid, gid).with_routing_required(); - assert_eq!(resp.further_action, FurtherAction::RoutingActivationRequired); + assert_eq!( + resp.further_action, + FurtherAction::RoutingActivationRequired + ); } #[test] @@ -278,9 +337,9 @@ mod tests { let resp = Response::new(vin, 0x1000, eid, gid); let bytes = resp.to_bytes(); - assert_eq!(bytes.len(), 32); - assert_eq!(&bytes[0..17], b"WVWZZZ3CZWE123456"); - assert_eq!(&bytes[17..19], &[0x10, 0x00]); // logical address + assert_eq!(bytes.len(), Response::MIN_LEN); + assert_eq!(&bytes[..VIN_LEN], b"WVWZZZ3CZWE123456"); + assert_eq!(&bytes[ADDR_START..ADDR_END], &[0x10, 0x00]); // logical address } #[test] @@ -289,12 +348,11 @@ mod tests { let eid = [0; 6]; let gid = [0; 6]; - let resp = Response::new(vin, 0x1000, eid, gid) - .with_sync_status(SyncStatus::Synchronized); + let resp = Response::new(vin, 0x1000, eid, gid).with_sync_status(SyncStatus::Synchronized); let bytes = resp.to_bytes(); - assert_eq!(bytes.len(), 33); - assert_eq!(bytes[32], 0x00); // sync status + assert_eq!(bytes.len(), Response::MAX_LEN); + assert_eq!(bytes[SYNC_STATUS_IDX], SyncStatus::Synchronized as u8); // sync status } #[test] diff --git a/src/error.rs b/src/error.rs index 58ec31f..4b30946 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,19 +1,36 @@ -//! Error Types for DoIP Server (ISO 13400-2 & ISO 14229) +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +//! Error Types for `DoIP` Server (ISO 13400-2:2019 & ISO 14229-1:2020) use std::io; use thiserror::Error; -/// Result type alias -pub type Result = std::result::Result; +// Re-export from the canonical definitions in the protocol modules +pub use crate::doip::diagnostic_message::NackCode as DiagnosticNackCode; +pub use crate::doip::header_parser::GenericNackCode; +pub use crate::doip::routing_activation::ResponseCode as RoutingActivationCode; -/// Main DoIP Error type +/// Result type alias for `DoIP` operations +pub type DoipResult = std::result::Result; + +/// Main `DoIP` Error type #[derive(Error, Debug)] pub enum DoipError { #[error("I/O error: {0}")] Io(#[from] io::Error), #[error("Configuration error: {0}")] - Config(String), + InvalidConfig(String), #[error("Invalid protocol version: expected 0x{expected:02X}, got 0x{actual:02X}")] InvalidProtocolVersion { expected: u8, actual: u8 }, @@ -24,6 +41,27 @@ pub enum DoipError { #[error("Unknown payload type: 0x{0:04X}")] UnknownPayloadType(u16), + #[error("payload too short: need {expected} bytes, got {actual}")] + PayloadTooShort { expected: usize, actual: usize }, + + #[error("unknown routing activation response code: {0:#04x}")] + UnknownRoutingActivationResponseCode(u8), + + #[error("unknown activation type: {0:#04x}")] + UnknownActivationType(u8), + + #[error("unknown diagnostic nack code: {0:#04x}")] + UnknownNackCode(u8), + + #[error("diagnostic message has no user data")] + EmptyUserData, + + #[error("invalid VIN length: expected 17, got {0}")] + InvalidVinLength(usize), + + #[error("invalid EID length: expected 6, got {0}")] + InvalidEidLength(usize), + #[error("Message too large: {size} bytes (max: {max})")] MessageTooLarge { size: usize, max: usize }, @@ -43,61 +81,7 @@ pub enum DoipError { UdsError { service: u8, nrc: u8 }, } -/// Generic Header NACK codes -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum GenericNackCode { - IncorrectPatternFormat = 0x00, - UnknownPayloadType = 0x01, - MessageTooLarge = 0x02, - OutOfMemory = 0x03, - InvalidPayloadLength = 0x04, -} - -impl GenericNackCode { - pub const fn as_u8(self) -> u8 { self as u8 } -} - -/// Routing Activation Response codes -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum RoutingActivationCode { - UnknownSourceAddress = 0x00, - AllSocketsRegistered = 0x01, - DifferentSourceAddress = 0x02, - SourceAddressAlreadyActive = 0x03, - MissingAuthentication = 0x04, - RejectedConfirmation = 0x05, - UnsupportedActivationType = 0x06, - SuccessfullyActivated = 0x10, - ConfirmationRequired = 0x11, -} - -impl RoutingActivationCode { - pub const fn as_u8(self) -> u8 { self as u8 } - pub const fn is_success(self) -> bool { - matches!(self, Self::SuccessfullyActivated | Self::ConfirmationRequired) - } -} - -/// Diagnostic Message NACK codes -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum DiagnosticNackCode { - InvalidSourceAddress = 0x02, - UnknownTargetAddress = 0x03, - DiagnosticMessageTooLarge = 0x04, - OutOfMemory = 0x05, - TargetUnreachable = 0x06, - UnknownNetwork = 0x07, - TransportProtocolError = 0x08, -} - -impl DiagnosticNackCode { - pub const fn as_u8(self) -> u8 { self as u8 } -} - -/// UDS Negative Response Codes +/// UDS Negative Response Codes (ISO 14229-1:2020) #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum UdsNrc { @@ -118,7 +102,10 @@ pub enum UdsNrc { } impl UdsNrc { - pub const fn as_u8(self) -> u8 { self as u8 } + #[must_use] + pub const fn as_u8(self) -> u8 { + self as u8 + } } #[cfg(test)] @@ -134,6 +121,112 @@ mod tests { #[test] fn test_nrc_values() { assert_eq!(UdsNrc::ServiceNotSupported.as_u8(), 0x11); - assert_eq!(DiagnosticNackCode::UnknownTargetAddress.as_u8(), 0x03); + assert_eq!(DiagnosticNackCode::UnknownTargetAddress as u8, 0x03); + } + + #[test] + fn test_generic_nack_code_values() { + let cases = [ + (GenericNackCode::IncorrectPatternFormat, 0x00), + (GenericNackCode::UnknownPayloadType, 0x01), + (GenericNackCode::MessageTooLarge, 0x02), + (GenericNackCode::OutOfMemory, 0x03), + (GenericNackCode::InvalidPayloadLength, 0x04), + ]; + for (code, expected) in cases { + assert_eq!(code as u8, expected); + } + } + + #[test] + fn test_routing_activation_code_values() { + let cases = [ + (RoutingActivationCode::UnknownSourceAddress, 0x00), + (RoutingActivationCode::AllSocketsRegistered, 0x01), + (RoutingActivationCode::DifferentSourceAddress, 0x02), + (RoutingActivationCode::SourceAddressAlreadyActive, 0x03), + (RoutingActivationCode::MissingAuthentication, 0x04), + (RoutingActivationCode::RejectedConfirmation, 0x05), + (RoutingActivationCode::UnsupportedActivationType, 0x06), + (RoutingActivationCode::SuccessfullyActivated, 0x10), + (RoutingActivationCode::ConfirmationRequired, 0x11), + ]; + for (code, expected) in cases { + assert_eq!(code as u8, expected); + } + } + + #[test] + fn test_diagnostic_nack_code_values() { + let cases = [ + (DiagnosticNackCode::InvalidSourceAddress, 0x02), + (DiagnosticNackCode::UnknownTargetAddress, 0x03), + (DiagnosticNackCode::DiagnosticMessageTooLarge, 0x04), + (DiagnosticNackCode::OutOfMemory, 0x05), + (DiagnosticNackCode::TargetUnreachable, 0x06), + (DiagnosticNackCode::UnknownNetwork, 0x07), + (DiagnosticNackCode::TransportProtocolError, 0x08), + ]; + for (code, expected) in cases { + assert_eq!(code as u8, expected); + } } -} \ No newline at end of file + + #[test] + fn test_uds_nrc_values() { + let cases = [ + (UdsNrc::GeneralReject, 0x10), + (UdsNrc::ServiceNotSupported, 0x11), + (UdsNrc::SubFunctionNotSupported, 0x12), + (UdsNrc::IncorrectMessageLength, 0x13), + (UdsNrc::BusyRepeatRequest, 0x21), + (UdsNrc::ConditionsNotCorrect, 0x22), + (UdsNrc::RequestSequenceError, 0x24), + (UdsNrc::RequestOutOfRange, 0x31), + (UdsNrc::SecurityAccessDenied, 0x33), + (UdsNrc::InvalidKey, 0x35), + (UdsNrc::ExceededNumberOfAttempts, 0x36), + (UdsNrc::RequiredTimeDelayNotExpired, 0x37), + (UdsNrc::ResponsePending, 0x78), + (UdsNrc::ServiceNotSupportedInActiveSession, 0x7F), + ]; + for (code, expected) in cases { + assert_eq!(code.as_u8(), expected); + } + } + + #[test] + fn test_doip_error_display_variants() { + let errors = [ + DoipError::Io(io::Error::other("io")), + DoipError::InvalidConfig("bad config".to_string()), + DoipError::InvalidProtocolVersion { + expected: 0x02, + actual: 0x00, + }, + DoipError::InvalidHeader("bad header".to_string()), + DoipError::UnknownPayloadType(0x1234), + DoipError::MessageTooLarge { size: 10, max: 1 }, + DoipError::RoutingActivationFailed { + code: 0x00, + message: "fail".to_string(), + }, + DoipError::SessionNotFound, + DoipError::SessionClosed, + DoipError::Timeout("timeout".to_string()), + DoipError::UdsError { + service: 0x10, + nrc: 0x11, + }, + DoipError::PayloadTooShort { expected: 8, actual: 4 }, + DoipError::UnknownRoutingActivationResponseCode(0x99), + DoipError::EmptyUserData, + DoipError::InvalidVinLength(10), + DoipError::InvalidEidLength(4), + ]; + + for err in errors { + let _ = err.to_string(); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 16e1767..181919a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,17 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ pub mod doip; +pub mod error; +pub mod uds; + +pub use error::DoipError; diff --git a/src/server/config.rs b/src/server/config.rs new file mode 100644 index 0000000..b5c15d4 --- /dev/null +++ b/src/server/config.rs @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! `DoIP` Server Configuration + +use serde::Deserialize; +use std::net::SocketAddr; +use std::path::Path; + +// ============================================================================ +// Default Configuration Constants (per ISO 13400-2 DoIP specification) +// ============================================================================ + +/// Default `DoIP` port for both TCP and UDP as defined in ISO 13400-2 +const DEFAULT_DOIP_PORT: u16 = 13400; + +/// Default bind address - listen on all network interfaces +const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0"; + +/// Default ECU logical address (`DoIP` entity address) +const DEFAULT_LOGICAL_ADDRESS: u16 = 0x0091; + +/// Default Vehicle Identification Number (17 ASCII characters per ISO 3779) +const DEFAULT_VIN: &[u8; 17] = b"TESTVIN1234567890"; + +/// Default Entity Identification (6 bytes, typically MAC address) +const DEFAULT_EID: [u8; 6] = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]; + +/// Default Group Identification (6 bytes) +const DEFAULT_GID: [u8; 6] = [0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54]; + +/// Maximum concurrent TCP connections allowed +const DEFAULT_MAX_CONNECTIONS: usize = 10; + +/// Initial inactivity timeout in milliseconds (`T_TCP_Initial` per ISO 13400-2: 2 seconds) +const DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS: u64 = 2_000; + +/// General inactivity timeout in milliseconds (`T_TCP_General` per ISO 13400-2: 5 minutes) +const DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS: u64 = 300_000; + +#[derive(Debug, Clone)] +pub struct ServerConfig { + pub tcp_addr: SocketAddr, + pub udp_addr: SocketAddr, + pub logical_address: u16, + pub vin: [u8; 17], + pub eid: [u8; 6], + pub gid: [u8; 6], + pub max_connections: usize, + pub initial_inactivity_timeout_ms: u64, + pub general_inactivity_timeout_ms: u64, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + tcp_addr: SocketAddr::from(([0, 0, 0, 0], DEFAULT_DOIP_PORT)), + udp_addr: SocketAddr::from(([0, 0, 0, 0], DEFAULT_DOIP_PORT)), + logical_address: DEFAULT_LOGICAL_ADDRESS, + vin: *DEFAULT_VIN, + eid: DEFAULT_EID, + gid: DEFAULT_GID, + max_connections: DEFAULT_MAX_CONNECTIONS, + initial_inactivity_timeout_ms: DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS, + general_inactivity_timeout_ms: DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS, + } + } +} + +fn default_doip_port() -> u16 { + DEFAULT_DOIP_PORT +} +fn default_bind_address() -> String { + DEFAULT_BIND_ADDRESS.to_string() +} +fn default_max_connections() -> usize { + DEFAULT_MAX_CONNECTIONS +} +fn default_logical_address() -> u16 { + DEFAULT_LOGICAL_ADDRESS +} +fn default_initial_inactivity_ms() -> u64 { + DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS +} +fn default_general_inactivity_ms() -> u64 { + DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS +} + +#[derive(Debug, Deserialize, Default)] +struct ConfigFile { + #[serde(default)] + server: ServerSection, + #[serde(default)] + vehicle: VehicleSection, + #[serde(default)] + timeouts: TimeoutSection, +} + +#[derive(Debug, Deserialize)] +struct ServerSection { + #[serde(default = "default_doip_port")] + tcp_port: u16, + #[serde(default = "default_doip_port")] + udp_port: u16, + #[serde(default = "default_bind_address")] + bind_address: String, + #[serde(default = "default_max_connections")] + max_connections: usize, +} + +impl Default for ServerSection { + fn default() -> Self { + Self { + tcp_port: DEFAULT_DOIP_PORT, + udp_port: DEFAULT_DOIP_PORT, + bind_address: DEFAULT_BIND_ADDRESS.to_string(), + max_connections: DEFAULT_MAX_CONNECTIONS, + } + } +} + +#[derive(Debug, Deserialize)] +struct VehicleSection { + #[serde(default = "default_logical_address")] + logical_address: u16, + vin: Option, + eid: Option, + gid: Option, +} + +impl Default for VehicleSection { + fn default() -> Self { + Self { + logical_address: DEFAULT_LOGICAL_ADDRESS, + vin: None, + eid: None, + gid: None, + } + } +} + +#[derive(Debug, Deserialize)] +struct TimeoutSection { + #[serde(default = "default_initial_inactivity_ms")] + initial_inactivity_ms: u64, + #[serde(default = "default_general_inactivity_ms")] + general_inactivity_ms: u64, +} + +impl Default for TimeoutSection { + fn default() -> Self { + Self { + initial_inactivity_ms: DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS, + general_inactivity_ms: DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS, + } + } +} + +impl ServerConfig { + #[must_use] + pub fn new(logical_address: u16) -> Self { + Self { + logical_address, + ..Default::default() + } + } + + /// Load configuration from TOML file + /// + /// # Errors + /// Returns error if file cannot be read, parsed, or contains invalid values + pub fn from_file>(path: P) -> anyhow::Result { + let content = std::fs::read_to_string(path)?; + let file: ConfigFile = toml::from_str(&content)?; + + let bind = &file.server.bind_address; + Ok(Self { + tcp_addr: format!("{bind}:{}", file.server.tcp_port).parse()?, + udp_addr: format!("{bind}:{}", file.server.udp_port).parse()?, + max_connections: file.server.max_connections, + logical_address: file.vehicle.logical_address, + vin: file.vehicle.vin.as_deref().map(Self::parse_vin).transpose()?.unwrap_or(*DEFAULT_VIN), + eid: file.vehicle.eid.as_deref().map(Self::parse_hex_array).transpose()?.unwrap_or(DEFAULT_EID), + gid: file.vehicle.gid.as_deref().map(Self::parse_hex_array).transpose()?.unwrap_or(DEFAULT_GID), + initial_inactivity_timeout_ms: file.timeouts.initial_inactivity_ms, + general_inactivity_timeout_ms: file.timeouts.general_inactivity_ms, + }) + } + + fn parse_vin(s: &str) -> anyhow::Result<[u8; 17]> { + let bytes = s.as_bytes(); + if bytes.len() != 17 { + anyhow::bail!("VIN must be exactly 17 characters"); + } + let mut vin = [0u8; 17]; + vin.copy_from_slice(bytes); + Ok(vin) + } + + fn parse_hex_array(s: &str) -> anyhow::Result<[u8; N]> { + let s = s.trim_start_matches("0x").replace([':', '-', ' '], ""); + let bytes = hex::decode(&s)?; + if bytes.len() != N { + anyhow::bail!("Expected {} bytes, got {}", N, bytes.len()); + } + let mut arr = [0u8; N]; + arr.copy_from_slice(&bytes); + Ok(arr) + } + + #[must_use] + pub fn with_vin(mut self, vin: [u8; 17]) -> Self { + self.vin = vin; + self + } + + #[must_use] + pub fn with_addresses(mut self, tcp: SocketAddr, udp: SocketAddr) -> Self { + self.tcp_addr = tcp; + self.udp_addr = udp; + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config() { + let config = ServerConfig::default(); + + assert_eq!(config.tcp_addr.port(), DEFAULT_DOIP_PORT); + assert_eq!(config.udp_addr.port(), DEFAULT_DOIP_PORT); + assert_eq!(config.logical_address, DEFAULT_LOGICAL_ADDRESS); + assert_eq!(config.vin, *DEFAULT_VIN); + assert_eq!(config.eid, DEFAULT_EID); + assert_eq!(config.gid, DEFAULT_GID); + assert_eq!(config.max_connections, DEFAULT_MAX_CONNECTIONS); + assert_eq!( + config.initial_inactivity_timeout_ms, + DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS + ); + assert_eq!( + config.general_inactivity_timeout_ms, + DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS + ); + } + + #[test] + fn test_new_with_logical_address() { + let config = ServerConfig::new(0x1234); + + assert_eq!(config.logical_address, 0x1234); + assert_eq!(config.tcp_addr.port(), DEFAULT_DOIP_PORT); + } + + #[test] + fn test_parse_vin_valid() { + let result = ServerConfig::parse_vin("WVWZZZ3CZWE123456"); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 17); + } + + #[test] + fn test_parse_vin_invalid_length() { + let result = ServerConfig::parse_vin("SHORTVIN"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_hex_array_valid() { + let result: anyhow::Result<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]); + } + + #[test] + fn test_parse_hex_array_with_0x_prefix() { + let result: anyhow::Result<[u8; 6]> = ServerConfig::parse_hex_array("0x001A2B3C4D5E"); + assert!(result.is_ok()); + } + + #[test] + fn test_parse_hex_array_invalid_length() { + let result: anyhow::Result<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B"); + assert!(result.is_err()); + } + + #[test] + fn test_with_vin_builder() { + let new_vin = *b"NEWVIN12345678901"; + let config = ServerConfig::default().with_vin(new_vin); + + assert_eq!(config.vin, new_vin); + } + + #[test] + fn test_with_addresses_builder() { + let tcp: SocketAddr = "192.168.1.1:13400".parse().unwrap(); + let udp: SocketAddr = "192.168.1.1:13401".parse().unwrap(); + let config = ServerConfig::default().with_addresses(tcp, udp); + + assert_eq!(config.tcp_addr, tcp); + assert_eq!(config.udp_addr, udp); + } +} diff --git a/src/server/session.rs b/src/server/session.rs new file mode 100644 index 0000000..41a0ac1 --- /dev/null +++ b/src/server/session.rs @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +//! Session management for `DoIP` connections + +use parking_lot::RwLock; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use tracing::debug; + +/// Session states per ISO 13400-2:2019 connection lifecycle +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SessionState { + Connected, + RoutingActive, + Closed, +} + +#[derive(Debug, Clone)] +pub struct Session { + pub id: u64, + pub peer_addr: SocketAddr, + pub tester_address: u16, + pub state: SessionState, +} + +impl Session { + #[must_use] + pub fn new(id: u64, peer_addr: SocketAddr) -> Self { + Self { + id, + peer_addr, + tester_address: 0, + state: SessionState::Connected, + } + } + + pub fn activate_routing(&mut self, tester_address: u16) { + debug!("Session {} routing activated: tester_address=0x{:04X}", self.id, tester_address); + self.tester_address = tester_address; + self.state = SessionState::RoutingActive; + } + + #[must_use] + pub fn is_routing_active(&self) -> bool { + self.state == SessionState::RoutingActive + } +} + +#[derive(Debug, Default)] +pub struct SessionManager { + sessions: RwLock>, + addr_to_session: RwLock>, + next_id: RwLock, +} + +impl SessionManager { + #[must_use] + pub fn new() -> Arc { + Arc::new(Self::default()) + } + + pub fn create_session(&self, peer_addr: SocketAddr) -> Session { + let mut next_id = self.next_id.write(); + let id = *next_id; + *next_id = next_id.saturating_add(1); + + let session = Session::new(id, peer_addr); + self.sessions.write().insert(id, session.clone()); + self.addr_to_session.write().insert(peer_addr, id); + + debug!("Session {} created for {}", id, peer_addr); + session + } + + pub fn get_session(&self, id: u64) -> Option { + self.sessions.read().get(&id).cloned() + } + + pub fn get_session_by_addr(&self, addr: &SocketAddr) -> Option { + let id = self.addr_to_session.read().get(addr).copied()?; + self.get_session(id) + } + + pub fn update_session(&self, id: u64, f: F) -> bool + where + F: FnOnce(&mut Session), + { + if let Some(session) = self.sessions.write().get_mut(&id) { + f(session); + true + } else { + false + } + } + + pub fn remove_session(&self, id: u64) -> Option { + let session = self.sessions.write().remove(&id)?; + self.addr_to_session.write().remove(&session.peer_addr); + debug!("Session {} removed (peer: {})", id, session.peer_addr); + Some(session) + } + + pub fn remove_session_by_addr(&self, addr: &SocketAddr) -> Option { + let id = self.addr_to_session.write().remove(addr)?; + let session = self.sessions.write().remove(&id)?; + debug!("Session {} removed by addr (peer: {})", id, addr); + Some(session) + } + + pub fn session_count(&self) -> usize { + self.sessions.read().len() + } + + pub fn is_tester_registered(&self, tester_address: u16) -> bool { + self.sessions + .read() + .values() + .any(|s| s.tester_address == tester_address && s.state == SessionState::RoutingActive) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_and_get_session() { + let mgr = SessionManager::new(); + let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); + + let session = mgr.create_session(addr); + assert_eq!(session.state, SessionState::Connected); + + let retrieved = mgr.get_session(session.id).unwrap(); + assert_eq!(retrieved.peer_addr, addr); + } + + #[test] + fn activate_routing() { + let mgr = SessionManager::new(); + let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); + + let session = mgr.create_session(addr); + mgr.update_session(session.id, |s| s.activate_routing(0x0E80)); + + let updated = mgr.get_session(session.id).unwrap(); + assert!(updated.is_routing_active()); + assert_eq!(updated.tester_address, 0x0E80); + } + + #[test] + fn remove_session() { + let mgr = SessionManager::new(); + let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); + + let session = mgr.create_session(addr); + assert_eq!(mgr.session_count(), 1); + + mgr.remove_session(session.id); + assert_eq!(mgr.session_count(), 0); + assert!(mgr.get_session(session.id).is_none()); + } + + #[test] + fn check_tester_registered() { + let mgr = SessionManager::new(); + let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); + + let session = mgr.create_session(addr); + assert!(!mgr.is_tester_registered(0x0E80)); + + mgr.update_session(session.id, |s| s.activate_routing(0x0E80)); + assert!(mgr.is_tester_registered(0x0E80)); + } +} diff --git a/src/uds/handler.rs b/src/uds/handler.rs index 3d4f50f..928f034 100644 --- a/src/uds/handler.rs +++ b/src/uds/handler.rs @@ -1,121 +1,99 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + //! UDS Handler Trait //! -//! Defines the interface between DoIP transport and UDS processing. -//! The DoIP server extracts UDS bytes from DoIP frames and delegates +//! Defines the interface between `DoIP` transport and UDS processing. +//! The `DoIP` server extracts UDS bytes from `DoIP` frames and delegates //! to the handler. The handler returns UDS response bytes. use bytes::Bytes; -/// UDS request extracted from DoIP diagnostic message +/// UDS Service IDs (ISO 14229-1:2020) +pub mod service_id { + pub const DIAGNOSTIC_SESSION_CONTROL: u8 = 0x10; + pub const ECU_RESET: u8 = 0x11; + pub const SECURITY_ACCESS: u8 = 0x27; + pub const COMMUNICATION_CONTROL: u8 = 0x28; + pub const TESTER_PRESENT: u8 = 0x3E; + pub const CONTROL_DTC_SETTING: u8 = 0x85; + pub const READ_DATA_BY_IDENTIFIER: u8 = 0x22; + pub const WRITE_DATA_BY_IDENTIFIER: u8 = 0x2E; + pub const ROUTINE_CONTROL: u8 = 0x31; + pub const REQUEST_DOWNLOAD: u8 = 0x34; + pub const REQUEST_UPLOAD: u8 = 0x35; + pub const TRANSFER_DATA: u8 = 0x36; + pub const REQUEST_TRANSFER_EXIT: u8 = 0x37; + pub const READ_DTC_INFORMATION: u8 = 0x19; + pub const CLEAR_DTC_INFORMATION: u8 = 0x14; +} + +/// UDS request extracted from `DoIP` diagnostic message #[derive(Debug, Clone)] pub struct UdsRequest { pub source_address: u16, pub target_address: u16, - pub data: Bytes, + pub payload: Bytes, } impl UdsRequest { - pub fn new(source: u16, target: u16, data: Bytes) -> Self { + #[must_use] + pub fn new(source: u16, target: u16, payload: Bytes) -> Self { Self { source_address: source, target_address: target, - data, + payload, } } + /// Returns the UDS Service ID, which is the first byte of the UDS payload + /// per ISO 14229-1:2020 (UDS). None is returned for empty payloads. pub fn service_id(&self) -> Option { - self.data.first().copied() + self.payload.first().copied() } } -/// UDS response to be wrapped in DoIP diagnostic message +/// UDS response to be wrapped in `DoIP` diagnostic message #[derive(Debug, Clone)] pub struct UdsResponse { pub source_address: u16, pub target_address: u16, - pub data: Bytes, + pub payload: Bytes, } impl UdsResponse { - pub fn new(source: u16, target: u16, data: Bytes) -> Self { + #[must_use] + pub fn new(source: u16, target: u16, payload: Bytes) -> Self { Self { source_address: source, target_address: target, - data, + payload, } } } /// Trait for handling UDS requests /// -/// Implement this trait to connect the DoIP server to a UDS backend +/// Implement this trait to connect the `DoIP` server to a UDS backend /// (e.g., UDS2SOVD converter, ODX/MDD handler, or ECU simulator) pub trait UdsHandler: Send + Sync { fn handle(&self, request: UdsRequest) -> UdsResponse; } -/// Stub handler that returns NRC 0x11 (Service Not Supported) for all requests -#[derive(Debug, Default, Clone)] -pub struct StubHandler; - -impl UdsHandler for StubHandler { - fn handle(&self, request: UdsRequest) -> UdsResponse { - let sid = request.service_id().unwrap_or(0); - // Negative response: 0x7F + SID + NRC - let nrc_service_not_supported = 0x11; - let data = Bytes::from(vec![0x7F, sid, nrc_service_not_supported]); - - UdsResponse::new( - request.target_address, - request.source_address, - data, - ) - } -} - #[cfg(test)] mod tests { use super::*; - - #[test] - fn stub_handler_returns_nrc() { - let handler = StubHandler; - let request = UdsRequest::new( - 0x0E00, - 0x1000, - Bytes::from(vec![0x22, 0xF1, 0x90]), // ReadDataByIdentifier - ); - - let response = handler.handle(request); - - assert_eq!(response.source_address, 0x1000); - assert_eq!(response.target_address, 0x0E00); - assert_eq!(response.data.as_ref(), &[0x7F, 0x22, 0x11]); - } - - #[test] - fn stub_handler_handles_empty_request() { - let handler = StubHandler; - let request = UdsRequest::new(0x0E00, 0x1000, Bytes::new()); - - let response = handler.handle(request); - - assert_eq!(response.data.as_ref(), &[0x7F, 0x00, 0x11]); - } - - #[test] - fn stub_handler_tester_present() { - let handler = StubHandler; - let request = UdsRequest::new( - 0x0E00, - 0x1000, - Bytes::from(vec![0x3E, 0x00]), // TesterPresent - ); - - let response = handler.handle(request); - - assert_eq!(response.data.as_ref(), &[0x7F, 0x3E, 0x11]); - } + use bytes::Bytes; #[test] fn uds_request_service_id() { @@ -125,4 +103,14 @@ mod tests { let empty = UdsRequest::new(0x0E00, 0x1000, Bytes::new()); assert_eq!(empty.service_id(), None); } + + #[test] + fn uds_response_new_sets_payload() { + let payload = Bytes::from(vec![0x62, 0x01]); + let response = UdsResponse::new(0x1000, 0x0E00, payload.clone()); + + assert_eq!(response.source_address, 0x1000); + assert_eq!(response.target_address, 0x0E00); + assert_eq!(response.payload, payload); + } } diff --git a/src/uds/mod.rs b/src/uds/mod.rs index be783e7..8209c1c 100644 --- a/src/uds/mod.rs +++ b/src/uds/mod.rs @@ -1,7 +1,24 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + //! UDS Module //! -//! Provides the interface between DoIP transport and UDS processing. +//! Provides the interface between `DoIP` transport and UDS processing. +// #[cfg(any(test, feature = "test-handlers"))] +// pub mod dummy_handler; pub mod handler; +// #[cfg(any(test, feature = "test-handlers"))] +// pub mod stub_handler; -pub use handler::{UdsHandler, UdsRequest, UdsResponse, StubHandler}; +pub use handler::{service_id, UdsHandler, UdsRequest, UdsResponse}; From 05c030b07f5e25fb9f01cffebc3cb9715088e00a Mon Sep 17 00:00:00 2001 From: vinayrs Date: Tue, 3 Mar 2026 11:27:00 +0530 Subject: [PATCH 06/10] style: apply cargo fmt --- src/doip/alive_check.rs | 9 +++-- src/doip/diagnostic_message.rs | 4 +- src/doip/header_parser.rs | 9 ++++- src/doip/mod.rs | 14 ++++--- src/doip/payload.rs | 16 ++++---- src/doip/routing_activation.rs | 18 ++++++--- src/doip/vehicle_id.rs | 69 ++++++++++++++++------------------ src/error.rs | 5 ++- 8 files changed, 80 insertions(+), 64 deletions(-) diff --git a/src/doip/alive_check.rs b/src/doip/alive_check.rs index 358834a..0ffb422 100644 --- a/src/doip/alive_check.rs +++ b/src/doip/alive_check.rs @@ -12,10 +12,10 @@ */ //! Alive Check handlers (ISO 13400-2) +use super::{DoipParseable, DoipSerializable}; +use crate::DoipError; use bytes::{BufMut, BytesMut}; use tracing::warn; -use crate::DoipError; -use super::{DoipParseable, DoipSerializable}; // Alive Check Request (0x0007) - no payload // Server sends this to check if tester is still connected @@ -49,7 +49,10 @@ impl DoipParseable for Response { .get(..Self::LEN) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { - let e = DoipError::PayloadTooShort { expected: Self::LEN, actual: payload.len() }; + let e = DoipError::PayloadTooShort { + expected: Self::LEN, + actual: payload.len(), + }; warn!("AliveCheck Response parse failed: {}", e); e })?; diff --git a/src/doip/diagnostic_message.rs b/src/doip/diagnostic_message.rs index 2ed0988..b345c3a 100644 --- a/src/doip/diagnostic_message.rs +++ b/src/doip/diagnostic_message.rs @@ -12,10 +12,10 @@ */ //! Diagnostic Message handlers (ISO 13400-2:2019) +use super::{check_min_len, too_short, DoipParseable, DoipSerializable}; +use crate::DoipError; use bytes::{Buf, BufMut, Bytes, BytesMut}; use tracing::warn; -use crate::DoipError; -use super::{DoipParseable, DoipSerializable, too_short, check_min_len}; const ADDRESS_BYTES: usize = 2; const HEADER_BYTES: usize = ADDRESS_BYTES * 2; diff --git a/src/doip/header_parser.rs b/src/doip/header_parser.rs index 1da76b2..c00f0d7 100644 --- a/src/doip/header_parser.rs +++ b/src/doip/header_parser.rs @@ -397,7 +397,11 @@ impl DoipMessage { // version in every response. No production callers exist in this branch yet — // server handlers live on feature/doip-async-server and have not been ported here. #[allow(dead_code)] - pub(crate) fn with_version(version: u8, payload_type: PayloadType, payload: Bytes) -> Result { + pub(crate) fn with_version( + version: u8, + payload_type: PayloadType, + payload: Bytes, + ) -> Result { Ok(Self { header: DoipHeader { version, @@ -770,7 +774,8 @@ mod tests { #[test] fn create_vehicle_id_broadcast() { // Empty payload for discovery - let msg = DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()).unwrap(); + let msg = + DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()).unwrap(); assert_eq!(msg.header.payload_length, 0); assert_eq!(msg.message_length(), 8); // Just the header diff --git a/src/doip/mod.rs b/src/doip/mod.rs index 950fd16..42d3aa9 100644 --- a/src/doip/mod.rs +++ b/src/doip/mod.rs @@ -20,8 +20,8 @@ pub mod payload; pub mod routing_activation; pub mod vehicle_id; -use bytes::{Bytes, BytesMut}; use crate::DoipError; +use bytes::{Bytes, BytesMut}; /// Trait for DoIP message types that can be parsed from a raw payload slice. /// @@ -67,7 +67,10 @@ pub trait DoipSerializable { /// Build a [`DoipError::PayloadTooShort`] from the given slice and expected length. pub(crate) fn too_short(payload: &[u8], expected: usize) -> DoipError { - DoipError::PayloadTooShort { expected, actual: payload.len() } + DoipError::PayloadTooShort { + expected, + actual: payload.len(), + } } /// Return `Err` if `payload` is shorter than `expected` bytes. @@ -82,9 +85,8 @@ pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> std::result::Res // Re-export core types and constants for convenient access. // Constants are exported to allow external testing and custom DoIP message construction. pub use header_parser::{ - DoipCodec, DoipHeader, DoipMessage, GenericNackCode, PayloadType, - DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, - MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, - DOIP_VERSION_DEFAULT, DOIP_HEADER_VERSION_MASK, + DoipCodec, DoipHeader, DoipMessage, GenericNackCode, PayloadType, DEFAULT_PROTOCOL_VERSION, + DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, DOIP_HEADER_VERSION_MASK, + DOIP_VERSION_DEFAULT, MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, }; pub use payload::DoipPayload; diff --git a/src/doip/payload.rs b/src/doip/payload.rs index cc27854..1ae90d0 100644 --- a/src/doip/payload.rs +++ b/src/doip/payload.rs @@ -27,11 +27,11 @@ //! } //! ``` -use crate::DoipError; use super::{ - DoipMessage, DoipParseable, GenericNackCode, PayloadType, - alive_check, diagnostic_message, routing_activation, vehicle_id, + alive_check, diagnostic_message, routing_activation, vehicle_id, DoipMessage, DoipParseable, + GenericNackCode, PayloadType, }; +use crate::DoipError; /// A fully-parsed DoIP message payload. /// @@ -121,10 +121,10 @@ impl DoipPayload { vehicle_id::Response::parse(payload)?, )), PayloadType::GenericNack => { - let byte = payload - .first() - .copied() - .ok_or(DoipError::PayloadTooShort { expected: 1, actual: 0 })?; + let byte = payload.first().copied().ok_or(DoipError::PayloadTooShort { + expected: 1, + actual: 0, + })?; let code = GenericNackCode::try_from(byte) .map_err(|b| DoipError::UnknownPayloadType(u16::from(b)))?; Ok(Self::GenericNack(code)) @@ -165,8 +165,8 @@ impl DoipPayload { #[cfg(test)] mod tests { use super::*; - use bytes::Bytes; use crate::doip::{DoipMessage, PayloadType}; + use bytes::Bytes; fn make_msg(payload_type: PayloadType, payload: impl Into) -> DoipMessage { DoipMessage { diff --git a/src/doip/routing_activation.rs b/src/doip/routing_activation.rs index f8282ed..840b660 100644 --- a/src/doip/routing_activation.rs +++ b/src/doip/routing_activation.rs @@ -12,10 +12,10 @@ */ //! Routing Activation handlers (ISO 13400-2:2019) +use super::{check_min_len, too_short, DoipParseable, DoipSerializable}; +use crate::DoipError; use bytes::{Buf, BufMut, Bytes, BytesMut}; use tracing::warn; -use crate::DoipError; -use super::{DoipParseable, DoipSerializable, too_short, check_min_len}; // Response codes per ISO 13400-2:2019 Table 25 #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -261,8 +261,8 @@ mod tests { const TESTER_ADDR_END: usize = 2; const ENTITY_ADDR_END: usize = 4; const RESP_CODE_IDX: usize = 4; - const OEM_DATA_START: usize = Response::MIN_LEN; // 9 - const OEM_DATA_END: usize = Response::MAX_LEN; // 13 + const OEM_DATA_START: usize = Response::MIN_LEN; // 9 + const OEM_DATA_END: usize = Response::MAX_LEN; // 13 #[test] fn response_code_success_check() { @@ -341,7 +341,10 @@ mod tests { assert_eq!(bytes.len(), Response::MIN_LEN); assert_eq!(&bytes[..TESTER_ADDR_END], &[0x0E, 0x80]); assert_eq!(&bytes[TESTER_ADDR_END..ENTITY_ADDR_END], &[0x10, 0x00]); - assert_eq!(bytes[RESP_CODE_IDX], ResponseCode::SuccessfullyActivated as u8); + assert_eq!( + bytes[RESP_CODE_IDX], + ResponseCode::SuccessfullyActivated as u8 + ); } #[test] @@ -351,7 +354,10 @@ mod tests { let bytes = resp.to_bytes(); assert_eq!(bytes.len(), Response::MAX_LEN); - assert_eq!(&bytes[OEM_DATA_START..OEM_DATA_END], &[0x12, 0x34, 0x56, 0x78]); + assert_eq!( + &bytes[OEM_DATA_START..OEM_DATA_END], + &[0x12, 0x34, 0x56, 0x78] + ); } #[test] diff --git a/src/doip/vehicle_id.rs b/src/doip/vehicle_id.rs index 2c9f2ba..98cbb4c 100644 --- a/src/doip/vehicle_id.rs +++ b/src/doip/vehicle_id.rs @@ -13,10 +13,10 @@ //! Vehicle Identification handlers (ISO 13400-2:2019) +use super::{check_min_len, too_short, DoipParseable, DoipSerializable}; +use crate::DoipError; use bytes::{BufMut, BytesMut}; use tracing::warn; -use crate::DoipError; -use super::{DoipParseable, DoipSerializable, too_short, check_min_len}; // Wire-format field lengths for VehicleIdentificationResponse (ISO 13400-2:2019) const VIN_LEN: usize = 17; @@ -26,14 +26,14 @@ const GID_LEN: usize = 6; const FURTHER_ACTION_LEN: usize = 1; // Pre-computed byte offsets derived from field layout -const VIN_END: usize = VIN_LEN; // 17 -const ADDR_START: usize = VIN_END; // 17 -const ADDR_END: usize = ADDR_START + LOGICAL_ADDR_LEN; // 19 -const EID_START: usize = ADDR_END; // 19 -const EID_END: usize = EID_START + EID_LEN; // 25 -const GID_START: usize = EID_END; // 25 -const GID_END: usize = GID_START + GID_LEN; // 31 -const FURTHER_ACTION_IDX: usize = GID_END; // 31 +const VIN_END: usize = VIN_LEN; // 17 +const ADDR_START: usize = VIN_END; // 17 +const ADDR_END: usize = ADDR_START + LOGICAL_ADDR_LEN; // 19 +const EID_START: usize = ADDR_END; // 19 +const EID_END: usize = EID_START + EID_LEN; // 25 +const GID_START: usize = EID_END; // 25 +const GID_END: usize = GID_START + GID_LEN; // 31 +const FURTHER_ACTION_IDX: usize = GID_END; // 31 const SYNC_STATUS_IDX: usize = FURTHER_ACTION_IDX + FURTHER_ACTION_LEN; // 32 // Vehicle Identification Request (0x0001) - no payload @@ -126,8 +126,8 @@ pub struct Response { } impl Response { - pub const MIN_LEN: usize = SYNC_STATUS_IDX; // 32: VIN(17) + Addr(2) + EID(6) + GID(6) + FurtherAction(1) - pub const MAX_LEN: usize = SYNC_STATUS_IDX + 1; // 33: adds optional SyncStatus(1) + pub const MIN_LEN: usize = SYNC_STATUS_IDX; // 32: VIN(17) + Addr(2) + EID(6) + GID(6) + FurtherAction(1) + pub const MAX_LEN: usize = SYNC_STATUS_IDX + 1; // 33: adds optional SyncStatus(1) #[must_use] pub fn new(vin: [u8; 17], logical_address: u16, eid: [u8; 6], gid: [u8; 6]) -> Self { @@ -202,39 +202,36 @@ impl DoipParseable for Response { return Err(e); }; - let vin: [u8; VIN_LEN] = - payload - .get(..VIN_END) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; - - let addr_bytes: [u8; LOGICAL_ADDR_LEN] = - payload - .get(ADDR_START..ADDR_END) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let vin: [u8; VIN_LEN] = payload + .get(..VIN_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + + let addr_bytes: [u8; LOGICAL_ADDR_LEN] = payload + .get(ADDR_START..ADDR_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; let logical_address = u16::from_be_bytes(addr_bytes); - let eid: [u8; EID_LEN] = - payload - .get(EID_START..EID_END) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let eid: [u8; EID_LEN] = payload + .get(EID_START..EID_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; - let gid: [u8; GID_LEN] = - payload - .get(GID_START..GID_END) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let gid: [u8; GID_LEN] = payload + .get(GID_START..GID_END) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; let further_action_byte = payload .get(FURTHER_ACTION_IDX) .copied() .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; - let further_action = FurtherAction::try_from(further_action_byte) - .unwrap_or(FurtherAction::NoFurtherAction); + let further_action = + FurtherAction::try_from(further_action_byte).unwrap_or(FurtherAction::NoFurtherAction); - let sync_status = payload.get(SYNC_STATUS_IDX) + let sync_status = payload + .get(SYNC_STATUS_IDX) .map(|&b| SyncStatus::try_from(b).unwrap_or(SyncStatus::Synchronized)); Ok(Self { diff --git a/src/error.rs b/src/error.rs index 4b30946..bc2a1ad 100644 --- a/src/error.rs +++ b/src/error.rs @@ -218,7 +218,10 @@ mod tests { service: 0x10, nrc: 0x11, }, - DoipError::PayloadTooShort { expected: 8, actual: 4 }, + DoipError::PayloadTooShort { + expected: 8, + actual: 4, + }, DoipError::UnknownRoutingActivationResponseCode(0x99), DoipError::EmptyUserData, DoipError::InvalidVinLength(10), From 1907f32b04e4815cdee4dfbb649bd46214a3eed9 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Thu, 5 Mar 2026 12:42:51 +0530 Subject: [PATCH 07/10] refactor(doip): address PR review comments - Split header_parser.rs into header.rs + codec.rs - Merge ParseError into DoipError (single error type) - Add parse_fixed_slice helper, remove duplicated parse patterns - Add DoipParseable / DoipSerializable traits - Make struct fields private, add public getters - Set default-features = false on all dependencies - Trim tokio features to io-util only - Remove dead test-handlers feature from Cargo.toml --- Cargo.lock | 581 +---------------------- src/doip/alive_check.rs | 8 +- src/doip/codec.rs | 173 +++++++ src/doip/diagnostic_message.rs | 69 ++- src/doip/{header_parser.rs => header.rs} | 343 ++++--------- src/doip/mod.rs | 39 +- src/doip/payload.rs | 54 +-- src/doip/routing_activation.rs | 131 +++-- src/doip/vehicle_id.rs | 57 ++- src/error.rs | 18 +- src/lib.rs | 2 +- src/server/config.rs | 177 +++++-- src/server/mod.rs | 21 + src/server/session.rs | 77 ++- src/uds/handler.rs | 65 ++- src/uds/mod.rs | 6 +- 16 files changed, 785 insertions(+), 1036 deletions(-) create mode 100644 src/doip/codec.rs rename src/doip/{header_parser.rs => header.rs} (73%) create mode 100644 src/server/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d705ef4..6117b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,83 +2,12 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - [[package]] name = "bytes" version = "1.11.0" @@ -91,80 +20,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "clap" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "doip-server" version = "0.1.0" dependencies = [ - "anyhow", "bytes", - "clap", - "dashmap", + "hex", "parking_lot", "serde", "thiserror", @@ -172,24 +33,6 @@ dependencies = [ "tokio-util", "toml", "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", ] [[package]] @@ -205,66 +48,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "libc" @@ -281,59 +68,12 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "parking_lot" version = "0.12.5" @@ -381,12 +121,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -396,29 +130,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "scopeguard" version = "1.2.0" @@ -464,47 +175,12 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "syn" version = "2.0.114" @@ -536,15 +212,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "tokio" version = "1.49.0" @@ -552,25 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", - "libc", - "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] [[package]] @@ -592,12 +241,10 @@ version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ - "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", - "toml_writer", "winnow", ] @@ -619,12 +266,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - [[package]] name = "tracing" version = "0.1.44" @@ -632,21 +273,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.36" @@ -654,36 +283,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -692,186 +291,14 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" -dependencies = [ - "getrandom", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" diff --git a/src/doip/alive_check.rs b/src/doip/alive_check.rs index 0ffb422..5cd1045 100644 --- a/src/doip/alive_check.rs +++ b/src/doip/alive_check.rs @@ -40,7 +40,7 @@ impl DoipSerializable for Request { // Tester responds with its logical address #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { - pub source_address: u16, + source_address: u16, } impl DoipParseable for Response { @@ -79,6 +79,12 @@ impl Response { pub fn new(source_address: u16) -> Self { Self { source_address } } + + /// The logical source address of the tester + #[must_use] + pub fn source_address(&self) -> u16 { + self.source_address + } } #[cfg(test)] diff --git a/src/doip/codec.rs b/src/doip/codec.rs new file mode 100644 index 0000000..dd5f748 --- /dev/null +++ b/src/doip/codec.rs @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +//! `DoIP` TCP Codec +//! +//! Provides a Tokio [`Decoder`]/[`Encoder`] pair for framing `DoIP` messages over TCP +//! streams according to ISO 13400-2:2019. A simple two-state machine handles reassembly +//! across packet boundaries: +//! +//! 1. **Header** – wait for 8 bytes, validate, then transition to Payload. +//! 2. **Payload** – wait for the declared payload length, then emit a [`DoipMessage`]. +//! +//! See [`header`](super::header) for the underlying type definitions. + +use bytes::BytesMut; +use std::io; +use tokio_util::codec::{Decoder, Encoder}; +use tracing::{debug, warn}; + +use super::header::{DOIP_HEADER_LENGTH, DoipHeader, DoipMessage, MAX_DOIP_MESSAGE_SIZE}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DecodeState { + Header, + Payload(DoipHeader), +} + +const DEFAULT_MAX_PAYLOAD_SIZE: u32 = MAX_DOIP_MESSAGE_SIZE; + +/// `DoIP` TCP Codec +/// +/// Implements the Tokio [`Decoder`] / [`Encoder`] trait pair to frame raw TCP bytes +/// into [`DoipMessage`] values and vice-versa. +/// +/// The codec enforces a configurable maximum payload size (default 4 MB) to provide +/// `DoS` protection against oversized message attacks. +#[derive(Debug)] +pub struct DoipCodec { + state: DecodeState, + max_payload_size: u32, +} + +impl DoipCodec { + #[must_use] + pub fn new() -> Self { + Self { + state: DecodeState::Header, + max_payload_size: DEFAULT_MAX_PAYLOAD_SIZE, + } + } + + /// Create codec with custom max payload size limit + /// + /// The size is u32 to match the `DoIP` header `payload_length` field (4 bytes). + /// This provides `DoS` protection by rejecting oversized messages early. + #[must_use] + pub fn with_max_payload_size(max_size: u32) -> Self { + Self { + state: DecodeState::Header, + max_payload_size: max_size, + } + } +} + +impl Default for DoipCodec { + fn default() -> Self { + Self::new() + } +} + +impl Decoder for DoipCodec { + type Item = DoipMessage; + type Error = io::Error; + + fn decode( + &mut self, + src: &mut BytesMut, + ) -> std::result::Result, Self::Error> { + loop { + match self.state { + DecodeState::Header => { + if src.len() < DOIP_HEADER_LENGTH { + // Reserve space to reduce reallocations when more data arrives + src.reserve(DOIP_HEADER_LENGTH); + return Ok(None); + } + + // Log raw bytes for debugging + let header_slice = src.get(..DOIP_HEADER_LENGTH).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "buffer too short") + })?; + debug!("Received raw header bytes: {:02X?}", header_slice); + + let header = DoipHeader::parse(header_slice) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + if let Some(nack_code) = header.validate() { + warn!( + "Header validation failed: {:?} - raw bytes: {:02X?}", + nack_code, header_slice + ); + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("validation failed: {nack_code:?}"), + )); + } + + if header.payload_length() > self.max_payload_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "payload too large: {} > {}", + header.payload_length(), + self.max_payload_size + ), + )); + } + + // Pre-allocate buffer for the complete message (best-effort hint) + if let Some(reserve_len) = header.message_length() { + src.reserve(reserve_len); + } + self.state = DecodeState::Payload(header); + } + + DecodeState::Payload(header) => { + let Some(total_len) = header.message_length() else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "payload length overflows usize", + )); + }; + if src.len() < total_len { + // Still waiting for complete payload - this is normal for large messages + // or when data arrives in multiple TCP packets + return Ok(None); + } + + let _ = src.split_to(DOIP_HEADER_LENGTH); + let payload = src.split_to(total_len - DOIP_HEADER_LENGTH).freeze(); + + self.state = DecodeState::Header; + return Ok(Some(DoipMessage { header, payload })); + } + } + } + } +} + +impl Encoder for DoipCodec { + type Error = io::Error; + + fn encode( + &mut self, + item: DoipMessage, + dst: &mut BytesMut, + ) -> std::result::Result<(), Self::Error> { + dst.reserve(item.message_length()); + item.header.write_to(dst); + dst.extend_from_slice(&item.payload); + Ok(()) + } +} diff --git a/src/doip/diagnostic_message.rs b/src/doip/diagnostic_message.rs index b345c3a..fde3c8b 100644 --- a/src/doip/diagnostic_message.rs +++ b/src/doip/diagnostic_message.rs @@ -12,7 +12,7 @@ */ //! Diagnostic Message handlers (ISO 13400-2:2019) -use super::{check_min_len, too_short, DoipParseable, DoipSerializable}; +use super::{DoipParseable, DoipSerializable, parse_fixed_slice, too_short}; use crate::DoipError; use bytes::{Buf, BufMut, Bytes, BytesMut}; use tracing::warn; @@ -29,6 +29,12 @@ pub enum AckCode { Acknowledged = 0x00, } +impl From for u8 { + fn from(code: AckCode) -> u8 { + code as u8 + } +} + // Diagnostic message negative ack codes per ISO 13400-2:2019 Table 28 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] @@ -59,13 +65,19 @@ impl TryFrom for NackCode { } } +impl From for u8 { + fn from(code: NackCode) -> u8 { + code as u8 + } +} + /// Diagnostic Message - carries UDS data between tester and ECU /// -/// Represents a DoIP diagnostic message as defined in ISO 13400-2:2019. +/// Represents a `DoIP` diagnostic message as defined in ISO 13400-2:2019. /// The message contains source/target addresses and UDS payload data. /// /// # Wire Format -/// Payload: SA(2) + TA(2) + user_data(1+) +/// Payload: SA(2) + TA(2) + `user_data(1`+) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Message { source_address: u16, @@ -124,10 +136,10 @@ impl Message { /// Diagnostic Message Positive Acknowledgment (message type 0x8002) /// -/// Sent by a DoIP entity to acknowledge receipt of a diagnostic message. +/// Sent by a `DoIP` entity to acknowledge receipt of a diagnostic message. /// /// # Wire Format -/// Payload: SA(2) + TA(2) + ack_code(1) + optional previous_diag_data +/// Payload: SA(2) + TA(2) + `ack_code(1)` + optional `previous_diag_data` #[derive(Debug, Clone, PartialEq, Eq)] pub struct PositiveAck { source_address: u16, @@ -141,6 +153,7 @@ impl PositiveAck { pub const MIN_LEN: usize = HEADER_BYTES + ACK_CODE_BYTES; /// Create a new positive acknowledgment + #[must_use] pub fn new(source: u16, target: u16) -> Self { Self { source_address: source, @@ -184,11 +197,11 @@ impl PositiveAck { /// Diagnostic Message Negative Acknowledgment (message type 0x8003) /// -/// Sent by a DoIP entity to indicate rejection of a diagnostic message. +/// Sent by a `DoIP` entity to indicate rejection of a diagnostic message. /// NACK codes are defined in ISO 13400-2:2019 Table 28. /// /// # Wire Format -/// Payload: SA(2) + TA(2) + nack_code(1) + optional previous_diag_data +/// Payload: SA(2) + TA(2) + `nack_code(1)` + optional `previous_diag_data` #[derive(Debug, Clone, PartialEq, Eq)] pub struct NegativeAck { source_address: u16, @@ -202,6 +215,7 @@ impl NegativeAck { pub const MIN_LEN: usize = HEADER_BYTES + ACK_CODE_BYTES; /// Create a new negative acknowledgment + #[must_use] pub fn new(source: u16, target: u16, code: NackCode) -> Self { Self { source_address: source, @@ -245,14 +259,7 @@ impl NegativeAck { impl DoipParseable for Message { fn parse(payload: &[u8]) -> std::result::Result { - let header: [u8; HEADER_BYTES] = payload - .get(..HEADER_BYTES) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| { - let e = too_short(payload, Self::MIN_LEN); - warn!("DiagnosticMessage parse failed: {}", e); - e - })?; + let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticMessage")?; let source_address = u16::from_be_bytes([header[0], header[1]]); let target_address = u16::from_be_bytes([header[2], header[3]]); @@ -293,15 +300,7 @@ impl DoipSerializable for Message { impl DoipParseable for PositiveAck { fn parse(payload: &[u8]) -> std::result::Result { - if let Err(e) = check_min_len(payload, Self::MIN_LEN) { - warn!("DiagnosticPositiveAck parse failed: {}", e); - return Err(e); - } - - let header: [u8; HEADER_BYTES] = payload - .get(..HEADER_BYTES) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticPositiveAck")?; let source_address = u16::from_be_bytes([header[0], header[1]]); let target_address = u16::from_be_bytes([header[2], header[3]]); @@ -323,13 +322,13 @@ impl DoipParseable for PositiveAck { impl DoipSerializable for PositiveAck { fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, |d| d.len())) + Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, bytes::Bytes::len)) } fn write_to(&self, buf: &mut BytesMut) { buf.put_u16(self.source_address); buf.put_u16(self.target_address); - buf.put_u8(self.ack_code as u8); + buf.put_u8(u8::from(self.ack_code)); if let Some(ref data) = self.previous_data { buf.extend_from_slice(data); } @@ -338,23 +337,15 @@ impl DoipSerializable for PositiveAck { impl DoipParseable for NegativeAck { fn parse(payload: &[u8]) -> std::result::Result { - if let Err(e) = check_min_len(payload, Self::MIN_LEN) { - warn!("DiagnosticNegativeAck parse failed: {}", e); - return Err(e); - } - - let header: [u8; HEADER_BYTES] = payload - .get(..HEADER_BYTES) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; + let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticNegativeAck")?; let source_address = u16::from_be_bytes([header[0], header[1]]); let target_address = u16::from_be_bytes([header[2], header[3]]); let nack_code = payload .get(HEADER_BYTES) .copied() - .and_then(|b| NackCode::try_from(b).ok()) - .unwrap_or(NackCode::TransportProtocolError); + .ok_or_else(|| too_short(payload, Self::MIN_LEN)) + .and_then(NackCode::try_from)?; let previous_data = payload .get(Self::MIN_LEN..) @@ -372,13 +363,13 @@ impl DoipParseable for NegativeAck { impl DoipSerializable for NegativeAck { fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, |d| d.len())) + Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, bytes::Bytes::len)) } fn write_to(&self, buf: &mut BytesMut) { buf.put_u16(self.source_address); buf.put_u16(self.target_address); - buf.put_u8(self.nack_code as u8); + buf.put_u8(u8::from(self.nack_code)); if let Some(ref data) = self.previous_data { buf.extend_from_slice(data); } diff --git a/src/doip/header_parser.rs b/src/doip/header.rs similarity index 73% rename from src/doip/header_parser.rs rename to src/doip/header.rs index c00f0d7..884b6db 100644 --- a/src/doip/header_parser.rs +++ b/src/doip/header.rs @@ -11,15 +11,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! `DoIP` Header Parser and Codec +//! `DoIP` Header Types //! -//! This module implements the core `DoIP` protocol header parsing and message framing -//! according to ISO 13400-2:2019. It provides: -//! -//! - **Header Validation**: Checks protocol version, payload types, and message sizes -//! - **Message Framing**: Tokio codec for TCP stream processing with proper buffering -//! - **Type Safety**: Strongly-typed payload types and error codes per ISO specification -//! - **`DoS` Protection**: Configurable max message size limits (default 4MB) +//! Defines the core `DoIP` protocol types for header parsing and message structures +//! according to ISO 13400-2:2019. //! //! The `DoIP` header consists of 8 bytes: //! - Protocol version (1 byte) + inverse version (1 byte) @@ -28,10 +23,10 @@ //! //! This module accepts protocol versions 0x01, 0x02, 0x03, and 0xFF to support //! both ISO 13400-2:2012 and ISO 13400-2:2019 specifications. +//! +//! See [`codec`](super::codec) for the Tokio TCP framing codec. -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use std::io; -use tokio_util::codec::{Decoder, Encoder}; +use bytes::{BufMut, Bytes, BytesMut}; use tracing::{debug, warn}; /// Generic Header NACK codes (ISO 13400-2:2019 Table 17) @@ -63,19 +58,6 @@ impl TryFrom for GenericNackCode { } } -#[derive(thiserror::Error, Debug)] -pub(crate) enum ParseError { - #[error("Invalid header: {0}")] - InvalidHeader(String), - /// Payload length exceeds `u32::MAX` (DoIP protocol limit is 4 MB) - #[error("DoIP payload exceeds u32::MAX (protocol limit is 4 MB)")] - PayloadTooLarge, - #[error("IO error: {0}")] - IoError(#[from] io::Error), -} - -pub(crate) type Result = std::result::Result; - /// `DoIP` protocol version 0x01 (legacy, pre-ISO 13400-2:2012) pub const PROTOCOL_VERSION_V1: u8 = 0x01; /// Default `DoIP` protocol version 0x02 (ISO 13400-2:2012 / 2019) @@ -148,6 +130,12 @@ impl TryFrom for PayloadType { } } +impl From for u16 { + fn from(pt: PayloadType) -> u16 { + pt as u16 + } +} + impl PayloadType { /// Returns minimum payload length for this payload type (ISO 13400-2:2019 Section 7) /// @@ -187,23 +175,23 @@ impl PayloadType { /// during validation but don't cause parsing failures. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DoipHeader { - pub version: u8, - pub inverse_version: u8, - pub payload_type: u16, - pub payload_length: u32, + version: u8, + inverse_version: u8, + payload_type: u16, + payload_length: u32, } impl DoipHeader { /// Parse a `DoIP` header from a byte slice /// /// # Errors - /// Returns `ParseError::InvalidHeader` if data is less than 8 bytes - pub(crate) fn parse(data: &[u8]) -> Result { + /// Returns [`crate::DoipError::InvalidHeader`] if data is less than 8 bytes. + pub(crate) fn parse(data: &[u8]) -> std::result::Result { let header: [u8; DOIP_HEADER_LENGTH] = data .get(..DOIP_HEADER_LENGTH) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { - ParseError::InvalidHeader(format!( + crate::DoipError::InvalidHeader(format!( "DoIP header too short: expected {}, got {}", DOIP_HEADER_LENGTH, data.len() @@ -218,31 +206,6 @@ impl DoipHeader { }) } - /// Parse a `DoIP` header from a mutable Bytes buffer - /// - /// # Errors - /// Returns `ParseError::InvalidHeader` if buffer is less than 8 bytes - // - // Used by the streaming decode path in the TCP/UDP server handlers - // (feature/doip-async-server). Not yet called in this branch because - // the server handler layer has not been ported here yet. - #[allow(dead_code)] - pub(crate) fn parse_from_buf(buf: &mut Bytes) -> Result { - if buf.len() < DOIP_HEADER_LENGTH { - return Err(ParseError::InvalidHeader(format!( - "DoIP header too short: expected {}, got {}", - DOIP_HEADER_LENGTH, - buf.len() - ))); - } - Ok(Self { - version: buf.get_u8(), - inverse_version: buf.get_u8(), - payload_type: buf.get_u16(), - payload_length: buf.get_u32(), - }) - } - pub fn validate(&self) -> Option { debug!( "Validating DoIP header: version=0x{:02X}, inverse=0x{:02X}, type=0x{:04X}, len={}", @@ -284,7 +247,10 @@ impl DoipHeader { warn!("Message too large: {} bytes", self.payload_length); return Some(GenericNackCode::MessageTooLarge); } - if (self.payload_length as usize) < payload_type.min_payload_length() { + let Ok(payload_len_usize) = usize::try_from(self.payload_length) else { + return Some(GenericNackCode::MessageTooLarge); + }; + if payload_len_usize < payload_type.min_payload_length() { warn!( "Payload too short for {:?}: {} < {}", payload_type, @@ -302,10 +268,14 @@ impl DoipHeader { self.validate().is_none() } - /// Returns the total message length (header + payload) + /// Returns the total message length (header + payload), or `None` if + /// `payload_length` overflows `usize` (impossible on 32/64-bit platforms, + /// but handled explicitly to avoid any panic path). #[must_use] - pub fn message_length(&self) -> usize { - DOIP_HEADER_LENGTH.saturating_add(self.payload_length as usize) + pub fn message_length(&self) -> Option { + usize::try_from(self.payload_length) + .ok() + .map(|pl| DOIP_HEADER_LENGTH.saturating_add(pl)) } /// Serialize header to bytes @@ -322,6 +292,30 @@ impl DoipHeader { buf.put_u16(self.payload_type); buf.put_u32(self.payload_length); } + + /// Protocol version byte + #[must_use] + pub fn version(&self) -> u8 { + self.version + } + + /// Inverted protocol version byte + #[must_use] + pub fn inverse_version(&self) -> u8 { + self.inverse_version + } + + /// Raw payload type as `u16` + #[must_use] + pub fn payload_type(&self) -> u16 { + self.payload_type + } + + /// Payload length in bytes + #[must_use] + pub fn payload_length(&self) -> u32 { + self.payload_length + } } impl Default for DoipHeader { @@ -355,63 +349,23 @@ impl std::fmt::Display for DoipHeader { /// ISO 13400-2 uses the term "message" throughout the specification. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DoipMessage { - pub header: DoipHeader, - pub payload: Bytes, + pub(crate) header: DoipHeader, + pub(crate) payload: Bytes, } impl DoipMessage { /// Create a new `DoIP` message with the default protocol version. - /// - /// # Errors - /// - /// Returns [`ParseError::PayloadTooLarge`] if `payload.len()` exceeds `u32::MAX`. - /// In practice this cannot occur because the `DoIP` codec enforces a 4 MB message limit. - // - // Called by server handlers to build outgoing response messages. - // No production callers exist in this branch yet — server handlers live - // on feature/doip-async-server and have not been ported here. - #[allow(dead_code)] - pub(crate) fn new(payload_type: PayloadType, payload: Bytes) -> Result { - Ok(Self { + #[cfg(test)] + pub(crate) fn new(payload_type: PayloadType, payload: Bytes) -> Self { + Self { header: DoipHeader { version: DEFAULT_PROTOCOL_VERSION, inverse_version: DEFAULT_PROTOCOL_VERSION_INV, - payload_type: payload_type as u16, - payload_length: u32::try_from(payload.len()) - .map_err(|_| ParseError::PayloadTooLarge)?, + payload_type: u16::from(payload_type), + payload_length: u32::try_from(payload.len()).expect("test payload fits in u32"), }, payload, - }) - } - - /// Create a `DoIP` message with a specific protocol version. - /// - /// This mirrors the version from the incoming request. - /// - /// # Errors - /// - /// Returns [`ParseError::PayloadTooLarge`] if `payload.len()` exceeds `u32::MAX`. - /// In practice this cannot occur because the `DoIP` codec enforces a 4 MB message limit. - // - // Required by ISO 13400-2:2019: the server must mirror the client's protocol - // version in every response. No production callers exist in this branch yet — - // server handlers live on feature/doip-async-server and have not been ported here. - #[allow(dead_code)] - pub(crate) fn with_version( - version: u8, - payload_type: PayloadType, - payload: Bytes, - ) -> Result { - Ok(Self { - header: DoipHeader { - version, - inverse_version: version ^ DOIP_HEADER_VERSION_MASK, // Compute inverse - payload_type: payload_type as u16, - payload_length: u32::try_from(payload.len()) - .map_err(|_| ParseError::PayloadTooLarge)?, - }, - payload, - }) + } } /// Create a DoIP message with a raw (unparsed) payload type @@ -425,9 +379,7 @@ impl DoipMessage { version: DEFAULT_PROTOCOL_VERSION, inverse_version: DEFAULT_PROTOCOL_VERSION_INV, payload_type, - // Safe cast: DoIP messages are limited to MAX_DOIP_MESSAGE_SIZE (4MB) - // which fits in u32. The codec enforces this limit during decode. - payload_length: payload.len() as u32, + payload_length: u32::try_from(payload.len()).expect("test payload fits in u32"), }, payload, } @@ -437,6 +389,16 @@ impl DoipMessage { PayloadType::try_from(self.header.payload_type).ok() } + /// The `DoIP` header + pub fn header(&self) -> &DoipHeader { + &self.header + } + + /// The message payload bytes + pub fn payload(&self) -> &Bytes { + &self.payload + } + /// Returns the total message length (header + payload) #[must_use] pub fn message_length(&self) -> usize { @@ -451,134 +413,6 @@ impl DoipMessage { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DecodeState { - Header, - Payload(DoipHeader), -} - -#[derive(Debug)] -pub struct DoipCodec { - state: DecodeState, - max_payload_size: u32, -} - -impl DoipCodec { - #[must_use] - pub fn new() -> Self { - Self { - state: DecodeState::Header, - max_payload_size: MAX_DOIP_MESSAGE_SIZE, - } - } - - /// Create codec with custom max payload size limit - /// - /// The size is u32 to match the `DoIP` header `payload_length` field (4 bytes). - /// This provides `DoS` protection by rejecting oversized messages early. - #[must_use] - pub fn with_max_payload_size(max_size: u32) -> Self { - Self { - state: DecodeState::Header, - max_payload_size: max_size, - } - } -} - -impl Default for DoipCodec { - fn default() -> Self { - Self::new() - } -} - -impl Decoder for DoipCodec { - type Item = DoipMessage; - type Error = io::Error; - - // Using fully qualified std::result::Result because the module-level Result - // type alias uses ParseError, but the Decoder trait requires io::Error. - fn decode( - &mut self, - src: &mut BytesMut, - ) -> std::result::Result, Self::Error> { - loop { - match self.state { - DecodeState::Header => { - if src.len() < DOIP_HEADER_LENGTH { - // Reserve space to reduce reallocations when more data arrives - src.reserve(DOIP_HEADER_LENGTH); - return Ok(None); - } - - // Log raw bytes for debugging - let header_slice = src.get(..DOIP_HEADER_LENGTH).ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "buffer too short") - })?; - debug!("Received raw header bytes: {:02X?}", header_slice); - - let header = DoipHeader::parse(header_slice) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - - if let Some(nack_code) = header.validate() { - warn!( - "Header validation failed: {:?} - raw bytes: {:02X?}", - nack_code, header_slice - ); - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("validation failed: {nack_code:?}"), - )); - } - - if header.payload_length > self.max_payload_size { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "payload too large: {} > {}", - header.payload_length, self.max_payload_size - ), - )); - } - - // Pre-allocate buffer for the complete message - src.reserve(header.message_length()); - self.state = DecodeState::Payload(header); - } - - DecodeState::Payload(header) => { - let total_len = header.message_length(); - if src.len() < total_len { - // Still waiting for complete payload - this is normal for large messages - // or when data arrives in multiple TCP packets - return Ok(None); - } - - let _ = src.split_to(DOIP_HEADER_LENGTH); - let payload = src.split_to(header.payload_length as usize).freeze(); - - self.state = DecodeState::Header; - return Ok(Some(DoipMessage { header, payload })); - } - } - } - } -} - -impl Encoder for DoipCodec { - type Error = io::Error; - - fn encode( - &mut self, - item: DoipMessage, - dst: &mut BytesMut, - ) -> std::result::Result<(), Self::Error> { - dst.reserve(item.message_length()); - item.header.write_to(dst); - dst.extend_from_slice(&item.payload); - Ok(()) - } -} - // ============================================================================ // Unit Tests // ============================================================================ @@ -586,6 +420,8 @@ impl Encoder for DoipCodec { #[cfg(test)] mod tests { use super::*; + use crate::doip::codec::DoipCodec; + use tokio_util::codec::{Decoder, Encoder}; // --- Helper to build a valid DoIP header quickly --- fn make_header(payload_type: u16, payload_len: u32) -> DoipHeader { @@ -764,7 +600,7 @@ mod tests { fn create_tester_present_message() { // UDS TesterPresent: 0x3E 0x00 let uds = Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x3E, 0x00]); - let msg = DoipMessage::new(PayloadType::DiagnosticMessage, uds).unwrap(); + let msg = DoipMessage::new(PayloadType::DiagnosticMessage, uds); assert_eq!(msg.header.payload_type, 0x8001); assert_eq!(msg.header.payload_length, 6); @@ -774,8 +610,7 @@ mod tests { #[test] fn create_vehicle_id_broadcast() { // Empty payload for discovery - let msg = - DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()).unwrap(); + let msg = DoipMessage::new(PayloadType::VehicleIdentificationRequest, Bytes::new()); assert_eq!(msg.header.payload_length, 0); assert_eq!(msg.message_length(), 8); // Just the header @@ -790,7 +625,7 @@ mod tests { #[test] fn serialize_message_to_wire_format() { - let msg = DoipMessage::new(PayloadType::AliveCheckRequest, Bytes::new()).unwrap(); + let msg = DoipMessage::new(PayloadType::AliveCheckRequest, Bytes::new()); let wire = msg.to_bytes(); assert_eq!(wire.len(), 8); @@ -876,7 +711,7 @@ mod tests { fn encode_diagnostic_message() { let mut codec = DoipCodec::new(); let payload = Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x3E]); - let msg = DoipMessage::new(PayloadType::DiagnosticMessage, payload).unwrap(); + let msg = DoipMessage::new(PayloadType::DiagnosticMessage, payload); let mut buf = BytesMut::new(); codec.encode(msg, &mut buf).unwrap(); @@ -896,8 +731,7 @@ mod tests { let original = DoipMessage::new( PayloadType::DiagnosticMessage, Bytes::from_static(&[0x0E, 0x80, 0x10, 0x01, 0x22, 0xF1, 0x90]), - ) - .unwrap(); + ); let mut buf = BytesMut::new(); codec.encode(original.clone(), &mut buf).unwrap(); @@ -936,20 +770,9 @@ mod tests { #[test] fn parse_error_shows_useful_message() { - let err = ParseError::InvalidHeader("buffer too short".into()); - let msg = format!("{err}"); - assert!(msg.contains("Invalid header")); - assert!(msg.contains("buffer too short")); - } - - #[test] - fn io_errors_convert_to_parse_errors() { - let io_err = io::Error::new(io::ErrorKind::UnexpectedEof, "connection lost"); - let parse_err: ParseError = io_err.into(); - match parse_err { - ParseError::IoError(e) => assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof), - _ => panic!("expected Io variant"), - } + let result = DoipHeader::parse(&[]); + let msg = format!("{}", result.unwrap_err()); + assert!(msg.contains("DoIP header")); } #[test] diff --git a/src/doip/mod.rs b/src/doip/mod.rs index 42d3aa9..d9ebdba 100644 --- a/src/doip/mod.rs +++ b/src/doip/mod.rs @@ -14,28 +14,30 @@ //! This module provides the core `DoIP` protocol types and codec for TCP/UDP communication. pub mod alive_check; +pub mod codec; pub mod diagnostic_message; -pub mod header_parser; +pub mod header; pub mod payload; pub mod routing_activation; pub mod vehicle_id; use crate::DoipError; use bytes::{Bytes, BytesMut}; +use tracing::warn; -/// Trait for DoIP message types that can be parsed from a raw payload slice. +/// Trait for `DoIP` message types that can be parsed from a raw payload slice. /// /// Implement this for every message struct so callers can decode incoming -/// DoIP frames through a uniform interface. +/// `DoIP` frames through a uniform interface. pub trait DoipParseable: Sized { - /// Parse a DoIP message from a raw payload byte slice. + /// Parse a `DoIP` message from a raw payload byte slice. /// /// # Errors /// Returns [`DoipError`] if the payload is malformed or too short. fn parse(payload: &[u8]) -> std::result::Result; } -/// Trait for DoIP message types that can be serialized to a [`Bytes`] buffer. +/// Trait for `DoIP` message types that can be serialized to a [`Bytes`] buffer. /// /// Implement [`write_to`] with the wire-format logic. The default [`to_bytes`] /// wraps it in a `BytesMut` and calls `freeze()`, so you never write that @@ -82,11 +84,30 @@ pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> std::result::Res } } +/// Extract the first `N` bytes of `payload` as a fixed-size array. +/// +/// Logs a warning and returns [`DoipError::PayloadTooShort`] when the slice +/// is shorter than `N` bytes, using `context` to identify the call site in the log. +pub(crate) fn parse_fixed_slice( + payload: &[u8], + context: &str, +) -> std::result::Result<[u8; N], DoipError> { + payload + .get(..N) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| { + let e = too_short(payload, N); + warn!("{} parse failed: {}", context, e); + e + }) +} + // Re-export core types and constants for convenient access. // Constants are exported to allow external testing and custom DoIP message construction. -pub use header_parser::{ - DoipCodec, DoipHeader, DoipMessage, GenericNackCode, PayloadType, DEFAULT_PROTOCOL_VERSION, - DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, DOIP_HEADER_VERSION_MASK, - DOIP_VERSION_DEFAULT, MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, +pub use codec::DoipCodec; +pub use header::{ + DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, + DOIP_HEADER_VERSION_MASK, DOIP_VERSION_DEFAULT, DoipHeader, DoipMessage, GenericNackCode, + MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, PayloadType, }; pub use payload::DoipPayload; diff --git a/src/doip/payload.rs b/src/doip/payload.rs index 1ae90d0..ba484e2 100644 --- a/src/doip/payload.rs +++ b/src/doip/payload.rs @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -//! Typed dispatch envelope for DoIP message payloads (ISO 13400-2:2019). +//! Typed dispatch envelope for `DoIP` message payloads (ISO 13400-2:2019). //! //! [`DoipPayload`] is a strongly-typed enum that wraps every concrete payload //! struct. Use [`DoipPayload::parse`] to decode a raw [`DoipMessage`] into @@ -18,22 +18,26 @@ //! [`PayloadType`] throughout the codebase. //! //! # Example -//! ```ignore -//! let payload = DoipPayload::parse(&msg)?; -//! match payload { -//! DoipPayload::DiagnosticMessage(m) => handle_diag(m), -//! DoipPayload::AliveCheckRequest(_) => send_alive_response(), -//! _ => {} +//! ```no_run +//! # use doip_server::doip::{DoipMessage, DoipPayload}; +//! # use doip_server::DoipError; +//! fn dispatch(msg: &DoipMessage) -> Result<(), DoipError> { +//! match DoipPayload::parse(msg)? { +//! DoipPayload::DiagnosticMessage(m) => println!("UDS payload: {:?}", m), +//! DoipPayload::AliveCheckRequest(_) => println!("Alive check received"), +//! _ => {} +//! } +//! Ok(()) //! } //! ``` use super::{ - alive_check, diagnostic_message, routing_activation, vehicle_id, DoipMessage, DoipParseable, - GenericNackCode, PayloadType, + DoipMessage, DoipParseable, GenericNackCode, PayloadType, alive_check, diagnostic_message, + routing_activation, vehicle_id, }; use crate::DoipError; -/// A fully-parsed DoIP message payload. +/// A fully-parsed `DoIP` message payload. /// /// Each variant corresponds to one [`PayloadType`] and wraps the concrete /// struct returned by its [`DoipParseable`] impl. @@ -61,7 +65,7 @@ pub enum DoipPayload { VehicleIdentificationRequestWithVin(vehicle_id::RequestWithVin), /// `0x0004` – Vehicle Identification Response / Announce VehicleIdentificationResponse(vehicle_id::Response), - /// `0x0000` – Generic DoIP Header Negative Acknowledgement + /// `0x0000` – Generic `DoIP` Header Negative Acknowledgement GenericNack(GenericNackCode), } @@ -71,16 +75,16 @@ impl DoipPayload { /// # Errors /// Returns [`DoipError::UnknownPayloadType`] when the `payload_type` field /// in the header does not map to a known [`PayloadType`] variant, or when - /// the DoIP payload byte is not a recognized `GenericNackCode`. + /// the `DoIP` payload byte is not a recognized `GenericNackCode`. /// /// Returns a more specific [`DoipError`] (e.g. [`DoipError::PayloadTooShort`]) /// when the payload bytes are present but malformed. pub fn parse(msg: &DoipMessage) -> std::result::Result { - let payload = msg.payload.as_ref(); + let payload = msg.payload().as_ref(); let payload_type = msg .payload_type() - .ok_or(DoipError::UnknownPayloadType(msg.header.payload_type))?; + .ok_or(DoipError::UnknownPayloadType(msg.header().payload_type()))?; match payload_type { PayloadType::AliveCheckRequest => Ok(Self::AliveCheckRequest( @@ -133,7 +137,7 @@ impl DoipPayload { | PayloadType::DoipEntityStatusResponse | PayloadType::DiagnosticPowerModeRequest | PayloadType::DiagnosticPowerModeResponse => { - Err(DoipError::UnknownPayloadType(payload_type as u16)) + Err(DoipError::UnknownPayloadType(u16::from(payload_type))) } } } @@ -169,15 +173,7 @@ mod tests { use bytes::Bytes; fn make_msg(payload_type: PayloadType, payload: impl Into) -> DoipMessage { - DoipMessage { - header: crate::doip::DoipHeader { - version: crate::doip::DEFAULT_PROTOCOL_VERSION, - inverse_version: crate::doip::DEFAULT_PROTOCOL_VERSION_INV, - payload_type: payload_type as u16, - payload_length: 0, - }, - payload: payload.into(), - } + DoipMessage::new(payload_type, payload.into()) } #[test] @@ -224,15 +220,7 @@ mod tests { #[test] fn unknown_payload_type_error() { - let msg = DoipMessage { - header: crate::doip::DoipHeader { - version: crate::doip::DEFAULT_PROTOCOL_VERSION, - inverse_version: crate::doip::DEFAULT_PROTOCOL_VERSION_INV, - payload_type: 0xFFFF, - payload_length: 0, - }, - payload: Bytes::new(), - }; + let msg = DoipMessage::with_raw_payload_type(0xFFFF, Bytes::new()); let err = DoipPayload::parse(&msg).unwrap_err(); assert!(matches!(err, crate::DoipError::UnknownPayloadType(0xFFFF))); } diff --git a/src/doip/routing_activation.rs b/src/doip/routing_activation.rs index 840b660..e25b05b 100644 --- a/src/doip/routing_activation.rs +++ b/src/doip/routing_activation.rs @@ -12,24 +12,34 @@ */ //! Routing Activation handlers (ISO 13400-2:2019) -use super::{check_min_len, too_short, DoipParseable, DoipSerializable}; +use super::{DoipParseable, DoipSerializable, check_min_len, parse_fixed_slice}; use crate::DoipError; use bytes::{Buf, BufMut, Bytes, BytesMut}; use tracing::warn; -// Response codes per ISO 13400-2:2019 Table 25 +/// Routing activation response codes per ISO 13400-2:2019 Table 25. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ResponseCode { + /// Source address unknown to the `DoIP` entity (`0x00`) UnknownSourceAddress = 0x00, + /// All TCP sockets on the `DoIP` entity are registered and active (`0x01`) AllSocketsRegistered = 0x01, + /// Source address differs from the one registered to the socket (`0x02`) DifferentSourceAddress = 0x02, + /// Source address is already registered on a different socket (`0x03`) SourceAddressAlreadyActive = 0x03, + /// Routing activation denied; authentication required (`0x04`) MissingAuthentication = 0x04, + /// Routing activation denied; confirmation rejected (`0x05`) RejectedConfirmation = 0x05, + /// Unsupported routing activation type requested (`0x06`) UnsupportedActivationType = 0x06, + /// TLS connection required before routing can be activated (`0x07`) TlsRequired = 0x07, + /// Routing successfully activated (`0x10`) SuccessfullyActivated = 0x10, + /// Routing activation pending; confirmation required (`0x11`) ConfirmationRequired = 0x11, } @@ -53,7 +63,14 @@ impl TryFrom for ResponseCode { } } +impl From for u8 { + fn from(code: ResponseCode) -> u8 { + code as u8 + } +} + impl ResponseCode { + /// Returns `true` if this code represents a successful or pending-confirmation activation. #[must_use] pub fn is_success(self) -> bool { matches!( @@ -63,12 +80,15 @@ impl ResponseCode { } } -// Activation types per ISO 13400-2:2019 Table 24 +/// Routing activation types per ISO 13400-2:2019 Table 24. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ActivationType { + /// Default routing activation (`0x00`) Default = 0x00, + /// WWH-OBD routing activation (`0x01`) WwhObd = 0x01, + /// Central security routing activation (`0xE0`) CentralSecurity = 0xE0, } @@ -88,16 +108,40 @@ impl TryFrom for ActivationType { // Routing Activation Request - payload is 7 bytes min, 11 with OEM data #[derive(Debug, Clone, PartialEq, Eq)] pub struct Request { - pub source_address: u16, - pub activation_type: ActivationType, - pub reserved: u32, - pub oem_specific: Option, + source_address: u16, + activation_type: ActivationType, + reserved: u32, + oem_specific: Option, } impl Request { pub const MIN_LEN: usize = 7; pub const MAX_LEN: usize = 11; + /// Tester logical source address + #[must_use] + pub fn source_address(&self) -> u16 { + self.source_address + } + + /// Activation type requested + #[must_use] + pub fn activation_type(&self) -> ActivationType { + self.activation_type + } + + /// Reserved field (must be 0x00000000) + #[must_use] + pub fn reserved(&self) -> u32 { + self.reserved + } + + /// Optional OEM-specific data + #[must_use] + pub fn oem_specific(&self) -> Option { + self.oem_specific + } + /// Parse routing activation request from buffer /// /// # Errors @@ -130,55 +174,81 @@ impl Request { // Routing Activation Response - 9 bytes min, 13 with OEM data #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { - pub tester_address: u16, - pub entity_address: u16, - pub response_code: ResponseCode, - pub reserved: u32, - pub oem_specific: Option, + tester_address: u16, + entity_address: u16, + code: ResponseCode, + reserved: u32, + oem_specific: Option, } impl Response { pub const MIN_LEN: usize = 9; pub const MAX_LEN: usize = 13; + /// Build a successful routing activation response. #[must_use] pub fn success(tester_address: u16, entity_address: u16) -> Self { Self { tester_address, entity_address, - response_code: ResponseCode::SuccessfullyActivated, + code: ResponseCode::SuccessfullyActivated, reserved: 0, oem_specific: None, } } + /// Build a denied routing activation response with the given `code`. #[must_use] pub fn denial(tester_address: u16, entity_address: u16, code: ResponseCode) -> Self { Self { tester_address, entity_address, - response_code: code, + code, reserved: 0, oem_specific: None, } } + /// Returns `true` if the response code indicates successful or pending-confirmation activation. #[must_use] pub fn is_success(&self) -> bool { - self.response_code.is_success() + self.code.is_success() + } + + /// Tester logical address + #[must_use] + pub fn tester_address(&self) -> u16 { + self.tester_address + } + + /// `DoIP` entity logical address + #[must_use] + pub fn entity_address(&self) -> u16 { + self.entity_address + } + + /// Routing activation response code + #[must_use] + pub fn response_code(&self) -> ResponseCode { + self.code + } + + /// Reserved field + #[must_use] + pub fn reserved(&self) -> u32 { + self.reserved + } + + /// Optional OEM-specific data + #[must_use] + pub fn oem_specific(&self) -> Option { + self.oem_specific } } impl DoipParseable for Request { fn parse(payload: &[u8]) -> std::result::Result { - let header: [u8; Self::MIN_LEN] = payload - .get(..Self::MIN_LEN) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| { - let e = too_short(payload, Self::MIN_LEN); - warn!("RoutingActivation Request parse failed: {}", e); - e - })?; + let header: [u8; Self::MIN_LEN] = parse_fixed_slice(payload, "RoutingActivation Request")?; let source_address = u16::from_be_bytes([header[0], header[1]]); let activation_type = ActivationType::try_from(header[2]).map_err(|e| { @@ -203,14 +273,7 @@ impl DoipParseable for Request { impl DoipParseable for Response { fn parse(payload: &[u8]) -> std::result::Result { - let header: [u8; Self::MIN_LEN] = payload - .get(..Self::MIN_LEN) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| { - let e = too_short(payload, Self::MIN_LEN); - warn!("RoutingActivation Response parse failed: {}", e); - e - })?; + let header: [u8; Self::MIN_LEN] = parse_fixed_slice(payload, "RoutingActivation Response")?; let tester_address = u16::from_be_bytes([header[0], header[1]]); let entity_address = u16::from_be_bytes([header[2], header[3]]); @@ -228,7 +291,7 @@ impl DoipParseable for Response { Ok(Self { tester_address, entity_address, - response_code, + code: response_code, reserved, oem_specific, }) @@ -243,7 +306,7 @@ impl DoipSerializable for Response { fn write_to(&self, buf: &mut BytesMut) { buf.put_u16(self.tester_address); buf.put_u16(self.entity_address); - buf.put_u8(self.response_code as u8); + buf.put_u8(u8::from(self.code)); buf.put_u32(self.reserved); if let Some(oem) = self.oem_specific { buf.put_u32(oem); @@ -374,7 +437,7 @@ mod tests { let payload = [0x0E, 0x80, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]; let resp = Response::parse(&payload).unwrap(); assert!(!resp.is_success()); - assert_eq!(resp.response_code, ResponseCode::AllSocketsRegistered); + assert_eq!(resp.response_code(), ResponseCode::AllSocketsRegistered); } #[test] diff --git a/src/doip/vehicle_id.rs b/src/doip/vehicle_id.rs index 98cbb4c..c52eb22 100644 --- a/src/doip/vehicle_id.rs +++ b/src/doip/vehicle_id.rs @@ -13,7 +13,7 @@ //! Vehicle Identification handlers (ISO 13400-2:2019) -use super::{check_min_len, too_short, DoipParseable, DoipSerializable}; +use super::{DoipParseable, DoipSerializable, check_min_len, too_short}; use crate::DoipError; use bytes::{BufMut, BytesMut}; use tracing::warn; @@ -43,7 +43,7 @@ pub struct Request; // Vehicle Identification Request with EID (0x0002) - 6 byte EID #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestWithEid { - pub eid: [u8; 6], + eid: [u8; 6], } impl RequestWithEid { @@ -53,12 +53,18 @@ impl RequestWithEid { pub fn new(eid: [u8; 6]) -> Self { Self { eid } } + + /// The EID filter value + #[must_use] + pub fn eid(&self) -> &[u8; 6] { + &self.eid + } } // Vehicle Identification Request with VIN (0x0003) - 17 byte VIN #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestWithVin { - pub vin: [u8; 17], + vin: [u8; 17], } impl RequestWithVin { @@ -69,6 +75,12 @@ impl RequestWithVin { Self { vin } } + /// The VIN filter value as bytes + #[must_use] + pub fn vin(&self) -> &[u8; 17] { + &self.vin + } + #[must_use] pub fn vin_string(&self) -> String { String::from_utf8_lossy(&self.vin).to_string() @@ -94,6 +106,12 @@ impl TryFrom for FurtherAction { } } +impl From for u8 { + fn from(action: FurtherAction) -> u8 { + action as u8 + } +} + // Synchronization status per ISO 13400-2:2019 Table 22 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] @@ -113,16 +131,22 @@ impl TryFrom for SyncStatus { } } +impl From for u8 { + fn from(status: SyncStatus) -> u8 { + status as u8 + } +} + // Vehicle Identification Response (0x0004) // VIN(17) + LogicalAddr(2) + EID(6) + GID(6) + FurtherAction(1) = 32 bytes min #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { - pub vin: [u8; 17], - pub logical_address: u16, - pub eid: [u8; 6], - pub gid: [u8; 6], - pub further_action: FurtherAction, - pub sync_status: Option, + vin: [u8; 17], + logical_address: u16, + eid: [u8; 6], + gid: [u8; 6], + further_action: FurtherAction, + sync_status: Option, } impl Response { @@ -200,7 +224,7 @@ impl DoipParseable for Response { if let Err(e) = check_min_len(payload, Self::MIN_LEN) { warn!("VehicleId Response parse failed: {}", e); return Err(e); - }; + } let vin: [u8; VIN_LEN] = payload .get(..VIN_END) @@ -227,12 +251,13 @@ impl DoipParseable for Response { .get(FURTHER_ACTION_IDX) .copied() .ok_or_else(|| too_short(payload, Self::MIN_LEN))?; - let further_action = - FurtherAction::try_from(further_action_byte).unwrap_or(FurtherAction::NoFurtherAction); + let further_action = FurtherAction::try_from(further_action_byte) + .map_err(DoipError::UnknownFurtherAction)?; let sync_status = payload .get(SYNC_STATUS_IDX) - .map(|&b| SyncStatus::try_from(b).unwrap_or(SyncStatus::Synchronized)); + .map(|&b| SyncStatus::try_from(b).map_err(DoipError::UnknownSyncStatus)) + .transpose()?; Ok(Self { vin, @@ -247,7 +272,7 @@ impl DoipParseable for Response { impl DoipSerializable for Response { fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + if self.sync_status.is_some() { 1 } else { 0 }) + Some(Self::MIN_LEN + usize::from(self.sync_status.is_some())) } fn write_to(&self, buf: &mut BytesMut) { @@ -255,9 +280,9 @@ impl DoipSerializable for Response { buf.put_u16(self.logical_address); buf.extend_from_slice(&self.eid); buf.extend_from_slice(&self.gid); - buf.put_u8(self.further_action as u8); + buf.put_u8(u8::from(self.further_action)); if let Some(status) = self.sync_status { - buf.put_u8(status as u8); + buf.put_u8(u8::from(status)); } } } diff --git a/src/error.rs b/src/error.rs index bc2a1ad..616e042 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,11 +13,12 @@ //! Error Types for `DoIP` Server (ISO 13400-2:2019 & ISO 14229-1:2020) use std::io; +use std::net::AddrParseError; use thiserror::Error; // Re-export from the canonical definitions in the protocol modules pub use crate::doip::diagnostic_message::NackCode as DiagnosticNackCode; -pub use crate::doip::header_parser::GenericNackCode; +pub use crate::doip::header::GenericNackCode; pub use crate::doip::routing_activation::ResponseCode as RoutingActivationCode; /// Result type alias for `DoIP` operations @@ -32,6 +33,15 @@ pub enum DoipError { #[error("Configuration error: {0}")] InvalidConfig(String), + #[error("Configuration file error: {0}")] + ConfigFileError(String), + + #[error("Hex decode error: {0}")] + HexDecodeError(String), + + #[error("Invalid address: {0}")] + InvalidAddress(#[from] AddrParseError), + #[error("Invalid protocol version: expected 0x{expected:02X}, got 0x{actual:02X}")] InvalidProtocolVersion { expected: u8, actual: u8 }, @@ -53,6 +63,12 @@ pub enum DoipError { #[error("unknown diagnostic nack code: {0:#04x}")] UnknownNackCode(u8), + #[error("unknown further action byte: {0:#04x}")] + UnknownFurtherAction(u8), + + #[error("unknown sync status byte: {0:#04x}")] + UnknownSyncStatus(u8), + #[error("diagnostic message has no user data")] EmptyUserData, diff --git a/src/lib.rs b/src/lib.rs index 181919a..d888778 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ */ pub mod doip; pub mod error; +pub mod server; pub mod uds; - pub use error::DoipError; diff --git a/src/server/config.rs b/src/server/config.rs index b5c15d4..f7887ac 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -13,6 +13,7 @@ //! `DoIP` Server Configuration +use crate::DoipError; use serde::Deserialize; use std::net::SocketAddr; use std::path::Path; @@ -48,17 +49,30 @@ const DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS: u64 = 2_000; /// General inactivity timeout in milliseconds (`T_TCP_General` per ISO 13400-2: 5 minutes) const DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS: u64 = 300_000; +/// Runtime configuration for the `DoIP` server. +/// +/// Constructed either from a TOML file via [`ServerConfig::from_file`] or +/// from sensible ISO-compliant defaults via [`ServerConfig::default`]. #[derive(Debug, Clone)] pub struct ServerConfig { - pub tcp_addr: SocketAddr, - pub udp_addr: SocketAddr, - pub logical_address: u16, - pub vin: [u8; 17], - pub eid: [u8; 6], - pub gid: [u8; 6], - pub max_connections: usize, - pub initial_inactivity_timeout_ms: u64, - pub general_inactivity_timeout_ms: u64, + /// TCP bind address for diagnostic connections (ISO 13400-2 default port: 13400) + tcp_addr: SocketAddr, + /// UDP bind address for vehicle identification broadcasts (ISO 13400-2 default port: 13400) + udp_addr: SocketAddr, + /// Logical address of this `DoIP` entity (ISO 13400-2 Section 7.3) + logical_address: u16, + /// Vehicle Identification Number — 17 ASCII bytes per ISO 3779 + vin: [u8; 17], + /// Entity Identification — 6 bytes, typically the MAC address + eid: [u8; 6], + /// Group Identification — 6 bytes identifying a functional group + gid: [u8; 6], + /// Maximum number of concurrent TCP diagnostic connections + max_connections: usize, + /// Initial inactivity timeout in milliseconds (`T_TCP_Initial`, ISO 13400-2: 2 s) + initial_inactivity_timeout_ms: u64, + /// General inactivity timeout in milliseconds (`T_TCP_General`, ISO 13400-2: 5 min) + general_inactivity_timeout_ms: u64, } impl Default for ServerConfig { @@ -167,6 +181,7 @@ impl Default for TimeoutSection { } impl ServerConfig { + /// Create a config with all defaults except for the given `logical_address`. #[must_use] pub fn new(logical_address: u16) -> Self { Self { @@ -178,10 +193,14 @@ impl ServerConfig { /// Load configuration from TOML file /// /// # Errors - /// Returns error if file cannot be read, parsed, or contains invalid values - pub fn from_file>(path: P) -> anyhow::Result { - let content = std::fs::read_to_string(path)?; - let file: ConfigFile = toml::from_str(&content)?; + /// Returns [`DoipError::ConfigFileError`] if file cannot be read or parsed. + /// Returns [`DoipError::InvalidConfig`] if values are invalid. + /// Returns [`DoipError::InvalidAddress`] if bind address is malformed. + pub fn from_file>(path: P) -> std::result::Result { + let content = + std::fs::read_to_string(path).map_err(|e| DoipError::ConfigFileError(e.to_string()))?; + let file: ConfigFile = + toml::from_str(&content).map_err(|e| DoipError::ConfigFileError(e.to_string()))?; let bind = &file.server.bind_address; Ok(Self { @@ -189,29 +208,54 @@ impl ServerConfig { udp_addr: format!("{bind}:{}", file.server.udp_port).parse()?, max_connections: file.server.max_connections, logical_address: file.vehicle.logical_address, - vin: file.vehicle.vin.as_deref().map(Self::parse_vin).transpose()?.unwrap_or(*DEFAULT_VIN), - eid: file.vehicle.eid.as_deref().map(Self::parse_hex_array).transpose()?.unwrap_or(DEFAULT_EID), - gid: file.vehicle.gid.as_deref().map(Self::parse_hex_array).transpose()?.unwrap_or(DEFAULT_GID), + vin: file + .vehicle + .vin + .as_deref() + .map(Self::parse_vin) + .transpose()? + .unwrap_or(*DEFAULT_VIN), + eid: file + .vehicle + .eid + .as_deref() + .map(Self::parse_hex_array) + .transpose()? + .unwrap_or(DEFAULT_EID), + gid: file + .vehicle + .gid + .as_deref() + .map(Self::parse_hex_array) + .transpose()? + .unwrap_or(DEFAULT_GID), initial_inactivity_timeout_ms: file.timeouts.initial_inactivity_ms, general_inactivity_timeout_ms: file.timeouts.general_inactivity_ms, }) } - fn parse_vin(s: &str) -> anyhow::Result<[u8; 17]> { + fn parse_vin(s: &str) -> std::result::Result<[u8; 17], DoipError> { let bytes = s.as_bytes(); if bytes.len() != 17 { - anyhow::bail!("VIN must be exactly 17 characters"); + return Err(DoipError::InvalidConfig(format!( + "VIN must be exactly 17 characters, got {}", + bytes.len() + ))); } let mut vin = [0u8; 17]; vin.copy_from_slice(bytes); Ok(vin) } - fn parse_hex_array(s: &str) -> anyhow::Result<[u8; N]> { + fn parse_hex_array(s: &str) -> std::result::Result<[u8; N], DoipError> { let s = s.trim_start_matches("0x").replace([':', '-', ' '], ""); - let bytes = hex::decode(&s)?; + let bytes = hex::decode(&s).map_err(|e| DoipError::HexDecodeError(e.to_string()))?; if bytes.len() != N { - anyhow::bail!("Expected {} bytes, got {}", N, bytes.len()); + return Err(DoipError::InvalidConfig(format!( + "Expected {} bytes, got {}", + N, + bytes.len() + ))); } let mut arr = [0u8; N]; arr.copy_from_slice(&bytes); @@ -230,6 +274,60 @@ impl ServerConfig { self.udp_addr = udp; self } + + /// Returns the TCP bind address. + #[must_use] + pub fn tcp_addr(&self) -> SocketAddr { + self.tcp_addr + } + + /// Returns the UDP bind address. + #[must_use] + pub fn udp_addr(&self) -> SocketAddr { + self.udp_addr + } + + /// Returns the logical address of this `DoIP` entity. + #[must_use] + pub fn logical_address(&self) -> u16 { + self.logical_address + } + + /// Returns the Vehicle Identification Number. + #[must_use] + pub fn vin(&self) -> [u8; 17] { + self.vin + } + + /// Returns the Entity Identification bytes. + #[must_use] + pub fn eid(&self) -> [u8; 6] { + self.eid + } + + /// Returns the Group Identification bytes. + #[must_use] + pub fn gid(&self) -> [u8; 6] { + self.gid + } + + /// Returns the maximum number of concurrent TCP connections. + #[must_use] + pub fn max_connections(&self) -> usize { + self.max_connections + } + + /// Returns the initial inactivity timeout in milliseconds. + #[must_use] + pub fn initial_inactivity_timeout_ms(&self) -> u64 { + self.initial_inactivity_timeout_ms + } + + /// Returns the general inactivity timeout in milliseconds. + #[must_use] + pub fn general_inactivity_timeout_ms(&self) -> u64 { + self.general_inactivity_timeout_ms + } } #[cfg(test)] @@ -240,19 +338,19 @@ mod tests { fn test_default_config() { let config = ServerConfig::default(); - assert_eq!(config.tcp_addr.port(), DEFAULT_DOIP_PORT); - assert_eq!(config.udp_addr.port(), DEFAULT_DOIP_PORT); - assert_eq!(config.logical_address, DEFAULT_LOGICAL_ADDRESS); - assert_eq!(config.vin, *DEFAULT_VIN); - assert_eq!(config.eid, DEFAULT_EID); - assert_eq!(config.gid, DEFAULT_GID); - assert_eq!(config.max_connections, DEFAULT_MAX_CONNECTIONS); + assert_eq!(config.tcp_addr().port(), DEFAULT_DOIP_PORT); + assert_eq!(config.udp_addr().port(), DEFAULT_DOIP_PORT); + assert_eq!(config.logical_address(), DEFAULT_LOGICAL_ADDRESS); + assert_eq!(config.vin(), *DEFAULT_VIN); + assert_eq!(config.eid(), DEFAULT_EID); + assert_eq!(config.gid(), DEFAULT_GID); + assert_eq!(config.max_connections(), DEFAULT_MAX_CONNECTIONS); assert_eq!( - config.initial_inactivity_timeout_ms, + config.initial_inactivity_timeout_ms(), DEFAULT_INITIAL_INACTIVITY_TIMEOUT_MS ); assert_eq!( - config.general_inactivity_timeout_ms, + config.general_inactivity_timeout_ms(), DEFAULT_GENERAL_INACTIVITY_TIMEOUT_MS ); } @@ -261,8 +359,8 @@ mod tests { fn test_new_with_logical_address() { let config = ServerConfig::new(0x1234); - assert_eq!(config.logical_address, 0x1234); - assert_eq!(config.tcp_addr.port(), DEFAULT_DOIP_PORT); + assert_eq!(config.logical_address(), 0x1234); + assert_eq!(config.tcp_addr().port(), DEFAULT_DOIP_PORT); } #[test] @@ -280,20 +378,23 @@ mod tests { #[test] fn test_parse_hex_array_valid() { - let result: anyhow::Result<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); + let result: std::result::Result<[u8; 6], DoipError> = + ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); assert!(result.is_ok()); assert_eq!(result.unwrap(), [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]); } #[test] fn test_parse_hex_array_with_0x_prefix() { - let result: anyhow::Result<[u8; 6]> = ServerConfig::parse_hex_array("0x001A2B3C4D5E"); + let result: std::result::Result<[u8; 6], DoipError> = + ServerConfig::parse_hex_array("0x001A2B3C4D5E"); assert!(result.is_ok()); } #[test] fn test_parse_hex_array_invalid_length() { - let result: anyhow::Result<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B"); + let result: std::result::Result<[u8; 6], DoipError> = + ServerConfig::parse_hex_array("00:1A:2B"); assert!(result.is_err()); } @@ -302,7 +403,7 @@ mod tests { let new_vin = *b"NEWVIN12345678901"; let config = ServerConfig::default().with_vin(new_vin); - assert_eq!(config.vin, new_vin); + assert_eq!(config.vin(), new_vin); } #[test] @@ -311,7 +412,7 @@ mod tests { let udp: SocketAddr = "192.168.1.1:13401".parse().unwrap(); let config = ServerConfig::default().with_addresses(tcp, udp); - assert_eq!(config.tcp_addr, tcp); - assert_eq!(config.udp_addr, udp); + assert_eq!(config.tcp_addr(), tcp); + assert_eq!(config.udp_addr(), udp); } } diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..f9969df --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + */ +//! Server Module +//! +//! `DoIP` server configuration and session management. + +pub mod config; +pub mod session; + +pub use config::ServerConfig; +pub use session::{Session, SessionManager}; diff --git a/src/server/session.rs b/src/server/session.rs index 41a0ac1..ce90227 100644 --- a/src/server/session.rs +++ b/src/server/session.rs @@ -26,15 +26,21 @@ pub enum SessionState { Closed, } +/// A single `DoIP` tester connection and its lifecycle state. #[derive(Debug, Clone)] pub struct Session { - pub id: u64, - pub peer_addr: SocketAddr, - pub tester_address: u16, - pub state: SessionState, + /// Unique monotonic session identifier assigned at connection time + id: u64, + /// Remote socket address of the connected tester + peer_addr: SocketAddr, + /// Tester logical address registered during routing activation (`0` until activated) + tester_address: u16, + /// Current state in the ISO 13400-2 connection lifecycle + state: SessionState, } impl Session { + /// Create a new session in the [`SessionState::Connected`] state. #[must_use] pub fn new(id: u64, peer_addr: SocketAddr) -> Self { Self { @@ -45,18 +51,52 @@ impl Session { } } + /// Transition this session to [`SessionState::RoutingActive`] and record the tester's logical address. pub fn activate_routing(&mut self, tester_address: u16) { - debug!("Session {} routing activated: tester_address=0x{:04X}", self.id, tester_address); + debug!( + "Session {} routing activated: tester_address=0x{:04X}", + self.id, tester_address + ); self.tester_address = tester_address; self.state = SessionState::RoutingActive; } + /// Returns `true` if routing has been activated for this session. #[must_use] pub fn is_routing_active(&self) -> bool { self.state == SessionState::RoutingActive } + + /// Returns the unique session ID. + #[must_use] + pub fn id(&self) -> u64 { + self.id + } + + /// Returns the remote socket address of the connected tester. + #[must_use] + pub fn peer_addr(&self) -> SocketAddr { + self.peer_addr + } + + /// Returns the tester logical address (`0` until routing is activated). + #[must_use] + pub fn tester_address(&self) -> u16 { + self.tester_address + } + + /// Returns the current lifecycle state. + #[must_use] + pub fn state(&self) -> SessionState { + self.state + } } +/// Thread-safe registry of active `DoIP` sessions. +/// +/// Internally uses `parking_lot::RwLock` maps keyed by session ID and +/// remote [`SocketAddr`]. Access this via the [`Arc`] returned by +/// [`SessionManager::new`]. #[derive(Debug, Default)] pub struct SessionManager { sessions: RwLock>, @@ -65,11 +105,13 @@ pub struct SessionManager { } impl SessionManager { + /// Create a new `SessionManager` wrapped in an [`Arc`] for shared ownership across tasks. #[must_use] pub fn new() -> Arc { Arc::new(Self::default()) } + /// Register a new session for `peer_addr` and return it. pub fn create_session(&self, peer_addr: SocketAddr) -> Session { let mut next_id = self.next_id.write(); let id = *next_id; @@ -83,15 +125,18 @@ impl SessionManager { session } + /// Look up a session by its numeric ID. Returns `None` if not found. pub fn get_session(&self, id: u64) -> Option { self.sessions.read().get(&id).cloned() } + /// Look up a session by the tester's remote address. Returns `None` if not found. pub fn get_session_by_addr(&self, addr: &SocketAddr) -> Option { let id = self.addr_to_session.read().get(addr).copied()?; self.get_session(id) } + /// Apply a mutation `f` to the session with the given `id`. Returns `true` if found. pub fn update_session(&self, id: u64, f: F) -> bool where F: FnOnce(&mut Session), @@ -104,6 +149,7 @@ impl SessionManager { } } + /// Remove and return the session with the given `id`, or `None` if not found. pub fn remove_session(&self, id: u64) -> Option { let session = self.sessions.write().remove(&id)?; self.addr_to_session.write().remove(&session.peer_addr); @@ -111,6 +157,7 @@ impl SessionManager { Some(session) } + /// Remove and return the session associated with `addr`, or `None` if not found. pub fn remove_session_by_addr(&self, addr: &SocketAddr) -> Option { let id = self.addr_to_session.write().remove(addr)?; let session = self.sessions.write().remove(&id)?; @@ -118,10 +165,12 @@ impl SessionManager { Some(session) } + /// Returns the number of currently registered sessions. pub fn session_count(&self) -> usize { self.sessions.read().len() } + /// Returns `true` if any active session has `tester_address` registered with routing active. pub fn is_tester_registered(&self, tester_address: u16) -> bool { self.sessions .read() @@ -140,10 +189,10 @@ mod tests { let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); let session = mgr.create_session(addr); - assert_eq!(session.state, SessionState::Connected); + assert_eq!(session.state(), SessionState::Connected); - let retrieved = mgr.get_session(session.id).unwrap(); - assert_eq!(retrieved.peer_addr, addr); + let retrieved = mgr.get_session(session.id()).unwrap(); + assert_eq!(retrieved.peer_addr(), addr); } #[test] @@ -152,11 +201,11 @@ mod tests { let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); let session = mgr.create_session(addr); - mgr.update_session(session.id, |s| s.activate_routing(0x0E80)); + mgr.update_session(session.id(), |s| s.activate_routing(0x0E80)); - let updated = mgr.get_session(session.id).unwrap(); + let updated = mgr.get_session(session.id()).unwrap(); assert!(updated.is_routing_active()); - assert_eq!(updated.tester_address, 0x0E80); + assert_eq!(updated.tester_address(), 0x0E80); } #[test] @@ -167,9 +216,9 @@ mod tests { let session = mgr.create_session(addr); assert_eq!(mgr.session_count(), 1); - mgr.remove_session(session.id); + mgr.remove_session(session.id()); assert_eq!(mgr.session_count(), 0); - assert!(mgr.get_session(session.id).is_none()); + assert!(mgr.get_session(session.id()).is_none()); } #[test] @@ -180,7 +229,7 @@ mod tests { let session = mgr.create_session(addr); assert!(!mgr.is_tester_registered(0x0E80)); - mgr.update_session(session.id, |s| s.activate_routing(0x0E80)); + mgr.update_session(session.id(), |s| s.activate_routing(0x0E80)); assert!(mgr.is_tester_registered(0x0E80)); } } diff --git a/src/uds/handler.rs b/src/uds/handler.rs index 928f034..440aa66 100644 --- a/src/uds/handler.rs +++ b/src/uds/handler.rs @@ -38,15 +38,19 @@ pub mod service_id { pub const CLEAR_DTC_INFORMATION: u8 = 0x14; } -/// UDS request extracted from `DoIP` diagnostic message +/// UDS request extracted from a `DoIP` diagnostic message (ISO 14229-1:2020). #[derive(Debug, Clone)] pub struct UdsRequest { - pub source_address: u16, - pub target_address: u16, - pub payload: Bytes, + /// Logical address of the tester sending the request + source_address: u16, + /// Logical address of the target ECU + target_address: u16, + /// Raw UDS payload bytes (service ID + data) + payload: Bytes, } impl UdsRequest { + /// Construct a new `UdsRequest` from address pair and raw UDS payload bytes. #[must_use] pub fn new(source: u16, target: u16, payload: Bytes) -> Self { Self { @@ -61,17 +65,39 @@ impl UdsRequest { pub fn service_id(&self) -> Option { self.payload.first().copied() } + + /// Returns the tester's logical source address. + #[must_use] + pub fn source_address(&self) -> u16 { + self.source_address + } + + /// Returns the target ECU's logical address. + #[must_use] + pub fn target_address(&self) -> u16 { + self.target_address + } + + /// Returns the raw UDS payload bytes. + #[must_use] + pub fn payload(&self) -> &Bytes { + &self.payload + } } -/// UDS response to be wrapped in `DoIP` diagnostic message +/// UDS response to be wrapped in a `DoIP` diagnostic message. #[derive(Debug, Clone)] pub struct UdsResponse { - pub source_address: u16, - pub target_address: u16, - pub payload: Bytes, + /// Logical address of the ECU sending the response + source_address: u16, + /// Logical address of the tester the response is directed to + target_address: u16, + /// Raw UDS response payload bytes (positive or negative response) + payload: Bytes, } impl UdsResponse { + /// Construct a new `UdsResponse` from address pair and raw UDS payload bytes. #[must_use] pub fn new(source: u16, target: u16, payload: Bytes) -> Self { Self { @@ -80,6 +106,24 @@ impl UdsResponse { payload, } } + + /// Returns the ECU's logical source address. + #[must_use] + pub fn source_address(&self) -> u16 { + self.source_address + } + + /// Returns the tester's logical target address. + #[must_use] + pub fn target_address(&self) -> u16 { + self.target_address + } + + /// Returns the raw UDS response payload bytes. + #[must_use] + pub fn payload(&self) -> &Bytes { + &self.payload + } } /// Trait for handling UDS requests @@ -87,6 +131,11 @@ impl UdsResponse { /// Implement this trait to connect the `DoIP` server to a UDS backend /// (e.g., UDS2SOVD converter, ODX/MDD handler, or ECU simulator) pub trait UdsHandler: Send + Sync { + /// Process a UDS `request` and return the corresponding response. + /// + /// Called by the `DoIP` server for every inbound diagnostic message after + /// routing activation. The implementation should decode the UDS service ID, + /// execute the requested service, and return a positive or negative response. fn handle(&self, request: UdsRequest) -> UdsResponse; } diff --git a/src/uds/mod.rs b/src/uds/mod.rs index 8209c1c..e370952 100644 --- a/src/uds/mod.rs +++ b/src/uds/mod.rs @@ -15,10 +15,6 @@ //! //! Provides the interface between `DoIP` transport and UDS processing. -// #[cfg(any(test, feature = "test-handlers"))] -// pub mod dummy_handler; pub mod handler; -// #[cfg(any(test, feature = "test-handlers"))] -// pub mod stub_handler; -pub use handler::{service_id, UdsHandler, UdsRequest, UdsResponse}; +pub use handler::{UdsHandler, UdsRequest, UdsResponse, service_id}; From 7cbed82edfa339e59f9b3c8e445e8c28d15cc724 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Fri, 13 Mar 2026 07:42:19 +0530 Subject: [PATCH 08/10] refactor: fix PR review Replace std::result::Result<_, DoipError> with crate::DoipResult across all files; add pub use error::DoipResult in lib.rs - remove redundant AckCode enum - remove redundant ack_code field from PositiveAck - merge PositiveAck + NegativeAck into DiagnosticAck with AckResult enum (Positive | Negative(NackCode)) --- src/doip/alive_check.rs | 21 +-- src/doip/diagnostic_message.rs | 305 ++++++++++++--------------------- src/doip/header.rs | 2 +- src/doip/mod.rs | 6 +- src/doip/payload.rs | 14 +- src/doip/routing_activation.rs | 36 +--- src/doip/vehicle_id.rs | 30 +--- src/error.rs | 44 +---- src/lib.rs | 1 + src/server/config.rs | 15 +- 10 files changed, 149 insertions(+), 325 deletions(-) diff --git a/src/doip/alive_check.rs b/src/doip/alive_check.rs index 5cd1045..fae49a6 100644 --- a/src/doip/alive_check.rs +++ b/src/doip/alive_check.rs @@ -12,10 +12,8 @@ */ //! Alive Check handlers (ISO 13400-2) -use super::{DoipParseable, DoipSerializable}; -use crate::DoipError; +use super::{DoipParseable, DoipSerializable, parse_fixed_slice}; use bytes::{BufMut, BytesMut}; -use tracing::warn; // Alive Check Request (0x0007) - no payload // Server sends this to check if tester is still connected @@ -23,7 +21,7 @@ use tracing::warn; pub struct Request; impl DoipParseable for Request { - fn parse(_payload: &[u8]) -> std::result::Result { + fn parse(_payload: &[u8]) -> crate::DoipResult { Ok(Self) } } @@ -44,19 +42,8 @@ pub struct Response { } impl DoipParseable for Response { - fn parse(payload: &[u8]) -> std::result::Result { - let bytes: [u8; 2] = payload - .get(..Self::LEN) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| { - let e = DoipError::PayloadTooShort { - expected: Self::LEN, - actual: payload.len(), - }; - warn!("AliveCheck Response parse failed: {}", e); - e - })?; - + fn parse(payload: &[u8]) -> crate::DoipResult { + let bytes: [u8; 2] = parse_fixed_slice(payload, "AliveCheck Response")?; let source_address = u16::from_be_bytes(bytes); Ok(Self { source_address }) } diff --git a/src/doip/diagnostic_message.rs b/src/doip/diagnostic_message.rs index fde3c8b..7d615b0 100644 --- a/src/doip/diagnostic_message.rs +++ b/src/doip/diagnostic_message.rs @@ -14,7 +14,7 @@ use super::{DoipParseable, DoipSerializable, parse_fixed_slice, too_short}; use crate::DoipError; -use bytes::{Buf, BufMut, Bytes, BytesMut}; +use bytes::{BufMut, Bytes, BytesMut}; use tracing::warn; const ADDRESS_BYTES: usize = 2; @@ -22,20 +22,18 @@ const HEADER_BYTES: usize = ADDRESS_BYTES * 2; const ACK_CODE_BYTES: usize = 1; const MIN_USER_DATA_BYTES: usize = 1; -// Diagnostic message positive ack codes per ISO 13400-2:2019 Table 27 +/// Outcome of a diagnostic message acknowledgment (ISO 13400-2:2019). +/// +/// Used by [`DiagnosticAck`] to represent either a positive or negative result. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum AckCode { - Acknowledged = 0x00, +pub enum AckResult { + /// Positive acknowledgment — message was accepted (wire code 0x00). + Positive, + /// Negative acknowledgment — message was rejected with the given code. + Negative(NackCode), } -impl From for u8 { - fn from(code: AckCode) -> u8 { - code as u8 - } -} - -// Diagnostic message negative ack codes per ISO 13400-2:2019 Table 28 +/// Diagnostic message negative acknowledgment codes per ISO 13400-2:2019 Table 28. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum NackCode { @@ -118,147 +116,130 @@ impl Message { &self.user_data } - /// Parse a Diagnostic Message from a mutable Bytes buffer - /// - /// # Errors - /// Returns [`DoipError::PayloadTooShort`] if buffer is less than 5 bytes - /// Returns [`DoipError::EmptyUserData`] if no UDS data is present - pub fn parse_buf(buf: &mut Bytes) -> std::result::Result { - let msg = ::parse(buf)?; - buf.advance(buf.len()); - Ok(msg) - } - pub fn service_id(&self) -> Option { self.user_data.first().copied() } } -/// Diagnostic Message Positive Acknowledgment (message type 0x8002) +/// Diagnostic Message Acknowledgment (payload types 0x8002 and 0x8003) /// -/// Sent by a `DoIP` entity to acknowledge receipt of a diagnostic message. +/// Represents both positive and negative acknowledgments as defined in +/// ISO 13400-2:2019. Use [`AckResult`] to distinguish the outcome. /// -/// # Wire Format -/// Payload: SA(2) + TA(2) + `ack_code(1)` + optional `previous_diag_data` +/// # Wire Format +/// Payload: SA(2) + TA(2) + code(1) + optional `previous_diag_data` #[derive(Debug, Clone, PartialEq, Eq)] -pub struct PositiveAck { +pub struct DiagnosticAck { source_address: u16, target_address: u16, - ack_code: AckCode, + result: AckResult, previous_data: Option, } -impl PositiveAck { - /// Minimum positive ack length in bytes +impl DiagnosticAck { + /// Minimum ack length in bytes (SA + TA + code byte). pub const MIN_LEN: usize = HEADER_BYTES + ACK_CODE_BYTES; - /// Create a new positive acknowledgment + /// Create a positive acknowledgment. #[must_use] - pub fn new(source: u16, target: u16) -> Self { + pub fn positive(source: u16, target: u16) -> Self { Self { source_address: source, target_address: target, - ack_code: AckCode::Acknowledged, + result: AckResult::Positive, previous_data: None, } } - /// Create a positive acknowledgment with previous diagnostic data - pub fn with_previous_data(source: u16, target: u16, data: Bytes) -> Self { + /// Create a negative acknowledgment. + #[must_use] + pub fn negative(source: u16, target: u16, code: NackCode) -> Self { Self { source_address: source, target_address: target, - ack_code: AckCode::Acknowledged, - // ISO 13400-2:2019: previous diagnostic data is optional; treat empty as absent - previous_data: if data.is_empty() { None } else { Some(data) }, + result: AckResult::Negative(code), + previous_data: None, } } - /// Get the source address + /// Returns the source address. + #[must_use] pub fn source_address(&self) -> u16 { self.source_address } - /// Get the target address + /// Returns the target address. + #[must_use] pub fn target_address(&self) -> u16 { self.target_address } - /// Get the acknowledgment code - pub fn ack_code(&self) -> AckCode { - self.ack_code + /// Returns the acknowledgment result. + #[must_use] + pub fn result(&self) -> AckResult { + self.result } - /// Get the previous diagnostic data + /// Returns the previous diagnostic data, if any. + #[must_use] pub fn previous_data(&self) -> Option<&Bytes> { self.previous_data.as_ref() } -} - -/// Diagnostic Message Negative Acknowledgment (message type 0x8003) -/// -/// Sent by a `DoIP` entity to indicate rejection of a diagnostic message. -/// NACK codes are defined in ISO 13400-2:2019 Table 28. -/// -/// # Wire Format -/// Payload: SA(2) + TA(2) + `nack_code(1)` + optional `previous_diag_data` -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NegativeAck { - source_address: u16, - target_address: u16, - nack_code: NackCode, - previous_data: Option, -} - -impl NegativeAck { - /// Minimum negative ack length in bytes - pub const MIN_LEN: usize = HEADER_BYTES + ACK_CODE_BYTES; - - /// Create a new negative acknowledgment - #[must_use] - pub fn new(source: u16, target: u16, code: NackCode) -> Self { - Self { - source_address: source, - target_address: target, - nack_code: code, - previous_data: None, - } - } - - /// Get the source address - pub fn source_address(&self) -> u16 { - self.source_address - } - /// Get the target address - pub fn target_address(&self) -> u16 { - self.target_address - } - - /// Get the negative acknowledgment code - pub fn nack_code(&self) -> NackCode { - self.nack_code + /// Parse a positive acknowledgment payload (payload type 0x8002). + /// + /// # Errors + /// Returns [`DoipError::PayloadTooShort`] if payload is less than 4 bytes. + pub fn parse_positive(payload: &[u8]) -> crate::DoipResult { + let (source_address, target_address, previous_data) = + Self::parse_ack_header(payload, "DiagnosticPositiveAck")?; + Ok(Self { + source_address, + target_address, + result: AckResult::Positive, + previous_data, + }) } - /// Get the previous diagnostic data - pub fn previous_data(&self) -> Option<&Bytes> { - self.previous_data.as_ref() + /// Parse a negative acknowledgment payload (payload type 0x8003). + /// + /// # Errors + /// Returns [`DoipError::PayloadTooShort`] if payload is less than 5 bytes. + /// Returns [`DoipError::UnknownNackCode`] for unrecognized NACK codes. + pub fn parse_negative(payload: &[u8]) -> crate::DoipResult { + let (source_address, target_address, previous_data) = + Self::parse_ack_header(payload, "DiagnosticNegativeAck")?; + let nack_code = payload + .get(HEADER_BYTES) + .copied() + .ok_or_else(|| too_short(payload, Self::MIN_LEN)) + .and_then(NackCode::try_from)?; + Ok(Self { + source_address, + target_address, + result: AckResult::Negative(nack_code), + previous_data, + }) } - /// Create a negative acknowledgment with previous diagnostic data - pub fn with_previous_data(source: u16, target: u16, code: NackCode, data: Bytes) -> Self { - Self { - source_address: source, - target_address: target, - nack_code: code, - // ISO 13400-2:2019: previous diagnostic data is optional; treat empty as absent - previous_data: if data.is_empty() { None } else { Some(data) }, - } + /// Parse SA, TA and optional trailing `previous_data` from an ack payload. + fn parse_ack_header( + payload: &[u8], + context: &str, + ) -> crate::DoipResult<(u16, u16, Option)> { + let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, context)?; + let source_address = u16::from_be_bytes([header[0], header[1]]); + let target_address = u16::from_be_bytes([header[2], header[3]]); + let previous_data = payload + .get(Self::MIN_LEN..) + .filter(|d| !d.is_empty()) + .map(Bytes::copy_from_slice); + Ok((source_address, target_address, previous_data)) } } impl DoipParseable for Message { - fn parse(payload: &[u8]) -> std::result::Result { + fn parse(payload: &[u8]) -> crate::DoipResult { let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticMessage")?; let source_address = u16::from_be_bytes([header[0], header[1]]); @@ -298,29 +279,7 @@ impl DoipSerializable for Message { } } -impl DoipParseable for PositiveAck { - fn parse(payload: &[u8]) -> std::result::Result { - let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticPositiveAck")?; - - let source_address = u16::from_be_bytes([header[0], header[1]]); - let target_address = u16::from_be_bytes([header[2], header[3]]); - let ack_code = AckCode::Acknowledged; - - let previous_data = payload - .get(Self::MIN_LEN..) - .filter(|d| !d.is_empty()) - .map(Bytes::copy_from_slice); - - Ok(Self { - source_address, - target_address, - ack_code, - previous_data, - }) - } -} - -impl DoipSerializable for PositiveAck { +impl DoipSerializable for DiagnosticAck { fn serialized_len(&self) -> Option { Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, bytes::Bytes::len)) } @@ -328,48 +287,10 @@ impl DoipSerializable for PositiveAck { fn write_to(&self, buf: &mut BytesMut) { buf.put_u16(self.source_address); buf.put_u16(self.target_address); - buf.put_u8(u8::from(self.ack_code)); - if let Some(ref data) = self.previous_data { - buf.extend_from_slice(data); - } - } -} - -impl DoipParseable for NegativeAck { - fn parse(payload: &[u8]) -> std::result::Result { - let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticNegativeAck")?; - - let source_address = u16::from_be_bytes([header[0], header[1]]); - let target_address = u16::from_be_bytes([header[2], header[3]]); - let nack_code = payload - .get(HEADER_BYTES) - .copied() - .ok_or_else(|| too_short(payload, Self::MIN_LEN)) - .and_then(NackCode::try_from)?; - - let previous_data = payload - .get(Self::MIN_LEN..) - .filter(|d| !d.is_empty()) - .map(Bytes::copy_from_slice); - - Ok(Self { - source_address, - target_address, - nack_code, - previous_data, - }) - } -} - -impl DoipSerializable for NegativeAck { - fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, bytes::Bytes::len)) - } - - fn write_to(&self, buf: &mut BytesMut) { - buf.put_u16(self.source_address); - buf.put_u16(self.target_address); - buf.put_u8(u8::from(self.nack_code)); + buf.put_u8(match self.result { + AckResult::Positive => 0x00, + AckResult::Negative(code) => u8::from(code), + }); if let Some(ref data) = self.previous_data { buf.extend_from_slice(data); } @@ -429,38 +350,32 @@ mod tests { #[test] fn build_positive_ack() { - let ack = PositiveAck::new(0x1000, 0x0E80); - let bytes = ack.to_bytes(); - - assert_eq!(bytes.len(), PositiveAck::MIN_LEN); - assert_eq!(&bytes[..ADDRESS_BYTES], &[0x10, 0x00]); // source (ECU) - assert_eq!(&bytes[ADDRESS_BYTES..HEADER_BYTES], &[0x0E, 0x80]); // target (tester) - assert_eq!(bytes[HEADER_BYTES], 0x00); // ack code - } - - #[test] - fn build_positive_ack_with_prev_data() { - let prev = Bytes::from_static(&[0x22, 0xF1, 0x90]); - let expected_len = PositiveAck::MIN_LEN + prev.len(); - let ack = PositiveAck::with_previous_data(0x1000, 0x0E80, prev); + let ack = DiagnosticAck::positive(0x1000, 0x0E80); let bytes = ack.to_bytes(); - assert_eq!(bytes.len(), expected_len); - assert_eq!(&bytes[PositiveAck::MIN_LEN..], &[0x22, 0xF1, 0x90]); + assert_eq!(bytes.len(), DiagnosticAck::MIN_LEN); + assert_eq!(&bytes[..ADDRESS_BYTES], &[0x10, 0x00]); + assert_eq!(&bytes[ADDRESS_BYTES..HEADER_BYTES], &[0x0E, 0x80]); + assert_eq!(bytes[HEADER_BYTES], 0x00); // positive ack wire code + assert_eq!(ack.result(), AckResult::Positive); } #[test] fn build_negative_ack() { - let nack = NegativeAck::new(0x1000, 0x0E80, NackCode::UnknownTargetAddress); + let nack = DiagnosticAck::negative(0x1000, 0x0E80, NackCode::UnknownTargetAddress); let bytes = nack.to_bytes(); - assert_eq!(bytes.len(), NegativeAck::MIN_LEN); + assert_eq!(bytes.len(), DiagnosticAck::MIN_LEN); assert_eq!(bytes[HEADER_BYTES], 0x03); + assert_eq!( + nack.result(), + AckResult::Negative(NackCode::UnknownTargetAddress) + ); } #[test] fn build_negative_ack_target_unreachable() { - let nack = NegativeAck::new(0x1000, 0x0E80, NackCode::TargetUnreachable); + let nack = DiagnosticAck::negative(0x1000, 0x0E80, NackCode::TargetUnreachable); let bytes = nack.to_bytes(); assert_eq!(bytes[HEADER_BYTES], 0x06); } @@ -468,19 +383,23 @@ mod tests { #[test] fn parse_positive_ack() { let payload = [0x10, 0x00, 0x0E, 0x80, 0x00]; - let ack = PositiveAck::parse(&payload).unwrap(); + let ack = DiagnosticAck::parse_positive(&payload).unwrap(); assert_eq!(ack.source_address(), 0x1000); assert_eq!(ack.target_address(), 0x0E80); + assert_eq!(ack.result(), AckResult::Positive); assert!(ack.previous_data().is_none()); } #[test] fn parse_negative_ack() { let payload = [0x10, 0x00, 0x0E, 0x80, 0x03]; - let nack = NegativeAck::parse(&payload).unwrap(); + let nack = DiagnosticAck::parse_negative(&payload).unwrap(); - assert_eq!(nack.nack_code(), NackCode::UnknownTargetAddress); + assert_eq!( + nack.result(), + AckResult::Negative(NackCode::UnknownTargetAddress) + ); } #[test] @@ -493,17 +412,17 @@ mod tests { #[test] fn roundtrip_positive_ack() { - let original = PositiveAck::new(0x1000, 0x0E80); + let original = DiagnosticAck::positive(0x1000, 0x0E80); let bytes = original.to_bytes(); - let parsed = PositiveAck::parse(&bytes).unwrap(); + let parsed = DiagnosticAck::parse_positive(&bytes).unwrap(); assert_eq!(original, parsed); } #[test] fn roundtrip_negative_ack() { - let original = NegativeAck::new(0x1000, 0x0E80, NackCode::OutOfMemory); + let original = DiagnosticAck::negative(0x1000, 0x0E80, NackCode::OutOfMemory); let bytes = original.to_bytes(); - let parsed = NegativeAck::parse(&bytes).unwrap(); + let parsed = DiagnosticAck::parse_negative(&bytes).unwrap(); assert_eq!(original, parsed); } } diff --git a/src/doip/header.rs b/src/doip/header.rs index 884b6db..b1a4046 100644 --- a/src/doip/header.rs +++ b/src/doip/header.rs @@ -186,7 +186,7 @@ impl DoipHeader { /// /// # Errors /// Returns [`crate::DoipError::InvalidHeader`] if data is less than 8 bytes. - pub(crate) fn parse(data: &[u8]) -> std::result::Result { + pub(crate) fn parse(data: &[u8]) -> crate::DoipResult { let header: [u8; DOIP_HEADER_LENGTH] = data .get(..DOIP_HEADER_LENGTH) .and_then(|s| s.try_into().ok()) diff --git a/src/doip/mod.rs b/src/doip/mod.rs index d9ebdba..f4861fa 100644 --- a/src/doip/mod.rs +++ b/src/doip/mod.rs @@ -34,7 +34,7 @@ pub trait DoipParseable: Sized { /// /// # Errors /// Returns [`DoipError`] if the payload is malformed or too short. - fn parse(payload: &[u8]) -> std::result::Result; + fn parse(payload: &[u8]) -> crate::DoipResult; } /// Trait for `DoIP` message types that can be serialized to a [`Bytes`] buffer. @@ -76,7 +76,7 @@ pub(crate) fn too_short(payload: &[u8], expected: usize) -> DoipError { } /// Return `Err` if `payload` is shorter than `expected` bytes. -pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> std::result::Result<(), DoipError> { +pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> crate::DoipResult<()> { if payload.len() < expected { Err(too_short(payload, expected)) } else { @@ -91,7 +91,7 @@ pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> std::result::Res pub(crate) fn parse_fixed_slice( payload: &[u8], context: &str, -) -> std::result::Result<[u8; N], DoipError> { +) -> crate::DoipResult<[u8; N]> { payload .get(..N) .and_then(|s| s.try_into().ok()) diff --git a/src/doip/payload.rs b/src/doip/payload.rs index ba484e2..735121b 100644 --- a/src/doip/payload.rs +++ b/src/doip/payload.rs @@ -20,8 +20,8 @@ //! # Example //! ```no_run //! # use doip_server::doip::{DoipMessage, DoipPayload}; -//! # use doip_server::DoipError; -//! fn dispatch(msg: &DoipMessage) -> Result<(), DoipError> { +//! # use doip_server::DoipResult; +//! fn dispatch(msg: &DoipMessage) -> DoipResult<()> { //! match DoipPayload::parse(msg)? { //! DoipPayload::DiagnosticMessage(m) => println!("UDS payload: {:?}", m), //! DoipPayload::AliveCheckRequest(_) => println!("Alive check received"), @@ -54,9 +54,9 @@ pub enum DoipPayload { /// `0x8001` – Diagnostic Message (UDS data) DiagnosticMessage(diagnostic_message::Message), /// `0x8002` – Diagnostic Message Positive Acknowledgement - DiagnosticMessagePositiveAck(diagnostic_message::PositiveAck), + DiagnosticMessagePositiveAck(diagnostic_message::DiagnosticAck), /// `0x8003` – Diagnostic Message Negative Acknowledgement - DiagnosticMessageNegativeAck(diagnostic_message::NegativeAck), + DiagnosticMessageNegativeAck(diagnostic_message::DiagnosticAck), /// `0x0001` – Vehicle Identification Request (no filter) VehicleIdentificationRequest(vehicle_id::Request), /// `0x0002` – Vehicle Identification Request filtered by EID @@ -79,7 +79,7 @@ impl DoipPayload { /// /// Returns a more specific [`DoipError`] (e.g. [`DoipError::PayloadTooShort`]) /// when the payload bytes are present but malformed. - pub fn parse(msg: &DoipMessage) -> std::result::Result { + pub fn parse(msg: &DoipMessage) -> crate::DoipResult { let payload = msg.payload().as_ref(); let payload_type = msg @@ -103,10 +103,10 @@ impl DoipPayload { diagnostic_message::Message::parse(payload)?, )), PayloadType::DiagnosticMessagePositiveAck => Ok(Self::DiagnosticMessagePositiveAck( - diagnostic_message::PositiveAck::parse(payload)?, + diagnostic_message::DiagnosticAck::parse_positive(payload)?, )), PayloadType::DiagnosticMessageNegativeAck => Ok(Self::DiagnosticMessageNegativeAck( - diagnostic_message::NegativeAck::parse(payload)?, + diagnostic_message::DiagnosticAck::parse_negative(payload)?, )), PayloadType::VehicleIdentificationRequest => Ok(Self::VehicleIdentificationRequest( vehicle_id::Request::parse(payload)?, diff --git a/src/doip/routing_activation.rs b/src/doip/routing_activation.rs index e25b05b..5ba7133 100644 --- a/src/doip/routing_activation.rs +++ b/src/doip/routing_activation.rs @@ -12,9 +12,9 @@ */ //! Routing Activation handlers (ISO 13400-2:2019) -use super::{DoipParseable, DoipSerializable, check_min_len, parse_fixed_slice}; +use super::{DoipParseable, DoipSerializable, parse_fixed_slice}; use crate::DoipError; -use bytes::{Buf, BufMut, Bytes, BytesMut}; +use bytes::{BufMut, BytesMut}; use tracing::warn; /// Routing activation response codes per ISO 13400-2:2019 Table 25. @@ -141,34 +141,6 @@ impl Request { pub fn oem_specific(&self) -> Option { self.oem_specific } - - /// Parse routing activation request from buffer - /// - /// # Errors - /// - /// Returns an error if the buffer is too short or contains invalid data. - pub fn parse_buf(buf: &mut Bytes) -> std::result::Result { - check_min_len(buf.as_ref(), Self::MIN_LEN)?; - - let source_address = buf.get_u16(); - let activation_type = ActivationType::try_from(buf.get_u8()).map_err(|e| { - warn!("RoutingActivation Request parse_buf: {}", e); - e - })?; - let reserved = buf.get_u32(); - let oem_specific = if buf.remaining() >= 4 { - Some(buf.get_u32()) - } else { - None - }; - - Ok(Self { - source_address, - activation_type, - reserved, - oem_specific, - }) - } } // Routing Activation Response - 9 bytes min, 13 with OEM data @@ -247,7 +219,7 @@ impl Response { } impl DoipParseable for Request { - fn parse(payload: &[u8]) -> std::result::Result { + fn parse(payload: &[u8]) -> crate::DoipResult { let header: [u8; Self::MIN_LEN] = parse_fixed_slice(payload, "RoutingActivation Request")?; let source_address = u16::from_be_bytes([header[0], header[1]]); @@ -272,7 +244,7 @@ impl DoipParseable for Request { } impl DoipParseable for Response { - fn parse(payload: &[u8]) -> std::result::Result { + fn parse(payload: &[u8]) -> crate::DoipResult { let header: [u8; Self::MIN_LEN] = parse_fixed_slice(payload, "RoutingActivation Response")?; let tester_address = u16::from_be_bytes([header[0], header[1]]); diff --git a/src/doip/vehicle_id.rs b/src/doip/vehicle_id.rs index c52eb22..2cf7b40 100644 --- a/src/doip/vehicle_id.rs +++ b/src/doip/vehicle_id.rs @@ -13,7 +13,7 @@ //! Vehicle Identification handlers (ISO 13400-2:2019) -use super::{DoipParseable, DoipSerializable, check_min_len, too_short}; +use super::{DoipParseable, DoipSerializable, check_min_len, parse_fixed_slice, too_short}; use crate::DoipError; use bytes::{BufMut, BytesMut}; use tracing::warn; @@ -184,43 +184,27 @@ impl Response { } impl DoipParseable for Request { - fn parse(_payload: &[u8]) -> std::result::Result { + fn parse(_payload: &[u8]) -> crate::DoipResult { Ok(Self) } } impl DoipParseable for RequestWithEid { - fn parse(payload: &[u8]) -> std::result::Result { - let eid: [u8; 6] = payload - .get(..Self::LEN) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| { - let e = too_short(payload, Self::LEN); - warn!("VehicleId RequestWithEid parse failed: {}", e); - e - })?; - + fn parse(payload: &[u8]) -> crate::DoipResult { + let eid: [u8; 6] = parse_fixed_slice(payload, "VehicleId RequestWithEid")?; Ok(Self { eid }) } } impl DoipParseable for RequestWithVin { - fn parse(payload: &[u8]) -> std::result::Result { - let vin: [u8; 17] = payload - .get(..Self::LEN) - .and_then(|s| s.try_into().ok()) - .ok_or_else(|| { - let e = too_short(payload, Self::LEN); - warn!("VehicleId RequestWithVin parse failed: {}", e); - e - })?; - + fn parse(payload: &[u8]) -> crate::DoipResult { + let vin: [u8; 17] = parse_fixed_slice(payload, "VehicleId RequestWithVin")?; Ok(Self { vin }) } } impl DoipParseable for Response { - fn parse(payload: &[u8]) -> std::result::Result { + fn parse(payload: &[u8]) -> crate::DoipResult { if let Err(e) = check_min_len(payload, Self::MIN_LEN) { warn!("VehicleId Response parse failed: {}", e); return Err(e); diff --git a/src/error.rs b/src/error.rs index 616e042..68e5a62 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,36 +71,13 @@ pub enum DoipError { #[error("diagnostic message has no user data")] EmptyUserData, - - #[error("invalid VIN length: expected 17, got {0}")] - InvalidVinLength(usize), - - #[error("invalid EID length: expected 6, got {0}")] - InvalidEidLength(usize), - - #[error("Message too large: {size} bytes (max: {max})")] - MessageTooLarge { size: usize, max: usize }, - - #[error("Routing activation failed: {message}")] - RoutingActivationFailed { code: u8, message: String }, - - #[error("Session not found")] - SessionNotFound, - - #[error("Session closed")] - SessionClosed, - - #[error("Timeout: {0}")] - Timeout(String), - - #[error("UDS error: service 0x{service:02X}, NRC 0x{nrc:02X}")] - UdsError { service: u8, nrc: u8 }, } +#[cfg(test)] /// UDS Negative Response Codes (ISO 14229-1:2020) #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -pub enum UdsNrc { +enum UdsNrc { GeneralReject = 0x10, ServiceNotSupported = 0x11, SubFunctionNotSupported = 0x12, @@ -117,9 +94,10 @@ pub enum UdsNrc { ServiceNotSupportedInActiveSession = 0x7F, } +#[cfg(test)] impl UdsNrc { #[must_use] - pub const fn as_u8(self) -> u8 { + const fn as_u8(self) -> u8 { self as u8 } } @@ -222,26 +200,12 @@ mod tests { }, DoipError::InvalidHeader("bad header".to_string()), DoipError::UnknownPayloadType(0x1234), - DoipError::MessageTooLarge { size: 10, max: 1 }, - DoipError::RoutingActivationFailed { - code: 0x00, - message: "fail".to_string(), - }, - DoipError::SessionNotFound, - DoipError::SessionClosed, - DoipError::Timeout("timeout".to_string()), - DoipError::UdsError { - service: 0x10, - nrc: 0x11, - }, DoipError::PayloadTooShort { expected: 8, actual: 4, }, DoipError::UnknownRoutingActivationResponseCode(0x99), DoipError::EmptyUserData, - DoipError::InvalidVinLength(10), - DoipError::InvalidEidLength(4), ]; for err in errors { diff --git a/src/lib.rs b/src/lib.rs index d888778..a2e3cfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,3 +15,4 @@ pub mod error; pub mod server; pub mod uds; pub use error::DoipError; +pub use error::DoipResult; diff --git a/src/server/config.rs b/src/server/config.rs index f7887ac..644bcec 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -196,7 +196,7 @@ impl ServerConfig { /// Returns [`DoipError::ConfigFileError`] if file cannot be read or parsed. /// Returns [`DoipError::InvalidConfig`] if values are invalid. /// Returns [`DoipError::InvalidAddress`] if bind address is malformed. - pub fn from_file>(path: P) -> std::result::Result { + pub fn from_file>(path: P) -> crate::DoipResult { let content = std::fs::read_to_string(path).map_err(|e| DoipError::ConfigFileError(e.to_string()))?; let file: ConfigFile = @@ -234,7 +234,7 @@ impl ServerConfig { }) } - fn parse_vin(s: &str) -> std::result::Result<[u8; 17], DoipError> { + fn parse_vin(s: &str) -> crate::DoipResult<[u8; 17]> { let bytes = s.as_bytes(); if bytes.len() != 17 { return Err(DoipError::InvalidConfig(format!( @@ -247,7 +247,7 @@ impl ServerConfig { Ok(vin) } - fn parse_hex_array(s: &str) -> std::result::Result<[u8; N], DoipError> { + fn parse_hex_array(s: &str) -> crate::DoipResult<[u8; N]> { let s = s.trim_start_matches("0x").replace([':', '-', ' '], ""); let bytes = hex::decode(&s).map_err(|e| DoipError::HexDecodeError(e.to_string()))?; if bytes.len() != N { @@ -378,23 +378,20 @@ mod tests { #[test] fn test_parse_hex_array_valid() { - let result: std::result::Result<[u8; 6], DoipError> = - ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); + let result: crate::DoipResult<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); assert!(result.is_ok()); assert_eq!(result.unwrap(), [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]); } #[test] fn test_parse_hex_array_with_0x_prefix() { - let result: std::result::Result<[u8; 6], DoipError> = - ServerConfig::parse_hex_array("0x001A2B3C4D5E"); + let result: crate::DoipResult<[u8; 6]> = ServerConfig::parse_hex_array("0x001A2B3C4D5E"); assert!(result.is_ok()); } #[test] fn test_parse_hex_array_invalid_length() { - let result: std::result::Result<[u8; 6], DoipError> = - ServerConfig::parse_hex_array("00:1A:2B"); + let result: crate::DoipResult<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B"); assert!(result.is_err()); } From 61798f016cf477109e8540181f4c5425b1a0826e Mon Sep 17 00:00:00 2001 From: vinayrs Date: Tue, 17 Mar 2026 11:43:09 +0530 Subject: [PATCH 09/10] chore: move doip-server into Cargo workspace to resolve merge conflict --- Cargo.lock | 56 +++++++++---------- doip-server/Cargo.toml | 40 +++++++++++++ {src => doip-server/src}/doip/alive_check.rs | 0 {src => doip-server/src}/doip/codec.rs | 0 .../src}/doip/diagnostic_message.rs | 0 {src => doip-server/src}/doip/header.rs | 0 {src => doip-server/src}/doip/mod.rs | 0 {src => doip-server/src}/doip/payload.rs | 0 .../src}/doip/routing_activation.rs | 0 {src => doip-server/src}/doip/vehicle_id.rs | 0 {src => doip-server/src}/error.rs | 0 {src => doip-server/src}/lib.rs | 0 {src => doip-server/src}/server/config.rs | 0 {src => doip-server/src}/server/mod.rs | 0 {src => doip-server/src}/server/session.rs | 0 {src => doip-server/src}/uds/handler.rs | 0 {src => doip-server/src}/uds/mod.rs | 0 17 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 doip-server/Cargo.toml rename {src => doip-server/src}/doip/alive_check.rs (100%) rename {src => doip-server/src}/doip/codec.rs (100%) rename {src => doip-server/src}/doip/diagnostic_message.rs (100%) rename {src => doip-server/src}/doip/header.rs (100%) rename {src => doip-server/src}/doip/mod.rs (100%) rename {src => doip-server/src}/doip/payload.rs (100%) rename {src => doip-server/src}/doip/routing_activation.rs (100%) rename {src => doip-server/src}/doip/vehicle_id.rs (100%) rename {src => doip-server/src}/error.rs (100%) rename {src => doip-server/src}/lib.rs (100%) rename {src => doip-server/src}/server/config.rs (100%) rename {src => doip-server/src}/server/mod.rs (100%) rename {src => doip-server/src}/server/session.rs (100%) rename {src => doip-server/src}/uds/handler.rs (100%) rename {src => doip-server/src}/uds/mod.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 6117b7f..f382651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,15 +4,15 @@ version = 4 [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cfg-if" @@ -37,15 +37,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "hex" @@ -55,9 +55,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "lock_api" @@ -70,9 +70,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot" @@ -99,9 +99,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "proc-macro2" @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -183,9 +183,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -214,9 +214,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "pin-project-lite", @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "serde_core", "serde_spanned", @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "windows-link" @@ -299,6 +299,6 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" diff --git a/doip-server/Cargo.toml b/doip-server/Cargo.toml new file mode 100644 index 0000000..72c7a9d --- /dev/null +++ b/doip-server/Cargo.toml @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 + +[package] +name = "doip-server" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +# ---- async runtime ---- +[dependencies] +tokio = { version = "1", default-features = false, features = ["io-util"] } +tokio-util = { version = "0.7", default-features = false, features = ["codec"] } + +# ---- serialization ---- +serde = { version = "1.0", default-features = false, features = ["std", "derive"] } +toml = { version = "0.9", default-features = false, features = ["parse", "serde"] } + +# ---- tracing & logging ---- +tracing = { version = "0.1", default-features = false, features = ["std"] } + +# ---- error handling ---- +thiserror = { version = "2.0", default-features = false, features = ["std"] } + +# ---- data structures ---- +bytes = { version = "1.0", default-features = false, features = ["std"] } +parking_lot = { version = "0.12", default-features = false } + +# ---- utility crates ---- +hex = { version = "0.4", default-features = false, features = ["std"] } + +[lints] +workspace = true diff --git a/src/doip/alive_check.rs b/doip-server/src/doip/alive_check.rs similarity index 100% rename from src/doip/alive_check.rs rename to doip-server/src/doip/alive_check.rs diff --git a/src/doip/codec.rs b/doip-server/src/doip/codec.rs similarity index 100% rename from src/doip/codec.rs rename to doip-server/src/doip/codec.rs diff --git a/src/doip/diagnostic_message.rs b/doip-server/src/doip/diagnostic_message.rs similarity index 100% rename from src/doip/diagnostic_message.rs rename to doip-server/src/doip/diagnostic_message.rs diff --git a/src/doip/header.rs b/doip-server/src/doip/header.rs similarity index 100% rename from src/doip/header.rs rename to doip-server/src/doip/header.rs diff --git a/src/doip/mod.rs b/doip-server/src/doip/mod.rs similarity index 100% rename from src/doip/mod.rs rename to doip-server/src/doip/mod.rs diff --git a/src/doip/payload.rs b/doip-server/src/doip/payload.rs similarity index 100% rename from src/doip/payload.rs rename to doip-server/src/doip/payload.rs diff --git a/src/doip/routing_activation.rs b/doip-server/src/doip/routing_activation.rs similarity index 100% rename from src/doip/routing_activation.rs rename to doip-server/src/doip/routing_activation.rs diff --git a/src/doip/vehicle_id.rs b/doip-server/src/doip/vehicle_id.rs similarity index 100% rename from src/doip/vehicle_id.rs rename to doip-server/src/doip/vehicle_id.rs diff --git a/src/error.rs b/doip-server/src/error.rs similarity index 100% rename from src/error.rs rename to doip-server/src/error.rs diff --git a/src/lib.rs b/doip-server/src/lib.rs similarity index 100% rename from src/lib.rs rename to doip-server/src/lib.rs diff --git a/src/server/config.rs b/doip-server/src/server/config.rs similarity index 100% rename from src/server/config.rs rename to doip-server/src/server/config.rs diff --git a/src/server/mod.rs b/doip-server/src/server/mod.rs similarity index 100% rename from src/server/mod.rs rename to doip-server/src/server/mod.rs diff --git a/src/server/session.rs b/doip-server/src/server/session.rs similarity index 100% rename from src/server/session.rs rename to doip-server/src/server/session.rs diff --git a/src/uds/handler.rs b/doip-server/src/uds/handler.rs similarity index 100% rename from src/uds/handler.rs rename to doip-server/src/uds/handler.rs diff --git a/src/uds/mod.rs b/doip-server/src/uds/mod.rs similarity index 100% rename from src/uds/mod.rs rename to doip-server/src/uds/mod.rs From d2f96e911be4c236de2278363c9ee812f9f9fc99 Mon Sep 17 00:00:00 2001 From: vinayrs Date: Fri, 20 Mar 2026 15:03:05 +0530 Subject: [PATCH 10/10] fix(review): address review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - structured tracing logging; error! for parse failures - rename DoipResult → Result - single RwLock + AtomicU64 - merge DiagnosticAck + AckResult; remove AckCode - doc comments on all public API items --- .github/workflows/pr-checks.yml | 4 +- .github/workflows/pre-commit.yaml | 2 +- .gitignore | 3 + deny.toml | 4 +- doip-server/Cargo.toml | 10 ++- doip-server/src/doip/alive_check.rs | 28 +++++--- doip-server/src/doip/codec.rs | 15 ++-- doip-server/src/doip/diagnostic_message.rs | 32 +++++---- doip-server/src/doip/header.rs | 61 ++++++++++------ doip-server/src/doip/mod.rs | 51 +++++++------ doip-server/src/doip/payload.rs | 9 +-- doip-server/src/doip/routing_activation.rs | 36 +++++++--- doip-server/src/doip/vehicle_id.rs | 71 +++++++++++++----- doip-server/src/error.rs | 11 +-- doip-server/src/lib.rs | 7 +- doip-server/src/server/config.rs | 21 +++--- doip-server/src/server/mod.rs | 2 + doip-server/src/server/session.rs | 84 +++++++++++++--------- doip-server/src/uds/handler.rs | 18 ++++- doip-server/src/uds/mod.rs | 1 + 20 files changed, 314 insertions(+), 156 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 8584da1..232610d 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -35,7 +35,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Format and Clippy - Nightly toolchain pinned - uses: eclipse-opensovd/cicd-workflows/rust-lint-and-format-action@41c531349e9d7e7852455c35682cd200519fb81b + uses: eclipse-opensovd/cicd-workflows/rust-lint-and-format-action@fef5cc7b2d4c4c6593b105402fbf4edd54db7d5a with: toolchain: nightly-2025-07-14 github-token: ${{ secrets.GITHUB_TOKEN }} @@ -46,7 +46,7 @@ jobs: - name: Format and Clippy - Nightly toolchain latest id: nightly-clippy continue-on-error: true - uses: eclipse-opensovd/cicd-workflows/rust-lint-and-format-action@41c531349e9d7e7852455c35682cd200519fb81b + uses: eclipse-opensovd/cicd-workflows/rust-lint-and-format-action@fef5cc7b2d4c4c6593b105402fbf4edd54db7d5a with: toolchain: nightly github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 05725da..9c4cd8b 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -33,4 +33,4 @@ jobs: runs-on: ubuntu-latest steps: - name: Run checks - uses: eclipse-opensovd/cicd-workflows/pre-commit-action@41c531349e9d7e7852455c35682cd200519fb81b + uses: eclipse-opensovd/cicd-workflows/pre-commit-action@fef5cc7b2d4c4c6593b105402fbf4edd54db7d5a diff --git a/.gitignore b/.gitignore index b31a6b4..3c5442b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) + .env .cache .gradle diff --git a/deny.toml b/deny.toml index 10e3516..992f9f0 100644 --- a/deny.toml +++ b/deny.toml @@ -32,7 +32,9 @@ ignore = [] # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. -allow = ["Apache-2.0", "MIT"] +# Unicode-3.0 is required by `unicode-ident` (transitive dep of serde/thiserror) +# and is an OSI-approved license covering Unicode data tables. +allow = ["Apache-2.0", "MIT", "Unicode-3.0"] confidence-threshold = 0.9 exceptions = [] diff --git a/doip-server/Cargo.toml b/doip-server/Cargo.toml index 72c7a9d..4c91f5f 100644 --- a/doip-server/Cargo.toml +++ b/doip-server/Cargo.toml @@ -20,8 +20,14 @@ tokio = { version = "1", default-features = false, features = ["io-util"] } tokio-util = { version = "0.7", default-features = false, features = ["codec"] } # ---- serialization ---- -serde = { version = "1.0", default-features = false, features = ["std", "derive"] } -toml = { version = "0.9", default-features = false, features = ["parse", "serde"] } +serde = { version = "1.0", default-features = false, features = [ + "std", + "derive", +] } +toml = { version = "0.9", default-features = false, features = [ + "parse", + "serde", +] } # ---- tracing & logging ---- tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/doip-server/src/doip/alive_check.rs b/doip-server/src/doip/alive_check.rs index fae49a6..5fc9162 100644 --- a/doip-server/src/doip/alive_check.rs +++ b/doip-server/src/doip/alive_check.rs @@ -12,16 +12,19 @@ */ //! Alive Check handlers (ISO 13400-2) -use super::{DoipParseable, DoipSerializable, parse_fixed_slice}; use bytes::{BufMut, BytesMut}; -// Alive Check Request (0x0007) - no payload -// Server sends this to check if tester is still connected +use super::{DoipParseable, DoipSerializable, parse_fixed_slice}; + +/// Alive Check Request (payload type `0x0007`) – sent by the `DoIP` entity to verify +/// a tester is still connected. +/// +/// This message carries no payload (zero-length body per ISO 13400-2:2019 §7.6). #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Request; impl DoipParseable for Request { - fn parse(_payload: &[u8]) -> crate::DoipResult { + fn parse(_payload: &[u8]) -> crate::Result { Ok(Self) } } @@ -31,18 +34,25 @@ impl DoipSerializable for Request { Some(0) } - fn write_to(&self, _buf: &mut BytesMut) {} + /// Alive Check Request carries no payload bytes per ISO 13400-2; + /// nothing is written to the buffer by design. + fn write_to(&self, _buf: &mut BytesMut) { + // Intentionally empty: Alive Check Request has a zero-length payload. + } } -// Alive Check Response (0x0008) - 2 byte source address -// Tester responds with its logical address +/// Alive Check Response (payload type `0x0008`) – sent by the tester in reply, +/// carrying its logical address. +/// +/// # Wire Format +/// Payload: `source_address` (2 bytes, big-endian) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { source_address: u16, } impl DoipParseable for Response { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { let bytes: [u8; 2] = parse_fixed_slice(payload, "AliveCheck Response")?; let source_address = u16::from_be_bytes(bytes); Ok(Self { source_address }) @@ -60,8 +70,10 @@ impl DoipSerializable for Response { } impl Response { + /// Fixed wire-format length of the Alive Check Response payload (2-byte source address). pub const LEN: usize = 2; + /// Create a new Alive Check Response with the given tester source address. #[must_use] pub fn new(source_address: u16) -> Self { Self { source_address } diff --git a/doip-server/src/doip/codec.rs b/doip-server/src/doip/codec.rs index dd5f748..afef9c7 100644 --- a/doip-server/src/doip/codec.rs +++ b/doip-server/src/doip/codec.rs @@ -22,8 +22,9 @@ //! //! See [`header`](super::header) for the underlying type definitions. -use bytes::BytesMut; use std::io; + +use bytes::BytesMut; use tokio_util::codec::{Decoder, Encoder}; use tracing::{debug, warn}; @@ -51,6 +52,7 @@ pub struct DoipCodec { } impl DoipCodec { + /// Create a new `DoipCodec` with the default maximum payload size. #[must_use] pub fn new() -> Self { Self { @@ -99,15 +101,16 @@ impl Decoder for DoipCodec { let header_slice = src.get(..DOIP_HEADER_LENGTH).ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidData, "buffer too short") })?; - debug!("Received raw header bytes: {:02X?}", header_slice); + debug!(header_bytes = ?header_slice, "Received raw header bytes"); let header = DoipHeader::parse(header_slice) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; if let Some(nack_code) = header.validate() { warn!( - "Header validation failed: {:?} - raw bytes: {:02X?}", - nack_code, header_slice + nack_code = ?nack_code, + header_bytes = ?header_slice, + "Header validation failed" ); return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -147,7 +150,9 @@ impl Decoder for DoipCodec { } let _ = src.split_to(DOIP_HEADER_LENGTH); - let payload = src.split_to(total_len - DOIP_HEADER_LENGTH).freeze(); + let payload = src + .split_to(total_len.saturating_sub(DOIP_HEADER_LENGTH)) + .freeze(); self.state = DecodeState::Header; return Ok(Some(DoipMessage { header, payload })); diff --git a/doip-server/src/doip/diagnostic_message.rs b/doip-server/src/doip/diagnostic_message.rs index 7d615b0..84b2e64 100644 --- a/doip-server/src/doip/diagnostic_message.rs +++ b/doip-server/src/doip/diagnostic_message.rs @@ -12,10 +12,11 @@ */ //! Diagnostic Message handlers (ISO 13400-2:2019) +use bytes::{BufMut, Bytes, BytesMut}; +use tracing::error; + use super::{DoipParseable, DoipSerializable, parse_fixed_slice, too_short}; use crate::DoipError; -use bytes::{BufMut, Bytes, BytesMut}; -use tracing::warn; const ADDRESS_BYTES: usize = 2; const HEADER_BYTES: usize = ADDRESS_BYTES * 2; @@ -106,7 +107,7 @@ impl Message { self.source_address } - /// Get the target address + /// Get the target address pub fn target_address(&self) -> u16 { self.target_address } @@ -116,6 +117,7 @@ impl Message { &self.user_data } + /// Returns the UDS service ID (first byte of user data), or `None` if the payload is empty. pub fn service_id(&self) -> Option { self.user_data.first().copied() } @@ -190,7 +192,7 @@ impl DiagnosticAck { /// /// # Errors /// Returns [`DoipError::PayloadTooShort`] if payload is less than 4 bytes. - pub fn parse_positive(payload: &[u8]) -> crate::DoipResult { + pub fn parse_positive(payload: &[u8]) -> crate::Result { let (source_address, target_address, previous_data) = Self::parse_ack_header(payload, "DiagnosticPositiveAck")?; Ok(Self { @@ -206,7 +208,7 @@ impl DiagnosticAck { /// # Errors /// Returns [`DoipError::PayloadTooShort`] if payload is less than 5 bytes. /// Returns [`DoipError::UnknownNackCode`] for unrecognized NACK codes. - pub fn parse_negative(payload: &[u8]) -> crate::DoipResult { + pub fn parse_negative(payload: &[u8]) -> crate::Result { let (source_address, target_address, previous_data) = Self::parse_ack_header(payload, "DiagnosticNegativeAck")?; let nack_code = payload @@ -223,10 +225,7 @@ impl DiagnosticAck { } /// Parse SA, TA and optional trailing `previous_data` from an ack payload. - fn parse_ack_header( - payload: &[u8], - context: &str, - ) -> crate::DoipResult<(u16, u16, Option)> { + fn parse_ack_header(payload: &[u8], context: &str) -> crate::Result<(u16, u16, Option)> { let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, context)?; let source_address = u16::from_be_bytes([header[0], header[1]]); let target_address = u16::from_be_bytes([header[2], header[3]]); @@ -239,7 +238,7 @@ impl DiagnosticAck { } impl DoipParseable for Message { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { let header: [u8; HEADER_BYTES] = parse_fixed_slice(payload, "DiagnosticMessage")?; let source_address = u16::from_be_bytes([header[0], header[1]]); @@ -250,12 +249,16 @@ impl DoipParseable for Message { .map(Bytes::copy_from_slice) .ok_or_else(|| { let e = too_short(payload, Self::MIN_LEN); - warn!("DiagnosticMessage parse failed: {}", e); + error!(error = %e, "DiagnosticMessage parse failed"); e })?; if user_data.is_empty() { - warn!("DiagnosticMessage parse failed: empty user data"); + error!( + message_type = "DiagnosticMessage", + reason = "empty_user_data", + "parse failed" + ); return Err(DoipError::EmptyUserData); } @@ -269,7 +272,7 @@ impl DoipParseable for Message { impl DoipSerializable for Message { fn serialized_len(&self) -> Option { - Some(HEADER_BYTES + self.user_data.len()) + Some(HEADER_BYTES.saturating_add(self.user_data.len())) } fn write_to(&self, buf: &mut BytesMut) { @@ -281,7 +284,7 @@ impl DoipSerializable for Message { impl DoipSerializable for DiagnosticAck { fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + self.previous_data.as_ref().map_or(0, bytes::Bytes::len)) + Some(Self::MIN_LEN.saturating_add(self.previous_data.as_ref().map_or(0, bytes::Bytes::len))) } fn write_to(&self, buf: &mut BytesMut) { @@ -298,6 +301,7 @@ impl DoipSerializable for DiagnosticAck { } #[cfg(test)] +#[allow(clippy::indexing_slicing)] mod tests { use super::*; use crate::doip::{DoipParseable, DoipSerializable}; diff --git a/doip-server/src/doip/header.rs b/doip-server/src/doip/header.rs index b1a4046..96bc9fa 100644 --- a/doip-server/src/doip/header.rs +++ b/doip-server/src/doip/header.rs @@ -186,7 +186,7 @@ impl DoipHeader { /// /// # Errors /// Returns [`crate::DoipError::InvalidHeader`] if data is less than 8 bytes. - pub(crate) fn parse(data: &[u8]) -> crate::DoipResult { + pub(crate) fn parse(data: &[u8]) -> crate::Result { let header: [u8; DOIP_HEADER_LENGTH] = data .get(..DOIP_HEADER_LENGTH) .and_then(|s| s.try_into().ok()) @@ -206,10 +206,16 @@ impl DoipHeader { }) } + /// Validate the `DoIP` header against ISO 13400-2:2019 rules. + /// + /// Returns `None` if valid, or the `GenericNackCode` describing the first violation. pub fn validate(&self) -> Option { debug!( - "Validating DoIP header: version=0x{:02X}, inverse=0x{:02X}, type=0x{:04X}, len={}", - self.version, self.inverse_version, self.payload_type, self.payload_length + version = format!("0x{:02X}", self.version), + inverse_version = format!("0x{:02X}", self.inverse_version), + payload_type = format!("0x{:04X}", self.payload_type), + payload_length = self.payload_length, + "validating DoIP header" ); // Accept DoIP protocol versions V1, V2, V3 (and wildcard 0xFF for default/any) @@ -222,29 +228,32 @@ impl DoipHeader { | DOIP_VERSION_DEFAULT ); if !valid_version { - warn!("Invalid DoIP version: 0x{:02X}", self.version); + warn!(version = self.version, "Invalid DoIP version"); return Some(GenericNackCode::IncorrectPatternFormat); } // Check version XOR inverse_version == DOIP_HEADER_VERSION_MASK (0xFF) if self.version ^ self.inverse_version != DOIP_HEADER_VERSION_MASK { warn!( - "Version/inverse mismatch: 0x{:02X} ^ 0x{:02X} = 0x{:02X} (expected 0x{:02X})", - self.version, - self.inverse_version, - self.version ^ self.inverse_version, - DOIP_HEADER_VERSION_MASK, + version = self.version, + inverse_version = self.inverse_version, + expected_mask = DOIP_HEADER_VERSION_MASK, + "Version/inverse mismatch" ); return Some(GenericNackCode::IncorrectPatternFormat); } let Some(payload_type) = PayloadType::try_from(self.payload_type).ok() else { - warn!("Unknown payload type: 0x{:04X}", self.payload_type); + warn!(payload_type = self.payload_type, "Unknown payload type"); return Some(GenericNackCode::UnknownPayloadType); }; if self.payload_length > MAX_DOIP_MESSAGE_SIZE { - warn!("Message too large: {} bytes", self.payload_length); + warn!( + payload_length = self.payload_length, + max = MAX_DOIP_MESSAGE_SIZE, + "Message too large" + ); return Some(GenericNackCode::MessageTooLarge); } let Ok(payload_len_usize) = usize::try_from(self.payload_length) else { @@ -252,10 +261,10 @@ impl DoipHeader { }; if payload_len_usize < payload_type.min_payload_length() { warn!( - "Payload too short for {:?}: {} < {}", - payload_type, - self.payload_length, - payload_type.min_payload_length() + payload_type = ?payload_type, + payload_length = self.payload_length, + min_length = payload_type.min_payload_length(), + "Payload too short for payload type" ); return Some(GenericNackCode::InvalidPayloadLength); } @@ -263,6 +272,7 @@ impl DoipHeader { None } + /// Returns `true` if [`validate`](Self::validate) finds no errors in this header. #[must_use] pub fn is_valid(&self) -> bool { self.validate().is_none() @@ -286,6 +296,7 @@ impl DoipHeader { buf.freeze() } + /// Write the 8-byte `DoIP` header into `buf`. pub fn write_to(&self, buf: &mut BytesMut) { buf.put_u8(self.version); buf.put_u8(self.inverse_version); @@ -368,10 +379,14 @@ impl DoipMessage { } } - /// Create a DoIP message with a raw (unparsed) payload type + /// Create a `DoIP` message with a raw (unparsed) payload type. /// /// This is primarily used for testing unknown/invalid payload types. /// Production code should use `new()` or `with_version()` instead. + /// + /// # Panics + /// + /// Panics if `payload.len()` exceeds `u32::MAX`. #[cfg(test)] pub fn with_raw_payload_type(payload_type: u16, payload: Bytes) -> Self { Self { @@ -385,6 +400,7 @@ impl DoipMessage { } } + /// Returns the decoded `PayloadType`, or `None` if the raw value is not a known type. pub fn payload_type(&self) -> Option { PayloadType::try_from(self.header.payload_type).ok() } @@ -405,6 +421,7 @@ impl DoipMessage { DOIP_HEADER_LENGTH.saturating_add(self.payload.len()) } + /// Serialize the full `DoIP` message (header + payload) into a `Bytes` buffer. pub fn to_bytes(&self) -> Bytes { let mut buf = BytesMut::with_capacity(self.message_length()); self.header.write_to(&mut buf); @@ -418,10 +435,12 @@ impl DoipMessage { // ============================================================================ #[cfg(test)] +#[allow(clippy::indexing_slicing)] mod tests { + use tokio_util::codec::{Decoder, Encoder}; + use super::*; use crate::doip::codec::DoipCodec; - use tokio_util::codec::{Decoder, Encoder}; // --- Helper to build a valid DoIP header quickly --- fn make_header(payload_type: u16, payload_len: u32) -> DoipHeader { @@ -571,10 +590,10 @@ mod tests { #[test] fn payload_type_gaps_return_none() { // These are in gaps between valid ranges - assert!(PayloadType::try_from(0x0009_u16).is_err()); - assert!(PayloadType::try_from(0x4000_u16).is_err()); - assert!(PayloadType::try_from(0x8000_u16).is_err()); - assert!(PayloadType::try_from(0xFFFF_u16).is_err()); + assert!(PayloadType::try_from(0x0009u16).is_err()); + assert!(PayloadType::try_from(0x4000u16).is_err()); + assert!(PayloadType::try_from(0x8000u16).is_err()); + assert!(PayloadType::try_from(0xFFFFu16).is_err()); } #[test] diff --git a/doip-server/src/doip/mod.rs b/doip-server/src/doip/mod.rs index f4861fa..183854c 100644 --- a/doip-server/src/doip/mod.rs +++ b/doip-server/src/doip/mod.rs @@ -13,17 +13,34 @@ //! //! This module provides the core `DoIP` protocol types and codec for TCP/UDP communication. +/// Alive Check request/response handlers (ISO 13400-2:2019 §7.6). pub mod alive_check; +/// Tokio codec framing for DoIP TCP streams. pub mod codec; +/// Diagnostic Message request and acknowledgment handlers (ISO 13400-2:2019 §7.9). pub mod diagnostic_message; +/// DoIP header parsing, validation, and serialization. pub mod header; +/// DoIP payload type enumeration and dispatch. pub mod payload; +/// Routing Activation request/response handlers (ISO 13400-2:2019 §7.7). pub mod routing_activation; +/// Vehicle Identification request/response handlers (ISO 13400-2:2019 §7.5). pub mod vehicle_id; -use crate::DoipError; +// Re-export core types and constants for convenient access. +// Constants are exported to allow external testing and custom DoIP message construction. use bytes::{Bytes, BytesMut}; -use tracing::warn; +pub use codec::DoipCodec; +pub use header::{ + DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, + DOIP_HEADER_VERSION_MASK, DOIP_VERSION_DEFAULT, DoipHeader, DoipMessage, GenericNackCode, + MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, PayloadType, +}; +pub use payload::DoipPayload; +use tracing::error; + +use crate::DoipError; /// Trait for `DoIP` message types that can be parsed from a raw payload slice. /// @@ -34,7 +51,7 @@ pub trait DoipParseable: Sized { /// /// # Errors /// Returns [`DoipError`] if the payload is malformed or too short. - fn parse(payload: &[u8]) -> crate::DoipResult; + fn parse(payload: &[u8]) -> crate::Result; } /// Trait for `DoIP` message types that can be serialized to a [`Bytes`] buffer. @@ -46,10 +63,14 @@ pub trait DoipSerializable { /// Write the serialized wire-format bytes into `buf`. fn write_to(&self, buf: &mut BytesMut); - /// Return the exact serialized byte count, if known without encoding. + /// Returns `Some(n)` when the size is known ahead of serialization, + /// enabling [`to_bytes`] to pre-allocate the buffer and avoid incremental + /// `BytesMut` reallocations for large messages. /// - /// Override this to enable pre-allocated buffers in [`to_bytes`], avoiding - /// incremental `BytesMut` reallocations for large messages. + /// Returns `None` (the default) to indicate the size is not known in + /// advance; [`to_bytes`] will then use a dynamically-growing buffer. + /// Override this in your implementation whenever the encoded length is + /// computable upfront. fn serialized_len(&self) -> Option { None } @@ -76,7 +97,7 @@ pub(crate) fn too_short(payload: &[u8], expected: usize) -> DoipError { } /// Return `Err` if `payload` is shorter than `expected` bytes. -pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> crate::DoipResult<()> { +pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> crate::Result<()> { if payload.len() < expected { Err(too_short(payload, expected)) } else { @@ -86,28 +107,18 @@ pub(crate) fn check_min_len(payload: &[u8], expected: usize) -> crate::DoipResul /// Extract the first `N` bytes of `payload` as a fixed-size array. /// -/// Logs a warning and returns [`DoipError::PayloadTooShort`] when the slice +/// Logs an error and returns [`DoipError::PayloadTooShort`] when the slice /// is shorter than `N` bytes, using `context` to identify the call site in the log. pub(crate) fn parse_fixed_slice( payload: &[u8], context: &str, -) -> crate::DoipResult<[u8; N]> { +) -> crate::Result<[u8; N]> { payload .get(..N) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { let e = too_short(payload, N); - warn!("{} parse failed: {}", context, e); + error!(context, error = %e, "parse failed"); e }) } - -// Re-export core types and constants for convenient access. -// Constants are exported to allow external testing and custom DoIP message construction. -pub use codec::DoipCodec; -pub use header::{ - DEFAULT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION_INV, DOIP_HEADER_LENGTH, - DOIP_HEADER_VERSION_MASK, DOIP_VERSION_DEFAULT, DoipHeader, DoipMessage, GenericNackCode, - MAX_DOIP_MESSAGE_SIZE, PROTOCOL_VERSION_V1, PROTOCOL_VERSION_V3, PayloadType, -}; -pub use payload::DoipPayload; diff --git a/doip-server/src/doip/payload.rs b/doip-server/src/doip/payload.rs index 735121b..ac79b3f 100644 --- a/doip-server/src/doip/payload.rs +++ b/doip-server/src/doip/payload.rs @@ -20,8 +20,8 @@ //! # Example //! ```no_run //! # use doip_server::doip::{DoipMessage, DoipPayload}; -//! # use doip_server::DoipResult; -//! fn dispatch(msg: &DoipMessage) -> DoipResult<()> { +//! # use doip_server::Result; +//! fn dispatch(msg: &DoipMessage) -> Result<()> { //! match DoipPayload::parse(msg)? { //! DoipPayload::DiagnosticMessage(m) => println!("UDS payload: {:?}", m), //! DoipPayload::AliveCheckRequest(_) => println!("Alive check received"), @@ -79,7 +79,7 @@ impl DoipPayload { /// /// Returns a more specific [`DoipError`] (e.g. [`DoipError::PayloadTooShort`]) /// when the payload bytes are present but malformed. - pub fn parse(msg: &DoipMessage) -> crate::DoipResult { + pub fn parse(msg: &DoipMessage) -> crate::Result { let payload = msg.payload().as_ref(); let payload_type = msg @@ -168,9 +168,10 @@ impl DoipPayload { #[cfg(test)] mod tests { + use bytes::Bytes; + use super::*; use crate::doip::{DoipMessage, PayloadType}; - use bytes::Bytes; fn make_msg(payload_type: PayloadType, payload: impl Into) -> DoipMessage { DoipMessage::new(payload_type, payload.into()) diff --git a/doip-server/src/doip/routing_activation.rs b/doip-server/src/doip/routing_activation.rs index 5ba7133..99d5710 100644 --- a/doip-server/src/doip/routing_activation.rs +++ b/doip-server/src/doip/routing_activation.rs @@ -12,10 +12,11 @@ */ //! Routing Activation handlers (ISO 13400-2:2019) +use bytes::{BufMut, BytesMut}; +use tracing::error; + use super::{DoipParseable, DoipSerializable, parse_fixed_slice}; use crate::DoipError; -use bytes::{BufMut, BytesMut}; -use tracing::warn; /// Routing activation response codes per ISO 13400-2:2019 Table 25. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -105,7 +106,11 @@ impl TryFrom for ActivationType { } } -// Routing Activation Request - payload is 7 bytes min, 11 with OEM data +/// Routing Activation Request (payload type `0x0005`) – sent by the tester +/// to activate a routing path. +/// +/// # Wire Format +/// Payload: SA(2) + type(1) + reserved(4) + optional OEM(4) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Request { source_address: u16, @@ -115,7 +120,11 @@ pub struct Request { } impl Request { + /// Minimum wire-format length of a Routing Activation Request payload + /// (7 bytes: SA + type + reserved, without OEM data). pub const MIN_LEN: usize = 7; + /// Maximum wire-format length of a Routing Activation Request payload + /// (11 bytes: includes optional 4-byte OEM data). pub const MAX_LEN: usize = 11; /// Tester logical source address @@ -143,7 +152,11 @@ impl Request { } } -// Routing Activation Response - 9 bytes min, 13 with OEM data +/// Routing Activation Response (payload type `0x0006`) – sent by the `DoIP` +/// entity to confirm or deny routing. +/// +/// # Wire Format +/// Payload: testerAddr(2) + entityAddr(2) + code(1) + reserved(4) + optional OEM(4) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { tester_address: u16, @@ -154,7 +167,11 @@ pub struct Response { } impl Response { + /// Minimum wire-format length of a Routing Activation Response payload + /// (9 bytes: tester addr + entity addr + code + reserved, without OEM data). pub const MIN_LEN: usize = 9; + /// Maximum wire-format length of a Routing Activation Response payload + /// (13 bytes: includes optional 4-byte OEM data). pub const MAX_LEN: usize = 13; /// Build a successful routing activation response. @@ -219,12 +236,12 @@ impl Response { } impl DoipParseable for Request { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { let header: [u8; Self::MIN_LEN] = parse_fixed_slice(payload, "RoutingActivation Request")?; let source_address = u16::from_be_bytes([header[0], header[1]]); let activation_type = ActivationType::try_from(header[2]).map_err(|e| { - warn!("RoutingActivation Request parse failed: {}", e); + error!(error = %e, "RoutingActivation Request parse failed"); e })?; let reserved = u32::from_be_bytes([header[3], header[4], header[5], header[6]]); @@ -244,13 +261,13 @@ impl DoipParseable for Request { } impl DoipParseable for Response { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { let header: [u8; Self::MIN_LEN] = parse_fixed_slice(payload, "RoutingActivation Response")?; let tester_address = u16::from_be_bytes([header[0], header[1]]); let entity_address = u16::from_be_bytes([header[2], header[3]]); let response_code = ResponseCode::try_from(header[4]).map_err(|e| { - warn!("RoutingActivation Response parse failed: {}", e); + error!(error = %e, "RoutingActivation Response parse failed"); e })?; let reserved = u32::from_be_bytes([header[5], header[6], header[7], header[8]]); @@ -272,7 +289,7 @@ impl DoipParseable for Response { impl DoipSerializable for Response { fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + if self.oem_specific.is_some() { 4 } else { 0 }) + Some(Self::MIN_LEN.saturating_add(if self.oem_specific.is_some() { 4 } else { 0 })) } fn write_to(&self, buf: &mut BytesMut) { @@ -287,6 +304,7 @@ impl DoipSerializable for Response { } #[cfg(test)] +#[allow(clippy::indexing_slicing)] mod tests { use super::*; use crate::doip::{DoipParseable, DoipSerializable}; diff --git a/doip-server/src/doip/vehicle_id.rs b/doip-server/src/doip/vehicle_id.rs index 2cf7b40..2b4756a 100644 --- a/doip-server/src/doip/vehicle_id.rs +++ b/doip-server/src/doip/vehicle_id.rs @@ -13,10 +13,11 @@ //! Vehicle Identification handlers (ISO 13400-2:2019) +use bytes::{BufMut, BytesMut}; +use tracing::error; + use super::{DoipParseable, DoipSerializable, check_min_len, parse_fixed_slice, too_short}; use crate::DoipError; -use bytes::{BufMut, BytesMut}; -use tracing::warn; // Wire-format field lengths for VehicleIdentificationResponse (ISO 13400-2:2019) const VIN_LEN: usize = 17; @@ -36,19 +37,26 @@ const GID_END: usize = GID_START + GID_LEN; // 31 const FURTHER_ACTION_IDX: usize = GID_END; // 31 const SYNC_STATUS_IDX: usize = FURTHER_ACTION_IDX + FURTHER_ACTION_LEN; // 32 -// Vehicle Identification Request (0x0001) - no payload +/// Vehicle Identification Request (payload type `0x0001`) – broadcast with no filter criteria. +/// +/// The `DoIP` entity responds with a Vehicle Identification Response containing VIN, EID, and GID. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Request; -// Vehicle Identification Request with EID (0x0002) - 6 byte EID +/// Vehicle Identification Request filtered by EID (payload type `0x0002`). +/// +/// Only the `DoIP` entity with a matching 6-byte EID should respond. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestWithEid { eid: [u8; 6], } impl RequestWithEid { + /// Fixed wire-format length of a Vehicle Identification Request with EID + /// payload (6-byte EID filter). pub const LEN: usize = 6; + /// Create a new Vehicle Identification Request filtered by the given 6-byte EID. #[must_use] pub fn new(eid: [u8; 6]) -> Self { Self { eid } @@ -61,15 +69,19 @@ impl RequestWithEid { } } -// Vehicle Identification Request with VIN (0x0003) - 17 byte VIN +/// Vehicle Identification Request filtered by VIN (payload type `0x0003`). +/// +/// Only the `DoIP` entity with a matching 17-byte VIN should respond. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestWithVin { vin: [u8; 17], } impl RequestWithVin { + /// Fixed wire-format length of a Vehicle Identification Request with VIN payload (17-byte VIN). pub const LEN: usize = 17; + /// Create a new Vehicle Identification Request filtered by the given 17-byte VIN. #[must_use] pub fn new(vin: [u8; 17]) -> Self { Self { vin } @@ -81,13 +93,17 @@ impl RequestWithVin { &self.vin } + /// The VIN filter value as a UTF-8 string (lossy – non-UTF-8 bytes replaced with `�`). #[must_use] pub fn vin_string(&self) -> String { String::from_utf8_lossy(&self.vin).to_string() } } -// Further action codes per ISO 13400-2:2019 Table 23 +/// Further action codes per ISO 13400-2:2019 Table 23. +/// +/// Indicates whether the tester must take additional steps (e.g., routing +/// activation) after identification. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum FurtherAction { @@ -112,7 +128,9 @@ impl From for u8 { } } -// Synchronization status per ISO 13400-2:2019 Table 22 +/// GID synchronization status per ISO 13400-2:2019 Table 22. +/// +/// Indicates whether the `DoIP` entity's Group ID is synchronized across all ECUs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum SyncStatus { @@ -137,8 +155,12 @@ impl From for u8 { } } -// Vehicle Identification Response (0x0004) -// VIN(17) + LogicalAddr(2) + EID(6) + GID(6) + FurtherAction(1) = 32 bytes min +/// Vehicle Identification Response (payload type `0x0004`) – sent by the `DoIP` entity. +/// +/// Contains VIN, logical address, EID, GID, further action code, and optional sync status. +/// +/// # Wire Format +/// VIN(17) + LogicalAddr(2) + EID(6) + GID(6) + FurtherAction(1) + optional SyncStatus(1) #[derive(Debug, Clone, PartialEq, Eq)] pub struct Response { vin: [u8; 17], @@ -150,9 +172,17 @@ pub struct Response { } impl Response { - pub const MIN_LEN: usize = SYNC_STATUS_IDX; // 32: VIN(17) + Addr(2) + EID(6) + GID(6) + FurtherAction(1) - pub const MAX_LEN: usize = SYNC_STATUS_IDX + 1; // 33: adds optional SyncStatus(1) - + /// Minimum wire-format length of a Vehicle Identification Response payload + /// (32 bytes: VIN(17) + LogicalAddr(2) + EID(6) + GID(6) + FurtherAction(1)). + pub const MIN_LEN: usize = SYNC_STATUS_IDX; + /// Maximum wire-format length of a Vehicle Identification Response payload + /// (33 bytes: adds optional SyncStatus(1)). + pub const MAX_LEN: usize = SYNC_STATUS_IDX + 1; + + /// Create a new Vehicle Identification Response with the required fields. + /// + /// `further_action` defaults to [`FurtherAction::NoFurtherAction`]; use + /// [`with_routing_required`](Self::with_routing_required) to override. #[must_use] pub fn new(vin: [u8; 17], logical_address: u16, eid: [u8; 6], gid: [u8; 6]) -> Self { Self { @@ -165,18 +195,22 @@ impl Response { } } + /// Set `FurtherAction` to `RoutingActivationRequired` (ISO 13400-2:2019 Table 23 – 0x10). #[must_use] pub fn with_routing_required(mut self) -> Self { self.further_action = FurtherAction::RoutingActivationRequired; self } + /// Attach an optional GID synchronization status byte to the response + /// (ISO 13400-2:2019 Table 22). #[must_use] pub fn with_sync_status(mut self, status: SyncStatus) -> Self { self.sync_status = Some(status); self } + /// Returns the VIN as a UTF-8 string (lossy – non-UTF-8 bytes replaced with `�`). #[must_use] pub fn vin_string(&self) -> String { String::from_utf8_lossy(&self.vin).to_string() @@ -184,29 +218,29 @@ impl Response { } impl DoipParseable for Request { - fn parse(_payload: &[u8]) -> crate::DoipResult { + fn parse(_payload: &[u8]) -> crate::Result { Ok(Self) } } impl DoipParseable for RequestWithEid { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { let eid: [u8; 6] = parse_fixed_slice(payload, "VehicleId RequestWithEid")?; Ok(Self { eid }) } } impl DoipParseable for RequestWithVin { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { let vin: [u8; 17] = parse_fixed_slice(payload, "VehicleId RequestWithVin")?; Ok(Self { vin }) } } impl DoipParseable for Response { - fn parse(payload: &[u8]) -> crate::DoipResult { + fn parse(payload: &[u8]) -> crate::Result { if let Err(e) = check_min_len(payload, Self::MIN_LEN) { - warn!("VehicleId Response parse failed: {}", e); + error!(error = %e, "VehicleId Response parse failed"); return Err(e); } @@ -256,7 +290,7 @@ impl DoipParseable for Response { impl DoipSerializable for Response { fn serialized_len(&self) -> Option { - Some(Self::MIN_LEN + usize::from(self.sync_status.is_some())) + Some(Self::MIN_LEN.saturating_add(usize::from(self.sync_status.is_some()))) } fn write_to(&self, buf: &mut BytesMut) { @@ -272,6 +306,7 @@ impl DoipSerializable for Response { } #[cfg(test)] +#[allow(clippy::indexing_slicing)] mod tests { use super::*; use crate::doip::{DoipParseable, DoipSerializable}; diff --git a/doip-server/src/error.rs b/doip-server/src/error.rs index 68e5a62..986d921 100644 --- a/doip-server/src/error.rs +++ b/doip-server/src/error.rs @@ -12,17 +12,18 @@ */ //! Error Types for `DoIP` Server (ISO 13400-2:2019 & ISO 14229-1:2020) -use std::io; -use std::net::AddrParseError; +use std::{io, net::AddrParseError}; + use thiserror::Error; // Re-export from the canonical definitions in the protocol modules pub use crate::doip::diagnostic_message::NackCode as DiagnosticNackCode; -pub use crate::doip::header::GenericNackCode; -pub use crate::doip::routing_activation::ResponseCode as RoutingActivationCode; +pub use crate::doip::{ + header::GenericNackCode, routing_activation::ResponseCode as RoutingActivationCode, +}; /// Result type alias for `DoIP` operations -pub type DoipResult = std::result::Result; +pub type Result = std::result::Result; /// Main `DoIP` Error type #[derive(Error, Debug)] diff --git a/doip-server/src/lib.rs b/doip-server/src/lib.rs index a2e3cfd..0c8b098 100644 --- a/doip-server/src/lib.rs +++ b/doip-server/src/lib.rs @@ -10,9 +10,12 @@ * * SPDX-License-Identifier: Apache-2.0 */ +/// Core DoIP protocol types, codec, and wire-format handlers (ISO 13400-2:2019). pub mod doip; +/// Error types and the crate-level [`Result`] alias. pub mod error; +/// DoIP server configuration and session management. pub mod server; +/// UDS service layer – bridges DoIP transport to ISO 14229-1 request/response handling. pub mod uds; -pub use error::DoipError; -pub use error::DoipResult; +pub use error::{DoipError, Result}; diff --git a/doip-server/src/server/config.rs b/doip-server/src/server/config.rs index 644bcec..39fa96c 100644 --- a/doip-server/src/server/config.rs +++ b/doip-server/src/server/config.rs @@ -13,10 +13,11 @@ //! `DoIP` Server Configuration -use crate::DoipError; +use std::{net::SocketAddr, path::Path}; + use serde::Deserialize; -use std::net::SocketAddr; -use std::path::Path; + +use crate::DoipError; // ============================================================================ // Default Configuration Constants (per ISO 13400-2 DoIP specification) @@ -196,7 +197,7 @@ impl ServerConfig { /// Returns [`DoipError::ConfigFileError`] if file cannot be read or parsed. /// Returns [`DoipError::InvalidConfig`] if values are invalid. /// Returns [`DoipError::InvalidAddress`] if bind address is malformed. - pub fn from_file>(path: P) -> crate::DoipResult { + pub fn from_file>(path: P) -> crate::Result { let content = std::fs::read_to_string(path).map_err(|e| DoipError::ConfigFileError(e.to_string()))?; let file: ConfigFile = @@ -234,7 +235,7 @@ impl ServerConfig { }) } - fn parse_vin(s: &str) -> crate::DoipResult<[u8; 17]> { + fn parse_vin(s: &str) -> crate::Result<[u8; 17]> { let bytes = s.as_bytes(); if bytes.len() != 17 { return Err(DoipError::InvalidConfig(format!( @@ -247,7 +248,7 @@ impl ServerConfig { Ok(vin) } - fn parse_hex_array(s: &str) -> crate::DoipResult<[u8; N]> { + fn parse_hex_array(s: &str) -> crate::Result<[u8; N]> { let s = s.trim_start_matches("0x").replace([':', '-', ' '], ""); let bytes = hex::decode(&s).map_err(|e| DoipError::HexDecodeError(e.to_string()))?; if bytes.len() != N { @@ -262,12 +263,14 @@ impl ServerConfig { Ok(arr) } + /// Override the VIN advertised in Vehicle Identification Responses. #[must_use] pub fn with_vin(mut self, vin: [u8; 17]) -> Self { self.vin = vin; self } + /// Override the TCP and UDP bind addresses (default: `0.0.0.0:13400`). #[must_use] pub fn with_addresses(mut self, tcp: SocketAddr, udp: SocketAddr) -> Self { self.tcp_addr = tcp; @@ -378,20 +381,20 @@ mod tests { #[test] fn test_parse_hex_array_valid() { - let result: crate::DoipResult<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); + let result: crate::Result<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B:3C:4D:5E"); assert!(result.is_ok()); assert_eq!(result.unwrap(), [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]); } #[test] fn test_parse_hex_array_with_0x_prefix() { - let result: crate::DoipResult<[u8; 6]> = ServerConfig::parse_hex_array("0x001A2B3C4D5E"); + let result: crate::Result<[u8; 6]> = ServerConfig::parse_hex_array("0x001A2B3C4D5E"); assert!(result.is_ok()); } #[test] fn test_parse_hex_array_invalid_length() { - let result: crate::DoipResult<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B"); + let result: crate::Result<[u8; 6]> = ServerConfig::parse_hex_array("00:1A:2B"); assert!(result.is_err()); } diff --git a/doip-server/src/server/mod.rs b/doip-server/src/server/mod.rs index f9969df..fdfffd2 100644 --- a/doip-server/src/server/mod.rs +++ b/doip-server/src/server/mod.rs @@ -14,7 +14,9 @@ //! //! `DoIP` server configuration and session management. +/// DoIP server configuration (addresses, timeouts, logical address, VIN, EID, GID). pub mod config; +/// Thread-safe session registry for active DoIP tester connections. pub mod session; pub use config::ServerConfig; diff --git a/doip-server/src/server/session.rs b/doip-server/src/server/session.rs index ce90227..cfd2dee 100644 --- a/doip-server/src/server/session.rs +++ b/doip-server/src/server/session.rs @@ -12,10 +12,16 @@ */ //! Session management for `DoIP` connections +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; + use parking_lot::RwLock; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::sync::Arc; use tracing::debug; /// Session states per ISO 13400-2:2019 connection lifecycle @@ -51,12 +57,10 @@ impl Session { } } - /// Transition this session to [`SessionState::RoutingActive`] and record the tester's logical address. + /// Transition this session to [`SessionState::RoutingActive`] and record + /// the tester's logical address. pub fn activate_routing(&mut self, tester_address: u16) { - debug!( - "Session {} routing activated: tester_address=0x{:04X}", - self.id, tester_address - ); + debug!(session_id = self.id, tester_address, "routing activated"); self.tester_address = tester_address; self.state = SessionState::RoutingActive; } @@ -94,14 +98,25 @@ impl Session { /// Thread-safe registry of active `DoIP` sessions. /// -/// Internally uses `parking_lot::RwLock` maps keyed by session ID and -/// remote [`SocketAddr`]. Access this via the [`Arc`] returned by -/// [`SessionManager::new`]. +/// The mutable session state is held in a single [`RwLock`]-protected `Inner` +/// struct for atomic multi-map updates, while the monotonic ID counter uses +/// an [`AtomicU64`] to avoid taking the write-lock just to mint a new ID. +/// Access this via the [`Arc`] returned by [`SessionManager::new`]. +#[derive(Debug, Default)] +struct SessionManagerInner { + sessions: HashMap, + addr_to_session: HashMap, +} + +/// Thread-safe registry of active `DoIP` tester sessions. +/// +/// Uses a single [`RwLock`] over [`SessionManagerInner`] to ensure atomic consistency +/// between the session map and the address-to-ID index, plus a lock-free [`AtomicU64`] +/// counter for session ID allocation. #[derive(Debug, Default)] pub struct SessionManager { - sessions: RwLock>, - addr_to_session: RwLock>, - next_id: RwLock, + inner: RwLock, + next_id: AtomicU64, } impl SessionManager { @@ -113,27 +128,25 @@ impl SessionManager { /// Register a new session for `peer_addr` and return it. pub fn create_session(&self, peer_addr: SocketAddr) -> Session { - let mut next_id = self.next_id.write(); - let id = *next_id; - *next_id = next_id.saturating_add(1); - + let id = self.next_id.fetch_add(1, Ordering::Relaxed); let session = Session::new(id, peer_addr); - self.sessions.write().insert(id, session.clone()); - self.addr_to_session.write().insert(peer_addr, id); - - debug!("Session {} created for {}", id, peer_addr); + let mut inner = self.inner.write(); + inner.sessions.insert(id, session.clone()); + inner.addr_to_session.insert(peer_addr, id); + debug!(session_id = id, peer = %peer_addr, "Session created"); session } /// Look up a session by its numeric ID. Returns `None` if not found. pub fn get_session(&self, id: u64) -> Option { - self.sessions.read().get(&id).cloned() + self.inner.read().sessions.get(&id).cloned() } /// Look up a session by the tester's remote address. Returns `None` if not found. pub fn get_session_by_addr(&self, addr: &SocketAddr) -> Option { - let id = self.addr_to_session.read().get(addr).copied()?; - self.get_session(id) + let inner = self.inner.read(); + let id = inner.addr_to_session.get(addr).copied()?; + inner.sessions.get(&id).cloned() } /// Apply a mutation `f` to the session with the given `id`. Returns `true` if found. @@ -141,7 +154,7 @@ impl SessionManager { where F: FnOnce(&mut Session), { - if let Some(session) = self.sessions.write().get_mut(&id) { + if let Some(session) = self.inner.write().sessions.get_mut(&id) { f(session); true } else { @@ -151,29 +164,32 @@ impl SessionManager { /// Remove and return the session with the given `id`, or `None` if not found. pub fn remove_session(&self, id: u64) -> Option { - let session = self.sessions.write().remove(&id)?; - self.addr_to_session.write().remove(&session.peer_addr); - debug!("Session {} removed (peer: {})", id, session.peer_addr); + let mut inner = self.inner.write(); + let session = inner.sessions.remove(&id)?; + inner.addr_to_session.remove(&session.peer_addr); + debug!(session_id = id, peer = %session.peer_addr, "Session removed"); Some(session) } /// Remove and return the session associated with `addr`, or `None` if not found. pub fn remove_session_by_addr(&self, addr: &SocketAddr) -> Option { - let id = self.addr_to_session.write().remove(addr)?; - let session = self.sessions.write().remove(&id)?; - debug!("Session {} removed by addr (peer: {})", id, addr); + let mut inner = self.inner.write(); + let id = inner.addr_to_session.remove(addr)?; + let session = inner.sessions.remove(&id)?; + debug!(session_id = id, peer = %addr, "Session removed by addr"); Some(session) } /// Returns the number of currently registered sessions. pub fn session_count(&self) -> usize { - self.sessions.read().len() + self.inner.read().sessions.len() } /// Returns `true` if any active session has `tester_address` registered with routing active. pub fn is_tester_registered(&self, tester_address: u16) -> bool { - self.sessions + self.inner .read() + .sessions .values() .any(|s| s.tester_address == tester_address && s.state == SessionState::RoutingActive) } diff --git a/doip-server/src/uds/handler.rs b/doip-server/src/uds/handler.rs index 440aa66..6a29043 100644 --- a/doip-server/src/uds/handler.rs +++ b/doip-server/src/uds/handler.rs @@ -21,20 +21,35 @@ use bytes::Bytes; /// UDS Service IDs (ISO 14229-1:2020) pub mod service_id { + /// Diagnostic Session Control (0x10) – switch the ECU into a specific diagnostic session. pub const DIAGNOSTIC_SESSION_CONTROL: u8 = 0x10; + /// ECU Reset (0x11) – request the ECU to perform a hard, soft, or key-off-on reset. pub const ECU_RESET: u8 = 0x11; + /// Security Access (0x27) – seed/key challenge-response to unlock protected services. pub const SECURITY_ACCESS: u8 = 0x27; + /// Communication Control (0x28) – enable or disable specific ECU communication paths. pub const COMMUNICATION_CONTROL: u8 = 0x28; + /// Tester Present (0x3E) – keep the active diagnostic session alive. pub const TESTER_PRESENT: u8 = 0x3E; + /// Control DTC Setting (0x85) – enable or disable DTC storage in the ECU. pub const CONTROL_DTC_SETTING: u8 = 0x85; + /// Read Data By Identifier (0x22) – read one or more data records by their 2-byte identifier. pub const READ_DATA_BY_IDENTIFIER: u8 = 0x22; + /// Write Data By Identifier (0x2E) – write a data record by its 2-byte identifier. pub const WRITE_DATA_BY_IDENTIFIER: u8 = 0x2E; + /// Routine Control (0x31) – start, stop, or query the result of an ECU routine. pub const ROUTINE_CONTROL: u8 = 0x31; + /// Request Download (0x34) – initiate a data download transfer to the ECU. pub const REQUEST_DOWNLOAD: u8 = 0x34; + /// Request Upload (0x35) – initiate a data upload transfer from the ECU. pub const REQUEST_UPLOAD: u8 = 0x35; + /// Transfer Data (0x36) – transfer a block of data during an active download or upload. pub const TRANSFER_DATA: u8 = 0x36; + /// Request Transfer Exit (0x37) – terminate an active download or upload transfer. pub const REQUEST_TRANSFER_EXIT: u8 = 0x37; + /// Read DTC Information (0x19) – read Diagnostic Trouble Code data from the ECU. pub const READ_DTC_INFORMATION: u8 = 0x19; + /// Clear DTC Information (0x14) – erase stored DTCs and associated snapshot data. pub const CLEAR_DTC_INFORMATION: u8 = 0x14; } @@ -141,9 +156,10 @@ pub trait UdsHandler: Send + Sync { #[cfg(test)] mod tests { - use super::*; use bytes::Bytes; + use super::*; + #[test] fn uds_request_service_id() { let request = UdsRequest::new(0x0E00, 0x1000, Bytes::from(vec![0x10, 0x01])); diff --git a/doip-server/src/uds/mod.rs b/doip-server/src/uds/mod.rs index e370952..3718925 100644 --- a/doip-server/src/uds/mod.rs +++ b/doip-server/src/uds/mod.rs @@ -15,6 +15,7 @@ //! //! Provides the interface between `DoIP` transport and UDS processing. +/// UDS handler trait and request/response types bridging DoIP and ISO 14229-1. pub mod handler; pub use handler::{UdsHandler, UdsRequest, UdsResponse, service_id};