From c8b36dd406e325263df280941d5da1623f96305b Mon Sep 17 00:00:00 2001 From: SG Date: Wed, 6 Oct 2021 19:40:28 +1000 Subject: [PATCH] [FL-1791] Flipper file format (#740) * Lib: new flipper file format library * Lib: flipper file format cpp wrapper * Storage: simple function for remove file and check error * iButton app: remove file worker, use new flipper file format instead * Dialogs: storage error message * Storage: simple function for mkdir and check error * iButton app: error messages * Libs: update makefile * RFID app: remove file worker, use new flipper file format instead * Flipper File: library documentation Co-authored-by: Aleksandr Kutuzov --- applications/dialogs/dialogs-api.c | 11 + applications/dialogs/dialogs.h | 7 + applications/ibutton/ibutton-app.cpp | 175 ++++---- applications/ibutton/ibutton-app.h | 5 + applications/ibutton/ibutton-key.cpp | 66 ++- applications/ibutton/ibutton-key.h | 4 + applications/lfrfid/lfrfid-app.cpp | 110 ++--- applications/lfrfid/lfrfid-app.h | 5 + applications/storage/storage-external-api.c | 12 + applications/storage/storage.h | 18 + lib/lib.mk | 1 + lib/toolbox/flipper-file-cpp.cpp | 72 +++ lib/toolbox/flipper-file-cpp.h | 41 ++ lib/toolbox/flipper-file.c | 464 ++++++++++++++++++++ lib/toolbox/flipper-file.h | 267 +++++++++++ 15 files changed, 1100 insertions(+), 158 deletions(-) create mode 100644 lib/toolbox/flipper-file-cpp.cpp create mode 100644 lib/toolbox/flipper-file-cpp.h create mode 100644 lib/toolbox/flipper-file.c create mode 100644 lib/toolbox/flipper-file.h diff --git a/applications/dialogs/dialogs-api.c b/applications/dialogs/dialogs-api.c index 681724fa..95af3b47 100644 --- a/applications/dialogs/dialogs-api.c +++ b/applications/dialogs/dialogs-api.c @@ -60,3 +60,14 @@ DialogMessageButton dialog_message_show(DialogsApp* context, const DialogMessage return return_data.dialog_value; } + +/****************** Storage error ******************/ + +void dialog_message_show_storage_error(DialogsApp* context, const char* error_text) { + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_text(message, error_text, 88, 32, AlignCenter, AlignCenter); + dialog_message_set_icon(message, &I_SDQuestion_35x43, 5, 6); + dialog_message_set_buttons(message, "Back", NULL, NULL); + dialog_message_show(context, message); + dialog_message_free(message); +} \ No newline at end of file diff --git a/applications/dialogs/dialogs.h b/applications/dialogs/dialogs.h index 6c756148..2a05ef41 100644 --- a/applications/dialogs/dialogs.h +++ b/applications/dialogs/dialogs.h @@ -123,6 +123,13 @@ void dialog_message_set_buttons( */ DialogMessageButton dialog_message_show(DialogsApp* context, const DialogMessage* message); +/** + * Show SD error message (with question sign) + * @param context + * @param error_text + */ +void dialog_message_show_storage_error(DialogsApp* context, const char* error_text); + #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/applications/ibutton/ibutton-app.cpp b/applications/ibutton/ibutton-app.cpp index 651d754d..3491c95e 100644 --- a/applications/ibutton/ibutton-app.cpp +++ b/applications/ibutton/ibutton-app.cpp @@ -2,11 +2,12 @@ #include #include #include -#include -#include +#include +#include const char* iButtonApp::app_folder = "/any/ibutton"; const char* iButtonApp::app_extension = ".ibtn"; +const char* iButtonApp::app_filetype = "Flipper iButton key"; void iButtonApp::run(void* args) { iButtonEvent event; @@ -37,7 +38,9 @@ void iButtonApp::run(void* args) { } iButtonApp::iButtonApp() - : notification{"notification"} { + : notification{"notification"} + , storage{"storage"} + , dialogs{"dialogs"} { furi_hal_power_insomnia_enter(); key_worker = new KeyWorker(&ibutton_gpio); } @@ -188,102 +191,90 @@ bool iButtonApp::save_key(const char* key_name) { // Create ibutton directory if necessary make_app_folder(); - FileWorkerCpp file_worker; + FlipperFileCpp file(storage); string_t key_file_name; bool result = false; + string_init(key_file_name); - // First remove key if it was saved - string_init_printf(key_file_name, "%s/%s%s", app_folder, get_key()->get_name(), app_extension); - if(!file_worker.remove(string_get_cstr(key_file_name))) { - string_clear(key_file_name); - return false; - }; + do { + // First remove key if it was saved (we rename the key) + if(!delete_key()) break; - // Save the key - get_key()->set_name(key_name); - string_printf(key_file_name, "%s/%s%s", app_folder, get_key()->get_name(), app_extension); + // Save the key + key.set_name(key_name); - bool res = file_worker.open(string_get_cstr(key_file_name), FSAM_WRITE, FSOM_CREATE_ALWAYS); + // Set full file name, for new key + string_printf(key_file_name, "%s/%s%s", app_folder, key.get_name(), app_extension); + + // Open file for write + if(!file.new_write(string_get_cstr(key_file_name))) break; + + // Write header + if(!file.write_header_cstr(iButtonApp::app_filetype, 1)) break; + + // Write key type + if(!file.write_comment_cstr("Key type can be Cyfral, Dallas or Metakom")) break; + const char* key_type = key.get_key_type_string_by_type(key.get_key_type()); + if(!file.write_string_cstr("Key type", key_type)) break; + + // Write data + if(!file.write_comment_cstr( + "Data size for Cyfral is 2, for Metakom is 4, for Dallas is 8")) + break; + + if(!file.write_hex_array("Data", key.get_data(), key.get_type_data_size())) break; + result = true; + + } while(false); + + file.close(); string_clear(key_file_name); - if(res) { - // type header - const char* key_type = "E "; - - switch(get_key()->get_key_type()) { - case iButtonKeyType::KeyCyfral: - key_type = "C "; - break; - case iButtonKeyType::KeyDallas: - key_type = "D "; - break; - case iButtonKeyType::KeyMetakom: - key_type = "M "; - break; - } - - if(!file_worker.write(key_type, 2)) { - file_worker.close(); - return false; - } - - if(!file_worker.write_hex(get_key()->get_data(), get_key()->get_type_data_size())) { - file_worker.close(); - return false; - } - result = true; + if(!result) { + dialog_message_show_storage_error(dialogs, "Cannot save\nkey file"); } - file_worker.close(); - return result; } bool iButtonApp::load_key_data(string_t key_path) { - FileWorkerCpp file_worker; + FlipperFileCpp file(storage); + bool result = false; + string_t data; + string_init(data); - // Open key file - if(!file_worker.open(string_get_cstr(key_path), FSAM_READ, FSOM_OPEN_EXISTING)) { - file_worker.close(); - return false; + do { + if(!file.open_read(string_get_cstr(key_path))) break; + + // header + uint32_t version; + if(!file.read_header(data, &version)) break; + if(string_cmp_str(data, iButtonApp::app_filetype) != 0) break; + if(version != 1) break; + + // key type + iButtonKeyType type; + if(!file.read_string("Key type", data)) break; + if(!key.get_key_type_by_type_string(string_get_cstr(data), &type)) break; + + // key data + uint8_t key_data[IBUTTON_KEY_DATA_SIZE] = {0}; + if(!file.read_hex_array("Data", key_data, key.get_type_data_size_by_type(type))) break; + + key.set_type(type); + key.set_data(key_data, IBUTTON_KEY_DATA_SIZE); + + result = true; + } while(false); + + file.close(); + string_clear(data); + + if(!result) { + dialog_message_show_storage_error(dialogs, "Cannot load\nkey file"); } - const uint8_t byte_text_size = 4; - char byte_text[byte_text_size] = {0, 0, 0, 0}; - - // Load type header - if(!file_worker.read(byte_text, 2)) { - file_worker.close(); - return false; - } - - iButtonKeyType key_type = iButtonKeyType::KeyCyfral; - if(strcmp(byte_text, "C ") == 0) { - key_type = iButtonKeyType::KeyCyfral; - } else if(strcmp(byte_text, "M ") == 0) { - key_type = iButtonKeyType::KeyMetakom; - } else if(strcmp(byte_text, "D ") == 0) { - key_type = iButtonKeyType::KeyDallas; - } else { - file_worker.show_error("Cannot parse\nkey file"); - file_worker.close(); - return false; - } - - iButtonKeyType old_type = get_key()->get_key_type(); - get_key()->set_type(key_type); - - uint8_t key_data[IBUTTON_KEY_DATA_SIZE] = {0, 0, 0, 0, 0, 0, 0, 0}; - if(!file_worker.read_hex(key_data, get_key()->get_type_data_size())) { - get_key()->set_type(old_type); - file_worker.close(); - return false; - } - - file_worker.close(); - get_key()->set_data(key_data, IBUTTON_KEY_DATA_SIZE); - - return true; + return result; } bool iButtonApp::load_key(const char* key_name) { @@ -303,11 +294,15 @@ bool iButtonApp::load_key(const char* key_name) { bool iButtonApp::load_key() { bool result = false; - FileWorkerCpp file_worker; // Input events and views are managed by file_select - bool res = file_worker.file_select( - app_folder, app_extension, get_file_name(), get_file_name_size(), get_key()->get_name()); + bool res = dialog_file_select_show( + dialogs, + app_folder, + app_extension, + get_file_name(), + get_file_name_size(), + get_key()->get_name()); if(res) { string_t key_str; @@ -328,16 +323,16 @@ bool iButtonApp::load_key() { bool iButtonApp::delete_key() { string_t file_name; bool result = false; - FileWorkerCpp file_worker; string_init_printf(file_name, "%s/%s%s", app_folder, get_key()->get_name(), app_extension); - result = file_worker.remove(string_get_cstr(file_name)); + result = storage_simply_remove(storage, string_get_cstr(file_name)); string_clear(file_name); return result; } void iButtonApp::make_app_folder() { - FileWorkerCpp file_worker; - file_worker.mkdir(app_folder); + if(!storage_simply_mkdir(storage, app_folder)) { + dialog_message_show_storage_error(dialogs, "Cannot create\napp folder"); + } } \ No newline at end of file diff --git a/applications/ibutton/ibutton-app.h b/applications/ibutton/ibutton-app.h index 7e6017d5..9751191f 100644 --- a/applications/ibutton/ibutton-app.h +++ b/applications/ibutton/ibutton-app.h @@ -30,6 +30,8 @@ #include "ibutton-key.h" #include +#include +#include #include @@ -126,6 +128,8 @@ private: iButtonKey key; RecordController notification; + RecordController storage; + RecordController dialogs; static const uint8_t file_name_size = 100; char file_name[file_name_size]; @@ -135,6 +139,7 @@ private: static const char* app_folder; static const char* app_extension; + static const char* app_filetype; bool load_key_data(string_t key_path); void make_app_folder(); diff --git a/applications/ibutton/ibutton-key.cpp b/applications/ibutton/ibutton-key.cpp index f8da3775..3d459c60 100644 --- a/applications/ibutton/ibutton-key.cpp +++ b/applications/ibutton/ibutton-key.cpp @@ -22,21 +22,7 @@ uint8_t* iButtonKey::get_data() { } uint8_t iButtonKey::get_type_data_size() { - uint8_t size = 0; - - switch(type) { - case iButtonKeyType::KeyCyfral: - size = 2; - break; - case iButtonKeyType::KeyMetakom: - size = 4; - break; - case iButtonKeyType::KeyDallas: - size = 8; - break; - } - - return size; + return get_type_data_size_by_type(type); } void iButtonKey::set_name(const char* _name) { @@ -55,5 +41,55 @@ iButtonKeyType iButtonKey::get_key_type() { return type; } +const char* iButtonKey::get_key_type_string_by_type(iButtonKeyType key_type) { + switch(key_type) { + case iButtonKeyType::KeyCyfral: + return "Cyfral"; + break; + case iButtonKeyType::KeyMetakom: + return "Metakom"; + break; + case iButtonKeyType::KeyDallas: + return "Dallas"; + break; + default: + furi_crash("Invalid iButton type"); + return ""; + break; + } +} + +bool iButtonKey::get_key_type_by_type_string(const char* type_string, iButtonKeyType* key_type) { + if(strcmp(type_string, get_key_type_string_by_type(iButtonKeyType::KeyCyfral)) == 0) { + *key_type = iButtonKeyType::KeyCyfral; + } else if(strcmp(type_string, get_key_type_string_by_type(iButtonKeyType::KeyMetakom)) == 0) { + *key_type = iButtonKeyType::KeyMetakom; + } else if(strcmp(type_string, get_key_type_string_by_type(iButtonKeyType::KeyDallas)) == 0) { + *key_type = iButtonKeyType::KeyDallas; + } else { + return false; + } + + return true; +} + +uint8_t iButtonKey::get_type_data_size_by_type(iButtonKeyType key_type) { + uint8_t size = 0; + + switch(key_type) { + case iButtonKeyType::KeyCyfral: + size = 2; + break; + case iButtonKeyType::KeyMetakom: + size = 4; + break; + case iButtonKeyType::KeyDallas: + size = 8; + break; + } + + return size; +} + iButtonKey::iButtonKey() { } diff --git a/applications/ibutton/ibutton-key.h b/applications/ibutton/ibutton-key.h index d7d8de4b..08e68102 100644 --- a/applications/ibutton/ibutton-key.h +++ b/applications/ibutton/ibutton-key.h @@ -17,6 +17,10 @@ public: void set_type(iButtonKeyType key_type); iButtonKeyType get_key_type(); + const char* get_key_type_string_by_type(iButtonKeyType key_type); + bool get_key_type_by_type_string(const char* type_string, iButtonKeyType* key_type); + uint8_t get_type_data_size_by_type(iButtonKeyType key_type); + iButtonKey(); private: diff --git a/applications/lfrfid/lfrfid-app.cpp b/applications/lfrfid/lfrfid-app.cpp index 41cba8d7..92ddaa63 100644 --- a/applications/lfrfid/lfrfid-app.cpp +++ b/applications/lfrfid/lfrfid-app.cpp @@ -16,15 +16,18 @@ #include "scene/lfrfid-app-scene-delete-confirm.h" #include "scene/lfrfid-app-scene-delete-success.h" -#include #include +#include const char* LfRfidApp::app_folder = "/any/lfrfid"; const char* LfRfidApp::app_extension = ".rfid"; +const char* LfRfidApp::app_filetype = "Flipper RFID key"; LfRfidApp::LfRfidApp() : scene_controller{this} , notification{"notification"} + , storage{"storage"} + , dialogs{"dialogs"} , text_store(40) { furi_hal_power_insomnia_enter(); } @@ -77,20 +80,20 @@ bool LfRfidApp::save_key(RfidKey* key) { } bool LfRfidApp::load_key_from_file_select(bool need_restore) { - FileWorkerCpp file_worker; TextStore* filename_ts = new TextStore(64); - bool result; + bool result = false; if(need_restore) { - result = file_worker.file_select( + result = dialog_file_select_show( + dialogs, app_folder, app_extension, filename_ts->text, filename_ts->text_size, worker.key.get_name()); } else { - result = file_worker.file_select( - app_folder, app_extension, filename_ts->text, filename_ts->text_size, NULL); + result = dialog_file_select_show( + dialogs, app_folder, app_extension, filename_ts->text, filename_ts->text_size, NULL); } if(result) { @@ -105,86 +108,87 @@ bool LfRfidApp::load_key_from_file_select(bool need_restore) { } bool LfRfidApp::delete_key(RfidKey* key) { - FileWorkerCpp file_worker; string_t file_name; bool result = false; string_init_printf(file_name, "%s/%s%s", app_folder, key->get_name(), app_extension); - result = file_worker.remove(string_get_cstr(file_name)); + result = storage_simply_remove(storage, string_get_cstr(file_name)); string_clear(file_name); return result; } bool LfRfidApp::load_key_data(const char* path, RfidKey* key) { - FileWorkerCpp file_worker; + FlipperFileCpp file(storage); bool result = false; + string_t str_result; + string_init(str_result); - bool res = file_worker.open(path, FSAM_READ, FSOM_OPEN_EXISTING); + do { + if(!file.open_read(path)) break; - if(res) { - string_t str_result; - string_init(str_result); + // header + uint32_t version; + if(!file.read_header(str_result, &version)) break; + if(string_cmp_str(str_result, app_filetype) != 0) break; + if(version != 1) break; - do { - RfidKey loaded_key; - LfrfidKeyType loaded_type; + // key type + LfrfidKeyType type; + RfidKey loaded_key; - // load type - if(!file_worker.read_until(str_result, ' ')) break; - if(!lfrfid_key_get_string_type(string_get_cstr(str_result), &loaded_type)) { - file_worker.show_error("Cannot parse\nfile"); - break; - } - loaded_key.set_type(loaded_type); + if(!file.read_string("Key type", str_result)) break; + if(!lfrfid_key_get_string_type(string_get_cstr(str_result), &type)) break; + loaded_key.set_type(type); - // load data - uint8_t tmp_data[loaded_key.get_type_data_count()]; - if(!file_worker.read_hex(tmp_data, loaded_key.get_type_data_count())) break; - loaded_key.set_data(tmp_data, loaded_key.get_type_data_count()); + // key data + uint8_t key_data[loaded_key.get_type_data_count()] = {}; + if(!file.read_hex_array("Data", key_data, loaded_key.get_type_data_count())) break; + loaded_key.set_data(key_data, loaded_key.get_type_data_count()); - *key = loaded_key; - result = true; - } while(0); - - // load name path_extract_filename_no_ext(path, str_result); - key->set_name(string_get_cstr(str_result)); + loaded_key.set_name(string_get_cstr(str_result)); - string_clear(str_result); + *key = loaded_key; + result = true; + } while(0); + + file.close(); + string_clear(str_result); + + if(!result) { + dialog_message_show_storage_error(dialogs, "Cannot load\nkey file"); } - file_worker.close(); - return result; } bool LfRfidApp::save_key_data(const char* path, RfidKey* key) { - FileWorkerCpp file_worker; + FlipperFileCpp file(storage); bool result = false; - bool res = file_worker.open(path, FSAM_WRITE, FSOM_CREATE_ALWAYS); + do { + if(!file.new_write(path)) break; + if(!file.write_header_cstr(app_filetype, 1)) break; + if(!file.write_comment_cstr("Key type can be EM4100, H10301 or I40134")) break; + if(!file.write_string_cstr("Key type", lfrfid_key_get_type_string(key->get_type()))) break; + if(!file.write_comment_cstr("Data size for EM4100 is 5, for H10301 is 3, for I40134 is 3")) + break; + if(!file.write_hex_array("Data", key->get_data(), key->get_type_data_count())) break; + result = true; + } while(0); - if(res) { - do { - // type header - const char* key_type = lfrfid_key_get_type_string(key->get_type()); - char delimeter = ' '; + file.close(); - if(!file_worker.write(key_type, strlen(key_type))) break; - if(!file_worker.write(&delimeter)) break; - if(!file_worker.write_hex(key->get_data(), key->get_type_data_count())) break; - - result = true; - } while(0); + if(!result) { + dialog_message_show_storage_error(dialogs, "Cannot save\nkey file"); } - file_worker.close(); - return result; } void LfRfidApp::make_app_folder() { - FileWorkerCpp file_worker; - file_worker.mkdir(app_folder); + if(!storage_simply_mkdir(storage, app_folder)) { + dialog_message_show_storage_error(dialogs, "Cannot create\napp folder"); + } } \ No newline at end of file diff --git a/applications/lfrfid/lfrfid-app.h b/applications/lfrfid/lfrfid-app.h index b60d6994..d1b44e61 100644 --- a/applications/lfrfid/lfrfid-app.h +++ b/applications/lfrfid/lfrfid-app.h @@ -16,6 +16,8 @@ #include "view/container-vm.h" #include +#include +#include #include "helpers/rfid-worker.h" @@ -63,6 +65,8 @@ public: LfRfidApp(); RecordController notification; + RecordController storage; + RecordController dialogs; RfidWorker worker; @@ -72,6 +76,7 @@ public: static const char* app_folder; static const char* app_extension; + static const char* app_filetype; bool save_key(RfidKey* key); bool load_key_from_file_select(bool need_restore); diff --git a/applications/storage/storage-external-api.c b/applications/storage/storage-external-api.c index 7063d182..aa11161e 100644 --- a/applications/storage/storage-external-api.c +++ b/applications/storage/storage-external-api.c @@ -380,4 +380,16 @@ void storage_file_free(File* file) { } free(file); +} + +bool storage_simply_remove(Storage* storage, const char* path) { + FS_Error result; + result = storage_common_remove(storage, path); + return result == FSE_OK || result == FSE_NOT_EXIST; +} + +bool storage_simply_mkdir(Storage* storage, const char* path) { + FS_Error result; + result = storage_common_mkdir(storage, path); + return result == FSE_OK || result == FSE_EXIST; } \ No newline at end of file diff --git a/applications/storage/storage.h b/applications/storage/storage.h index 50e70b84..d38153bf 100644 --- a/applications/storage/storage.h +++ b/applications/storage/storage.h @@ -230,6 +230,24 @@ FS_Error storage_sd_info(Storage* api, SDInfo* info); */ FS_Error storage_sd_status(Storage* api); +/***************** Simplified Functions ******************/ + +/** + * Removes a file/directory from the repository, the directory must be empty and the file/directory must not be open + * @param storage pointer to the api + * @param path + * @return true on success or if file/dir is not exist + */ +bool storage_simply_remove(Storage* storage, const char* path); + +/** + * Creates a directory + * @param storage + * @param path + * @return true on success or if directory is already exist + */ +bool storage_simply_mkdir(Storage* storage, const char* path); + #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/lib/lib.mk b/lib/lib.mk index 4b4b6b8e..ce16722b 100644 --- a/lib/lib.mk +++ b/lib/lib.mk @@ -108,6 +108,7 @@ CPP_SOURCES += $(wildcard $(LIB_DIR)/app-scened-template/*/*.cpp) # Toolbox C_SOURCES += $(wildcard $(LIB_DIR)/toolbox/*.c) +CPP_SOURCES += $(wildcard $(LIB_DIR)/toolbox/*.cpp) # USB Stack CFLAGS += -I$(LIB_DIR)/libusb_stm32/inc diff --git a/lib/toolbox/flipper-file-cpp.cpp b/lib/toolbox/flipper-file-cpp.cpp new file mode 100644 index 00000000..84e6d181 --- /dev/null +++ b/lib/toolbox/flipper-file-cpp.cpp @@ -0,0 +1,72 @@ +#include "flipper-file-cpp.h" + +FlipperFileCpp::FlipperFileCpp(Storage* storage) { + file = flipper_file_alloc(storage); +} + +FlipperFileCpp::~FlipperFileCpp() { + flipper_file_free(file); +} + +bool FlipperFileCpp::open_read(const char* filename) { + return flipper_file_open_read(file, filename); +} + +bool FlipperFileCpp::new_write(const char* filename) { + return flipper_file_new_write(file, filename); +} + +bool FlipperFileCpp::close() { + return flipper_file_close(file); +} + +bool FlipperFileCpp::read_header(string_t filetype, uint32_t* version) { + return flipper_file_read_header(file, filetype, version); +} + +bool FlipperFileCpp::write_header(string_t filetype, const uint32_t version) { + return flipper_file_write_header(file, filetype, version); +} + +bool FlipperFileCpp::write_header_cstr(const char* filetype, const uint32_t version) { + return flipper_file_write_header_cstr(file, filetype, version); +} + +bool FlipperFileCpp::read_string(const char* key, string_t data) { + return flipper_file_read_string(file, key, data); +} + +bool FlipperFileCpp::write_string(const char* key, string_t data) { + return flipper_file_write_string(file, key, data); +} + +bool FlipperFileCpp::write_string_cstr(const char* key, const char* data) { + return flipper_file_write_string_cstr(file, key, data); +} + +bool FlipperFileCpp::read_uint32(const char* key, uint32_t* data) { + return flipper_file_read_uint32(file, key, data); +} + +bool FlipperFileCpp::write_uint32(const char* key, const uint32_t data) { + return flipper_file_write_uint32(file, key, data); +} + +bool FlipperFileCpp::write_comment(string_t data) { + return flipper_file_write_comment(file, data); +} + +bool FlipperFileCpp::write_comment_cstr(const char* data) { + return flipper_file_write_comment_cstr(file, data); +} + +bool FlipperFileCpp::write_hex_array( + const char* key, + const uint8_t* data, + const uint16_t data_size) { + return flipper_file_write_hex_array(file, key, data, data_size); +} + +bool FlipperFileCpp::read_hex_array(const char* key, uint8_t* data, const uint16_t data_size) { + return flipper_file_read_hex_array(file, key, data, data_size); +} diff --git a/lib/toolbox/flipper-file-cpp.h b/lib/toolbox/flipper-file-cpp.h new file mode 100644 index 00000000..bac19cba --- /dev/null +++ b/lib/toolbox/flipper-file-cpp.h @@ -0,0 +1,41 @@ +#pragma once +#include "flipper-file.h" + +class FlipperFileCpp { +private: + FlipperFile* file; + +public: + FlipperFileCpp(Storage* storage); + ~FlipperFileCpp(); + + bool open_read(const char* filename); + + bool new_write(const char* filename); + + bool close(); + + bool read_header(string_t filetype, uint32_t* version); + + bool write_header(string_t filetype, const uint32_t version); + + bool write_header_cstr(const char* filetype, const uint32_t version); + + bool read_string(const char* key, string_t data); + + bool write_string(const char* key, string_t data); + + bool write_string_cstr(const char* key, const char* data); + + bool read_uint32(const char* key, uint32_t* data); + + bool write_uint32(const char* key, const uint32_t data); + + bool write_comment(string_t data); + + bool write_comment_cstr(const char* data); + + bool write_hex_array(const char* key, const uint8_t* data, const uint16_t data_size); + + bool read_hex_array(const char* key, uint8_t* data, const uint16_t data_size); +}; diff --git a/lib/toolbox/flipper-file.c b/lib/toolbox/flipper-file.c new file mode 100644 index 00000000..e16cd868 --- /dev/null +++ b/lib/toolbox/flipper-file.c @@ -0,0 +1,464 @@ +#include +#include "flipper-file.h" +#include +#include + +struct FlipperFile { + File* file; +}; + +const char* flipper_file_filetype_key = "Filetype"; +const char* flipper_file_version_key = "Version"; +const char flipper_file_eoln = '\n'; +const char flipper_file_eolr = '\r'; +const char flipper_file_delimiter = ':'; +const char flipper_file_comment = '#'; + +/** + * Writes data to a file as a hexadecimal array. + * @param file + * @param data + * @param data_size + * @return true on success write + */ +bool flipper_file_write_hex_internal(File* file, const uint8_t* data, const uint16_t data_size) { + const uint8_t byte_text_size = 3; + char byte_text[byte_text_size]; + + bool result = true; + uint16_t bytes_written; + for(uint8_t i = 0; i < data_size; i++) { + snprintf(byte_text, byte_text_size, "%02X", data[i]); + + if(i != 0) { + // space + const char space = ' '; + bytes_written = storage_file_write(file, &space, sizeof(space)); + if(bytes_written != sizeof(space)) { + result = false; + break; + } + } + + bytes_written = storage_file_write(file, &byte_text, strlen(byte_text)); + if(bytes_written != strlen(byte_text)) { + result = false; + break; + } + } + + return result; +} + +/** + * Reads a valid key from a file as a string. + * After reading, the rw pointer will be on the flipper_file_delimiter symbol. + * Optimized not to read comments and values into RAM. + * @param file + * @param key + * @return true on success read + */ +bool flipper_file_read_valid_key(File* file, string_t key) { + string_clean(key); + bool found = false; + bool error = false; + const uint8_t buffer_size = 32; + uint8_t buffer[buffer_size]; + bool accumulate = true; + bool new_line = true; + + while(true) { + uint16_t bytes_were_read = storage_file_read(file, buffer, buffer_size); + if(bytes_were_read == 0) break; + + for(uint16_t i = 0; i < bytes_were_read; i++) { + if(buffer[i] == flipper_file_eoln) { + // EOL found, clean data, start accumulating data and set the new_line flag + string_clean(key); + accumulate = true; + new_line = true; + } else if(buffer[i] == flipper_file_eolr) { + // Ignore + } else if(buffer[i] == flipper_file_comment && new_line) { + // if there is a comment character and we are at the beginning of a new line + // do not accumulate comment data and reset the new_line flag + accumulate = false; + new_line = false; + } else if(buffer[i] == flipper_file_delimiter) { + if(new_line) { + // we are on a "new line" and found the delimiter + // this can only be if we have previously found some kind of key, so + // clear the data, set the flag that we no longer want to accumulate data + // and reset the new_line flag + string_clean(key); + accumulate = false; + new_line = false; + } else { + // parse the delimiter only if we are accumulating data + if(accumulate) { + // we found the delimiter, move the rw pointer to the correct location + // and signal that we have found something + // TODO negative seek + uint64_t position = storage_file_tell(file); + position = position - bytes_were_read + i; + if(!storage_file_seek(file, position, true)) { + error = true; + break; + } + + found = true; + break; + } + } + } else { + // just new symbol, reset the new_line flag + new_line = false; + if(accumulate) { + // and accumulate data if we want + string_push_back(key, buffer[i]); + } + } + } + + if(found || error) break; + } + + return found; +} + +/** + * Sets rw pointer to the data after the key + * @param file + * @param key + * @return true if key was found + */ +bool flipper_file_seek_to_key(File* file, const char* key) { + bool found = false; + string_t readed_key; + + string_init(readed_key); + + // TODO optimize this to search from a stored rw pointer + if(storage_file_seek(file, 0, true)) { + while(!storage_file_eof(file)) { + if(flipper_file_read_valid_key(file, readed_key)) { + if(string_cmp_str(readed_key, key) == 0) { + uint64_t position = storage_file_tell(file); + if(!storage_file_seek(file, position + 2, true)) break; + + found = true; + break; + } + } + } + } + string_clear(readed_key); + + return found; +} + +/** + * Reads data as a string from the stored rw pointer to the \r or \n symbol position + * @param file + * @param str_result + * @return true on success read + */ +bool flipper_file_read_until(File* file, string_t str_result) { + string_clean(str_result); + const uint8_t buffer_size = 32; + uint8_t buffer[buffer_size]; + + do { + uint16_t bytes_were_read = storage_file_read(file, buffer, buffer_size); + if(bytes_were_read == 0) break; + + bool result = false; + bool error = false; + for(uint16_t i = 0; i < bytes_were_read; i++) { + if(buffer[i] == flipper_file_eoln) { + // TODO negative seek + uint64_t position = storage_file_tell(file); + position = position - bytes_were_read + i; + if(!storage_file_seek(file, position, true)) { + error = true; + break; + } + + result = true; + break; + } else if(buffer[i] == flipper_file_eolr) { + // Ignore + } else { + string_push_back(str_result, buffer[i]); + } + } + + if(result || error) { + break; + } + } while(true); + + return string_size(str_result) != 0; +} + +/** + * Reads single hexadecimal data from a file to byte + * @param file + * @param byte + * @return bool + */ +bool flipper_file_read_hex_byte(File* file, uint8_t* byte) { + uint8_t hi_nibble_value, low_nibble_value; + uint8_t text[3]; + bool result = false; + + uint16_t bytes_were_read = storage_file_read(file, text, 3); + if(bytes_were_read >= 2) { + if(text[0] != ' ') { + if(hex_char_to_hex_nibble(text[0], &hi_nibble_value) && + hex_char_to_hex_nibble(text[1], &low_nibble_value)) { + *byte = (hi_nibble_value << 4) | low_nibble_value; + result = true; + } + } else { + if(hex_char_to_hex_nibble(text[1], &hi_nibble_value) && + hex_char_to_hex_nibble(text[2], &low_nibble_value)) { + *byte = (hi_nibble_value << 4) | low_nibble_value; + result = true; + } + } + } + + return result; +} + +FlipperFile* flipper_file_alloc(Storage* storage) { + FlipperFile* flipper_file = malloc(sizeof(FlipperFile)); + flipper_file->file = storage_file_alloc(storage); + return flipper_file; +} + +void flipper_file_free(FlipperFile* flipper_file) { + furi_assert(flipper_file); + if(storage_file_is_open(flipper_file->file)) { + storage_file_close(flipper_file->file); + } + storage_file_free(flipper_file->file); + free(flipper_file); +} + +bool flipper_file_open_read(FlipperFile* flipper_file, const char* filename) { + furi_assert(flipper_file); + bool result = storage_file_open(flipper_file->file, filename, FSAM_READ, FSOM_OPEN_EXISTING); + return result; +} + +bool flipper_file_new_write(FlipperFile* flipper_file, const char* filename) { + furi_assert(flipper_file); + bool result = storage_file_open(flipper_file->file, filename, FSAM_WRITE, FSOM_CREATE_ALWAYS); + return result; +} + +bool flipper_file_close(FlipperFile* flipper_file) { + furi_assert(flipper_file); + if(storage_file_is_open(flipper_file->file)) { + return storage_file_close(flipper_file->file); + } + return true; +} + +bool flipper_file_read_header(FlipperFile* flipper_file, string_t filetype, uint32_t* version) { + bool result = false; + do { + result = flipper_file_read_string(flipper_file, flipper_file_filetype_key, filetype); + if(!result) break; + result = flipper_file_read_uint32(flipper_file, flipper_file_version_key, version); + if(!result) break; + } while(false); + + return result; +} + +bool flipper_file_write_header( + FlipperFile* flipper_file, + string_t filetype, + const uint32_t version) { + bool result = false; + do { + result = flipper_file_write_string(flipper_file, flipper_file_filetype_key, filetype); + if(!result) break; + result = flipper_file_write_uint32(flipper_file, flipper_file_version_key, version); + if(!result) break; + } while(false); + + return result; +} + +bool flipper_file_write_header_cstr( + FlipperFile* flipper_file, + const char* filetype, + const uint32_t version) { + bool result = false; + string_t value; + string_init_set(value, filetype); + result = flipper_file_write_header(flipper_file, value, version); + string_clear(value); + return result; +} + +bool flipper_file_read_string(FlipperFile* flipper_file, const char* key, string_t data) { + furi_assert(flipper_file); + + bool result = false; + if(flipper_file_seek_to_key(flipper_file->file, key)) { + if(flipper_file_read_until(flipper_file->file, data)) { + result = true; + } + } + return result; +} + +bool flipper_file_write_string(FlipperFile* flipper_file, const char* key, string_t data) { + furi_assert(flipper_file); + + bool result = false; + do { + uint16_t bytes_written; + bytes_written = storage_file_write(flipper_file->file, key, strlen(key)); + if(bytes_written != strlen(key)) break; + + const char delimiter_buffer[2] = {flipper_file_delimiter, ' '}; + bytes_written = + storage_file_write(flipper_file->file, delimiter_buffer, sizeof(delimiter_buffer)); + if(bytes_written != sizeof(delimiter_buffer)) break; + + bytes_written = + storage_file_write(flipper_file->file, string_get_cstr(data), string_size(data)); + if(bytes_written != string_size(data)) break; + + bytes_written = + storage_file_write(flipper_file->file, &flipper_file_eoln, sizeof(flipper_file_eoln)); + if(bytes_written != sizeof(flipper_file_eoln)) break; + + result = true; + } while(false); + + return result; +} + +bool flipper_file_write_string_cstr(FlipperFile* flipper_file, const char* key, const char* data) { + bool result = false; + string_t value; + string_init_set(value, data); + result = flipper_file_write_string(flipper_file, key, value); + string_clear(value); + return result; +} + +bool flipper_file_read_uint32(FlipperFile* flipper_file, const char* key, uint32_t* data) { + bool result = false; + string_t value; + string_init(value); + + result = flipper_file_read_string(flipper_file, key, value); + if(result) { + int ret = sscanf(string_get_cstr(value), "%" PRIu32, data); + if(ret != 1) result = false; + } + + string_clear(value); + return result; +} + +bool flipper_file_write_uint32(FlipperFile* flipper_file, const char* key, const uint32_t data) { + bool result = false; + string_t value; + string_init_printf(value, "%" PRIu32, data); + result = flipper_file_write_string(flipper_file, key, value); + string_clear(value); + return result; +} + +bool flipper_file_write_comment(FlipperFile* flipper_file, string_t data) { + furi_assert(flipper_file); + + bool result = false; + do { + uint16_t bytes_written; + const char comment_buffer[2] = {flipper_file_comment, ' '}; + bytes_written = + storage_file_write(flipper_file->file, comment_buffer, sizeof(comment_buffer)); + if(bytes_written != sizeof(comment_buffer)) break; + + bytes_written = + storage_file_write(flipper_file->file, string_get_cstr(data), string_size(data)); + if(bytes_written != string_size(data)) break; + + bytes_written = + storage_file_write(flipper_file->file, &flipper_file_eoln, sizeof(flipper_file_eoln)); + if(bytes_written != sizeof(flipper_file_eoln)) break; + + result = true; + } while(false); + + return result; +} + +bool flipper_file_write_comment_cstr(FlipperFile* flipper_file, const char* data) { + bool result = false; + string_t value; + string_init_set(value, data); + result = flipper_file_write_comment(flipper_file, value); + string_clear(value); + return result; +} + +bool flipper_file_write_hex_array( + FlipperFile* flipper_file, + const char* key, + const uint8_t* data, + const uint16_t data_size) { + furi_assert(flipper_file); + + bool result = false; + do { + uint16_t bytes_written; + bytes_written = storage_file_write(flipper_file->file, key, strlen(key)); + if(bytes_written != strlen(key)) break; + + const char delimiter_buffer[2] = {flipper_file_delimiter, ' '}; + bytes_written = + storage_file_write(flipper_file->file, delimiter_buffer, sizeof(delimiter_buffer)); + if(bytes_written != sizeof(delimiter_buffer)) break; + + if(!flipper_file_write_hex_internal(flipper_file->file, data, data_size)) break; + + bytes_written = + storage_file_write(flipper_file->file, &flipper_file_eoln, sizeof(flipper_file_eoln)); + if(bytes_written != sizeof(flipper_file_eoln)) break; + + result = true; + } while(false); + + return result; +} + +bool flipper_file_read_hex_array( + FlipperFile* flipper_file, + const char* key, + uint8_t* data, + const uint16_t data_size) { + furi_assert(flipper_file); + + bool result = false; + if(flipper_file_seek_to_key(flipper_file->file, key)) { + result = true; + for(uint16_t i = 0; i < data_size; i++) { + if(!flipper_file_read_hex_byte(flipper_file->file, &data[i])) { + result = false; + break; + } + } + } + return result; +} \ No newline at end of file diff --git a/lib/toolbox/flipper-file.h b/lib/toolbox/flipper-file.h new file mode 100644 index 00000000..db6ad938 --- /dev/null +++ b/lib/toolbox/flipper-file.h @@ -0,0 +1,267 @@ +/** + * @file flipper-file.h + * Flipper File Format helper library. + * + * Flipper File Format is a fairly simple format for storing data in a file. + * + * Flipper file structure: + * + * ~~~~~~~~~~~~~~~~~~~~~ + * # Commentary + * Field name: field value + * ~~~~~~~~~~~~~~~~~~~~~ + * + * Lines starting with the # character are ignored (considered as comments). The separator between the name of the value and the value itself is the string ": ". + * + * Currently supported types: + * + * ~~~~~~~~~~~~~~~~~~~~~ + * String: text + * Uint32: 1 + * Hex Array: A4 B3 C2 D1 12 FF + * ~~~~~~~~~~~~~~~~~~~~~ + * + * End of line is LF when writing, but CR is supported when reading. + * + * The library is designed in such a way that comments and field values are completely ignored when searching for keys, that is, they do not consume memory. + * + * File example: + * + * ~~~~~~~~~~~~~~~~~~~~~ + * Filetype: Flipper Test File + * Version: 1 + * # Just test file + * String: String value + * UINT: 1234 + * Hex Array: 00 01 FF A3 + * ~~~~~~~~~~~~~~~~~~~~~ + * + * Writing: + * + * ~~~~~~~~~~~~~~~~~~~~~ + * FlipperFile file = flipper_file_alloc(storage); + * + * do { + * const uint32_t version = 1; + * const char* string_value = "String value"; + * const uint32_t uint32_value = 1234; + * const uint16_t array_size = 4; + * const uint8_t* array[array_size] = {0x00, 0x01, 0xFF, 0xA3}; + * + * if(!flipper_file_new_write(file, "/ext/flipper_file_test")) break; + * if(!flipper_file_write_header_cstr(file, "Flipper Test File", version)) break; + * if(!flipper_file_write_comment_cstr(file, "Just test file")) break; + * if(!flipper_file_write_string_cstr(file, "String", string_value)) break; + * if(!flipper_file_flipper_file_write_uint32(file, "UINT", uint32_value)) break; + * if(!flipper_file_write_hex_array(file, "Hex Array", array, array_size)) break; + * + * // signal that the file was written successfully + * } while(0); + * + * flipper_file_close(file); + * flipper_file_free(file); + * ~~~~~~~~~~~~~~~~~~~~~ + * + * Reading: + * + * ~~~~~~~~~~~~~~~~~~~~~ + * FlipperFile file = flipper_file_alloc(storage); + * + * do { + * uint32_t version = 1; + * string_t file_type; + * string_t string_value; + * uint32_t uint32_value = 1; + * uint16_t array_size = 4; + * uint8_t* array[array_size] = {0}; + * string_init(file_type); + * string_init(string_value); + * + * if(!flipper_file_open_read(file, "/ext/flipper_file_test")) break; + * if(!flipper_file_read_header(file, file_type, &version)) break; + * if(!flipper_file_read_string(file, "String", string_value)) break; + * if(!flipper_file_read_uint32(file, "UINT", &uint32_value)) break; + * if(!flipper_file_read_hex_array(file, "Hex Array", array, array_size)) break; + * + * // signal that the file was read successfully + * } while(0); + * + * flipper_file_close(file); + * flipper_file_free(file); + * ~~~~~~~~~~~~~~~~~~~~~ + * + */ + +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** FlipperFile type anonymous structure. */ +typedef struct FlipperFile FlipperFile; + +/** + * Allocate FlipperFile. + * @param storage storage api + * @return FlipperFile* Pointer to a FlipperFile instance + */ +FlipperFile* flipper_file_alloc(Storage* storage); + +/** + * Free FlipperFile. + * @param flipper_file Pointer to a FlipperFile instance + */ +void flipper_file_free(FlipperFile* flipper_file); + +/** + * Open file for reading. + * @param flipper_file Pointer to a FlipperFile instance + * @param filename File name and path + * @return True on success + */ +bool flipper_file_open_read(FlipperFile* flipper_file, const char* filename); + +/** + * Open file for writing. Creates a new file, or deletes the contents of the file if it already exists. + * @param flipper_file Pointer to a FlipperFile instance + * @param filename File name and path + * @return True on success + */ +bool flipper_file_new_write(FlipperFile* flipper_file, const char* filename); + +/** + * Close the file. + * @param flipper_file Pointer to a FlipperFile instance + * @return True on success + */ +bool flipper_file_close(FlipperFile* flipper_file); + +/** + * Read the header (file type and version) from the file. + * @param flipper_file Pointer to a FlipperFile instance + * @param filetype File type string + * @param version Version Value + * @return True on success + */ +bool flipper_file_read_header(FlipperFile* flipper_file, string_t filetype, uint32_t* version); + +/** + * Write the header (file type and version) to the file. + * @param flipper_file Pointer to a FlipperFile instance + * @param filetype File type string + * @param version Version Value + * @return True on success + */ +bool flipper_file_write_header( + FlipperFile* flipper_file, + string_t filetype, + const uint32_t version); + +/** + * Write the header (file type and version) to the file. Plain C string version. + * @param flipper_file Pointer to a FlipperFile instance + * @param filetype File type string + * @param version Version Value + * @return True on success + */ +bool flipper_file_write_header_cstr( + FlipperFile* flipper_file, + const char* filetype, + const uint32_t version); + +/** + * Read a string from a file by Key + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @return True on success + */ +bool flipper_file_read_string(FlipperFile* flipper_file, const char* key, string_t data); + +/** + * Write key and string to file. + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @return True on success + */ +bool flipper_file_write_string(FlipperFile* flipper_file, const char* key, string_t data); + +/** + * Write key and string to file. Plain C string version. + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @return True on success + */ +bool flipper_file_write_string_cstr(FlipperFile* flipper_file, const char* key, const char* data); + +/** + * Read uint32 from a file by Key + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @return True on success + */ +bool flipper_file_read_uint32(FlipperFile* flipper_file, const char* key, uint32_t* data); + +/** + * Write key and uint32 to file. + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @return True on success + */ +bool flipper_file_write_uint32(FlipperFile* flipper_file, const char* key, const uint32_t data); + +/** + * Write comment to file. + * @param flipper_file Pointer to a FlipperFile instance + * @param data Comment text + * @return True on success + */ +bool flipper_file_write_comment(FlipperFile* flipper_file, string_t data); + +/** + * Write comment to file. Plain C string version. + * @param flipper_file Pointer to a FlipperFile instance + * @param data Comment text + * @return True on success + */ +bool flipper_file_write_comment_cstr(FlipperFile* flipper_file, const char* data); + +/** + * Read hex array from a file by Key + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @param data_size Value size + * @return True on success + */ +bool flipper_file_read_hex_array( + FlipperFile* flipper_file, + const char* key, + uint8_t* data, + const uint16_t data_size); + +/** + * Write key and hex array to file. + * @param flipper_file Pointer to a FlipperFile instance + * @param key Key + * @param data Value + * @param data_size Value size + * @return True on success + */ +bool flipper_file_write_hex_array( + FlipperFile* flipper_file, + const char* key, + const uint8_t* data, + const uint16_t data_size); + +#ifdef __cplusplus +} +#endif \ No newline at end of file