From 148ecd7a72d46f1958f5d43f63619bbdaa7571b0 Mon Sep 17 00:00:00 2001 From: CinderSocket Date: Wed, 17 Jun 2026 15:24:33 -0700 Subject: [PATCH] Add host-tested board and HF bridge policies --- Makefile | 4 + board_identity.c | 18 +++ board_identity.h | 14 ++ hf_bridge_policy.c | 73 +++++++++++ hf_bridge_policy.h | 42 ++++++ lib/host_tests/test_board_identity.c | 46 +++++++ lib/host_tests/test_hf_bridge_policy.c | 171 +++++++++++++++++++++++++ lib/host_tests/test_main.c | 4 + 8 files changed, 372 insertions(+) create mode 100644 board_identity.c create mode 100644 board_identity.h create mode 100644 hf_bridge_policy.c create mode 100644 hf_bridge_policy.h create mode 100644 lib/host_tests/test_board_identity.c create mode 100644 lib/host_tests/test_hf_bridge_policy.c diff --git a/Makefile b/Makefile index 1f0992f..1a5eada 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ test-host: cc $(HOST_TEST_CFLAGS) \ lib/host_tests/vendor/munit/munit.c \ lib/host_tests/test_main.c \ + lib/host_tests/test_board_identity.c \ lib/host_tests/test_lrc.c \ lib/host_tests/test_board_power_lifecycle.c \ lib/host_tests/test_hf_read_lifecycle.c \ @@ -29,11 +30,13 @@ test-host: lib/host_tests/test_uhf_status_label.c \ lib/host_tests/test_credential_sio_label.c \ lib/host_tests/test_hf_read_plan.c \ + lib/host_tests/test_hf_bridge_policy.c \ lib/host_tests/test_wiegand_plugin.c \ lib/host_tests/test_runtime_policy.c \ lib/host_tests/t1_test_stubs.c \ lib/host_tests/bit_buffer_mock.c \ lrc.c \ + board_identity.c \ board_power_lifecycle.c \ sam_startup_ui.c \ ccid_logic.c \ @@ -48,6 +51,7 @@ test-host: uhf_tag_config_view.c \ uhf_snmp_probe.c \ hf_read_lifecycle.c \ + hf_bridge_policy.c \ seader_hf_read_plan.c \ wiegand_interface_fal/wiegand.c \ runtime_policy.c \ diff --git a/board_identity.c b/board_identity.c new file mode 100644 index 0000000..2f82d69 --- /dev/null +++ b/board_identity.c @@ -0,0 +1,18 @@ +#include "board_identity.h" + +SeaderBoardClass seader_board_classify(bool pa4_high, bool pc1_high, bool pc0_high) { + if(pa4_high) { + return SeaderBoardClassUhfCarrier; + } + + if(pc1_high || pc0_high) { + return SeaderBoardClassSamOnly; + } + + return SeaderBoardClassNone; +} + +bool seader_board_class_supports_uhf(SeaderBoardClass board_class) { + return board_class == SeaderBoardClassUhfCarrier; +} + diff --git a/board_identity.h b/board_identity.h new file mode 100644 index 0000000..e35cb76 --- /dev/null +++ b/board_identity.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +typedef enum { + SeaderBoardClassUnknown = 0, + SeaderBoardClassNone, + SeaderBoardClassSamOnly, + SeaderBoardClassUhfCarrier, +} SeaderBoardClass; + +SeaderBoardClass seader_board_classify(bool pa4_high, bool pc1_high, bool pc0_high); +bool seader_board_class_supports_uhf(SeaderBoardClass board_class); + diff --git a/hf_bridge_policy.c b/hf_bridge_policy.c new file mode 100644 index 0000000..30bd010 --- /dev/null +++ b/hf_bridge_policy.c @@ -0,0 +1,73 @@ +#include "hf_bridge_policy.h" + +int seader_hf_bridge_begin_conversation( + void* context, + const SeaderHfBridgeConversationOps* ops, + int stop_command) { + if(!ops || !ops->set_conversation || !ops->begin_card_session || !ops->set_fail || + !ops->run_conversation) { + return stop_command; + } + + ops->set_conversation(context); + if(!ops->begin_card_session(context)) { + ops->set_fail(context); + return stop_command; + } + + return ops->run_conversation(context); +} + +uint16_t seader_hf_bridge_rf_status_code(SeaderHfBridgeRfStatus status) { + switch(status) { + case SeaderHfBridgeRfStatusSuccess: + return 0x0000U; + case SeaderHfBridgeRfStatusTimeout: + return 0x0020U; + case SeaderHfBridgeRfStatusCrc: + case SeaderHfBridgeRfStatusProtocol: + default: + return 0x0004U; + } +} + +void seader_hf_bridge_rf_status_bytes(SeaderHfBridgeRfStatus status, uint8_t bytes[2]) { + if(!bytes) { + return; + } + + const uint16_t code = seader_hf_bridge_rf_status_code(status); + bytes[0] = (uint8_t)((code >> 8) & 0xFFU); + bytes[1] = (uint8_t)(code & 0xFFU); +} + +SeaderHfBridgeApduDecision seader_hf_bridge_apdu_decision( + bool virtual_credential, + bool conversation_stage, + size_t len, + size_t max_len, + bool queue_has_space) { + if(!virtual_credential && !conversation_stage) { + return SeaderHfBridgeApduDecisionDiscardStale; + } + + if(len > max_len || !queue_has_space) { + return SeaderHfBridgeApduDecisionFailProtocol; + } + + return SeaderHfBridgeApduDecisionQueue; +} + +uint32_t seader_hf_bridge_timeout_us_to_fwt_fc(uint32_t timeout_us) { + if(timeout_us == 0U) { + return 0U; + } + + const uint64_t fwt_fc = ((uint64_t)timeout_us * 1356U + 99U) / 100U; + if(fwt_fc > UINT32_MAX) { + return UINT32_MAX; + } + + return (uint32_t)fwt_fc; +} + diff --git a/hf_bridge_policy.h b/hf_bridge_policy.h new file mode 100644 index 0000000..8beb0e6 --- /dev/null +++ b/hf_bridge_policy.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +typedef enum { + SeaderHfBridgeRfStatusSuccess = 0, + SeaderHfBridgeRfStatusTimeout, + SeaderHfBridgeRfStatusCrc, + SeaderHfBridgeRfStatusProtocol, +} SeaderHfBridgeRfStatus; + +typedef enum { + SeaderHfBridgeApduDecisionDiscardStale = 0, + SeaderHfBridgeApduDecisionQueue, + SeaderHfBridgeApduDecisionFailProtocol, +} SeaderHfBridgeApduDecision; + +typedef struct { + void (*set_conversation)(void* context); + bool (*begin_card_session)(void* context); + void (*set_fail)(void* context); + int (*run_conversation)(void* context); +} SeaderHfBridgeConversationOps; + +int seader_hf_bridge_begin_conversation( + void* context, + const SeaderHfBridgeConversationOps* ops, + int stop_command); + +uint16_t seader_hf_bridge_rf_status_code(SeaderHfBridgeRfStatus status); +void seader_hf_bridge_rf_status_bytes(SeaderHfBridgeRfStatus status, uint8_t bytes[2]); +SeaderHfBridgeApduDecision seader_hf_bridge_apdu_decision( + bool virtual_credential, + bool conversation_stage, + size_t len, + size_t max_len, + bool queue_has_space); + +/* SAM nfcSend.timeOut is microseconds; Flipper NFC uses 13.56 MHz carrier cycles. */ +uint32_t seader_hf_bridge_timeout_us_to_fwt_fc(uint32_t timeout_us); diff --git a/lib/host_tests/test_board_identity.c b/lib/host_tests/test_board_identity.c new file mode 100644 index 0000000..1075510 --- /dev/null +++ b/lib/host_tests/test_board_identity.c @@ -0,0 +1,46 @@ +#include "munit.h" + +#include "board_identity.h" + +static MunitResult test_classifies_strap_states(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_int( + seader_board_classify(false, false, false), ==, SeaderBoardClassNone); + munit_assert_int( + seader_board_classify(false, true, false), ==, SeaderBoardClassSamOnly); + munit_assert_int( + seader_board_classify(false, false, true), ==, SeaderBoardClassSamOnly); + munit_assert_int( + seader_board_classify(true, false, false), ==, SeaderBoardClassUhfCarrier); + munit_assert_int( + seader_board_classify(true, true, true), ==, SeaderBoardClassUhfCarrier); + return MUNIT_OK; +} + +static MunitResult test_uhf_support_policy(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_false(seader_board_class_supports_uhf(SeaderBoardClassUnknown)); + munit_assert_false(seader_board_class_supports_uhf(SeaderBoardClassNone)); + munit_assert_false(seader_board_class_supports_uhf(SeaderBoardClassSamOnly)); + munit_assert_true(seader_board_class_supports_uhf(SeaderBoardClassUhfCarrier)); + return MUNIT_OK; +} + +static MunitTest test_board_identity_cases[] = { + {(char*)"/strap-classifier", test_classifies_strap_states, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/uhf-support-policy", test_uhf_support_policy, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_board_identity_suite = { + "", + test_board_identity_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; + diff --git a/lib/host_tests/test_hf_bridge_policy.c b/lib/host_tests/test_hf_bridge_policy.c new file mode 100644 index 0000000..6240a2c --- /dev/null +++ b/lib/host_tests/test_hf_bridge_policy.c @@ -0,0 +1,171 @@ +#include "munit.h" + +#include "hf_bridge_policy.h" + +typedef struct { + unsigned index; + const char* calls[4]; + bool begin_result; + int run_result; +} BridgeRecorder; + +static void record_call(BridgeRecorder* recorder, const char* name) { + if(recorder && recorder->index < (sizeof(recorder->calls) / sizeof(recorder->calls[0]))) { + recorder->calls[recorder->index++] = name; + } +} + +static void record_set_conversation(void* context) { + record_call(context, "set-conversation"); +} + +static bool record_begin_card_session(void* context) { + BridgeRecorder* recorder = context; + record_call(recorder, "begin-card-session"); + return recorder->begin_result; +} + +static void record_set_fail(void* context) { + record_call(context, "set-fail"); +} + +static int record_run_conversation(void* context) { + BridgeRecorder* recorder = context; + record_call(recorder, "run-conversation"); + return recorder->run_result; +} + +static const SeaderHfBridgeConversationOps bridge_ops = { + .set_conversation = record_set_conversation, + .begin_card_session = record_begin_card_session, + .set_fail = record_set_fail, + .run_conversation = record_run_conversation, +}; + +static MunitResult test_begin_conversation_sets_stage_before_card_session( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + BridgeRecorder recorder = { + .begin_result = true, + .run_result = 42, + }; + + const int result = seader_hf_bridge_begin_conversation(&recorder, &bridge_ops, -1); + + munit_assert_int(result, ==, 42); + munit_assert_uint(recorder.index, ==, 3); + munit_assert_string_equal(recorder.calls[0], "set-conversation"); + munit_assert_string_equal(recorder.calls[1], "begin-card-session"); + munit_assert_string_equal(recorder.calls[2], "run-conversation"); + return MUNIT_OK; +} + +static MunitResult test_begin_conversation_fails_without_running_conversation( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + BridgeRecorder recorder = { + .begin_result = false, + .run_result = 42, + }; + + const int result = seader_hf_bridge_begin_conversation(&recorder, &bridge_ops, -1); + + munit_assert_int(result, ==, -1); + munit_assert_uint(recorder.index, ==, 3); + munit_assert_string_equal(recorder.calls[0], "set-conversation"); + munit_assert_string_equal(recorder.calls[1], "begin-card-session"); + munit_assert_string_equal(recorder.calls[2], "set-fail"); + return MUNIT_OK; +} + +static MunitResult test_rf_status_mapping(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + uint8_t bytes[2] = {0xFF, 0xFF}; + + seader_hf_bridge_rf_status_bytes(SeaderHfBridgeRfStatusSuccess, bytes); + munit_assert_uint8(bytes[0], ==, 0x00); + munit_assert_uint8(bytes[1], ==, 0x00); + + seader_hf_bridge_rf_status_bytes(SeaderHfBridgeRfStatusTimeout, bytes); + munit_assert_uint8(bytes[0], ==, 0x00); + munit_assert_uint8(bytes[1], ==, 0x20); + + seader_hf_bridge_rf_status_bytes(SeaderHfBridgeRfStatusCrc, bytes); + munit_assert_uint8(bytes[0], ==, 0x00); + munit_assert_uint8(bytes[1], ==, 0x04); + + seader_hf_bridge_rf_status_bytes(SeaderHfBridgeRfStatusProtocol, bytes); + munit_assert_uint8(bytes[0], ==, 0x00); + munit_assert_uint8(bytes[1], ==, 0x04); + return MUNIT_OK; +} + +static MunitResult test_apdu_decision_discards_stale_messages( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + munit_assert_int( + seader_hf_bridge_apdu_decision(false, false, 16U, 258U, true), + ==, + SeaderHfBridgeApduDecisionDiscardStale); + munit_assert_int( + seader_hf_bridge_apdu_decision(true, false, 16U, 258U, true), + ==, + SeaderHfBridgeApduDecisionQueue); + return MUNIT_OK; +} + +static MunitResult test_apdu_decision_fails_overflow( + const MunitParameter params[], + void* fixture) { + (void)params; + (void)fixture; + + munit_assert_int( + seader_hf_bridge_apdu_decision(false, true, 259U, 258U, true), + ==, + SeaderHfBridgeApduDecisionFailProtocol); + munit_assert_int( + seader_hf_bridge_apdu_decision(false, true, 16U, 258U, false), + ==, + SeaderHfBridgeApduDecisionFailProtocol); + return MUNIT_OK; +} + +static MunitResult test_timeout_us_to_fwt_fc(const MunitParameter params[], void* fixture) { + (void)params; + (void)fixture; + + munit_assert_uint32(seader_hf_bridge_timeout_us_to_fwt_fc(0U), ==, 0U); + munit_assert_uint32(seader_hf_bridge_timeout_us_to_fwt_fc(1000U), ==, 13560U); + munit_assert_uint32(seader_hf_bridge_timeout_us_to_fwt_fc(20000U), ==, 271200U); + return MUNIT_OK; +} + +static MunitTest test_hf_bridge_policy_cases[] = { + {(char*)"/begin-conversation-order", test_begin_conversation_sets_stage_before_card_session, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/begin-conversation-fail", test_begin_conversation_fails_without_running_conversation, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/rf-status-mapping", test_rf_status_mapping, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/apdu-discard-stale", test_apdu_decision_discards_stale_messages, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/apdu-overflow-fails", test_apdu_decision_fails_overflow, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {(char*)"/timeout-us-to-fwt", test_timeout_us_to_fwt_fc, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL}, + {NULL, NULL, NULL, NULL, 0, NULL}, +}; + +MunitSuite test_hf_bridge_policy_suite = { + "", + test_hf_bridge_policy_cases, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE, +}; diff --git a/lib/host_tests/test_main.c b/lib/host_tests/test_main.c index 081cc3c..3ce337e 100644 --- a/lib/host_tests/test_main.c +++ b/lib/host_tests/test_main.c @@ -1,6 +1,7 @@ #include "munit.h" extern MunitSuite test_lrc_suite; +extern MunitSuite test_board_identity_suite; extern MunitSuite test_board_power_lifecycle_suite; extern MunitSuite test_hf_read_lifecycle_suite; extern MunitSuite test_sam_startup_ui_suite; @@ -12,12 +13,14 @@ extern MunitSuite test_snmp_suite; extern MunitSuite test_uhf_status_label_suite; extern MunitSuite test_credential_sio_label_suite; extern MunitSuite test_hf_read_plan_suite; +extern MunitSuite test_hf_bridge_policy_suite; extern MunitSuite test_runtime_policy_suite; extern MunitSuite test_wiegand_plugin_suite; int main(int argc, char* argv[]) { MunitSuite child_suites[] = { {"/lrc", test_lrc_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/board-identity", test_board_identity_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/board-power-lifecycle", test_board_power_lifecycle_suite.tests, NULL, @@ -41,6 +44,7 @@ int main(int argc, char* argv[]) { {"/uhf-status-label", test_uhf_status_label_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/credential-sio-label", test_credential_sio_label_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/hf-read-plan", test_hf_read_plan_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, + {"/hf-bridge-policy", test_hf_bridge_policy_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/runtime-policy", test_runtime_policy_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {"/wiegand-plugin", test_wiegand_plugin_suite.tests, NULL, 1, MUNIT_SUITE_OPTION_NONE}, {NULL, NULL, NULL, 0, 0},