From 39a2edf4f153ebe09bad3a88191c269b1804aa22 Mon Sep 17 00:00:00 2001 From: uniflare Date: Tue, 7 Jun 2022 21:54:03 +0200 Subject: [PATCH] !R (Storage) Moved storage from mod storage to profile storage - Single-player/Locally hosted environments will always use mod storage - Auto migration from mod storage to profile storage - Auto detection of profile folder location (Windows+Linux) - Simplified function detection (using std::string and trim) - Simplified file manager (using std::filesystem) - Moved general methods to new utility unit --- FileXT/dllmain.cpp | 237 +++++++------------------ FileXT/filemgr.cpp | 81 ++++----- FileXT/filemgr.h | 21 +-- FileXT/utility.cpp | 426 +++++++++++++++++++++++++++++++++++++++++++++ FileXT/utility.h | 197 +++++++++++++++++++++ 5 files changed, 731 insertions(+), 231 deletions(-) create mode 100644 FileXT/utility.cpp create mode 100644 FileXT/utility.h diff --git a/FileXT/dllmain.cpp b/FileXT/dllmain.cpp index dd2a950..a250632 100644 --- a/FileXT/dllmain.cpp +++ b/FileXT/dllmain.cpp @@ -3,55 +3,12 @@ #include "common.h" #include "errorCodes.h" #include "filemgr.h" +#include "utility.h" #include "value.h" - -#ifndef _MSC_VER -#include -#endif - -using namespace std; - // Globals filext::filemgr gFileMgr; -std::unique_ptr gpFile(std::fopen(getLogPath().u8string().c_str(), "w"), &std::fclose); - -bool checkFileName(string& fileName); -string getDllFolder(); - -const std::string& GetAndEnsureStorageDir() -{ - // keep this static path internal so it doesn't get used too early or modified externally. - static string fileStorageFolder; - - if(fileStorageFolder.empty() || !filesystem::exists(fileStorageFolder)) - { - LOG_VERBOSE("File storage directory isn't set or doesn't exist. Attempting to find or create it now."); - std::string dllFolder = getDllFolder(); - LOG_VERBOSE("DLL Folder: %s", dllFolder.c_str()); - - if(dllFolder.empty() || !filesystem::exists(dllFolder)) - { - // Use LOG_VERBOSE here because LOG depends on this function completing. - LOG_CRITICAL("The DLL folder was not found: \"%s\"", dllFolder.c_str()); - } - fileStorageFolder = dllFolder + string("storage/"); - - if(!filesystem::exists(fileStorageFolder)) - { - LOG_VERBOSE("fileStorageFolder not found, creating it now: \"%s\"", fileStorageFolder.c_str()); - filesystem::create_directory(fileStorageFolder); - } - - if(!filesystem::exists(fileStorageFolder)) - { - LOG_CRITICAL("The file storage directory could not be created."); - fileStorageFolder.clear(); - } - - } - return fileStorageFolder; -} +std::unique_ptr gpFile(std::fopen(filext::getLogPath().u8string().c_str(), "w"), &std::fclose); #ifndef _MSC_VER __attribute__((constructor)) @@ -59,7 +16,7 @@ __attribute__((constructor)) static void Entry() { LOG_VERBOSE("FileXT Dll entry\n"); - std::string const& storageDirectory = GetAndEnsureStorageDir(); + filext::getStorage(); } #ifndef _MSC_VER @@ -116,57 +73,83 @@ callExtension arguments are: */ FILEXT_EXPORT int FILEXT_CALL RVExtensionArgs(char* output, int outputSize, const char* function, const char** argv, int argc) { + filext::tryMigrateStorageFolder(); + static const std::filesystem::path& storage = filext::getStorage(); + // Extract function name - const char* functionName = argv[0]; + const std::string functionName = string::trim(argv[0], "\""); const char* data = function; // It doesn't have the quotes! - std::string const& storageDirectory = GetAndEnsureStorageDir(); + // Resolve function name + + // ["", ["getFiles"]] + if (functionName == "getFiles") { + ASSERT_EXT_ARGC(argc, 1); - // Exctract file name: remove leading and trailing " - string fileName(""); + std::vector filenames; + if (!std::filesystem::exists(storage)) { + LOG_CRITICAL("failed to getFiles\n"); + return 0; + } + + // Collect list of all files in storage folder + for (const auto& dirEntry : std::filesystem::directory_iterator(storage)) { + LOG("File: %s\n", dirEntry.path().c_str()); + std::string filename = dirEntry.path().filename().string(); + filenames.emplace_back(filename); + } + + sqf::value filenamesSQF(filenames); + std::string strOut = filenamesSQF.to_string(); + + ASSERT_BUFFER_SIZE((int)outputSize - 1, (int)strOut.size()); + std::strcpy(output, strOut.c_str()); + return 0; + }; + + // Extract file name: remove leading and trailing " + std::filesystem::path file; if (argc >= 2) { - fileName = string(argv[1]); - fileName.erase(0, 1); - fileName.pop_back(); - //LOG("Argc: %i\n", argc); - - // Check file name - // Bail if file name is wrong - if (!checkFileName(fileName)) - return FILEXT_ERROR_WRONG_FILE_NAME; - fileName = storageDirectory + fileName; + file = string::trim(argv[1], "\""); } - LOG_VERBOSE("RVExtensionArgs: function: %s, fileName: %s, outputSize: %i\n", functionName, fileName.c_str(), outputSize); + // Check file name + // Bail if file name is wrong + if (file.empty()) + { + return FILEXT_ERROR_WRONG_FILE_NAME; + } - // Resolve function name + file = storage / file; + + LOG_VERBOSE("RVExtensionArgs: function: %s, fileName: %s, outputSize: %i\n", functionName, file.string().c_str(), outputSize); // ["", ["open", fileName]] - if (strcmp(functionName, "\"open\"") == 0) { + if (functionName == "open") { ASSERT_EXT_ARGC(argc, 2); - return gFileMgr.open(fileName); + return gFileMgr.open(file); }; // ["", ["close", fileName]] - if (strcmp(functionName, "\"close\"") == 0) { + if (functionName == "close") { ASSERT_EXT_ARGC(argc, 2); - return gFileMgr.close(fileName); + return gFileMgr.close(file); }; // ["", ["write", fileName]] - if (strcmp(functionName, "\"write\"") == 0) { + if (functionName == "write") { ASSERT_EXT_ARGC(argc, 2); - return gFileMgr.write(fileName); + return gFileMgr.write(file); }; // ["", ["read", fileName]] - if (strcmp(functionName, "\"read\"") == 0) { + if (functionName == "read") { ASSERT_EXT_ARGC(argc, 2); - return gFileMgr.read(fileName); + return gFileMgr.read(file); }; // ["", ["get", fileName, key, reset(0/1)]] - if (strcmp(functionName, "\"get\"") == 0) { + if (functionName == "get") { ASSERT_EXT_ARGC(argc, 4); std::string strOut(""); int reset = 0; @@ -175,56 +158,31 @@ FILEXT_EXPORT int FILEXT_CALL RVExtensionArgs(char* output, int outputSize, cons } catch ( ... ) { reset = 0; } - int retInt = gFileMgr.get(fileName, argv[2], strOut, outputSize-4, (bool)reset); // Just to be safe, reduce size a bit + int retInt = gFileMgr.get(file, argv[2], strOut, outputSize-4, (bool)reset); // Just to be safe, reduce size a bit LOG(" Returning string of size: %i\n", (unsigned int)strOut.size()); ASSERT_BUFFER_SIZE((int)outputSize -1, (int)strOut.size()); - strcpy(output, strOut.c_str()); + std::strcpy(output, strOut.c_str()); return retInt; }; // [value, ["set", fileName, key]] - if (strcmp(functionName, "\"set\"") == 0) { + if (functionName == "set") { ASSERT_EXT_ARGC(argc, 3); - return gFileMgr.set(fileName, argv[2], data); + return gFileMgr.set(file, argv[2], data); }; // ["", ["eraseKey", fileName, key]] - if (strcmp(functionName, "\"eraseKey\"") == 0) { + if (functionName == "eraseKey") { ASSERT_EXT_ARGC(argc, 3); - return gFileMgr.eraseKey(fileName, argv[2]); - }; - - // ["", ["getFiles"]] - if (strcmp(functionName, "\"getFiles\"") == 0) { - ASSERT_EXT_ARGC(argc, 1); - - // prevent filesystem exception on directory_iterator - if (!filesystem::exists(storageDirectory)) { - LOG_CRITICAL("failed to getFiles"); - return 0; - } - - vector vectorFileNamesSQF; - for (const auto& entry : filesystem::directory_iterator(storageDirectory)) { - string fileName = entry.path().filename().string(); - vectorFileNamesSQF.push_back(sqf::value(fileName)); - LOG("File: %s\n", fileName.c_str()); - } - - sqf::value fileNamesSQFArray(vectorFileNamesSQF); - string strOut = fileNamesSQFArray.to_string(); - - ASSERT_BUFFER_SIZE((int)outputSize - 1, (int)strOut.size()); - strcpy(output, strOut.c_str()); - return 0; + return gFileMgr.eraseKey(file, argv[2]); }; // ["", ["deleteFile", fileName]] - if (strcmp(functionName, "\"deleteFile\"") == 0) { - if(filesystem::exists(fileName)) + if (functionName == "deleteFile") { + if (std::filesystem::exists(file)) { try { - filesystem::remove(fileName); + std::filesystem::remove(file); } catch (...) { return FILEXT_ERROR_WRONG_FILE_NAME; } @@ -233,12 +191,13 @@ FILEXT_EXPORT int FILEXT_CALL RVExtensionArgs(char* output, int outputSize, cons { return FILEXT_ERROR_WRONG_FILE_NAME; } + return FILEXT_SUCCESS; }; // ["", ["fileExists", fileName]] - if (strcmp(functionName, "\"fileExists\"") == 0) { - if (filesystem::exists(fileName)) + if (functionName == "fileExists") { + if (std::filesystem::exists(file)) return FILEXT_SUCCESS; else return FILEXT_ERROR_WRONG_FILE_NAME; @@ -264,68 +223,4 @@ FILEXT_EXPORT void FILEXT_CALL RVExtensionVersion(char* output, int outputSize) { strcpy(output, versionString.c_str()); } -} - -// Ensures that the file name is correct: -// Has non-negative length -// Doesn't have / or \ inside it -bool checkFileName(string& fileName) { - - // Check length - if (fileName.size() == 0) - return false; - - // Check forbidden characters - string forbiddenChars("\\/?*:|\"<>,;="); - if (fileName.find_first_of(forbiddenChars) != string::npos) - return false; - - // All good so far - return true; -} - -std::string getDllFolder() -{ -#ifdef _MSC_VER - // Borrowed from https://gist.github.com/pwm1234/05280cf2e462853e183d - - char path[FILENAME_MAX]; - HMODULE hm = NULL; - - if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | - GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - (LPCSTR)getDllFolder, - &hm)) - { - LOG("error: GetModuleHandle returned %i", GetLastError()); - return std::string(""); - } - - GetModuleFileNameA(hm, path, sizeof(path)); - std::string p(path); - - // Remove DLL name from the path - auto pos = p.rfind('\\'); - p.erase(pos + 1, p.size()); - return p; -#else - Dl_info dl_info; - dladdr((void*)getDllFolder, &dl_info); - std::string p = std::string(dl_info.dli_fname); - - // Remove DLL name from the path - auto pos = p.rfind('/'); - p.erase(pos + 1, p.size()); - return p; -#endif -} - -std::filesystem::path& getLogPath() { - static std::filesystem::path logPath; - - if (logPath.empty()) { - logPath = std::filesystem::path(getDllFolder()) / "filext_log.log"; - } - - return logPath; } \ No newline at end of file diff --git a/FileXT/filemgr.cpp b/FileXT/filemgr.cpp index 8bdbda2..1a80487 100644 --- a/FileXT/filemgr.cpp +++ b/FileXT/filemgr.cpp @@ -9,51 +9,42 @@ using namespace std; // FILEINFO STRUCT -fileInfo::fileInfo(std::string& fileName) : +fileInfo::fileInfo(const std::filesystem::path& filepath) : m_map(unordered_map(512)), - m_fileName(fileName), + m_filepath(filepath), m_currentGetID(0), - m_currentGetKey("") -{ - LOG("NEW fileInfo: %s\n", fileName.c_str()); + m_currentGetKey("") { + LOG("NEW fileInfo: %s\n", filepath.c_str()); } -fileInfo::~fileInfo() -{ - LOG("DELETE fileinfo: %s\n", m_fileName.c_str()); +fileInfo::~fileInfo() { + LOG("DELETE fileinfo: %s\n", m_filepath.c_str()); } - - - // FILEMGR -int filext::filemgr::open(const std::string& fName) -{ - LOG("open(%s)\n", fName.c_str()); +int filext::filemgr::open(const std::filesystem::path& filepath) { + LOG("open(%s)\n", filepath.c_str()); // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { LOG(" FOUND in m_fileMap\n"); } else { LOG(" NOT found in m_fileMap\n"); // Create a new entry - fileInfo* finfoPtr = new fileInfo(fNameStr); - m_fileMap[fNameStr] = finfoPtr; + fileInfo* finfoPtr = new fileInfo(filepath); + m_fileMap[filepath] = finfoPtr; }; return 0; // Success } -int filext::filemgr::close(const std::string& fName) -{ - LOG("close(%s)\n", fName.c_str()); +int filext::filemgr::close(const std::filesystem::path& filepath) { + LOG("close(%s)\n", filepath.c_str()); // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { LOG(" FOUND in m_fileMap\n"); delete (search->second); @@ -67,13 +58,11 @@ int filext::filemgr::close(const std::string& fName) return 0; // Success } -int filext::filemgr::set(const std::string& fName, const char* key, const char* value) -{ - LOG("set(%s, %s, %s)\n", fName.c_str(), key, value); +int filext::filemgr::set(const std::filesystem::path& filepath, const char* key, const char* value) { + LOG("set(%s, %s, %s)\n", filepath.c_str(), key, value); // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { fileInfo* finfo = search->second; finfo->m_map[string(key)] = string(value); @@ -85,13 +74,11 @@ int filext::filemgr::set(const std::string& fName, const char* key, const char* }; } -int filext::filemgr::eraseKey(const std::string& fName, const char* key) -{ - LOG("eraseKey(%s, %s)\n", fName.c_str(), key); +int filext::filemgr::eraseKey(const std::filesystem::path& filepath, const char* key) { + LOG("eraseKey(%s, %s)\n", filepath.c_str(), key); // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { fileInfo* finfo = search->second; finfo->m_map.erase(string(key)); @@ -103,15 +90,13 @@ int filext::filemgr::eraseKey(const std::string& fName, const char* key) }; } -int filext::filemgr::get(const std::string& fName, const char* key, string& outValue, unsigned int outputSize, bool reset) -{ - LOG("get(%s, %s, reset: %i, outputSize: %i)\n", fName.c_str(), key, reset, outputSize); +int filext::filemgr::get(const std::filesystem::path& filepath, const char* key, string& outValue, unsigned int outputSize, bool reset) { + LOG("get(%s, %s, reset: %i, outputSize: %i)\n", filepath.c_str(), key, reset, outputSize); unsigned int nBytesToGet = outputSize - 1; // We need to reserve some space for null // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { fileInfo* finfo = search->second; string keyStr(key); @@ -159,17 +144,15 @@ int filext::filemgr::get(const std::string& fName, const char* key, string& outV } } -int filext::filemgr::write(const std::string& fName) -{ - LOG("write(%s)\n", fName.c_str()); +int filext::filemgr::write(const std::filesystem::path& filepath) { + LOG("write(%s)\n", filepath.c_str()); // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { fileInfo* finfo = search->second; - ofstream f(finfo->m_fileName, ios_base::out | ios_base::binary); + ofstream f(finfo->m_filepath, ios_base::out | ios_base::binary); char endKey[] = { 0 }; char endVal[] = { 0, '\n' }; if (f.is_open()) { @@ -207,17 +190,15 @@ int filext::filemgr::write(const std::string& fName) return 0; } -int filext::filemgr::read(const std::string& fName) -{ - LOG("read(%s)\n", fName.c_str()); +int filext::filemgr::read(const std::filesystem::path& filepath) { + LOG("read(%s)\n", filepath.c_str()); // Check if this file is already open - string fNameStr(fName); - auto search = m_fileMap.find(fNameStr); + auto search = m_fileMap.find(filepath); if (search != m_fileMap.end()) { fileInfo* finfo = search->second; - ifstream f(finfo->m_fileName, ios_base::in | ios_base::binary); + ifstream f(finfo->m_filepath, ios_base::in | ios_base::binary); if (f.is_open()) { // Read header LOG(" Reading header: "); diff --git a/FileXT/filemgr.h b/FileXT/filemgr.h index ef0d489..ca30089 100644 --- a/FileXT/filemgr.h +++ b/FileXT/filemgr.h @@ -6,15 +6,16 @@ Class which represents whole extension. #include #include +#include #include namespace filext { struct fileInfo { - fileInfo(std::string& fileName); + fileInfo(const std::filesystem::path& filepath); ~fileInfo(); - std::string m_fileName; + std::filesystem::path m_filepath; std::unordered_map m_map; unsigned int m_currentGetID; std::string m_currentGetKey; @@ -37,16 +38,16 @@ namespace filext class filemgr { public: - int open(const std::string& fName); - int close(const std::string& fName); - int set(const std::string& fName, const char* key, const char* value); - int get(const std::string& fName, const char* key, std::string& outValue, unsigned int outputSize, bool reset); - int eraseKey(const std::string& fName, const char* key); - int write(const std::string& fName); - int read(const std::string& fName); + int open(const std::filesystem::path& filepath); + int close(const std::filesystem::path& filepath); + int set(const std::filesystem::path& filepath, const char* key, const char* value); + int get(const std::filesystem::path& filepath, const char* key, std::string& outValue, unsigned int outputSize, bool reset); + int eraseKey(const std::filesystem::path& filepath, const char* key); + int write(const std::filesystem::path& filepath); + int read(const std::filesystem::path& filepath); private: - std::map m_fileMap; + std::map m_fileMap; }; }; diff --git a/FileXT/utility.cpp b/FileXT/utility.cpp new file mode 100644 index 0000000..fdad6be --- /dev/null +++ b/FileXT/utility.cpp @@ -0,0 +1,426 @@ +#include "pch.h" +#include "utility.h" + +#include "common.h" +#include "platform.h" + +#if defined(WIN32) +#include +#include +#include +#include +#ifndef getcwd +#define getcwd _getcwd +#endif +#endif + +#ifdef __linux__ +#include +#include +#include +#include +#include +#endif + +#include + +namespace filext +{ + // Because f* you that's why + std::filesystem::path BuildProfilePath(const std::filesystem::path& folder, const std::string& name) { + std::filesystem::path path; + + // No Folder, No Name + // Linux: "~share/Arma 3 - Other Profiles/Player" + // Windows: "%UserProfile%\Documents\Arma 3" + if (folder.empty() && name.empty()) + { + path = getSystemUserFolder(); +#ifdef _WIN32 + path /= "Arma 3"; +#elif defined(__linux__) + path /= "Arma 3 - Other Profiles"; + path.append(getDefaultProfileName()); +#endif + } + + // Absolute Folder, No Name + // Linux: "$folder/home/Player" + // Windows: "$folder\users\%AccountName%" + else if (!folder.empty() && folder.is_absolute() && name.empty()) + { + path = folder; +#ifdef _WIN32 + path /= "users"; +#elif defined(__linux__) + path /= "home"; +#endif + path.append(getDefaultProfileName()); + } + + // Relative Folder, No Name + // Linux: "%ArmaRoot%/$folder/home/Player" + // Windows: "%ArmaRoot%\$folder\users\%AccountName%" + else if (!folder.empty() && folder.is_relative() && name.empty()) + { + path = process::getArmaRootFolder() / folder; +#ifdef _WIN32 + path /= "users"; +#elif defined(__linux__) + path /= "home"; +#endif + path.append(getDefaultProfileName()); + } + + // No Folder, Name + // Linux: "~share/Arma 3 - Other Profiles/$name" + // Windows: "%UserProfile%\Documents\Arma 3 - Other Profiles\$name" + else if (folder.empty() && !name.empty()) + { + path = getSystemUserFolder() / "Arma 3 - Other Profiles"; + path.append(convertName(name)); + } + + // Absolute Folder, Name + // Linux: "$folder/home/$name" + // Windows: "$folder\users\$name" + else if (!folder.empty() && folder.is_absolute() && !name.empty()) + { + path = folder; +#ifdef _WIN32 + path /= "users"; +#elif defined(__linux__) + path /= "home"; +#endif + path /= convertName(name); + } + + // Relative Folder, Name + // Linux: "%ArmaRoot%/$folder/home/$name" + // Windows: "%ArmaRoot%\$folder\users\$name" + else if (!folder.empty() && folder.is_relative() && !name.empty()) + { + path = process::getArmaRootFolder() / folder; +#ifdef _WIN32 + path /= "users"; +#elif defined(__linux__) + path /= "home"; +#endif + path /= convertName(name); + } + + return path; + } + + std::string convertName(const std::string& name) { + std::ostringstream escaped; + escaped.fill('0'); + escaped << std::hex; + + constexpr std::array whitelist { + '!', '$', '^', '&', '(', ')', '~', '#', '@', '{', '}', '[', ']', '_', '+', '\'' + }; + + for (std::string::const_iterator i = name.begin(), n = name.end(); i != n; ++i) { + std::string::value_type c = (*i); + + // Keep alphanumeric and other accepted characters intact + if (std::isalnum(c) || std::find(whitelist.cbegin(), whitelist.cend(), c) != whitelist.cend()) { + escaped << c; + continue; + } + else if (c == '%') { + escaped << "%%"; + continue; + } + + // Any other characters are percent-encoded + escaped << '%' << std::setw(2) << int((unsigned char)c); + } + + return escaped.str(); + } + + std::filesystem::path findProfileStorage() { + std::filesystem::path path; + + // Find profile folder and name arguments in Arma process command line + std::string profileName; + std::string profileFolder; + + for (auto& arg : process::tryGetCommandLineArgs()) { + if (profileName.empty() && string::startsWith(arg, "-name=")) { + profileName = arg.substr(6); + } + else if (profileFolder.empty() && string::startsWith(arg, "-profiles=")) { + profileFolder = arg.substr(10); + } + + if (!profileName.empty() && !profileFolder.empty()) { + break; + } + } + + std::filesystem::path profilePath = BuildProfilePath(profileFolder, profileName); + if (std::filesystem::exists(profilePath)) { + path = profilePath / "storage"; + if (!std::filesystem::exists(path)) { + std::filesystem::create_directories(path); + } + } + + return path; + } + + std::filesystem::path findModStorage() { + std::filesystem::path modStorage; + +#ifdef _WIN32 + // Borrowed from https://gist.github.com/pwm1234/05280cf2e462853e183d + char path[1024]; + HMODULE hm = NULL; + + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR)findModStorage, + &hm)) { + LOG("error: GetModuleHandle returned %i\n", GetLastError()); + } + else { + GetModuleFileNameA(hm, path, sizeof(path)); + modStorage = path; + } +#elif defined(__linux__) + Dl_info dl_info; + dladdr((void*)findModStorage, &dl_info); + modStorage = dl_info.dli_fname; +#endif + + if (!modStorage.empty()) { + modStorage = modStorage.parent_path() / "storage"; // Strip binary name + } + + if (!std::filesystem::exists(modStorage)) { + std::filesystem::create_directories(modStorage); + } + + return modStorage; + } + + std::string getDefaultProfileName() { + std::string name; +#ifdef _WIN32 + char username[UNLEN + 1]; + DWORD username_len = UNLEN + 1; + GetUserName(username, &username_len); + name = username; +#elif defined(__linux__) + name = "Player"; +#endif + return name; + } + + const std::filesystem::path& getLogPath() { + static std::filesystem::path logPath; + + if (logPath.empty()) { + logPath = getStorage().parent_path(); // Storage is always a sub-directory + } + + return logPath; + } + + const std::filesystem::path& getStorage() { + static std::filesystem::path storage; + + if (storage.empty()) { + // If locally hosted (hosted via client) - don't try to find profile storage + const std::string executable = process::getProcessExecutable().filename().string(); + if (string::startsWith(executable, "arma3server")) + { + auto profileFolder = findProfileStorage(); + if (!profileFolder.empty()) { + storage = profileFolder; + return storage; + } + } + + auto modFolder = findModStorage(); + if (!modFolder.empty()) { + storage = modFolder; + return storage; + } + + if (!std::filesystem::exists(storage)) { + throw new std::runtime_error("Failed to find a suitable storage folder."); + } + } + + return storage; + } + + std::filesystem::path getSystemUserFolder() { + std::filesystem::path userDir; +#ifdef _WIN32 + // Folder = Documents + PWSTR documentPath = nullptr; + HRESULT hresult = SHGetKnownFolderPath(FOLDERID_Documents, NULL, NULL, &documentPath); + if (SUCCEEDED(hresult)) { + std::string temp; + size_t len = wcstombs(nullptr, documentPath, 0); + temp.resize(len); + std::wcstombs(temp.data(), documentPath, len); + userDir = temp; + } + CoTaskMemFree(documentPath); +#elif defined(__linux__) + // Folder = /home/~user/.local/share + struct passwd* record = getpwuid(geteuid()); + userDir.append(record->pw_dir) + .append(".local") + .append("share"); +#endif + return userDir; + } + + void tryMigrateStorageFolder() { + static bool isMigrated = false; + + if (!isMigrated) { + isMigrated = true; + + static const std::filesystem::path& oldStorage = findModStorage(); + static const std::filesystem::path& storage = getStorage(); + + // Don't migrate if we fell back to mod storage + if (oldStorage != storage) { + + // Migrate any files from deprecated storage + if (std::filesystem::exists(oldStorage)) { + for (const auto& dirEntry : std::filesystem::directory_iterator(oldStorage)) { + const std::filesystem::path storageFile = storage / dirEntry.path().filename(); + + // Don't overwrite any files that may already exist + if (!std::filesystem::exists(storageFile)) { + std::filesystem::copy(dirEntry, storageFile); + } + } + } + } + } + } +} + +namespace process +{ +#if defined(_WIN32) + bool convertWinArg(const wchar_t* in, std::string& out) { + int clen = WideCharToMultiByte(CP_UTF8, 0, in, -1, NULL, 0, NULL, NULL) - 1; // -1 to not write the null terminator + out.resize(clen); + + if (WideCharToMultiByte(CP_UTF8, 0, in, clen, out.data(), clen, NULL, NULL) != clen) { + return false; + } + + return true; + } +#endif + +#if defined(__linux__) + bool convertLinuxArg(const std::string& in, std::string& out) + { + iconv_t conv = iconv_open("UTF-8", "WINDOWS-1252"); + if (conv != (iconv_t)-1) { + out.resize(in.size() * 4); + + // Mutable 'cursors' for iconv + char* pIn = const_cast(in.data()); // inbuffer isn't mutated + size_t inLen = in.size(); + char* pOut = out.data(); + size_t outLen = out.size(); + + while (true) { + size_t rc = iconv(conv, &pIn, &inLen, &pOut, &outLen); + if (rc == (size_t)-1) { + if (inLen > 0) { + // HACK: Attempt direct copy with ex code + *pOut = 0xC2; ++pOut; --outLen; + *pOut = *pIn; ++pOut; --outLen; + ++pIn; --inLen; + continue; + } + } + + break; + } + + iconv_close(conv); + return true; + } + + return false; + } +#endif + + std::filesystem::path getProcessExecutable() { + char szPath[1024]; +#ifdef _WIN32 + GetModuleFileNameA(NULL, szPath, 1024); +#elif defined(__linux__) + readlink("/proc/self/exe", szPath, 1024); +#endif + return szPath; + } + + std::filesystem::path getArmaRootFolder() { + return getProcessExecutable().parent_path(); + } + + std::vector tryGetCommandLineArgs() { + std::vector args; + +#ifdef _WIN32 + int n_args; + wchar_t** argv16 = CommandLineToArgvW(GetCommandLineW(), &n_args); + + if (argv16 != NULL) { + for (int i = 0; i < n_args; i++) { + wchar_t* arg = argv16[i]; + std::string converted; + if (convertWinArg(arg, converted)) { + args.emplace_back(converted); + } + } + } + LocalFree(argv16); + +#elif defined(__linux__) + // Taken from DTool library in Panda3D + std::ifstream proc("/proc/self/cmdline"); + if (!proc.fail()) { + int ch = proc.get(); + int index = 0; + while (!proc.eof() && !proc.fail()) { + std::string arg; + + while (!proc.eof() && !proc.fail() && ch != '\0') { + arg += (char)ch; + ch = proc.get(); + } + + if (index > 0) { + std::string converted; + if (convertLinuxArg(arg.c_str(), converted)) { + args.push_back(converted); + } + } + + index++; + + ch = proc.get(); + } + } +#endif + return args; + } +} \ No newline at end of file diff --git a/FileXT/utility.h b/FileXT/utility.h new file mode 100644 index 0000000..b0f8505 --- /dev/null +++ b/FileXT/utility.h @@ -0,0 +1,197 @@ +#pragma once + +#include +#include +#include +#include + +namespace string +{ + /// + /// Trims the string of all /tokens/ from the start of the string to the first character that isn't in /tokens/. + /// This functions modifies the original string. + /// + /// String to trim + /// String of individual characters to trim + inline void ltrim(std::string& s, std::string tokens) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [&tokens](unsigned char ch) { + return tokens.find(ch) == std::string::npos; + })); + } + + /// + /// Trims the string of all /tokens/ from the end of the string to the first character that isn't in /tokens/. + /// This functions modifies the original string. + /// + /// String to trim + /// String of individual characters to trim + inline void rtrim(std::string& s, std::string tokens) { + s.erase(std::find_if(s.rbegin(), s.rend(), [&tokens](unsigned char ch) { + return tokens.find(ch) == std::string::npos; + }).base(), s.end()); + } + + /// + /// Trims the string of all /tokens/ from the start and end of the string to the first character on each side that isn't in /tokens/. + /// This functions modifies the original string. + /// + /// String to trim + /// String of individual characters to trim + inline void trim(std::string& s, std::string tokens) { + ltrim(s, tokens); + rtrim(s, tokens); + } + + /// + /// Trims the string of all /tokens/ from the start of the string to the first character that isn't in /tokens/. + /// This function creates a new string. + /// + /// String to trim + /// String of individual characters to trim + inline std::string trim(const char* szInput, std::string tokens) { + std::string s(szInput); + ltrim(s, tokens); + rtrim(s, tokens); + return s; + } + + /// + /// Checks if /token/ appears at the immediate start of /str/. + /// This check is case-sensitive. + /// + /// The string to check for /token/. + /// The string to find. + /// True if /token/ appears at the immediate start of /str/. False otherwise. + inline bool startsWith(const std::string& str, const std::string& token) { + if (str.size() < token.size()) { + return false; + } + + for (int i = 0; i < token.size(); i++) { + if (str.at(i) != token.at(i)) { + return false; + } + } + + return true; + } + + /// + /// Replaces occurrences of token with replacement in subject + /// + /// The subject to execute replacements on + /// The token to replace + /// The replacement to insert + inline void replaceAll(std::string& subject, const std::string& token, const std::string& replacement) { + if (token.empty()) + return; + size_t start_pos = 0; + while ((start_pos = subject.find(token, start_pos)) != std::string::npos) { + subject.replace(start_pos, token.length(), replacement); + start_pos += replacement.length(); + } + } +} + +namespace filext +{ + /// + /// Depending on platform and profile related command-line arguments, this + /// function builds a path to the current profile folder. + /// + /// Profile folder path. + std::filesystem::path BuildProfilePath(const std::filesystem::path& folder, const std::string& name); + + /// + /// Converts characters of the -name argument the same as the Arma executable does + /// + /// The string of the -name argument + /// A converted string that should match Arma's conversion + std::string convertName(const std::string& name); + + /// + /// Attempts to get the folder that holds the Arma Server profile data. + /// If it cannot find the Profile folder, it will fall-back to the mod folder. + /// On total failure, this function raises a std::runtime_error. + /// + /// Profile folder path. + std::filesystem::path findProfileStorage(); + + /// + /// Attempts to get the parent folder of the FileXT DLL. (FileXT mod folder). + /// + /// Folder path of FileXT mod. Empty path on failure. + std::filesystem::path findModStorage(); + + /// + /// Attempts to get the profile name that would be used by default for the + /// server if no profile name was specified on the command-line. + /// + /// Profile name + std::string getDefaultProfileName(); + + /// + /// Gets (and stores internally) a suitable path for the log file. (Stored in storage directory). + /// + /// + const std::filesystem::path& getLogPath(); + + /// + /// Gets (and stores internally) a suitable location for storage data. + /// This will either be the server profile folder, or the FileXT mod folder. + /// + /// Path to storage location. + const std::filesystem::path& getStorage(); + + /// + /// Returns the path to the default user folder for the host platform. + /// + /// Path to the user folder. + std::filesystem::path getSystemUserFolder(); + + /// + /// Tries to migrate any files from the mod folder to the new profile folder. + /// + void tryMigrateStorageFolder(); +} + +namespace process +{ +#ifdef _WIN32 + /// + /// Converts a string the same way Arma does (an incorrect Unicode conversion) + /// + /// The wide string as provided to the application (original) + /// Reference to string for the converted output + /// True if the argument was converted successfully + bool convertWinArg(const wchar_t* in, std::string& out); +#endif + +#ifdef __linux__ + /// + /// Converts a string the same way Arma does (an incorrect Unicode conversion) + /// + /// The string as provided to the application (original) + /// Reference to string for the converted output + /// True if the argument was converted successfully + bool convertLinuxArg(const std::string& in, std::string& out); +#endif + + /// + /// Returns the full path of the current executing process. (eg, arma3server_x64.exe) + /// + /// Path to the current process executable. + std::filesystem::path getProcessExecutable(); + + /// + /// Returns the parent path of the current executing process. (Arma root folder). + /// + /// Path to parent folder of current process. + std::filesystem::path getArmaRootFolder(); + + /// + /// Attempts to get the command line arguments used to call this process. + /// + /// Contiguous list of arguments used to start this process + std::vector tryGetCommandLineArgs(); +} \ No newline at end of file