Просмотр исходного кода

Various improvements: Toolbox, Updater and Unit Tests. (#2250)

* Toolbox: add seek to character stream method. UpdateUtils: reverse manifest iterator. UnitTests: more unit tests.
* Target: bump API version. Updater: delete empty folders from manifest before resource deployment.
* UnitTests: use manifest from unit_tests folder instead of global one
* Make PVS happy
* sector cache: allocate always
* Better PVS config for manifest.c
* PVS: Move exception outside of condition
* PVS: remove confusing condition

Co-authored-by: SG <who.just.the.doctor@gmail.com>
あく 3 лет назад
Родитель
Сommit
41c43f4805

+ 75 - 0
applications/debug/unit_tests/manifest/manifest.c

@@ -0,0 +1,75 @@
+#include <furi.c>
+#include "../minunit.h"
+#include <update_util/resources/manifest.h>
+
+#define TAG "Manifest"
+
+MU_TEST(manifest_type_test) {
+    mu_assert(ResourceManifestEntryTypeUnknown == 0, "ResourceManifestEntryTypeUnknown != 0\r\n");
+    mu_assert(ResourceManifestEntryTypeVersion == 1, "ResourceManifestEntryTypeVersion != 1\r\n");
+    mu_assert(
+        ResourceManifestEntryTypeTimestamp == 2, "ResourceManifestEntryTypeTimestamp != 2\r\n");
+    mu_assert(
+        ResourceManifestEntryTypeDirectory == 3, "ResourceManifestEntryTypeDirectory != 3\r\n");
+    mu_assert(ResourceManifestEntryTypeFile == 4, "ResourceManifestEntryTypeFile != 4\r\n");
+}
+
+MU_TEST(manifest_iteration_test) {
+    bool result = true;
+    size_t counters[5] = {0};
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    ResourceManifestReader* manifest_reader = resource_manifest_reader_alloc(storage);
+    do {
+        // Open manifest file
+        if(!resource_manifest_reader_open(manifest_reader, EXT_PATH("unit_tests/Manifest"))) {
+            result = false;
+            break;
+        }
+
+        // Iterate forward
+        ResourceManifestEntry* entry_ptr = NULL;
+        while((entry_ptr = resource_manifest_reader_next(manifest_reader))) {
+            FURI_LOG_D(TAG, "F:%u:%s", entry_ptr->type, furi_string_get_cstr(entry_ptr->name));
+            if(entry_ptr->type > 4) {
+                mu_fail("entry_ptr->type > 4\r\n");
+                result = false;
+                break;
+            }
+            counters[entry_ptr->type]++;
+        }
+        if(!result) break;
+
+        // Iterate backward
+        while((entry_ptr = resource_manifest_reader_previous(manifest_reader))) {
+            FURI_LOG_D(TAG, "B:%u:%s", entry_ptr->type, furi_string_get_cstr(entry_ptr->name));
+            if(entry_ptr->type > 4) {
+                mu_fail("entry_ptr->type > 4\r\n");
+                result = false;
+                break;
+            }
+            counters[entry_ptr->type]--;
+        }
+    } while(false);
+
+    resource_manifest_reader_free(manifest_reader);
+    furi_record_close(RECORD_STORAGE);
+
+    mu_assert(counters[ResourceManifestEntryTypeUnknown] == 0, "Unknown counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeVersion] == 0, "Version counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeTimestamp] == 0, "Timestamp counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeDirectory] == 0, "Directory counter != 0\r\n");
+    mu_assert(counters[ResourceManifestEntryTypeFile] == 0, "File counter != 0\r\n");
+
+    mu_assert(result, "Manifest forward iterate failed\r\n");
+}
+
+MU_TEST_SUITE(manifest_suite) {
+    MU_RUN_TEST(manifest_type_test);
+    MU_RUN_TEST(manifest_iteration_test);
+}
+
+int run_minunit_test_manifest() {
+    MU_RUN_SUITE(manifest_suite);
+    return MU_EXIT_CODE;
+}

+ 24 - 0
applications/debug/unit_tests/stream/stream_test.c

@@ -72,8 +72,32 @@ MU_TEST_1(stream_composite_subtest, Stream* stream) {
     mu_check(stream_seek(stream, -3, StreamOffsetFromEnd));
     mu_check(stream_seek(stream, -3, StreamOffsetFromEnd));
     mu_check(stream_tell(stream) == 4);
     mu_check(stream_tell(stream) == 4);
 
 
+    // test seeks to char. content: '1337_69'
+    stream_rewind(stream);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 1);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 2);
+    mu_check(stream_seek_to_char(stream, '_', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 4);
+    mu_check(stream_seek_to_char(stream, '9', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 6);
+    mu_check(!stream_seek_to_char(stream, '9', StreamDirectionForward));
+    mu_check(stream_tell(stream) == 6);
+    mu_check(stream_seek_to_char(stream, '_', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 4);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 2);
+    mu_check(stream_seek_to_char(stream, '3', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 1);
+    mu_check(!stream_seek_to_char(stream, '3', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 1);
+    mu_check(stream_seek_to_char(stream, '1', StreamDirectionBackward));
+    mu_check(stream_tell(stream) == 0);
+
     // write string with replacemet
     // write string with replacemet
     // "1337_69" -> "1337lee"
     // "1337_69" -> "1337lee"
+    mu_check(stream_seek(stream, 4, StreamOffsetFromStart));
     mu_check(stream_write_string(stream, string_lee) == 3);
     mu_check(stream_write_string(stream, string_lee) == 3);
     mu_check(stream_size(stream) == 7);
     mu_check(stream_size(stream) == 7);
     mu_check(stream_tell(stream) == 7);
     mu_check(stream_tell(stream) == 7);

+ 2 - 0
applications/debug/unit_tests/test_index.c

@@ -13,6 +13,7 @@ int run_minunit_test_furi_hal();
 int run_minunit_test_furi_string();
 int run_minunit_test_furi_string();
 int run_minunit_test_infrared();
 int run_minunit_test_infrared();
 int run_minunit_test_rpc();
 int run_minunit_test_rpc();
+int run_minunit_test_manifest();
 int run_minunit_test_flipper_format();
 int run_minunit_test_flipper_format();
 int run_minunit_test_flipper_format_string();
 int run_minunit_test_flipper_format_string();
 int run_minunit_test_stream();
 int run_minunit_test_stream();
@@ -41,6 +42,7 @@ const UnitTest unit_tests[] = {
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "storage", .entry = run_minunit_test_storage},
     {.name = "stream", .entry = run_minunit_test_stream},
     {.name = "stream", .entry = run_minunit_test_stream},
     {.name = "dirwalk", .entry = run_minunit_test_dirwalk},
     {.name = "dirwalk", .entry = run_minunit_test_dirwalk},
+    {.name = "manifest", .entry = run_minunit_test_manifest},
     {.name = "flipper_format", .entry = run_minunit_test_flipper_format},
     {.name = "flipper_format", .entry = run_minunit_test_flipper_format},
     {.name = "flipper_format_string", .entry = run_minunit_test_flipper_format_string},
     {.name = "flipper_format_string", .entry = run_minunit_test_flipper_format_string},
     {.name = "rpc", .entry = run_minunit_test_rpc},
     {.name = "rpc", .entry = run_minunit_test_rpc},

+ 42 - 2
applications/system/updater/util/update_task_worker_backup.c

@@ -79,8 +79,8 @@ static void
                 update_task_set_progress(
                 update_task_set_progress(
                     update_task,
                     update_task,
                     UpdateTaskStageProgress,
                     UpdateTaskStageProgress,
-                    /* For this stage, first 30% of progress = cleanup */
-                    (n_processed_files++ * 30) / (n_approx_file_entries + 1));
+                    /* For this stage, first 20% of progress = cleanup files */
+                    (n_processed_files++ * 20) / (n_approx_file_entries + 1));
 
 
                 FuriString* file_path = furi_string_alloc();
                 FuriString* file_path = furi_string_alloc();
                 path_concat(
                 path_concat(
@@ -90,6 +90,46 @@ static void
                 furi_string_free(file_path);
                 furi_string_free(file_path);
             }
             }
         }
         }
+
+        while((entry_ptr = resource_manifest_reader_previous(manifest_reader))) {
+            if(entry_ptr->type == ResourceManifestEntryTypeDirectory) {
+                update_task_set_progress(
+                    update_task,
+                    UpdateTaskStageProgress,
+                    /* For this stage, second 10% of progress = cleanup directories */
+                    (n_processed_files++ * 10) / (n_approx_file_entries + 1));
+
+                FuriString* folder_path = furi_string_alloc();
+                File* folder_file = storage_file_alloc(update_task->storage);
+
+                do {
+                    path_concat(
+                        STORAGE_EXT_PATH_PREFIX,
+                        furi_string_get_cstr(entry_ptr->name),
+                        folder_path);
+
+                    FURI_LOG_D(TAG, "Removing folder %s", furi_string_get_cstr(folder_path));
+                    if(!storage_dir_open(folder_file, furi_string_get_cstr(folder_path))) {
+                        FURI_LOG_W(
+                            TAG,
+                            "%s can't be opened, skipping",
+                            furi_string_get_cstr(folder_path));
+                        break;
+                    }
+
+                    if(storage_dir_read(folder_file, NULL, NULL, 0)) {
+                        FURI_LOG_I(
+                            TAG, "%s is not empty, skipping", furi_string_get_cstr(folder_path));
+                        break;
+                    }
+
+                    storage_simply_remove(update_task->storage, furi_string_get_cstr(folder_path));
+                } while(false);
+
+                storage_file_free(folder_file);
+                furi_string_free(folder_path);
+            }
+        }
     } while(false);
     } while(false);
     resource_manifest_reader_free(manifest_reader);
     resource_manifest_reader_free(manifest_reader);
 }
 }

