#include "storage_processing.h" #include "storage_sorting.h" #include #include #define STORAGE_PATH_PREFIX_LEN 4u _Static_assert( sizeof(STORAGE_ANY_PATH_PREFIX) == STORAGE_PATH_PREFIX_LEN + 1, "Any path prefix len mismatch"); _Static_assert( sizeof(STORAGE_EXT_PATH_PREFIX) == STORAGE_PATH_PREFIX_LEN + 1, "Ext path prefix len mismatch"); _Static_assert( sizeof(STORAGE_INT_PATH_PREFIX) == STORAGE_PATH_PREFIX_LEN + 1, "Int path prefix len mismatch"); #define FS_CALL(_storage, _fn) ret = _storage->fs_api->_fn; static bool storage_type_is_valid(StorageType type) { #ifdef FURI_RAM_EXEC return type == ST_EXT; #else return type < ST_ERROR; #endif } static StorageData* get_storage_by_file(File* file, StorageData* storages) { StorageData* storage_data = NULL; for(uint8_t i = 0; i < STORAGE_COUNT; i++) { if(storage_has_file(file, &storages[i])) { storage_data = &storages[i]; } } return storage_data; } static const char* cstr_path_without_vfs_prefix(FuriString* path) { const char* path_cstr = furi_string_get_cstr(path); return path_cstr + MIN(STORAGE_PATH_PREFIX_LEN, strlen(path_cstr)); } static StorageType storage_get_type_by_path(FuriString* path) { StorageType type = ST_ERROR; const char* path_cstr = furi_string_get_cstr(path); if(furi_string_size(path) > STORAGE_PATH_PREFIX_LEN) { if(path_cstr[STORAGE_PATH_PREFIX_LEN] != '/') { return ST_ERROR; } } if(memcmp(path_cstr, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { type = ST_EXT; } else if(memcmp(path_cstr, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { type = ST_INT; } else if(memcmp(path_cstr, STORAGE_ANY_PATH_PREFIX, strlen(STORAGE_ANY_PATH_PREFIX)) == 0) { type = ST_ANY; } return type; } static void storage_path_change_to_real_storage(FuriString* path, StorageType real_storage) { if(furi_string_search(path, STORAGE_ANY_PATH_PREFIX) == 0) { switch(real_storage) { case ST_EXT: furi_string_replace_at( path, 0, strlen(STORAGE_EXT_PATH_PREFIX), STORAGE_EXT_PATH_PREFIX); break; case ST_INT: furi_string_replace_at( path, 0, strlen(STORAGE_INT_PATH_PREFIX), STORAGE_INT_PATH_PREFIX); break; default: break; } } } static FS_Error storage_get_data(Storage* app, FuriString* path, StorageData** storage) { StorageType type = storage_get_type_by_path(path); if(storage_type_is_valid(type)) { if(type == ST_ANY) { type = ST_INT; if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusOK) { type = ST_EXT; } storage_path_change_to_real_storage(path, type); } furi_assert(type == ST_EXT || type == ST_INT); *storage = &app->storage[type]; return FSE_OK; } else { return FSE_INVALID_NAME; } } /******************* File Functions *******************/ static bool storage_process_file_open( Storage* app, File* file, FuriString* path, FS_AccessMode access_mode, FS_OpenMode open_mode) { bool ret = false; StorageData* storage; file->error_id = storage_get_data(app, path, &storage); if(file->error_id == FSE_OK) { if(storage_path_already_open(path, storage)) { file->error_id = FSE_ALREADY_OPEN; } else { if(access_mode & FSAM_WRITE) { storage_data_timestamp(storage); } storage_push_storage_file(file, path, storage); const char* path_cstr_no_vfs = cstr_path_without_vfs_prefix(path); FS_CALL(storage, file.open(storage, file, path_cstr_no_vfs, access_mode, open_mode)); } } return ret; } static bool storage_process_file_close(Storage* app, File* file) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, file.close(storage, file)); storage_pop_storage_file(file, storage); StorageEvent event = {.type = StorageEventTypeFileClose}; furi_pubsub_publish(app->pubsub, &event); } return ret; } static uint16_t storage_process_file_read(Storage* app, File* file, void* buff, uint16_t const bytes_to_read) { uint16_t ret = 0; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, file.read(storage, file, buff, bytes_to_read)); } return ret; } static uint16_t storage_process_file_write( Storage* app, File* file, const void* buff, uint16_t const bytes_to_write) { uint16_t ret = 0; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { storage_data_timestamp(storage); FS_CALL(storage, file.write(storage, file, buff, bytes_to_write)); } return ret; } static bool storage_process_file_seek( Storage* app, File* file, const uint32_t offset, const bool from_start) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, file.seek(storage, file, offset, from_start)); } return ret; } static uint64_t storage_process_file_tell(Storage* app, File* file) { uint64_t ret = 0; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, file.tell(storage, file)); } return ret; } static bool storage_process_file_truncate(Storage* app, File* file) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { storage_data_timestamp(storage); FS_CALL(storage, file.truncate(storage, file)); } return ret; } static bool storage_process_file_sync(Storage* app, File* file) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { storage_data_timestamp(storage); FS_CALL(storage, file.sync(storage, file)); } return ret; } static uint64_t storage_process_file_size(Storage* app, File* file) { uint64_t ret = 0; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, file.size(storage, file)); } return ret; } static bool storage_process_file_eof(Storage* app, File* file) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, file.eof(storage, file)); } return ret; } /*************** Sorting Dir Functions ***************/ static bool storage_process_dir_rewind_internal(StorageData* storage, File* file); static bool storage_process_dir_read_internal( StorageData* storage, File* file, FileInfo* fileinfo, char* name, const uint16_t name_length); static int storage_sorted_file_record_compare(const void* sorted_a, const void* sorted_b) { SortedFileRecord* a = (SortedFileRecord*)sorted_a; SortedFileRecord* b = (SortedFileRecord*)sorted_b; if(a->info.flags & FSF_DIRECTORY && !(b->info.flags & FSF_DIRECTORY)) return -1; else if(!(a->info.flags & FSF_DIRECTORY) && b->info.flags & FSF_DIRECTORY) return 1; else return furi_string_cmpi(a->name, b->name); } static bool storage_sorted_dir_read_next( SortedDir* dir, FileInfo* fileinfo, char* name, const uint16_t name_length) { bool ret = false; if(dir->index < dir->count) { SortedFileRecord* sorted = &dir->sorted[dir->index]; if(fileinfo) { *fileinfo = sorted->info; } if(name) { strncpy(name, furi_string_get_cstr(sorted->name), name_length); } dir->index++; ret = true; } return ret; } static void storage_sorted_dir_rewind(SortedDir* dir) { dir->index = 0; } static bool storage_sorted_dir_prepare(SortedDir* dir, StorageData* storage, File* file) { bool ret = true; dir->count = 0; dir->index = 0; FileInfo info; char name[SORTING_MAX_NAME_LENGTH + 1] = {0}; furi_check(!dir->sorted); while(storage_process_dir_read_internal(storage, file, &info, name, SORTING_MAX_NAME_LENGTH)) { if(memmgr_get_free_heap() < SORTING_MIN_FREE_MEMORY) { ret = false; break; } if(dir->count == 0) { //-V547 dir->sorted = malloc(sizeof(SortedFileRecord)); } else { // Our realloc actually mallocs a new block and copies the data over, // so we need to check if we have enough memory for the new block size_t size = sizeof(SortedFileRecord) * (dir->count + 1); if(memmgr_heap_get_max_free_block() >= size) { dir->sorted = realloc(dir->sorted, sizeof(SortedFileRecord) * (dir->count + 1)); //-V701 } else { ret = false; break; } } dir->sorted[dir->count].name = furi_string_alloc_set(name); dir->sorted[dir->count].info = info; dir->count++; } return ret; } static void storage_sorted_dir_sort(SortedDir* dir) { qsort(dir->sorted, dir->count, sizeof(SortedFileRecord), storage_sorted_file_record_compare); } static void storage_sorted_dir_clear_data(SortedDir* dir) { if(dir->sorted != NULL) { for(size_t i = 0; i < dir->count; i++) { furi_string_free(dir->sorted[i].name); } free(dir->sorted); dir->sorted = NULL; } } static void storage_file_remove_sort_data(File* file) { if(file->sort_data != NULL) { storage_sorted_dir_clear_data(file->sort_data); free(file->sort_data); file->sort_data = NULL; } } static void storage_file_add_sort_data(File* file, StorageData* storage) { file->sort_data = malloc(sizeof(SortedDir)); if(storage_sorted_dir_prepare(file->sort_data, storage, file)) { storage_sorted_dir_sort(file->sort_data); } else { storage_file_remove_sort_data(file); storage_process_dir_rewind_internal(storage, file); } } static bool storage_file_has_sort_data(File* file) { return file->sort_data != NULL; } /******************* Dir Functions *******************/ static bool storage_process_dir_read_internal( StorageData* storage, File* file, FileInfo* fileinfo, char* name, const uint16_t name_length) { bool ret = false; FS_CALL(storage, dir.read(storage, file, fileinfo, name, name_length)); return ret; } static bool storage_process_dir_rewind_internal(StorageData* storage, File* file) { bool ret = false; FS_CALL(storage, dir.rewind(storage, file)); return ret; } static bool storage_process_dir_open(Storage* app, File* file, FuriString* path) { bool ret = false; StorageData* storage; file->error_id = storage_get_data(app, path, &storage); if(file->error_id == FSE_OK) { if(storage_path_already_open(path, storage)) { file->error_id = FSE_ALREADY_OPEN; } else { storage_push_storage_file(file, path, storage); FS_CALL(storage, dir.open(storage, file, cstr_path_without_vfs_prefix(path))); if(file->error_id == FSE_OK) { storage_file_add_sort_data(file, storage); } } } return ret; } static bool storage_process_dir_close(Storage* app, File* file) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { FS_CALL(storage, dir.close(storage, file)); storage_file_remove_sort_data(file); storage_pop_storage_file(file, storage); StorageEvent event = {.type = StorageEventTypeDirClose}; furi_pubsub_publish(app->pubsub, &event); } return ret; } static bool storage_process_dir_read( Storage* app, File* file, FileInfo* fileinfo, char* name, const uint16_t name_length) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { if(storage_file_has_sort_data(file)) { ret = storage_sorted_dir_read_next(file->sort_data, fileinfo, name, name_length); if(ret) { file->error_id = FSE_OK; } else { file->error_id = FSE_NOT_EXIST; } } else { ret = storage_process_dir_read_internal(storage, file, fileinfo, name, name_length); } } return ret; } static bool storage_process_dir_rewind(Storage* app, File* file) { bool ret = false; StorageData* storage = get_storage_by_file(file, app->storage); if(storage == NULL) { file->error_id = FSE_INVALID_PARAMETER; } else { if(storage_file_has_sort_data(file)) { storage_sorted_dir_rewind(file->sort_data); ret = true; } else { ret = storage_process_dir_rewind_internal(storage, file); } } return ret; } /******************* Common FS Functions *******************/ static FS_Error storage_process_common_timestamp(Storage* app, FuriString* path, uint32_t* timestamp) { StorageData* storage; FS_Error ret = storage_get_data(app, path, &storage); if(ret == FSE_OK) { *timestamp = storage_data_get_timestamp(storage); } return ret; } static FS_Error storage_process_common_stat(Storage* app, FuriString* path, FileInfo* fileinfo) { StorageData* storage; FS_Error ret = storage_get_data(app, path, &storage); if(ret == FSE_OK) { FS_CALL(storage, common.stat(storage, cstr_path_without_vfs_prefix(path), fileinfo)); } return ret; } static FS_Error storage_process_common_remove(Storage* app, FuriString* path) { StorageData* storage; FS_Error ret = storage_get_data(app, path, &storage); do { if(storage_path_already_open(path, storage)) { ret = FSE_ALREADY_OPEN; break; } storage_data_timestamp(storage); FS_CALL(storage, common.remove(storage, cstr_path_without_vfs_prefix(path))); } while(false); return ret; } static FS_Error storage_process_common_mkdir(Storage* app, FuriString* path) { StorageData* storage; FS_Error ret = storage_get_data(app, path, &storage); if(ret == FSE_OK) { storage_data_timestamp(storage); FS_CALL(storage, common.mkdir(storage, cstr_path_without_vfs_prefix(path))); } return ret; } static FS_Error storage_process_common_fs_info( Storage* app, FuriString* path, uint64_t* total_space, uint64_t* free_space) { StorageData* storage; FS_Error ret = storage_get_data(app, path, &storage); if(ret == FSE_OK) { FS_CALL( storage, common.fs_info(storage, cstr_path_without_vfs_prefix(path), total_space, free_space)); } return ret; } /****************** Raw SD API ******************/ // TODO think about implementing a custom storage API to split that kind of api linkage #include "storages/storage_ext.h" static FS_Error storage_process_sd_format(Storage* app) { FS_Error ret = FSE_OK; if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusNotReady) { ret = FSE_NOT_READY; } else { ret = sd_format_card(&app->storage[ST_EXT]); storage_data_timestamp(&app->storage[ST_EXT]); } return ret; } static FS_Error storage_process_sd_unmount(Storage* app) { FS_Error ret = FSE_OK; if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusNotReady) { ret = FSE_NOT_READY; } else { sd_unmount_card(&app->storage[ST_EXT]); storage_data_timestamp(&app->storage[ST_EXT]); } return ret; } static FS_Error storage_process_sd_info(Storage* app, SDInfo* info) { FS_Error ret = FSE_OK; if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusNotReady) { ret = FSE_NOT_READY; } else { ret = sd_card_info(&app->storage[ST_EXT], info); } return ret; } static FS_Error storage_process_sd_status(Storage* app) { FS_Error ret; StorageStatus status = storage_data_status(&app->storage[ST_EXT]); switch(status) { case StorageStatusOK: ret = FSE_OK; break; case StorageStatusNotReady: ret = FSE_NOT_READY; break; default: ret = FSE_INTERNAL; break; } return ret; } /******************** Aliases processing *******************/ static void storage_process_alias( Storage* app, FuriString* path, FuriThreadId thread_id, bool create_folders) { if(furi_string_start_with(path, STORAGE_APP_DATA_PATH_PREFIX)) { FuriString* apps_data_path_with_appsid = furi_string_alloc_set(APPS_DATA_PATH "/"); furi_string_cat(apps_data_path_with_appsid, furi_thread_get_appid(thread_id)); // "/data" -> "/ext/apps_data/appsid" furi_string_replace_at( path, 0, strlen(STORAGE_APP_DATA_PATH_PREFIX), furi_string_get_cstr(apps_data_path_with_appsid)); // Create app data folder if not exists if(create_folders && storage_process_common_stat(app, apps_data_path_with_appsid, NULL) != FSE_OK) { furi_string_set(apps_data_path_with_appsid, APPS_DATA_PATH); storage_process_common_mkdir(app, apps_data_path_with_appsid); furi_string_cat(apps_data_path_with_appsid, "/"); furi_string_cat(apps_data_path_with_appsid, furi_thread_get_appid(thread_id)); storage_process_common_mkdir(app, apps_data_path_with_appsid); } furi_string_free(apps_data_path_with_appsid); } else if(furi_string_start_with(path, STORAGE_APP_ASSETS_PATH_PREFIX)) { FuriString* apps_assets_path_with_appsid = furi_string_alloc_set(APPS_ASSETS_PATH "/"); furi_string_cat(apps_assets_path_with_appsid, furi_thread_get_appid(thread_id)); // "/assets" -> "/ext/apps_assets/appsid" furi_string_replace_at( path, 0, strlen(STORAGE_APP_ASSETS_PATH_PREFIX), furi_string_get_cstr(apps_assets_path_with_appsid)); furi_string_free(apps_assets_path_with_appsid); } } /****************** API calls processing ******************/ static void storage_process_message_internal(Storage* app, StorageMessage* message) { FuriString* path = NULL; switch(message->command) { // File operations case StorageCommandFileOpen: path = furi_string_alloc_set(message->data->fopen.path); storage_process_alias(app, path, message->data->fopen.thread_id, true); message->return_data->bool_value = storage_process_file_open( app, message->data->fopen.file, path, message->data->fopen.access_mode, message->data->fopen.open_mode); break; case StorageCommandFileClose: message->return_data->bool_value = storage_process_file_close(app, message->data->fopen.file); break; case StorageCommandFileRead: message->return_data->uint16_value = storage_process_file_read( app, message->data->fread.file, message->data->fread.buff, message->data->fread.bytes_to_read); break; case StorageCommandFileWrite: message->return_data->uint16_value = storage_process_file_write( app, message->data->fwrite.file, message->data->fwrite.buff, message->data->fwrite.bytes_to_write); break; case StorageCommandFileSeek: message->return_data->bool_value = storage_process_file_seek( app, message->data->fseek.file, message->data->fseek.offset, message->data->fseek.from_start); break; case StorageCommandFileTell: message->return_data->uint64_value = storage_process_file_tell(app, message->data->file.file); break; case StorageCommandFileTruncate: message->return_data->bool_value = storage_process_file_truncate(app, message->data->file.file); break; case StorageCommandFileSync: message->return_data->bool_value = storage_process_file_sync(app, message->data->file.file); break; case StorageCommandFileSize: message->return_data->uint64_value = storage_process_file_size(app, message->data->file.file); break; case StorageCommandFileEof: message->return_data->bool_value = storage_process_file_eof(app, message->data->file.file); break; // Dir operations case StorageCommandDirOpen: path = furi_string_alloc_set(message->data->dopen.path); storage_process_alias(app, path, message->data->dopen.thread_id, true); message->return_data->bool_value = storage_process_dir_open(app, message->data->dopen.file, path); break; case StorageCommandDirClose: message->return_data->bool_value = storage_process_dir_close(app, message->data->file.file); break; case StorageCommandDirRead: message->return_data->bool_value = storage_process_dir_read( app, message->data->dread.file, message->data->dread.fileinfo, message->data->dread.name, message->data->dread.name_length); break; case StorageCommandDirRewind: message->return_data->bool_value = storage_process_dir_rewind(app, message->data->file.file); break; // Common operations case StorageCommandCommonTimestamp: path = furi_string_alloc_set(message->data->ctimestamp.path); storage_process_alias(app, path, message->data->ctimestamp.thread_id, false); message->return_data->error_value = storage_process_common_timestamp(app, path, message->data->ctimestamp.timestamp); break; case StorageCommandCommonStat: path = furi_string_alloc_set(message->data->cstat.path); storage_process_alias(app, path, message->data->cstat.thread_id, false); message->return_data->error_value = storage_process_common_stat(app, path, message->data->cstat.fileinfo); break; case StorageCommandCommonRemove: path = furi_string_alloc_set(message->data->path.path); storage_process_alias(app, path, message->data->path.thread_id, false); message->return_data->error_value = storage_process_common_remove(app, path); break; case StorageCommandCommonMkDir: path = furi_string_alloc_set(message->data->path.path); storage_process_alias(app, path, message->data->path.thread_id, true); message->return_data->error_value = storage_process_common_mkdir(app, path); break; case StorageCommandCommonFSInfo: path = furi_string_alloc_set(message->data->cfsinfo.fs_path); storage_process_alias(app, path, message->data->cfsinfo.thread_id, false); message->return_data->error_value = storage_process_common_fs_info( app, path, message->data->cfsinfo.total_space, message->data->cfsinfo.free_space); break; case StorageCommandCommonResolvePath: storage_process_alias( app, message->data->cresolvepath.path, message->data->cresolvepath.thread_id, true); break; // SD operations case StorageCommandSDFormat: message->return_data->error_value = storage_process_sd_format(app); break; case StorageCommandSDUnmount: message->return_data->error_value = storage_process_sd_unmount(app); break; case StorageCommandSDInfo: message->return_data->error_value = storage_process_sd_info(app, message->data->sdinfo.info); break; case StorageCommandSDStatus: message->return_data->error_value = storage_process_sd_status(app); break; } if(path != NULL) { //-V547 furi_string_free(path); } api_lock_unlock(message->lock); } void storage_process_message(Storage* app, StorageMessage* message) { storage_process_message_internal(app, message); }