mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-11 06:09:08 -07:00
Merge remote-tracking branch 'mntm/dev' into ofw-3822-nestednonces
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
Filetype: Flipper SubGhz Key File
|
||||
Version: 1
|
||||
Frequency: 433920000
|
||||
Preset: FuriHalSubGhzPresetOok270Async
|
||||
Latitute: nan
|
||||
Longitude: nan
|
||||
Protocol: Bresser-3CH
|
||||
Id: 23
|
||||
Bit: 40
|
||||
Data: 00 00 00 17 26 0C 40 89
|
||||
Batt: 0
|
||||
Hum: 64
|
||||
Ts: 1726436378
|
||||
Ch: 2
|
||||
Btn: 0
|
||||
Temp: 18.222225
|
||||
File diff suppressed because one or more lines are too long
51
applications/debug/unit_tests/tests/furi/furi_errno_test.c
Normal file
51
applications/debug/unit_tests/tests/furi/furi_errno_test.c
Normal file
@@ -0,0 +1,51 @@
|
||||
#include <furi.h>
|
||||
#include <errno.h>
|
||||
#include "../test.h" // IWYU pragma: keep
|
||||
|
||||
#define TAG "ErrnoTest"
|
||||
#define THREAD_CNT 16
|
||||
#define ITER_CNT 1000
|
||||
|
||||
static int32_t errno_fuzzer(void* context) {
|
||||
int start_value = (int)context;
|
||||
int32_t fails = 0;
|
||||
|
||||
for(int i = start_value; i < start_value + ITER_CNT; i++) {
|
||||
errno = i;
|
||||
furi_thread_yield();
|
||||
if(errno != i) fails++;
|
||||
}
|
||||
|
||||
for(int i = 0; i < ITER_CNT; i++) {
|
||||
errno = 0;
|
||||
furi_thread_yield();
|
||||
UNUSED(strtol("123456", NULL, 10)); // -V530
|
||||
furi_thread_yield();
|
||||
if(errno != 0) fails++;
|
||||
|
||||
errno = 0;
|
||||
furi_thread_yield();
|
||||
UNUSED(strtol("123456123456123456123456123456123456123456123456", NULL, 10)); // -V530
|
||||
furi_thread_yield();
|
||||
if(errno != ERANGE) fails++;
|
||||
}
|
||||
|
||||
return fails;
|
||||
}
|
||||
|
||||
void test_errno_saving(void) {
|
||||
FuriThread* threads[THREAD_CNT];
|
||||
|
||||
for(int i = 0; i < THREAD_CNT; i++) {
|
||||
int start_value = i * ITER_CNT;
|
||||
threads[i] = furi_thread_alloc_ex("ErrnoFuzzer", 1024, errno_fuzzer, (void*)start_value);
|
||||
furi_thread_set_priority(threads[i], FuriThreadPriorityNormal);
|
||||
furi_thread_start(threads[i]);
|
||||
}
|
||||
|
||||
for(int i = 0; i < THREAD_CNT; i++) {
|
||||
furi_thread_join(threads[i]);
|
||||
mu_assert_int_eq(0, furi_thread_get_return_code(threads[i]));
|
||||
furi_thread_free(threads[i]);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ void test_furi_concurrent_access(void);
|
||||
void test_furi_pubsub(void);
|
||||
void test_furi_memmgr(void);
|
||||
void test_furi_event_loop(void);
|
||||
void test_errno_saving(void);
|
||||
|
||||
static int foo = 0;
|
||||
|
||||
@@ -42,6 +43,10 @@ MU_TEST(mu_test_furi_event_loop) {
|
||||
test_furi_event_loop();
|
||||
}
|
||||
|
||||
MU_TEST(mu_test_errno_saving) {
|
||||
test_errno_saving();
|
||||
}
|
||||
|
||||
MU_TEST_SUITE(test_suite) {
|
||||
MU_SUITE_CONFIGURE(&test_setup, &test_teardown);
|
||||
MU_RUN_TEST(test_check);
|
||||
@@ -51,6 +56,7 @@ MU_TEST_SUITE(test_suite) {
|
||||
MU_RUN_TEST(mu_test_furi_pubsub);
|
||||
MU_RUN_TEST(mu_test_furi_memmgr);
|
||||
MU_RUN_TEST(mu_test_furi_event_loop);
|
||||
MU_RUN_TEST(mu_test_errno_saving);
|
||||
}
|
||||
|
||||
int run_minunit_test_furi(void) {
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
// This is a hack to access internal storage functions and definitions
|
||||
#include <storage/storage_i.h>
|
||||
|
||||
#define UNIT_TESTS_PATH(path) EXT_PATH("unit_tests/" path)
|
||||
#define UNIT_TESTS_RESOURCES_PATH(path) EXT_PATH("unit_tests/" path)
|
||||
#define UNIT_TESTS_PATH(path) EXT_PATH(".tmp/unit_tests/" path)
|
||||
|
||||
#define STORAGE_LOCKED_FILE EXT_PATH("locked_file.test")
|
||||
#define STORAGE_LOCKED_FILE UNIT_TESTS_PATH("locked_file.test")
|
||||
#define STORAGE_LOCKED_DIR STORAGE_INT_PATH_PREFIX
|
||||
|
||||
#define STORAGE_TEST_DIR UNIT_TESTS_PATH("test_dir")
|
||||
@@ -369,33 +370,78 @@ MU_TEST(storage_file_rename) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
mu_check(write_file_13DA(storage, EXT_PATH("file.old")));
|
||||
mu_check(check_file_13DA(storage, EXT_PATH("file.old")));
|
||||
mu_check(write_file_13DA(storage, UNIT_TESTS_PATH("file.old")));
|
||||
mu_check(check_file_13DA(storage, UNIT_TESTS_PATH("file.old")));
|
||||
mu_assert_int_eq(
|
||||
FSE_OK, storage_common_rename(storage, EXT_PATH("file.old"), EXT_PATH("file.new")));
|
||||
mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, EXT_PATH("file.old"), NULL));
|
||||
mu_assert_int_eq(FSE_OK, storage_common_stat(storage, EXT_PATH("file.new"), NULL));
|
||||
mu_check(check_file_13DA(storage, EXT_PATH("file.new")));
|
||||
mu_assert_int_eq(FSE_OK, storage_common_remove(storage, EXT_PATH("file.new")));
|
||||
FSE_OK,
|
||||
storage_common_rename(storage, UNIT_TESTS_PATH("file.old"), UNIT_TESTS_PATH("file.new")));
|
||||
mu_assert_int_eq(
|
||||
FSE_NOT_EXIST, storage_common_stat(storage, UNIT_TESTS_PATH("file.old"), NULL));
|
||||
mu_assert_int_eq(FSE_OK, storage_common_stat(storage, UNIT_TESTS_PATH("file.new"), NULL));
|
||||
mu_check(check_file_13DA(storage, UNIT_TESTS_PATH("file.new")));
|
||||
mu_assert_int_eq(FSE_OK, storage_common_remove(storage, UNIT_TESTS_PATH("file.new")));
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
static const char* dir_rename_tests[][2] = {
|
||||
{UNIT_TESTS_PATH("dir.old"), UNIT_TESTS_PATH("dir.new")},
|
||||
{UNIT_TESTS_PATH("test_dir"), UNIT_TESTS_PATH("test_dir-new")},
|
||||
{UNIT_TESTS_PATH("test"), UNIT_TESTS_PATH("test-test")},
|
||||
};
|
||||
|
||||
MU_TEST(storage_dir_rename) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
storage_dir_create(storage, EXT_PATH("dir.old"));
|
||||
for(size_t i = 0; i < COUNT_OF(dir_rename_tests); i++) {
|
||||
const char* old_path = dir_rename_tests[i][0];
|
||||
const char* new_path = dir_rename_tests[i][1];
|
||||
|
||||
mu_check(storage_dir_rename_check(storage, EXT_PATH("dir.old")));
|
||||
storage_dir_create(storage, old_path);
|
||||
mu_check(storage_dir_rename_check(storage, old_path));
|
||||
|
||||
mu_assert_int_eq(FSE_OK, storage_common_rename(storage, old_path, new_path));
|
||||
mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, old_path, NULL));
|
||||
mu_check(storage_dir_rename_check(storage, new_path));
|
||||
|
||||
storage_dir_remove(storage, new_path);
|
||||
mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, new_path, NULL));
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
MU_TEST(storage_equiv_and_subdir) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
mu_assert_int_eq(
|
||||
FSE_OK, storage_common_rename(storage, EXT_PATH("dir.old"), EXT_PATH("dir.new")));
|
||||
mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, EXT_PATH("dir.old"), NULL));
|
||||
mu_check(storage_dir_rename_check(storage, EXT_PATH("dir.new")));
|
||||
true,
|
||||
storage_common_equivalent_path(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah")));
|
||||
mu_assert_int_eq(
|
||||
true,
|
||||
storage_common_equivalent_path(
|
||||
storage, UNIT_TESTS_PATH("blah/"), UNIT_TESTS_PATH("blah/")));
|
||||
mu_assert_int_eq(
|
||||
false,
|
||||
storage_common_equivalent_path(
|
||||
storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah-blah")));
|
||||
mu_assert_int_eq(
|
||||
false,
|
||||
storage_common_equivalent_path(
|
||||
storage, UNIT_TESTS_PATH("blah/"), UNIT_TESTS_PATH("blah-blah/")));
|
||||
|
||||
storage_dir_remove(storage, EXT_PATH("dir.new"));
|
||||
mu_assert_int_eq(FSE_NOT_EXIST, storage_common_stat(storage, EXT_PATH("dir.new"), NULL));
|
||||
mu_assert_int_eq(
|
||||
true, storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah")));
|
||||
mu_assert_int_eq(
|
||||
true,
|
||||
storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah/blah")));
|
||||
mu_assert_int_eq(
|
||||
false,
|
||||
storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah/blah"), UNIT_TESTS_PATH("blah")));
|
||||
mu_assert_int_eq(
|
||||
false,
|
||||
storage_common_is_subdir(storage, UNIT_TESTS_PATH("blah"), UNIT_TESTS_PATH("blah-blah")));
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
@@ -403,10 +449,13 @@ MU_TEST(storage_dir_rename) {
|
||||
MU_TEST_SUITE(storage_rename) {
|
||||
MU_RUN_TEST(storage_file_rename);
|
||||
MU_RUN_TEST(storage_dir_rename);
|
||||
MU_RUN_TEST(storage_equiv_and_subdir);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
storage_dir_remove(storage, EXT_PATH("dir.old"));
|
||||
storage_dir_remove(storage, EXT_PATH("dir.new"));
|
||||
for(size_t i = 0; i < COUNT_OF(dir_rename_tests); i++) {
|
||||
storage_dir_remove(storage, dir_rename_tests[i][0]);
|
||||
storage_dir_remove(storage, dir_rename_tests[i][1]);
|
||||
}
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
@@ -653,7 +702,7 @@ MU_TEST(test_md5_calc) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
const char* path = UNIT_TESTS_PATH("storage/md5.txt");
|
||||
const char* path = UNIT_TESTS_RESOURCES_PATH("storage/md5.txt");
|
||||
const char* md5_cstr = "2a456fa43e75088fdde41c93159d62a2";
|
||||
const uint8_t md5[MD5_HASH_SIZE] = {
|
||||
0x2a,
|
||||
|
||||
@@ -677,6 +677,13 @@ MU_TEST(subghz_decoder_solight_te44_test) {
|
||||
"Test decoder " WS_PROTOCOL_SOLIGHT_TE44_NAME " error\r\n");
|
||||
}
|
||||
|
||||
MU_TEST(subghz_decoder_bresser_3ch_test) {
|
||||
mu_assert(
|
||||
subghz_decoder_test(
|
||||
EXT_PATH("unit_tests/subghz/bresser_3ch_raw.sub"), WS_PROTOCOL_BRESSER_3CH_NAME),
|
||||
"Test decoder " WS_PROTOCOL_BRESSER_3CH_NAME " error\r\n");
|
||||
}
|
||||
|
||||
//test encoders
|
||||
MU_TEST(subghz_encoder_princeton_test) {
|
||||
mu_assert(
|
||||
|
||||
Submodule applications/external updated: 2f67c3c918...4915916f6f
@@ -3,6 +3,5 @@ ADD_SCENE(gpio, test, Test)
|
||||
ADD_SCENE(gpio, usb_uart, UsbUart)
|
||||
ADD_SCENE(gpio, usb_uart_cfg, UsbUartCfg)
|
||||
ADD_SCENE(gpio, usb_uart_close_rpc, UsbUartCloseRpc)
|
||||
ADD_SCENE(gpio, exit_confirm, ExitConfirm)
|
||||
ADD_SCENE(gpio, i2c_scanner, I2CScanner)
|
||||
ADD_SCENE(gpio, i2c_sfp, I2CSfp)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#include "gpio_app_i.h"
|
||||
|
||||
void gpio_scene_exit_confirm_dialog_callback(DialogExResult result, void* context) {
|
||||
GpioApp* app = context;
|
||||
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, result);
|
||||
}
|
||||
|
||||
void gpio_scene_exit_confirm_on_enter(void* context) {
|
||||
GpioApp* app = context;
|
||||
DialogEx* dialog = app->dialog;
|
||||
|
||||
dialog_ex_set_context(dialog, app);
|
||||
dialog_ex_set_left_button_text(dialog, "Exit");
|
||||
dialog_ex_set_right_button_text(dialog, "Stay");
|
||||
dialog_ex_set_header(dialog, "Exit USB-UART?", 22, 12, AlignLeft, AlignTop);
|
||||
dialog_ex_set_result_callback(dialog, gpio_scene_exit_confirm_dialog_callback);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, GpioAppViewExitConfirm);
|
||||
}
|
||||
|
||||
bool gpio_scene_exit_confirm_on_event(void* context, SceneManagerEvent event) {
|
||||
GpioApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == DialogExResultRight) {
|
||||
consumed = scene_manager_previous_scene(app->scene_manager);
|
||||
} else if(event.event == DialogExResultLeft) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, GpioSceneStart);
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void gpio_scene_exit_confirm_on_exit(void* context) {
|
||||
GpioApp* app = context;
|
||||
|
||||
// Clean view
|
||||
dialog_ex_reset(app->dialog);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ typedef struct {
|
||||
UsbUartState state;
|
||||
} SceneUsbUartBridge;
|
||||
|
||||
static SceneUsbUartBridge* scene_usb_uart;
|
||||
static SceneUsbUartBridge* scene_usb_uart = NULL;
|
||||
|
||||
void gpio_scene_usb_uart_callback(GpioCustomEvent event, void* context) {
|
||||
furi_assert(context);
|
||||
@@ -14,10 +14,21 @@ void gpio_scene_usb_uart_callback(GpioCustomEvent event, void* context) {
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, event);
|
||||
}
|
||||
|
||||
void gpio_scene_usb_uart_dialog_callback(DialogExResult result, void* context) {
|
||||
GpioApp* app = context;
|
||||
if(result == DialogExResultLeft) {
|
||||
usb_uart_disable(app->usb_uart_bridge);
|
||||
free(scene_usb_uart);
|
||||
scene_usb_uart = NULL;
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, GpioSceneStart);
|
||||
} else {
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, GpioAppViewUsbUart);
|
||||
}
|
||||
}
|
||||
|
||||
void gpio_scene_usb_uart_on_enter(void* context) {
|
||||
GpioApp* app = context;
|
||||
uint32_t prev_state = scene_manager_get_scene_state(app->scene_manager, GpioAppViewUsbUart);
|
||||
if(prev_state == 0) {
|
||||
if(!scene_usb_uart) {
|
||||
scene_usb_uart = malloc(sizeof(SceneUsbUartBridge));
|
||||
scene_usb_uart->cfg.vcp_ch = 0;
|
||||
scene_usb_uart->cfg.uart_ch = 0;
|
||||
@@ -31,7 +42,6 @@ void gpio_scene_usb_uart_on_enter(void* context) {
|
||||
usb_uart_get_state(app->usb_uart_bridge, &scene_usb_uart->state);
|
||||
|
||||
gpio_usb_uart_set_callback(app->gpio_usb_uart, gpio_scene_usb_uart_callback, app);
|
||||
scene_manager_set_scene_state(app->scene_manager, GpioSceneUsbUart, 0);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, GpioAppViewUsbUart);
|
||||
notification_message(app->notifications, &sequence_display_backlight_enforce_on);
|
||||
}
|
||||
@@ -39,11 +49,16 @@ void gpio_scene_usb_uart_on_enter(void* context) {
|
||||
bool gpio_scene_usb_uart_on_event(void* context, SceneManagerEvent event) {
|
||||
GpioApp* app = context;
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(app->scene_manager, GpioSceneUsbUart, 1);
|
||||
scene_manager_next_scene(app->scene_manager, GpioSceneUsbUartCfg);
|
||||
return true;
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
scene_manager_next_scene(app->scene_manager, GpioSceneExitConfirm);
|
||||
DialogEx* dialog = app->dialog;
|
||||
dialog_ex_set_context(dialog, app);
|
||||
dialog_ex_set_left_button_text(dialog, "Exit");
|
||||
dialog_ex_set_right_button_text(dialog, "Stay");
|
||||
dialog_ex_set_header(dialog, "Exit USB-UART?", 22, 12, AlignLeft, AlignTop);
|
||||
dialog_ex_set_result_callback(dialog, gpio_scene_usb_uart_dialog_callback);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, GpioAppViewExitConfirm);
|
||||
return true;
|
||||
} else if(event.type == SceneManagerEventTypeTick) {
|
||||
uint32_t tx_cnt_last = scene_usb_uart->state.tx_cnt;
|
||||
@@ -61,10 +76,5 @@ bool gpio_scene_usb_uart_on_event(void* context, SceneManagerEvent event) {
|
||||
|
||||
void gpio_scene_usb_uart_on_exit(void* context) {
|
||||
GpioApp* app = context;
|
||||
uint32_t prev_state = scene_manager_get_scene_state(app->scene_manager, GpioSceneUsbUart);
|
||||
if(prev_state == 0) {
|
||||
usb_uart_disable(app->usb_uart_bridge);
|
||||
free(scene_usb_uart);
|
||||
}
|
||||
notification_message(app->notifications, &sequence_display_backlight_enforce_auto);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,15 @@ App(
|
||||
sources=["plugins/supported_cards/all_in_one.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="smartrider_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="smartrider_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/smartrider.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="microel_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
|
||||
334
applications/main/nfc/plugins/supported_cards/smartrider.c
Normal file
334
applications/main/nfc/plugins/supported_cards/smartrider.c
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <bit_lib.h>
|
||||
#include <flipper_application.h>
|
||||
#include <furi.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MAX_TRIPS 10
|
||||
#define TAG "SmartRider"
|
||||
#define MAX_BLOCKS 64
|
||||
#define MAX_DATE_ITERATIONS 366
|
||||
|
||||
static const uint8_t STANDARD_KEYS[3][6] = {
|
||||
{0x20, 0x31, 0xD1, 0xE5, 0x7A, 0x3B},
|
||||
{0x4C, 0xA6, 0x02, 0x9F, 0x94, 0x73},
|
||||
{0x19, 0x19, 0x53, 0x98, 0xE3, 0x2F}};
|
||||
|
||||
typedef struct {
|
||||
uint32_t timestamp;
|
||||
uint16_t cost;
|
||||
uint16_t transaction_number;
|
||||
uint16_t journey_number;
|
||||
char route[5];
|
||||
uint8_t tap_on : 1;
|
||||
uint8_t block;
|
||||
} __attribute__((packed)) TripData;
|
||||
|
||||
typedef struct {
|
||||
uint32_t balance;
|
||||
uint16_t issued_days;
|
||||
uint16_t expiry_days;
|
||||
uint16_t purchase_cost;
|
||||
uint16_t auto_load_threshold;
|
||||
uint16_t auto_load_value;
|
||||
char card_serial_number[11];
|
||||
uint8_t token;
|
||||
TripData trips[MAX_TRIPS];
|
||||
uint8_t trip_count;
|
||||
} __attribute__((packed)) SmartRiderData;
|
||||
|
||||
static const char* const CONCESSION_TYPES[] = {
|
||||
"Pre-issue",
|
||||
"Standard Fare",
|
||||
"Student",
|
||||
NULL,
|
||||
"Tertiary",
|
||||
NULL,
|
||||
"Seniors",
|
||||
"Health Care",
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
"PTA Staff",
|
||||
"Pensioner",
|
||||
"Free Travel"};
|
||||
|
||||
static inline const char* get_concession_type(uint8_t token) {
|
||||
return (token <= 0x10) ? CONCESSION_TYPES[token] : "Unknown";
|
||||
}
|
||||
|
||||
static bool authenticate_and_read(
|
||||
Nfc* nfc,
|
||||
uint8_t sector,
|
||||
const uint8_t* key,
|
||||
MfClassicKeyType key_type,
|
||||
MfClassicBlock* block_data) {
|
||||
MfClassicKey mf_key;
|
||||
memcpy(mf_key.data, key, 6);
|
||||
uint8_t block = mf_classic_get_first_block_num_of_sector(sector);
|
||||
|
||||
if(mf_classic_poller_sync_auth(nfc, block, &mf_key, key_type, NULL) != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Authentication failed for sector %d key type %d", sector, key_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(mf_classic_poller_sync_read_block(nfc, block, &mf_key, key_type, block_data) !=
|
||||
MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Read failed for sector %d", sector);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool smartrider_verify(Nfc* nfc) {
|
||||
furi_assert(nfc);
|
||||
MfClassicBlock block_data;
|
||||
|
||||
for(int i = 0; i < 3; i++) {
|
||||
if(!authenticate_and_read(
|
||||
nfc,
|
||||
i * 6,
|
||||
STANDARD_KEYS[i],
|
||||
i % 2 == 0 ? MfClassicKeyTypeA : MfClassicKeyTypeB,
|
||||
&block_data) ||
|
||||
memcmp(block_data.data, STANDARD_KEYS[i], 6) != 0) {
|
||||
FURI_LOG_D(TAG, "Authentication or key mismatch for key %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "SmartRider card verified");
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
parse_trip_data(const MfClassicBlock* block_data, TripData* trip, uint8_t block_number) {
|
||||
trip->timestamp = bit_lib_bytes_to_num_le(block_data->data + 3, 4);
|
||||
trip->tap_on = (block_data->data[7] & 0x10) == 0x10;
|
||||
memcpy(trip->route, block_data->data + 8, 4);
|
||||
trip->route[4] = '\0';
|
||||
trip->cost = bit_lib_bytes_to_num_le(block_data->data + 13, 2);
|
||||
trip->transaction_number = bit_lib_bytes_to_num_le(block_data->data, 2);
|
||||
trip->journey_number = bit_lib_bytes_to_num_le(block_data->data + 2, 2);
|
||||
trip->block = block_number;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool smartrider_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
MfClassicType type;
|
||||
if(mf_classic_poller_sync_detect_type(nfc, &type) != MfClassicErrorNone ||
|
||||
type != MfClassicType1k) {
|
||||
mf_classic_free(data);
|
||||
return false;
|
||||
}
|
||||
data->type = type;
|
||||
|
||||
MfClassicDeviceKeys keys = {.key_a_mask = 0, .key_b_mask = 0};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
memcpy(keys.key_a[i].data, STANDARD_KEYS[i == 0 ? 0 : 1], sizeof(STANDARD_KEYS[0]));
|
||||
if(i > 0) {
|
||||
memcpy(keys.key_b[i].data, STANDARD_KEYS[2], sizeof(STANDARD_KEYS[0]));
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
}
|
||||
|
||||
MfClassicError error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_W(TAG, "Failed to read data");
|
||||
mf_classic_free(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
mf_classic_free(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_leap_year(uint16_t year) {
|
||||
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
||||
}
|
||||
|
||||
static void calculate_date(uint32_t timestamp, char* date_str, size_t date_str_size) {
|
||||
uint32_t seconds_since_2000 = timestamp;
|
||||
uint32_t days_since_2000 = seconds_since_2000 / 86400;
|
||||
uint16_t year = 2000;
|
||||
uint8_t month = 1;
|
||||
uint16_t day = 1;
|
||||
|
||||
static const uint16_t days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
|
||||
while(days_since_2000 >= (is_leap_year(year) ? 366 : 365)) {
|
||||
days_since_2000 -= (is_leap_year(year) ? 366 : 365);
|
||||
year++;
|
||||
}
|
||||
|
||||
for(month = 0; month < 12; month++) {
|
||||
uint16_t dim = days_in_month[month];
|
||||
if(month == 1 && is_leap_year(year)) {
|
||||
dim++;
|
||||
}
|
||||
if(days_since_2000 < dim) {
|
||||
break;
|
||||
}
|
||||
days_since_2000 -= dim;
|
||||
}
|
||||
|
||||
day = days_since_2000 + 1;
|
||||
month++; // Adjust month to 1-based
|
||||
|
||||
if(date_str_size > 0) {
|
||||
size_t written = 0;
|
||||
written += snprintf(date_str + written, date_str_size - written, "%02u", day);
|
||||
if(written < date_str_size - 1) {
|
||||
written += snprintf(date_str + written, date_str_size - written, "/");
|
||||
}
|
||||
if(written < date_str_size - 1) {
|
||||
written += snprintf(date_str + written, date_str_size - written, "%02u", month);
|
||||
}
|
||||
if(written < date_str_size - 1) {
|
||||
written += snprintf(date_str + written, date_str_size - written, "/");
|
||||
}
|
||||
if(written < date_str_size - 1) {
|
||||
snprintf(date_str + written, date_str_size - written, "%02u", year % 100);
|
||||
}
|
||||
} else {
|
||||
// If the buffer size is 0, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
static bool smartrider_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
furi_assert(parsed_data);
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
SmartRiderData sr_data = {0};
|
||||
|
||||
if(data->type != MfClassicType1k) {
|
||||
FURI_LOG_E(TAG, "Invalid card type");
|
||||
return false;
|
||||
}
|
||||
|
||||
const MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 0);
|
||||
if(!sec_tr || memcmp(sec_tr->key_a.data, STANDARD_KEYS[0], 6) != 0) {
|
||||
FURI_LOG_E(TAG, "Key verification failed for sector 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
static const uint8_t required_blocks[] = {14, 4, 5, 1, 52, 50, 0};
|
||||
for(size_t i = 0; i < COUNT_OF(required_blocks); i++) {
|
||||
if(required_blocks[i] >= MAX_BLOCKS ||
|
||||
!mf_classic_is_block_read(data, required_blocks[i])) {
|
||||
FURI_LOG_E(TAG, "Required block %d is not read or out of range", required_blocks[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sr_data.balance = bit_lib_bytes_to_num_le(data->block[14].data + 7, 2);
|
||||
sr_data.issued_days = bit_lib_bytes_to_num_le(data->block[4].data + 16, 2);
|
||||
sr_data.expiry_days = bit_lib_bytes_to_num_le(data->block[4].data + 18, 2);
|
||||
sr_data.auto_load_threshold = bit_lib_bytes_to_num_le(data->block[4].data + 20, 2);
|
||||
sr_data.auto_load_value = bit_lib_bytes_to_num_le(data->block[4].data + 22, 2);
|
||||
sr_data.token = data->block[5].data[8];
|
||||
sr_data.purchase_cost = bit_lib_bytes_to_num_le(data->block[0].data + 14, 2);
|
||||
|
||||
snprintf(
|
||||
sr_data.card_serial_number,
|
||||
sizeof(sr_data.card_serial_number),
|
||||
"%02X%02X%02X%02X%02X",
|
||||
data->block[1].data[6],
|
||||
data->block[1].data[7],
|
||||
data->block[1].data[8],
|
||||
data->block[1].data[9],
|
||||
data->block[1].data[10]);
|
||||
|
||||
for(uint8_t block_number = 40; block_number <= 52 && sr_data.trip_count < MAX_TRIPS;
|
||||
block_number++) {
|
||||
if((block_number != 43 && block_number != 47 && block_number != 51) &&
|
||||
mf_classic_is_block_read(data, block_number) &&
|
||||
parse_trip_data(
|
||||
&data->block[block_number], &sr_data.trips[sr_data.trip_count], block_number)) {
|
||||
sr_data.trip_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort trips by timestamp (descending order)
|
||||
for(uint8_t i = 0; i < sr_data.trip_count - 1; i++) {
|
||||
for(uint8_t j = 0; j < sr_data.trip_count - i - 1; j++) {
|
||||
if(sr_data.trips[j].timestamp < sr_data.trips[j + 1].timestamp) {
|
||||
TripData temp = sr_data.trips[j];
|
||||
sr_data.trips[j] = sr_data.trips[j + 1];
|
||||
sr_data.trips[j + 1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_printf(
|
||||
parsed_data,
|
||||
"\e#SmartRider\nBalance: $%lu.%02lu\nConcession: %s\nSerial: %s%s\n"
|
||||
"Total Cost: $%u.%02u\nAuto-Load: $%u.%02u/$%u.%02u\n\e#Tag On/Off History\n",
|
||||
sr_data.balance / 100,
|
||||
sr_data.balance % 100,
|
||||
get_concession_type(sr_data.token),
|
||||
memcmp(sr_data.card_serial_number, "00", 2) == 0 ? "SR0" : "",
|
||||
memcmp(sr_data.card_serial_number, "00", 2) == 0 ? sr_data.card_serial_number + 2 :
|
||||
sr_data.card_serial_number,
|
||||
sr_data.purchase_cost / 100,
|
||||
sr_data.purchase_cost % 100,
|
||||
sr_data.auto_load_threshold / 100,
|
||||
sr_data.auto_load_threshold % 100,
|
||||
sr_data.auto_load_value / 100,
|
||||
sr_data.auto_load_value % 100);
|
||||
|
||||
for(uint8_t i = 0; i < sr_data.trip_count; i++) {
|
||||
char date_str[9];
|
||||
calculate_date(sr_data.trips[i].timestamp, date_str, sizeof(date_str));
|
||||
|
||||
uint32_t cost = sr_data.trips[i].cost;
|
||||
if(cost > 0) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"%s %c $%lu.%02lu %s\n",
|
||||
date_str,
|
||||
sr_data.trips[i].tap_on ? '+' : '-',
|
||||
cost / 100,
|
||||
cost % 100,
|
||||
sr_data.trips[i].route);
|
||||
} else {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"%s %c %s\n",
|
||||
date_str,
|
||||
sr_data.trips[i].tap_on ? '+' : '-',
|
||||
sr_data.trips[i].route);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
static const NfcSupportedCardsPlugin smartrider_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = smartrider_verify,
|
||||
.read = smartrider_read,
|
||||
.parse = smartrider_parse,
|
||||
};
|
||||
|
||||
__attribute__((used)) const FlipperAppPluginDescriptor* smartrider_plugin_ep() {
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &smartrider_plugin,
|
||||
};
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
|
||||
// made with love by jay candel <3
|
||||
@@ -2495,6 +2495,18 @@ EA19E58DD046
|
||||
3F41891454EE
|
||||
7EDAE7923287
|
||||
11DDA4862A1C
|
||||
# +------------------------------------------------------------------------------------------------------------------------+
|
||||
# | https://github.com/Next-Flip/Momentum-Firmware/blob/dev/applications/main/nfc/resources/nfc/assets/mf_classic_dict.nfc |
|
||||
# +------------------------------------------------------------------------------------------------------------------------+
|
||||
##############################################
|
||||
# BG Hotels - keys from Bulgaria
|
||||
# Found with FlipperNestedRecovery by Z3r0L1nk
|
||||
8000806B5072
|
||||
8430A669558C
|
||||
1202165D4EAB
|
||||
B02094F92A71
|
||||
0402B44FB679
|
||||
A23412F92811
|
||||
# +---------------------------------------------------------------------+
|
||||
# | https://github.com/Stepzor11/NFC_keys/blob/main/mf_classic_dict.nfc |
|
||||
# +---------------------------------------------------------------------+
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
static void subghz_scene_receiver_update_statusbar(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
FuriString* history_stat_str = furi_string_alloc();
|
||||
bool show_sats = subghz->gps && furi_hal_rtc_get_timestamp() % 2;
|
||||
if(!subghz_history_get_text_space_left(
|
||||
subghz->history,
|
||||
history_stat_str,
|
||||
subghz->gps ? subghz->gps->satellites : 0,
|
||||
subghz->last_settings->delete_old_signals)) {
|
||||
subghz->last_settings->delete_old_signals,
|
||||
show_sats,
|
||||
show_sats ? subghz->gps->satellites : 0)) {
|
||||
FuriString* frequency_str = furi_string_alloc();
|
||||
FuriString* modulation_str = furi_string_alloc();
|
||||
|
||||
@@ -25,6 +27,7 @@ static void subghz_scene_receiver_update_statusbar(void* context) {
|
||||
furi_string_get_cstr(history_stat_str),
|
||||
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
|
||||
READ_BIT(subghz->filter, SubGhzProtocolFlag_BinRAW) > 0,
|
||||
show_sats,
|
||||
subghz->repeater);
|
||||
|
||||
furi_string_free(frequency_str);
|
||||
@@ -37,6 +40,7 @@ static void subghz_scene_receiver_update_statusbar(void* context) {
|
||||
"",
|
||||
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
|
||||
READ_BIT(subghz->filter, SubGhzProtocolFlag_BinRAW) > 0,
|
||||
show_sats,
|
||||
subghz->repeater);
|
||||
}
|
||||
furi_string_free(history_stat_str);
|
||||
|
||||
@@ -50,11 +50,13 @@ const NotificationSequence subghz_sequence_tx_beep = {
|
||||
static void subghz_scene_receiver_update_statusbar(void* context) {
|
||||
SubGhz* subghz = context;
|
||||
FuriString* history_stat_str = furi_string_alloc();
|
||||
bool show_sats = subghz->gps && furi_hal_rtc_get_timestamp() % 2;
|
||||
if(!subghz_history_get_text_space_left(
|
||||
subghz->history,
|
||||
history_stat_str,
|
||||
subghz->gps ? subghz->gps->satellites : 0,
|
||||
subghz->last_settings->delete_old_signals)) {
|
||||
subghz->last_settings->delete_old_signals,
|
||||
show_sats,
|
||||
show_sats ? subghz->gps->satellites : 0)) {
|
||||
FuriString* frequency_str = furi_string_alloc();
|
||||
FuriString* modulation_str = furi_string_alloc();
|
||||
|
||||
@@ -87,6 +89,7 @@ static void subghz_scene_receiver_update_statusbar(void* context) {
|
||||
furi_string_get_cstr(history_stat_str),
|
||||
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
|
||||
READ_BIT(subghz->filter, SubGhzProtocolFlag_BinRAW) > 0,
|
||||
show_sats,
|
||||
subghz->repeater);
|
||||
|
||||
furi_string_free(frequency_str);
|
||||
@@ -99,6 +102,7 @@ static void subghz_scene_receiver_update_statusbar(void* context) {
|
||||
"",
|
||||
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
|
||||
READ_BIT(subghz->filter, SubGhzProtocolFlag_BinRAW) > 0,
|
||||
show_sats,
|
||||
subghz->repeater);
|
||||
}
|
||||
furi_string_free(history_stat_str);
|
||||
|
||||
@@ -193,8 +193,9 @@ FlipperFormat* subghz_history_get_raw_data(SubGhzHistory* instance, uint16_t idx
|
||||
bool subghz_history_get_text_space_left(
|
||||
SubGhzHistory* instance,
|
||||
FuriString* output,
|
||||
uint8_t sats,
|
||||
bool ignore_full) {
|
||||
bool ignore_full,
|
||||
bool show_sats,
|
||||
uint8_t sats) {
|
||||
furi_assert(instance);
|
||||
if(!ignore_full) {
|
||||
if(memmgr_get_free_heap() < SUBGHZ_HISTORY_FREE_HEAP) {
|
||||
@@ -207,14 +208,10 @@ bool subghz_history_get_text_space_left(
|
||||
}
|
||||
}
|
||||
if(output != NULL) {
|
||||
if(sats == 0) {
|
||||
furi_string_printf(output, "%02u", instance->last_index_write);
|
||||
if(show_sats) {
|
||||
furi_string_printf(output, "%d", sats);
|
||||
} else {
|
||||
if(furi_hal_rtc_get_timestamp() % 2) {
|
||||
furi_string_printf(output, "%02u", instance->last_index_write);
|
||||
} else {
|
||||
furi_string_printf(output, "%d sats", sats);
|
||||
}
|
||||
furi_string_printf(output, "%02u", instance->last_index_write);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -118,19 +118,21 @@ void subghz_history_get_text_item_menu(SubGhzHistory* instance, FuriString* outp
|
||||
*/
|
||||
void subghz_history_get_time_item_menu(SubGhzHistory* instance, FuriString* output, uint16_t idx);
|
||||
|
||||
/** Get string the remaining number of records to history
|
||||
/** Get string the remaining number of records to history, or sats
|
||||
*
|
||||
* @param instance - SubGhzHistory instance
|
||||
* @param output - FuriString* output
|
||||
* @param sats - Number of satellites
|
||||
* @param instance - SubGhzHistory instance
|
||||
* @param output - FuriString* output
|
||||
* @param ignore_full - Ignore if history is full
|
||||
* @param show_sats - Whether to show the satellite number
|
||||
* @param sats - Number of satellites
|
||||
* @return bool - is FULL
|
||||
*/
|
||||
bool subghz_history_get_text_space_left(
|
||||
SubGhzHistory* instance,
|
||||
FuriString* output,
|
||||
uint8_t sats,
|
||||
bool ignore_full);
|
||||
bool ignore_full,
|
||||
bool show_sats,
|
||||
uint8_t sats);
|
||||
|
||||
/** Return last index
|
||||
*
|
||||
|
||||
@@ -66,6 +66,7 @@ typedef struct {
|
||||
FuriString* progress_str;
|
||||
bool hopping_enabled;
|
||||
bool bin_raw_enabled;
|
||||
bool show_sats;
|
||||
SubGhzRepeaterState repeater_state;
|
||||
SubGhzReceiverHistory* history;
|
||||
uint16_t idx;
|
||||
@@ -210,6 +211,7 @@ void subghz_view_receiver_add_data_statusbar(
|
||||
const char* history_stat_str,
|
||||
bool hopping_enabled,
|
||||
bool bin_raw_enabled,
|
||||
bool show_sats,
|
||||
SubGhzRepeaterState repeater_state) {
|
||||
furi_assert(subghz_receiver);
|
||||
with_view_model(
|
||||
@@ -221,6 +223,7 @@ void subghz_view_receiver_add_data_statusbar(
|
||||
furi_string_set(model->history_stat_str, history_stat_str);
|
||||
model->hopping_enabled = hopping_enabled;
|
||||
model->bin_raw_enabled = bin_raw_enabled;
|
||||
model->show_sats = show_sats;
|
||||
model->repeater_state = repeater_state;
|
||||
},
|
||||
true);
|
||||
@@ -408,7 +411,11 @@ void subghz_view_receiver_draw(Canvas* canvas, SubGhzViewReceiverModel* model) {
|
||||
AlignRight,
|
||||
AlignBottom,
|
||||
furi_string_get_cstr(model->history_stat_str));
|
||||
canvas_draw_icon(canvas, 116, 53, &I_sub1_10px);
|
||||
if(model->show_sats) {
|
||||
canvas_draw_icon(canvas, 118, 54, &I_Sats_6x9);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 116, 53, &I_sub1_10px);
|
||||
}
|
||||
}
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
elements_bold_rounded_frame(canvas, 14, 8, 99, 48);
|
||||
@@ -453,7 +460,11 @@ void subghz_view_receiver_draw(Canvas* canvas, SubGhzViewReceiverModel* model) {
|
||||
AlignRight,
|
||||
AlignBottom,
|
||||
furi_string_get_cstr(model->history_stat_str));
|
||||
canvas_draw_icon(canvas, 116, 53, &I_sub1_10px);
|
||||
if(model->show_sats) {
|
||||
canvas_draw_icon(canvas, 118, 54, &I_Sats_6x9);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 116, 53, &I_sub1_10px);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ void subghz_view_receiver_add_data_statusbar(
|
||||
const char* history_stat_str,
|
||||
bool hopping_enabled,
|
||||
bool bin_raw_enabled,
|
||||
bool show_sats,
|
||||
SubGhzRepeaterState repeater_enabled);
|
||||
|
||||
void subghz_view_receiver_set_radio_device_type(
|
||||
|
||||
@@ -428,21 +428,26 @@ bool storage_common_exists(Storage* storage, const char* path);
|
||||
* - /int/Test and /int/test -> false (Case-sensitive storage),
|
||||
* - /ext/Test and /ext/test -> true (Case-insensitive storage).
|
||||
*
|
||||
* If the truncate parameter is set to true, the second path will be
|
||||
* truncated to be no longer than the first one. It is useful to determine
|
||||
* whether path2 is a subdirectory of path1.
|
||||
*
|
||||
* @param storage pointer to a storage API instance.
|
||||
* @param path1 pointer to a zero-terminated string containing the first path.
|
||||
* @param path2 pointer to a zero-terminated string containing the second path.
|
||||
* @param truncate whether to truncate path2 to be no longer than path1.
|
||||
* @return true if paths are equivalent, false otherwise.
|
||||
*/
|
||||
bool storage_common_equivalent_path(
|
||||
Storage* storage,
|
||||
const char* path1,
|
||||
const char* path2,
|
||||
bool truncate);
|
||||
bool storage_common_equivalent_path(Storage* storage, const char* path1, const char* path2);
|
||||
|
||||
/**
|
||||
* @brief Check whether a path is a subpath of another path.
|
||||
*
|
||||
* This function respects storage-defined equivalence rules
|
||||
* (see `storage_common_equivalent_path`).
|
||||
*
|
||||
* @param storage pointer to a storage API instance.
|
||||
* @param parent pointer to a zero-terminated string containing the parent path.
|
||||
* @param child pointer to a zero-terminated string containing the child path.
|
||||
* @return true if `child` is a subpath of `parent`, or if `child` is equivalent
|
||||
* to `parent`; false otherwise.
|
||||
*/
|
||||
bool storage_common_is_subdir(Storage* storage, const char* parent, const char* child);
|
||||
|
||||
/******************* Error Functions *******************/
|
||||
|
||||
|
||||
@@ -507,13 +507,13 @@ FS_Error storage_common_rename(Storage* storage, const char* old_path, const cha
|
||||
}
|
||||
|
||||
// Cannot rename a directory to itself or to a nested directory
|
||||
if(storage_common_equivalent_path(storage, old_path, new_path, true)) {
|
||||
if(storage_common_is_subdir(storage, old_path, new_path)) {
|
||||
error = FSE_INVALID_NAME;
|
||||
break;
|
||||
}
|
||||
|
||||
// Renaming a regular file to itself does nothing and always succeeds
|
||||
} else if(storage_common_equivalent_path(storage, old_path, new_path, false)) {
|
||||
} else if(storage_common_equivalent_path(storage, old_path, new_path)) {
|
||||
error = FSE_OK;
|
||||
break;
|
||||
}
|
||||
@@ -567,13 +567,13 @@ FS_Error storage_common_rename_safe(Storage* storage, const char* old_path, cons
|
||||
|
||||
if(storage_dir_exists(storage, old_path)) {
|
||||
// Cannot rename a directory to itself or to a nested directory
|
||||
if(storage_common_equivalent_path(storage, old_path, new_path, true)) {
|
||||
if(storage_common_is_subdir(storage, old_path, new_path)) {
|
||||
error = FSE_INVALID_NAME;
|
||||
break;
|
||||
}
|
||||
|
||||
// Renaming a regular file to itself does nothing and always succeeds
|
||||
} else if(storage_common_equivalent_path(storage, old_path, new_path, false)) {
|
||||
} else if(storage_common_equivalent_path(storage, old_path, new_path)) {
|
||||
error = FSE_OK;
|
||||
break;
|
||||
}
|
||||
@@ -608,7 +608,7 @@ FS_Error storage_common_rename_safe(Storage* storage, const char* old_path, cons
|
||||
|
||||
static FS_Error
|
||||
storage_copy_recursive(Storage* storage, const char* old_path, const char* new_path) {
|
||||
if(storage_common_equivalent_path(storage, old_path, new_path, true)) {
|
||||
if(storage_common_is_subdir(storage, old_path, new_path)) {
|
||||
return FSE_INVALID_NAME;
|
||||
}
|
||||
|
||||
@@ -927,11 +927,11 @@ bool storage_common_exists(Storage* storage, const char* path) {
|
||||
return storage_common_stat(storage, path, &file_info) == FSE_OK;
|
||||
}
|
||||
|
||||
bool storage_common_equivalent_path(
|
||||
static bool storage_internal_equivalent_path(
|
||||
Storage* storage,
|
||||
const char* path1,
|
||||
const char* path2,
|
||||
bool truncate) {
|
||||
bool check_subdir) {
|
||||
furi_check(storage);
|
||||
|
||||
S_API_PROLOGUE;
|
||||
@@ -940,7 +940,7 @@ bool storage_common_equivalent_path(
|
||||
.cequivpath = {
|
||||
.path1 = path1,
|
||||
.path2 = path2,
|
||||
.truncate = truncate,
|
||||
.check_subdir = check_subdir,
|
||||
.thread_id = furi_thread_get_current_id(),
|
||||
}};
|
||||
|
||||
@@ -950,6 +950,14 @@ bool storage_common_equivalent_path(
|
||||
return S_RETURN_BOOL;
|
||||
}
|
||||
|
||||
bool storage_common_equivalent_path(Storage* storage, const char* path1, const char* path2) {
|
||||
return storage_internal_equivalent_path(storage, path1, path2, false);
|
||||
}
|
||||
|
||||
bool storage_common_is_subdir(Storage* storage, const char* parent, const char* child) {
|
||||
return storage_internal_equivalent_path(storage, parent, child, true);
|
||||
}
|
||||
|
||||
/****************** ERROR ******************/
|
||||
|
||||
const char* storage_error_get_desc(FS_Error error_id) {
|
||||
|
||||
@@ -77,7 +77,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char* path1;
|
||||
const char* path2;
|
||||
bool truncate;
|
||||
bool check_subdir;
|
||||
FuriThreadId thread_id;
|
||||
} SADataCEquivPath;
|
||||
|
||||
|
||||
@@ -757,11 +757,23 @@ void storage_process_message_internal(Storage* app, StorageMessage* message) {
|
||||
storage_path_trim_trailing_slashes(path2);
|
||||
storage_process_alias(app, path1, message->data->cequivpath.thread_id, false);
|
||||
storage_process_alias(app, path2, message->data->cequivpath.thread_id, false);
|
||||
// Comparison is done on path name, same beginning of name != same file/folder
|
||||
// Check with a / suffixed to ensure same file/folder name
|
||||
furi_string_cat(path1, "/");
|
||||
furi_string_cat(path2, "/");
|
||||
if(message->data->cequivpath.truncate) {
|
||||
if(message->data->cequivpath.check_subdir) {
|
||||
// by appending slashes at the end and then truncating the second path, we can
|
||||
// effectively check for shared path components:
|
||||
// example 1:
|
||||
// path1: "/ext/blah" -> "/ext/blah/" -> "/ext/blah/"
|
||||
// path2: "/ext/blah-blah" -> "/ect/blah-blah/" -> "/ext/blah-"
|
||||
// results unequal, conclusion: path2 is not a subpath of path1
|
||||
// example 2:
|
||||
// path1: "/ext/blah" -> "/ext/blah/" -> "/ext/blah/"
|
||||
// path2: "/ext/blah/blah" -> "/ect/blah/blah/" -> "/ext/blah/"
|
||||
// results equal, conclusion: path2 is a subpath of path1
|
||||
// example 3:
|
||||
// path1: "/ext/blah/blah" -> "/ect/blah/blah/" -> "/ext/blah/blah/"
|
||||
// path2: "/ext/blah" -> "/ext/blah/" -> "/ext/blah/"
|
||||
// results unequal, conclusion: path2 is not a subpath of path1
|
||||
furi_string_push_back(path1, '/');
|
||||
furi_string_push_back(path2, '/');
|
||||
furi_string_left(path2, furi_string_size(path1));
|
||||
}
|
||||
message->return_data->bool_value =
|
||||
|
||||
Reference in New Issue
Block a user