mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-22 22:28:22 +03:00
271 lines
8.8 KiB
Rust
271 lines
8.8 KiB
Rust
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<CardPresenter> = OnceLock::new();
|
|
|
|
pub struct CardPresenter {
|
|
current_card: Mutex<Option<StoredCard>>,
|
|
}
|
|
|
|
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<bool>) -> Option<StoredCard> {
|
|
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"),
|
|
}
|
|
}
|
|
}
|
|
} |