use crate::error::{Error, Result};
use crate::length::{self, LengthForm};
use crate::resource::ResourceId;
use broadcast_common::{Parse, Serialize};
pub mod tags {
pub const SESSION_NUMBER: u8 = 0x90;
pub const OPEN_SESSION_REQUEST: u8 = 0x91;
pub const OPEN_SESSION_RESPONSE: u8 = 0x92;
pub const CREATE_SESSION: u8 = 0x93;
pub const CREATE_SESSION_RESPONSE: u8 = 0x94;
pub const CLOSE_SESSION_REQUEST: u8 = 0x95;
pub const CLOSE_SESSION_RESPONSE: u8 = 0x96;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum SessionStatus {
Ok,
ResourceNonExistent,
ResourceUnavailable,
ResourceVersionTooLow,
ResourceBusy,
Reserved(u8),
}
impl SessionStatus {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v {
0x00 => Self::Ok,
0xF0 => Self::ResourceNonExistent,
0xF1 => Self::ResourceUnavailable,
0xF2 => Self::ResourceVersionTooLow,
0xF3 => Self::ResourceBusy,
other => Self::Reserved(other),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Ok => 0x00,
Self::ResourceNonExistent => 0xF0,
Self::ResourceUnavailable => 0xF1,
Self::ResourceVersionTooLow => 0xF2,
Self::ResourceBusy => 0xF3,
Self::Reserved(v) => v,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Ok => "ok",
Self::ResourceNonExistent => "resource_non_existent",
Self::ResourceUnavailable => "resource_unavailable",
Self::ResourceVersionTooLow => "resource_version_too_low",
Self::ResourceBusy => "resource_busy",
Self::Reserved(_) => "reserved",
}
}
}
broadcast_common::impl_spec_display!(SessionStatus, Reserved);
fn parse_spdu_header<'a>(
bytes: &'a [u8],
expected: u8,
what: &'static str,
) -> Result<(LengthForm, &'a [u8])> {
let first = *bytes.first().ok_or(Error::BufferTooShort {
need: 1,
have: 0,
what,
})?;
if first != expected {
return Err(Error::UnexpectedSpduTag {
got: first,
expected,
what,
});
}
let (len, hdr, form) = length::decode_form(&bytes[1..])?;
let start = 1 + hdr;
let end = start + len;
if bytes.len() < end {
return Err(Error::LengthMismatch {
what,
declared: len,
actual: bytes.len().saturating_sub(start),
});
}
Ok((form, &bytes[start..end]))
}
fn spdu_len(value_len: usize, form: LengthForm) -> usize {
1 + length::encoded_len_with(value_len, form) + value_len
}
fn write_spdu_header(tag: u8, value_len: usize, form: LengthForm, buf: &mut [u8]) -> Result<usize> {
let total = spdu_len(value_len, form);
if buf.len() < total {
return Err(Error::OutputBufferTooSmall {
need: total,
have: buf.len(),
});
}
buf[0] = tag;
let n = length::encode_with(value_len, form, &mut buf[1..])?;
Ok(1 + n)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct OpenSessionRequest {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub resource: ResourceId,
}
impl<'a> Parse<'a> for OpenSessionRequest {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) =
parse_spdu_header(bytes, tags::OPEN_SESSION_REQUEST, "open_session_request")?;
if v.len() != ResourceId::LEN {
return Err(Error::InvalidObject {
what: "open_session_request",
reason: "value must be 4 bytes",
});
}
Ok(Self {
length_form,
resource: ResourceId::parse(v)?,
})
}
}
impl Serialize for OpenSessionRequest {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(ResourceId::LEN, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = write_spdu_header(
tags::OPEN_SESSION_REQUEST,
ResourceId::LEN,
self.length_form,
buf,
)?;
let n = self.resource.serialize_into(&mut buf[pos..])?;
Ok(pos + n)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct OpenSessionResponse {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub status: SessionStatus,
pub resource: ResourceId,
pub session_nb: u16,
}
impl<'a> Parse<'a> for OpenSessionResponse {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) =
parse_spdu_header(bytes, tags::OPEN_SESSION_RESPONSE, "open_session_response")?;
if v.len() != 7 {
return Err(Error::InvalidObject {
what: "open_session_response",
reason: "value must be 7 bytes",
});
}
Ok(Self {
length_form,
status: SessionStatus::from_u8(v[0]),
resource: ResourceId::parse(&v[1..5])?,
session_nb: u16::from_be_bytes([v[5], v[6]]),
})
}
}
impl Serialize for OpenSessionResponse {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(7, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::OPEN_SESSION_RESPONSE, 7, self.length_form, buf)?;
buf[pos] = self.status.to_u8();
pos += 1;
pos += self.resource.serialize_into(&mut buf[pos..])?;
buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
Ok(pos + 2)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CreateSession {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub resource: ResourceId,
pub session_nb: u16,
}
impl<'a> Parse<'a> for CreateSession {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) = parse_spdu_header(bytes, tags::CREATE_SESSION, "create_session")?;
if v.len() != 6 {
return Err(Error::InvalidObject {
what: "create_session",
reason: "value must be 6 bytes",
});
}
Ok(Self {
length_form,
resource: ResourceId::parse(&v[0..4])?,
session_nb: u16::from_be_bytes([v[4], v[5]]),
})
}
}
impl Serialize for CreateSession {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(6, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::CREATE_SESSION, 6, self.length_form, buf)?;
pos += self.resource.serialize_into(&mut buf[pos..])?;
buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
Ok(pos + 2)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CreateSessionResponse {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub status: SessionStatus,
pub resource: ResourceId,
pub session_nb: u16,
}
impl<'a> Parse<'a> for CreateSessionResponse {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) = parse_spdu_header(
bytes,
tags::CREATE_SESSION_RESPONSE,
"create_session_response",
)?;
if v.len() != 7 {
return Err(Error::InvalidObject {
what: "create_session_response",
reason: "value must be 7 bytes",
});
}
Ok(Self {
length_form,
status: SessionStatus::from_u8(v[0]),
resource: ResourceId::parse(&v[1..5])?,
session_nb: u16::from_be_bytes([v[5], v[6]]),
})
}
}
impl Serialize for CreateSessionResponse {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(7, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::CREATE_SESSION_RESPONSE, 7, self.length_form, buf)?;
buf[pos] = self.status.to_u8();
pos += 1;
pos += self.resource.serialize_into(&mut buf[pos..])?;
buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
Ok(pos + 2)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CloseSessionRequest {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub session_nb: u16,
}
impl<'a> Parse<'a> for CloseSessionRequest {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) =
parse_spdu_header(bytes, tags::CLOSE_SESSION_REQUEST, "close_session_request")?;
if v.len() != 2 {
return Err(Error::InvalidObject {
what: "close_session_request",
reason: "value must be 2 bytes",
});
}
Ok(Self {
length_form,
session_nb: u16::from_be_bytes([v[0], v[1]]),
})
}
}
impl Serialize for CloseSessionRequest {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(2, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = write_spdu_header(tags::CLOSE_SESSION_REQUEST, 2, self.length_form, buf)?;
buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
Ok(pos + 2)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CloseSessionResponse {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub status: SessionStatus,
pub session_nb: u16,
}
impl<'a> Parse<'a> for CloseSessionResponse {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) = parse_spdu_header(
bytes,
tags::CLOSE_SESSION_RESPONSE,
"close_session_response",
)?;
if v.len() != 3 {
return Err(Error::InvalidObject {
what: "close_session_response",
reason: "value must be 3 bytes",
});
}
Ok(Self {
length_form,
status: SessionStatus::from_u8(v[0]),
session_nb: u16::from_be_bytes([v[1], v[2]]),
})
}
}
impl Serialize for CloseSessionResponse {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(3, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::CLOSE_SESSION_RESPONSE, 3, self.length_form, buf)?;
buf[pos] = self.status.to_u8();
pos += 1;
buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
Ok(pos + 2)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SessionNumber {
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: crate::length::LengthForm,
pub session_nb: u16,
}
impl SessionNumber {
pub const HEADER_LEN: usize = 4;
#[must_use]
pub fn header_len(&self) -> usize {
1 + length::encoded_len_with(2, self.length_form) + 2
}
}
impl<'a> Parse<'a> for SessionNumber {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, v) = parse_spdu_header(bytes, tags::SESSION_NUMBER, "session_number")?;
if v.len() != 2 {
return Err(Error::InvalidObject {
what: "session_number",
reason: "value must be 2 bytes",
});
}
Ok(Self {
length_form,
session_nb: u16::from_be_bytes([v[0], v[1]]),
})
}
}
impl Serialize for SessionNumber {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(2, self.length_form)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = write_spdu_header(tags::SESSION_NUMBER, 2, self.length_form, buf)?;
buf[pos..pos + 2].copy_from_slice(&self.session_nb.to_be_bytes());
Ok(pos + 2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resource::CONDITIONAL_ACCESS_SUPPORT;
#[test]
fn open_session_request_round_trip() {
let o = OpenSessionRequest {
length_form: crate::length::LengthForm::Minimal,
resource: CONDITIONAL_ACCESS_SUPPORT,
};
let bytes = o.try_to_bytes().unwrap();
assert_eq!(bytes, [0x91, 0x04, 0x00, 0x03, 0x00, 0x41]);
assert_eq!(OpenSessionRequest::parse(&bytes).unwrap(), o);
}
#[test]
fn open_session_response_round_trip() {
let o = OpenSessionResponse {
length_form: crate::length::LengthForm::Minimal,
status: SessionStatus::Ok,
resource: CONDITIONAL_ACCESS_SUPPORT,
session_nb: 0x0102,
};
let bytes = o.try_to_bytes().unwrap();
assert_eq!(bytes[0], tags::OPEN_SESSION_RESPONSE);
assert_eq!(bytes[1], 0x07);
let parsed = OpenSessionResponse::parse(&bytes).unwrap();
assert_eq!(parsed, o);
assert_eq!(parsed.status.name(), "ok");
}
#[test]
fn create_session_round_trip() {
let c = CreateSession {
length_form: crate::length::LengthForm::Minimal,
resource: CONDITIONAL_ACCESS_SUPPORT,
session_nb: 5,
};
let bytes = c.try_to_bytes().unwrap();
assert_eq!(bytes[1], 0x06);
assert_eq!(CreateSession::parse(&bytes).unwrap(), c);
}
#[test]
fn create_session_response_round_trip() {
let c = CreateSessionResponse {
length_form: crate::length::LengthForm::Minimal,
status: SessionStatus::ResourceBusy,
resource: CONDITIONAL_ACCESS_SUPPORT,
session_nb: 5,
};
let bytes = c.try_to_bytes().unwrap();
let parsed = CreateSessionResponse::parse(&bytes).unwrap();
assert_eq!(parsed, c);
assert_eq!(parsed.status.name(), "resource_busy");
}
#[test]
fn close_session_round_trips() {
let req = CloseSessionRequest {
length_form: crate::length::LengthForm::Minimal,
session_nb: 0x00FF,
};
assert_eq!(req.try_to_bytes().unwrap(), [0x95, 0x02, 0x00, 0xFF]);
assert_eq!(
CloseSessionRequest::parse(&req.try_to_bytes().unwrap()).unwrap(),
req
);
let resp = CloseSessionResponse {
length_form: crate::length::LengthForm::Minimal,
status: SessionStatus::Ok,
session_nb: 0x00FF,
};
assert_eq!(resp.try_to_bytes().unwrap(), [0x96, 0x03, 0x00, 0x00, 0xFF]);
assert_eq!(
CloseSessionResponse::parse(&resp.try_to_bytes().unwrap()).unwrap(),
resp
);
}
#[test]
fn session_number_round_trip_and_header_len() {
let sn = SessionNumber {
length_form: crate::length::LengthForm::Minimal,
session_nb: 0x1234,
};
let bytes = sn.try_to_bytes().unwrap();
assert_eq!(bytes, [0x90, 0x02, 0x12, 0x34]);
assert_eq!(bytes.len(), SessionNumber::HEADER_LEN);
let mut with_body = bytes.to_vec();
with_body.extend_from_slice(&[0x9F, 0x80, 0x30, 0x00]);
assert_eq!(SessionNumber::parse(&with_body).unwrap(), sn);
}
#[test]
fn mutating_session_nb_changes_bytes() {
let req = CloseSessionRequest {
length_form: crate::length::LengthForm::Minimal,
session_nb: 1,
};
let a = req.try_to_bytes().unwrap();
let b = CloseSessionRequest {
length_form: crate::length::LengthForm::Minimal,
session_nb: 2,
}
.try_to_bytes()
.unwrap();
assert_ne!(a, b);
}
#[test]
fn rejects_wrong_tag() {
assert!(matches!(
OpenSessionRequest::parse(&[0x92, 0x04, 0, 0, 0, 0]),
Err(Error::UnexpectedSpduTag { .. })
));
}
fn widen(wire: &[u8], octets: usize) -> alloc::vec::Vec<u8> {
let (value, hdr) = length::decode(&wire[1..]).unwrap();
let mut out = alloc::vec![wire[0], 0x80 | octets as u8];
for i in (0..octets).rev() {
out.push((value >> (8 * i)) as u8);
}
out.extend_from_slice(&wire[1 + hdr..]);
out
}
#[test]
fn every_spdu_round_trips_each_length_form_byte_exactly() {
macro_rules! spdu_forms {
($ty:ty, $wire:expr) => {{
let wire: &[u8] = &$wire;
assert_eq!(<$ty>::parse(wire).unwrap().try_to_bytes().unwrap(), wire);
for octets in 1..=4 {
let wide = widen(wire, octets);
let parsed = <$ty>::parse(&wide).unwrap();
assert_eq!(
parsed.try_to_bytes().unwrap(),
wide,
"{} {octets}-octet form",
stringify!($ty)
);
assert_ne!(parsed, <$ty>::parse(wire).unwrap());
}
}};
}
spdu_forms!(OpenSessionRequest, [0x91, 0x04, 0x00, 0x03, 0x00, 0x41]);
spdu_forms!(
OpenSessionResponse,
[0x92, 0x07, 0x00, 0x00, 0x03, 0x00, 0x41, 0x01, 0x02]
);
spdu_forms!(
CreateSession,
[0x93, 0x06, 0x00, 0x03, 0x00, 0x41, 0x01, 0x02]
);
spdu_forms!(
CreateSessionResponse,
[0x94, 0x07, 0x00, 0x00, 0x03, 0x00, 0x41, 0x01, 0x02]
);
spdu_forms!(CloseSessionRequest, [0x95, 0x02, 0x01, 0x02]);
spdu_forms!(CloseSessionResponse, [0x96, 0x03, 0x00, 0x01, 0x02]);
spdu_forms!(SessionNumber, [0x90, 0x02, 0x01, 0x02]);
}
#[test]
fn session_number_header_len_follows_the_length_form() {
let minimal = SessionNumber::parse(&[0x90, 0x02, 0x00, 0x03]).unwrap();
assert_eq!(minimal.header_len(), SessionNumber::HEADER_LEN);
let long = SessionNumber::parse(&[0x90, 0x82, 0x00, 0x02, 0x00, 0x03]).unwrap();
assert_eq!(long.header_len(), 6);
assert_eq!(long.session_nb, 3);
}
}