use crate::error::{Error, Result};
use crate::length;
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<&'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) = length::decode(&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(&bytes[start..end])
}
fn spdu_len(value_len: usize) -> usize {
1 + length::encoded_len(value_len) + value_len
}
fn write_spdu_header(tag: u8, value_len: usize, buf: &mut [u8]) -> Result<usize> {
let total = spdu_len(value_len);
if buf.len() < total {
return Err(Error::OutputBufferTooSmall {
need: total,
have: buf.len(),
});
}
buf[0] = tag;
let n = length::encode_into(value_len, &mut buf[1..])?;
Ok(1 + n)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct OpenSessionRequest {
pub resource: ResourceId,
}
impl<'a> Parse<'a> for OpenSessionRequest {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let v = parse_spdu_header(bytes, tags::OPEN_SESSION_REQUEST, "open_session_request")?;
Ok(Self {
resource: ResourceId::parse(v)?,
})
}
}
impl Serialize for OpenSessionRequest {
type Error = Error;
fn serialized_len(&self) -> usize {
spdu_len(ResourceId::LEN)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = write_spdu_header(tags::OPEN_SESSION_REQUEST, ResourceId::LEN, 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 {
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 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 {
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)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::OPEN_SESSION_RESPONSE, 7, 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 {
pub resource: ResourceId,
pub session_nb: u16,
}
impl<'a> Parse<'a> for CreateSession {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let 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 {
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)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::CREATE_SESSION, 6, 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 {
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 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 {
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)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::CREATE_SESSION_RESPONSE, 7, 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 {
pub session_nb: u16,
}
impl<'a> Parse<'a> for CloseSessionRequest {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let 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 {
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)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = write_spdu_header(tags::CLOSE_SESSION_REQUEST, 2, 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 {
pub status: SessionStatus,
pub session_nb: u16,
}
impl<'a> Parse<'a> for CloseSessionResponse {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let 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 {
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)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let mut pos = write_spdu_header(tags::CLOSE_SESSION_RESPONSE, 3, 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 {
pub session_nb: u16,
}
impl SessionNumber {
pub const HEADER_LEN: usize = 4;
}
impl<'a> Parse<'a> for SessionNumber {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let 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 {
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)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = write_spdu_header(tags::SESSION_NUMBER, 2, 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 {
resource: CONDITIONAL_ACCESS_SUPPORT,
};
let bytes = o.to_bytes();
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 {
status: SessionStatus::Ok,
resource: CONDITIONAL_ACCESS_SUPPORT,
session_nb: 0x0102,
};
let bytes = o.to_bytes();
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 {
resource: CONDITIONAL_ACCESS_SUPPORT,
session_nb: 5,
};
let bytes = c.to_bytes();
assert_eq!(bytes[1], 0x06);
assert_eq!(CreateSession::parse(&bytes).unwrap(), c);
}
#[test]
fn create_session_response_round_trip() {
let c = CreateSessionResponse {
status: SessionStatus::ResourceBusy,
resource: CONDITIONAL_ACCESS_SUPPORT,
session_nb: 5,
};
let bytes = c.to_bytes();
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 { session_nb: 0x00FF };
assert_eq!(req.to_bytes(), [0x95, 0x02, 0x00, 0xFF]);
assert_eq!(CloseSessionRequest::parse(&req.to_bytes()).unwrap(), req);
let resp = CloseSessionResponse {
status: SessionStatus::Ok,
session_nb: 0x00FF,
};
assert_eq!(resp.to_bytes(), [0x96, 0x03, 0x00, 0x00, 0xFF]);
assert_eq!(CloseSessionResponse::parse(&resp.to_bytes()).unwrap(), resp);
}
#[test]
fn session_number_round_trip_and_header_len() {
let sn = SessionNumber { session_nb: 0x1234 };
let bytes = sn.to_bytes();
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 { session_nb: 1 };
let a = req.to_bytes();
let b = CloseSessionRequest { session_nb: 2 }.to_bytes();
assert_ne!(a, b);
}
#[test]
fn rejects_wrong_tag() {
assert!(matches!(
OpenSessionRequest::parse(&[0x92, 0x04, 0, 0, 0, 0]),
Err(Error::UnexpectedSpduTag { .. })
));
}
}