Explorar o código

[FL-1926] Flipper File Format addons (#753)

* Flipper file format: remove C wrapper
* Flipper file format: open append, float, uint32_t as array, delete key, value count
* Flipper file format: fix scratchpad location
* Flipper file format: add EOL on append
* SubGHZ keystore: update encryption type read and write
* Flipper File Format: enhanced version
* Flipper File Format: fix naming
* Flipper File Format: fix "open" subset naming
* Flipper File Format: tests
* Flipper File Format: file helper naming
* SubGHZ keystore: merge with current state of flipper file format
* Flipper File Format:  update make recipe
* Flipper File Format: open new file method
SG %!s(int64=4) %!d(string=hai) anos
pai
achega
7f94ef3179

+ 21 - 16
applications/ibutton/ibutton-app.cpp

@@ -3,7 +3,7 @@
 #include <callback-connector.h>
 #include <m-string.h>
 #include <toolbox/path.h>
-#include <toolbox/flipper-file-cpp.h>
+#include <flipper_file/flipper_file.h>
 
 const char* iButtonApp::app_folder = "/any/ibutton";
 const char* iButtonApp::app_extension = ".ibtn";
@@ -191,7 +191,7 @@ bool iButtonApp::save_key(const char* key_name) {
     // Create ibutton directory if necessary
     make_app_folder();
 
-    FlipperFileCpp file(storage);
+    FlipperFile* file = flipper_file_alloc(storage);
     string_t key_file_name;
     bool result = false;
     string_init(key_file_name);
@@ -207,27 +207,30 @@ bool iButtonApp::save_key(const char* key_name) {
         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;
+        if(!flipper_file_open_always(file, string_get_cstr(key_file_name))) break;
 
         // Write header
-        if(!file.write_header_cstr(iButtonApp::app_filetype, 1)) break;
+        if(!flipper_file_write_header_cstr(file, iButtonApp::app_filetype, 1)) break;
 
         // Write key type
-        if(!file.write_comment_cstr("Key type can be Cyfral, Dallas or Metakom")) break;
+        if(!flipper_file_write_comment_cstr(file, "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;
+        if(!flipper_file_write_string_cstr(file, "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"))
+        if(!flipper_file_write_comment_cstr(
+               file, "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;
+        if(!flipper_file_write_hex(file, "Data", key.get_data(), key.get_type_data_size())) break;
         result = true;
 
     } while(false);
 
-    file.close();
+    flipper_file_close(file);
+    flipper_file_free(file);
+
     string_clear(key_file_name);
 
     if(!result) {
@@ -238,28 +241,29 @@ bool iButtonApp::save_key(const char* key_name) {
 }
 
 bool iButtonApp::load_key_data(string_t key_path) {
-    FlipperFileCpp file(storage);
+    FlipperFile* file = flipper_file_alloc(storage);
     bool result = false;
     string_t data;
     string_init(data);
 
     do {
-        if(!file.open_read(string_get_cstr(key_path))) break;
+        if(!flipper_file_open_existing(file, string_get_cstr(key_path))) break;
 
         // header
         uint32_t version;
-        if(!file.read_header(data, &version)) break;
+        if(!flipper_file_read_header(file, 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(!flipper_file_read_string(file, "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;
+        if(!flipper_file_read_hex(file, "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);
@@ -267,7 +271,8 @@ bool iButtonApp::load_key_data(string_t key_path) {
         result = true;
     } while(false);
 
-    file.close();
+    flipper_file_close(file);
+    flipper_file_free(file);
     string_clear(data);
 
     if(!result) {

+ 23 - 16
applications/lfrfid/lfrfid-app.cpp

@@ -16,8 +16,8 @@
 #include "scene/lfrfid-app-scene-delete-confirm.h"
 #include "scene/lfrfid-app-scene-delete-success.h"
 
-#include <lib/toolbox/path.h>
-#include <lib/toolbox/flipper-file-cpp.h>
+#include <toolbox/path.h>
+#include <flipper_file/flipper_file.h>
 
 const char* LfRfidApp::app_folder = "/any/lfrfid";
 const char* LfRfidApp::app_extension = ".rfid";
@@ -119,17 +119,17 @@ bool LfRfidApp::delete_key(RfidKey* key) {
 }
 
 bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
-    FlipperFileCpp file(storage);
+    FlipperFile* file = flipper_file_alloc(storage);
     bool result = false;
     string_t str_result;
     string_init(str_result);
 
     do {
-        if(!file.open_read(path)) break;
+        if(!flipper_file_open_existing(file, path)) break;
 
         // header
         uint32_t version;
-        if(!file.read_header(str_result, &version)) break;
+        if(!flipper_file_read_header(file, str_result, &version)) break;
         if(string_cmp_str(str_result, app_filetype) != 0) break;
         if(version != 1) break;
 
@@ -137,13 +137,13 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
         LfrfidKeyType type;
         RfidKey loaded_key;
 
-        if(!file.read_string("Key type", str_result)) break;
+        if(!flipper_file_read_string(file, "Key type", str_result)) break;
         if(!lfrfid_key_get_string_type(string_get_cstr(str_result), &type)) break;
         loaded_key.set_type(type);
 
         // 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;
+        if(!flipper_file_read_hex(file, "Data", key_data, loaded_key.get_type_data_count())) break;
         loaded_key.set_data(key_data, loaded_key.get_type_data_count());
 
         path_extract_filename_no_ext(path, str_result);
@@ -153,7 +153,8 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
         result = true;
     } while(0);
 
-    file.close();
+    flipper_file_close(file);
+    flipper_file_free(file);
     string_clear(str_result);
 
     if(!result) {
@@ -164,21 +165,27 @@ bool LfRfidApp::load_key_data(const char* path, RfidKey* key) {
 }
 
 bool LfRfidApp::save_key_data(const char* path, RfidKey* key) {
-    FlipperFileCpp file(storage);
+    FlipperFile* file = flipper_file_alloc(storage);
     bool result = false;
 
     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"))
+        if(!flipper_file_open_always(file, path)) break;
+        if(!flipper_file_write_header_cstr(file, app_filetype, 1)) break;
+        if(!flipper_file_write_comment_cstr(file, "Key type can be EM4100, H10301 or I40134"))
+            break;
+        if(!flipper_file_write_string_cstr(
+               file, "Key type", lfrfid_key_get_type_string(key->get_type())))
+            break;
+        if(!flipper_file_write_comment_cstr(
+               file, "Data size for EM4100 is 5, for H10301 is 3, for I40134 is 3"))
+            break;
+        if(!flipper_file_write_hex(file, "Data", key->get_data(), key->get_type_data_count()))
             break;
-        if(!file.write_hex_array("Data", key->get_data(), key->get_type_data_count())) break;
         result = true;
     } while(0);
 
-    file.close();
+    flipper_file_close(file);
+    flipper_file_free(file);
 
     if(!result) {
         dialog_message_show_storage_error(dialogs, "Cannot save\nkey file");

+ 500 - 0
applications/tests/flipper_file/flipper_file_test.c

@@ -0,0 +1,500 @@
+#include <furi.h>
+#include <flipper_file/flipper_file.h>
+#include "../minunit.h"
+
+#define TEST_DIR TEST_DIR_NAME "/"
+#define TEST_DIR_NAME "/ext/unit_tests_tmp"
+
+static const char* test_filetype = "Flipper File test";
+static const uint32_t test_version = 666;
+
+static const char* test_string_key = "String data";
+static const char* test_string_data = "String";
+static const char* test_string_updated_data = "New string";
+
+static const char* test_int_key = "Int32 data";
+static const int32_t test_int_data[] = {1234, -6345, 7813, 0};
+static const int32_t test_int_updated_data[] = {-1337, 69};
+
+static const char* test_uint_key = "Uint32 data";
+static const uint32_t test_uint_data[] = {1234, 0, 5678, 9098, 7654321};
+static const uint32_t test_uint_updated_data[] = {8, 800, 555, 35, 35};
+
+static const char* test_float_key = "Float data";
+static const float test_float_data[] = {1.5f, 1000.0f};
+static const float test_float_updated_data[] = {1.2f};
+
+static const char* test_hex_key = "Hex data";
+static const uint8_t test_hex_data[] = {0xDE, 0xAD, 0xBE};
+static const uint8_t test_hex_updated_data[] = {0xFE, 0xCA};
+
+#define READ_TEST_WIN "ff_win.test"
+static const char* test_data_win = "Filetype: Flipper File test\n"
+                                   "Version: 666\n"
+                                   "# This is comment\n"
+                                   "String data: String\n"
+                                   "Int32 data: 1234 -6345 7813 0\n"
+                                   "Uint32 data: 1234 0 5678 9098 7654321\n"
+                                   "Float data: 1.5 1000.0\n"
+                                   "Hex data: DE AD BE";
+
+#define READ_TEST_NIX "ff_nix.test"
+static const char* test_data_nix = "Filetype: Flipper File test\r\n"
+                                   "Version: 666\r\n"
+                                   "# This is comment\r\n"
+                                   "String data: String\r\n"
+                                   "Int32 data: 1234 -6345 7813 0\r\n"
+                                   "Uint32 data: 1234 0 5678 9098 7654321\r\n"
+                                   "Float data: 1.5 1000.0\r\n"
+                                   "Hex data: DE AD BE";
+
+#define READ_TEST_FLP "ff_flp.test"
+
+// data created by user on linux machine
+const char* test_file_linux = TEST_DIR READ_TEST_WIN;
+// data created by user on windows machine
+const char* test_file_windows = TEST_DIR READ_TEST_NIX;
+// data created by flipper itself
+const char* test_file_flipper = TEST_DIR READ_TEST_FLP;
+
+static bool storage_write_string(const char* path, const char* data) {
+    Storage* storage = furi_record_open("storage");
+    File* file = storage_file_alloc(storage);
+    bool result = false;
+
+    do {
+        if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) break;
+        if(storage_file_write(file, data, strlen(data)) != strlen(data)) break;
+
+        result = true;
+    } while(false);
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static void tests_setup() {
+    Storage* storage = furi_record_open("storage");
+    mu_assert(storage_simply_remove_recursive(storage, TEST_DIR_NAME), "Cannot clean data");
+    mu_assert(storage_simply_mkdir(storage, TEST_DIR_NAME), "Cannot create dir");
+    furi_record_close("storage");
+}
+
+static void tests_teardown() {
+    Storage* storage = furi_record_open("storage");
+    mu_assert(storage_simply_remove_recursive(storage, TEST_DIR_NAME), "Cannot clean data");
+    furi_record_close("storage");
+}
+
+static bool test_read(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+
+    FlipperFile* file = flipper_file_alloc(storage);
+    string_t string_value;
+    string_init(string_value);
+    uint32_t uint32_value;
+    void* scratchpad = malloc(512);
+
+    do {
+        if(!flipper_file_open_existing(file, file_name)) break;
+
+        if(!flipper_file_read_header(file, string_value, &uint32_value)) break;
+        if(string_cmp_str(string_value, test_filetype) != 0) break;
+        if(uint32_value != test_version) break;
+
+        if(!flipper_file_read_string(file, test_string_key, string_value)) break;
+        if(string_cmp_str(string_value, test_string_data) != 0) break;
+
+        if(!flipper_file_get_value_count(file, test_int_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_int_data)) break;
+        if(!flipper_file_read_int32(file, test_int_key, scratchpad, uint32_value)) break;
+        if(memcmp(scratchpad, test_int_data, sizeof(int32_t) * COUNT_OF(test_int_data)) != 0)
+            break;
+
+        if(!flipper_file_get_value_count(file, test_uint_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_uint_data)) break;
+        if(!flipper_file_read_uint32(file, test_uint_key, scratchpad, uint32_value)) break;
+        if(memcmp(scratchpad, test_uint_data, sizeof(uint32_t) * COUNT_OF(test_uint_data)) != 0)
+            break;
+
+        if(!flipper_file_get_value_count(file, test_float_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_float_data)) break;
+        if(!flipper_file_read_float(file, test_float_key, scratchpad, uint32_value)) break;
+        if(memcmp(scratchpad, test_float_data, sizeof(float) * COUNT_OF(test_float_data)) != 0)
+            break;
+
+        if(!flipper_file_get_value_count(file, test_hex_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_hex_data)) break;
+        if(!flipper_file_read_hex(file, test_hex_key, scratchpad, uint32_value)) break;
+        if(memcmp(scratchpad, test_hex_data, sizeof(uint8_t) * COUNT_OF(test_hex_data)) != 0)
+            break;
+
+        result = true;
+    } while(false);
+
+    free(scratchpad);
+    string_clear(string_value);
+    flipper_file_close(file);
+    flipper_file_free(file);
+
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_read_updated(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+
+    FlipperFile* file = flipper_file_alloc(storage);
+    string_t string_value;
+    string_init(string_value);
+    uint32_t uint32_value;
+    void* scratchpad = malloc(512);
+
+    do {
+        if(!flipper_file_open_existing(file, file_name)) break;
+
+        if(!flipper_file_read_header(file, string_value, &uint32_value)) break;
+        if(string_cmp_str(string_value, test_filetype) != 0) break;
+        if(uint32_value != test_version) break;
+
+        if(!flipper_file_read_string(file, test_string_key, string_value)) break;
+        if(string_cmp_str(string_value, test_string_updated_data) != 0) break;
+
+        if(!flipper_file_get_value_count(file, test_int_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_int_updated_data)) break;
+        if(!flipper_file_read_int32(file, test_int_key, scratchpad, uint32_value)) break;
+        if(memcmp(
+               scratchpad,
+               test_int_updated_data,
+               sizeof(int32_t) * COUNT_OF(test_int_updated_data)) != 0)
+            break;
+
+        if(!flipper_file_get_value_count(file, test_uint_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_uint_updated_data)) break;
+        if(!flipper_file_read_uint32(file, test_uint_key, scratchpad, uint32_value)) break;
+        if(memcmp(
+               scratchpad,
+               test_uint_updated_data,
+               sizeof(uint32_t) * COUNT_OF(test_uint_updated_data)) != 0)
+            break;
+
+        if(!flipper_file_get_value_count(file, test_float_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_float_updated_data)) break;
+        if(!flipper_file_read_float(file, test_float_key, scratchpad, uint32_value)) break;
+        if(memcmp(
+               scratchpad,
+               test_float_updated_data,
+               sizeof(float) * COUNT_OF(test_float_updated_data)) != 0)
+            break;
+
+        if(!flipper_file_get_value_count(file, test_hex_key, &uint32_value)) break;
+        if(uint32_value != COUNT_OF(test_hex_updated_data)) break;
+        if(!flipper_file_read_hex(file, test_hex_key, scratchpad, uint32_value)) break;
+        if(memcmp(
+               scratchpad,
+               test_hex_updated_data,
+               sizeof(uint8_t) * COUNT_OF(test_hex_updated_data)) != 0)
+            break;
+
+        result = true;
+    } while(false);
+
+    free(scratchpad);
+    string_clear(string_value);
+    flipper_file_close(file);
+    flipper_file_free(file);
+
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_write(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    do {
+        if(!flipper_file_open_always(file, file_name)) break;
+        if(!flipper_file_write_header_cstr(file, test_filetype, test_version)) break;
+        if(!flipper_file_write_comment_cstr(file, "This is comment")) break;
+        if(!flipper_file_write_string_cstr(file, test_string_key, test_string_data)) break;
+        if(!flipper_file_write_int32(file, test_int_key, test_int_data, COUNT_OF(test_int_data)))
+            break;
+        if(!flipper_file_write_uint32(
+               file, test_uint_key, test_uint_data, COUNT_OF(test_uint_data)))
+            break;
+        if(!flipper_file_write_float(
+               file, test_float_key, test_float_data, COUNT_OF(test_float_data)))
+            break;
+        if(!flipper_file_write_hex(file, test_hex_key, test_hex_data, COUNT_OF(test_hex_data)))
+            break;
+        result = true;
+    } while(false);
+
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_delete_last_key(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    do {
+        if(!flipper_file_open_existing(file, file_name)) break;
+        if(!flipper_file_delete_key(file, test_hex_key)) break;
+        result = true;
+    } while(false);
+
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_append_key(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    do {
+        if(!flipper_file_open_append(file, file_name)) break;
+        if(!flipper_file_write_hex(file, test_hex_key, test_hex_data, COUNT_OF(test_hex_data)))
+            break;
+        result = true;
+    } while(false);
+
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_update(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    do {
+        if(!flipper_file_open_existing(file, file_name)) break;
+        if(!flipper_file_update_string_cstr(file, test_string_key, test_string_updated_data))
+            break;
+        if(!flipper_file_update_int32(
+               file, test_int_key, test_int_updated_data, COUNT_OF(test_int_updated_data)))
+            break;
+        if(!flipper_file_update_uint32(
+               file, test_uint_key, test_uint_updated_data, COUNT_OF(test_uint_updated_data)))
+            break;
+        if(!flipper_file_update_float(
+               file, test_float_key, test_float_updated_data, COUNT_OF(test_float_updated_data)))
+            break;
+        if(!flipper_file_update_hex(
+               file, test_hex_key, test_hex_updated_data, COUNT_OF(test_hex_updated_data)))
+            break;
+
+        result = true;
+    } while(false);
+
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_update_backward(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    do {
+        if(!flipper_file_open_existing(file, file_name)) break;
+        if(!flipper_file_update_string_cstr(file, test_string_key, test_string_data)) break;
+        if(!flipper_file_update_int32(file, test_int_key, test_int_data, COUNT_OF(test_int_data)))
+            break;
+        if(!flipper_file_update_uint32(
+               file, test_uint_key, test_uint_data, COUNT_OF(test_uint_data)))
+            break;
+        if(!flipper_file_update_float(
+               file, test_float_key, test_float_data, COUNT_OF(test_float_data)))
+            break;
+        if(!flipper_file_update_hex(file, test_hex_key, test_hex_data, COUNT_OF(test_hex_data)))
+            break;
+
+        result = true;
+    } while(false);
+
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_write_multikey(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    do {
+        if(!flipper_file_open_always(file, file_name)) break;
+        if(!flipper_file_write_header_cstr(file, test_filetype, test_version)) break;
+
+        bool error = false;
+        for(uint8_t index = 0; index < 100; index++) {
+            if(!flipper_file_write_hex(file, test_hex_key, &index, 1)) {
+                error = true;
+                break;
+            }
+        }
+        if(error) break;
+
+        result = true;
+    } while(false);
+
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+static bool test_read_multikey(const char* file_name) {
+    Storage* storage = furi_record_open("storage");
+    bool result = false;
+    FlipperFile* file = flipper_file_alloc(storage);
+
+    string_t string_value;
+    string_init(string_value);
+    uint32_t uint32_value;
+
+    do {
+        if(!flipper_file_open_existing(file, file_name)) break;
+        if(!flipper_file_read_header(file, string_value, &uint32_value)) break;
+        if(string_cmp_str(string_value, test_filetype) != 0) break;
+        if(uint32_value != test_version) break;
+
+        bool error = false;
+        uint8_t uint8_value;
+        for(uint8_t index = 0; index < 100; index++) {
+            if(!flipper_file_read_hex(file, test_hex_key, &uint8_value, 1)) {
+                error = true;
+                break;
+            }
+
+            if(uint8_value != index) {
+                error = true;
+                break;
+            }
+        }
+        if(error) break;
+
+        result = true;
+    } while(false);
+
+    string_clear(string_value);
+    flipper_file_close(file);
+    flipper_file_free(file);
+    furi_record_close("storage");
+
+    return result;
+}
+
+MU_TEST(flipper_file_write_test) {
+    mu_assert(storage_write_string(test_file_linux, test_data_nix), "Write test error [Linux]");
+    mu_assert(
+        storage_write_string(test_file_windows, test_data_win), "Write test error [Windows]");
+    mu_assert(test_write(test_file_flipper), "Write test error [Flipper]");
+}
+
+MU_TEST(flipper_file_read_test) {
+    mu_assert(test_read(test_file_linux), "Read test error [Linux]");
+    mu_assert(test_read(test_file_windows), "Read test error [Windows]");
+    mu_assert(test_read(test_file_flipper), "Read test error [Flipper]");
+}
+
+MU_TEST(flipper_file_delete_test) {
+    mu_assert(test_delete_last_key(test_file_linux), "Cannot delete key [Linux]");
+    mu_assert(test_delete_last_key(test_file_windows), "Cannot delete key [Windows]");
+    mu_assert(test_delete_last_key(test_file_flipper), "Cannot delete key [Flipper]");
+}
+
+MU_TEST(flipper_file_delete_result_test) {
+    mu_assert(!test_read(test_file_linux), "Key deleted incorrectly [Linux]");
+    mu_assert(!test_read(test_file_windows), "Key deleted incorrectly [Windows]");
+    mu_assert(!test_read(test_file_flipper), "Key deleted incorrectly [Flipper]");
+}
+
+MU_TEST(flipper_file_append_test) {
+    mu_assert(test_append_key(test_file_linux), "Cannot append data [Linux]");
+    mu_assert(test_append_key(test_file_windows), "Cannot append data [Windows]");
+    mu_assert(test_append_key(test_file_flipper), "Cannot append data [Flipper]");
+}
+
+MU_TEST(flipper_file_append_result_test) {
+    mu_assert(test_read(test_file_linux), "Data appended incorrectly [Linux]");
+    mu_assert(test_read(test_file_windows), "Data appended incorrectly [Windows]");
+    mu_assert(test_read(test_file_flipper), "Data appended incorrectly [Flipper]");
+}
+
+MU_TEST(flipper_file_update_1_test) {
+    mu_assert(test_update(test_file_linux), "Cannot update data #1 [Linux]");
+    mu_assert(test_update(test_file_windows), "Cannot update data #1 [Windows]");
+    mu_assert(test_update(test_file_flipper), "Cannot update data #1 [Flipper]");
+}
+
+MU_TEST(flipper_file_update_1_result_test) {
+    mu_assert(test_read_updated(test_file_linux), "Data #1 updated incorrectly [Linux]");
+    mu_assert(test_read_updated(test_file_windows), "Data #1 updated incorrectly [Windows]");
+    mu_assert(test_read_updated(test_file_flipper), "Data #1 updated incorrectly [Flipper]");
+}
+
+MU_TEST(flipper_file_update_2_test) {
+    mu_assert(test_update_backward(test_file_linux), "Cannot update data #2 [Linux]");
+    mu_assert(test_update_backward(test_file_windows), "Cannot update data #2 [Windows]");
+    mu_assert(test_update_backward(test_file_flipper), "Cannot update data #2 [Flipper]");
+}
+
+MU_TEST(flipper_file_update_2_result_test) {
+    mu_assert(test_read(test_file_linux), "Data #2 updated incorrectly [Linux]");
+    mu_assert(test_read(test_file_windows), "Data #2 updated incorrectly [Windows]");
+    mu_assert(test_read(test_file_flipper), "Data #2 updated incorrectly [Flipper]");
+}
+
+MU_TEST(flipper_file_multikey_test) {
+    mu_assert(test_write_multikey(TEST_DIR "ff_multiline.test"), "Multikey write test error");
+    mu_assert(test_read_multikey(TEST_DIR "ff_multiline.test"), "Multikey read test error");
+}
+
+MU_TEST_SUITE(flipper_file) {
+    tests_setup();
+    MU_RUN_TEST(flipper_file_write_test);
+    MU_RUN_TEST(flipper_file_read_test);
+    MU_RUN_TEST(flipper_file_delete_test);
+    MU_RUN_TEST(flipper_file_delete_result_test);
+    MU_RUN_TEST(flipper_file_append_test);
+    MU_RUN_TEST(flipper_file_append_result_test);
+    MU_RUN_TEST(flipper_file_update_1_test);
+    MU_RUN_TEST(flipper_file_update_1_result_test);
+    MU_RUN_TEST(flipper_file_update_2_test);
+    MU_RUN_TEST(flipper_file_update_2_result_test);
+    MU_RUN_TEST(flipper_file_multikey_test);
+    tests_teardown();
+}
+
+int run_minunit_test_flipper_file() {
+    MU_RUN_SUITE(flipper_file);
+    return MU_EXIT_CODE;
+}

+ 12 - 3
applications/tests/test_index.c

@@ -13,6 +13,7 @@
 int run_minunit();
 int run_minunit_test_irda_decoder_encoder();
 int run_minunit_test_rpc();
+int run_minunit_test_flipper_file();
 
 void minunit_print_progress(void) {
     static char progress[] = {'\\', '|', '/', '-'};
@@ -37,11 +38,9 @@ void unit_tests_cli(Cli* cli, string_t args, void* context) {
     minunit_status = 0;
 
     Loader* loader = furi_record_open("loader");
-    furi_record_close("loader");
-
     NotificationApp* notification = furi_record_open("notification");
-    furi_record_close("notification");
 
+    // TODO: lock device while test running
     if(loader_is_locked(loader)) {
         FURI_LOG_E(TESTS_TAG, "RPC: stop all applications to run tests");
         notification_message(notification, &sequence_blink_magenta_100);
@@ -49,10 +48,15 @@ void unit_tests_cli(Cli* cli, string_t args, void* context) {
         notification_message_block(notification, &sequence_set_only_blue_255);
 
         uint32_t heap_before = memmgr_get_free_heap();
+        uint32_t cycle_counter = DWT->CYCCNT;
 
         test_result |= run_minunit();
         test_result |= run_minunit_test_irda_decoder_encoder();
         test_result |= run_minunit_test_rpc();
+        test_result |= run_minunit_test_flipper_file();
+        cycle_counter = (DWT->CYCCNT - cycle_counter);
+
+        FURI_LOG_I(TESTS_TAG, "Consumed: %0.2fs", (float)cycle_counter / (SystemCoreClock));
 
         if(test_result == 0) {
             delay(200); /* wait for tested services and apps to deallocate */
@@ -69,10 +73,15 @@ void unit_tests_cli(Cli* cli, string_t args, void* context) {
             FURI_LOG_E(TESTS_TAG, "FAILED");
         }
     }
+
+    furi_record_close("notification");
+    furi_record_close("loader");
 }
 
 void unit_tests_cli_init() {
     Cli* cli = furi_record_open("cli");
+
+    // We need to launch apps from tests, so we cannot lock loader
     cli_add_command(cli, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL);
     furi_record_close("cli");
 }

+ 204 - 0
lib/flipper_file/file_helper.c

@@ -0,0 +1,204 @@
+#include "file_helper.h"
+
+const char flipper_file_eoln = '\n';
+const char flipper_file_eolr = '\r';
+
+bool file_helper_seek(File* file, int32_t offset) {
+    uint64_t position = storage_file_tell(file);
+    return storage_file_seek(file, position + offset, true);
+}
+
+bool file_helper_write_hex(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(char));
+            if(bytes_written != sizeof(char)) {
+                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;
+}
+
+bool file_helper_read_line(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);
+        // TODO process EOF
+        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) {
+                if(!file_helper_seek(file, i - bytes_were_read)) {
+                    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;
+}
+
+bool file_helper_seek_to_next_line(File* file) {
+    const uint8_t buffer_size = 32;
+    uint8_t buffer[buffer_size];
+    bool result = false;
+    bool error = false;
+
+    do {
+        uint16_t bytes_were_read = storage_file_read(file, buffer, buffer_size);
+        if(bytes_were_read == 0) {
+            if(storage_file_eof(file)) {
+                result = true;
+                break;
+            }
+        }
+
+        for(uint16_t i = 0; i < bytes_were_read; i++) {
+            if(buffer[i] == flipper_file_eoln) {
+                if(!file_helper_seek(file, i - bytes_were_read)) {
+                    error = true;
+                    break;
+                }
+
+                result = true;
+                break;
+            }
+        }
+
+        if(result || error) {
+            break;
+        }
+    } while(true);
+
+    return result;
+}
+
+bool file_helper_read_value(File* file, string_t value, bool* last) {
+    string_clean(value);
+    const uint8_t buffer_size = 32;
+    uint8_t buffer[buffer_size];
+    bool result = false;
+    bool error = false;
+
+    while(true) {
+        uint16_t bytes_were_read = storage_file_read(file, buffer, buffer_size);
+
+        if(bytes_were_read == 0) {
+            // check EOF
+            if(storage_file_eof(file) && string_size(value) > 0) {
+                result = true;
+                *last = true;
+                break;
+            }
+        }
+
+        for(uint16_t i = 0; i < bytes_were_read; i++) {
+            if(buffer[i] == flipper_file_eoln) {
+                if(string_size(value) > 0) {
+                    if(!file_helper_seek(file, i - bytes_were_read)) {
+                        error = true;
+                        break;
+                    }
+
+                    result = true;
+                    *last = true;
+                    break;
+                } else {
+                    error = true;
+                }
+            } else if(buffer[i] == ' ') {
+                if(string_size(value) > 0) {
+                    if(!file_helper_seek(file, i - bytes_were_read)) {
+                        error = true;
+                        break;
+                    }
+
+                    result = true;
+                    *last = false;
+                    break;
+                }
+
+            } else if(buffer[i] == flipper_file_eolr) {
+                // Ignore
+            } else {
+                string_push_back(value, buffer[i]);
+            }
+        }
+
+        if(error || result) break;
+    }
+
+    return result;
+}
+
+bool file_helper_write(File* file, const void* data, uint16_t data_size) {
+    uint16_t bytes_written = storage_file_write(file, data, data_size);
+    return bytes_written == data_size;
+}
+
+bool file_helper_write_eol(File* file) {
+    return file_helper_write(file, &flipper_file_eoln, sizeof(char));
+}
+
+bool file_helper_copy(File* file_from, File* file_to, uint64_t start_offset, uint64_t stop_offset) {
+    bool result = false;
+
+    const uint8_t buffer_size = 32;
+    uint8_t buffer[buffer_size];
+    uint64_t current_offset = start_offset;
+
+    if(storage_file_seek(file_from, start_offset, true)) {
+        do {
+            int32_t bytes_count = MIN(buffer_size, stop_offset - current_offset);
+            if(bytes_count <= 0) {
+                result = true;
+                break;
+            }
+
+            uint16_t bytes_were_read = storage_file_read(file_from, buffer, bytes_count);
+            if(bytes_were_read != bytes_count) break;
+
+            uint16_t bytes_were_written = storage_file_write(file_to, buffer, bytes_count);
+            if(bytes_were_written != bytes_count) break;
+
+            current_offset += bytes_count;
+        } while(true);
+    }
+
+    return result;
+}

+ 81 - 0
lib/flipper_file/file_helper.h

@@ -0,0 +1,81 @@
+#pragma once
+#include <stdint.h>
+#include <mlib/m-string.h>
+#include <storage/storage.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern const char flipper_file_eoln;
+extern const char flipper_file_eolr;
+
+/**
+ * Negative seek helper
+ * @param file 
+ * @param offset 
+ * @return bool 
+ */
+bool file_helper_seek(File* file, int32_t offset);
+
+/**
+ * Writes data to a file as a hexadecimal array.
+ * @param file 
+ * @param data 
+ * @param data_size 
+ * @return true on success write 
+ */
+bool file_helper_write_hex(File* file, const uint8_t* data, const uint16_t data_size);
+
+/**
+ * Reads data as a string from the stored rw pointer to the \\n symbol position. Ignores \r.
+ * @param file 
+ * @param str_result 
+ * @return true on success read
+ */
+bool file_helper_read_line(File* file, string_t str_result);
+
+/**
+ * Moves the RW pointer to the beginning of the next line
+ * @param file 
+ * @return bool 
+ */
+bool file_helper_seek_to_next_line(File* file);
+
+/**
+ * Read one value from array-like string (separated by ' ')
+ * @param file 
+ * @param value 
+ * @return bool 
+ */
+bool file_helper_read_value(File* file, string_t value, bool* last);
+
+/**
+ * Write helper
+ * @param file 
+ * @param data 
+ * @param data_size 
+ * @return bool 
+ */
+bool file_helper_write(File* file, const void* data, uint16_t data_size);
+
+/**
+ * Write EOL
+ * @param file 
+ * @return bool 
+ */
+bool file_helper_write_eol(File* file);
+
+/**
+ * Appends part of one file to the end of another file
+ * @param file_from 
+ * @param file_to 
+ * @param start_offset 
+ * @param stop_offset 
+ * @return bool 
+ */
+bool file_helper_copy(File* file_from, File* file_to, uint64_t start_offset, uint64_t stop_offset);
+
+#ifdef __cplusplus
+}
+#endif

+ 395 - 0
lib/flipper_file/flipper_file.c

@@ -0,0 +1,395 @@
+#include <furi.h>
+#include "file_helper.h"
+#include "flipper_file_helper.h"
+#include "flipper_file.h"
+#include "flipper_file_i.h"
+#include <inttypes.h>
+#include <toolbox/hex.h>
+
+FlipperFile* flipper_file_alloc(Storage* storage) {
+    // furi_assert(storage);
+
+    FlipperFile* flipper_file = malloc(sizeof(FlipperFile));
+    flipper_file->storage = storage;
+    flipper_file->file = storage_file_alloc(flipper_file->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_existing(FlipperFile* flipper_file, const char* filename) {
+    furi_assert(flipper_file);
+    bool result = storage_file_open(
+        flipper_file->file, filename, FSAM_READ | FSAM_WRITE, FSOM_OPEN_EXISTING);
+    return result;
+}
+
+bool flipper_file_open_append(FlipperFile* flipper_file, const char* filename) {
+    furi_assert(flipper_file);
+
+    bool result =
+        storage_file_open(flipper_file->file, filename, FSAM_READ | FSAM_WRITE, FSOM_OPEN_APPEND);
+
+    // Add EOL if it is not there
+    if(storage_file_size(flipper_file->file) >= 1) {
+        do {
+            char last_char;
+            result = false;
+
+            if(!file_helper_seek(flipper_file->file, -1)) break;
+
+            uint16_t bytes_were_read = storage_file_read(flipper_file->file, &last_char, 1);
+            if(bytes_were_read != 1) break;
+
+            if(last_char != flipper_file_eoln) {
+                if(!file_helper_write_eol(flipper_file->file)) break;
+            }
+
+            result = true;
+        } while(false);
+    }
+
+    return result;
+}
+
+bool flipper_file_open_always(FlipperFile* flipper_file, const char* filename) {
+    furi_assert(flipper_file);
+    bool result = storage_file_open(
+        flipper_file->file, filename, FSAM_READ | FSAM_WRITE, FSOM_CREATE_ALWAYS);
+    return result;
+}
+
+bool flipper_file_open_new(FlipperFile* flipper_file, const char* filename) {
+    furi_assert(flipper_file);
+    bool result = storage_file_open(
+        flipper_file->file, filename, FSAM_READ | FSAM_WRITE, FSOM_CREATE_NEW);
+    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_rewind(FlipperFile* flipper_file) {
+    furi_assert(flipper_file);
+    return storage_file_seek(flipper_file->file, 0, 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, 1);
+        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, 1);
+        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_get_value_count(FlipperFile* flipper_file, const char* key, uint32_t* count) {
+    furi_assert(flipper_file);
+    bool result = false;
+    bool last = false;
+
+    string_t value;
+    string_init(value);
+
+    uint32_t position = storage_file_tell(flipper_file->file);
+    do {
+        if(!flipper_file_seek_to_key(flipper_file->file, key)) break;
+
+        // Balance between speed and memory consumption
+        // I prefer lower speed but less memory consumption
+        *count = 0;
+
+        result = true;
+        while(true) {
+            if(!file_helper_read_value(flipper_file->file, value, &last)) {
+                result = false;
+                break;
+            }
+
+            *count = *count + 1;
+            if(last) break;
+        }
+
+    } while(true);
+
+    if(!storage_file_seek(flipper_file->file, position, true)) {
+        result = false;
+    }
+
+    string_clear(value);
+    return result;
+}
+
+bool flipper_file_write_comment(FlipperFile* flipper_file, string_t data) {
+    furi_assert(flipper_file);
+
+    bool result = false;
+    do {
+        const char comment_buffer[2] = {flipper_file_comment, ' '};
+        result = file_helper_write(flipper_file->file, comment_buffer, sizeof(comment_buffer));
+        if(!result) break;
+
+        result = file_helper_write(flipper_file->file, string_get_cstr(data), string_size(data));
+        if(!result) break;
+
+        result = file_helper_write_eol(flipper_file->file);
+    } 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_delete_key_and_call(
+    FlipperFile* flipper_file,
+    const char* key,
+    flipper_file_cb call,
+    const char* cb_key,
+    const void* cb_data,
+    const uint16_t cb_data_size) {
+    bool result = false;
+    File* scratch_file = storage_file_alloc(flipper_file->storage);
+
+    do {
+        // get size
+        uint64_t file_size = storage_file_size(flipper_file->file);
+        if(file_size == 0) break;
+
+        if(!storage_file_seek(flipper_file->file, 0, true)) break;
+
+        // find key
+        if(!flipper_file_seek_to_key(flipper_file->file, key)) break;
+        // get key start position
+        uint64_t start_position = storage_file_tell(flipper_file->file) - strlen(key);
+        if(start_position >= 2) {
+            start_position -= 2;
+        } else {
+            // something wrong
+            break;
+        }
+
+        // get value end position
+        if(!file_helper_seek_to_next_line(flipper_file->file)) break;
+        uint64_t end_position = storage_file_tell(flipper_file->file);
+        // newline symbol
+        if(end_position < file_size) {
+            end_position += 1;
+        }
+
+        // open scratchpad
+        const char* scratch_name = "";
+        if(!flipper_file_get_scratchpad_name(&scratch_name)) break;
+
+        if(!storage_file_open(
+               scratch_file, scratch_name, FSAM_READ | FSAM_WRITE, FSOM_CREATE_ALWAYS))
+            break;
+
+        // copy key file before key to scratchpad
+        if(!file_helper_copy(flipper_file->file, scratch_file, 0, start_position)) break;
+
+        // do something in between if needed
+        if(call != NULL) {
+            if(!call(scratch_file, cb_key, cb_data, cb_data_size)) break;
+        };
+
+        // copy key file after key value to scratchpad
+        if(!file_helper_copy(flipper_file->file, scratch_file, end_position, file_size)) break;
+
+        file_size = storage_file_tell(scratch_file);
+        if(file_size == 0) break;
+
+        if(!storage_file_seek(flipper_file->file, 0, true)) break;
+
+        // copy whole scratchpad file to the original file
+        if(!file_helper_copy(scratch_file, flipper_file->file, 0, file_size)) break;
+
+        // and truncate original file
+        if(!storage_file_truncate(flipper_file->file)) break;
+
+        // close and remove scratchpad file
+        if(!storage_file_close(scratch_file)) break;
+        if(storage_common_remove(flipper_file->storage, scratch_name) != FSE_OK) break;
+        result = true;
+    } while(false);
+
+    storage_file_free(scratch_file);
+
+    return result;
+}
+
+bool flipper_file_delete_key(FlipperFile* flipper_file, const char* key) {
+    furi_assert(flipper_file);
+    return flipper_file_delete_key_and_call(flipper_file, key, NULL, NULL, NULL, 0);
+}
+
+bool flipper_file_write_internal(
+    File* file,
+    const char* key,
+    const void* _data,
+    const uint16_t data_size,
+    FlipperFileValueType type) {
+    bool result = false;
+    string_t value;
+    string_init(value);
+
+    do {
+        result = flipper_file_write_key(file, key);
+        if(!result) break;
+
+        for(uint16_t i = 0; i < data_size; i++) {
+            switch(type) {
+            case FlipperFileValueHex: {
+                const uint8_t* data = _data;
+                string_printf(value, "%02X", data[i]);
+            }; break;
+            case FlipperFileValueFloat: {
+                const float* data = _data;
+                string_printf(value, "%f", data[i]);
+            }; break;
+            case FlipperFileValueInt32: {
+                const int32_t* data = _data;
+                string_printf(value, "%" PRIi32, data[i]);
+            }; break;
+            case FlipperFileValueUint32: {
+                const uint32_t* data = _data;
+                string_printf(value, "%" PRId32, data[i]);
+            }; break;
+            }
+
+            if((i + 1) < data_size) {
+                string_cat(value, " ");
+            }
+
+            result = file_helper_write(file, string_get_cstr(value), string_size(value));
+            if(!result) break;
+        }
+
+        result = file_helper_write_eol(file);
+    } while(false);
+
+    string_clear(value);
+    return result;
+}
+
+bool flipper_file_read_internal(
+    File* file,
+    const char* key,
+    void* _data,
+    const uint16_t data_size,
+    FlipperFileValueType type) {
+    bool result = false;
+    string_t value;
+    string_init(value);
+
+    if(flipper_file_seek_to_key(file, key)) {
+        result = true;
+        for(uint16_t i = 0; i < data_size; i++) {
+            bool last = false;
+            result = file_helper_read_value(file, value, &last);
+            if(result) {
+                int scan_values = 0;
+                switch(type) {
+                case FlipperFileValueHex: {
+                    uint8_t* data = _data;
+                    // sscanf "%02X" does not work here
+                    if(hex_chars_to_uint8(
+                           string_get_char(value, 0), string_get_char(value, 1), &data[i])) {
+                        scan_values = 1;
+                    }
+                }; break;
+                case FlipperFileValueFloat: {
+                    float* data = _data;
+                    // newlib-nano does not have sscanf for floats
+                    // scan_values = sscanf(string_get_cstr(value), "%f", &data[i]);
+                    char* end_char;
+                    data[i] = strtof(string_get_cstr(value), &end_char);
+                    if(*end_char == 0) {
+                        // very probably ok
+                        scan_values = 1;
+                    }
+                }; break;
+                case FlipperFileValueInt32: {
+                    int32_t* data = _data;
+                    scan_values = sscanf(string_get_cstr(value), "%" PRIi32, &data[i]);
+                }; break;
+                case FlipperFileValueUint32: {
+                    uint32_t* data = _data;
+                    scan_values = sscanf(string_get_cstr(value), "%" PRId32, &data[i]);
+                }; break;
+                }
+
+                if(scan_values != 1) {
+                    result = false;
+                    break;
+                }
+            } else {
+                break;
+            }
+
+            if(last && ((i + 1) != data_size)) {
+                result = false;
+                break;
+            }
+        }
+    }
+
+    string_clear(value);
+    return result;
+}
+
+File* flipper_file_get_file(FlipperFile* flipper_file) {
+    furi_assert(flipper_file);
+    furi_assert(flipper_file->file);
+
+    return flipper_file->file;
+}

+ 212 - 32
lib/toolbox/flipper-file.h → lib/flipper_file/flipper_file.h

@@ -17,8 +17,10 @@
  * 
  * ~~~~~~~~~~~~~~~~~~~~~
  * String: text
- * Uint32: 1
- * Hex Array: A4 B3 C2 D1 12 FF
+ * Int32: 1 2 -3 4
+ * Uint32: 1 2 3 4
+ * Float: 1.0 1234.654
+ * Hex: A4 B3 C2 D1 12 FF
  * ~~~~~~~~~~~~~~~~~~~~~
  * 
  * End of line is LF when writing, but CR is supported when reading.
@@ -33,13 +35,13 @@
  * # Just test file
  * String: String value
  * UINT: 1234
- * Hex Array: 00 01 FF A3
+ * Hex: 00 01 FF A3
  * ~~~~~~~~~~~~~~~~~~~~~
  * 
  * Writing:
  * 
  * ~~~~~~~~~~~~~~~~~~~~~
- * FlipperFile* file = flipper_file_alloc(storage);
+ * FlipperFile file = flipper_file_alloc(storage);
  * 
  * do {
  *     const uint32_t version = 1;
@@ -48,12 +50,12 @@
  *     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_open_new(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;
+ *     if(!flipper_file_flipper_file_write_uint32(file, "UINT", &uint32_value, 1)) break;
+ *     if(!flipper_file_write_hex(file, "Hex Array", array, array_size)) break;
  *     
  *     // signal that the file was written successfully
  * } while(0);
@@ -65,7 +67,7 @@
  * Reading:
  * 
  * ~~~~~~~~~~~~~~~~~~~~~
- * FlipperFile* file = flipper_file_alloc(storage);
+ * FlipperFile file = flipper_file_alloc(storage);
  * 
  * do {
  *     uint32_t version = 1;
@@ -77,11 +79,11 @@
  *     string_init(file_type);
  *     string_init(string_value);
  *     
- *     if(!flipper_file_open_read(file, "/ext/flipper_file_test")) break;
+ *     if(!flipper_file_open_existing(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;
+ *     if(!flipper_file_read_uint32(file, "UINT", &uint32_value, 1)) break;
+ *     if(!flipper_file_read_hex(file, "Hex Array", array, array_size)) break;
  *     
  *     // signal that the file was read successfully
  * } while(0);
@@ -118,20 +120,36 @@ FlipperFile* flipper_file_alloc(Storage* storage);
 void flipper_file_free(FlipperFile* flipper_file);
 
 /**
- * Open file for reading.
+ * Open existing file.
  * @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);
+bool flipper_file_open_existing(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.
+ * Open existing file for writing and add values to the end of file.
  * @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);
+bool flipper_file_open_append(FlipperFile* flipper_file, const char* filename);
+
+/**
+ * Open file. 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_open_always(FlipperFile* flipper_file, const char* filename);
+
+/**
+ * Open file. Creates a new file, fails if file already exists.
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @param filename File name and path
+ * @return True on success
+ */
+bool flipper_file_open_new(FlipperFile* flipper_file, const char* filename);
 
 /**
  * Close the file.
@@ -140,6 +158,13 @@ bool flipper_file_new_write(FlipperFile* flipper_file, const char* filename);
  */
 bool flipper_file_close(FlipperFile* flipper_file);
 
+/**
+ * Rewind the file RW pointer.
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @return True on success
+ */
+bool flipper_file_rewind(FlipperFile* flipper_file);
+
 /**
  * Read the header (file type and version) from the file.
  * @param flipper_file Pointer to a FlipperFile instance
@@ -173,6 +198,15 @@ bool flipper_file_write_header_cstr(
     const char* filetype,
     const uint32_t version);
 
+/**
+ * Get the count of values by key
+ * @param flipper_file 
+ * @param key 
+ * @param count 
+ * @return bool 
+ */
+bool flipper_file_get_value_count(FlipperFile* flipper_file, const char* key, uint32_t* count);
+
 /**
  * Read a string from a file by Key
  * @param flipper_file Pointer to a FlipperFile instance
@@ -201,22 +235,116 @@ bool flipper_file_write_string(FlipperFile* flipper_file, const char* key, strin
 bool flipper_file_write_string_cstr(FlipperFile* flipper_file, const char* key, const char* data);
 
 /**
- * Read uint32 from a file by Key
+ * Read array of uint32 from a file by Key
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_read_uint32(
+    FlipperFile* flipper_file,
+    const char* key,
+    uint32_t* data,
+    const uint16_t data_size);
+
+/**
+ * Write key and array of uint32 to file.
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_write_uint32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const uint32_t* data,
+    const uint16_t data_size);
+
+/**
+ * Read array of int32 from a file by Key
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_read_int32(
+    FlipperFile* flipper_file,
+    const char* key,
+    int32_t* data,
+    const uint16_t data_size);
+
+/**
+ * Write key and array of int32 to file.
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_write_int32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const int32_t* data,
+    const uint16_t data_size);
+
+/**
+ * Read array of float from a file by Key
+ * @param flipper_file Pointer to a FlipperFile instance
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_read_float(
+    FlipperFile* flipper_file,
+    const char* key,
+    float* data,
+    const uint16_t data_size);
+
+/**
+ * Write key and array of float to file.
  * @param flipper_file Pointer to a FlipperFile instance
  * @param key Key
  * @param data Value
+ * @param data_size Values count
  * @return True on success
  */
-bool flipper_file_read_uint32(FlipperFile* flipper_file, const char* key, uint32_t* data);
+bool flipper_file_write_float(
+    FlipperFile* flipper_file,
+    const char* key,
+    const float* data,
+    const uint16_t data_size);
+
+/**
+ * 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(
+    FlipperFile* flipper_file,
+    const char* key,
+    uint8_t* data,
+    const uint16_t data_size);
 
 /**
- * Write key and uint32 to file.
+ * Write key and hex array to file.
  * @param flipper_file Pointer to a FlipperFile instance
  * @param key Key
  * @param data Value
+ * @param data_size Values count
  * @return True on success
  */
-bool flipper_file_write_uint32(FlipperFile* flipper_file, const char* key, const uint32_t data);
+bool flipper_file_write_hex(
+    FlipperFile* flipper_file,
+    const char* key,
+    const uint8_t* data,
+    const uint16_t data_size);
 
 /**
  * Write comment to file.
@@ -235,41 +363,93 @@ bool flipper_file_write_comment(FlipperFile* flipper_file, string_t data);
 bool flipper_file_write_comment_cstr(FlipperFile* flipper_file, const char* data);
 
 /**
- * Read hex array from a file by Key
+ * Removes the first matching key and its value from the file. Changes the RW pointer to an undefined position.
  * @param flipper_file Pointer to a FlipperFile instance
  * @param key Key
+ * @return True on success
+ */
+bool flipper_file_delete_key(FlipperFile* flipper_file, const char* key);
+
+/**
+ * Updates the value of the first matching key to a string value. Changes the RW pointer to an undefined position.
+ * @param flipper_file Pointer to a FlipperFile instance 
+ * @param key Key
+ * @param data Value
+ * @return True on success
+ */
+bool flipper_file_update_string(FlipperFile* flipper_file, const char* key, string_t data);
+
+/**
+ * Updates the value of the first matching key to a string value. Plain C version. Changes the RW pointer to an undefined position.
+ * @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(
+bool flipper_file_update_string_cstr(FlipperFile* flipper_file, const char* key, const char* data);
+
+/**
+ * Updates the value of the first matching key to a uint32 array value. Changes the RW pointer to an undefined position.
+ * @param flipper_file Pointer to a FlipperFile instance 
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_update_uint32(
     FlipperFile* flipper_file,
     const char* key,
-    uint8_t* data,
+    const uint32_t* data,
     const uint16_t data_size);
 
 /**
- * Write key and hex array to file.
- * @param flipper_file Pointer to a FlipperFile instance
+ * Updates the value of the first matching key to a int32 array value. Changes the RW pointer to an undefined position.
+ * @param flipper_file Pointer to a FlipperFile instance 
  * @param key Key
  * @param data Value
- * @param data_size Value size
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_update_int32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const int32_t* data,
+    const uint16_t data_size);
+
+/**
+ * Updates the value of the first matching key to a float array value. Changes the RW pointer to an undefined position.
+ * @param flipper_file Pointer to a FlipperFile instance 
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
  * @return True on success
  */
-bool flipper_file_write_hex_array(
+bool flipper_file_update_float(
+    FlipperFile* flipper_file,
+    const char* key,
+    const float* data,
+    const uint16_t data_size);
+
+/**
+ * Updates the value of the first matching key to a hex array value. Changes the RW pointer to an undefined position.
+ * @param flipper_file Pointer to a FlipperFile instance 
+ * @param key Key
+ * @param data Value
+ * @param data_size Values count
+ * @return True on success
+ */
+bool flipper_file_update_hex(
     FlipperFile* flipper_file,
     const char* key,
     const uint8_t* data,
     const uint16_t data_size);
 
 /** Get file descriptor.
- *
+ * 
  * We higly don't recommend to use it.
  * This instance is owned by FlipperFile.
- *
- * @param      flipper_file pointer to FlipperFile instance
- *
- * @return     pointer to File instance
+ * @param flipper_file 
+ * @return File* 
  */
 File* flipper_file_get_file(FlipperFile* flipper_file);
 

+ 42 - 0
lib/flipper_file/flipper_file_float.c

@@ -0,0 +1,42 @@
+#include <furi.h>
+
+#include "flipper_file.h"
+#include "flipper_file_i.h"
+#include "flipper_file_helper.h"
+
+static bool flipper_file_write_float_internal(
+    File* file,
+    const char* key,
+    const void* _data,
+    const uint16_t data_size) {
+    return flipper_file_write_internal(file, key, _data, data_size, FlipperFileValueFloat);
+};
+
+bool flipper_file_read_float(
+    FlipperFile* flipper_file,
+    const char* key,
+    float* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_read_internal(
+        flipper_file->file, key, data, data_size, FlipperFileValueFloat);
+}
+
+bool flipper_file_write_float(
+    FlipperFile* flipper_file,
+    const char* key,
+    const float* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_write_float_internal(flipper_file->file, key, data, data_size);
+}
+
+bool flipper_file_update_float(
+    FlipperFile* flipper_file,
+    const char* key,
+    const float* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_delete_key_and_call(
+        flipper_file, key, flipper_file_write_float_internal, key, data, data_size);
+}

+ 118 - 0
lib/flipper_file/flipper_file_helper.c

@@ -0,0 +1,118 @@
+#include "flipper_file_helper.h"
+
+const char* flipper_file_filetype_key = "Filetype";
+const char* flipper_file_version_key = "Version";
+const char flipper_file_delimiter = ':';
+const char flipper_file_comment = '#';
+
+#ifdef __linux__
+const char* flipper_file_scratchpad = ".scratch.pad";
+#else
+const char* flipper_file_scratchpad = "/any/.scratch.pad";
+#endif
+
+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
+                        if(!file_helper_seek(file, i - bytes_were_read)) {
+                            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;
+}
+
+bool flipper_file_seek_to_key(File* file, const char* key) {
+    bool found = false;
+    string_t readed_key;
+
+    string_init(readed_key);
+
+    while(!storage_file_eof(file)) {
+        if(flipper_file_read_valid_key(file, readed_key)) {
+            if(string_cmp_str(readed_key, key) == 0) {
+                if(!file_helper_seek(file, 2)) break;
+
+                found = true;
+                break;
+            }
+        }
+    }
+    string_clear(readed_key);
+
+    return found;
+}
+
+bool flipper_file_write_key(File* file, const char* key) {
+    bool result = false;
+
+    do {
+        result = file_helper_write(file, key, strlen(key));
+        if(!result) break;
+
+        const char delimiter_buffer[2] = {flipper_file_delimiter, ' '};
+        result = file_helper_write(file, delimiter_buffer, sizeof(delimiter_buffer));
+    } while(false);
+
+    return result;
+}
+
+bool flipper_file_get_scratchpad_name(const char** name) {
+    // TODO do not rewrite existing file
+    *name = flipper_file_scratchpad;
+    return true;
+}

+ 51 - 0
lib/flipper_file/flipper_file_helper.h

@@ -0,0 +1,51 @@
+#pragma once
+#include <stdint.h>
+#include <mlib/m-string.h>
+#include <storage/storage.h>
+#include "file_helper.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern const char* flipper_file_filetype_key;
+extern const char* flipper_file_version_key;
+extern const char flipper_file_delimiter;
+extern const char flipper_file_comment;
+
+/**
+ * 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);
+
+/**
+ * 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);
+
+/**
+ * Write key and key delimiter
+ * @param file 
+ * @param key 
+ * @return bool 
+ */
+bool flipper_file_write_key(File* file, const char* key);
+
+/**
+ * Get scratchpad name and path
+ * @param name 
+ * @return bool 
+ */
+bool flipper_file_get_scratchpad_name(const char** name);
+
+#ifdef __cplusplus
+}
+#endif

+ 42 - 0
lib/flipper_file/flipper_file_hex.c

@@ -0,0 +1,42 @@
+#include <furi.h>
+
+#include "flipper_file.h"
+#include "flipper_file_i.h"
+#include "flipper_file_helper.h"
+
+static bool flipper_file_write_hex_internal(
+    File* file,
+    const char* key,
+    const void* _data,
+    const uint16_t data_size) {
+    return flipper_file_write_internal(file, key, _data, data_size, FlipperFileValueHex);
+};
+
+bool flipper_file_write_hex(
+    FlipperFile* flipper_file,
+    const char* key,
+    const uint8_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_write_hex_internal(flipper_file->file, key, data, data_size);
+}
+
+bool flipper_file_read_hex(
+    FlipperFile* flipper_file,
+    const char* key,
+    uint8_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_read_internal(
+        flipper_file->file, key, data, data_size, FlipperFileValueHex);
+}
+
+bool flipper_file_update_hex(
+    FlipperFile* flipper_file,
+    const char* key,
+    const uint8_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_delete_key_and_call(
+        flipper_file, key, flipper_file_write_hex_internal, key, data, data_size);
+}

+ 72 - 0
lib/flipper_file/flipper_file_i.h

@@ -0,0 +1,72 @@
+#include <stdint.h>
+#include <storage/storage.h>
+
+struct FlipperFile {
+    File* file;
+    Storage* storage;
+};
+
+/**
+ *  Value write type callback
+ */
+typedef bool (*flipper_file_cb)(File* file, const char* key, const void* data, uint16_t data_size);
+
+/**
+ * 
+ * @param flipper_file 
+ * @param key 
+ * @param cb 
+ * @param cb_key 
+ * @param cb_data 
+ * @param cb_data_size 
+ * @return bool 
+ */
+bool flipper_file_delete_key_and_call(
+    FlipperFile* flipper_file,
+    const char* key,
+    flipper_file_cb cb,
+    const char* cb_key,
+    const void* cb_data,
+    const uint16_t cb_data_size);
+
+/**
+ * Value types
+ */
+typedef enum {
+    FlipperFileValueHex,
+    FlipperFileValueFloat,
+    FlipperFileValueInt32,
+    FlipperFileValueUint32,
+} FlipperFileValueType;
+
+/**
+ * Internal write values function
+ * @param file 
+ * @param key 
+ * @param _data 
+ * @param data_size 
+ * @param type 
+ * @return bool 
+ */
+bool flipper_file_write_internal(
+    File* file,
+    const char* key,
+    const void* _data,
+    const uint16_t data_size,
+    FlipperFileValueType type);
+
+/**
+ * Internal read values function
+ * @param file 
+ * @param key 
+ * @param _data 
+ * @param data_size 
+ * @param type 
+ * @return bool 
+ */
+bool flipper_file_read_internal(
+    File* file,
+    const char* key,
+    void* _data,
+    const uint16_t data_size,
+    FlipperFileValueType type);

+ 42 - 0
lib/flipper_file/flipper_file_int32.c

@@ -0,0 +1,42 @@
+#include <furi.h>
+
+#include "flipper_file.h"
+#include "flipper_file_i.h"
+#include "flipper_file_helper.h"
+
+static bool flipper_file_write_int32_internal(
+    File* file,
+    const char* key,
+    const void* _data,
+    const uint16_t data_size) {
+    return flipper_file_write_internal(file, key, _data, data_size, FlipperFileValueInt32);
+};
+
+bool flipper_file_read_int32(
+    FlipperFile* flipper_file,
+    const char* key,
+    int32_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_read_internal(
+        flipper_file->file, key, data, data_size, FlipperFileValueInt32);
+}
+
+bool flipper_file_write_int32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const int32_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_write_int32_internal(flipper_file->file, key, data, data_size);
+}
+
+bool flipper_file_update_int32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const int32_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_delete_key_and_call(
+        flipper_file, key, flipper_file_write_int32_internal, key, data, data_size);
+}

+ 67 - 0
lib/flipper_file/flipper_file_string.c

@@ -0,0 +1,67 @@
+#include <furi.h>
+
+#include "flipper_file.h"
+#include "flipper_file_i.h"
+#include "flipper_file_helper.h"
+
+static bool flipper_file_write_string_internal(
+    File* file,
+    const char* key,
+    const void* data,
+    const uint16_t data_size) {
+    bool result = false;
+    (void)data_size;
+
+    do {
+        result = flipper_file_write_key(file, key);
+        if(!result) break;
+
+        result = file_helper_write(file, string_get_cstr(data), string_size(data));
+        if(!result) break;
+
+        result = file_helper_write_eol(file);
+    } while(false);
+
+    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(file_helper_read_line(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);
+    return flipper_file_write_string_internal(flipper_file->file, key, data, 0);
+}
+
+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_update_string(FlipperFile* flipper_file, const char* key, string_t data) {
+    furi_assert(flipper_file);
+    return flipper_file_delete_key_and_call(
+        flipper_file, key, flipper_file_write_string_internal, key, data, 0);
+}
+
+bool flipper_file_update_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_update_string(flipper_file, key, value);
+    string_clear(value);
+    return result;
+}

+ 42 - 0
lib/flipper_file/flipper_file_uint32.c

@@ -0,0 +1,42 @@
+#include <furi.h>
+
+#include "flipper_file.h"
+#include "flipper_file_i.h"
+#include "flipper_file_helper.h"
+
+static bool flipper_file_write_uint32_internal(
+    File* file,
+    const char* key,
+    const void* _data,
+    const uint16_t data_size) {
+    return flipper_file_write_internal(file, key, _data, data_size, FlipperFileValueUint32);
+};
+
+bool flipper_file_read_uint32(
+    FlipperFile* flipper_file,
+    const char* key,
+    uint32_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_read_internal(
+        flipper_file->file, key, data, data_size, FlipperFileValueUint32);
+}
+
+bool flipper_file_write_uint32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const uint32_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_write_uint32_internal(flipper_file->file, key, data, data_size);
+}
+
+bool flipper_file_update_uint32(
+    FlipperFile* flipper_file,
+    const char* key,
+    const uint32_t* data,
+    const uint16_t data_size) {
+    furi_assert(flipper_file);
+    return flipper_file_delete_key_and_call(
+        flipper_file, key, flipper_file_write_uint32_internal, key, data, data_size);
+}

+ 4 - 0
lib/lib.mk

@@ -120,3 +120,7 @@ C_SOURCES		+= $(wildcard $(LIB_DIR)/nanopb/*.c)
 # heatshrink
 CFLAGS			+= -I$(LIB_DIR)/heatshrink
 C_SOURCES		+= $(wildcard $(LIB_DIR)/heatshrink/*.c)
+
+# Toolbox
+CFLAGS			+= -I$(LIB_DIR)/flipper_file
+C_SOURCES		+= $(wildcard $(LIB_DIR)/flipper_file/*.c)

+ 18 - 16
lib/subghz/subghz_keystore.c

@@ -4,8 +4,8 @@
 #include <furi-hal.h>
 
 #include <storage/storage.h>
-#include <lib/toolbox/hex.h>
-#include <lib/toolbox/flipper-file.h>
+#include <toolbox/hex.h>
+#include <flipper_file/flipper_file.h>
 
 #define SUBGHZ_KEYSTORE_TAG "SubGhzParser"
 
@@ -185,7 +185,7 @@ bool subghz_keystore_load(SubGhzKeystore* instance, const char* file_name) {
 
     FlipperFile* flipper_file = flipper_file_alloc(storage);
     do {
-        if(!flipper_file_open_read(flipper_file, file_name)) {
+        if(!flipper_file_open_existing(flipper_file, file_name)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to open file for read: %s", file_name);
             break;
         }
@@ -193,7 +193,7 @@ bool subghz_keystore_load(SubGhzKeystore* instance, const char* file_name) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing or incorrect header");
             break;
         }
-        if(!flipper_file_read_uint32(flipper_file, "Encryption", (uint32_t*)&encryption)) {
+        if(!flipper_file_read_uint32(flipper_file, "Encryption", (uint32_t*)&encryption, 1)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing encryption type");
             break;
         }
@@ -208,7 +208,7 @@ bool subghz_keystore_load(SubGhzKeystore* instance, const char* file_name) {
         if(encryption == SubGhzKeystoreEncryptionNone) {
             result = subghz_keystore_read_file(instance, file, NULL);
         } else if(encryption == SubGhzKeystoreEncryptionAES256) {
-            if(!flipper_file_read_hex_array(flipper_file, "IV", iv, 16)) {
+            if(!flipper_file_read_hex(flipper_file, "IV", iv, 16)) {
                 FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing IV");
                 break;
             }
@@ -239,7 +239,7 @@ bool subghz_keystore_save(SubGhzKeystore* instance, const char* file_name, uint8
 
     FlipperFile* flipper_file = flipper_file_alloc(storage);
     do {
-        if(!flipper_file_new_write(flipper_file, file_name)) {
+        if(!flipper_file_open_always(flipper_file, file_name)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to open file for write: %s", file_name);
             break;
         }
@@ -248,11 +248,12 @@ bool subghz_keystore_save(SubGhzKeystore* instance, const char* file_name, uint8
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to add header");
             break;
         }
-        if(!flipper_file_write_uint32(flipper_file, "Encryption", SubGhzKeystoreEncryptionAES256)) {
+        SubGhzKeystoreEncryption encryption = SubGhzKeystoreEncryptionAES256;
+        if(!flipper_file_write_uint32(flipper_file, "Encryption", (uint32_t*)&encryption, 1)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to add Encryption");
             break;
         }
-        if(!flipper_file_write_hex_array(flipper_file, "IV", iv, 16)) {
+        if(!flipper_file_write_hex(flipper_file, "IV", iv, 16)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to add IV");
             break;
         }
@@ -342,7 +343,7 @@ bool subghz_keystore_raw_encrypted_save(
 
     FlipperFile* input_flipper_file = flipper_file_alloc(storage);
     do {
-        if(!flipper_file_open_read(input_flipper_file, input_file_name)) {
+        if(!flipper_file_open_existing(input_flipper_file, input_file_name)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to open file for read: %s", input_file_name);
             break;
         }
@@ -350,7 +351,7 @@ bool subghz_keystore_raw_encrypted_save(
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing or incorrect header");
             break;
         }
-        if(!flipper_file_read_uint32(input_flipper_file, "Encryption", (uint32_t*)&encryption)) {
+        if(!flipper_file_read_uint32(input_flipper_file, "Encryption", (uint32_t*)&encryption, 1)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing encryption type");
             break;
         }
@@ -369,7 +370,7 @@ bool subghz_keystore_raw_encrypted_save(
 
         FlipperFile* output_flipper_file = flipper_file_alloc(storage);
 
-        if(!flipper_file_new_write(output_flipper_file, output_file_name)) {
+        if(!flipper_file_open_always(output_flipper_file, output_file_name)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to open file for write: %s", output_file_name);
             break;
         }
@@ -378,12 +379,13 @@ bool subghz_keystore_raw_encrypted_save(
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to add header");
             break;
         }
+        SubGhzKeystoreEncryption tmp_encryption = SubGhzKeystoreEncryptionAES256;
         if(!flipper_file_write_uint32(
-               output_flipper_file, "Encryption", SubGhzKeystoreEncryptionAES256)) {
+               output_flipper_file, "Encryption", (uint32_t*)&tmp_encryption, 1)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to add Encryption");
             break;
         }
-        if(!flipper_file_write_hex_array(output_flipper_file, "IV", iv, 16)) {
+        if(!flipper_file_write_hex(output_flipper_file, "IV", iv, 16)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to add IV");
             break;
         }
@@ -480,7 +482,7 @@ bool subghz_keystore_raw_get_data(const char* file_name, size_t offset, uint8_t*
 
     FlipperFile* flipper_file = flipper_file_alloc(storage);
     do {
-        if(!flipper_file_open_read(flipper_file, file_name)) {
+        if(!flipper_file_open_existing(flipper_file, file_name)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Unable to open file for read: %s", file_name);
             break;
         }
@@ -488,7 +490,7 @@ bool subghz_keystore_raw_get_data(const char* file_name, size_t offset, uint8_t*
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing or incorrect header");
             break;
         }
-        if(!flipper_file_read_uint32(flipper_file, "Encryption", (uint32_t*)&encryption)) {
+        if(!flipper_file_read_uint32(flipper_file, "Encryption", (uint32_t*)&encryption, 1)) {
             FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing encryption type");
             break;
         }
@@ -506,7 +508,7 @@ bool subghz_keystore_raw_get_data(const char* file_name, size_t offset, uint8_t*
         }
 
         if(offset < 16) {
-            if(!flipper_file_read_hex_array(flipper_file, "IV", iv, 16)) {
+            if(!flipper_file_read_hex(flipper_file, "IV", iv, 16)) {
                 FURI_LOG_E(SUBGHZ_KEYSTORE_TAG, "Missing IV");
                 break;
             }

+ 0 - 72
lib/toolbox/flipper-file-cpp.cpp

@@ -1,72 +0,0 @@
-#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);
-}

+ 0 - 41
lib/toolbox/flipper-file-cpp.h

@@ -1,41 +0,0 @@
-#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);
-};

+ 0 - 471
lib/toolbox/flipper-file.c

@@ -1,471 +0,0 @@
-#include <furi.h>
-#include "flipper-file.h"
-#include <toolbox/hex.h>
-#include <inttypes.h>
-
-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;
-}
-
-File* flipper_file_get_file(FlipperFile* flipper_file) {
-    furi_assert(flipper_file);
-    furi_assert(flipper_file->file);
-
-    return flipper_file->file;
-}

+ 12 - 0
lib/toolbox/hex.c

@@ -13,4 +13,16 @@ bool hex_char_to_hex_nibble(char c, uint8_t* nibble) {
     } else {
         return false;
     }
+}
+
+bool hex_chars_to_uint8(char hi, char low, uint8_t* value) {
+    uint8_t hi_nibble_value, low_nibble_value;
+
+    if(hex_char_to_hex_nibble(hi, &hi_nibble_value) &&
+       hex_char_to_hex_nibble(low, &low_nibble_value)) {
+        *value = (hi_nibble_value << 4) | low_nibble_value;
+        return true;
+    } else {
+        return false;
+    }
 }

+ 10 - 2
lib/toolbox/hex.h

@@ -7,14 +7,22 @@ extern "C" {
 #endif
 
 /**
- * @brief Convert ASCII hex value to nibble 
- * 
+ * Convert ASCII hex value to nibble 
  * @param c ASCII character
  * @param nibble nibble pointer, output
  * @return bool conversion status
  */
 bool hex_char_to_hex_nibble(char c, uint8_t* nibble);
 
+/**
+ * Convert ASCII hex values to byte
+ * @param hi hi nibble text 
+ * @param low low nibble text
+ * @param value output value
+ * @return bool conversion status
+ */
+bool hex_chars_to_uint8(char hi, char low, uint8_t* value);
+
 #ifdef __cplusplus
 }
 #endif