diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 9db9e2d94..cbeb98d53 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -1,20 +1,18 @@ /** * @file FSCommon.cpp - * @brief This file contains functions for common filesystem operations such as - * copying, renaming, listing and deleting files and directories. + * @brief This file contains functions for common filesystem operations such as copying, renaming, listing and deleting files and + * directories. * - * The functions in this file are used to perform common filesystem operations - * such as copying, renaming, listing and deleting files and directories. These - * functions are used in the Meshtastic-device project to manage files and - * directories on the device's filesystem. + * The functions in this file are used to perform common filesystem operations such as copying, renaming, listing and deleting + * files and directories. These functions are used in the Meshtastic-device project to manage files and directories on the + * device's filesystem. * */ #include "FSCommon.h" #include "SPILock.h" #include "configuration.h" -// Software SPI is used by MUI so disable SD card here until it's also -// implemented +// Software SPI is used by MUI so disable SD card here until it's also implemented #if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) #include #include @@ -39,33 +37,34 @@ SPIClass SPI_HSPI(HSPI); * @param to The path of the destination file. * @return true if the file was successfully copied, false otherwise. */ -bool copyFile(const char *from, const char *to) { +bool copyFile(const char *from, const char *to) +{ #ifdef FSCom - // take SPI Lock - concurrency::LockGuard g(spiLock); - unsigned char cbuffer[16]; + // take SPI Lock + concurrency::LockGuard g(spiLock); + unsigned char cbuffer[16]; - File f1 = FSCom.open(from, FILE_O_READ); - if (!f1) { - LOG_ERROR("Failed to open source file %s", from); - return false; - } + File f1 = FSCom.open(from, FILE_O_READ); + if (!f1) { + LOG_ERROR("Failed to open source file %s", from); + return false; + } - File f2 = FSCom.open(to, FILE_O_WRITE); - if (!f2) { - LOG_ERROR("Failed to open destination file %s", to); - return false; - } + File f2 = FSCom.open(to, FILE_O_WRITE); + if (!f2) { + LOG_ERROR("Failed to open destination file %s", to); + return false; + } - while (f1.available() > 0) { - byte i = f1.read(cbuffer, 16); - f2.write(cbuffer, i); - } + while (f1.available() > 0) { + byte i = f1.read(cbuffer, 16); + f2.write(cbuffer, i); + } - f2.flush(); - f2.close(); - f1.close(); - return true; + f2.flush(); + f2.close(); + f1.close(); + return true; #endif } @@ -77,191 +76,258 @@ bool copyFile(const char *from, const char *to) { * * @return True if the file was successfully renamed, false otherwise. */ -bool renameFile(const char *pathFrom, const char *pathTo) { +bool renameFile(const char *pathFrom, const char *pathTo) +{ #ifdef FSCom #ifdef ARCH_ESP32 - // take SPI Lock - spiLock->lock(); - // rename was fixed for ESP32 IDF LittleFS in April - bool result = FSCom.rename(pathFrom, pathTo); - spiLock->unlock(); - return result; + // take SPI Lock + spiLock->lock(); + // rename was fixed for ESP32 IDF LittleFS in April + bool result = FSCom.rename(pathFrom, pathTo); + spiLock->unlock(); + return result; #else - // copyFile does its own locking. - if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { - return true; - } else { - return false; - } + // copyFile does its own locking. + if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) { + return true; + } else { + return false; + } #endif #endif } +#include +#include +#include #include -/** - * @brief Get the list of files in a directory. - * - * This function returns a list of files in a directory. The list includes the - * full path of each file. We can't use SPILOCK here because of recursion. - * Callers of this function should use SPILOCK. - * - * @param dirname The name of the directory. - * @param levels The number of levels of subdirectories to list. - * @return A vector of strings containing the full path of each file in the - * directory. - */ -std::vector getFiles(const char *dirname, uint8_t levels) { - std::vector filenames = {}; #ifdef FSCom - File root = FSCom.open(dirname, FILE_O_READ); - if (!root) - return filenames; - if (!root.isDirectory()) - return filenames; +namespace +{ +bool pathEndsWithDot(const char *path) +{ + if (!path) + return false; - File file = root.openNextFile(); - while (file && - file.name()[0]) { // file.name()[0] check is a workaround for a bug in - // the Adafruit LittleFS nrf52 glue (see issue 4395) - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { -#ifdef ARCH_ESP32 - std::vector subDirFilenames = - getFiles(file.path(), levels - 1); -#else - std::vector subDirFilenames = - getFiles(file.name(), levels - 1); -#endif - filenames.insert(filenames.end(), subDirFilenames.begin(), - subDirFilenames.end()); - file.close(); - } - } else { - meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; -#ifdef ARCH_ESP32 - strcpy(fileInfo.file_name, file.path()); -#else - strcpy(fileInfo.file_name, file.name()); -#endif - if (!String(fileInfo.file_name).endsWith(".")) { - filenames.push_back(fileInfo); - } - file.close(); + size_t length = strlen(path); + return length > 0 && path[length - 1] == '.'; +} + +bool copyFilePath(char *dest, size_t destSize, const char *path, bool *wasLimited) +{ + if (!path || destSize == 0) { + if (wasLimited) + *wasLimited = true; + return false; } - file = root.openNextFile(); - } - root.close(); + + if (strlcpy(dest, path, destSize) >= destSize) { + if (wasLimited) + *wasLimited = true; + return false; + } + + return true; +} + +void collectFiles(const char *dirname, uint8_t levels, size_t maxCount, std::vector &filenames, + bool *wasLimited) +{ + if (!dirname) + return; + + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) + return; + if (!root.isDirectory()) { + root.close(); + return; + } + + File file = root.openNextFile(); + // file.name()[0] check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395) + while (file && file.name()[0]) { + if (filenames.size() >= maxCount) { + if (wasLimited) + *wasLimited = true; + file.close(); + break; + } + const char *fileName = file.name(); + if (file.isDirectory() && !pathEndsWithDot(fileName)) { + char pathBuffer[sizeof(((meshtastic_FileInfo *)nullptr)->file_name)] = {}; +#ifdef ARCH_ESP32 + const char *subDirPath = file.path(); +#else + const char *subDirPath = fileName; #endif - return filenames; + bool hasSubDirPath = copyFilePath(pathBuffer, sizeof(pathBuffer), subDirPath, wasLimited); + file.close(); + + if (levels && hasSubDirPath) { + collectFiles(pathBuffer, levels - 1, maxCount, filenames, wasLimited); + } else if (wasLimited) { + *wasLimited = true; + } + } else { + meshtastic_FileInfo fileInfo = {"", static_cast(file.size())}; +#ifdef ARCH_ESP32 + bool hasFilePath = copyFilePath(fileInfo.file_name, sizeof(fileInfo.file_name), file.path(), wasLimited); +#else + bool hasFilePath = copyFilePath(fileInfo.file_name, sizeof(fileInfo.file_name), file.name(), wasLimited); +#endif + if (hasFilePath && !pathEndsWithDot(fileInfo.file_name)) { + filenames.push_back(fileInfo); + } + file.close(); + } + file = root.openNextFile(); + } + root.close(); +} +} // namespace +#endif + +// Callers must hold the SPI lock; recursion prevents taking it here. +std::vector getFiles(const char *dirname, uint8_t levels, size_t maxCount, bool *wasLimited) +{ + std::vector filenames = {}; + if (wasLimited) + *wasLimited = false; +#ifdef FSCom +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) + size_t reservedCount = maxCount; + while (reservedCount > 0) { + try { + filenames.reserve(reservedCount); + break; + } catch (const std::bad_alloc &) { + reservedCount /= 2; + } catch (const std::length_error &) { + reservedCount /= 2; + } + } + if (reservedCount == 0) { + if (wasLimited) + *wasLimited = true; + return filenames; + } + if (reservedCount < maxCount) { + if (wasLimited) + *wasLimited = true; + maxCount = reservedCount; + } +#endif + collectFiles(dirname, levels, maxCount, filenames, wasLimited); +#endif + return filenames; } /** * Lists the contents of a directory. - * We can't use SPILOCK here because of recursion. Callers of this function - * should use SPILOCK. + * We can't use SPILOCK here because of recursion. Callers of this function should use SPILOCK. * * @param dirname The name of the directory to list. * @param levels The number of levels of subdirectories to list. - * @param del Whether or not to delete the contents of the directory after - * listing. + * @param del Whether or not to delete the contents of the directory after listing. */ -void listDir(const char *dirname, uint8_t levels, bool del) { +void listDir(const char *dirname, uint8_t levels, bool del) +{ #ifdef FSCom #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - char buffer[255]; -#endif - File root = FSCom.open(dirname, FILE_O_READ); - if (!root) { - return; - } - if (!root.isDirectory()) { - return; - } - - File file = root.openNextFile(); - while ( - file && - file.name()[0]) { // This file.name() check is a workaround for a bug in - // the Adafruit LittleFS nrf52 glue (see issue 4395) - if (file.isDirectory() && !String(file.name()).endsWith(".")) { - if (levels) { -#ifdef ARCH_ESP32 - listDir(file.path(), levels - 1, del); - if (del) { - LOG_DEBUG("Remove %s", file.path()); - strncpy(buffer, file.path(), sizeof(buffer)); - file.close(); - FSCom.rmdir(buffer); - } else { - file.close(); - } -#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - listDir(file.name(), levels - 1, del); - if (del) { - LOG_DEBUG("Remove %s", file.name()); - strncpy(buffer, file.name(), sizeof(buffer)); - file.close(); - FSCom.rmdir(buffer); - } else { - file.close(); - } -#else - LOG_DEBUG(" %s (directory)", file.name()); - listDir(file.name(), levels - 1, del); - file.close(); -#endif - } - } else { -#ifdef ARCH_ESP32 - if (del) { - LOG_DEBUG("Delete %s", file.path()); - strncpy(buffer, file.path(), sizeof(buffer)); - file.close(); - FSCom.remove(buffer); - } else { - LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); - file.close(); - } -#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - if (del) { - LOG_DEBUG("Delete %s", file.name()); - strncpy(buffer, file.name(), sizeof(buffer)); - file.close(); - FSCom.remove(buffer); - } else { - LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); - file.close(); - } -#else - LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); - file.close(); + char buffer[255]; #endif + File root = FSCom.open(dirname, FILE_O_READ); + if (!root) { + return; } - file = root.openNextFile(); - } + if (!root.isDirectory()) { + return; + } + + File file = root.openNextFile(); + while ( + file && + file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395) + if (file.isDirectory() && !String(file.name()).endsWith(".")) { + if (levels) { #ifdef ARCH_ESP32 - if (del) { - LOG_DEBUG("Remove %s", root.path()); - strncpy(buffer, root.path(), sizeof(buffer)); - root.close(); - FSCom.rmdir(buffer); - } else { - root.close(); - } + listDir(file.path(), levels - 1, del); + if (del) { + LOG_DEBUG("Remove %s", file.path()); + strncpy(buffer, file.path(), sizeof(buffer)); + file.close(); + FSCom.rmdir(buffer); + } else { + file.close(); + } #elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - if (del) { - LOG_DEBUG("Remove %s", root.name()); - strncpy(buffer, root.name(), sizeof(buffer)); - root.close(); - FSCom.rmdir(buffer); - } else { - root.close(); - } + listDir(file.name(), levels - 1, del); + if (del) { + LOG_DEBUG("Remove %s", file.name()); + strncpy(buffer, file.name(), sizeof(buffer)); + file.close(); + FSCom.rmdir(buffer); + } else { + file.close(); + } #else - root.close(); + LOG_DEBUG(" %s (directory)", file.name()); + listDir(file.name(), levels - 1, del); + file.close(); +#endif + } + } else { +#ifdef ARCH_ESP32 + if (del) { + LOG_DEBUG("Delete %s", file.path()); + strncpy(buffer, file.path(), sizeof(buffer)); + file.close(); + FSCom.remove(buffer); + } else { + LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size()); + file.close(); + } +#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + if (del) { + LOG_DEBUG("Delete %s", file.name()); + strncpy(buffer, file.name(), sizeof(buffer)); + file.close(); + FSCom.remove(buffer); + } else { + LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); + file.close(); + } +#else + LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size()); + file.close(); +#endif + } + file = root.openNextFile(); + } +#ifdef ARCH_ESP32 + if (del) { + LOG_DEBUG("Remove %s", root.path()); + strncpy(buffer, root.path(), sizeof(buffer)); + root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) + if (del) { + LOG_DEBUG("Remove %s", root.name()); + strncpy(buffer, root.name(), sizeof(buffer)); + root.close(); + FSCom.rmdir(buffer); + } else { + root.close(); + } +#else + root.close(); #endif #endif } @@ -269,19 +335,19 @@ void listDir(const char *dirname, uint8_t levels, bool del) { /** * @brief Removes a directory and all its contents. * - * This function recursively removes a directory and all its contents, including - * subdirectories and files. + * This function recursively removes a directory and all its contents, including subdirectories and files. * * @param dirname The name of the directory to remove. */ -void rmDir(const char *dirname) { +void rmDir(const char *dirname) +{ #ifdef FSCom #if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO)) - listDir(dirname, 10, true); + listDir(dirname, 10, true); #elif defined(ARCH_NRF52) - // nRF52 implementation of LittleFS has a recursive delete function - FSCom.rmdir_r(dirname); + // nRF52 implementation of LittleFS has a recursive delete function + FSCom.rmdir_r(dirname); #endif #endif @@ -292,54 +358,55 @@ void rmDir(const char *dirname) { */ __attribute__((weak, noinline)) void preFSBegin() {} -void fsInit() { +void fsInit() +{ #ifdef FSCom - concurrency::LockGuard g(spiLock); - preFSBegin(); - if (!FSBegin()) { - LOG_ERROR("Filesystem mount failed"); - // assert(0); This auto-formats the partition, so no need to fail here. - } + concurrency::LockGuard g(spiLock); + preFSBegin(); + if (!FSBegin()) { + LOG_ERROR("Filesystem mount failed"); + // assert(0); This auto-formats the partition, so no need to fail here. + } #if defined(ARCH_ESP32) - LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), - FSCom.totalBytes()); + LOG_DEBUG("Filesystem files (%d/%d Bytes):", FSCom.usedBytes(), FSCom.totalBytes()); #else - LOG_DEBUG("Filesystem files:"); + LOG_DEBUG("Filesystem files:"); #endif - listDir("/", 10); + listDir("/", 10); #endif } /** * Initializes the SD card and mounts the file system. */ -void setupSDCard() { +void setupSDCard() +{ #if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) - concurrency::LockGuard g(spiLock); - SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); - if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { - LOG_DEBUG("No SD_MMC card detected"); - return; - } - uint8_t cardType = SD.cardType(); - if (cardType == CARD_NONE) { - LOG_DEBUG("No SD_MMC card attached"); - return; - } - LOG_DEBUG("SD_MMC Card Type: "); - if (cardType == CARD_MMC) { - LOG_DEBUG("MMC"); - } else if (cardType == CARD_SD) { - LOG_DEBUG("SDSC"); - } else if (cardType == CARD_SDHC) { - LOG_DEBUG("SDHC"); - } else { - LOG_DEBUG("UNKNOWN"); - } + concurrency::LockGuard g(spiLock); + SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) { + LOG_DEBUG("No SD_MMC card detected"); + return; + } + uint8_t cardType = SD.cardType(); + if (cardType == CARD_NONE) { + LOG_DEBUG("No SD_MMC card attached"); + return; + } + LOG_DEBUG("SD_MMC Card Type: "); + if (cardType == CARD_MMC) { + LOG_DEBUG("MMC"); + } else if (cardType == CARD_SD) { + LOG_DEBUG("SDSC"); + } else if (cardType == CARD_SDHC) { + LOG_DEBUG("SDHC"); + } else { + LOG_DEBUG("UNKNOWN"); + } - uint64_t cardSize = SD.cardSize() / (1024 * 1024); - LOG_DEBUG("SD Card Size: %lu MB", (uint32_t)cardSize); - LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); - LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); + uint64_t cardSize = SD.cardSize() / (1024 * 1024); + LOG_DEBUG("SD Card Size: %lu MB", (uint32_t)cardSize); + LOG_DEBUG("Total space: %lu MB", (uint32_t)(SD.totalBytes() / (1024 * 1024))); + LOG_DEBUG("Used space: %lu MB", (uint32_t)(SD.usedBytes() / (1024 * 1024))); #endif -} \ No newline at end of file +} diff --git a/src/FSCommon.h b/src/FSCommon.h index fdc0b76ec..c85c07962 100644 --- a/src/FSCommon.h +++ b/src/FSCommon.h @@ -52,7 +52,7 @@ void fsInit(); void fsListFiles(); bool copyFile(const char *from, const char *to); bool renameFile(const char *pathFrom, const char *pathTo); -std::vector getFiles(const char *dirname, uint8_t levels); +std::vector getFiles(const char *dirname, uint8_t levels, size_t maxCount = 64, bool *wasLimited = nullptr); void listDir(const char *dirname, uint8_t levels, bool del = false); void rmDir(const char *dirname); -void setupSDCard(); \ No newline at end of file +void setupSDCard(); diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 9ff27d33e..6c2639cb1 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -36,6 +36,17 @@ // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; +namespace +{ +constexpr uint8_t FILES_MANIFEST_LEVELS = 3; +constexpr size_t FILES_MANIFEST_MAX_COUNT = 64; + +void releaseFilesManifest(std::vector &filesManifest) +{ + std::vector().swap(filesManifest); +} +} // namespace + PhoneAPI::PhoneAPI() { lastContactMsec = millis(); @@ -70,10 +81,23 @@ void PhoneAPI::handleStartConfig() state = STATE_SEND_MY_INFO; } pauseBluetoothLogging = true; - spiLock->lock(); - filesManifest = getFiles("/", 10); - spiLock->unlock(); - LOG_DEBUG("Got %d files in manifest", filesManifest.size()); + // Manifest is never read on the node-info-only path (STATE_SEND_FILEMANIFEST + // short-circuits to sendConfigComplete), so skip the SPI lock + FS walk. + if (config_nonce != SPECIAL_NONCE_ONLY_NODES) { + bool filesManifestLimited = false; + { + concurrency::LockGuard guard(spiLock); + filesManifest = getFiles("/", FILES_MANIFEST_LEVELS, FILES_MANIFEST_MAX_COUNT, &filesManifestLimited); + } + if (filesManifestLimited) { + LOG_WARN("Got %zu files in manifest (limited to %zu entries/depth %u)", filesManifest.size(), + FILES_MANIFEST_MAX_COUNT, static_cast(FILES_MANIFEST_LEVELS)); + } else { + LOG_DEBUG("Got %zu files in manifest", filesManifest.size()); + } + } else { + releaseFilesManifest(filesManifest); + } LOG_INFO("Start API client config millis=%u", millis()); // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. @@ -122,8 +146,7 @@ void PhoneAPI::close() nodeInfoQueue.clear(); } packetForPhone = NULL; - filesManifest.clear(); - filesManifest.shrink_to_fit(); + releaseFilesManifest(filesManifest); lastPortNumToRadio.clear(); fromRadioNum = 0; config_nonce = 0; @@ -532,7 +555,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest config_state = 0; - filesManifest.clear(); + releaseFilesManifest(filesManifest); // Skip to complete packet sendConfigComplete(); } else {