+ 76 - 0
assets/unit_tests/Manifest

@@ -0,0 +1,76 @@
+V:0
+T:1672935435
+D:infrared
+D:nfc
+D:subghz
+F:4bff70f2a2ae771f81de5cfb090b3d74:3952:infrared/test_kaseikyo.irtest
+F:8556d32d7c54e66771d9da78d007d379:21463:infrared/test_nec.irtest
+F:860c0c475573878842180a6cb50c85c7:2012:infrared/test_nec42.irtest
+F:2b3cbf3fe7d3642190dfb8362dcc0ed6:3522:infrared/test_nec42ext.irtest
+F:c74bbd7f885ab8fbc3b3363598041bc1:18976:infrared/test_necext.irtest
+F:cab5e604abcb233bcb27903baec24462:7460:infrared/test_rc5.irtest
+F:3d22b3ec2531bb8f4842c9c0c6a8d97c:547:infrared/test_rc5x.irtest
+F:c9cb9fa4decbdd077741acb845f21343:8608:infrared/test_rc6.irtest
+F:97de943385bc6ad1c4a58fc4fedb5244:16975:infrared/test_samsung32.irtest
+F:4eb36c62d4f2e737a3e4a64b5ff0a8e7:41623:infrared/test_sirc.irtest
+F:e4ec3299cbe1f528fb1b9b45aac53556:4182:nfc/nfc_nfca_signal_long.nfc
+F:af4d10974834c2703ad29e859eea78c2:1020:nfc/nfc_nfca_signal_short.nfc
+F:224d12457a26774d8d2aa0d4b3a15652:160:subghz/ansonic.sub
+F:ce9fc98dc01230387a340332316774f1:13642:subghz/ansonic_raw.sub
+F:f958927b656d0804036c28b4a31ff856:157:subghz/bett.sub
+F:b4b17b2603fa3a144dbea4d9ede9f61d:5913:subghz/bett_raw.sub
+F:370a0c62be967b420da5e60ffcdc078b:157:subghz/came.sub
+F:0156915c656d8c038c6d555d34349a36:6877:subghz/came_atomo_raw.sub
+F:111a8b796661f3cbd6f49f756cf91107:8614:subghz/came_raw.sub
+F:2101b0a5a72c87f9dce77223b2885aa7:162:subghz/came_twee.sub
+F:c608b78b8e4646eeb94db37644623254:10924:subghz/came_twee_raw.sub
+F:c4a55acddb68fc3111d592c9292022a8:21703:subghz/cenmax_raw.sub
+F:51d6bd600345954b9c84a5bc6e999313:159:subghz/clemsa.sub
+F:14fa0d5931a32674bfb2ddf288f3842b:21499:subghz/clemsa_raw.sub
+F:f38b6dfa0920199200887b2cd5c0a385:161:subghz/doitrand.sub
+F:c7e53da8e3588a2c0721aa794699ccd4:24292:subghz/doitrand_raw.sub
+F:cc73b6f4d05bfe30c67a0d18b63e58d9:159:subghz/doorhan.sub
+F:22fec89c5cc43504ad4391e61e12c7e0:10457:subghz/doorhan_raw.sub
+F:3a97d8bd32ddaff42932b4c3033ee2d2:12732:subghz/faac_slh_raw.sub
+F:06d3226f5330665f48d41c49e34fed15:159:subghz/gate_tx.sub
+F:8b150a8d38ac7c4f7063ee0d42050399:13827:subghz/gate_tx_raw.sub
+F:a7904e17b0c18c083ae1acbefc330c7a:159:subghz/holtek.sub
+F:72bb528255ef1c135cb3f436414897d3:173:subghz/holtek_ht12x.sub
+F:54ceacb8c156f9534fc7ee0a0911f4da:11380:subghz/holtek_ht12x_raw.sub
+F:4a9567c1543cf3e7bb5350b635d9076f:31238:subghz/holtek_raw.sub
+F:ca86c0d78364d704ff62b0698093d396:162:subghz/honeywell_wdb.sub
+F:f606548c935adc8d8bc804326ef67543:38415:subghz/honeywell_wdb_raw.sub
+F:20bba4b0aec006ced7e82513f9459e31:15532:subghz/hormann_hsm_raw.sub
+F:3392f2db6aa7777e937db619b86203bb:10637:subghz/ido_117_111_raw.sub
+F:cc5c7968527cc233ef11a08986e31bf2:167:subghz/intertechno_v3.sub
+F:70bceb941739260ab9f6162cfdeb0347:18211:subghz/intertechno_v3_raw.sub
+F:bc9a4622f3e22fd7f82eb3f26e61f59b:44952:subghz/kia_seed_raw.sub
+F:6b6e95fc70ea481dc6184d291466d16a:159:subghz/linear.sub
+F:77aaa9005db54c0357451ced081857b2:14619:subghz/linear_raw.sub
+F:1a618e21e6ffa9984d465012e704c450:161:subghz/magellan.sub
+F:bf43cb85d79e20644323d6acad87e028:5808:subghz/magellan_raw.sub
+F:4ef17320f936ee88e92582a9308b2faa:161:subghz/marantec.sub
+F:507a8413a1603ad348eea945123fb7cc:21155:subghz/marantec_raw.sub
+F:22b69dc490d5425488342b5c5a838d55:161:subghz/megacode.sub
+F:4f8fe9bef8bdd9c52f3f77e829f8986f:6205:subghz/megacode_raw.sub
+F:b39f62cb108c2fa9916e0a466596ab87:18655:subghz/nero_radio_raw.sub
+F:d0d70f8183032096805a41e1808c093b:26436:subghz/nero_sketch_raw.sub
+F:c6999bd0eefd0fccf34820e17bcbc8ba:161:subghz/nice_flo.sub
+F:9b1200600b9ec2a73166797ff243fbfc:3375:subghz/nice_flo_raw.sub
+F:b52bafb098282676d1c7163bfb0d6e73:8773:subghz/nice_flor_s_raw.sub
+F:e4df94dfdee2efadf2ed9a1e9664f8b2:163:subghz/phoenix_v2.sub
+F:8ec066976df93fba6335b3f6dc47014c:8548:subghz/phoenix_v2_raw.sub
+F:2b1192e4898aaf274caebbb493b9f96e:164:subghz/power_smart.sub
+F:8b8195cab1d9022fe38e802383fb923a:3648:subghz/power_smart_raw.sub
+F:1ccf1289533e0486a1d010d934ad7b06:170:subghz/princeton.sub
+F:8bccc506a61705ec429aecb879e5d7ce:7344:subghz/princeton_raw.sub
+F:0bda91d783e464165190c3b3d16666a7:38724:subghz/scher_khan_magic_code.sub
+F:116d7e1a532a0c9e00ffeee105f7138b:166:subghz/security_pls_1_0.sub
+F:441fc7fc6fa11ce0068fde3f6145177b:69413:subghz/security_pls_1_0_raw.sub
+F:e5e33c24c5e55f592ca892b5aa8fa31f:208:subghz/security_pls_2_0.sub
+F:2614f0aef367042f8623719d765bf2c0:62287:subghz/security_pls_2_0_raw.sub
+F:8eb533544c4c02986800c90e935184ff:168:subghz/smc5326.sub
+F:fc67a4fe7e0b3bc81a1c8da8caca7658:4750:subghz/smc5326_raw.sub
+F:24196a4c4af1eb03404a2ee434c864bf:4096:subghz/somfy_keytis_raw.sub
+F:6a5ece145a5694e543d99bf1b970baf0:9741:subghz/somfy_telis_raw.sub
+F:0ad046bfa9ec872e92141a69bbf03d92:382605:subghz/test_random_raw.sub

