diff --git a/radio/src/gui/colorlcd/libui/file_browser.cpp b/radio/src/gui/colorlcd/libui/file_browser.cpp index 82656e9c01e..ce806508e64 100644 --- a/radio/src/gui/colorlcd/libui/file_browser.cpp +++ b/radio/src/gui/colorlcd/libui/file_browser.cpp @@ -22,6 +22,7 @@ #include "file_browser.h" #include "lib_file.h" #include "fonts.h" +#include "sdcard.h" #include #include @@ -29,22 +30,6 @@ #define CELL_CTRL_DIR LV_TABLE_CELL_CTRL_CUSTOM_1 #define CELL_CTRL_FILE LV_TABLE_CELL_CTRL_CUSTOM_2 -static const char* getFullPath(const char* filename) -{ - static char full_path[FF_MAX_LFN + 1]; - f_getcwd((TCHAR*)full_path, FF_MAX_LFN); - strcat(full_path, "/"); - strcat(full_path, filename); - return full_path; -} - -static const char* getCurrentPath() -{ - static char path[FF_MAX_LFN + 1]; - f_getcwd((TCHAR*)path, FF_MAX_LFN); - return path; -} - static int strnatcasecmp(char const *s1, char const *s2) { int i1, i2; @@ -104,7 +89,7 @@ static bool natural_compare_nocase(const std::string & first, const std::string return strnatcasecmp(first.c_str(), second.c_str()) < 0; } -static int scan_files(std::list& files, +int FileBrowser::scan_files(std::list& files, std::list& directories) { FILINFO fno; @@ -116,14 +101,18 @@ static int scan_files(std::list& files, // read all entries bool firstTime = true; for (;;) { - res = sdReadDir(&dir, &fno, firstTime); + if (firstTime && currentPath != ROOT_PATH) { + strcpy(fno.fname, ".."); + fno.fattrib = AM_DIR; + } else { + res = f_readdir(&dir, &fno); - if (res != FR_OK || fno.fname[0] == 0) - break; // Break on error or end of dir - // if (strlen((const char*)fno.fname) > SD_SCREEN_FILE_LENGTH) - // continue; - if (fno.fattrib & (AM_HID|AM_SYS)) continue; /* Ignore hidden and system files */ - if (fno.fname[0] == '.' && fno.fname[1] != '.') continue; // Ignore hidden files under UNIX, but not .. + if (res != FR_OK || fno.fname[0] == 0) + break; // Break on error or end of dir + if (fno.fattrib & (AM_HID|AM_SYS)) continue; /* Ignore hidden and system files */ + if (fno.fname[0] == '.') continue; // Ignore hidden files under UNIX + } + firstTime = false; if (fno.fattrib & AM_DIR) { directories.push_back((char*)fno.fname); @@ -138,10 +127,25 @@ static int scan_files(std::list& files, return 0; } +void FileBrowser::setFullPath(const char* name) +{ + if (currentPath.back() == '/') + fullPathBuf = currentPath + name; + else + fullPathBuf = currentPath + "/" + name; +} + FileBrowser::FileBrowser(Window* parent, const rect_t& rect, const char* dir) : - TableField(parent, rect) + TableField(parent, rect), currentPath((dir && dir[0]) ? dir : "/") { - f_chdir(dir); + // Normalize once: drop any trailing slash (except root) so the join/parent + // logic never has to deal with it later. + while (currentPath.size() > 1 && currentPath.back() == '/') currentPath.pop_back(); + + if (etxChdir(currentPath.c_str()) != FR_OK) { + currentPath = ROOT_PATH; // keep currentPath in sync with the FatFs CWD + etxChdir(currentPath.c_str()); + } setAutoEdit(); @@ -244,18 +248,25 @@ void FileBrowser::onSelected(const char* name, bool is_dir) return; } - const char* path = getCurrentPath(); - const char* fullpath = getFullPath(name); - if (fileSelected) fileSelected(path, name, fullpath, is_dir); + setFullPath(name); + if (fileSelected) fileSelected(currentPath.c_str(), name, fullPathBuf.c_str(), is_dir); selected = name; } void FileBrowser::onPress(const char* name, bool is_dir) { - const char* path = getCurrentPath(); - const char* fullpath = getFullPath(name); if (is_dir) { - f_chdir(fullpath); + std::string nextPath = currentPath; + if (strcmp(name, "..") == 0) { + // Go up: trim last segment (f_chdir("..") / f_getcwd are no-ops on exFAT). + auto pos = nextPath.find_last_of('/'); + nextPath = (pos == 0 || pos == std::string::npos) ? "/" : nextPath.substr(0, pos); + } else { + setFullPath(name); + nextPath = fullPathBuf; + } + // Only commit the tracked path if the CWD actually changed. + if (etxChdir(nextPath.c_str()) == FR_OK) currentPath = nextPath; if (fileSelected) fileSelected(nullptr, nullptr, nullptr, is_dir); selected = nullptr; refresh(); @@ -268,20 +279,22 @@ void FileBrowser::onPress(const char* name, bool is_dir) } if (fileAction){ - fileAction(path, name, fullpath, is_dir); + setFullPath(name); + fileAction(currentPath.c_str(), name, fullPathBuf.c_str(), is_dir); } } void FileBrowser::onPressLong(const char* name, bool is_dir) { - const char* path = getCurrentPath(); - const char* fullpath = getFullPath(name); + // the synthetic parent-directory row is navigation-only + if (strcmp(name, "..") == 0) return; if (!selected || (selected != name)) { onSelected(name, is_dir); } if (fileAction){ - fileAction(path, name, fullpath, is_dir); + setFullPath(name); + fileAction(currentPath.c_str(), name, fullPathBuf.c_str(), is_dir); } } diff --git a/radio/src/gui/colorlcd/libui/file_browser.h b/radio/src/gui/colorlcd/libui/file_browser.h index e968a5f9fb9..6ca4fec5866 100644 --- a/radio/src/gui/colorlcd/libui/file_browser.h +++ b/radio/src/gui/colorlcd/libui/file_browser.h @@ -21,6 +21,8 @@ #pragma once +#include + #include "table.h" class FileBrowser : public TableField @@ -52,6 +54,15 @@ class FileBrowser : public TableField void onPressLong(const char* name, bool is_dir); private: + // Current dir, tracked explicitly because f_getcwd() is broken on exFAT. + std::string currentPath; + // Outlives the file-action call: callbacks capture the pointer for later use. + std::string fullPathBuf; + void setFullPath(const char* name); + + int scan_files(std::list& files, + std::list& directories); + const char* selected = nullptr; FileAction fileAction; FileAction fileSelected; diff --git a/radio/src/gui/colorlcd/libui/filechoice.cpp b/radio/src/gui/colorlcd/libui/filechoice.cpp index f3a47b3f7a6..d88dcc24dba 100644 --- a/radio/src/gui/colorlcd/libui/filechoice.cpp +++ b/radio/src/gui/colorlcd/libui/filechoice.cpp @@ -127,15 +127,14 @@ void FileChoice::loadFiles() FRESULT res = f_opendir(&dir, folder.c_str()); // Open the directory if (res == FR_OK) { - bool firstTime = true; for (;;) { - res = sdReadDir(&dir, &fno, firstTime); + res = f_readdir(&dir, &fno); if (res != FR_OK || fno.fname[0] == 0) break; // break on error or end of dir if (fno.fattrib & (AM_HID | AM_SYS | AM_DIR)) continue; // Ignore subfolders, hidden files and system files - if (fno.fname[0] == '.' && fno.fname[1] != '.') - continue; // Ignore hidden files under UNIX, but not .. + if (fno.fname[0] == '.') + continue; // Ignore hidden files under UNIX fnExt = getFileExtension(fno.fname, 0, 0, &fnLen, &extLen); diff --git a/radio/src/gui/colorlcd/radio/radio_sdmanager.cpp b/radio/src/gui/colorlcd/radio/radio_sdmanager.cpp index 611008772a1..a89ed07bb4a 100644 --- a/radio/src/gui/colorlcd/radio/radio_sdmanager.cpp +++ b/radio/src/gui/colorlcd/radio/radio_sdmanager.cpp @@ -467,14 +467,19 @@ void RadioSdManagerPage::fileAction(const char* path, const char* name, } menu->addLine(STR_COPY_FILE, [=]() { clipboard.type = CLIPBOARD_TYPE_SD_FILE; - f_getcwd(clipboard.data.sd.directory, CLIPBOARD_PATH_LEN); + // f_getcwd() is a no-op on exFAT; use the tracked path. + strncpy(clipboard.data.sd.directory, path, CLIPBOARD_PATH_LEN - 1); + clipboard.data.sd.directory[CLIPBOARD_PATH_LEN - 1] = '\0'; strncpy(clipboard.data.sd.filename, name, CLIPBOARD_PATH_LEN - 1); + clipboard.data.sd.filename[CLIPBOARD_PATH_LEN - 1] = '\0'; }); if (clipboard.type == CLIPBOARD_TYPE_SD_FILE) { menu->addLine(STR_PASTE, [=]() { static char lfn[FF_MAX_LFN + 1]; // TODO optimize that! char destFileName[2 * CLIPBOARD_PATH_LEN + 1]; - f_getcwd((TCHAR*)lfn, FF_MAX_LFN); + // f_getcwd() is a no-op on exFAT; use the tracked path. + strncpy(lfn, path, FF_MAX_LFN); + lfn[FF_MAX_LFN] = '\0'; // prevent copying to the same directory with the same name char* destNamePtr = clipboard.data.sd.filename; if (!strcmp(clipboard.data.sd.directory, lfn)) { diff --git a/radio/src/gui/colorlcd/radio/radio_tools.cpp b/radio/src/gui/colorlcd/radio/radio_tools.cpp index 782b6b43e8e..4a8536922d3 100644 --- a/radio/src/gui/colorlcd/radio/radio_tools.cpp +++ b/radio/src/gui/colorlcd/radio/radio_tools.cpp @@ -50,7 +50,7 @@ static void run_lua_tool(const std::string& path) char toolPath[FF_MAX_LFN + 1]; strncpy(toolPath, path.c_str(), sizeof(toolPath) - 1); *((char*)getBasename(toolPath) - 1) = '\0'; - f_chdir(toolPath); + etxChdir(toolPath); luaExecStandalone(path.c_str()); } diff --git a/radio/src/gui/colorlcd/themes/theme_manager.cpp b/radio/src/gui/colorlcd/themes/theme_manager.cpp index 0bea14b4e2c..6700a6300b0 100644 --- a/radio/src/gui/colorlcd/themes/theme_manager.cpp +++ b/radio/src/gui/colorlcd/themes/theme_manager.cpp @@ -319,16 +319,19 @@ void ThemePersistance::scanForThemes() if (res == FR_OK) { TRACE("scanForThemes: open successful"); // read all entries - bool firstTime = true; for (;;) { - res = sdReadDir(&dir, &fno, firstTime); + res = f_readdir(&dir, &fno); if (res != FR_OK || fno.fname[0] == 0) break; // Break on error or end of dir - if (strlen((const char*)fno.fname) > SD_SCREEN_FILE_LENGTH) continue; - if (fno.fattrib & AM_DIR) - scanThemeFolder(fno.fname); + if (fno.fattrib & (AM_HID | AM_SYS)) continue; // skip hidden/system + if (fno.fname[0] == '.') continue; // skip dot entries + + if (fno.fattrib & AM_DIR) { + if (strlen((const char*)fno.fname) <= SD_SCREEN_FILE_LENGTH) + scanThemeFolder(fno.fname); + } } f_closedir(&dir); diff --git a/radio/src/gui/common/stdlcd/radio_sdmanager.cpp b/radio/src/gui/common/stdlcd/radio_sdmanager.cpp index 4890cf004d2..f1f591d8cd8 100644 --- a/radio/src/gui/common/stdlcd/radio_sdmanager.cpp +++ b/radio/src/gui/common/stdlcd/radio_sdmanager.cpp @@ -61,11 +61,31 @@ inline bool isFilenameLower(bool isfile, const char * fn, const char * line) return (!isfile && IS_FILE(line)) || (isfile==IS_FILE(line) && strcasecmp(fn, line) < 0); } +// Current dir, tracked explicitly: on exFAT f_getcwd() and f_chdir("..") are no-ops. +static char sdManagerPath[FF_MAX_LFN + 1] = "/"; + +static void sdManagerChdir(const char* name) +{ + if (!strcmp(name, "..")) { + // go up: drop the last path segment + char* sep = strrchr(sdManagerPath, '/'); + if (sep == sdManagerPath) sep[1] = '\0'; // parent is the root + else if (sep) *sep = '\0'; + } else { + // descend into 'name' (bounded, no duplicate slash at the root) + size_t len = strlen(sdManagerPath); + if (strcmp(sdManagerPath, ROOT_PATH) != 0 && len + 1 < sizeof(sdManagerPath)) + sdManagerPath[len++] = '/'; + strncpy(sdManagerPath + len, name, sizeof(sdManagerPath) - len - 1); + sdManagerPath[sizeof(sdManagerPath) - 1] = '\0'; + } + etxChdir(sdManagerPath); // absolute path: resolves on FAT and exFAT alike +} + void getSelectionFullPath(char * lfn) { - f_getcwd(lfn, FF_MAX_LFN); - strcat(lfn, "/"); - strcat(lfn, reusableBuffer.sdManager.lines[menuVerticalPosition - HEADER_LINE - menuVerticalOffset]); + snprintf(lfn, FF_MAX_LFN + 1, "%s/%s", sdManagerPath, + reusableBuffer.sdManager.lines[menuVerticalPosition - HEADER_LINE - menuVerticalOffset]); } #if defined(PXX2) @@ -121,12 +141,14 @@ void onSdManagerMenu(const char * result) } else if (result == STR_COPY_FILE) { clipboard.type = CLIPBOARD_TYPE_SD_FILE; - f_getcwd(clipboard.data.sd.directory, CLIPBOARD_PATH_LEN); + strncpy(clipboard.data.sd.directory, sdManagerPath, CLIPBOARD_PATH_LEN - 1); + clipboard.data.sd.directory[CLIPBOARD_PATH_LEN - 1] = '\0'; strncpy(clipboard.data.sd.filename, line, CLIPBOARD_PATH_LEN-1); + clipboard.data.sd.filename[CLIPBOARD_PATH_LEN - 1] = '\0'; } else if (result == STR_PASTE) { char destFileName[2 * CLIPBOARD_PATH_LEN + 1]; - f_getcwd(lfn, FF_MAX_LFN); + strcpy(lfn, sdManagerPath); // if destination is dir, copy into that dir if (IS_DIRECTORY(line)) { strcat(lfn, "/"); @@ -278,7 +300,8 @@ void menuRadioSdManager(event_t _event) #endif if (_event == EVT_ENTRY) { - f_chdir(ROOT_PATH); + etxChdir(ROOT_PATH); + strcpy(sdManagerPath, ROOT_PATH); #if LCD_DEPTH > 1 lastPos = -1; #endif @@ -325,7 +348,7 @@ void menuRadioSdManager(event_t _event) else { int index = menuVerticalPosition - HEADER_LINE - menuVerticalOffset; if (IS_DIRECTORY(reusableBuffer.sdManager.lines[index])) { - f_chdir(reusableBuffer.sdManager.lines[index]); + sdManagerChdir(reusableBuffer.sdManager.lines[index]); menuVerticalOffset = 0; menuVerticalPosition = HEADER_LINE; REFRESH_FILES(); @@ -483,11 +506,17 @@ void menuRadioSdManager(event_t _event) if (res == FR_OK) { bool firstTime = true; for (;;) { - res = sdReadDir(&dir, &fno, firstTime); - if (res != FR_OK || fno.fname[0] == 0) break; /* Break on error or end of dir */ - if (strlen(fno.fname) > SD_SCREEN_FILE_LENGTH) continue; - if (fno.fattrib & (AM_HID|AM_SYS)) continue; /* Ignore hidden and system files */ - if (fno.fname[0] == '.' && fno.fname[1] != '.') continue; /* Ignore UNIX hidden files, but not .. */ + if (firstTime && (strcmp(sdManagerPath, ROOT_PATH) != 0)) { + strcpy(fno.fname, ".."); + fno.fattrib = AM_DIR; + } else { + res = f_readdir(&dir, &fno); + if (res != FR_OK || fno.fname[0] == 0) break; /* Break on error or end of dir */ + if (strlen(fno.fname) > SD_SCREEN_FILE_LENGTH) continue; + if (fno.fattrib & (AM_HID|AM_SYS)) continue; /* Ignore hidden and system files */ + if (fno.fname[0] == '.') continue; /* Ignore UNIX hidden files */ + } + firstTime = false; reusableBuffer.sdManager.count++; diff --git a/radio/src/gui/common/stdlcd/radio_tools.cpp b/radio/src/gui/common/stdlcd/radio_tools.cpp index 9ffbc85dc08..dec3cd325d2 100644 --- a/radio/src/gui/common/stdlcd/radio_tools.cpp +++ b/radio/src/gui/common/stdlcd/radio_tools.cpp @@ -58,7 +58,7 @@ static void displayRadioTool(uint8_t index) pushMenu(reusableBuffer.radioTools.script[index - menuVerticalOffset].tool); } else if (reusableBuffer.radioTools.script[index - menuVerticalOffset].filename[0]) { - f_chdir(SCRIPTS_TOOLS_PATH); + etxChdir(SCRIPTS_TOOLS_PATH); char path[FF_MAX_LFN + 1] = SCRIPTS_TOOLS_PATH "/"; strcat(path, reusableBuffer.radioTools.script[index - menuVerticalOffset].filename); luaExec(path); diff --git a/radio/src/lib_file.cpp b/radio/src/lib_file.cpp index ec13d6b58b4..cdafe7fd41e 100644 --- a/radio/src/lib_file.cpp +++ b/radio/src/lib_file.cpp @@ -83,38 +83,73 @@ bool isExtensionMatching(const char * extension, const char * pattern, char * ma return false; } -// returns true if current working dir is at the root level -bool isCwdAtRoot() +#if !defined(BOOT) + +// CWD tracker + path normalizer (see lib_file.h). + +static char s_currentDir[FF_MAX_LFN + 1] = "/"; + +void etxNormalizePath(const char* in, char* out, size_t outLen) { - char path[10]; - if (f_getcwd(path, sizeof(path)-1) == FR_OK) { - return (strcasecmp("/", path) == 0); + if (outLen == 0) return; + out[0] = '\0'; + if (outLen < 2) return; + if (!in) in = ""; + + // absolute input replaces the CWD; relative input is appended + char work[FF_MAX_LFN + 1]; + if (in[0] == '/') { + strncpy(work, in, sizeof(work) - 1); + work[sizeof(work) - 1] = '\0'; + } else { + snprintf(work, sizeof(work), "%s/%s", s_currentDir, in); + } + + size_t len = 0; + bool truncated = false; + + char* p = work; + while (*p) { + while (*p == '/') p++; + if (!*p) break; + char* seg = p; + while (*p && *p != '/') p++; + size_t seglen = (size_t)(p - seg); + + if (seglen == 1 && seg[0] == '.') continue; + if (seglen == 2 && seg[0] == '.' && seg[1] == '.') { // pop, clamp at root + while (len > 0 && out[len - 1] != '/') len--; + if (len > 0) len--; + out[len] = '\0'; + continue; + } + if (len + 1 + seglen >= outLen) { truncated = true; break; } + out[len++] = '/'; + memcpy(out + len, seg, seglen); + len += seglen; + out[len] = '\0'; + } + + // genuine collapse to root -> "/"; don't fabricate it on truncation + if (len == 0 && !truncated) { + out[0] = '/'; + out[1] = '\0'; } - return false; } -/* - Wrapper around the f_readdir() function which - also returns ".." entry for sub-dirs. (FatFS 0.12 does - not return ".", ".." dirs anymore) -*/ -FRESULT sdReadDir(DIR * dir, FILINFO * fno, bool & firstTime) +FRESULT etxChdir(const char* path) { - FRESULT res; - if (firstTime && !isCwdAtRoot()) { - // fake parent directory entry - strcpy(fno->fname, ".."); - fno->fattrib = AM_DIR; - res = FR_OK; + char abs[FF_MAX_LFN + 1]; + etxNormalizePath(path, abs, sizeof(abs)); + FRESULT res = f_chdir(abs); + if (res == FR_OK) { + strncpy(s_currentDir, abs, sizeof(s_currentDir) - 1); + s_currentDir[sizeof(s_currentDir) - 1] = '\0'; } - else { - res = f_readdir(dir, fno); /* Read a directory item */ - } - firstTime = false; return res; } -#if !defined(BOOT) +const char* etxGetcwd() { return s_currentDir; } // Replace FatFS implementation of f_puts and f_printf diff --git a/radio/src/lib_file.h b/radio/src/lib_file.h index 20718fbdba7..574f5160d03 100644 --- a/radio/src/lib_file.h +++ b/radio/src/lib_file.h @@ -26,7 +26,18 @@ constexpr uint8_t LEN_FILE_EXTENSION_MAX = 5; // longest used, including the do const char * getFileExtension(const char * filename, uint8_t size = 0, uint8_t extMaxLen = 0, uint8_t * fnlen = nullptr, uint8_t * extlen = nullptr); bool isExtensionMatching(const char * extension, const char * pattern, char * match = nullptr); -FRESULT sdReadDir(DIR * dir, FILINFO * fno, bool & firstTime); + +// CWD tracker + path normalizer. FatFS can't resolve "."/".." or report the CWD +// on exFAT, so we track a normalized absolute CWD and resolve paths to absolute +// before they reach FatFS, matching FAT12/16/32 behavior. + +// Resolve 'in' (absolute or relative to the tracked CWD) to a normalized +// absolute path in 'out' (bounds-checked against outLen). +void etxNormalizePath(const char * in, char * out, size_t outLen); +// f_chdir() to a normalized absolute path; updates the tracked CWD on success. +FRESULT etxChdir(const char * path); +// Tracked absolute CWD (replacement for the exFAT-broken f_getcwd()). +const char * etxGetcwd(); // comparison, not case sensitive. static inline bool compare_nocase(const std::string& first, const std::string& second) { diff --git a/radio/src/lua/api_colorlcd.cpp b/radio/src/lua/api_colorlcd.cpp index fda476ff4e5..3eb0ab06734 100644 --- a/radio/src/lua/api_colorlcd.cpp +++ b/radio/src/lua/api_colorlcd.cpp @@ -31,6 +31,7 @@ #include "lua_api.h" #include "lua_widget.h" #include "api_colorlcd.h" +#include "lib_file.h" #define BITMAP_METATABLE "BITMAP*" @@ -507,7 +508,9 @@ Bitmap loading can fail if: */ static int luaOpenBitmap(lua_State *L) { - const char *filename = luaL_checkstring(L, 1); + const char *path = luaL_checkstring(L, 1); + char filename[FF_MAX_LFN + 1]; + etxNormalizePath(path, filename, sizeof(filename)); BitmapBuffer **b = (BitmapBuffer **)lua_newuserdata(L, sizeof(BitmapBuffer *)); diff --git a/radio/src/lua/api_filesystem.cpp b/radio/src/lua/api_filesystem.cpp index 33c85c72775..7c711bdab46 100644 --- a/radio/src/lua/api_filesystem.cpp +++ b/radio/src/lua/api_filesystem.cpp @@ -24,6 +24,7 @@ #include "edgetx.h" #include "lua_api.h" #include "api_filesystem.h" +#include "lib_file.h" // garbage collector for luaDir static int dir_gc(lua_State* L) @@ -67,12 +68,15 @@ static int dir_iter(lua_State* L) int luaDir(lua_State* L) { const char* path = luaL_optstring(L, 1, nullptr); + char fullPath[FF_MAX_LFN + 1]; + etxNormalizePath(path, fullPath, sizeof(fullPath)); + DIR* dir = (DIR*)lua_newuserdata(L, sizeof(DIR)); luaL_getmetatable(L, DIR_METATABLE); lua_setmetatable(L, -2); - FRESULT res = f_opendir(dir, path); + FRESULT res = f_opendir(dir, fullPath); if (res != FR_OK) { TRACE("luaDir cannot open %s", path); return 0; @@ -126,11 +130,13 @@ void luaPushDateTime(lua_State * L, uint32_t year, uint32_t mon, uint32_t day, int luaFstat(lua_State* L) { const char * path = luaL_optstring(L, 1, nullptr); + char fullPath[FF_MAX_LFN + 1]; + etxNormalizePath(path, fullPath, sizeof(fullPath)); FRESULT res; FILINFO info; - res = f_stat(path, &info); + res = f_stat(fullPath, &info); if (res != FR_OK) { TRACE("luaFstat cannot open %s", path); return 0; @@ -174,8 +180,10 @@ int luaFstat(lua_State* L) int luaDelete(lua_State* L) { const char* filename = luaL_optstring(L, 1, nullptr); + char fullPath[FF_MAX_LFN + 1]; + etxNormalizePath(filename, fullPath, sizeof(fullPath)); - FRESULT res = f_unlink(filename); + FRESULT res = f_unlink(fullPath); if (res != FR_OK) { TRACE("luaDelete cannot delete file/folder %s", filename); } @@ -198,7 +206,7 @@ int luaDelete(lua_State* L) static int luaChdir(lua_State * L) { const char * directory = luaL_optstring(L, 1, nullptr); - f_chdir(directory); + etxChdir(directory); return 0; } @@ -218,7 +226,9 @@ static int luaChdir(lua_State * L) static int luaMkdir(lua_State * L) { const char * directory = luaL_checkstring(L, 1); - FRESULT res = f_mkdir(directory); + char fullPath[FF_MAX_LFN + 1]; + etxNormalizePath(directory, fullPath, sizeof(fullPath)); + FRESULT res = f_mkdir(fullPath); lua_pushunsigned(L, res); return 1; } @@ -243,7 +253,11 @@ static int luaRename(lua_State * L) { const char * from_path = luaL_checkstring(L, 1); const char * to_path = luaL_checkstring(L, 2); - FRESULT res = f_rename(from_path, to_path); + char fullFrom[FF_MAX_LFN + 1]; + char fullTo[FF_MAX_LFN + 1]; + etxNormalizePath(from_path, fullFrom, sizeof(fullFrom)); + etxNormalizePath(to_path, fullTo, sizeof(fullTo)); + FRESULT res = f_rename(fullFrom, fullTo); lua_pushunsigned(L, res); return 1; } diff --git a/radio/src/lua/api_general.cpp b/radio/src/lua/api_general.cpp index dddda874642..12aa7171a53 100644 --- a/radio/src/lua/api_general.cpp +++ b/radio/src/lua/api_general.cpp @@ -27,6 +27,7 @@ #include "stamp.h" #include "lua_api.h" #include "api_filesystem.h" +#include "lib_file.h" #include "hal/module_port.h" #include "hal/adc_driver.h" #include "hal/rotary_encoder.h" @@ -1484,17 +1485,21 @@ static int luaPlayFile(lua_State * L) if(volume != USE_SETTINGS_VOLUME) volume = limit(-2, volume-3, 2); // (rescale 1..5) to internal format and limit to (-2..2) + char file[AUDIO_FILENAME_MAXLEN+1]; if (filename[0] != '/') { // relative sound file path - use current language dir for absolute path - char file[AUDIO_FILENAME_MAXLEN+1]; char * str = getAudioPath(file); strncpy(str, filename, AUDIO_FILENAME_MAXLEN - (str-file)); file[AUDIO_FILENAME_MAXLEN] = 0; - audioQueue.playFile(file, 0, 0, volume); } else { - audioQueue.playFile(filename, 0, 0, volume); + strncpy(file, filename, AUDIO_FILENAME_MAXLEN); + file[AUDIO_FILENAME_MAXLEN] = 0; } + // resolve to an absolute path (works on exFAT) + char norm[AUDIO_FILENAME_MAXLEN+1]; + etxNormalizePath(file, norm, sizeof(norm)); + audioQueue.playFile(norm, 0, 0, volume); return 0; } diff --git a/radio/src/lua/interface.cpp b/radio/src/lua/interface.cpp index bb204b42a51..0adc61351a4 100644 --- a/radio/src/lua/interface.cpp +++ b/radio/src/lua/interface.cpp @@ -466,15 +466,19 @@ int luaLoadScriptFileToState(lua_State * L, const char * filename, const char * memclear(&fnoLuaS, sizeof(FILINFO)); memclear(&fnoLuaC, sizeof(FILINFO)); - fnamelen = strlen(filename); + // resolve to an absolute path (works on exFAT) + char normPath[FF_MAX_LFN + 1]; + etxNormalizePath(filename, normPath, sizeof(normPath)); + + fnamelen = strlen(normPath); // check if file extension is already in the file name and strip it - getFileExtension(filename, fnamelen, 0, nullptr, &extlen); + getFileExtension(normPath, fnamelen, 0, nullptr, &extlen); fnamelen -= extlen; if (fnamelen > sizeof(filenameFull) - sizeof(SCRIPT_BIN_EXT)) { TRACE_ERROR("luaLoadScriptFileToState(%s, %s): Error loading script: filename buffer overflow.\n", filename, lmode); return ret; } - strncat(filenameFull, filename, fnamelen); + strncat(filenameFull, normPath, fnamelen); // check if binary version exists strcpy(filenameFull + fnamelen, SCRIPT_BIN_EXT); @@ -535,8 +539,9 @@ int luaLoadScriptFileToState(lua_State * L, const char * filename, const char * #else // !defined(LUA_COMPILER) - // use passed file name as-is - const char *filenameFull = filename; + // resolve to an absolute path (works on exFAT) + char filenameFull[FF_MAX_LFN + 1]; + etxNormalizePath(filename, filenameFull, sizeof(filenameFull)); #endif @@ -1404,7 +1409,10 @@ bool readToolName(char * toolName, const char * filename) char buffer[1024]; UINT count; - if (f_open(&file, filename, FA_READ) != FR_OK) { + char fullPath[FF_MAX_LFN + 1]; + etxNormalizePath(filename, fullPath, sizeof(fullPath)); + + if (f_open(&file, fullPath, FA_READ) != FR_OK) { return false; } diff --git a/radio/src/lua/widgets.cpp b/radio/src/lua/widgets.cpp index a577bbecf98..56b289cb47f 100644 --- a/radio/src/lua/widgets.cpp +++ b/radio/src/lua/widgets.cpp @@ -188,7 +188,7 @@ static void luaLoadFiles(const char * directory) FILINFO fno; DIR dir; - strcpy(path, directory); + etxNormalizePath(directory, path, sizeof(path)); TRACE("luaLoadFiles() %s", path); FRESULT res = f_opendir(&dir, path); /* Open the directory */ diff --git a/radio/src/model_init.cpp b/radio/src/model_init.cpp index 752e3510f99..844152e1d5f 100644 --- a/radio/src/model_init.cpp +++ b/radio/src/model_init.cpp @@ -22,6 +22,7 @@ #include "edgetx.h" #include "hal/adc_driver.h" #include "input_mapping.h" +#include "lib_file.h" #include "mixes.h" #if defined(COLORLCD) @@ -193,7 +194,7 @@ void setModelDefaults(uint8_t id) #if defined(LUA) && defined(PCBTARANIS) if (isFileAvailable(WIZARD_PATH "/" WIZARD_NAME)) { - f_chdir(WIZARD_PATH); + etxChdir(WIZARD_PATH); luaExec(WIZARD_NAME); } #endif diff --git a/radio/src/tests/fs_path.cpp b/radio/src/tests/fs_path.cpp new file mode 100644 index 00000000000..68daf036efe --- /dev/null +++ b/radio/src/tests/fs_path.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "gtests.h" +#include "lib_file.h" + +// Helper: normalize with the given CWD (set via etxChdir, which needs the dir +// to exist under the sim SD root = radio/src/tests). "/" and "/images" exist. +static std::string norm(const char* cwd, const char* in) +{ + if (etxChdir(cwd) != FR_OK) { + ADD_FAILURE() << "chdir failed: " << cwd; + return {}; + } + char out[FF_MAX_LFN + 1]; + etxNormalizePath(in, out, sizeof(out)); + return std::string(out); +} + +// Path normalization must collapse "."/".." to a clean absolute path so Lua +// file access behaves the same on exFAT as on FAT32 (FatFS cannot resolve ".." +// or report the CWD on exFAT). +TEST(FsPath, normalizeAbsolute) +{ + EXPECT_EQ(etxChdir("/"), FR_OK); + char out[FF_MAX_LFN + 1]; + + etxNormalizePath("/SCRIPTS/../IMAGE", out, sizeof(out)); + EXPECT_STREQ(out, "/IMAGE"); + + etxNormalizePath("/SCRIPTS/../IMAGE/file.jpg", out, sizeof(out)); + EXPECT_STREQ(out, "/IMAGE/file.jpg"); + + etxNormalizePath("/a/./b/./c", out, sizeof(out)); + EXPECT_STREQ(out, "/a/b/c"); + + etxNormalizePath("//a///b//", out, sizeof(out)); + EXPECT_STREQ(out, "/a/b"); + + // ".." can never escape the root + etxNormalizePath("/..", out, sizeof(out)); + EXPECT_STREQ(out, "/"); + etxNormalizePath("/../../x", out, sizeof(out)); + EXPECT_STREQ(out, "/x"); +} + +TEST(FsPath, normalizeRelative) +{ + // relative paths resolve against the tracked CWD + EXPECT_EQ(norm("/images", "logo.png"), "/images/logo.png"); + EXPECT_EQ(norm("/images", "../IMAGE"), "/IMAGE"); + EXPECT_EQ(norm("/images", "../IMAGE/file.jpg"), "/IMAGE/file.jpg"); + EXPECT_EQ(norm("/images", "./sub/x"), "/images/sub/x"); + EXPECT_EQ(norm("/", "a/b"), "/a/b"); + EXPECT_EQ(norm("/", ".."), "/"); +} + +TEST(FsPath, normalizeNullAndEmpty) +{ + EXPECT_EQ(etxChdir("/images"), FR_OK); + char out[FF_MAX_LFN + 1]; + + etxNormalizePath(nullptr, out, sizeof(out)); + EXPECT_STREQ(out, "/images"); // null/empty -> CWD + + etxNormalizePath("", out, sizeof(out)); + EXPECT_STREQ(out, "/images"); +} + +// etxChdir tracks an absolute CWD that etxGetcwd() returns (a replacement for +// the exFAT-broken f_getcwd()). A failed chdir must leave the CWD unchanged. +TEST(FsPath, chdirTracksCwd) +{ + EXPECT_EQ(etxChdir("/"), FR_OK); + EXPECT_STREQ(etxGetcwd(), "/"); + + EXPECT_EQ(etxChdir("/images"), FR_OK); + EXPECT_STREQ(etxGetcwd(), "/images"); + + // relative + dotted chdir is normalized to absolute + EXPECT_EQ(etxChdir(".."), FR_OK); + EXPECT_STREQ(etxGetcwd(), "/"); + + // a chdir to a missing dir fails and leaves the CWD untouched + EXPECT_EQ(etxChdir("/images"), FR_OK); + EXPECT_NE(etxChdir("/no/such/dir"), FR_OK); + EXPECT_STREQ(etxGetcwd(), "/images"); +} + +// Guard against buffer overruns: undersized output buffers must never write +// past outLen (ASan would abort), and must not fabricate a bogus "/" when the +// path did not actually collapse to root. (Regression for the buffer guard.) +TEST(FsPath, normalizeBufferBounds) +{ + EXPECT_EQ(etxChdir("/"), FR_OK); + char out[8]; + + // zero-length: no write at all + out[0] = '#'; + etxNormalizePath("/x", out, 0); + EXPECT_EQ(out[0], '#'); + + // room for terminator only -> empty string + etxNormalizePath("/x", out, 1); + EXPECT_STREQ(out, ""); + + // root just fits in 2 bytes + etxNormalizePath("..", out, 2); + EXPECT_STREQ(out, "/"); + + // first segment does not fit -> empty, NOT a fabricated "/" + etxNormalizePath("/VERYLONGNAME", out, 5); + EXPECT_STREQ(out, ""); + + // partial fit stays terminated and within bounds + etxNormalizePath("/ab/cd", out, 4); + EXPECT_STREQ(out, "/ab"); +}