From ba074068b0c0c620b6ab13b88c8e294b9d3aaf8d Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:56:13 +0300 Subject: [PATCH] [FL-3662] Do not remove file when renaming to itself (#3193) * Do not allow overwriting a file with dir and support renaming file to itself * Fix operator precedence error * Add support for storage-specific path equivalence checks * Fix typo * Fix updater compilation * Update Doxygen comments in storage.h Co-authored-by: Aleksandr Kutuzov --- .../storage/filesystem_api_internal.h | 8 + applications/services/storage/storage.h | 593 ++++++++++++------ .../services/storage/storage_external_api.c | 40 +- .../services/storage/storage_message.h | 9 + .../services/storage/storage_processing.c | 50 ++ .../services/storage/storages/storage_ext.c | 11 + .../services/storage/storages/storage_int.c | 5 + targets/f18/api_symbols.csv | 3 +- targets/f7/api_symbols.csv | 3 +- 9 files changed, 507 insertions(+), 215 deletions(-) diff --git a/applications/services/storage/filesystem_api_internal.h b/applications/services/storage/filesystem_api_internal.h index 967d3bb41c1..ba98b380e94 100644 --- a/applications/services/storage/filesystem_api_internal.h +++ b/applications/services/storage/filesystem_api_internal.h @@ -165,6 +165,13 @@ typedef struct { * @param total_space pointer to total space value * @param free_space pointer to free space value * @return FS_Error error info + * + * @var FS_Common_Api::equivalent_path + * @brief Test whether two paths are equivalent (e.g differing case on a case-insensitive fs) + * @param path1 first path to be compared + * @param path2 second path to be compared + * @param truncate if set to true, compare only up to the path1's length + * @return true if path1 and path2 are considered equivalent */ typedef struct { FS_Error (*const stat)(void* context, const char* path, FileInfo* fileinfo); @@ -175,6 +182,7 @@ typedef struct { const char* fs_path, uint64_t* total_space, uint64_t* free_space); + bool (*const equivalent_path)(const char* path1, const char* path2); } FS_Common_Api; /** Full filesystem api structure */ diff --git a/applications/services/storage/storage.h b/applications/services/storage/storage.h index 1abc8ed0ebd..3caa155c7dc 100644 --- a/applications/services/storage/storage.h +++ b/applications/services/storage/storage.h @@ -1,4 +1,9 @@ +/** + * @file storage.h + * @brief APIs for working with storages, directories and files. + */ #pragma once + #include #include "filesystem_api_defines.h" #include "storage_sd_api.h" @@ -23,43 +28,62 @@ extern "C" { typedef struct Storage Storage; -/** Allocates and initializes a file descriptor - * @return File* +/** + * @brief Allocate and initialize a file instance. + * + * @param storage pointer to a storage API instance. + * @return pointer to the created instance. */ File* storage_file_alloc(Storage* storage); -/** Frees the file descriptor. Closes the file if it was open. +/** + * @brief Free the file instance. + * + * If the file was open, calling this function will close it automatically. + * @param file pointer to the file instance to be freed. */ void storage_file_free(File* file); +/** + * @brief Enumeration of events emitted by the storage through the PubSub system. + */ typedef enum { - StorageEventTypeCardMount, - StorageEventTypeCardUnmount, - StorageEventTypeCardMountError, - StorageEventTypeFileClose, - StorageEventTypeDirClose, + StorageEventTypeCardMount, /**< SD card was mounted. */ + StorageEventTypeCardUnmount, /**< SD card was unmounted. */ + StorageEventTypeCardMountError, /**< An error occurred during mounting of an SD card. */ + StorageEventTypeFileClose, /**< A file was closed. */ + StorageEventTypeDirClose, /**< A directory was closed. */ } StorageEventType; +/** + * @brief Storage event (passed to the PubSub callback). + */ typedef struct { - StorageEventType type; + StorageEventType type; /**< Type of the event. */ } StorageEvent; /** - * Get storage pubsub. + * @brief Get the storage pubsub instance. + * * Storage will send StorageEvent messages. - * @param storage - * @return FuriPubSub* + * + * @param storage pointer to a storage API instance. + * @return pointer to the pubsub instance. */ FuriPubSub* storage_get_pubsub(Storage* storage); /******************* File Functions *******************/ -/** Opens an existing file or create a new one. - * @param file pointer to file object. - * @param path path to file - * @param access_mode access mode from FS_AccessMode +/** + * @brief Open an existing file or create a new one. + * + * @warning The calling code MUST call storage_file_close() even if the open operation had failed. + * + * @param file pointer to the file instance to be opened. + * @param path pointer to a zero-terminated string containing the path to the file to be opened. + * @param access_mode access mode from FS_AccessMode. * @param open_mode open mode from FS_OpenMode - * @return success flag. You need to close the file even if the open operation failed. + * @return true if the file was successfully opened, false otherwise. */ bool storage_file_open( File* file, @@ -67,202 +91,267 @@ bool storage_file_open( FS_AccessMode access_mode, FS_OpenMode open_mode); -/** Close the file. - * @param file pointer to a file object, the file object will be freed. - * @return success flag +/** + * @brief Close the file. + * + * @param file pointer to the file instance to be closed. + * @return true if the file was successfully closed, false otherwise. */ bool storage_file_close(File* file); -/** Tells if the file is open - * @param file pointer to a file object - * @return bool true if file is open +/** + * @brief Check whether the file is open. + * + * @param file pointer to the file instance in question. + * @return true if the file is open, false otherwise. */ bool storage_file_is_open(File* file); -/** Tells if the file is a directory - * @param file pointer to a file object - * @return bool true if file is a directory +/** + * @brief Check whether a file instance represents a directory. + * + * @param file pointer to the file instance in question. + * @return true if the file instance represents a directory, false otherwise. */ bool storage_file_is_dir(File* file); -/** Reads bytes from a file into a buffer - * @param file pointer to file object. - * @param buff pointer to a buffer, for reading - * @param bytes_to_read how many bytes to read. Must be less than or equal to the size of the buffer. - * @return uint16_t how many bytes were actually read +/** + * @brief Read bytes from a file into a buffer. + * + * @param file pointer to the file instance to read from. + * @param buff pointer to the buffer to be filled with read data. + * @param bytes_to_read number of bytes to read. Must be less than or equal to the size of the buffer. + * @return actual number of bytes read (may be fewer than requested). */ uint16_t storage_file_read(File* file, void* buff, uint16_t bytes_to_read); -/** Writes bytes from a buffer to a file - * @param file pointer to file object. - * @param buff pointer to buffer, for writing - * @param bytes_to_write how many bytes to write. Must be less than or equal to the size of the buffer. - * @return uint16_t how many bytes were actually written +/** + * @brief Write bytes from a buffer to a file. + * + * @param file pointer to the file instance to write into. + * @param buff pointer to the buffer containing the data to be written. + * @param bytes_to_write number of bytes to write. Must be less than or equal to the size of the buffer. + * @return actual number of bytes written (may be fewer than requested). */ uint16_t storage_file_write(File* file, const void* buff, uint16_t bytes_to_write); -/** Moves the r/w pointer - * @param file pointer to file object. - * @param offset offset to move the r/w pointer - * @param from_start set an offset from the start or from the current position +/** + * @brief Change the current access position in a file. + * + * @param file pointer to the file instance in question. + * @param offset access position offset (meaning depends on from_start parameter). + * @param from_start if true, set the access position relative to the file start, otherwise relative to the current position. * @return success flag */ bool storage_file_seek(File* file, uint32_t offset, bool from_start); -/** Gets the position of the r/w pointer - * @param file pointer to file object. - * @return uint64_t position of the r/w pointer +/** + * @brief Get the current access position. + * + * @param file pointer to the file instance in question. + * @return current access position. */ uint64_t storage_file_tell(File* file); -/** Truncates the file size to the current position of the r/w pointer - * @param file pointer to file object. - * @return bool success flag +/** + * @brief Truncate the file size to the current access position. + * + * @param file pointer to the file instance to be truncated. + * @return true if the file was successfully truncated, false otherwise. */ bool storage_file_truncate(File* file); -/** Gets the size of the file - * @param file pointer to file object. - * @return uint64_t size of the file +/** + * @brief Get the file size. + * + * @param file pointer to the file instance in question. + * @return size of the file, in bytes. */ uint64_t storage_file_size(File* file); -/** Writes file cache to storage - * @param file pointer to file object. - * @return bool success flag +/** + * @brief Synchronise the file cache with the actual storage. + * + * @param file pointer to the file instance in question. + * @return true if the file was successfully synchronised, false otherwise. */ bool storage_file_sync(File* file); -/** Checks that the r/w pointer is at the end of the file - * @param file pointer to file object. - * @return bool success flag +/** + * @brief Check whether the current access position is at the end of the file. + * + * @param file pointer to a file instance in question. + * @return bool true if the current access position is at the end of the file, false otherwise. */ bool storage_file_eof(File* file); /** - * @brief Check that file exists + * @brief Check whether a file exists. * - * @param storage - * @param path - * @return true if file exists + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path to the file in question. + * @return true if the file exists, false otherwise. */ bool storage_file_exists(Storage* storage, const char* path); /** - * @brief Copy data from one opened file to another opened file - * Size bytes will be copied from current position of source file to current position of destination file + * @brief Copy data from a source file to the destination file. + * + * Both files must be opened prior to calling this function. + * + * The requested amount of bytes will be copied from the current access position + * in the source file to the current access position in the destination file. * - * @param source source file - * @param destination destination file - * @param size size of data to copy - * @return bool success flag + * @param source pointer to a source file instance. + * @param destination pointer to a destination file instance. + * @param size data size to be copied, in bytes. + * @return true if the data was successfully copied, false otherwise. */ bool storage_file_copy_to_file(File* source, File* destination, uint32_t size); -/******************* Dir Functions *******************/ +/******************* Directory Functions *******************/ -/** Opens a directory to get objects from it - * @param app pointer to the api - * @param file pointer to file object. - * @param path path to directory - * @return bool success flag. You need to close the directory even if the open operation failed. +/** + * @brief Open a directory. + * + * Opening a directory is necessary to be able to read its contents with storage_dir_read(). + * + * @warning The calling code MUST call storage_dir_close() even if the open operation had failed. + * + * @param file pointer to a file instance representing the directory in question. + * @param path pointer to a zero-terminated string containing the path of the directory in question. + * @return true if the directory was successfully opened, false otherwise. */ bool storage_dir_open(File* file, const char* path); -/** Close the directory. Also free file handle structure and point it to the NULL. - * @param file pointer to a file object. - * @return bool success flag +/** + * @brief Close the directory. + * + * @param file pointer to a file instance representing the directory in question. + * @return true if the directory was successfully closed, false otherwise. */ bool storage_dir_close(File* file); -/** Reads the next object in the directory - * @param file pointer to file object. - * @param fileinfo pointer to the read FileInfo, may be NULL - * @param name pointer to name buffer, may be NULL - * @param name_length name buffer length - * @return success flag (if the next object does not exist, it also returns false and sets the file error id to FSE_NOT_EXIST) +/** + * @brief Get the next item in the directory. + * + * If the next object does not exist, this function returns false as well + * and sets the file error id to FSE_NOT_EXIST. + * + * @param file pointer to a file instance representing the directory in question. + * @param fileinfo pointer to the FileInfo structure to contain the info (may be NULL). + * @param name pointer to the buffer to contain the name (may be NULL). + * @param name_length maximum capacity of the name buffer, in bytes. + * @return true if the next item was successfully read, false otherwise. */ bool storage_dir_read(File* file, FileInfo* fileinfo, char* name, uint16_t name_length); -/** Rewinds the read pointer to first item in the directory - * @param file pointer to file object. - * @return bool success flag +/** + * @brief Change the access position to first item in the directory. + * + * @param file pointer to a file instance representing the directory in question. + * @return true if the access position was successfully changed, false otherwise. */ bool storage_dir_rewind(File* file); /** - * @brief Check that dir exists + * @brief Check whether a directory exists. * - * @param storage - * @param path - * @return bool + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path of the directory in question. + * @return true if the directory exists, false otherwise. */ bool storage_dir_exists(Storage* storage, const char* path); /******************* Common Functions *******************/ -/** Retrieves unix timestamp of last access - * - * @param storage The storage instance - * @param path path to file/directory - * @param timestamp the timestamp pointer +/** + * @brief Get the last access time in UNIX format. * - * @return FS_Error operation result + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path of the item in question. + * @param timestamp pointer to a value to contain the timestamp. + * @return FSE_OK if the timestamp has been successfully received, any other error code on failure. */ FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* timestamp); -/** Retrieves information about a file/directory - * @param app pointer to the api - * @param path path to file/directory - * @param fileinfo pointer to the read FileInfo, may be NULL - * @return FS_Error operation result +/** + * @brief Get information about a file or a directory. + * + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path of the item in question. + * @param fileinfo pointer to the FileInfo structure to contain the info (may be NULL). + * @return FSE_OK if the info has been successfully received, any other error code on failure. */ FS_Error storage_common_stat(Storage* storage, const char* path, FileInfo* fileinfo); -/** Removes a file/directory from the repository, the directory must be empty and the file/directory must not be open - * @param app pointer to the api - * @param path - * @return FS_Error operation result +/** + * @brief Remove a file or a directory. + * + * The directory must be empty. + * The file or the directory must NOT be open. + * + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path of the item to be removed. + * @return FSE_OK if the file or directory has been successfully removed, any other error code on failure. */ FS_Error storage_common_remove(Storage* storage, const char* path); -/** Renames file/directory, file/directory must not be open. Will overwrite existing file. - * @param app pointer to the api - * @param old_path old path - * @param new_path new path - * @return FS_Error operation result +/** + * @brief Rename a file or a directory. + * + * The file or the directory must NOT be open. + * Will overwrite the destination file if it already exists. + * + * Renaming a regular file to itself does nothing and always succeeds. + * Renaming a directory to itself or to a subdirectory of itself always fails. + * + * @param storage pointer to a storage API instance. + * @param old_path pointer to a zero-terminated string containing the source path. + * @param new_path pointer to a zero-terminated string containing the destination path. + * @return FSE_OK if the file or directory has been successfully renamed, any other error code on failure. */ FS_Error storage_common_rename(Storage* storage, const char* old_path, const char* new_path); -/** Copy file, file must not be open - * @param app pointer to the api - * @param old_path old path - * @param new_path new path - * @return FS_Error operation result +/** + * @brief Copy the file to a new location. + * + * The file must NOT be open at the time of calling this function. + * + * @param storage pointer to a storage API instance. + * @param old_path pointer to a zero-terminated string containing the source path. + * @param new_path pointer to a zero-terminated string containing the destination path. + * @return FSE_OK if the file has been successfully copied, any other error code on failure. */ FS_Error storage_common_copy(Storage* storage, const char* old_path, const char* new_path); -/** Copy one folder contents into another with rename of all conflicting files - * @param app pointer to the api - * @param old_path old path - * @param new_path new path - * @return FS_Error operation result +/** + * @brief Copy the contents of one directory into another and rename all conflicting files. + * + * @param storage pointer to a storage API instance. + * @param old_path pointer to a zero-terminated string containing the source path. + * @param new_path pointer to a zero-terminated string containing the destination path. + * @return FSE_OK if the directories have been successfully merged, any other error code on failure. */ FS_Error storage_common_merge(Storage* storage, const char* old_path, const char* new_path); -/** Creates a directory - * @param app pointer to the api - * @param path directory path - * @return FS_Error operation result +/** + * @brief Create a directory. + * + * @param storage pointer to a storage API instance. + * @param fs_path pointer to a zero-terminated string containing the directory path. + * @return FSE_OK if the directory has been successfully created, any other error code on failure. */ FS_Error storage_common_mkdir(Storage* storage, const char* path); -/** Gets general information about the storage - * @param app pointer to the api - * @param fs_path the path to the storage of interest - * @param total_space pointer to total space record, will be filled - * @param free_space pointer to free space record, will be filled - * @return FS_Error operation result +/** + * @brief Get the general information about the storage. + * + * @param storage pointer to a storage API instance. + * @param fs_path pointer to a zero-terminated string containing the path to the storage question. + * @param total_space pointer to the value to contain the total capacity, in bytes. + * @param free_space pointer to the value to contain the available space, in bytes. + * @return FSE_OK if the information has been successfully received, any other error code on failure. */ FS_Error storage_common_fs_info( Storage* storage, @@ -271,150 +360,242 @@ FS_Error storage_common_fs_info( uint64_t* free_space); /** - * @brief Parse aliases in path and replace them with real path - * Also will create special folders if they are not exist + * @brief Parse aliases in a path and replace them with the real path. + * + * Necessary special directories will be created automatically if they did not exist. * - * @param storage - * @param path - * @return bool + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path in question. + * @return true if the path was successfully resolved, false otherwise. */ void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, FuriString* path); /** - * @brief Move content of one folder to another, with rename of all conflicting files. - * Source folder will be deleted if the migration is successful. + * @brief Move the contents of source folder to destination one and rename all conflicting files. + * + * Source folder will be deleted if the migration was successful. * - * @param storage - * @param source - * @param dest - * @return FS_Error + * @param storage pointer to a storage API instance. + * @param source pointer to a zero-terminated string containing the source path. + * @param dest pointer to a zero-terminated string containing the destination path. + * @return FSE_OK if the migration was successfull completed, any other error code on failure. */ FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest); /** - * @brief Check that file or dir exists + * @brief Check whether a file or a directory exists. * - * @param storage - * @param path - * @return bool + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the path in question. + * @return true if a file or a directory exists, false otherwise. */ bool storage_common_exists(Storage* storage, const char* path); +/** + * @brief Check whether two paths are equivalent. + * + * This function will resolve aliases and apply filesystem-specific + * rules to determine whether the two given paths are equivalent. + * + * Examples: + * - /int/text and /ext/test -> false (Different storages), + * - /int/Test and /int/test -> false (Case-sensitive storage), + * - /ext/Test and /ext/test -> true (Case-insensitive storage). + * + * If the truncate parameter is set to true, the second path will be + * truncated to be no longer than the first one. It is useful to determine + * whether path2 is a subdirectory of path1. + * + * @param storage pointer to a storage API instance. + * @param path1 pointer to a zero-terminated string containing the first path. + * @param path2 pointer to a zero-terminated string containing the second path. + * @param truncate whether to truncate path2 to be no longer than path1. + * @return true if paths are equivalent, false otherwise. + */ +bool storage_common_equivalent_path( + Storage* storage, + const char* path1, + const char* path2, + bool truncate); + /******************* Error Functions *******************/ -/** Retrieves the error text from the error id - * @param error_id error id - * @return const char* error text +/** + * @brief Get the textual description of a numeric error identifer. + * + * @param error_id numeric identifier of the error in question. + * @return pointer to a statically allocated zero-terminated string containing the respective error text. */ const char* storage_error_get_desc(FS_Error error_id); -/** Retrieves the error id from the file object - * @param file pointer to file object. Pointer must not point to NULL. YOU CANNOT RETRIEVE THE ERROR ID IF THE FILE HAS BEEN CLOSED - * @return FS_Error error id +/** + * @brief Get the numeric error identifier from a file instance. + * + * @warning It is not possible to get the error identifier after the file has been closed. + * + * @param file pointer to the file instance in question (must NOT be NULL). + * @return numeric identifier of the last error associated with the file instance. */ FS_Error storage_file_get_error(File* file); -/** Retrieves the internal (storage-specific) error id from the file object - * @param file pointer to file object. Pointer must not point to NULL. YOU CANNOT RETRIEVE THE INTERNAL ERROR ID IF THE FILE HAS BEEN CLOSED - * @return FS_Error error id +/** + * @brief Get the internal (storage-specific) numeric error identifier from a file instance. + * + * @warning It is not possible to get the internal error identifier after the file has been closed. + * + * @param file pointer to the file instance in question (must NOT be NULL). + * @return numeric identifier of the last internal error associated with the file instance. */ int32_t storage_file_get_internal_error(File* file); -/** Retrieves the error text from the file object - * @param file pointer to file object. Pointer must not point to NULL. YOU CANNOT RETRIEVE THE ERROR TEXT IF THE FILE HAS BEEN CLOSED - * @return const char* error text +/** + * @brief Get the textual description of a the last error associated with a file instance. + * + * @warning It is not possible to get the error text after the file has been closed. + * + * @param file pointer to the file instance in question (must NOT be NULL). + * @return pointer to a statically allocated zero-terminated string containing the respective error text. */ const char* storage_file_get_error_desc(File* file); /******************* SD Card Functions *******************/ -/** Formats SD Card - * @param api pointer to the api - * @return FS_Error operation result +/** + * @brief Format the SD Card. + * + * @param storage pointer to a storage API instance. + * @return FSE_OK if the card was successfully formatted, any other error code on failure. */ -FS_Error storage_sd_format(Storage* api); +FS_Error storage_sd_format(Storage* storage); -/** Will unmount the SD card. - * Will return FSE_NOT_READY if the SD card is not mounted. - * Will return FSE_DENIED if there are open files on the SD card. - * @param api pointer to the api - * @return FS_Error operation result +/** + * @brief Unmount the SD card. + * + * These return values have special meaning: + * - FSE_NOT_READY if the SD card is not mounted. + * - FSE_DENIED if there are open files on the SD card. + * + * @param storage pointer to a storage API instance. + * @return FSE_OK if the card was successfully formatted, any other error code on failure. */ -FS_Error storage_sd_unmount(Storage* api); +FS_Error storage_sd_unmount(Storage* storage); -/** Will mount the SD card - * @param api pointer to the api - * @return FS_Error operation result +/** + * @brief Mount the SD card. + * + * @param storage pointer to a storage API instance. + * @return FSE_OK if the card was successfully mounted, any other error code on failure. */ -FS_Error storage_sd_mount(Storage* api); +FS_Error storage_sd_mount(Storage* storage); -/** Retrieves SD card information - * @param api pointer to the api - * @param info pointer to the info - * @return FS_Error operation result +/** + * @brief Get SD card information. + * + * @param storage pointer to a storage API instance. + * @param info pointer to the info object to contain the requested information. + * @return FSE_OK if the info was successfully received, any other error code on failure. */ -FS_Error storage_sd_info(Storage* api, SDInfo* info); +FS_Error storage_sd_info(Storage* storage, SDInfo* info); -/** Retrieves SD card status - * @param api pointer to the api - * @return FS_Error operation result +/** + * @brief Get SD card status. + * + * @param storage pointer to a storage API instance. + * @return storage status in the form of a numeric error identifier. */ -FS_Error storage_sd_status(Storage* api); +FS_Error storage_sd_status(Storage* storage); /******************* Internal LFS Functions *******************/ typedef void (*Storage_name_converter)(FuriString*); -/** Backs up internal storage to a tar archive - * @param api pointer to the api - * @param dstmane destination archive path - * @return FS_Error operation result +/** + * @brief Back up the internal storage contents to a *.tar archive. + * + * @param storage pointer to a storage API instance. + * @param dstname pointer to a zero-terminated string containing the archive file path. + * @return FSE_OK if the storage was successfully backed up, any other error code on failure. */ -FS_Error storage_int_backup(Storage* api, const char* dstname); +FS_Error storage_int_backup(Storage* storage, const char* dstname); -/** Restores internal storage from a tar archive - * @param api pointer to the api - * @param dstmane archive path - * @param converter pointer to filename conversion function, may be NULL - * @return FS_Error operation result +/** + * @brief Restore the internal storage contents from a *.tar archive. + * + * @param storage pointer to a storage API instance. + * @param dstname pointer to a zero-terminated string containing the archive file path. + * @param converter pointer to a filename conversion function (may be NULL). + * @return FSE_OK if the storage was successfully restored, any other error code on failure. */ FS_Error storage_int_restore(Storage* api, const char* dstname, Storage_name_converter converter); /***************** Simplified Functions ******************/ /** - * Removes a file/directory, the directory must be empty and the file/directory must not be open - * @param storage pointer to the api - * @param path - * @return true on success or if file/dir is not exist + * @brief Remove a file or a directory. + * + * The following conditions must be met: + * - the directory must be empty. + * - the file or the directory must NOT be open. + * + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the item path. + * @return true on success or if the item does not exist, false otherwise. */ bool storage_simply_remove(Storage* storage, const char* path); /** - * Recursively removes a file/directory, the directory can be not empty - * @param storage pointer to the api - * @param path - * @return true on success or if file/dir is not exist + * @brief Recursively remove a file or a directory. + * + * Unlike storage_simply_remove(), the directory does not need to be empty. + * + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the item path. + * @return true on success or if the item does not exist, false otherwise. */ bool storage_simply_remove_recursive(Storage* storage, const char* path); /** - * Creates a directory - * @param storage - * @param path - * @return true on success or if directory is already exist + * @brief Create a directory. + * + * @param storage pointer to a storage API instance. + * @param path pointer to a zero-terminated string containing the directory path. + * @return true on success or if directory does already exist, false otherwise. */ bool storage_simply_mkdir(Storage* storage, const char* path); /** - * @brief Get next free filename. + * @brief Get the next free filename in a directory. + * + * Usage example: + * ```c + * FuriString* file_name = furi_string_alloc(); + * Storage* storage = furi_record_open(RECORD_STORAGE); + * + * storage_get_next_filename(storage, + * "/ext/test", + * "cookies", + * ".yum", + * 20); + * + * furi_record_close(RECORD_STORAGE); + * + * use_file_name(file_name); + * + * furi_string_free(file_name); + * ``` + * Possible file_name values after calling storage_get_next_filename(): + * "cookies", "cookies1", "cookies2", ... etc depending on whether any of + * these files have already existed in the directory. + * + * @note If the resulting next file name length is greater than set by the max_len + * parameter, the original filename will be returned instead. * - * @param storage - * @param dirname - * @param filename - * @param fileextension - * @param nextfilename return name - * @param max_len max len name + * @param storage pointer to a storage API instance. + * @param dirname pointer to a zero-terminated string containing the directory path. + * @param filename pointer to a zero-terminated string containing the file name. + * @param fileextension pointer to a zero-terminated string containing the file extension. + * @param nextfilename pointer to a dynamic string containing the resulting file name. + * @param max_len maximum length of the new name. */ void storage_get_next_filename( Storage* storage, diff --git a/applications/services/storage/storage_external_api.c b/applications/services/storage/storage_external_api.c index ed69b49a57d..1027d43101a 100644 --- a/applications/services/storage/storage_external_api.c +++ b/applications/services/storage/storage_external_api.c @@ -431,17 +431,22 @@ FS_Error storage_common_rename(Storage* storage, const char* old_path, const cha } if(storage_dir_exists(storage, old_path)) { - FuriString* dir_path = furi_string_alloc_set_str(old_path); - if(!furi_string_end_with_str(dir_path, "/")) { - furi_string_cat_str(dir_path, "/"); + // Cannot overwrite a file with a directory + if(storage_file_exists(storage, new_path)) { + error = FSE_INVALID_NAME; + break; } - const char* dir_path_s = furi_string_get_cstr(dir_path); - if(strncmp(new_path, dir_path_s, strlen(dir_path_s)) == 0) { + + // Cannot rename a directory to itself or to a nested directory + if(storage_common_equivalent_path(storage, old_path, new_path, true)) { error = FSE_INVALID_NAME; - furi_string_free(dir_path); break; } - furi_string_free(dir_path); + + // Renaming a regular file to itself does nothing and always succeeds + } else if(storage_common_equivalent_path(storage, old_path, new_path, false)) { + error = FSE_OK; + break; } if(storage_file_exists(storage, new_path)) { @@ -742,6 +747,27 @@ bool storage_common_exists(Storage* storage, const char* path) { return storage_common_stat(storage, path, &file_info) == FSE_OK; } +bool storage_common_equivalent_path( + Storage* storage, + const char* path1, + const char* path2, + bool truncate) { + S_API_PROLOGUE; + + SAData data = { + .cequivpath = { + .path1 = path1, + .path2 = path2, + .truncate = truncate, + .thread_id = furi_thread_get_current_id(), + }}; + + S_API_MESSAGE(StorageCommandCommonEquivalentPath); + S_API_EPILOGUE; + + return S_RETURN_BOOL; +} + /****************** ERROR ******************/ const char* storage_error_get_desc(FS_Error error_id) { diff --git a/applications/services/storage/storage_message.h b/applications/services/storage/storage_message.h index 01bc2038010..cd45906d4ac 100644 --- a/applications/services/storage/storage_message.h +++ b/applications/services/storage/storage_message.h @@ -69,6 +69,13 @@ typedef struct { FuriThreadId thread_id; } SADataCResolvePath; +typedef struct { + const char* path1; + const char* path2; + bool truncate; + FuriThreadId thread_id; +} SADataCEquivPath; + typedef struct { uint32_t id; } SADataError; @@ -99,6 +106,7 @@ typedef union { SADataCStat cstat; SADataCFSInfo cfsinfo; SADataCResolvePath cresolvepath; + SADataCEquivPath cequivpath; SADataError error; @@ -142,6 +150,7 @@ typedef enum { StorageCommandSDStatus, StorageCommandCommonResolvePath, StorageCommandSDMount, + StorageCommandCommonEquivalentPath, } StorageCommand; typedef struct { diff --git a/applications/services/storage/storage_processing.c b/applications/services/storage/storage_processing.c index 00126af6f37..9e96765b624 100644 --- a/applications/services/storage/storage_processing.c +++ b/applications/services/storage/storage_processing.c @@ -98,6 +98,12 @@ static FS_Error storage_get_data(Storage* app, FuriString* path, StorageData** s } } +static void storage_path_trim_trailing_slashes(FuriString* path) { + while(furi_string_end_with(path, "/")) { + furi_string_left(path, furi_string_size(path) - 1); + } +} + /******************* File Functions *******************/ bool storage_process_file_open( @@ -357,6 +363,8 @@ static FS_Error storage_process_common_remove(Storage* app, FuriString* path) { FS_Error ret = storage_get_data(app, path, &storage); do { + if(ret != FSE_OK) break; + if(storage_path_already_open(path, storage)) { ret = FSE_ALREADY_OPEN; break; @@ -398,6 +406,31 @@ static FS_Error storage_process_common_fs_info( return ret; } +static bool + storage_process_common_equivalent_path(Storage* app, FuriString* path1, FuriString* path2) { + bool ret = false; + + do { + const StorageType storage_type1 = storage_get_type_by_path(path1); + const StorageType storage_type2 = storage_get_type_by_path(path2); + + // Paths on different storages are of course not equal + if(storage_type1 != storage_type2) break; + + StorageData* storage; + const FS_Error status = storage_get_data(app, path1, &storage); + + if(status != FSE_OK) break; + + FS_CALL( + storage, + common.equivalent_path(furi_string_get_cstr(path1), furi_string_get_cstr(path2))); + + } while(false); + + return ret; +} + /****************** Raw SD API ******************/ // TODO FL-3521: think about implementing a custom storage API to split that kind of api linkage #include "storages/storage_ext.h" @@ -649,6 +682,23 @@ void storage_process_message_internal(Storage* app, StorageMessage* message) { app, message->data->cresolvepath.path, message->data->cresolvepath.thread_id, true); break; + case StorageCommandCommonEquivalentPath: { + FuriString* path1 = furi_string_alloc_set(message->data->cequivpath.path1); + FuriString* path2 = furi_string_alloc_set(message->data->cequivpath.path2); + storage_path_trim_trailing_slashes(path1); + storage_path_trim_trailing_slashes(path2); + storage_process_alias(app, path1, message->data->cequivpath.thread_id, false); + storage_process_alias(app, path2, message->data->cequivpath.thread_id, false); + if(message->data->cequivpath.truncate) { + furi_string_left(path2, furi_string_size(path1)); + } + message->return_data->bool_value = + storage_process_common_equivalent_path(app, path1, path2); + furi_string_free(path1); + furi_string_free(path2); + break; + } + // SD operations case StorageCommandSDFormat: message->return_data->error_value = storage_process_sd_format(app); diff --git a/applications/services/storage/storages/storage_ext.c b/applications/services/storage/storages/storage_ext.c index 4630d99ea74..7e617c0ff20 100644 --- a/applications/services/storage/storages/storage_ext.c +++ b/applications/services/storage/storages/storage_ext.c @@ -596,6 +596,16 @@ static FS_Error storage_ext_common_fs_info( #endif } +static bool storage_ext_common_equivalent_path(const char* path1, const char* path2) { +#ifdef FURI_RAM_EXEC + UNUSED(path1); + UNUSED(path2); + return false; +#else + return strcasecmp(path1, path2) == 0; +#endif +} + /******************* Init Storage *******************/ static const FS_Api fs_api = { .file = @@ -624,6 +634,7 @@ static const FS_Api fs_api = { .mkdir = storage_ext_common_mkdir, .remove = storage_ext_common_remove, .fs_info = storage_ext_common_fs_info, + .equivalent_path = storage_ext_common_equivalent_path, }, }; diff --git a/applications/services/storage/storages/storage_int.c b/applications/services/storage/storages/storage_int.c index 2534d47a112..39b092c1d19 100644 --- a/applications/services/storage/storages/storage_int.c +++ b/applications/services/storage/storages/storage_int.c @@ -686,6 +686,10 @@ static FS_Error storage_int_common_fs_info( return storage_int_parse_error(result); } +static bool storage_int_common_equivalent_path(const char* path1, const char* path2) { + return strcmp(path1, path2) == 0; +} + /******************* Init Storage *******************/ static const FS_Api fs_api = { .file = @@ -714,6 +718,7 @@ static const FS_Api fs_api = { .mkdir = storage_int_common_mkdir, .remove = storage_int_common_remove, .fs_info = storage_int_common_fs_info, + .equivalent_path = storage_int_common_equivalent_path, }, }; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 44829c264da..8ed8f403c70 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,45.0,, +Version,+,45.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, Header,+,applications/services/cli/cli_vcp.h,, @@ -2071,6 +2071,7 @@ Function,+,st25r3916_write_pttsn_mem,void,"FuriHalSpiBusHandle*, uint8_t*, size_ Function,+,st25r3916_write_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" Function,+,st25r3916_write_test_reg,void,"FuriHalSpiBusHandle*, uint8_t, uint8_t" Function,+,storage_common_copy,FS_Error,"Storage*, const char*, const char*" +Function,+,storage_common_equivalent_path,_Bool,"Storage*, const char*, const char*, _Bool" Function,+,storage_common_exists,_Bool,"Storage*, const char*" Function,+,storage_common_fs_info,FS_Error,"Storage*, const char*, uint64_t*, uint64_t*" Function,+,storage_common_merge,FS_Error,"Storage*, const char*, const char*" diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 514f3328682..64695c2dbc9 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,45.0,, +Version,+,45.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/cli/cli.h,, @@ -2698,6 +2698,7 @@ Function,+,st25tb_save,_Bool,"const St25tbData*, FlipperFormat*" Function,+,st25tb_set_uid,_Bool,"St25tbData*, const uint8_t*, size_t" Function,+,st25tb_verify,_Bool,"St25tbData*, const FuriString*" Function,+,storage_common_copy,FS_Error,"Storage*, const char*, const char*" +Function,+,storage_common_equivalent_path,_Bool,"Storage*, const char*, const char*, _Bool" Function,+,storage_common_exists,_Bool,"Storage*, const char*" Function,+,storage_common_fs_info,FS_Error,"Storage*, const char*, uint64_t*, uint64_t*" Function,+,storage_common_merge,FS_Error,"Storage*, const char*, const char*"