+ 2 - 1
firmware/targets/f7/api_symbols.csv

@@ -1,5 +1,5 @@
 entry,status,name,type,params
 entry,status,name,type,params
-Version,+,11.5,,
+Version,+,11.6,,
 Header,+,applications/services/bt/bt_service/bt.h,,
 Header,+,applications/services/bt/bt_service/bt.h,,
 Header,+,applications/services/cli/cli.h,,
 Header,+,applications/services/cli/cli.h,,
 Header,+,applications/services/cli/cli_vcp.h,,
 Header,+,applications/services/cli/cli_vcp.h,,
@@ -2480,6 +2480,7 @@ Function,+,stream_read_line,_Bool,"Stream*, FuriString*"
 Function,+,stream_rewind,_Bool,Stream*
 Function,+,stream_rewind,_Bool,Stream*
 Function,+,stream_save_to_file,size_t,"Stream*, Storage*, const char*, FS_OpenMode"
 Function,+,stream_save_to_file,size_t,"Stream*, Storage*, const char*, FS_OpenMode"
 Function,+,stream_seek,_Bool,"Stream*, int32_t, StreamOffset"
 Function,+,stream_seek,_Bool,"Stream*, int32_t, StreamOffset"
+Function,+,stream_seek_to_char,_Bool,"Stream*, char, StreamDirection"
 Function,+,stream_size,size_t,Stream*
 Function,+,stream_size,size_t,Stream*
 Function,+,stream_split,_Bool,"Stream*, Stream*, Stream*"
 Function,+,stream_split,_Bool,"Stream*, Stream*, Stream*"
 Function,+,stream_tell,size_t,Stream*
 Function,+,stream_tell,size_t,Stream*

