use std::{
collections::HashMap,
time::{
Duration,
Instant,
SystemTime,
UNIX_EPOCH,
},
};
use super::{
super::{
apdu::ApduTag,
transport::CiTransport,
},
Resource,
ResourceContext,
ResourceId,
};
use crate::error::{
Error,
Result,
};
const MJD_UNIX_EPOCH: u64 = 40587;
fn bcd(value: u8) -> u8 {
((value / 10) << 4) | (value % 10)
}
fn date_time_body(unix_time: Duration) -> [u8; 5] {
let mjd = (MJD_UNIX_EPOCH + unix_time.as_secs() / 86400).min(u64::from(u16::MAX));
let time = unix_time.as_secs() % 86400;
[
(mjd >> 8) as u8,
mjd as u8,
bcd((time / 3600) as u8),
bcd((time / 60 % 60) as u8),
bcd((time % 60) as u8),
]
}
fn now_body() -> [u8; 5] {
let unix_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
date_time_body(unix_time)
}
struct DateTimeSession {
slot_id: u8,
interval: u8,
sent: Option<Instant>,
}
pub struct DateTimeResource {
sessions: HashMap<u16, DateTimeSession>,
}
impl DateTimeResource {
pub fn new() -> Self {
DateTimeResource {
sessions: HashMap::new(),
}
}
}
impl Resource for DateTimeResource {
fn resource_id(&self) -> ResourceId {
ResourceId::DATE_TIME
}
fn on_open(&mut self, ctx: &mut ResourceContext<'_>) -> Result<()> {
self.sessions.insert(
ctx.session_id,
DateTimeSession {
slot_id: ctx.slot_id,
interval: 0,
sent: None,
},
);
ctx.send_apdu(ApduTag::DATE_TIME, &now_body())
}
fn on_apdu(&mut self, ctx: &mut ResourceContext<'_>, tag: ApduTag, body: &[u8]) -> Result<()> {
match tag {
ApduTag::DATE_TIME_ENQ => {
let interval = body.first().copied().unwrap_or(0);
if let Some(session) = self.sessions.get_mut(&ctx.session_id) {
session.interval = interval;
session.sent = None;
}
ctx.send_apdu(ApduTag::DATE_TIME, &now_body())
}
tag => Err(Error::InvalidData(format!(
"ca slot {}: unexpected date-time apdu tag {:?}",
ctx.slot_id, tag
))),
}
}
fn on_close(&mut self, _slot_id: u8, session_id: u16) {
self.sessions.remove(&session_id);
}
fn on_tick(&mut self, transport: &mut CiTransport, now: Instant) -> Result<()> {
for (&session_id, session) in self.sessions.iter_mut() {
if session.interval == 0 {
continue;
}
let Some(sent) = session.sent else {
session.sent = Some(now);
continue;
};
if now.saturating_duration_since(sent)
< Duration::from_secs(u64::from(session.interval))
{
continue;
}
transport.send_apdu(session.slot_id, session_id, ApduTag::DATE_TIME, &now_body())?;
session.sent = Some(now);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bcd() {
assert_eq!(bcd(0), 0x00);
assert_eq!(bcd(9), 0x09);
assert_eq!(bcd(10), 0x10);
assert_eq!(bcd(23), 0x23);
assert_eq!(bcd(59), 0x59);
}
#[test]
fn test_date_time_body() {
assert_eq!(
date_time_body(Duration::ZERO),
[0x9E, 0x8B, 0x00, 0x00, 0x00]
);
assert_eq!(
date_time_body(Duration::from_secs(86399)),
[0x9E, 0x8B, 0x23, 0x59, 0x59]
);
assert_eq!(
date_time_body(Duration::from_secs(
20638 * 86400 + 12 * 3600 + 34 * 60 + 56
)),
[0xEF, 0x29, 0x12, 0x34, 0x56]
);
}
#[test]
fn test_date_time_body_mjd_saturation() {
let far_future = Duration::from_secs(3_000_000_000);
assert_eq!(&date_time_body(far_future)[.. 2], &[0xFF, 0xFF]);
}
}