use core::fmt;
pub const HEADER_LEN: usize = 8;
pub const PROTOCOL_VERSION: u8 = 0x01;
pub const SDP_PORT: u16 = 15118;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum PayloadType {
ExiEncodedV2gMessage,
Part20Main,
Part20Ac,
Part20Dc,
Part20Acdp,
Part20Wpt,
SdpRequest,
SdpResponse,
SdpWirelessRequest,
SdpWirelessResponse,
ManufacturerSpecific(u16),
Reserved(u16),
}
impl PayloadType {
#[must_use]
pub const fn as_u16(self) -> u16 {
match self {
Self::ExiEncodedV2gMessage => 0x8001,
Self::Part20Main => 0x8002,
Self::Part20Ac => 0x8003,
Self::Part20Dc => 0x8004,
Self::Part20Acdp => 0x8005,
Self::Part20Wpt => 0x8006,
Self::SdpRequest => 0x9000,
Self::SdpResponse => 0x9001,
Self::SdpWirelessRequest => 0x9002,
Self::SdpWirelessResponse => 0x9003,
Self::ManufacturerSpecific(v) | Self::Reserved(v) => v,
}
}
#[must_use]
pub const fn from_u16(value: u16) -> Self {
match value {
0x8001 => Self::ExiEncodedV2gMessage,
0x8002 => Self::Part20Main,
0x8003 => Self::Part20Ac,
0x8004 => Self::Part20Dc,
0x8005 => Self::Part20Acdp,
0x8006 => Self::Part20Wpt,
0x9000 => Self::SdpRequest,
0x9001 => Self::SdpResponse,
0x9002 => Self::SdpWirelessRequest,
0x9003 => Self::SdpWirelessResponse,
0xA000..=0xFFFF => Self::ManufacturerSpecific(value),
other => Self::Reserved(other),
}
}
#[must_use]
pub const fn is_ignorable(self) -> bool {
matches!(self, Self::ManufacturerSpecific(_) | Self::Reserved(_))
}
#[must_use]
#[allow(
clippy::match_same_arms,
reason = "one arm per generation's payload types; merging them by their \
shared `true` would hide which types belong to which"
)]
pub const fn belongs_to(self, protocol: Option<crate::Protocol>) -> bool {
use crate::Protocol as P;
match (self, protocol) {
(Self::ExiEncodedV2gMessage, None | Some(P::Iso2 | P::Din70121)) => true,
(
Self::Part20Main
| Self::Part20Ac
| Self::Part20Dc
| Self::Part20Acdp
| Self::Part20Wpt,
Some(P::Iso20),
) => true,
_ => false,
}
}
#[must_use]
pub const fn is_exi(self) -> bool {
matches!(
self,
Self::ExiEncodedV2gMessage
| Self::Part20Main
| Self::Part20Ac
| Self::Part20Dc
| Self::Part20Acdp
| Self::Part20Wpt
)
}
#[must_use]
pub const fn is_sdp_request(self) -> bool {
matches!(self, Self::SdpRequest | Self::SdpWirelessRequest)
}
#[must_use]
pub const fn is_sdp_response(self) -> bool {
matches!(self, Self::SdpResponse | Self::SdpWirelessResponse)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Header {
pub payload_type: PayloadType,
pub payload_len: u32,
}
impl Header {
#[must_use]
pub const fn new(payload_type: PayloadType, payload_len: u32) -> Self {
Self { payload_type, payload_len }
}
#[must_use]
pub const fn to_bytes(self) -> [u8; HEADER_LEN] {
let ty = self.payload_type.as_u16().to_be_bytes();
let len = self.payload_len.to_be_bytes();
[PROTOCOL_VERSION, !PROTOCOL_VERSION, ty[0], ty[1], len[0], len[1], len[2], len[3]]
}
pub fn write_to(self, out: &mut [u8]) -> Result<(), V2gtpError> {
let bytes = self.to_bytes();
out.get_mut(..HEADER_LEN).ok_or(V2gtpError::BufferTooSmall)?.copy_from_slice(&bytes);
Ok(())
}
pub fn decode(input: &[u8]) -> Result<Self, V2gtpError> {
let head: &[u8; HEADER_LEN] =
input.get(..HEADER_LEN).ok_or(V2gtpError::Incomplete)?.try_into().unwrap();
if head[0] != PROTOCOL_VERSION {
return Err(V2gtpError::UnsupportedVersion(head[0]));
}
if head[1] != !PROTOCOL_VERSION {
return Err(V2gtpError::VersionMismatch { version: head[0], inverse: head[1] });
}
let payload_type = PayloadType::from_u16(u16::from_be_bytes([head[2], head[3]]));
let payload_len = u32::from_be_bytes([head[4], head[5], head[6], head[7]]);
Ok(Self { payload_type, payload_len })
}
}
pub fn split_frame(
input: &[u8],
max_payload_len: usize,
) -> Result<(Header, &[u8], &[u8]), V2gtpError> {
let header = Header::decode(input)?;
let len = usize::try_from(header.payload_len).map_err(|_| V2gtpError::PayloadTooLarge {
declared: header.payload_len,
limit: max_payload_len,
})?;
if len > max_payload_len {
return Err(V2gtpError::PayloadTooLarge {
declared: header.payload_len,
limit: max_payload_len,
});
}
let end = HEADER_LEN.checked_add(len).ok_or(V2gtpError::Incomplete)?;
if input.len() < end {
return Err(V2gtpError::Incomplete);
}
Ok((header, &input[HEADER_LEN..end], &input[end..]))
}
pub fn write_frame(
payload_type: PayloadType,
payload: &[u8],
out: &mut [u8],
) -> Result<usize, V2gtpError> {
let len = u32::try_from(payload.len()).map_err(|_| V2gtpError::PayloadTooLarge {
declared: u32::MAX,
limit: u32::MAX as usize,
})?;
let total = HEADER_LEN.checked_add(payload.len()).ok_or(V2gtpError::BufferTooSmall)?;
if out.len() < total {
return Err(V2gtpError::BufferTooSmall);
}
Header::new(payload_type, len).write_to(out)?;
out[HEADER_LEN..total].copy_from_slice(payload);
Ok(total)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum V2gtpError {
Incomplete,
BufferTooSmall,
UnsupportedVersion(u8),
VersionMismatch {
version: u8,
inverse: u8,
},
PayloadTooLarge {
declared: u32,
limit: usize,
},
}
impl fmt::Display for V2gtpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Incomplete => f.write_str("incomplete V2GTP frame"),
Self::BufferTooSmall => f.write_str("output buffer too small for V2GTP frame"),
Self::UnsupportedVersion(v) => write!(f, "unsupported V2GTP protocol version {v:#04x}"),
Self::VersionMismatch { version, inverse } => {
write!(f, "V2GTP version {version:#04x} does not match its inverse {inverse:#04x}")
}
Self::PayloadTooLarge { declared, limit } => {
write!(f, "V2GTP payload of {declared} bytes exceeds the {limit} byte limit")
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for V2gtpError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_bytes_match_the_spec_layout() {
let h = Header::new(PayloadType::ExiEncodedV2gMessage, 0x1234);
assert_eq!(h.to_bytes(), [0x01, 0xFE, 0x80, 0x01, 0x00, 0x00, 0x12, 0x34]);
}
#[test]
fn header_roundtrips() {
for ty in [
PayloadType::ExiEncodedV2gMessage,
PayloadType::Part20Main,
PayloadType::Part20Ac,
PayloadType::Part20Dc,
PayloadType::Part20Acdp,
PayloadType::Part20Wpt,
PayloadType::SdpRequest,
PayloadType::SdpResponse,
PayloadType::SdpWirelessRequest,
PayloadType::SdpWirelessResponse,
PayloadType::ManufacturerSpecific(0xA123),
] {
let h = Header::new(ty, 42);
assert_eq!(Header::decode(&h.to_bytes()).unwrap(), h);
assert_eq!(PayloadType::from_u16(ty.as_u16()), ty);
}
}
#[test]
fn a_bad_inverse_version_is_rejected() {
let bytes = [0x01, 0x00, 0x80, 0x01, 0, 0, 0, 0];
assert_eq!(
Header::decode(&bytes),
Err(V2gtpError::VersionMismatch { version: 0x01, inverse: 0x00 })
);
}
#[test]
fn reserved_payload_types_classify_rather_than_fail() {
for value in [0x0000, 0x8000, 0x8007, 0x8FFF, 0x9004, 0x9FFF] {
let ty = PayloadType::from_u16(value);
assert_eq!(ty, PayloadType::Reserved(value));
assert!(ty.is_ignorable());
assert!(!ty.is_exi());
assert_eq!(ty.as_u16(), value, "the code round-trips so a log can name it");
}
for value in [0xA000, 0xA123, 0xFFFF] {
let ty = PayloadType::from_u16(value);
assert_eq!(ty, PayloadType::ManufacturerSpecific(value));
assert!(ty.is_ignorable());
}
assert!(!PayloadType::from_u16(0x8001).is_ignorable());
assert!(!PayloadType::from_u16(0x9000).is_ignorable());
}
#[test]
fn a_reserved_frame_does_not_lose_the_frame_boundary() {
let mut buf = [0u8; 64];
let n = write_frame(PayloadType::Reserved(0x8007), &[1, 2, 3], &mut buf).unwrap();
let (header, payload, rest) = split_frame(&buf[..n], 1024).unwrap();
assert_eq!(header.payload_type, PayloadType::Reserved(0x8007));
assert_eq!(payload, &[1, 2, 3]);
assert!(rest.is_empty());
}
#[test]
fn a_short_buffer_is_incomplete_not_invalid() {
assert_eq!(Header::decode(&[0x01, 0xFE, 0x80]), Err(V2gtpError::Incomplete));
}
#[test]
fn split_frame_returns_payload_and_remainder() {
let mut buf = [0u8; 32];
let n = write_frame(PayloadType::Part20Main, &[1, 2, 3, 4], &mut buf).unwrap();
assert_eq!(n, HEADER_LEN + 4);
buf[n] = 0xFF;
let (h, payload, rest) = split_frame(&buf[..=n], 1024).unwrap();
assert_eq!(h.payload_type, PayloadType::Part20Main);
assert_eq!(payload, &[1, 2, 3, 4]);
assert_eq!(rest, &[0xFF]);
}
#[test]
fn an_oversized_declared_payload_is_refused_before_reading() {
let bytes = [0x01, 0xFE, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF];
assert_eq!(
split_frame(&bytes, 65536),
Err(V2gtpError::PayloadTooLarge { declared: u32::MAX, limit: 65536 })
);
}
#[test]
fn a_truncated_payload_is_incomplete() {
let bytes = [0x01, 0xFE, 0x80, 0x01, 0x00, 0x00, 0x00, 0x10, 0xAA];
assert_eq!(split_frame(&bytes, 65536), Err(V2gtpError::Incomplete));
}
#[test]
fn payload_type_classification() {
assert!(PayloadType::Part20Dc.is_exi());
assert!(!PayloadType::SdpRequest.is_exi());
assert!(PayloadType::SdpWirelessRequest.is_sdp_request());
assert!(PayloadType::SdpResponse.is_sdp_response());
assert!(!PayloadType::SdpResponse.is_sdp_request());
}
}