use std::{sync::{Mutex, OnceLock}, time::{Duration, Instant}}; use tracing::debug; #[derive(Debug, Clone)] pub struct StoredCard { pub access_code: [u8; 10], pub card_idm: Option<[u8; 8]>, pub expiry_time: Instant } impl StoredCard { pub fn is_expired(&self) -> bool { Instant::now() > self.expiry_time } } pub enum CardPresentResult { Accepted, SlotUnavailable(Instant /* release time */), } static PRESENTER: OnceLock = OnceLock::new(); pub struct CardPresenter { current_card: Mutex>, } impl Default for CardPresenter { fn default() -> Self { Self { current_card: Mutex::new(None), } } } impl CardPresenter { pub fn instance() -> &'static CardPresenter { PRESENTER.get_or_init(|| CardPresenter::default()) } pub fn request_card(&self, require_idm: Option) -> Option { let mut guard = self.current_card.lock().unwrap(); let card = guard.as_ref()?; if let Some(require) = require_idm && card.card_idm.is_some() != require { return None; } let card = guard.take(); if let Some(ref stored_card) = card && stored_card.is_expired() { debug!("Stored card has expired and will not be provided"); return None; }; card } pub fn present_card(&self, access_code: [u8; 10], idm_hex: Option<[u8; 8]>, validity: Duration) -> CardPresentResult { let mut guard = self.current_card.lock().unwrap(); if let Some(card) = guard.as_ref() && !card.is_expired() { return CardPresentResult::SlotUnavailable(card.expiry_time); } let stored_card = StoredCard { access_code, card_idm: idm_hex, expiry_time: Instant::now() + validity, }; *guard = Some(stored_card); CardPresentResult::Accepted } } #[cfg(test)] mod tests { use super::*; use crate::utils::{parse_access_code, parse_idm_hex}; mod stored_card_tests { use super::*; #[test] fn is_expired_returns_false_for_future_expiry() { let card = StoredCard { access_code: [0; 10], card_idm: None, expiry_time: Instant::now() + Duration::from_secs(60), }; assert!(!card.is_expired()); } #[test] fn is_expired_returns_true_for_past_expiry() { let card = StoredCard { access_code: [0; 10], card_idm: None, expiry_time: Instant::now() - Duration::from_millis(1), }; assert!(card.is_expired()); } } mod card_presenter_tests { use super::*; fn create_presenter() -> CardPresenter { CardPresenter::default() } #[test] fn request_card_returns_none_when_empty() { let presenter = create_presenter(); assert!(presenter.request_card(None).is_none()); } #[test] fn present_card_accepts_valid_access_code() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); match presenter.present_card(access_code, None, Duration::from_secs(5)) { CardPresentResult::Accepted => {} _ => panic!("Expected Accepted"), } } #[test] fn present_card_rejects_invalid_access_code() { let access_code = parse_access_code(&"INVALID".to_string()); assert!(access_code.is_none(), "Expected parsing to fail for invalid access code"); } #[test] fn present_card_accepts_valid_idm() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); let idm = parse_idm_hex(&"0102030405060708".to_string()).unwrap(); match presenter.present_card(access_code, Some(idm), Duration::from_secs(5)) { CardPresentResult::Accepted => {} _ => panic!("Expected Accepted"), } } #[test] fn present_card_rejects_invalid_idm() { let idm = parse_idm_hex(&"INVALID_IDM".to_string()); assert!(idm.is_none(), "Expected parsing to fail for invalid IDm"); } #[test] fn present_card_returns_slot_unavailable_when_card_present() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); // Present first card presenter.present_card(access_code, None, Duration::from_secs(5)); // Try to present second card match presenter.present_card(access_code, None, Duration::from_secs(5)) { CardPresentResult::SlotUnavailable(_) => {} _ => panic!("Expected SlotUnavailable"), } } #[test] fn request_card_returns_card_after_present() { let presenter = create_presenter(); let access_code = parse_access_code(&"00101234567890123456".to_string()).unwrap(); presenter.present_card(access_code, None, Duration::from_secs(5)); let card = presenter.request_card(None); assert!(card.is_some()); let card = card.unwrap(); assert_eq!(card.access_code, [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]); } #[test] fn request_card_consumes_the_card() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); presenter.present_card(access_code, None, Duration::from_secs(5)); // First request should succeed assert!(presenter.request_card(None).is_some()); // Second request should return None assert!(presenter.request_card(None).is_none()); } #[test] fn request_card_filters_by_idm_requirement_true() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); // Present card without IDm presenter.present_card(access_code, None, Duration::from_secs(5)); // Request card that requires IDm should return None assert!(presenter.request_card(Some(true)).is_none()); } #[test] fn request_card_filters_by_idm_requirement_false() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); let idm = parse_idm_hex(&"0102030405060708".to_string()).unwrap(); // Present card with IDm presenter.present_card(access_code, Some(idm), Duration::from_secs(5)); // Request card that requires no IDm should return None assert!(presenter.request_card(Some(false)).is_none()); } #[test] fn request_card_returns_card_with_idm_when_required() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); let idm = parse_idm_hex(&"0102030405060708".to_string()).unwrap(); presenter.present_card(access_code, Some(idm), Duration::from_secs(5)); let card = presenter.request_card(Some(true)); assert!(card.is_some()); assert!(card.unwrap().card_idm.is_some()); } #[test] fn expired_card_cannot_be_requested() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); // Present card with very short validity presenter.present_card(access_code, None, Duration::from_millis(1)); // Wait for expiry std::thread::sleep(Duration::from_millis(10)); // Card should not be available assert!(presenter.request_card(None).is_none()); } #[test] fn can_present_card_after_previous_expires() { let presenter = create_presenter(); let access_code = parse_access_code(&"12345678901234567890".to_string()).unwrap(); // Present first card with very short validity presenter.present_card(access_code, None, Duration::from_millis(1)); // Wait for expiry std::thread::sleep(Duration::from_millis(10)); // Should be able to present another card match presenter.present_card(access_code, None, Duration::from_secs(5)) { CardPresentResult::Accepted => {} _ => panic!("Expected Accepted after previous card expired"), } } } }