From 6473689e3ef431ede56880facaf6850133deef5c Mon Sep 17 00:00:00 2001 From: Aleksejs Popovs Date: Tue, 20 Jan 2026 20:12:56 -0500 Subject: [PATCH] avoid overwriting on filename conflicts --- .../bluetooth/BluetoothActivity.cpp | 43 +++++++++++-------- src/activities/bluetooth/BluetoothActivity.h | 4 +- src/util/StringUtils.cpp | 8 ++++ src/util/StringUtils.h | 6 +++ 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/activities/bluetooth/BluetoothActivity.cpp b/src/activities/bluetooth/BluetoothActivity.cpp index 96d26db3..b00e5d14 100644 --- a/src/activities/bluetooth/BluetoothActivity.cpp +++ b/src/activities/bluetooth/BluetoothActivity.cpp @@ -4,6 +4,7 @@ #include "MappedInputManager.h" #include "fontIds.h" +#include "util/StringUtils.h" #define DEVICE_NAME "EPaper" #define SERVICE_UUID "4ae29d01-499a-480a-8c41-a82192105125" @@ -11,6 +12,7 @@ #define RESPONSE_CHARACTERISTIC_UUID "0c656023-dee6-47c5-9afb-e601dfbdaa1d" #define OUTPUT_DIRECTORY "/bt" +#define MAX_FILENAME_LENGTH 200 #define PROTOCOL_ASSERT(cond, fmt, ...) \ do { \ @@ -181,7 +183,7 @@ void BluetoothActivity::render() const { renderer.drawCenteredText(UI_10_FONT_ID, 75, stateText.c_str()); if (state == STATE_OFFERED || state == STATE_RECEIVING || state == STATE_DONE) { - renderer.drawCenteredText(UI_12_FONT_ID, 110, filename); + renderer.drawCenteredText(UI_12_FONT_ID, 110, filename.c_str()); } else if (state == STATE_ERROR) { renderer.drawCenteredText(UI_10_FONT_ID, 110, errorMessage); } @@ -249,25 +251,30 @@ void BluetoothActivity::onRequest(lfbt_message* msg, size_t msg_len) { totalBytes = msg->body.clientOffer.bodyLength; - size_t filenameLen = msg_len - 8 - sizeof(lfbt_msg_client_offer); - if (filenameLen > MAX_FILENAME) { - filenameLen = MAX_FILENAME; - } - - memcpy(filename, msg->body.clientOffer.name, filenameLen); - filename[filenameLen] = 0; - - // sanitize filename - for (char *p = filename; *p; p++) { - if (*p == '/' || *p == '\\' || *p == ':') { - *p = '_'; - } - } + size_t filenameLength = msg_len - 8 - sizeof(lfbt_msg_client_offer); + std::string originalFilename = StringUtils::sanitizeFilename( + std::string(msg->body.clientOffer.name, filenameLength), + MAX_FILENAME_LENGTH + ); PROTOCOL_ASSERT(SdMan.ensureDirectoryExists(OUTPUT_DIRECTORY), "Couldn't create output directory %s", OUTPUT_DIRECTORY); - char filepath[MAX_FILENAME + strlen(OUTPUT_DIRECTORY) + 2]; - snprintf(filepath, sizeof(filepath), "%s/%s", OUTPUT_DIRECTORY, filename); - // TODO: we could check if file already exists and append a number to filename to avoid overwriting + + // generate unique filepath + auto splitName = StringUtils::splitFileName(originalFilename); + filename = originalFilename; + std::string filepath = OUTPUT_DIRECTORY "/" + filename; + uint32_t duplicateIndex = 0; + while (SdMan.exists(filepath.c_str())) { + duplicateIndex++; + if (splitName.second.empty()) { + // no extension + filename = splitName.first + "-" + std::to_string(duplicateIndex); + } else { + filename = splitName.first + "-" + std::to_string(duplicateIndex) + splitName.second; + } + filepath = OUTPUT_DIRECTORY "/" + filename; + } + PROTOCOL_ASSERT(SdMan.openFileForWrite("BT", filepath, file), "Couldn't open file %s for writing", filepath); // TODO: would be neat to check if we have enough space, but SDCardManager doesn't seem to expose that info currently diff --git a/src/activities/bluetooth/BluetoothActivity.h b/src/activities/bluetooth/BluetoothActivity.h index 8b7cc1a4..bfb7d6e1 100644 --- a/src/activities/bluetooth/BluetoothActivity.h +++ b/src/activities/bluetooth/BluetoothActivity.h @@ -13,8 +13,6 @@ #include "../Activity.h" -#define MAX_FILENAME 200 - typedef struct __attribute__((packed)) { uint32_t version; uint32_t bodyLength; @@ -104,7 +102,7 @@ class BluetoothActivity final : public Activity { } State; State state = STATE_INITIALIZING; - char filename[MAX_FILENAME + 1]; + std::string filename; FsFile file; size_t receivedBytes = 0; size_t totalBytes = 0; diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp index 2426b687..54ef363f 100644 --- a/src/util/StringUtils.cpp +++ b/src/util/StringUtils.cpp @@ -61,6 +61,14 @@ bool checkFileExtension(const String& fileName, const char* extension) { return localFile.endsWith(localExtension); } +std::pair splitFileName(const std::string& name) { + size_t lastDot = name.find_last_of('.'); + if (lastDot == std::string::npos) { + return std::make_pair(name, ""); + } + return std::make_pair(name.substr(0, lastDot), name.substr(lastDot)); +} + size_t utf8RemoveLastChar(std::string& str) { if (str.empty()) return 0; size_t pos = str.size() - 1; diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index 5c8332f0..7f36554a 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -19,6 +19,12 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100); bool checkFileExtension(const std::string& fileName, const char* extension); bool checkFileExtension(const String& fileName, const char* extension); +/** + * Split a filename into base name and extension. + * If there is no extension, the second element of the pair will be an empty string. + */ +std::pair splitFileName(const std::string& name); + // UTF-8 safe string truncation - removes one character from the end // Returns the new size after removing one UTF-8 character size_t utf8RemoveLastChar(std::string& str);