Files
Mykhailo ShevchukandClaude Opus 5 5629fc5e17 SubGHz + Storage: share more duplicated bodies (-4,892 B of flash) (#1116)
* SubGhz: share the remaining duplicated serialize handlers

Follow-up to af2370573. Two families of decoder serialize handlers were still
copied per protocol: the one writing generic.data_2 as "Data" (beninca_arc,
jarolift, kinggates_stylo_4k) and the one writing TE (holtek_th12x, legrand,
smc5326). Both now live in common.c and are pointed at straight from the
vtable, so the per-protocol functions and their declarations are gone.

The data_2 block also existed three more times in the encoder create_data
paths of those same files; they share the helper too. princeton is that TE
handler plus a Guard_time write, so it calls the helper and adds its own
field. honeywell had generic and decoder in the reverse order, which was the
only reason af2370573 could not fold six of its handlers - swapping the two
members folds them.

The helpers log through generic.protocol_name instead of each file's TAG, so
they match the vtable slot signature and need no per-protocol wrapper. Two
diagnostics-only consequences: converted paths log under the protocol's
user-facing name rather than TAG, and the encoder create_data failure message
merges with the serialize one. Nothing in the tree filters on log tags.

SUBGHZ_ASSERT_*_LAYOUT pins the punned prefixes at compile time. Pinning the
tail offset alone would not have caught honeywell's swap: decoder and generic
are both multiples of 8, so te lands at the same offset either way. Verified
by restoring the old order and watching the build fail.

.text 696,740 -> 695,828 (-912 B), .rodata unchanged, api_symbols.csv untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: share the protocol alloc bodies

48 decoder and 48 encoder alloc functions differed only in the struct type,
the protocol descriptor and, for encoders, the repeat and upload-buffer
constants. They now call subghz_protocol_{decoder,encoder}_common_alloc and
keep a thunk each - the vtable slot passes only SubGhzEnvironment*, so the
per-protocol constants have to come from somewhere.

Keeping the thunk rather than making alloc data-driven is deliberate. Hanging
instance_size/repeat/size_upload off SubGhzProtocol and letting alloc be NULL
would remove all 96 thunks, but it moves the same constants into .rodata at
about 12 B per protocol across the whole registry, adds a dispatch branch, and
changes a struct that is FAP ABI (raw.h is an exported SDK header and includes
base.h). Net saving is roughly zero.

SubGhzProtocolEncoderCommon stays {base, encoder} so protocols that only use
free/stop/yield are not forced to carry a generic; the alloc helper puns
through a nested SubGhzProtocolEncoderCommonGeneric instead, mirroring how
SubGhzProtocolDecoderCommonTe extends the decoder type.

SUBGHZ_ASSERT_*_LAYOUT now covers all 114 structs any shared helper puns
through, not just the ones converted here - 14 of those were already being
punned by free/stop/yield/reset/serialize with no guard at all. The asserts
cost no flash.

23 allocs keep their own body for doing real extra work (keystore setup,
rainbow-table names, extra buffers). Most could call the helper and then add
their own lines the way princeton does for serialize, worth roughly 440 B;
left for a follow-up so this commit stays mechanical.

.text 695,828 -> 693,396 (-2,432 B), .rodata unchanged, api_symbols.csv untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: share the deserialize bodies that only differ by a callback

Two more shared helpers in common.c:
- subghz_protocol_encoder_common_deserialize(ctx, ff, min_count_bit, get_upload)
  covers 11 encoders whose bodies were identical apart from the bit count and
  their get_upload
- subghz_protocol_decoder_common_deserialize_te(ctx, ff, min_count_bit) covers
  the three decoders that read TE back, mirroring the serialize_te helper

The per-protocol get_upload functions now take void* instead of being cast to
the callback type. Calling through an incompatible function pointer type is
formally undefined even though it is harmless on AAPCS, and enabling LTO is on
the roadmap for this same effort - not a combination worth keeping.

Deliberately not converted:

The 35 decoders whose deserialize is just a tail call to
subghz_block_generic_deserialize_check_count_bit compile to 8 bytes each. A
decoder thunk is 6 bytes, so folding them would gain about 70 B - real, but a
poor return for touching 35 files and adding a second hop to a call that is
already a tail call into shared code.

The six encoders that call a per-protocol check_remote_controller before
get_upload would need a fifth argument, which spills to the stack on AAPCS and
costs more per thunk than it saves. They can instead be folded into the helper
above by giving their get_upload a bool return and moving the check inside it,
which also reaches allstar_firefly/elplast and, once the shared stop resets
encoder.front, came_twee/marantec/power_smart/hormann/magellan - about 390 B
across 13 protocols with no new helper. Left as a follow-up because it changes
those functions rather than just moving them.

.text 693,396 -> 692,900 (-496 B), .rodata unchanged, api_symbols.csv untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Storage: share the command-dispatch bodies

Twelve public storage calls had the same S_API macro body and differed only in
the StorageCommand they sent. Four static helpers now carry it:

  storage_file_command_bool    file_truncate, file_sync, file_eof, dir_rewind
  storage_file_command_uint64  file_tell, file_size
  storage_sd_command           sd_format, sd_unmount, sd_mount, sd_status
  storage_path_command         common_remove, common_mkdir

Split by return macro on purpose - S_RETURN_BOOL, S_RETURN_UINT64 and
S_RETURN_ERROR read different members of the SAReturn union, so one helper for
all twelve would read the wrong member. Each pairing was checked against what
storage_processing.c actually writes.

The public symbols stay as one-line thunks: all twelve are exported in
api_symbols.csv, so the bodies could move but the symbols could not.

file_close and dir_close share the shape but do work between the epilogue and
the return, so they keep their own bodies.

.text 692,900 -> 692,296 (-604 B), .rodata unchanged, api_symbols.csv untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: fold the allocs and deserializes the earlier reviews left behind

Follow-up to 84aca75eb and 5f4f4cc2b, both of which deliberately stopped at the
mechanical cases and listed what they skipped.

15 more allocs now call the shared helpers and keep only their own extra work -
keystore lookups, rainbow-table names, extra buffers: alutech_at_4n, beninca_arc,
bin_raw, dickert_mahs, faac_slh, jarolift, keeloq, kinggates_stylo_4k,
nice_flor_s, telcoma_edge.

8 more encoders join subghz_protocol_encoder_common_deserialize. Their
get_upload had no failure path and returned void, so it becomes
bool/void* and returns true; five of them also move their
check_remote_controller call from deserialize into the top of get_upload,
which needs a forward declaration since get_upload is defined above it.

min_count_bit widens from uint8_t to uint16_t in both deserialize helpers.
subghz_block_generic_deserialize_check_count_bit compares it against a
uint16_t, so the narrower parameter would have silently truncated any protocol
above 255 bits. None exists today; the tree's largest is 128.

The get_upload doc says "true Always; this encoder has no failure path" rather
than "on success". These eight have no bound check, unlike the eleven converted
in 5f4f4cc2b, and claiming success would imply a validation none of them do.
They cannot overrun today - deserialize_check_count_bit pins data_count_bit by
equality before get_upload runs - but keyfinder has only 4 spare entries in a
60-entry buffer, so adding the guards is worth its own commit.

Not done here, deliberately: the five moved check_remote_controller calls write
only serial/btn/cnt on the encoder instance, which nothing reads, so they could
be deleted outright rather than moved. Deleting a store is a behaviour change
and belongs with the encoder.front reset in its own commit.

.text 692,296 -> 691,848 (-448 B), .rodata unchanged, api_symbols.csv untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* upd changelog

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:31:16 +03:00

1042 lines
28 KiB
C

#include <core/log.h>
#include <core/record.h>
#include "storage.h"
#include "storage_i.h" // IWYU pragma: keep
#include "storage_message.h"
#include <toolbox/stream/file_stream.h>
#include <toolbox/dir_walk.h>
#include "toolbox/path.h"
#define MAX_NAME_LENGTH 256
#define MAX_EXT_LEN 16
#define FILE_BUFFER_SIZE 512
#define TAG "StorageApi"
#define S_API_PROLOGUE FuriApiLock lock = api_lock_alloc_locked();
#define S_FILE_API_PROLOGUE \
furi_check(file); \
Storage* storage = file->storage; \
furi_check(storage);
#define S_API_EPILOGUE \
furi_check( \
furi_message_queue_put(storage->message_queue, &message, FuriWaitForever) == \
FuriStatusOk); \
api_lock_wait_unlock_and_free(lock)
#define S_API_MESSAGE(_command) \
SAReturn return_data; \
StorageMessage message = { \
.lock = lock, \
.command = _command, \
.data = &data, \
.return_data = &return_data, \
};
#define S_API_DATA_FILE \
SAData data = { \
.file = { \
.file = file, \
}};
#define S_RETURN_BOOL (return_data.bool_value);
#define S_RETURN_UINT16 (return_data.uint16_value);
#define S_RETURN_UINT64 (return_data.uint64_value);
#define S_RETURN_ERROR (return_data.error_value);
#define S_RETURN_CSTRING (return_data.cstring_value);
typedef enum {
StorageEventFlagFileClose = (1 << 0),
} StorageEventFlag;
/****************** SHARED DISPATCH ******************/
// These four carry the whole body of a dozen API calls that differ only in the command.
static bool storage_file_command_bool(File* file, StorageCommand command) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
S_API_DATA_FILE;
S_API_MESSAGE(command);
S_API_EPILOGUE;
return S_RETURN_BOOL;
}
static uint64_t storage_file_command_uint64(File* file, StorageCommand command) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
S_API_DATA_FILE;
S_API_MESSAGE(command);
S_API_EPILOGUE;
return S_RETURN_UINT64;
}
static FS_Error storage_path_command(Storage* storage, const char* path, StorageCommand command) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.path = {
.path = path,
.thread_id = furi_thread_get_current_id(),
}};
S_API_MESSAGE(command);
S_API_EPILOGUE;
return S_RETURN_ERROR;
}
static FS_Error storage_sd_command(Storage* storage, StorageCommand command) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {};
S_API_MESSAGE(command);
S_API_EPILOGUE;
return S_RETURN_ERROR;
}
/****************** FILE ******************/
static bool storage_file_open_internal(
File* file,
const char* path,
FS_AccessMode access_mode,
FS_OpenMode open_mode) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
SAData data = {
.fopen = {
.file = file,
.path = path,
.access_mode = access_mode,
.open_mode = open_mode,
.thread_id = furi_thread_get_current_id(),
}};
file->type = FileTypeOpenFile;
S_API_MESSAGE(StorageCommandFileOpen);
S_API_EPILOGUE;
return S_RETURN_BOOL;
}
static void storage_file_close_callback(const void* message, void* context) {
const StorageEvent* storage_event = message;
if(storage_event->type == StorageEventTypeFileClose ||
storage_event->type == StorageEventTypeDirClose) {
furi_assert(context);
FuriEventFlag* event = context;
furi_event_flag_set(event, StorageEventFlagFileClose);
}
}
bool storage_file_open(
File* file,
const char* path,
FS_AccessMode access_mode,
FS_OpenMode open_mode) {
furi_check(file);
bool result;
FuriEventFlag* event = furi_event_flag_alloc();
FuriPubSubSubscription* subscription = furi_pubsub_subscribe(
storage_get_pubsub(file->storage), storage_file_close_callback, event);
do {
result = storage_file_open_internal(file, path, access_mode, open_mode);
if(!result && file->error_id == FSE_ALREADY_OPEN) {
furi_event_flag_wait(
event, StorageEventFlagFileClose, FuriFlagWaitAny, FuriWaitForever);
} else {
break;
}
} while(true);
furi_pubsub_unsubscribe(storage_get_pubsub(file->storage), subscription);
furi_event_flag_free(event);
FURI_LOG_T(
TAG,
"File %p - %p open (%s)",
(void*)((uint32_t)file - SRAM_BASE),
(void*)(file->file_id - SRAM_BASE),
path);
return result;
}
bool storage_file_close(File* file) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
S_API_DATA_FILE;
S_API_MESSAGE(StorageCommandFileClose);
S_API_EPILOGUE;
FURI_LOG_T(
TAG,
"File %p - %p closed",
(void*)((uint32_t)file - SRAM_BASE),
(void*)(file->file_id - SRAM_BASE));
file->type = FileTypeClosed;
return S_RETURN_BOOL;
}
static uint16_t storage_file_read_underlying(File* file, void* buff, uint16_t bytes_to_read) {
if(bytes_to_read == 0) {
return 0;
}
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
SAData data = {
.fread = {
.file = file,
.buff = buff,
.bytes_to_read = bytes_to_read,
}};
S_API_MESSAGE(StorageCommandFileRead);
S_API_EPILOGUE;
return S_RETURN_UINT16;
}
static uint16_t
storage_file_write_underlying(File* file, const void* buff, uint16_t bytes_to_write) {
if(bytes_to_write == 0) {
return 0;
}
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
SAData data = {
.fwrite = {
.file = file,
.buff = buff,
.bytes_to_write = bytes_to_write,
}};
S_API_MESSAGE(StorageCommandFileWrite);
S_API_EPILOGUE;
return S_RETURN_UINT16;
}
size_t storage_file_read(File* file, void* buff, size_t to_read) {
size_t total = 0;
const size_t max_chunk = UINT16_MAX;
do {
const size_t chunk = MIN((to_read - total), max_chunk);
size_t read = storage_file_read_underlying(file, buff + total, chunk);
total += read;
if(storage_file_get_error(file) != FSE_OK || read != chunk) {
break;
}
} while(total != to_read);
return total;
}
size_t storage_file_write(File* file, const void* buff, size_t to_write) {
furi_check(file);
size_t total = 0;
const size_t max_chunk = UINT16_MAX;
do {
const size_t chunk = MIN((to_write - total), max_chunk);
size_t written = storage_file_write_underlying(file, buff + total, chunk);
total += written;
if(storage_file_get_error(file) != FSE_OK || written != chunk) {
break;
}
} while(total != to_write);
return total;
}
bool storage_file_seek(File* file, uint32_t offset, bool from_start) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
SAData data = {
.fseek = {
.file = file,
.offset = offset,
.from_start = from_start,
}};
S_API_MESSAGE(StorageCommandFileSeek);
S_API_EPILOGUE;
return S_RETURN_BOOL;
}
uint64_t storage_file_tell(File* file) {
return storage_file_command_uint64(file, StorageCommandFileTell);
}
bool storage_file_truncate(File* file) {
return storage_file_command_bool(file, StorageCommandFileTruncate);
}
uint64_t storage_file_size(File* file) {
return storage_file_command_uint64(file, StorageCommandFileSize);
}
bool storage_file_sync(File* file) {
return storage_file_command_bool(file, StorageCommandFileSync);
}
bool storage_file_eof(File* file) {
return storage_file_command_bool(file, StorageCommandFileEof);
}
bool storage_file_exists(Storage* storage, const char* path) {
furi_check(storage);
bool exist = false;
FileInfo fileinfo;
FS_Error error = storage_common_stat(storage, path, &fileinfo);
if(error == FSE_OK && !file_info_is_dir(&fileinfo)) {
exist = true;
}
return exist;
}
bool storage_file_copy_to_file(File* source, File* destination, size_t size) {
furi_check(source);
furi_check(destination);
uint8_t* buffer = malloc(FILE_BUFFER_SIZE);
while(size) {
uint32_t read_size = size > FILE_BUFFER_SIZE ? FILE_BUFFER_SIZE : size;
if(storage_file_read(source, buffer, read_size) != read_size) {
break;
}
if(storage_file_write(destination, buffer, read_size) != read_size) {
break;
}
size -= read_size;
}
free(buffer);
return size == 0;
}
/****************** DIR ******************/
static bool storage_dir_open_internal(File* file, const char* path) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
SAData data = {
.dopen = {
.file = file,
.path = path,
.thread_id = furi_thread_get_current_id(),
}};
file->type = FileTypeOpenDir;
S_API_MESSAGE(StorageCommandDirOpen);
S_API_EPILOGUE;
return S_RETURN_BOOL;
}
bool storage_dir_open(File* file, const char* path) {
furi_check(file);
bool result;
FuriEventFlag* event = furi_event_flag_alloc();
FuriPubSubSubscription* subscription = furi_pubsub_subscribe(
storage_get_pubsub(file->storage), storage_file_close_callback, event);
do {
result = storage_dir_open_internal(file, path);
if(!result && file->error_id == FSE_ALREADY_OPEN) {
furi_event_flag_wait(
event, StorageEventFlagFileClose, FuriFlagWaitAny, FuriWaitForever);
} else {
break;
}
} while(true);
furi_pubsub_unsubscribe(storage_get_pubsub(file->storage), subscription);
furi_event_flag_free(event);
FURI_LOG_T(
TAG,
"Dir %p - %p open (%s)",
(void*)((uint32_t)file - SRAM_BASE),
(void*)(file->file_id - SRAM_BASE),
path);
return result;
}
bool storage_dir_close(File* file) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
S_API_DATA_FILE;
S_API_MESSAGE(StorageCommandDirClose);
S_API_EPILOGUE;
FURI_LOG_T(
TAG,
"Dir %p - %p closed",
(void*)((uint32_t)file - SRAM_BASE),
(void*)(file->file_id - SRAM_BASE));
file->type = FileTypeClosed;
return S_RETURN_BOOL;
}
bool storage_dir_read(File* file, FileInfo* fileinfo, char* name, uint16_t name_length) {
S_FILE_API_PROLOGUE;
S_API_PROLOGUE;
SAData data = {
.dread = {
.file = file,
.fileinfo = fileinfo,
.name = name,
.name_length = name_length,
}};
S_API_MESSAGE(StorageCommandDirRead);
S_API_EPILOGUE;
return S_RETURN_BOOL;
}
bool storage_dir_rewind(File* file) {
return storage_file_command_bool(file, StorageCommandDirRewind);
}
bool storage_dir_exists(Storage* storage, const char* path) {
furi_check(storage);
bool exist = false;
FileInfo fileinfo;
FS_Error error = storage_common_stat(storage, path, &fileinfo);
if(error == FSE_OK && file_info_is_dir(&fileinfo)) {
exist = true;
}
return exist;
}
/****************** COMMON ******************/
FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* timestamp) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.ctimestamp = {
.path = path,
.timestamp = timestamp,
.thread_id = furi_thread_get_current_id(),
}};
S_API_MESSAGE(StorageCommandCommonTimestamp);
S_API_EPILOGUE;
return S_RETURN_ERROR;
}
FS_Error storage_common_stat(Storage* storage, const char* path, FileInfo* fileinfo) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.cstat = {
.path = path,
.fileinfo = fileinfo,
.thread_id = furi_thread_get_current_id(),
}};
S_API_MESSAGE(StorageCommandCommonStat);
S_API_EPILOGUE;
return S_RETURN_ERROR;
}
FS_Error storage_common_remove(Storage* storage, const char* path) {
return storage_path_command(storage, path, StorageCommandCommonRemove);
}
FS_Error storage_common_rename(Storage* storage, const char* old_path, const char* new_path) {
furi_check(storage);
FS_Error error;
do {
if(!storage_common_exists(storage, old_path)) {
error = FSE_INVALID_NAME;
break;
}
if(storage_dir_exists(storage, old_path)) {
// Cannot overwrite a file with a directory
if(storage_file_exists(storage, new_path)) {
error = FSE_INVALID_NAME;
break;
}
// Cannot rename a directory to itself or to a nested directory
if(storage_common_is_subdir(storage, old_path, new_path)) {
error = FSE_INVALID_NAME;
break;
}
// Renaming a regular file to itself does nothing and always succeeds
} else if(storage_common_equivalent_path(storage, old_path, new_path)) {
error = FSE_OK;
break;
}
if(storage_file_exists(storage, new_path)) {
storage_common_remove(storage, new_path);
}
error = storage_common_copy(storage, old_path, new_path);
if(error != FSE_OK) {
break;
}
if(!storage_simply_remove_recursive(storage, old_path)) {
error = FSE_INTERNAL;
}
} while(false);
return error;
}
static FS_Error
storage_copy_recursive(Storage* storage, const char* old_path, const char* new_path) {
FS_Error error = storage_common_mkdir(storage, new_path);
DirWalk* dir_walk = dir_walk_alloc(storage);
FuriString* path;
FuriString* tmp_new_path;
FuriString* tmp_old_path;
FileInfo fileinfo;
path = furi_string_alloc();
tmp_new_path = furi_string_alloc();
tmp_old_path = furi_string_alloc();
do {
if(error != FSE_OK) break;
if(!dir_walk_open(dir_walk, old_path)) {
error = dir_walk_get_error(dir_walk);
break;
}
while(1) {
DirWalkResult res = dir_walk_read(dir_walk, path, &fileinfo);
if(res == DirWalkError) {
error = dir_walk_get_error(dir_walk);
break;
} else if(res == DirWalkLast) {
break;
} else {
furi_string_set(tmp_old_path, path);
furi_string_right(path, strlen(old_path));
furi_string_printf(tmp_new_path, "%s%s", new_path, furi_string_get_cstr(path));
if(file_info_is_dir(&fileinfo)) {
error = storage_common_mkdir(storage, furi_string_get_cstr(tmp_new_path));
} else {
error = storage_common_copy(
storage,
furi_string_get_cstr(tmp_old_path),
furi_string_get_cstr(tmp_new_path));
}
if(error != FSE_OK) break;
}
}
} while(false);
furi_string_free(tmp_new_path);
furi_string_free(tmp_old_path);
furi_string_free(path);
dir_walk_free(dir_walk);
return error;
}
FS_Error storage_common_copy(Storage* storage, const char* old_path, const char* new_path) {
furi_check(storage);
FS_Error error;
FileInfo fileinfo;
error = storage_common_stat(storage, old_path, &fileinfo);
if(error == FSE_OK) {
if(file_info_is_dir(&fileinfo)) {
error = storage_copy_recursive(storage, old_path, new_path);
} else {
Stream* stream_from = file_stream_alloc(storage);
Stream* stream_to = file_stream_alloc(storage);
do {
if(!file_stream_open(stream_from, old_path, FSAM_READ, FSOM_OPEN_EXISTING)) break;
if(!file_stream_open(stream_to, new_path, FSAM_WRITE, FSOM_CREATE_NEW)) break;
stream_copy_full(stream_from, stream_to);
} while(false);
error = file_stream_get_error(stream_from);
if(error == FSE_OK) {
error = file_stream_get_error(stream_to);
}
stream_free(stream_from);
stream_free(stream_to);
}
}
return error;
}
static FS_Error
storage_merge_recursive(Storage* storage, const char* old_path, const char* new_path) {
FS_Error error = FSE_OK;
DirWalk* dir_walk = dir_walk_alloc(storage);
FuriString *path, *file_basename, *tmp_new_path;
FileInfo fileinfo;
path = furi_string_alloc();
file_basename = furi_string_alloc();
tmp_new_path = furi_string_alloc();
do {
if(!storage_simply_mkdir(storage, new_path)) break;
dir_walk_set_recursive(dir_walk, false);
if(!dir_walk_open(dir_walk, old_path)) {
error = dir_walk_get_error(dir_walk);
break;
}
while(1) {
DirWalkResult res = dir_walk_read(dir_walk, path, &fileinfo);
if(res == DirWalkError) {
error = dir_walk_get_error(dir_walk);
break;
} else if(res == DirWalkLast) {
break;
} else {
path_extract_basename(furi_string_get_cstr(path), file_basename);
path_concat(new_path, furi_string_get_cstr(file_basename), tmp_new_path);
if(file_info_is_dir(&fileinfo)) {
if(storage_common_stat(
storage, furi_string_get_cstr(tmp_new_path), &fileinfo) == FSE_OK) {
if(file_info_is_dir(&fileinfo)) {
error =
storage_common_mkdir(storage, furi_string_get_cstr(tmp_new_path));
if(error != FSE_OK && error != FSE_EXIST) {
break;
}
}
}
}
error = storage_common_merge(
storage, furi_string_get_cstr(path), furi_string_get_cstr(tmp_new_path));
if(error != FSE_OK) {
break;
}
}
}
} while(false);
furi_string_free(tmp_new_path);
furi_string_free(file_basename);
furi_string_free(path);
dir_walk_free(dir_walk);
return error;
}
FS_Error storage_common_merge(Storage* storage, const char* old_path, const char* new_path) {
furi_check(storage);
FS_Error error;
const char* new_path_tmp = NULL;
FuriString* new_path_next = NULL;
new_path_next = furi_string_alloc();
FileInfo fileinfo;
error = storage_common_stat(storage, old_path, &fileinfo);
if(error == FSE_OK) {
if(file_info_is_dir(&fileinfo)) {
error = storage_merge_recursive(storage, old_path, new_path);
} else {
error = storage_common_stat(storage, new_path, &fileinfo);
if(error == FSE_OK) {
furi_string_set(new_path_next, new_path);
FuriString* dir_path;
FuriString* filename;
char extension[MAX_EXT_LEN] = {0};
dir_path = furi_string_alloc();
filename = furi_string_alloc();
path_extract_filename(new_path_next, filename, true);
path_extract_dirname(new_path, dir_path);
path_extract_extension(new_path_next, extension, MAX_EXT_LEN);
storage_get_next_filename(
storage,
furi_string_get_cstr(dir_path),
furi_string_get_cstr(filename),
extension,
new_path_next,
255);
furi_string_cat_printf(
dir_path, "/%s%s", furi_string_get_cstr(new_path_next), extension);
furi_string_set(new_path_next, dir_path);
furi_string_free(dir_path);
furi_string_free(filename);
new_path_tmp = furi_string_get_cstr(new_path_next);
} else {
new_path_tmp = new_path;
}
Stream* stream_from = file_stream_alloc(storage);
Stream* stream_to = file_stream_alloc(storage);
do {
if(!file_stream_open(stream_from, old_path, FSAM_READ, FSOM_OPEN_EXISTING)) break;
if(!file_stream_open(stream_to, new_path_tmp, FSAM_WRITE, FSOM_CREATE_NEW)) break;
stream_copy_full(stream_from, stream_to);
} while(false);
error = file_stream_get_error(stream_from);
if(error == FSE_OK) {
error = file_stream_get_error(stream_to);
}
stream_free(stream_from);
stream_free(stream_to);
}
}
furi_string_free(new_path_next);
return error;
}
FS_Error storage_common_mkdir(Storage* storage, const char* path) {
return storage_path_command(storage, path, StorageCommandCommonMkDir);
}
FS_Error storage_common_fs_info(
Storage* storage,
const char* fs_path,
uint64_t* total_space,
uint64_t* free_space) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.cfsinfo = {
.fs_path = fs_path,
.total_space = total_space,
.free_space = free_space,
.thread_id = furi_thread_get_current_id(),
}};
S_API_MESSAGE(StorageCommandCommonFSInfo);
S_API_EPILOGUE;
return S_RETURN_ERROR;
}
void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, FuriString* path) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.cresolvepath = {
.path = path,
.thread_id = furi_thread_get_current_id(),
}};
S_API_MESSAGE(StorageCommandCommonResolvePath);
S_API_EPILOGUE;
}
FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest) {
furi_check(storage);
if(!storage_common_exists(storage, source)) {
return FSE_OK;
}
FS_Error error = storage_common_merge(storage, source, dest);
if(error == FSE_OK) {
storage_simply_remove_recursive(storage, source);
}
return error;
}
bool storage_common_exists(Storage* storage, const char* path) {
furi_check(storage);
FileInfo file_info;
return storage_common_stat(storage, path, &file_info) == FSE_OK;
}
static bool storage_internal_equivalent_path(
Storage* storage,
const char* path1,
const char* path2,
bool check_subdir) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.cequivpath = {
.path1 = path1,
.path2 = path2,
.check_subdir = check_subdir,
.thread_id = furi_thread_get_current_id(),
}};
S_API_MESSAGE(StorageCommandCommonEquivalentPath);
S_API_EPILOGUE;
return S_RETURN_BOOL;
}
bool storage_common_equivalent_path(Storage* storage, const char* path1, const char* path2) {
return storage_internal_equivalent_path(storage, path1, path2, false);
}
bool storage_common_is_subdir(Storage* storage, const char* parent, const char* child) {
return storage_internal_equivalent_path(storage, parent, child, true);
}
/****************** ERROR ******************/
const char* storage_error_get_desc(FS_Error error_id) {
return filesystem_api_error_get_desc(error_id);
}
FS_Error storage_file_get_error(File* file) {
furi_check(file);
return file->error_id;
}
int32_t storage_file_get_internal_error(File* file) {
furi_check(file);
return file->internal_error_id;
}
const char* storage_file_get_error_desc(File* file) {
furi_check(file);
return filesystem_api_error_get_desc(file->error_id);
}
/****************** Raw SD API ******************/
FS_Error storage_sd_format(Storage* storage) {
return storage_sd_command(storage, StorageCommandSDFormat);
}
FS_Error storage_sd_unmount(Storage* storage) {
return storage_sd_command(storage, StorageCommandSDUnmount);
}
FS_Error storage_sd_mount(Storage* storage) {
return storage_sd_command(storage, StorageCommandSDMount);
}
FS_Error storage_sd_info(Storage* storage, SDInfo* info) {
furi_check(storage);
S_API_PROLOGUE;
SAData data = {
.sdinfo = {
.info = info,
}};
S_API_MESSAGE(StorageCommandSDInfo);
S_API_EPILOGUE;
return S_RETURN_ERROR;
}
FS_Error storage_sd_status(Storage* storage) {
return storage_sd_command(storage, StorageCommandSDStatus);
}
File* storage_file_alloc(Storage* storage) {
furi_check(storage);
File* file = malloc(sizeof(File));
file->type = FileTypeClosed;
file->storage = storage;
FURI_LOG_T(TAG, "File/Dir %p alloc", (void*)((uint32_t)file - SRAM_BASE));
return file;
}
bool storage_file_is_open(File* file) {
furi_check(file);
return file->type != FileTypeClosed;
}
bool storage_file_is_dir(File* file) {
furi_check(file);
return file->type == FileTypeOpenDir;
}
void storage_file_free(File* file) {
furi_check(file);
if(storage_file_is_open(file)) {
if(storage_file_is_dir(file)) {
storage_dir_close(file);
} else {
storage_file_close(file);
}
}
FURI_LOG_T(TAG, "File/Dir %p free", (void*)((uint32_t)file - SRAM_BASE));
free(file);
}
FuriPubSub* storage_get_pubsub(Storage* storage) {
furi_check(storage);
return storage->pubsub;
}
bool storage_simply_remove_recursive(Storage* storage, const char* path) {
furi_check(storage);
furi_check(path);
FileInfo fileinfo;
bool result = false;
FuriString* fullname;
FuriString* cur_dir;
if(storage_simply_remove(storage, path)) {
return true;
}
char* name = malloc(MAX_NAME_LENGTH + 1); //-V799
File* dir = storage_file_alloc(storage);
cur_dir = furi_string_alloc_set(path);
bool go_deeper = false;
while(1) {
if(!storage_dir_open(dir, furi_string_get_cstr(cur_dir))) {
storage_dir_close(dir);
break;
}
while(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) {
if(file_info_is_dir(&fileinfo)) {
furi_string_cat_printf(cur_dir, "/%s", name); //-V576
go_deeper = true;
break;
}
fullname = furi_string_alloc_printf("%s/%s", furi_string_get_cstr(cur_dir), name);
FS_Error error = storage_common_remove(storage, furi_string_get_cstr(fullname));
furi_check(error == FSE_OK);
furi_string_free(fullname);
}
storage_dir_close(dir);
if(go_deeper) {
go_deeper = false;
continue;
}
FS_Error error = storage_common_remove(storage, furi_string_get_cstr(cur_dir));
furi_check(error == FSE_OK);
if(furi_string_cmp(cur_dir, path)) {
size_t last_char = furi_string_search_rchar(cur_dir, '/');
furi_assert(last_char != FURI_STRING_FAILURE);
furi_string_left(cur_dir, last_char);
} else {
result = true;
break;
}
}
storage_file_free(dir);
furi_string_free(cur_dir);
free(name);
return result;
} //-V773
bool storage_simply_remove(Storage* storage, const char* path) {
furi_check(storage);
FS_Error result;
result = storage_common_remove(storage, path);
return result == FSE_OK || result == FSE_NOT_EXIST;
}
bool storage_simply_mkdir(Storage* storage, const char* path) {
furi_check(storage);
FS_Error result;
result = storage_common_mkdir(storage, path);
return result == FSE_OK || result == FSE_EXIST;
}
void storage_get_next_filename(
Storage* storage,
const char* dirname,
const char* filename,
const char* fileextension,
FuriString* nextfilename,
uint8_t max_len) {
furi_check(storage);
FuriString* temp_str;
uint16_t num = 0;
temp_str = furi_string_alloc_printf("%s/%s%s", dirname, filename, fileextension);
while(storage_common_stat(storage, furi_string_get_cstr(temp_str), NULL) == FSE_OK) {
num++;
furi_string_printf(temp_str, "%s/%s%d%s", dirname, filename, num, fileextension);
}
if(num && (max_len > strlen(filename))) {
furi_string_printf(nextfilename, "%s%d", filename, num);
} else {
furi_string_printf(nextfilename, "%s", filename);
}
furi_string_free(temp_str);
}