mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-22 22:28:22 +03:00
update parsing logic and add tests
This commit is contained in:
+199
-23
@@ -2,10 +2,8 @@ use std::{sync::{Mutex, OnceLock}, time::{Duration, Instant}};
|
||||
|
||||
use tracing::debug;
|
||||
|
||||
use crate::utils::{parse_access_code, parse_idm_hex};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub (crate) struct StoredCard {
|
||||
pub struct StoredCard {
|
||||
pub access_code: [u8; 10],
|
||||
pub card_idm: Option<[u8; 8]>,
|
||||
|
||||
@@ -18,16 +16,14 @@ impl StoredCard {
|
||||
}
|
||||
}
|
||||
|
||||
pub (crate) enum CardPresentResult {
|
||||
pub enum CardPresentResult {
|
||||
Accepted,
|
||||
Rejected(String /* reason */),
|
||||
SlotUnavailable(Instant /* release time */),
|
||||
}
|
||||
|
||||
static PRESENTER: OnceLock<CardPresenter> = OnceLock::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub (crate) struct CardPresenter {
|
||||
pub struct CardPresenter {
|
||||
current_card: Mutex<Option<StoredCard>>,
|
||||
}
|
||||
|
||||
@@ -62,29 +58,16 @@ impl CardPresenter {
|
||||
card
|
||||
}
|
||||
|
||||
pub fn present_card(&self, access_code: &String, idm_hex: Option<&String>, validity: Duration) -> CardPresentResult {
|
||||
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 access_code_bytes = match parse_access_code(access_code) {
|
||||
Some(bytes) => bytes,
|
||||
None => return CardPresentResult::Rejected("Invalid access code format".to_string()),
|
||||
};
|
||||
|
||||
let mut card_idm_bytes = None;
|
||||
if let Some(idm_hex) = idm_hex {
|
||||
match parse_idm_hex(idm_hex) {
|
||||
Some(bytes) => card_idm_bytes = Some(bytes),
|
||||
None => return CardPresentResult::Rejected("Invalid IDm format".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
let stored_card = StoredCard {
|
||||
access_code: access_code_bytes,
|
||||
card_idm: card_idm_bytes,
|
||||
access_code,
|
||||
card_idm: idm_hex,
|
||||
expiry_time: Instant::now() + validity,
|
||||
};
|
||||
|
||||
@@ -92,4 +75,197 @@ impl CardPresenter {
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-14
@@ -67,26 +67,21 @@ pub extern "C" fn aime_io_nfc_poll(unit_no: u8) -> i32 {
|
||||
}
|
||||
};
|
||||
|
||||
if aime_code.is_empty() || !utils::is_valid_access_code(&aime_code) {
|
||||
warn!("Access code file is either empty or contains invalid data");
|
||||
if aime_code.is_empty() {
|
||||
warn!("Access code file is empty");
|
||||
return 1;
|
||||
}
|
||||
|
||||
debug!("Access code read from file");
|
||||
let presenter_result = CardPresenter::instance().present_card(
|
||||
&aime_code,
|
||||
None,
|
||||
std::time::Duration::from_secs(5),
|
||||
);
|
||||
let Some(access_code) = utils::parse_access_code(&aime_code) else {
|
||||
warn!("Invalid access code read from file");
|
||||
return 1;
|
||||
};
|
||||
|
||||
match presenter_result {
|
||||
crate::card::CardPresentResult::Accepted => {
|
||||
match CardPresenter::instance().present_card(access_code, None, std::time::Duration::from_secs(5)) {
|
||||
card::CardPresentResult::Accepted => {
|
||||
return 0;
|
||||
},
|
||||
crate::card::CardPresentResult::Rejected(reason) => {
|
||||
warn!("Read access code was rejected: {}", reason);
|
||||
}
|
||||
crate::card::CardPresentResult::SlotUnavailable(release_time) => {
|
||||
card::CardPresentResult::SlotUnavailable(release_time) => {
|
||||
warn!("Access code was not accepted as there is already a card in the active slot until {:?}", release_time);
|
||||
}
|
||||
}
|
||||
|
||||
+108
-16
@@ -41,25 +41,117 @@ pub (crate) fn parse_idm_hex(idm_hex: &String) -> Option<[u8; 8]> {
|
||||
Some(idm_bytes)
|
||||
}
|
||||
|
||||
pub (crate) fn is_valid_access_code(code: &String) -> bool {
|
||||
if code.len() > 20 {
|
||||
return false;
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
let cleaned_code: String = code.chars().filter(|c| !c.is_whitespace()).collect();
|
||||
mod format_hex_bytes_tests {
|
||||
use super::*;
|
||||
|
||||
if cleaned_code.len() != 20 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut seen_non_zero = false;
|
||||
let is_all_digits = cleaned_code.chars().all(|c| {
|
||||
if c != '0' {
|
||||
seen_non_zero = true;
|
||||
#[test]
|
||||
fn formats_empty_bytes() {
|
||||
assert_eq!(format_hex_bytes(&[]), "");
|
||||
}
|
||||
|
||||
c.is_ascii_digit()
|
||||
});
|
||||
#[test]
|
||||
fn formats_access_code_bytes() {
|
||||
let bytes = [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56];
|
||||
assert_eq!(format_hex_bytes(&bytes), "0010 1234 5678 9012 3456");
|
||||
}
|
||||
}
|
||||
|
||||
is_all_digits && seen_non_zero
|
||||
mod parse_access_code_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_full_20_digit_code() {
|
||||
let code = "00101234567890123456".to_string();
|
||||
let result = parse_access_code(&code);
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_code_with_spaces() {
|
||||
let code = "0010 1234 5678 9012 3456".to_string();
|
||||
let result = parse_access_code(&code);
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), [0x00, 0x10, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_code_with_leading_zeros() {
|
||||
let code = "00000000000000001234".to_string(); // Valid 20-digit code with leading zeros
|
||||
let result = parse_access_code(&code);
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_invalid_hex() {
|
||||
let code = "GHIJKLMNOPQRSTUVWXYZ".to_string();
|
||||
let result = parse_access_code(&code);
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_code_with_all_zeros() {
|
||||
let code = "00000000000000000000".to_string();
|
||||
let result = parse_access_code(&code);
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), [0x00; 10]);
|
||||
}
|
||||
}
|
||||
|
||||
mod parse_idm_hex_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_full_16_char_idm() {
|
||||
let idm = "0102030405060708".to_string();
|
||||
let result = parse_idm_hex(&idm);
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_short_idm_returns_none() {
|
||||
let idm = "1234".to_string();
|
||||
let result = parse_idm_hex(&idm);
|
||||
|
||||
// IDm is too short - should fail
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_too_long_idm() {
|
||||
let idm = "01020304050607080910".to_string(); // 20 chars
|
||||
let result = parse_idm_hex(&idm);
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_non_hex_characters() {
|
||||
let idm = "0102GHIJ".to_string();
|
||||
let result = parse_idm_hex(&idm);
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_empty_idm_returns_none() {
|
||||
let idm = "".to_string();
|
||||
let result = parse_idm_hex(&idm);
|
||||
|
||||
// Empty IDm should fail
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ pub fn build_server() -> Router {
|
||||
.layer(services)
|
||||
}
|
||||
|
||||
fn build_error_response(status: StatusCode, message: &str) -> Response {
|
||||
let error = crate::webapi::ServerError {
|
||||
error: message.to_string(),
|
||||
};
|
||||
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
|
||||
async fn server_state_handler() -> Json<crate::webapi::ServerState> {
|
||||
let config = Config::instance();
|
||||
let metrics = SystemMetrics::instance();
|
||||
@@ -49,23 +57,26 @@ async fn card_submission_handler(
|
||||
return build_error_response(StatusCode::BAD_REQUEST, "Access code is required");
|
||||
}
|
||||
|
||||
if !utils::is_valid_access_code(&payload.access_code) {
|
||||
let Some(access_code) = utils::parse_access_code(&payload.access_code) else {
|
||||
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid access code provided");
|
||||
}
|
||||
};
|
||||
|
||||
if payload.card_idm_hex.as_ref().is_some_and(|idm| idm.chars().any(|c| !c.is_ascii_hexdigit())) {
|
||||
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid IDm provided");
|
||||
}
|
||||
let card_idm_bytes = if let Some(idm_hex) = payload.card_idm_hex.as_ref() {
|
||||
if idm_hex.len() != 16 {
|
||||
return build_error_response(StatusCode::BAD_REQUEST, "IDm must be a 16-char hex string");
|
||||
}
|
||||
|
||||
let presenter = CardPresenter::instance().present_card(
|
||||
&payload.access_code,
|
||||
payload.card_idm_hex.as_ref(),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
let Some(idm_bytes) = utils::parse_idm_hex(idm_hex) else {
|
||||
return build_error_response(StatusCode::UNPROCESSABLE_ENTITY, "Invalid IDm provided");
|
||||
};
|
||||
|
||||
match presenter {
|
||||
Some(idm_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match CardPresenter::instance().present_card(access_code, card_idm_bytes, Duration::from_secs(5)) {
|
||||
CardPresentResult::Accepted => StatusCode::ACCEPTED.into_response(),
|
||||
CardPresentResult::Rejected(message) => build_error_response(StatusCode::FORBIDDEN, format!("Card rejected: {}", message).as_str()),
|
||||
CardPresentResult::SlotUnavailable(release_time) => {
|
||||
let now = Instant::now();
|
||||
let wait_secs = if release_time > now {
|
||||
@@ -86,11 +97,3 @@ async fn card_submission_handler(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_error_response(status: StatusCode, message: &str) -> Response {
|
||||
let error = crate::webapi::ServerError {
|
||||
error: message.to_string(),
|
||||
};
|
||||
|
||||
(status, Json(error)).into_response()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user