+ 5 - 3
firmware/targets/f7/fatfs/sector_cache.c

@@ -8,6 +8,7 @@
 
 
 #define SECTOR_SIZE 512
 #define SECTOR_SIZE 512
 #define N_SECTORS 8
 #define N_SECTORS 8
+#define TAG "SDCache"
 
 
 typedef struct {
 typedef struct {
     uint32_t itr;
     uint32_t itr;
@@ -19,14 +20,15 @@ static SectorCache* cache = NULL;
 
 
 void sector_cache_init() {
 void sector_cache_init() {
     if(cache == NULL) {
     if(cache == NULL) {
-        cache = furi_hal_memory_alloc(sizeof(SectorCache));
+        // TODO: tuneup allocation order, to place cache in mem pool (MEM2)
+        cache = memmgr_alloc_from_pool(sizeof(SectorCache));
     }
     }
 
 
     if(cache != NULL) {
     if(cache != NULL) {
-        FURI_LOG_I("SectorCache", "Initializing sector cache");
+        FURI_LOG_I(TAG, "Init");
         memset(cache, 0, sizeof(SectorCache));
         memset(cache, 0, sizeof(SectorCache));
     } else {
     } else {
-        FURI_LOG_E("SectorCache", "Cannot enable sector cache");
+        FURI_LOG_E(TAG, "Init failed");
     }
     }
 }
 }
 
 

+ 80 - 3
lib/toolbox/stream/stream.c

@@ -4,6 +4,8 @@
 #include <core/check.h>
 #include <core/check.h>
 #include <core/common_defines.h>
 #include <core/common_defines.h>
 
 
+#define STREAM_BUFFER_SIZE (32U)
+
 void stream_free(Stream* stream) {
 void stream_free(Stream* stream) {
     furi_assert(stream);
     furi_assert(stream);
     stream->vtable->free(stream);
     stream->vtable->free(stream);
@@ -24,6 +26,82 @@ bool stream_seek(Stream* stream, int32_t offset, StreamOffset offset_type) {
     return stream->vtable->seek(stream, offset, offset_type);
     return stream->vtable->seek(stream, offset, offset_type);
 }
 }
 
 
+static bool stream_seek_to_char_forward(Stream* stream, char c) {
+    // Search is starting from seconds character
+    if(!stream_seek(stream, 1, StreamOffsetFromCurrent)) {
+        return false;
+    }
+
+    // Search character in a stream
+    bool result = false;
+    while(!result) {
+        uint8_t buffer[STREAM_BUFFER_SIZE] = {0};
+        size_t ret = stream_read(stream, buffer, STREAM_BUFFER_SIZE);
+        for(size_t i = 0; i < ret; i++) {
+            if(buffer[i] == c) {
+                stream_seek(stream, (int32_t)i - ret, StreamOffsetFromCurrent);
+                result = true;
+                break;
+            }
+        }
+        if(ret != STREAM_BUFFER_SIZE) break;
+    }
+    return result;
+}
+
+static bool stream_seek_to_char_backward(Stream* stream, char c) {
+    size_t anchor = stream_tell(stream);
+
+    // Special case, no previous characters
+    if(anchor == 0) {
+        return false;
+    }
+
+    bool result = false;
+    while(!result) {
+        // Seek back
+        uint8_t buffer[STREAM_BUFFER_SIZE] = {0};
+        size_t to_read = STREAM_BUFFER_SIZE;
+        if(to_read > anchor) {
+            to_read = anchor;
+        }
+
+        anchor -= to_read;
+        furi_check(stream_seek(stream, anchor, StreamOffsetFromStart));
+
+        size_t ret = stream_read(stream, buffer, to_read);
+        for(size_t i = 0; i < ret; i++) {
+            size_t cursor = ret - i - 1;
+            if(buffer[cursor] == c) {
+                result = true;
+                furi_check(stream_seek(stream, anchor + cursor, StreamOffsetFromStart));
+                break;
+            } else {
+            }
+        }
+        if(ret != STREAM_BUFFER_SIZE) break;
+    }
+    return result;
+}
+
+bool stream_seek_to_char(Stream* stream, char c, StreamDirection direction) {
+    const size_t old_position = stream_tell(stream);
+
+    bool result = false;
+    if(direction == StreamDirectionForward) {
+        result = stream_seek_to_char_forward(stream, c);
+    } else if(direction == StreamDirectionBackward) {
+        result = stream_seek_to_char_backward(stream, c);
+    }
+
+    // Rollback
+    if(!result) {
+        stream_seek(stream, old_position, StreamOffsetFromStart);
+    }
+
+    return result;
+}
+
 size_t stream_tell(Stream* stream) {
 size_t stream_tell(Stream* stream) {
     furi_assert(stream);
     furi_assert(stream);
     return stream->vtable->tell(stream);
     return stream->vtable->tell(stream);
@@ -69,11 +147,10 @@ static bool stream_write_struct(Stream* stream, const void* context) {
 
 
 bool stream_read_line(Stream* stream, FuriString* str_result) {
 bool stream_read_line(Stream* stream, FuriString* str_result) {
     furi_string_reset(str_result);
     furi_string_reset(str_result);
-    const uint8_t buffer_size = 32;
-    uint8_t buffer[buffer_size];
+    uint8_t buffer[STREAM_BUFFER_SIZE];
 
 
     do {
     do {
-        uint16_t bytes_were_read = stream_read(stream, buffer, buffer_size);
+        uint16_t bytes_were_read = stream_read(stream, buffer, STREAM_BUFFER_SIZE);
         if(bytes_were_read == 0) break;
         if(bytes_were_read == 0) break;
 
 
         bool result = false;
         bool result = false;

+ 27 - 12
lib/toolbox/stream/stream.h

@@ -16,6 +16,11 @@ typedef enum {
     StreamOffsetFromEnd,
     StreamOffsetFromEnd,
 } StreamOffset;
 } StreamOffset;
 
 
+typedef enum {
+    StreamDirectionForward,
+    StreamDirectionBackward,
+} StreamDirection;
+
 typedef bool (*StreamWriteCB)(Stream* stream, const void* context);
 typedef bool (*StreamWriteCB)(Stream* stream, const void* context);
 
 
 /**
 /**
@@ -31,15 +36,15 @@ void stream_free(Stream* stream);
 void stream_clean(Stream* stream);
 void stream_clean(Stream* stream);
 
 
 /**
 /**
- * Indicates that the rw pointer is at the end of the stream
+ * Indicates that the RW pointer is at the end of the stream
  * @param stream Stream instance
  * @param stream Stream instance
- * @return true if rw pointer is at the end of the stream
- * @return false if rw pointer is not at the end of the stream
+ * @return true if RW pointer is at the end of the stream
+ * @return false if RW pointer is not at the end of the stream
  */
  */
 bool stream_eof(Stream* stream);
 bool stream_eof(Stream* stream);
 
 
 /**
 /**
- * Moves the rw pointer.
+ * Moves the RW pointer.
  * @param stream Stream instance
  * @param stream Stream instance
  * @param offset how much to move the pointer
  * @param offset how much to move the pointer
  * @param offset_type starting from what
  * @param offset_type starting from what
@@ -48,10 +53,20 @@ bool stream_eof(Stream* stream);
  */
  */
 bool stream_seek(Stream* stream, int32_t offset, StreamOffset offset_type);
 bool stream_seek(Stream* stream, int32_t offset, StreamOffset offset_type);
 
 
+/** Seek to next occurrence of the character
+ *
+ * @param      stream     Pointer to the stream instance
+ * @param[in]  c          The Character
+ * @param[in]  direction  The Direction
+ *
+ * @return     true on success
+ */
+bool stream_seek_to_char(Stream* stream, char c, StreamDirection direction);
+
 /**
 /**
- * Gets the value of the rw pointer
+ * Gets the value of the RW pointer
  * @param stream Stream instance
  * @param stream Stream instance
- * @return size_t value of the rw pointer
+ * @return size_t value of the RW pointer
  */
  */
 size_t stream_tell(Stream* stream);
 size_t stream_tell(Stream* stream);
 
 
@@ -101,13 +116,13 @@ bool stream_delete_and_insert(
  * Read line from a stream (supports LF and CRLF line endings)
  * Read line from a stream (supports LF and CRLF line endings)
  * @param stream 
  * @param stream 
  * @param str_result 
  * @param str_result 
- * @return true if line lenght is not zero
+ * @return true if line length is not zero
  * @return false otherwise
  * @return false otherwise
  */
  */
 bool stream_read_line(Stream* stream, FuriString* str_result);
 bool stream_read_line(Stream* stream, FuriString* str_result);
 
 
 /**
 /**
- * Moves the rw pointer to the start
+ * Moves the RW pointer to the start
  * @param stream Stream instance
  * @param stream Stream instance
  */
  */
 bool stream_rewind(Stream* stream);
 bool stream_rewind(Stream* stream);
@@ -157,7 +172,7 @@ size_t stream_write_vaformat(Stream* stream, const char* format, va_list args);
 
 
 /**
 /**
  * Insert N chars to the stream, starting at the current pointer.
  * Insert N chars to the stream, starting at the current pointer.
- * Data will be inserted, not overwritteт, so the stream will be increased in size.
+ * Data will be inserted, not overwritten, so the stream will be increased in size.
  * @param stream Stream instance
  * @param stream Stream instance
  * @param data data to be inserted
  * @param data data to be inserted
  * @param size size of data to be inserted
  * @param size size of data to be inserted
@@ -273,7 +288,7 @@ bool stream_delete_and_insert_vaformat(
 
 
 /**
 /**
  * Remove N chars from the stream, starting at the current pointer.
  * Remove N chars from the stream, starting at the current pointer.
- * The size may be larger than stream size, the stream will be cleared from current rw pointer to the end.
+ * The size may be larger than stream size, the stream will be cleared from current RW pointer to the end.
  * @param stream Stream instance
  * @param stream Stream instance
  * @param size how many chars need to be deleted
  * @param size how many chars need to be deleted
  * @return true if the operation was successful
  * @return true if the operation was successful
@@ -282,7 +297,7 @@ bool stream_delete_and_insert_vaformat(
 bool stream_delete(Stream* stream, size_t size);
 bool stream_delete(Stream* stream, size_t size);
 
 
 /**
 /**
- * Copy data from one stream to another. Data will be copied from current rw pointer and to current rw pointer.
+ * Copy data from one stream to another. Data will be copied from current RW pointer and to current RW pointer.
  * @param stream_from 
  * @param stream_from 
  * @param stream_to 
  * @param stream_to 
  * @param size 
  * @param size 
@@ -328,7 +343,7 @@ size_t stream_load_from_file(Stream* stream, Storage* storage, const char* path)
 size_t stream_save_to_file(Stream* stream, Storage* storage, const char* path, FS_OpenMode mode);
 size_t stream_save_to_file(Stream* stream, Storage* storage, const char* path, FS_OpenMode mode);
 
 
 /**
 /**
- * Dump stream inner data (size, RW positiot, content)
+ * Dump stream inner data (size, RW position, content)
  * @param stream Stream instance 
  * @param stream Stream instance 
  */
  */
 void stream_dump_data(Stream* stream);
 void stream_dump_data(Stream* stream);

+ 51 - 3
lib/update_util/resources/manifest.c

@@ -60,6 +60,12 @@ ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* res
 
 
         char type_code = furi_string_get_char(resource_manifest->linebuf, 0);
         char type_code = furi_string_get_char(resource_manifest->linebuf, 0);
         switch(type_code) {
         switch(type_code) {
+        case 'V':
+            resource_manifest->entry.type = ResourceManifestEntryTypeVersion;
+            break;
+        case 'T':
+            resource_manifest->entry.type = ResourceManifestEntryTypeTimestamp;
+            break;
         case 'F':
         case 'F':
             resource_manifest->entry.type = ResourceManifestEntryTypeFile;
             resource_manifest->entry.type = ResourceManifestEntryTypeFile;
             break;
             break;
@@ -98,9 +104,9 @@ ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* res
             furi_string_right(resource_manifest->linebuf, offs + 1);
             furi_string_right(resource_manifest->linebuf, offs + 1);
 
 
             furi_string_set(resource_manifest->entry.name, resource_manifest->linebuf);
             furi_string_set(resource_manifest->entry.name, resource_manifest->linebuf);
-        } else if(resource_manifest->entry.type == ResourceManifestEntryTypeDirectory) { //-V547
-            /* Parse directory entry
-               D:<name> */
+        } else { //-V547
+            /* Everything else is plain key value. Parse version, timestamp or directory entry
+               <Type>:<Value> */
 
 
             /* Remove entry type code */
             /* Remove entry type code */
             furi_string_right(resource_manifest->linebuf, 2);
             furi_string_right(resource_manifest->linebuf, 2);
@@ -113,3 +119,45 @@ ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* res
 
 
     return NULL;
     return NULL;
 }
 }
+
+ResourceManifestEntry*
+    resource_manifest_reader_previous(ResourceManifestReader* resource_manifest) {
+    furi_assert(resource_manifest);
+
+    // Snapshot position for rollback
+    const size_t previous_position = stream_tell(resource_manifest->stream);
+
+    // We need to jump 2 lines back
+    size_t jumps = 2;
+    // Special case: end of the file.
+    const bool was_eof = stream_eof(resource_manifest->stream);
+    if(was_eof) {
+        jumps = 1;
+    }
+    while(jumps) {
+        if(!stream_seek_to_char(resource_manifest->stream, '\n', StreamDirectionBackward)) {
+            break;
+        }
+        if(stream_tell(resource_manifest->stream) < (previous_position - 1)) {
+            jumps--;
+        }
+    }
+
+    // Special case: first line. Force seek to zero
+    if(jumps == 1) {
+        jumps = 0;
+        stream_seek(resource_manifest->stream, 0, StreamOffsetFromStart);
+    }
+
+    if(jumps == 0) {
+        ResourceManifestEntry* entry = resource_manifest_reader_next(resource_manifest);
+        // Special case: was end of the file, prevent loop
+        if(was_eof) {
+            stream_seek(resource_manifest->stream, -1, StreamOffsetFromCurrent);
+        }
+        return entry;
+    } else {
+        stream_seek(resource_manifest->stream, previous_position, StreamOffsetFromStart);
+        return NULL;
+    }
+}

+ 14 - 0
lib/update_util/resources/manifest.h

@@ -11,6 +11,8 @@ extern "C" {
 
 
 typedef enum {
 typedef enum {
     ResourceManifestEntryTypeUnknown = 0,
     ResourceManifestEntryTypeUnknown = 0,
+    ResourceManifestEntryTypeVersion,
+    ResourceManifestEntryTypeTimestamp,
     ResourceManifestEntryTypeDirectory,
     ResourceManifestEntryTypeDirectory,
     ResourceManifestEntryTypeFile,
     ResourceManifestEntryTypeFile,
 } ResourceManifestEntryType;
 } ResourceManifestEntryType;
@@ -52,6 +54,18 @@ bool resource_manifest_reader_open(ResourceManifestReader* resource_manifest, co
  */
  */
 ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* resource_manifest);
 ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* resource_manifest);
 
 
+/** Read previous file/dir entry from manifest
+ *
+ * You must be at the end of the manifest to use this function.
+ * Intended to be used after reaching end with resource_manifest_reader_next
+ *
+ * @param      resource_manifest  Pointer to the ResourceManifestReader instance
+ *
+ * @return     entry or NULL if end of file
+ */
+ResourceManifestEntry*
+    resource_manifest_reader_previous(ResourceManifestReader* resource_manifest);
+
 #ifdef __cplusplus
 #ifdef __cplusplus
 } // extern "C"
 } // extern "C"
 #endif
 #endif