/* * 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 "simulib.h" #if !defined(SIMU_DISKIO) #include "ff.h" #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; using sysclock = std::chrono::system_clock; using ftime_type = fs::file_time_type; // path to the root of the SD card image static fs::path simuSdDirectory; // path to the root of the models and settings static fs::path simuSettingsDirectory; // current simulater path static fs::path simuCurrentPath; static std::string to_lower(const std::string& s) { std::string result(s.size(), '\0'); std::transform(s.begin(), s.end(), result.begin(), [](unsigned char c) { return std::tolower(c); }); return result; } static sysclock::time_point ftime_to_systime(ftime_type ftime) { return std::chrono::time_point_cast( ftime - ftime_type::clock::now() + sysclock::now()); } static ftime_type systime_to_ftime(sysclock::time_point systime) { return std::chrono::time_point_cast( systime - sysclock::now() + ftime_type::clock::now()); } static fs::path resolveCaseInsensitivePath( const fs::path& base, const fs::path& relativePath) { fs::path current = base; std::error_code ec; for (const auto& component : relativePath) { std::string componentStr = component.string(); // Skip current directory and parent directory references if (componentStr == "." || componentStr == "..") { current /= component; continue; } // Try exact match first fs::path exactPath = current / component; if (fs::exists(exactPath, ec) && !ec) { current = exactPath; continue; } // Look for case-insensitive match bool found = false; if (fs::is_directory(current, ec) && !ec) { std::string componentLower = to_lower(componentStr); for (const auto& entry : fs::directory_iterator(current, ec)) { if (ec) break; std::string entryName = entry.path().filename().string(); if (to_lower(entryName) == componentLower) { current = entry.path(); found = true; break; } } } // If not found, use original component name (file doesn't exist) if (!found) current /= component; } return current; } static fs::path makeAbsolute(const fs::path& path) { return path.is_relative() ? simuCurrentPath / path : path; } static fs::path resolveInBase(const fs::path& base, const fs::path& virtualPath) { return resolveCaseInsensitivePath( base, virtualPath.lexically_relative(virtualPath.root_path())); } // Resolve for read: try settingsPath first, fall back to sdPath static std::string resolveForRead(const fs::path& path) { fs::path p = makeAbsolute(path); if (!simuSettingsDirectory.empty()) { auto resolved = resolveInBase(simuSettingsDirectory, p); std::error_code ec; if (fs::exists(resolved, ec) && !ec) { return resolved.string(); } } return resolveInBase(simuSdDirectory, p).string(); } // Resolve for write: always use settingsPath if available static std::string resolveForWrite(const fs::path& path) { fs::path p = makeAbsolute(path); if (!simuSettingsDirectory.empty()) { return resolveInBase(simuSettingsDirectory, p).string(); } return resolveInBase(simuSdDirectory, p).string(); } void simuFatfsSetPaths(const char* sdPath, const char* settingsPath) { simuCurrentPath = fs::path{"/"}; if (sdPath && strlen(sdPath) > 0) { simuSdDirectory = fs::path{sdPath}.lexically_normal(); } else if (simuSdDirectory.empty()) { simuSdDirectory = fs::current_path(); } if (settingsPath && strlen(settingsPath) > 0) { simuSettingsDirectory = fs::path{settingsPath}.lexically_normal(); } } std::string simuFatfsGetCurrentPath() { return simuCurrentPath.string(); } std::string simuFatfsGetRealPath(const std::string &p) { return resolveForRead(p); } FRESULT file_stat(const std::string& realPath, FILINFO* fno) { std::error_code ec; fs::path fsPath(realPath); // Check if file/directory exists auto status = fs::status(fsPath, ec); if (ec || status.type() == fs::file_type::not_found) { return FR_INVALID_NAME; } if (fno) { // Set attributes fno->fattrib = (status.type() == fs::file_type::directory) ? AM_DIR : 0; // Get file size if (status.type() == fs::file_type::regular) { auto size = fs::file_size(fsPath, ec); fno->fsize = ec ? 0 : static_cast(size); } else { fno->fsize = 0; } // Get last write time auto ftime = fs::last_write_time(fsPath, ec); if (!ec) { auto systime = ftime_to_systime(ftime); auto time_t_val = std::chrono::system_clock::to_time_t(systime); struct tm ltime; if (simuLocalTime(time_t_val, <ime)) { // Convert to FatFs format fno->fdate = ((ltime.tm_year - 80) << 9) | ((ltime.tm_mon + 1) << 5) | ltime.tm_mday; fno->ftime = (ltime.tm_hour << 11) | (ltime.tm_min << 5) | (ltime.tm_sec / 2); } else { fno->fdate = 0; fno->ftime = 0; } } else { fno->fdate = 0; fno->ftime = 0; } } return FR_OK; } FRESULT f_stat(const TCHAR* name, FILINFO* fno) { std::string realPath = resolveForRead(name); return file_stat(realPath, fno); } FRESULT f_mount(FATFS*, const TCHAR*, BYTE opt) { return FR_OK; } struct _simu_FIL { std::unique_ptr stream; std::string filepath; std::ios::openmode mode; _simu_FIL(const std::string& path, std::ios::openmode m) : filepath(path), mode(m) { stream = std::make_unique(path, m); } }; FRESULT f_open(FIL* fil, const TCHAR* name, BYTE flag) { fil->obj.fs = nullptr; fil->fptr = 0; std::ios::openmode mode = std::ios::binary; std::string realPath; if (flag & FA_WRITE) { realPath = resolveForWrite(name); mode |= std::ios::out | std::ios::in; // Ensure parent directories exist in settingsPath, // but only if they exist in the overlay (either layer) std::error_code ec; fs::path virtualParent = makeAbsolute(fs::path{name}).parent_path(); std::string readParent = resolveForRead(virtualParent); if (fs::is_directory(readParent, ec) && !ec) { fs::path parentPath = fs::path(realPath).parent_path(); if (!parentPath.empty()) { fs::create_directories(parentPath, ec); } } if (flag & FA_CREATE_ALWAYS) { mode |= std::ios::trunc; } else { if (fs::exists(realPath, ec) && !ec) { mode |= std::ios::ate; // Open at end } else { // Copy-on-write: if the file exists in sdPath, copy it to // settingsPath so appends start from the original content std::string readPath = resolveForRead(name); if (fs::exists(readPath, ec) && !ec) { fs::copy_file(readPath, realPath, ec); mode |= std::ios::ate; } else { mode |= std::ios::trunc; // Create new file } } } } else { realPath = resolveForRead(name); mode |= std::ios::in; std::error_code ec; if (!fs::exists(realPath, ec) || ec) { return FR_INVALID_NAME; } auto size = fs::file_size(realPath, ec); if (!ec) { fil->obj.objsize = static_cast(size); } } auto simuFil = new _simu_FIL(realPath, mode); if (simuFil->stream->is_open()) { fil->obj.fs = reinterpret_cast(simuFil); return FR_OK; } else { delete simuFil; return FR_INVALID_NAME; } } FRESULT f_close(FIL* fil) { if (fil && fil->obj.fs) { delete reinterpret_cast<_simu_FIL*>(fil->obj.fs); fil->obj.fs = nullptr; } return FR_OK; } FRESULT f_read(FIL* fil, void* data, UINT size, UINT* read) { *read = 0; if (fil && fil->obj.fs) { _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); if (sf->stream && sf->stream->is_open()) { sf->stream->read(static_cast(data), size); *read = static_cast(sf->stream->gcount()); fil->fptr += *read; sf->stream->clear(); } } return FR_OK; } FRESULT f_write(FIL* fil, const void* data, UINT size, UINT* written) { *written = 0; if (fil && fil->obj.fs) { _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); if (sf->stream && sf->stream->is_open()) { auto pos_before = sf->stream->tellp(); sf->stream->write(static_cast(data), size); if (sf->stream->good()) { *written = size; fil->fptr += size; } else { auto pos_after = sf->stream->tellp(); if (pos_after != std::streampos(-1) && pos_before != std::streampos(-1)) { *written = static_cast(pos_after - pos_before); fil->fptr += *written; } } } } return FR_OK; } FRESULT f_lseek(FIL* fil, DWORD offset) { if (fil && fil->obj.fs) { _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); if (sf->stream && sf->stream->is_open()) { sf->stream->seekg(offset, std::ios::beg); sf->stream->seekp(offset, std::ios::beg); fil->fptr = offset; } } return FR_OK; } UINT f_size(FIL* fil) { if (fil && fil->obj.fs) { _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); // Use filesystem to get size (more efficient) std::error_code ec; auto size = fs::file_size(sf->filepath, ec); if (!ec) { return static_cast(size); } } return 0; } FRESULT f_chdir(const TCHAR *name) { fs::path p = makeAbsolute(fs::path{name}); std::error_code ec; auto realPath = resolveForRead(p); if (fs::exists(realPath, ec) && !ec) { simuCurrentPath = p; return FR_OK; } return FR_NO_PATH; } struct _simu_DIR { std::vector entries; size_t index = 0; void collectEntries(const std::string& dir, std::set& seen) { std::error_code ec; if (!fs::is_directory(dir, ec) || ec) return; for (const auto& entry : fs::directory_iterator(dir, ec)) { if (ec) break; std::string nameLower = to_lower(entry.path().filename().string()); if (seen.insert(nameLower).second) { entries.push_back(entry.path()); } } } bool hasNext() const { return index < entries.size(); } std::optional getNext() { if (index >= entries.size()) return std::nullopt; return entries[index++]; } }; FRESULT f_opendir(DIR* rep, const TCHAR* name) { fs::path p = makeAbsolute(fs::path{name}); fs::path relative = p.lexically_relative(p.root_path()); std::error_code ec; std::string settingsDir, sdDir; bool settingsExists = false, sdExists = false; if (!simuSettingsDirectory.empty()) { settingsDir = resolveCaseInsensitivePath(simuSettingsDirectory, relative).string(); settingsExists = fs::is_directory(settingsDir, ec) && !ec; } sdDir = resolveCaseInsensitivePath(simuSdDirectory, relative).string(); sdExists = fs::is_directory(sdDir, ec) && !ec; if (!settingsExists && !sdExists) { rep->obj.fs = nullptr; return FR_NO_PATH; } auto sd = new _simu_DIR(); std::set seen; // Settings entries take priority (added first) if (settingsExists) sd->collectEntries(settingsDir, seen); if (sdExists) sd->collectEntries(sdDir, seen); rep->obj.fs = reinterpret_cast(sd); return FR_OK; } FRESULT f_closedir(DIR* rep) { if (rep->obj.fs) { _simu_DIR* sd = reinterpret_cast<_simu_DIR*>(rep->obj.fs); rep->obj.fs = nullptr; delete sd; } return FR_OK; } FRESULT f_readdir(DIR* rep, FILINFO* fil) { _simu_DIR* sd = reinterpret_cast<_simu_DIR*>(rep->obj.fs); if (!sd || !sd->hasNext()) { return FR_NO_FILE; } auto maybe_path = sd->getNext(); if (!maybe_path.has_value()) { return FR_NO_FILE; } auto entryPath = maybe_path.value(); if (fil != nullptr) { memset(fil->fname, 0, FF_MAX_LFN); std::string filename = entryPath.filename().string(); size_t copyLen = std::min(filename.length(), (size_t)(FF_MAX_LFN - 1)); strncpy(fil->fname, filename.c_str(), copyLen); std::string fullPath = entryPath.string(); return file_stat(fullPath, fil); } return FR_OK; } FRESULT f_mkfs(const TCHAR* path, BYTE opt, DWORD au, void* work, UINT len) { return FR_OK; } FRESULT f_mkdir(const TCHAR* name) { std::string path = resolveForWrite(name); std::error_code ec; // Create parent directories in settingsPath if they exist in the overlay fs::path virtualParent = makeAbsolute(fs::path{name}).parent_path(); std::string readParent = resolveForRead(virtualParent); if (!fs::is_directory(readParent, ec) || ec) { return FR_NO_PATH; } fs::path parentPath = fs::path(path).parent_path(); if (!parentPath.empty()) { fs::create_directories(parentPath, ec); } bool created = fs::create_directory(path, ec); if (ec) return FR_INVALID_NAME; return created ? FR_OK : FR_EXIST; } FRESULT f_unlink(const TCHAR * name) { std::string path = resolveForWrite(name); std::error_code ec; bool removed = fs::remove(path, ec); if (ec) return FR_INVALID_NAME; return removed ? FR_OK : FR_INVALID_NAME; } FRESULT f_rename(const TCHAR *oldname, const TCHAR *newname) { std::string old = resolveForWrite(oldname); std::string path = resolveForWrite(newname); std::error_code ec; fs::rename(old.c_str(), path.c_str(), ec); if (ec) return FR_INVALID_NAME; return FR_OK; } FRESULT f_utime(const TCHAR* path, const FILINFO* fno) { if (fno == nullptr) { return FR_INVALID_PARAMETER; } std::string realPath = resolveForWrite(path); // Convert FatFs time to tm structure struct tm ltime = {}; ltime.tm_year = ((fno->fdate >> 9) & 0x7F) + 80; ltime.tm_mon = ((fno->fdate >> 5) & 0xF) - 1; ltime.tm_mday = (fno->fdate & 0x1F); ltime.tm_hour = ((fno->ftime >> 11) & 0x1F); ltime.tm_min = ((fno->ftime >> 5) & 0x3F); ltime.tm_sec = (fno->ftime & 0x1F) * 2; ltime.tm_isdst = -1; time_t timestamp = simuLocalTimeToEpoch(<ime); if (timestamp == -1) { return FR_INVALID_PARAMETER; } // Convert to file time and set it auto systime = std::chrono::system_clock::from_time_t(timestamp); auto ftime = systime_to_ftime(systime); std::error_code ec; fs::last_write_time(realPath, ftime, ec); return ec ? FR_DENIED : FR_OK; } int f_putc(TCHAR c, FIL* fil) { if (fil && fil->obj.fs) { _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); if (sf->stream && sf->stream->is_open()) { sf->stream->put(c); fil->fptr++; } } return EOF; } FRESULT f_getcwd(TCHAR *path, UINT sz_path) { auto p_str = simuCurrentPath.generic_string(); if (p_str.length() > sz_path) { return FR_NOT_ENOUGH_CORE; } strncpy(path, p_str.c_str(), sz_path + 1); return FR_OK; } FRESULT f_truncate(FIL* fil) { if (fil && fil->obj.fs) { _simu_FIL* sf = reinterpret_cast<_simu_FIL*>(fil->obj.fs); if (sf->stream && sf->stream->is_open()) { sf->stream->flush(); if (!sf->stream->good()) return FR_DISK_ERR; std::error_code ec; fs::resize_file(sf->filepath, fil->fptr, ec); if (ec) return FR_DISK_ERR; } } return FR_OK; } FRESULT f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs) { // just fake that we always have some clusters free *nclst = 10; return FR_OK; } #include "hal/storage.h" void storageInit() {} void storageDeInit() {} void storagePreMountHook() {} bool storageIsPresent() { return true; } #endif // #if defined(SIMU_USE_SDCARD)