use std::{
collections::VecDeque,
os::{
fd::{
AsFd,
BorrowedFd,
},
unix::io::{
AsRawFd,
RawFd,
},
},
time::Instant,
};
use super::{
apdu,
apdu::ApduTag,
capmt::Program,
controller::{
CaSlotFailure,
CaSlotStatus,
CamStatus,
},
resource::{
ApplicationInfo,
MmiMenu,
ResourceContext,
ResourceId,
ResourceRegistry,
mmi,
},
spdu,
spdu::Spdu,
transport::{
CiTransport,
TransportRecv,
},
};
use crate::error::{
Error,
Result,
};
const MAX_SLOT_SESSIONS: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaEvent {
SlotStatusChanged {
slot_id: u8,
old: CaSlotStatus,
new: CaSlotStatus,
},
CamStatusChanged {
slot_id: u8,
old: CamStatus,
new: CamStatus,
},
SlotFailed { slot_id: u8, reason: CaSlotFailure },
TransportReady { slot_id: u8 },
Malformed { slot_id: u8, context: String },
SessionOpened {
slot_id: u8,
session_id: u16,
resource_id: ResourceId,
},
SessionRefused {
slot_id: u8,
resource_id: ResourceId,
status: u8,
},
SessionClosed {
slot_id: u8,
session_id: u16,
resource_id: ResourceId,
},
ApplicationInfo { slot_id: u8, info: ApplicationInfo },
CaInfo {
slot_id: u8,
session_id: u16,
caids: Vec<u16>,
},
MmiClose {
slot_id: u8,
session_id: u16,
delay: Option<u8>,
},
MmiText {
slot_id: u8,
session_id: u16,
text: Vec<u8>,
},
MmiMenu {
slot_id: u8,
session_id: u16,
menu: MmiMenu,
},
MmiList {
slot_id: u8,
session_id: u16,
menu: MmiMenu,
},
MmiEnq {
slot_id: u8,
session_id: u16,
blind: bool,
answer_len: u8,
text: Vec<u8>,
},
Tune {
slot_id: u8,
network_id: u16,
original_network_id: u16,
transport_stream_id: u16,
service_id: u16,
},
Replace {
slot_id: u8,
replace_ref: u8,
replaced_pid: u16,
replacement_pid: u16,
},
ClearReplace { slot_id: u8, replace_ref: u8 },
}
enum SessionState {
Active,
Closing,
}
struct Session {
slot_id: u8,
resource_id: ResourceId,
state: SessionState,
}
pub struct CiSession {
transport: CiTransport,
resources: ResourceRegistry,
sessions: Vec<Option<Session>>,
events: VecDeque<CaEvent>,
}
impl AsRawFd for CiSession {
fn as_raw_fd(&self) -> RawFd {
self.transport.as_raw_fd()
}
}
impl AsFd for CiSession {
fn as_fd(&self) -> BorrowedFd<'_> {
self.transport.as_fd()
}
}
impl CiSession {
pub fn new(transport: CiTransport) -> Self {
CiSession {
transport,
resources: ResourceRegistry::new(),
sessions: Vec::new(),
events: VecDeque::new(),
}
}
pub fn transport(&self) -> &CiTransport {
&self.transport
}
pub fn transport_mut(&mut self) -> &mut CiTransport {
&mut self.transport
}
pub fn next_event(&mut self) -> Option<CaEvent> {
self.events.pop_front()
}
pub fn recv(&mut self) -> Result<bool> {
Ok(self.recv_filtered(|_| true)?.is_some())
}
pub(crate) fn recv_controller(&mut self, active_slots: &[bool]) -> Result<Option<u8>> {
self.recv_filtered(|slot_id| {
active_slots
.get(usize::from(slot_id))
.copied()
.unwrap_or(false)
})
}
fn recv_filtered(&mut self, accept_spdu: impl FnOnce(u8) -> bool) -> Result<Option<u8>> {
let recv = match self.transport.recv_apdu()? {
Some(recv) => recv,
None => return Ok(None),
};
let slot_id = recv.slot_id();
match recv {
TransportRecv::TcReply { slot_id } => {
self.events.push_back(CaEvent::TransportReady { slot_id });
}
TransportRecv::Spdu { slot_id, spdu } if accept_spdu(slot_id) => {
self.dispatch_spdu(slot_id, &spdu)?
}
TransportRecv::Spdu { slot_id, .. } => {
self.events.push_back(CaEvent::Malformed {
slot_id,
context: format!(
"ca slot {}: session data before the transport connection is active",
slot_id
),
});
}
TransportRecv::Status { .. } => {}
TransportRecv::Malformed { slot_id, context } => {
self.events
.push_back(CaEvent::Malformed { slot_id, context });
}
}
self.transport.flush(slot_id)?;
Ok(Some(slot_id))
}
pub fn tick(&mut self) -> Result<()> {
self.tick_at(Instant::now())
}
pub fn tick_at(&mut self, now: Instant) -> Result<()> {
self.resources.tick(&mut self.transport, now)
}
pub fn drop_slot(&mut self, slot_id: u8) {
self.transport.clear_slot(slot_id);
for index in 0 .. self.sessions.len() {
let matches = self.sessions[index]
.as_ref()
.is_some_and(|session| session.slot_id == slot_id);
if matches {
self.free_session((index + 1) as u16);
}
}
}
pub fn app_info(&self, slot_id: u8) -> Option<&ApplicationInfo> {
self.resources.application_info.info(slot_id)
}
pub fn caids(&self, slot_id: u8) -> Vec<u16> {
let mut caids = Vec::new();
for (index, session) in self.sessions.iter().enumerate() {
let Some(session) = session else {
continue;
};
if session.slot_id != slot_id
|| session.resource_id.base() != ResourceId::CONDITIONAL_ACCESS_SUPPORT.base()
|| !matches!(session.state, SessionState::Active)
{
continue;
}
if let Some(session_caids) = self
.resources
.conditional_access
.session_caids(slot_id, (index + 1) as u16)
{
caids.extend_from_slice(session_caids);
}
}
caids.sort_unstable();
caids.dedup();
caids
}
pub fn session_caids(&self, slot_id: u8, session_id: u16) -> Option<&[u16]> {
let session = self.session(session_id)?;
if session.slot_id != slot_id
|| session.resource_id.base() != ResourceId::CONDITIONAL_ACCESS_SUPPORT.base()
|| !matches!(session.state, SessionState::Active)
{
return None;
}
self.resources
.conditional_access
.session_caids(slot_id, session_id)
}
pub(super) fn set_program(&mut self, program: Program) -> Result<Vec<u8>> {
self.resources
.conditional_access
.set_program(&mut self.transport, program)
}
pub(super) fn remove_program(&mut self, program_number: u16) -> Result<Vec<u8>> {
self.resources
.conditional_access
.remove_program(&mut self.transport, program_number)
}
pub(crate) fn has_ca_info(&self, slot_id: u8) -> bool {
self.sessions.iter().enumerate().any(|(index, session)| {
session.as_ref().is_some_and(|session| {
session.slot_id == slot_id
&& session.resource_id.base() == ResourceId::CONDITIONAL_ACCESS_SUPPORT.base()
&& matches!(session.state, SessionState::Active)
&& self
.resources
.conditional_access
.session_caids(slot_id, (index + 1) as u16)
.is_some()
})
})
}
pub fn enter_menu(&mut self, slot_id: u8) -> Result<()> {
let confirmed_session = self
.resources
.application_info
.info_session(slot_id)
.map(|(session_id, _)| session_id)
.filter(|&session_id| {
matches!(
self.session(session_id),
Some(session)
if session.slot_id == slot_id
&& session.resource_id.base()
== ResourceId::APPLICATION_INFORMATION.base()
&& matches!(session.state, SessionState::Active)
)
});
let session_id = match confirmed_session {
Some(session_id) => session_id,
None => self.find_session(slot_id, ResourceId::APPLICATION_INFORMATION)?,
};
self.transport
.send_apdu(slot_id, session_id, ApduTag::ENTER_MENU, &[])
}
pub fn mmi_menu_answer(&mut self, slot_id: u8, session_id: u16, choice: u8) -> Result<()> {
self.require_session(slot_id, session_id, ResourceId::MMI)?;
self.transport
.send_apdu(slot_id, session_id, ApduTag::MENU_ANSW, &[choice])
}
pub fn mmi_list_close(&mut self, slot_id: u8, session_id: u16) -> Result<()> {
self.mmi_menu_answer(slot_id, session_id, 0)
}
pub fn mmi_answer(
&mut self,
slot_id: u8,
session_id: u16,
answer: Option<&[u8]>,
) -> Result<()> {
self.require_session(slot_id, session_id, ResourceId::MMI)?;
self.transport
.send_apdu(slot_id, session_id, ApduTag::ANSW, &mmi::build_answ(answer))
}
pub fn mmi_close(&mut self, slot_id: u8, session_id: u16) -> Result<()> {
self.require_session(slot_id, session_id, ResourceId::MMI)?;
self.transport
.send_apdu(slot_id, session_id, ApduTag::CLOSE_MMI, &mmi::build_close())
}
pub fn ask_release(&mut self, slot_id: u8) -> Result<()> {
let session_id = self.find_session(slot_id, ResourceId::HOST_CONTROL)?;
self.transport
.send_apdu(slot_id, session_id, ApduTag::ASK_RELEASE, &[])
}
fn session(&self, session_id: u16) -> Option<&Session> {
let index = usize::from(session_id.checked_sub(1)?);
self.sessions.get(index)?.as_ref()
}
fn require_session(&self, slot_id: u8, session_id: u16, resource_id: ResourceId) -> Result<()> {
if matches!(
self.session(session_id),
Some(session)
if session.slot_id == slot_id
&& session.resource_id.base() == resource_id.base()
&& matches!(session.state, SessionState::Active)
) {
return Ok(());
}
Err(Error::InvalidProperty(format!(
"ca slot {}: no active {:?} session {}",
slot_id, resource_id, session_id
)))
}
fn find_session(&self, slot_id: u8, resource_id: ResourceId) -> Result<u16> {
for (index, entry) in self.sessions.iter().enumerate() {
if let Some(session) = entry
&& session.slot_id == slot_id
&& session.resource_id.base() == resource_id.base()
&& matches!(session.state, SessionState::Active)
{
return Ok((index + 1) as u16);
}
}
Err(Error::InvalidProperty(format!(
"ca slot {}: no open {:?} session",
slot_id, resource_id
)))
}
fn alloc_session(&mut self, slot_id: u8, resource_id: ResourceId) -> Option<u16> {
let in_use = self
.sessions
.iter()
.flatten()
.filter(|session| session.slot_id == slot_id)
.count();
if in_use >= MAX_SLOT_SESSIONS {
return None;
}
let index = match self.sessions.iter().position(Option::is_none) {
Some(index) => index,
None => {
self.sessions.push(None);
self.sessions.len() - 1
}
};
self.sessions[index] = Some(Session {
slot_id,
resource_id,
state: SessionState::Active,
});
Some((index + 1) as u16)
}
fn free_session(&mut self, session_id: u16) {
let Some(index) = session_id.checked_sub(1) else {
return;
};
let Some(session) = self
.sessions
.get_mut(usize::from(index))
.and_then(Option::take)
else {
return;
};
if let Some(resource) = self.resources.lookup(session.resource_id) {
resource.on_close(session.slot_id, session_id);
}
self.events.push_back(CaEvent::SessionClosed {
slot_id: session.slot_id,
session_id,
resource_id: session.resource_id,
});
}
fn dispatch_spdu(&mut self, slot_id: u8, data: &[u8]) -> Result<()> {
let spdu = match spdu::parse(data) {
Ok(spdu) => spdu,
Err(Error::InvalidData(context)) => {
self.events
.push_back(CaEvent::Malformed { slot_id, context });
return Ok(());
}
Err(e) => return Err(e),
};
match spdu {
Spdu::SessionNumber { session_id, apdu } => {
self.dispatch_apdu(slot_id, session_id, apdu)
}
Spdu::OpenSessionRequest { resource_id } => self.open_session(slot_id, resource_id),
Spdu::CloseSessionRequest { session_id } => self.close_session(slot_id, session_id),
Spdu::CloseSessionResponse { status, session_id } => {
self.close_session_complete(slot_id, session_id, status);
Ok(())
}
Spdu::CreateSessionResponse { .. } => {
self.events.push_back(CaEvent::Malformed {
slot_id,
context: format!("ca slot {}: unexpected create_session_response", slot_id),
});
Ok(())
}
}
}
fn open_session(&mut self, slot_id: u8, resource_id: ResourceId) -> Result<()> {
let available_resource_id = self
.resources
.lookup(resource_id)
.map(|resource| resource.resource_id());
let response_resource_id = available_resource_id.unwrap_or(resource_id);
let mut session_id = 0;
let status = match available_resource_id {
None => spdu::SS_NOT_ALLOCATED,
Some(available) if resource_id.version() > available.version() => {
spdu::SS_LOWER_VERSION
}
Some(available) => match self.alloc_session(slot_id, available) {
Some(allocated) => {
session_id = allocated;
spdu::SS_OK
}
None => spdu::SS_BUSY,
},
};
let response = spdu::build_open_session_response(status, response_resource_id, session_id);
self.transport.send_spdu(slot_id, &response)?;
if status != spdu::SS_OK {
self.events.push_back(CaEvent::SessionRefused {
slot_id,
resource_id,
status,
});
return Ok(());
}
self.events.push_back(CaEvent::SessionOpened {
slot_id,
session_id,
resource_id: response_resource_id,
});
let resource = self
.resources
.lookup(response_resource_id)
.expect("the resource is present: the session was allocated");
let mut ctx = ResourceContext {
transport: &mut self.transport,
events: &mut self.events,
slot_id,
session_id,
close_session: false,
};
let result = resource.on_open(&mut ctx);
Self::resource_result(&mut self.events, slot_id, result)
}
fn dispatch_apdu(&mut self, slot_id: u8, session_id: u16, data: &[u8]) -> Result<()> {
let resource_id = match self.session(session_id) {
Some(session) if session.slot_id == slot_id => session.resource_id,
_ => {
self.events.push_back(CaEvent::Malformed {
slot_id,
context: format!(
"ca slot {}: apdu on unknown session {}",
slot_id, session_id
),
});
return Ok(());
}
};
let resource = self
.resources
.lookup(resource_id)
.expect("the resource is present: the session was allocated");
let mut close_session = false;
for item in apdu::iter(data) {
match item {
Ok(item) => {
let mut ctx = ResourceContext {
transport: &mut self.transport,
events: &mut self.events,
slot_id,
session_id,
close_session: false,
};
let result = resource.on_apdu(&mut ctx, item.tag, item.body);
close_session |= ctx.close_session;
Self::resource_result(&mut self.events, slot_id, result)?;
}
Err(Error::InvalidData(context)) => {
self.events
.push_back(CaEvent::Malformed { slot_id, context });
break;
}
Err(e) => return Err(e),
}
}
if close_session {
self.request_close(slot_id, session_id)?;
}
Ok(())
}
fn resource_result(
events: &mut VecDeque<CaEvent>,
slot_id: u8,
result: Result<()>,
) -> Result<()> {
match result {
Err(Error::InvalidData(context)) => {
events.push_back(CaEvent::Malformed { slot_id, context });
Ok(())
}
result => result,
}
}
fn request_close(&mut self, slot_id: u8, session_id: u16) -> Result<()> {
let Some(index) = session_id.checked_sub(1).map(usize::from) else {
return Ok(());
};
match self.sessions.get_mut(index) {
Some(Some(session))
if session.slot_id == slot_id && matches!(session.state, SessionState::Active) =>
{
session.state = SessionState::Closing;
}
_ => return Ok(()),
}
let request = spdu::build_close_session_request(session_id);
self.transport.send_spdu(slot_id, &request)
}
fn close_session(&mut self, slot_id: u8, session_id: u16) -> Result<()> {
let known = matches!(
self.session(session_id),
Some(session) if session.slot_id == slot_id
);
let status = if known {
spdu::SS_OK
} else {
spdu::SS_NOT_ALLOCATED
};
let response = spdu::build_close_session_response(status, session_id);
self.transport.send_spdu(slot_id, &response)?;
if known {
self.free_session(session_id);
} else {
self.events.push_back(CaEvent::Malformed {
slot_id,
context: format!(
"ca slot {}: close request for unknown session {}",
slot_id, session_id
),
});
}
Ok(())
}
fn close_session_complete(&mut self, slot_id: u8, session_id: u16, status: u8) {
let closing = matches!(
self.session(session_id),
Some(session) if session.slot_id == slot_id
&& matches!(session.state, SessionState::Closing)
);
if !closing {
self.events.push_back(CaEvent::Malformed {
slot_id,
context: format!(
"ca slot {}: unexpected close response for session {}",
slot_id, session_id
),
});
return;
}
match status {
spdu::SS_OK | spdu::SS_NOT_ALLOCATED => self.free_session(session_id),
status => {
if let Some(Some(session)) = self.sessions.get_mut(usize::from(session_id - 1)) {
session.state = SessionState::Active;
}
self.events.push_back(CaEvent::Malformed {
slot_id,
context: format!(
"ca slot {}: invalid close response status 0x{:02X} for session {}",
slot_id, status, session_id
),
});
}
}
}
}
#[cfg(test)]
mod tests {
use std::{
fs::File,
io::{
ErrorKind,
Read,
Write,
},
os::{
fd::OwnedFd,
unix::net::UnixDatagram,
},
time::Duration,
};
use super::{
super::{
CaDevice,
asn1,
tpdu::TpduTag,
},
*,
};
struct TestCam {
file: File,
}
fn pair_slots(slots_num: u8) -> (CiSession, TestCam) {
let (host, cam) = UnixDatagram::pair().unwrap();
host.set_nonblocking(true).unwrap();
cam.set_nonblocking(true).unwrap();
let host = File::from(OwnedFd::from(host));
let cam = File::from(OwnedFd::from(cam));
let session = CiSession::new(CiTransport::new(CaDevice::from_file(host), slots_num));
(session, TestCam { file: cam })
}
fn pair() -> (CiSession, TestCam) {
pair_slots(1)
}
impl TestCam {
fn send_slot(&mut self, slot_id: u8, tag: TpduTag, payload: &[u8]) {
let t_c_id = slot_id + 1;
let mut frame = vec![slot_id, t_c_id];
match tag {
TpduTag::SB => {}
TpduTag::CTC_REPLY => frame.extend_from_slice(&[tag.raw(), 0x01, t_c_id]),
TpduTag::DATA_LAST | TpduTag::DATA_MORE => {
frame.push(tag.raw());
asn1::encode(payload.len() as u16 + 1, &mut frame);
frame.push(t_c_id);
frame.extend_from_slice(payload);
}
tag => panic!("unsupported test tag {:?}", tag),
}
frame.extend_from_slice(&[TpduTag::SB.raw(), 0x02, t_c_id, 0x00]);
self.file.write_all(&frame).unwrap();
}
fn send(&mut self, tag: TpduTag, payload: &[u8]) {
self.send_slot(0, tag, payload);
}
fn send_spdu(&mut self, spdu: &[u8]) {
self.send(TpduTag::DATA_LAST, spdu);
}
fn send_apdu(&mut self, session_id: u16, tag: ApduTag, body: &[u8]) {
self.send_apdu_slot(0, session_id, tag, body);
}
fn send_apdu_slot(&mut self, slot_id: u8, session_id: u16, tag: ApduTag, body: &[u8]) {
let mut payload = spdu::build_session_number(session_id);
apdu::build(&mut payload, tag, body);
self.send_slot(slot_id, TpduTag::DATA_LAST, &payload);
}
fn recv(&mut self) -> Option<Vec<u8>> {
let mut buf = [0; 2048];
match self.file.read(&mut buf) {
Ok(len) => Some(buf[.. len].to_vec()),
Err(e) if e.kind() == ErrorKind::WouldBlock => None,
Err(e) => panic!("cam link read: {}", e),
}
}
fn unwrap_data_slot(frame: &[u8], slot_id: u8) -> Vec<u8> {
let t_c_id = slot_id + 1;
assert_eq!(&frame[.. 2], &[slot_id, t_c_id], "slot and t_c_id");
assert_eq!(frame[2], TpduTag::DATA_LAST.raw(), "data frame tag");
let (length, consumed) = asn1::decode(&frame[3 ..]).unwrap();
assert_eq!(frame[3 + consumed], t_c_id, "t_c_id byte");
let start = 3 + consumed + 1;
assert_eq!(frame.len(), start + usize::from(length) - 1);
frame[start ..].to_vec()
}
}
fn pump_slot(session: &mut CiSession, cam: &mut TestCam, slot_id: u8) -> Vec<Vec<u8>> {
let mut spdus = Vec::new();
loop {
while session.recv().unwrap() {}
match cam.recv() {
Some(frame) => {
spdus.push(TestCam::unwrap_data_slot(&frame, slot_id));
assert!(
cam.recv().is_none(),
"two outstanding frames on slot {}",
slot_id
);
cam.send_slot(slot_id, TpduTag::SB, &[]);
}
None => break,
}
}
spdus
}
fn pump(session: &mut CiSession, cam: &mut TestCam) -> Vec<Vec<u8>> {
pump_slot(session, cam, 0)
}
fn events(session: &mut CiSession) -> Vec<CaEvent> {
let mut events = Vec::new();
while let Some(event) = session.next_event() {
events.push(event);
}
events
}
fn open_session_request(resource_id: ResourceId) -> Vec<u8> {
let raw = resource_id.raw();
vec![
0x91,
0x04,
(raw >> 24) as u8,
(raw >> 16) as u8,
(raw >> 8) as u8,
raw as u8,
]
}
fn open_session(session: &mut CiSession, cam: &mut TestCam, resource_id: ResourceId) -> u16 {
open_session_slot(session, cam, 0, resource_id)
}
fn open_session_slot(
session: &mut CiSession,
cam: &mut TestCam,
slot_id: u8,
resource_id: ResourceId,
) -> u16 {
if slot_id == 0 {
cam.send_spdu(&open_session_request(resource_id));
} else {
cam.send_slot(
slot_id,
TpduTag::DATA_LAST,
&open_session_request(resource_id),
);
}
pump_slot(session, cam, slot_id);
match events(session).first() {
Some(&CaEvent::SessionOpened {
slot_id: event_slot,
session_id,
..
}) if event_slot == slot_id => session_id,
event => panic!("expected SessionOpened, got {:?}", event),
}
}
#[test]
fn test_transport_ready() {
let (mut session, mut cam) = pair();
cam.send(TpduTag::CTC_REPLY, &[]);
assert!(session.recv().unwrap());
assert_eq!(
session.next_event(),
Some(CaEvent::TransportReady { slot_id: 0 })
);
}
#[test]
fn test_rm_handshake() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(ResourceId::RESOURCE_MANAGER));
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 2);
assert_eq!(
spdus[0],
vec![0x92, 0x07, 0x00, 0x00, 0x01, 0x00, 0x41, 0x00, 0x01]
);
assert_eq!(
spdus[1],
vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionOpened {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::RESOURCE_MANAGER,
}]
);
cam.send_apdu(1, ApduTag::PROFILE, &[]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x12, 0x00]]
);
cam.send_apdu(1, ApduTag::PROFILE_ENQ, &[]);
let spdus = pump(&mut session, &mut cam);
let mut expected = vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x11, 24];
expected.extend_from_slice(&[
0x00, 0x01, 0x00, 0x41, 0x00, 0x02, 0x00, 0x41, 0x00, 0x03, 0x00, 0x41, 0x00, 0x20, 0x00, 0x41, 0x00, 0x24, 0x00, 0x41, 0x00, 0x40, 0x00, 0x41, ]);
assert_eq!(spdus, vec![expected]);
assert!(events(&mut session).is_empty());
}
#[test]
fn test_open_session_refused() {
let (mut session, mut cam) = pair();
let unknown = ResourceId::new(0x00FF_0041);
cam.send_spdu(&open_session_request(unknown));
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x92, 0x07, 0xF0, 0x00, 0xFF, 0x00, 0x41, 0x00, 0x00]]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionRefused {
slot_id: 0,
resource_id: unknown,
status: 0xF0,
}]
);
cam.send_spdu(&open_session_request(ResourceId::new(0x0003_0042)));
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x92, 0x07, 0xF2, 0x00, 0x03, 0x00, 0x41, 0x00, 0x00]]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionRefused {
slot_id: 0,
resource_id: ResourceId::new(0x0003_0042),
status: 0xF2,
}]
);
}
#[test]
fn test_open_lower_resource_version_uses_host_version() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(ResourceId::new(0x0003_0040)));
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![
vec![0x92, 0x07, 0x00, 0x00, 0x03, 0x00, 0x41, 0x00, 0x01],
vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x30, 0x00],
]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionOpened {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::CONDITIONAL_ACCESS_SUPPORT,
}]
);
}
#[test]
fn test_session_exhaustion() {
let (mut session, mut cam) = pair();
for expected in 1 ..= 16 {
assert_eq!(
open_session(&mut session, &mut cam, ResourceId::HOST_CONTROL),
expected
);
}
cam.send_spdu(&open_session_request(ResourceId::HOST_CONTROL));
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x92, 0x07, 0xF3, 0x00, 0x20, 0x00, 0x41, 0x00, 0x00]]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionRefused {
slot_id: 0,
resource_id: ResourceId::HOST_CONTROL,
status: 0xF3,
}]
);
cam.send_spdu(&[0x95, 0x02, 0x00, 0x05]);
pump(&mut session, &mut cam);
events(&mut session);
assert_eq!(
open_session(&mut session, &mut cam, ResourceId::HOST_CONTROL),
5
);
}
#[test]
fn test_application_info() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(ResourceId::APPLICATION_INFORMATION));
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 2);
assert_eq!(
spdus[1],
vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x20, 0x00]
);
events(&mut session);
let info_body = [
0x01, 0x12, 0x34, 0x56, 0x78, 0x08, b'T', b'e', b's', b't', b' ', b'C', b'A', b'M',
];
cam.send_apdu(1, ApduTag::APPLICATION_INFO, &info_body);
pump(&mut session, &mut cam);
let info = ApplicationInfo {
application_type: 0x01,
application_manufacturer: 0x1234,
manufacturer_code: 0x5678,
menu_string: b"Test CAM".to_vec(),
};
assert_eq!(
events(&mut session),
vec![CaEvent::ApplicationInfo {
slot_id: 0,
info: info.clone(),
}]
);
assert_eq!(session.app_info(0), Some(&info));
assert_eq!(session.app_info(1), None);
session.enter_menu(0).unwrap();
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x22, 0x00]]
);
}
#[test]
fn test_application_info_survives_another_session_close() {
let (mut session, mut cam) = pair();
let first = open_session(&mut session, &mut cam, ResourceId::APPLICATION_INFORMATION);
let second = open_session(&mut session, &mut cam, ResourceId::APPLICATION_INFORMATION);
cam.send_apdu(
second,
ApduTag::APPLICATION_INFO,
&[0x01, 0, 2, 0, 2, 3, b'T', b'w', b'o'],
);
pump(&mut session, &mut cam);
events(&mut session);
assert_eq!(session.app_info(0).unwrap().menu_string, b"Two");
session.enter_menu(0).unwrap();
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![0x90, 0x02, 0x00, 0x02, 0x9F, 0x80, 0x22, 0x00]]
);
cam.send_apdu(
first,
ApduTag::APPLICATION_INFO,
&[0x01, 0, 1, 0, 1, 3, b'O', b'n', b'e'],
);
pump(&mut session, &mut cam);
events(&mut session);
assert_eq!(session.app_info(0).unwrap().menu_string, b"One");
session.enter_menu(0).unwrap();
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x22, 0x00]]
);
cam.send_spdu(&[0x95, 0x02, 0x00, first as u8]);
pump(&mut session, &mut cam);
events(&mut session);
assert_eq!(session.app_info(0).unwrap().menu_string, b"Two");
}
#[test]
fn test_conditional_access_info() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(
ResourceId::CONDITIONAL_ACCESS_SUPPORT,
));
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![
vec![0x92, 0x07, 0x00, 0x00, 0x03, 0x00, 0x41, 0x00, 0x01],
vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x30, 0x00],
]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionOpened {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::CONDITIONAL_ACCESS_SUPPORT,
}]
);
assert_eq!(session.session_caids(0, 1), None);
assert!(session.caids(0).is_empty());
cam.send_apdu(
1,
ApduTag::CA_INFO,
&[0x05, 0x00, 0x01, 0x00, 0x05, 0x00, 0x0B, 0x00],
);
assert!(pump(&mut session, &mut cam).is_empty());
assert_eq!(
events(&mut session),
vec![CaEvent::CaInfo {
slot_id: 0,
session_id: 1,
caids: vec![0x0500, 0x0100, 0x0500, 0x0B00],
}]
);
assert_eq!(
session.session_caids(0, 1),
Some([0x0500, 0x0100, 0x0500, 0x0B00].as_slice())
);
assert_eq!(session.caids(0), [0x0100, 0x0500, 0x0B00]);
cam.send_apdu(1, ApduTag::CA_INFO, &[0x01, 0x00, 0xFF]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
assert_eq!(
session.session_caids(0, 1),
Some([0x0500, 0x0100, 0x0500, 0x0B00].as_slice())
);
cam.send_apdu(1, ApduTag::CA_INFO, &[]);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::CaInfo {
slot_id: 0,
session_id: 1,
caids: Vec::new(),
}]
);
assert_eq!(session.session_caids(0, 1), Some([].as_slice()));
cam.send_spdu(&[0x95, 0x02, 0x00, 0x01]);
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![0x96, 0x03, 0x00, 0x00, 0x01]]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionClosed {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::CONDITIONAL_ACCESS_SUPPORT,
}]
);
assert_eq!(session.session_caids(0, 1), None);
assert!(session.caids(0).is_empty());
}
#[test]
fn test_conditional_access_sessions_are_independent() {
let (mut session, mut cam) = pair();
let first = open_session(
&mut session,
&mut cam,
ResourceId::CONDITIONAL_ACCESS_SUPPORT,
);
let second = open_session(
&mut session,
&mut cam,
ResourceId::CONDITIONAL_ACCESS_SUPPORT,
);
assert_eq!((first, second), (1, 2));
cam.send_apdu(first, ApduTag::CA_INFO, &[0x05, 0x00, 0x01, 0x00]);
cam.send_apdu(second, ApduTag::CA_INFO, &[0x06, 0x04, 0x05, 0x00]);
pump(&mut session, &mut cam);
events(&mut session);
assert_eq!(
session.session_caids(0, first),
Some([0x0500, 0x0100].as_slice())
);
assert_eq!(
session.session_caids(0, second),
Some([0x0604, 0x0500].as_slice())
);
assert_eq!(session.caids(0), [0x0100, 0x0500, 0x0604]);
cam.send_spdu(&[0x95, 0x02, 0x00, first as u8]);
pump(&mut session, &mut cam);
events(&mut session);
assert_eq!(session.session_caids(0, first), None);
assert_eq!(session.caids(0), [0x0500, 0x0604]);
let reused = open_session(
&mut session,
&mut cam,
ResourceId::CONDITIONAL_ACCESS_SUPPORT,
);
assert_eq!(reused, first);
assert_eq!(session.session_caids(0, reused), None);
assert_eq!(session.caids(0), [0x0500, 0x0604]);
}
#[test]
fn test_conditional_access_caids_do_not_cross_slots() {
let (mut session, mut cam) = pair_slots(2);
let first = open_session_slot(
&mut session,
&mut cam,
0,
ResourceId::CONDITIONAL_ACCESS_SUPPORT,
);
let second = open_session_slot(
&mut session,
&mut cam,
1,
ResourceId::CONDITIONAL_ACCESS_SUPPORT,
);
cam.send_apdu_slot(0, first, ApduTag::CA_INFO, &[0x01, 0x00]);
pump_slot(&mut session, &mut cam, 0);
cam.send_apdu_slot(1, second, ApduTag::CA_INFO, &[0x06, 0x04]);
pump_slot(&mut session, &mut cam, 1);
events(&mut session);
assert_eq!(session.caids(0), [0x0100]);
assert_eq!(session.caids(1), [0x0604]);
assert_eq!(session.session_caids(0, second), None);
assert_eq!(session.session_caids(1, first), None);
session.drop_slot(0);
events(&mut session);
assert!(session.caids(0).is_empty());
assert_eq!(session.caids(1), [0x0604]);
}
#[test]
fn test_mmi_dialogue() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
assert_eq!(session_id, 1);
cam.send_apdu(session_id, ApduTag::DISPLAY_CONTROL, &[0x01, 0x01]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![
0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x02, 0x02, 0x01, 0x01
]]
);
let mut menu_body = vec![0x02];
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"Menu");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"Info");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"Exit");
cam.send_apdu(session_id, ApduTag::MENU_LAST, &menu_body);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiMenu {
slot_id: 0,
session_id,
menu: MmiMenu {
title: b"Menu".to_vec(),
sub_title: Vec::new(),
bottom: Vec::new(),
items: vec![b"Info".to_vec(), b"Exit".to_vec()],
},
}]
);
session.mmi_menu_answer(0, session_id, 2).unwrap();
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x0B, 0x01, 0x02]]
);
let mut enq_body = vec![0x01, 0x04];
enq_body.extend_from_slice(b"PIN:");
cam.send_apdu(session_id, ApduTag::ENQ, &enq_body);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiEnq {
slot_id: 0,
session_id,
blind: true,
answer_len: 4,
text: b"PIN:".to_vec(),
}]
);
session
.mmi_answer(0, session_id, Some(b"1234"))
.unwrap();
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![
0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x08, 0x05, 0x01, b'1', b'2', b'3', b'4',
]]
);
cam.send_apdu(session_id, ApduTag::CLOSE_MMI, &[0x00]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus, vec![vec![0x95, 0x02, 0x00, 0x01]]);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiClose {
slot_id: 0,
session_id,
delay: None,
}]
);
cam.send_spdu(&[0x96, 0x03, 0x00, 0x00, 0x01]);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionClosed {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::MMI,
}]
);
assert!(session.mmi_menu_answer(0, session_id, 1).is_err());
}
#[test]
fn test_mmi_commands_target_exact_session() {
let (mut session, mut cam) = pair();
let first = open_session(&mut session, &mut cam, ResourceId::MMI);
let second = open_session(&mut session, &mut cam, ResourceId::MMI);
session.mmi_menu_answer(0, second, 2).unwrap();
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![0x90, 0x02, 0x00, 0x02, 0x9F, 0x88, 0x0B, 0x01, 0x02]]
);
session.mmi_answer(0, first, Some(b"1")).unwrap();
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![
0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x08, 0x02, 0x01, b'1',
]]
);
assert!(session.mmi_close(1, second).is_err());
assert!(pump(&mut session, &mut cam).is_empty());
}
#[test]
fn test_fragmented_spdu() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::APPLICATION_INFORMATION);
let mut body = vec![0x01, 0x12, 0x34, 0x56, 0x78, 200];
body.extend_from_slice(&[b'x'; 200]);
let mut payload = spdu::build_session_number(session_id);
apdu::build(&mut payload, ApduTag::APPLICATION_INFO, &body);
let (head, tail) = payload.split_at(100);
cam.send(TpduTag::DATA_MORE, head);
cam.send(TpduTag::DATA_LAST, tail);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::ApplicationInfo { slot_id: 0, .. }]
));
assert_eq!(session.app_info(0).unwrap().menu_string, vec![b'x'; 200]);
}
#[test]
fn test_mmi_chained_objects() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
let mut menu_body = vec![0x02];
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"Menu");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"Info");
apdu::build(&mut menu_body, ApduTag::TEXT_LAST, b"Exit");
let (head, tail) = menu_body.split_at(7);
cam.send_apdu(session_id, ApduTag::MENU_MORE, head);
cam.send_apdu(session_id, ApduTag::MENU_LAST, tail);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiMenu {
slot_id: 0,
session_id,
menu: MmiMenu {
title: b"Menu".to_vec(),
sub_title: Vec::new(),
bottom: Vec::new(),
items: vec![b"Info".to_vec(), b"Exit".to_vec()],
},
}]
);
cam.send_apdu(session_id, ApduTag::TEXT_MORE, b"Hello, ");
cam.send_apdu(session_id, ApduTag::TEXT_LAST, b"world");
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiText {
slot_id: 0,
session_id,
text: b"Hello, world".to_vec(),
}]
);
let (head, tail) = menu_body.split_at(10);
cam.send_apdu(session_id, ApduTag::LIST_MORE, head);
cam.send_apdu(session_id, ApduTag::LIST_LAST, tail);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiList {
slot_id: 0,
session_id,
menu: MmiMenu {
title: b"Menu".to_vec(),
sub_title: Vec::new(),
bottom: Vec::new(),
items: vec![b"Info".to_vec(), b"Exit".to_vec()],
},
}]
);
session.mmi_list_close(0, session_id).unwrap();
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x0B, 0x01, 0x00]]
);
}
#[test]
fn test_mmi_display_control_errors() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
cam.send_apdu(session_id, ApduTag::DISPLAY_CONTROL, &[0x02]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x02, 0x01, 0xF0]]
);
cam.send_apdu(session_id, ApduTag::DISPLAY_CONTROL, &[0x01, 0x02]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x02, 0x01, 0xF1]]
);
}
#[test]
fn test_mmi_close_delay() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
cam.send_apdu(session_id, ApduTag::CLOSE_MMI, &[0x01, 0x05]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus, vec![vec![0x95, 0x02, 0x00, 0x01]]);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiClose {
slot_id: 0,
session_id,
delay: Some(5),
}]
);
}
#[test]
fn test_closing_session_races() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
cam.send_apdu(session_id, ApduTag::CLOSE_MMI, &[0x00]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus, vec![vec![0x95, 0x02, 0x00, 0x01]]);
events(&mut session);
cam.send_apdu(session_id, ApduTag::TEXT_LAST, b"bye");
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::MmiText {
slot_id: 0,
session_id,
text: b"bye".to_vec(),
}]
);
cam.send_apdu(session_id, ApduTag::CLOSE_MMI, &[0x00]);
let spdus = pump(&mut session, &mut cam);
assert!(spdus.is_empty());
events(&mut session);
cam.send_spdu(&[0x95, 0x02, 0x00, 0x01]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus, vec![vec![0x96, 0x03, 0x00, 0x00, 0x01]]);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionClosed {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::MMI,
}]
);
cam.send_spdu(&[0x96, 0x03, 0x00, 0x00, 0x01]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
}
#[test]
fn test_close_session_response_status() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
session.request_close(0, session_id).unwrap();
pump(&mut session, &mut cam);
cam.send_spdu(&[0x96, 0x03, spdu::SS_NOT_ALLOCATED, 0x00, 0x01]);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionClosed {
slot_id: 0,
session_id,
resource_id: ResourceId::MMI,
}]
);
assert!(session.mmi_menu_answer(0, session_id, 1).is_err());
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
session.request_close(0, session_id).unwrap();
pump(&mut session, &mut cam);
cam.send_spdu(&[0x96, 0x03, 0x01, 0x00, 0x01]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
session.mmi_menu_answer(0, session_id, 1).unwrap();
assert_eq!(
pump(&mut session, &mut cam),
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x88, 0x0B, 0x01, 0x01]]
);
}
#[test]
fn test_malformed_spdu() {
let (mut session, mut cam) = pair();
cam.send_spdu(&[0x91, 0x05, 0x00, 0x01, 0x00, 0x41]);
let spdus = pump(&mut session, &mut cam);
assert!(spdus.is_empty());
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
cam.send_spdu(&[0x93, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
assert_eq!(
open_session(&mut session, &mut cam, ResourceId::RESOURCE_MANAGER),
1
);
}
#[test]
fn test_rm_profile_change_from_module() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::RESOURCE_MANAGER);
cam.send_apdu(session_id, ApduTag::PROFILE_CHANGE, &[]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x10, 0x00]]
);
assert!(events(&mut session).is_empty());
}
#[test]
fn test_date_time_periodic() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(ResourceId::DATE_TIME));
pump(&mut session, &mut cam);
events(&mut session);
cam.send_apdu(1, ApduTag::DATE_TIME_ENQ, &[30]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 1);
let now = Instant::now();
session.tick_at(now).unwrap();
assert!(pump(&mut session, &mut cam).is_empty());
session.tick_at(now + Duration::from_secs(29)).unwrap();
assert!(pump(&mut session, &mut cam).is_empty());
session.tick_at(now + Duration::from_secs(30)).unwrap();
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 1);
assert_eq!(
&spdus[0][.. 8],
&[0x90, 0x02, 0x00, 0x01, 0x9F, 0x84, 0x41, 0x05]
);
session.tick().unwrap();
assert!(pump(&mut session, &mut cam).is_empty());
}
#[test]
fn test_date_time_enq_empty_body() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(ResourceId::DATE_TIME));
pump(&mut session, &mut cam);
events(&mut session);
cam.send_apdu(1, ApduTag::DATE_TIME_ENQ, &[]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 1);
assert_eq!(
&spdus[0][.. 8],
&[0x90, 0x02, 0x00, 0x01, 0x9F, 0x84, 0x41, 0x05]
);
assert!(events(&mut session).is_empty());
}
#[test]
fn test_two_slots() {
let (mut session, mut cam) = pair_slots(2);
let first = open_session(&mut session, &mut cam, ResourceId::HOST_CONTROL);
assert_eq!(first, 1);
cam.send_slot(
1,
TpduTag::DATA_LAST,
&open_session_request(ResourceId::MMI),
);
let spdus = pump_slot(&mut session, &mut cam, 1);
assert_eq!(
spdus,
vec![vec![0x92, 0x07, 0x00, 0x00, 0x40, 0x00, 0x41, 0x00, 0x02]]
);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionOpened {
slot_id: 1,
session_id: 2,
resource_id: ResourceId::MMI,
}]
);
let mut payload = spdu::build_session_number(1);
apdu::build(&mut payload, ApduTag::CLEAR_REPLACE, &[0x01]);
cam.send_slot(1, TpduTag::DATA_LAST, &payload);
pump_slot(&mut session, &mut cam, 1);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 1, .. }]
));
session.drop_slot(0);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionClosed {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::HOST_CONTROL,
}]
);
session.mmi_close(1, 2).unwrap();
let spdus = pump_slot(&mut session, &mut cam, 1);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x02, 0x9F, 0x88, 0x00, 0x01, 0x00]]
);
}
#[test]
fn test_date_time() {
let (mut session, mut cam) = pair();
cam.send_spdu(&open_session_request(ResourceId::DATE_TIME));
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 2);
assert_eq!(
&spdus[1][.. 8],
&[0x90, 0x02, 0x00, 0x01, 0x9F, 0x84, 0x41, 0x05]
);
assert_eq!(spdus[1].len(), 13);
let mjd = (u16::from(spdus[1][8]) << 8) | u16::from(spdus[1][9]);
assert!(mjd >= 61041, "mjd {}", mjd);
cam.send_apdu(1, ApduTag::DATE_TIME_ENQ, &[30]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus.len(), 1);
assert_eq!(
&spdus[0][.. 8],
&[0x90, 0x02, 0x00, 0x01, 0x9F, 0x84, 0x41, 0x05]
);
session.tick().unwrap();
assert!(pump(&mut session, &mut cam).is_empty());
}
#[test]
fn test_host_control() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::HOST_CONTROL);
cam.send_apdu(
session_id,
ApduTag::TUNE,
&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],
);
cam.send_apdu(
session_id,
ApduTag::REPLACE,
&[0x07, 0xFF, 0xFE, 0xE1, 0x23],
);
cam.send_apdu(session_id, ApduTag::CLEAR_REPLACE, &[0x07]);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![
CaEvent::Tune {
slot_id: 0,
network_id: 0x1122,
original_network_id: 0x3344,
transport_stream_id: 0x5566,
service_id: 0x7788,
},
CaEvent::Replace {
slot_id: 0,
replace_ref: 0x07,
replaced_pid: 0x1FFE,
replacement_pid: 0x0123,
},
CaEvent::ClearReplace {
slot_id: 0,
replace_ref: 0x07,
},
]
);
session.ask_release(0).unwrap();
let spdus = pump(&mut session, &mut cam);
assert_eq!(
spdus,
vec![vec![0x90, 0x02, 0x00, 0x01, 0x9F, 0x84, 0x03, 0x00]]
);
}
#[test]
fn test_session_number_reuse() {
let (mut session, mut cam) = pair();
assert_eq!(
open_session(&mut session, &mut cam, ResourceId::RESOURCE_MANAGER),
1
);
assert_eq!(
open_session(&mut session, &mut cam, ResourceId::APPLICATION_INFORMATION),
2
);
cam.send_spdu(&[0x95, 0x02, 0x00, 0x01]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus, vec![vec![0x96, 0x03, 0x00, 0x00, 0x01]]);
assert_eq!(
events(&mut session),
vec![CaEvent::SessionClosed {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::RESOURCE_MANAGER,
}]
);
assert_eq!(open_session(&mut session, &mut cam, ResourceId::MMI), 1);
}
#[test]
fn test_close_unknown_session() {
let (mut session, mut cam) = pair();
cam.send_spdu(&[0x95, 0x02, 0x00, 0x63]);
let spdus = pump(&mut session, &mut cam);
assert_eq!(spdus, vec![vec![0x96, 0x03, 0xF0, 0x00, 0x63]]);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
}
#[test]
fn test_malformed_apdu_keeps_slot_going() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::HOST_CONTROL);
cam.send_apdu(0x63, ApduTag::TUNE, &[0; 8]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
cam.send_apdu(session_id, ApduTag::PROFILE_ENQ, &[]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
cam.send_apdu(session_id, ApduTag::TUNE, &[0x11, 0x22]);
pump(&mut session, &mut cam);
assert!(matches!(
events(&mut session).as_slice(),
[CaEvent::Malformed { slot_id: 0, .. }]
));
cam.send_apdu(session_id, ApduTag::CLEAR_REPLACE, &[0x01]);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![CaEvent::ClearReplace {
slot_id: 0,
replace_ref: 0x01,
}]
);
}
#[test]
fn test_empty_session_number_tail() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::RESOURCE_MANAGER);
cam.send_spdu(&spdu::build_session_number(session_id));
pump(&mut session, &mut cam);
assert!(events(&mut session).is_empty());
}
#[test]
fn test_packed_apdus() {
let (mut session, mut cam) = pair();
let session_id = open_session(&mut session, &mut cam, ResourceId::HOST_CONTROL);
let mut payload = spdu::build_session_number(session_id);
apdu::build(
&mut payload,
ApduTag::REPLACE,
&[0x01, 0x00, 0x64, 0x00, 0xC8],
);
apdu::build(&mut payload, ApduTag::CLEAR_REPLACE, &[0x01]);
cam.send_spdu(&payload);
pump(&mut session, &mut cam);
assert_eq!(
events(&mut session),
vec![
CaEvent::Replace {
slot_id: 0,
replace_ref: 0x01,
replaced_pid: 0x0064,
replacement_pid: 0x00C8,
},
CaEvent::ClearReplace {
slot_id: 0,
replace_ref: 0x01,
},
]
);
}
#[test]
fn test_drop_slot() {
let (mut session, mut cam) = pair();
open_session(&mut session, &mut cam, ResourceId::APPLICATION_INFORMATION);
let mmi_session_id = open_session(&mut session, &mut cam, ResourceId::MMI);
cam.send_apdu(
1,
ApduTag::APPLICATION_INFO,
&[0x01, 0x12, 0x34, 0x56, 0x78, 0x00],
);
pump(&mut session, &mut cam);
events(&mut session);
assert!(session.app_info(0).is_some());
session.drop_slot(0);
assert_eq!(
events(&mut session),
vec![
CaEvent::SessionClosed {
slot_id: 0,
session_id: 1,
resource_id: ResourceId::APPLICATION_INFORMATION,
},
CaEvent::SessionClosed {
slot_id: 0,
session_id: 2,
resource_id: ResourceId::MMI,
},
]
);
assert!(session.app_info(0).is_none());
assert!(session.mmi_menu_answer(0, mmi_session_id, 1).is_err());
assert_eq!(
open_session(&mut session, &mut cam, ResourceId::RESOURCE_MANAGER),
1
);
}
}