diff options
author | B. Stack <bgstack15@gmail.com> | 2023-04-03 09:56:38 -0400 |
---|---|---|
committer | B. Stack <bgstack15@gmail.com> | 2023-04-03 09:56:38 -0400 |
commit | 765e1bb3bdeb5c49f08543c20206e55c772e6b80 (patch) | |
tree | 920f288910890016e540213fe65f2d6f38aa82be | |
parent | add upstream 12.1 (diff) | |
download | FreeFileSync-765e1bb3bdeb5c49f08543c20206e55c772e6b80.tar.gz FreeFileSync-765e1bb3bdeb5c49f08543c20206e55c772e6b80.tar.bz2 FreeFileSync-765e1bb3bdeb5c49f08543c20206e55c772e6b80.zip |
add upstream 12.212.2
36 files changed, 687 insertions, 574 deletions
diff --git a/Changelog.txt b/Changelog.txt index 9235d34a..8621e5ac 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,15 @@ +FreeFileSync 12.2 [2023-04-02] +------------------------------ +Fixed temporary access error when creating multiple folders in parallel +Log failure to copy folder attributes as warning only +Enable UTF-8, even if FTP server does not advertize in FEAT (vsftpd) +Fixed drag and drop for non-ASCII folders (macOS) +Explicitly detect MTP path without existence check +Fixed crash when parsing SFTP package from stream +Revert back to GTK2 build due to GTK3 hangs on KDE (Linux) +Fixed missing COM initialization for MTP path parsing + + FreeFileSync 12.1 [2023-02-20] ------------------------------ First official build based on GTK3 (Linux) diff --git a/FreeFileSync/Build/Resources/Languages.zip b/FreeFileSync/Build/Resources/Languages.zip Binary files differindex a6e9f1e8..6bcd626d 100644 --- a/FreeFileSync/Build/Resources/Languages.zip +++ b/FreeFileSync/Build/Resources/Languages.zip diff --git a/FreeFileSync/Source/RealTimeSync/config.cpp b/FreeFileSync/Source/RealTimeSync/config.cpp index 75b819ce..951aabd2 100644 --- a/FreeFileSync/Source/RealTimeSync/config.cpp +++ b/FreeFileSync/Source/RealTimeSync/config.cpp @@ -8,7 +8,8 @@ #include <zen/file_access.h> #include <zen/process_exec.h> #include <zenxml/xml.h> -#include <wx/intl.h> +//#include <wx/intl.h> +#include <wx/uilocale.h> #include "../ffs_paths.h" #include "../localization.h" @@ -25,7 +26,7 @@ namespace zen template <> inline bool readText(const std::string& input, wxLanguage& value) { - if (const wxLanguageInfo* lngInfo = wxLocale::FindLanguageInfo(utfTo<wxString>(input))) + if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo<wxString>(input))) { value = static_cast<wxLanguage>(lngInfo->Language); return true; diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index 0caffe70..f20580e9 100644 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -254,6 +254,7 @@ rts::AbortReason rts::runFolderMonitor(const XmlRealConfig& config, const wxStri warn_static("maybe not a good idea!? job for execve? https://rachelbythebay.com/w/2017/01/30/env/") ::wxSetEnv(L"change_path", utfTo<wxString>(changedItemPath)); //crude way to report changed file ::wxSetEnv(L"change_action", actionName); // + warn_static("caveat: %change_path% is not subsituted 'thanks' to our *static* env variables! luckily there's a workaround") //https://freefilesync.org/forum/viewtopic.php?t=10160 auto cmdLineExp = expandMacros(cmdLine); try diff --git a/FreeFileSync/Source/afs/abstract.cpp b/FreeFileSync/Source/afs/abstract.cpp index 17dd4f4c..c7f20a92 100644 --- a/FreeFileSync/Source/afs/abstract.cpp +++ b/FreeFileSync/Source/afs/abstract.cpp @@ -8,6 +8,7 @@ #include <zen/serialize.h> #include <zen/guid.h> #include <zen/crc.h> +#include <zen/ring_buffer.h> #include <typeindex> using namespace zen; @@ -19,7 +20,7 @@ AfsPath fff::sanitizeDeviceRelativePath(Zstring relPath) { if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(relPath, Zstr('/'), FILE_NAME_SEPARATOR); if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(relPath, Zstr('\\'), FILE_NAME_SEPARATOR); - trim(relPath, true, true, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); + trim(relPath, TrimSide::both, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); return AfsPath(relPath); } @@ -248,47 +249,62 @@ AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& sourcePath, c void AFS::createFolderIfMissingRecursion(const AbstractPath& folderPath) //throw FileError { - //expect that path already exists (see: versioning, base folder, log file path) => check first - const std::variant<ItemType, AfsPath /*last existing parent path*/> typeOrPath = getItemTypeIfExists(folderPath); //throw FileError - - try + auto getItemType2 = [&](const AbstractPath& itemPath) //throw FileError { - if (const ItemType* type = std::get_if<ItemType>(&typeOrPath)) + try + { return getItemType(itemPath); } //throw FileError + catch (const FileError& e) //need to add context! { - if (*type == ItemType::file /*obscure, but possible*/) - throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPath)))); + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), + replaceCpy(e.toString(), L"\n\n", L'\n')); } - else - { - const AfsPath existingFolderPath = std::get<AfsPath>(typeOrPath); - assert(startsWith(folderPath.afsPath.value, existingFolderPath.value)); + }; + + try + { + //- path most likely already exists (see: versioning, base folder, log file path) => check first + //- do NOT use getItemTypeIfExists()! race condition when multiple threads are calling createDirectoryIfMissingRecursion(): https://freefilesync.org/forum/viewtopic.php?t=10137#p38062 + //- find first existing + accessible parent folder (backwards iteration): + AbstractPath folderPathEx = folderPath; + RingBuffer<Zstring> folderNames; //caveat: 1. might have been created in the meantime 2. getItemType2() may have failed with access error + for (;;) + try + { + if (getItemType2(folderPathEx) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPathEx)))); + break; + } + catch (FileError&) //not yet existing or access error + { + const std::optional<AbstractPath> parentPath = getParentPath(folderPathEx); + if (!parentPath)//device root => quick access test + throw; + folderNames.push_front(getItemName(folderPathEx)); + folderPathEx = *parentPath; + } + //----------------------------------------------------------- - const ZstringView relPathTmp = makeStringView(folderPath.afsPath.value.begin() + existingFolderPath.value.size(), folderPath.afsPath.value.end()); - const std::vector<ZstringView> relPathComp = splitCpy(relPathTmp, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); //sanitize! relPathTmp starts with FILE_NAME_SEPARATOR unless existingFolderPath.empty() - assert(!relPathComp.empty()); + AbstractPath folderPathNew = folderPathEx; + for (const Zstring& folderName : folderNames) + try + { + folderPathNew = appendRelPath(folderPathNew, folderName); - AbstractPath folderPathNew{folderPath.afsDevice, existingFolderPath}; - for (const ZstringView folderName : relPathComp) + createFolderPlain(folderPathNew); //throw FileError + } + catch (FileError&) + { try { - folderPathNew = appendRelPath(folderPathNew, Zstring(folderName)); - - createFolderPlain(folderPathNew); //throw FileError + if (getItemType2(folderPathNew) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPathNew)))); + else + continue; //already existing => possible, if createDirectoryIfMissingRecursion() is run in parallel } - catch (FileError&) - { - try - { - if (getItemType(folderPathNew) == ItemType::file /*obscure, but possible*/) //throw FileError - throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(folderPathNew)))); - else - continue; //already existing => possible, if createFolderIfMissingRecursion() is run in parallel - } - catch (FileError&) {} //not yet existing or access error + catch (FileError&) {} //not yet existing or access error - throw; - } - } + throw; + } } catch (const SysError& e) { @@ -297,13 +313,6 @@ void AFS::createFolderIfMissingRecursion(const AbstractPath& folderPath) //throw } -bool AFS::itemExists(const AfsPath& itemPath) const //throw FileError -{ - const std::variant<ItemType, AfsPath /*last existing parent path*/> typeOrPath = getItemTypeIfExists(itemPath); //throw FileError - return std::get_if<ItemType>(&typeOrPath); -} - - //default implementation: folder traversal void AFS::removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError const std::function<void(const std::wstring& displayPath)>& onBeforeFileDeletion /*throw X*/, // @@ -317,11 +326,18 @@ void AFS::removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileE { std::vector<Zstring> fileNames; std::vector<Zstring> symlinkNames; - - traverseFolder(folderPath2, //throw FileError - [&](const FileInfo& fi) { fileNames.push_back(fi.itemName); }, - [&](const FolderInfo& fi) { folderNames.push_back(fi.itemName); }, - [&](const SymlinkInfo& si) { symlinkNames.push_back(si.itemName); }); + try + { + traverseFolder(folderPath2, //throw FileError + [&](const FileInfo& fi) { fileNames.push_back(fi.itemName); }, + [&](const FolderInfo& fi) { folderNames.push_back(fi.itemName); }, + [&](const SymlinkInfo& si) { symlinkNames.push_back(si.itemName); }); + } + catch (const FileError& e) //add context + { + throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath2))), + replaceCpy(e.toString(), L"\n\n", L'\n')); + } for (const Zstring& fileName : fileNames) { @@ -352,9 +368,20 @@ void AFS::removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileE }; //-------------------------------------------------------------------------------------------------------------- - //no error situation if directory is not existing! manual deletion relies on it! - const std::variant<ItemType, AfsPath /*last existing parent path*/> typeOrPath = getItemTypeIfExists(folderPath); //throw FileError - if (const ItemType* type = std::get_if<ItemType>(&typeOrPath)) + const std::optional<ItemType> type = [&] + { + try + { + return getItemTypeIfExists(folderPath); //throw FileError + } + catch (const FileError& e) //add context + { + throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), + replaceCpy(e.toString(), L"\n\n", L'\n')); + } + }(); + + if (type) { assert(*type != ItemType::symlink); @@ -368,7 +395,7 @@ void AFS::removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileE else removeFolderRecursionImpl(folderPath); //throw FileError } - else //even if the folder does not exist anymore, significant I/O work was done => report + else //no error situation if directory is not existing! manual deletion relies on it! significant I/O work was done => report: if (onBeforeFolderDeletion) onBeforeFolderDeletion(getDisplayPath(folderPath)); //throw X } diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index c5e8305f..72161695 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -9,7 +9,7 @@ #include <functional> #include <chrono> -#include <variant> +//#include <variant> #include <zen/file_error.h> #include <zen/file_path.h> #include <zen/serialize.h> //InputStream/OutputStream support buffered stream concept @@ -97,10 +97,10 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t // - all child item path parts must correspond to folder traversal // => conclude whether an item is *not* existing anymore by doing a *case-sensitive* name search => potentially SLOW! // - root path? => do access test - static std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExists(const AbstractPath& itemPath) + static std::optional<ItemType> getItemTypeIfExists(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().getItemTypeIfExists(itemPath.afsPath); } //throw FileError - static bool itemExists(const AbstractPath& itemPath) { return itemPath.afsDevice.ref().itemExists(itemPath.afsPath); } //throw FileError + static bool itemExists(const AbstractPath& itemPath) { return static_cast<bool>(getItemTypeIfExists(itemPath)); } //throw FileError //---------------------------------------------------------------------------------------------------------------- //already existing: fail @@ -288,9 +288,13 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //accummulated delta != file size! consider ADS, sparse, compressed files const zen::IoCallback& notifyUnbufferedIO /*throw X*/); + struct FolderCopyResult + { + std::optional<zen::FileError> errorAttribs; + }; //already existing: fail //symlink handling: follow - static void copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions); //throw FileError + static FolderCopyResult copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions); //throw FileError //already existing: fail static void copySymlink(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions); //throw FileError @@ -332,8 +336,6 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t protected: - bool itemExists(const AfsPath& itemPath) const; //throw FileError - //default implementation: folder traversal virtual void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError const std::function<void(const std::wstring& displayPath)>& onBeforeFileDeletion, @@ -365,7 +367,7 @@ private: //---------------------------------------------------------------------------------------------------------------- virtual ItemType getItemType(const AfsPath& itemPath) const = 0; //throw FileError - virtual std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExists(const AfsPath& itemPath) const = 0; //throw FileError + virtual std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const = 0; //throw FileError //already existing: fail virtual void createFolderPlain(const AfsPath& folderPath) const = 0; //throw FileError @@ -406,7 +408,7 @@ private: //symlink handling: follow //already existing: fail - virtual void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const = 0; //throw FileError + virtual FolderCopyResult copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const = 0; //throw FileError //already existing: fail virtual void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const = 0; //throw FileError @@ -541,22 +543,24 @@ void AbstractFileSystem::moveAndRenameItem(const AbstractPath& pathFrom, const A inline -void AbstractFileSystem::copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) //throw FileError +AbstractFileSystem::FolderCopyResult AbstractFileSystem::copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) //throw FileError { using namespace zen; - if (typeid(sourcePath.afsDevice.ref()) != typeid(targetPath.afsDevice.ref())) + if (typeid(sourcePath.afsDevice.ref()) != typeid(targetPath.afsDevice.ref())) //fall back: { - //fall back: - if (copyFilePermissions) - throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(getDisplayPath(targetPath))), - _("Operation not supported between different devices.")); - //already existing: fail createFolderPlain(targetPath); //throw FileError + + FolderCopyResult result; + if (copyFilePermissions) + result.errorAttribs = FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(getDisplayPath(targetPath))), + _("Operation not supported between different devices.")); + + return result; } else - sourcePath.afsDevice.ref().copyNewFolderForSameAfsType(sourcePath.afsPath, targetPath, copyFilePermissions); //throw FileError + return sourcePath.afsDevice.ref().copyNewFolderForSameAfsType(sourcePath.afsPath, targetPath, copyFilePermissions); //throw FileError } diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index 0bb9ee99..e8225790 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -317,10 +317,10 @@ public: //returns server response (header data) std::string perform(const AfsPath& itemPath, bool isDir, curl_ftpmethod pathMethod, - const std::vector<CurlOption>& extraOptions, bool requiresUtf8) //throw SysError, SysErrorPassword, SysErrorFtpProtocol + const std::vector<CurlOption>& extraOptions, bool requestUtf8) //throw SysError, SysErrorPassword, SysErrorFtpProtocol { - if (requiresUtf8) //avoid endless recursion - requestUtf8(); //throw SysError, SysErrorFtpProtocol + if (requestUtf8) //avoid endless recursion + initUtf8(); //throw SysError, SysErrorFtpProtocol if (!easyHandle_) { @@ -509,8 +509,8 @@ public: //======================================================================================================= const CURLcode rcPerf = ::curl_easy_perform(easyHandle_); - //WTF: curl_easy_perform() considers FTP response codes 4XX, 5XX as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!! - //note: CURLOPT_FAILONERROR(default:off) is only available for HTTP => BUT at least we can prefix FTP commands with * for same effect + //WTF: curl_easy_perform() considers FTP response codes >= 400 as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!! + //note: CURLOPT_FAILONERROR(default:off) is only available for HTTP => BUT at least we can prefix FTP commands with * for same effect: https://curl.se/libcurl/c/CURLOPT_QUOTE.html if (socketException) throw* socketException; //throw SysError @@ -536,9 +536,11 @@ public: if (rcPerf == CURLE_LOGIN_DENIED) throw SysErrorPassword(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); - long ftpStatusCode = CURLE_OK; //optional + long ftpStatusCode = 0; //optional /*const CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &ftpStatusCode); - if (ftpStatusCode != CURLE_OK) + //https://en.wikipedia.org/wiki/List_of_FTP_server_return_codes + assert(ftpStatusCode == 0 || 400 <= ftpStatusCode && ftpStatusCode < 600); + if (ftpStatusCode != 0) throw SysErrorFtpProtocol(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg), ftpStatusCode); throw SysError(formatSystemError("curl_easy_perform", formatCurlStatusCode(rcPerf), errorMsg)); @@ -549,7 +551,7 @@ public: } //returns server response (header data) - std::string runSingleFtpCommand(const std::string& ftpCmd, bool requiresUtf8) //throw SysError, SysErrorFtpProtocol + std::string runSingleFtpCommand(const std::string& ftpCmd, bool requestUtf8) //throw SysError, SysErrorFtpProtocol { curl_slist* quote = nullptr; ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(quote)); @@ -559,7 +561,7 @@ public: { {CURLOPT_NOBODY, 1L}, {CURLOPT_QUOTE, quote}, - }, requiresUtf8); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }, requestUtf8); //throw SysError, SysErrorPassword, SysErrorFtpProtocol } void testConnection() //throw SysError @@ -574,11 +576,10 @@ public: ... and it tells! FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU https://freefilesync.org/forum/viewtopic.php?t=8041 */ //=> '*' to the rescue: as long as we get an FTP response - *any* FTP response (including 550) - the connection itself is fine! - const std::string& featBuf = runSingleFtpCommand("*FEAT", false /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + const std::string& featBuf = runSingleFtpCommand("*FEAT", false /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol for (const std::string_view& line : splitFtpResponse(featBuf)) - if (startsWith(line, "211-") || - startsWith(line, "211 ") || + if (startsWith(line, "211 ") || startsWith(line, "500 ") || startsWith(line, "550 ")) return; @@ -608,7 +609,7 @@ public: easyHandle_ = nullptr; } - const std::string& pwdBuf = runSingleFtpCommand("PWD", true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + const std::string& pwdBuf = runSingleFtpCommand("PWD", true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol for (const std::string_view& line : splitFtpResponse(pwdBuf)) if (startsWith(line, "257 ")) @@ -644,7 +645,7 @@ public: if (*currentSocket == binaryEnabledSocket_) return; - runSingleFtpCommand("TYPE I", false /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + runSingleFtpCommand("TYPE I", false /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol //make sure our binary-enabled session is still there (== libcurl behaves as we expect) std::optional<curl_socket_t> currentSocket = getActiveSocket(); //throw SysError @@ -661,7 +662,14 @@ public: bool supportsMlsd() { return getFeatureSupport(&Features::mlsd); } // bool supportsMfmt() { return getFeatureSupport(&Features::mfmt); } //throw SysError bool supportsClnt() { return getFeatureSupport(&Features::clnt); } // - bool supportsUtf8() { return getFeatureSupport(&Features::utf8); } // + bool supportsUtf8() + { + if (getFeatureSupport(&Features::utf8)) + return true; + + initUtf8(); //vsFTPd (ftp.sunet.se): supports UTF8 via "OPTS UTF8 ON", even if "UTF8" is missing from "FEAT" + return socketUsesUtf8_; + } bool isHealthy() const { @@ -697,7 +705,7 @@ public: case ServerEncoding::utf8: if (!isValidUtf(str)) - throw SysError(_("Invalid character encoding:") + L" [UTF-8] " + utfTo<std::wstring>(str)); + throw SysError(_("Invalid character encoding:") + L' ' + utfTo<std::wstring>(str) + L' ' + _("Expected:") + L" [UTF-8]"); return utfTo<Zstring>(str); @@ -724,7 +732,7 @@ public: case ServerEncoding::utf8: //validate! we consider REPLACEMENT_CHAR as indication for server using ANSI encoding in serverToUtfEncoding() if (!isValidUtf(str)) - throw SysError(_("Invalid character encoding:") + (sizeof(str[0]) == 1 ? L" [UTF-8] " : L" [UTF-16] ") + utfTo<std::wstring>(str)); + throw SysError(_("Invalid character encoding:") + L' ' + utfTo<std::wstring>(str) + L' ' + _("Expected:") + (sizeof(str[0]) == 1 ? L" [UTF-8]" : L" [UTF-16]")); static_assert(sizeof(str[0]) == 1 || sizeof(str[0]) == 2); return utfTo<std::string>(str); @@ -775,29 +783,42 @@ private: return path; } - void requestUtf8() //throw SysError, SysErrorFtpProtocol + void initUtf8() //throw SysError, SysErrorFtpProtocol { - //Some RFC-2640-non-compliant servers require UTF8 to be explicitly enabled: https://wiki.filezilla-project.org/Character_Encoding#Conflicting_specification - //e.g. this one (Microsoft FTP Service): https://freefilesync.org/forum/viewtopic.php?t=4303 + /* 1. Some RFC-2640-non-compliant servers require UTF8 to be explicitly enabled: https://wiki.filezilla-project.org/Character_Encoding#Conflicting_specification + - e.g. Microsoft FTP Service: https://freefilesync.org/forum/viewtopic.php?t=4303 - //check supportsUtf8()? no, let's *always* request UTF8, even if server does not set UTF8 in FEAT response! e.g. like https://freefilesync.org/forum/viewtopic.php?t=9564 - //=> should not hurt + we rely on auto-encoding detection anyway; see serverToUtfEncoding() + 2. Others do not advertize "UTF8" in "FEAT", but *still* allow enabling it via "OPTS UTF8 ON": + - https://freefilesync.org/forum/viewtopic.php?t=9564 + - vsFTPd: ftp.sunet.se https://security.appspot.com/vsftpd.html#download - //"OPTS UTF8 ON" needs to be activated each time libcurl internally creates a new session - //hopyfully libcurl will offer a better solution: https://github.com/curl/curl/issues/1457 + "OPTS UTF8 ON" needs to be activated each time libcurl internally creates a new session + hopyfully libcurl will offer a better solution: https://github.com/curl/curl/issues/1457 */ if (std::optional<curl_socket_t> currentSocket = getActiveSocket()) //throw SysError if (*currentSocket == utf8RequestedSocket_) //caveat: a non-UTF8-enabled session might already exist, e.g. from a previous call to supportsMlsd() return; - //some servers even require "CLNT" before accepting "OPTS UTF8 ON": https://social.msdn.microsoft.com/Forums/en-US/d602574f-8a69-4d69-b337-52b6081902cf/problem-with-ftpwebrequestopts-utf8-on-501-please-clnt-first + //some (broken!?) servers require "CLNT" before accepting "OPTS UTF8 ON": https://social.msdn.microsoft.com/Forums/en-US/d602574f-8a69-4d69-b337-52b6081902cf/problem-with-ftpwebrequestopts-utf8-on-501-please-clnt-first if (supportsClnt()) //throw SysError - runSingleFtpCommand("CLNT FreeFileSync", false /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + runSingleFtpCommand("CLNT FreeFileSync", false /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol //"prefix the command with an asterisk to make libcurl continue even if the command fails" - //-> ignore if server does not know this legacy command (but report all *other* issues; else getActiveSocket() below won't return value and hide real error!) - //"If an RFC 2640 compliant client sends OPTS UTF-8 ON, it has to use UTF-8 regardless whether OPTS UTF-8 ON succeeds or not. " - runSingleFtpCommand("*OPTS UTF8 ON", false /*requiresUtf8*/); //throw SysError, (SysErrorFtpProtocol) + //-> ignore if server does not know this legacy command (but report all *other* issues; else getActiveSocket() below won't have a socket and we've hidden the real error!) + const std::string& optsBuf = runSingleFtpCommand("*OPTS UTF8 ON", false /*requestUtf8*/); //throw SysError, (SysErrorFtpProtocol) + + //get *last* FTP status code (can there be more than one!?) + int ftpStatusCode = 0; + for (const std::string_view& line : splitFtpResponse(optsBuf)) + if (line.size() >= 4 && + isDigit(line[0]) && + isDigit(line[1]) && + isDigit(line[2]) && + line[3] == ' ') + ftpStatusCode = stringTo<int>(line); + + socketUsesUtf8_ = ftpStatusCode == 200 || //"200 Always in UTF8 mode." "200 UTF8 set to on" + ftpStatusCode == 202; //"202 UTF8 mode is always enabled." //make sure our Unicode-enabled session is still there (== libcurl behaves as we expect) std::optional<curl_socket_t> currentSocket = getActiveSocket(); //throw SysError @@ -852,8 +873,8 @@ private: if (!featureCache_) { //*: ignore error if server does not support/allow FEAT - featureCache_ = parseFeatResponse(runSingleFtpCommand("*FEAT", false /*requiresUtf8*/)); //throw SysError, (SysErrorFtpProtocol) - //used by requestUtf8()! => requiresUtf8 = false!!! + featureCache_ = parseFeatResponse(runSingleFtpCommand("*FEAT", false /*requestUtf8*/)); //throw SysError, (SysErrorFtpProtocol) + //used by initUtf8()! => requestUtf8 = false!!! sf->access([&](FeatureList& feat) { feat.emplace(sessionCfg_.deviceId.server, *featureCache_); }); } @@ -884,7 +905,7 @@ private: //https://tools.ietf.org/html/rfc3659#section-7.8 //"a server-FTP process that supports MLST, and MLSD [...] MUST indicate that this support exists" //"there is no distinct FEAT output for MLSD. The presence of the MLST feature indicates that both MLST and MLSD are supported" - if (equalAsciiNoCase (line, " MLST") || + if (equalAsciiNoCase (line, " MLST") || startsWithAsciiNoCase(line, " MLST ") || //SP "MLST" [SP factlist] CRLF //so much the theory. In practice FTP server implementers can't read (specs): https://freefilesync.org/forum/viewtopic.php?t=6752 equalAsciiNoCase(line, " MLSD")) @@ -913,6 +934,8 @@ private: curl_socket_t utf8RequestedSocket_ = 0; curl_socket_t binaryEnabledSocket_ = 0; + bool socketUsesUtf8_ = false; + ServerEncoding encoding_ = ServerEncoding::unknown; std::optional<Features> featureCache_; @@ -1143,7 +1166,7 @@ FtpItem getFtpSymlinkInfo(const FtpLogin& login, const AfsPath& linkPath) //thro session.ensureBinaryMode(); //throw SysError //...or some server return ASCII size or fail with '550 SIZE not allowed in ASCII mode: https://freefilesync.org/forum/viewtopic.php?t=7669&start=30#p27742 const std::string sizeBuf = session.runSingleFtpCommand("*SIZE " + session.getServerPathInternal(linkPath), - true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol //alternative: use libcurl + CURLINFO_CONTENT_LENGTH_DOWNLOAD_T? => nah, suprise (motherfucker)! libcurl adds needless "REST 0" command! for (const std::string_view& line : splitFtpResponse(sizeBuf)) if (startsWith(line, "213 ")) // 213<space>[rubbish]<file size> according to libcurl @@ -1154,7 +1177,7 @@ FtpItem getFtpSymlinkInfo(const FtpLogin& login, const AfsPath& linkPath) //thro output.fileSize = stringTo<uint64_t>(makeStringView(it.base(), line.end())); mdtmBuf = session.runSingleFtpCommand("MDTM " + session.getServerPathInternal(linkPath), - true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol return; } break; @@ -1245,7 +1268,7 @@ public: //else: use "LIST" + CURLFTPMETHOD_SINGLECWD //caveat: let's better not use LIST parameters: https://cr.yp.to/ftp/list.html - session.perform(dirPath, true /*isDir*/, pathMethod, options, true /*requiresUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + session.perform(dirPath, true /*isDir*/, pathMethod, options, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol if (session.supportsMlsd()) //throw SysError output = parseMlsd(rawListing, session); //throw SysError @@ -1837,7 +1860,7 @@ void ftpFileDownload(const FtpLogin& login, const AfsPath& afsFilePath, //throw //{CURLOPT_BUFFERSIZE, 256 * 1024} -> defaults is 16 kB which seems to correspond to SSL packet size //=> setting larget buffers size does nothing (recv still returns only 16 kB) - }, true /*requiresUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -1908,7 +1931,7 @@ void ftpFileUpload(const FtpLogin& login, const AfsPath& afsFilePath, //{CURLOPT_PREQUOTE, quote}, //{CURLOPT_POSTQUOTE, quote}, - }, true /*requiresUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -2090,7 +2113,7 @@ private: throw SysError(L"Server does not support the MFMT command."); session.runSingleFtpCommand("MFMT " + isoTime + ' ' + session.getServerPathInternal(filePath_), - true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol //not relevant for OutputStreamFtp, but: does MFMT follow symlinks? for Linux FTP server (using utime) it does }); } @@ -2159,7 +2182,7 @@ private: return FtpDirectoryReader::execute(login_, *parentPath); //throw SysError, SysErrorFtpProtocol } catch (const SysError& e) //add context: error might be folder-specific - { throw SysError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getItemName(*parentPath))) + L'\n' + e.toString()); } + { throw SysError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(parentPath->value.empty() ? Zstr("/") : getItemName(*parentPath))) + L'\n' + e.toString()); } }(); const Zstring itemName = getItemName(itemPath); @@ -2178,7 +2201,7 @@ private: } } - std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExistsImpl(const AfsPath& itemPath) const //throw SysError + std::optional<ItemType> getItemTypeIfExistsImpl(const AfsPath& itemPath) const //throw SysError { const std::optional<AfsPath> parentPath = getParentPath(itemPath); if (!parentPath) //device root => quick access test @@ -2201,7 +2224,7 @@ private: if (item.itemName == itemName) //case-sensitive comparison! itemPath must be normalized! return item.type; - return *parentPath; + return std::nullopt; } catch (const SysErrorFtpProtocol& e) { @@ -2216,12 +2239,10 @@ private: } } catch (const SysError& e) //add context: error might be folder-specific - { throw SysError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getItemName(*parentPath))) + L'\n' + e.toString()); } + { throw SysError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(parentPath->value.empty() ? Zstr("/") : getItemName(*parentPath))) + L'\n' + e.toString()); } //---------------------------------------------------------------- - const std::variant<ItemType, AfsPath /*last existing parent path*/> parentTypeOrPath = getItemTypeIfExistsImpl(*parentPath); //throw SysError - - if (const ItemType* parentType = std::get_if<ItemType>(&parentTypeOrPath)) + if (const std::optional<ItemType> parentType = getItemTypeIfExistsImpl(*parentPath)) //throw SysError { if (*parentType == ItemType::file /*obscure, but possible*/) throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); @@ -2229,10 +2250,10 @@ private: throw* lastFtpError; //throw SysError; parent path existing, so traversal should not have failed! } else - return parentTypeOrPath; + return std::nullopt; } - std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError { try { @@ -2256,7 +2277,7 @@ private: { accessFtpSession(login_, [&](FtpSession& session) //throw SysError { - session.runSingleFtpCommand("MKD " + session.getServerPathInternal(folderPath), true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + session.runSingleFtpCommand("MKD " + session.getServerPathInternal(folderPath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -2271,7 +2292,7 @@ private: { accessFtpSession(login_, [&](FtpSession& session) //throw SysError { - session.runSingleFtpCommand("DELE " + session.getServerPathInternal(filePath), true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + session.runSingleFtpCommand("DELE " + session.getServerPathInternal(filePath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -2288,7 +2309,7 @@ private: { //works fine for Linux hosts, but what about Windows-hosted FTP??? Distinguish DELE/RMD? //Windows test, FileZilla Server and Windows IIS FTP: all symlinks are reported as regular folders - session.runSingleFtpCommand("DELE " + session.getServerPathInternal(linkPath), true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + session.runSingleFtpCommand("DELE " + session.getServerPathInternal(linkPath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -2305,7 +2326,7 @@ private: { //Windows server: FileZilla Server and Windows IIS FTP: all symlinks are reported as regular folders //Linux server (freefilesync.org): RMD will fail for symlinks! - session.runSingleFtpCommand("RMD " + session.getServerPathInternal(folderPath), true /*requiresUtf8*/); //throw SysError, SysErrorFtpProtocol + session.runSingleFtpCommand("RMD " + session.getServerPathInternal(folderPath), true /*requestUtf8*/); //throw SysError, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -2377,13 +2398,19 @@ private: //symlink handling: follow //already existing: fail - void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + FolderCopyResult copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError { - if (copyFilePermissions) - throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); - //already existing: fail AFS::createFolderPlain(targetPath); //throw FileError + + FolderCopyResult result; + try + { + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + catch (const FileError& e) { result.errorAttribs = e; } + return result; } //already existing: fail @@ -2422,7 +2449,7 @@ private: { {CURLOPT_NOBODY, 1L}, {CURLOPT_QUOTE, quote}, - }, true /*requiresUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol + }, true /*requestUtf8*/); //throw SysError, SysErrorPassword, SysErrorFtpProtocol }); } catch (const SysError& e) @@ -2445,7 +2472,7 @@ private: accessFtpSession(login_, [](FtpSession& session) //connect with FTP server, *unless* already connected (in which case *nothing* is sent) { session.perform(AfsPath(), true /*isDir*/, CURLFTPMETHOD_NOCWD, - {{CURLOPT_NOBODY, 1L}, {CURLOPT_SERVER_RESPONSE_TIMEOUT, 0}}, false /*requiresUtf8*/); + {{CURLOPT_NOBODY, 1L}, {CURLOPT_SERVER_RESPONSE_TIMEOUT, 0}}, false /*requestUtf8*/); //caveat: connection phase only, so disable CURLOPT_SERVER_RESPONSE_TIMEOUT, or next access may fail with CURLE_OPERATION_TIMEDOUT! }); //throw SysError, SysErrorPassword, SysErrorFtpProtocol }; @@ -2501,14 +2528,12 @@ private: std::unique_ptr<RecycleSession> createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, RecycleBinUnavailable { - throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath))), - _("Operation not supported by device.")); + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath)))); } void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable { - throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath))), - _("Operation not supported by device.")); + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath)))); } const FtpLogin login_; @@ -2596,7 +2621,7 @@ AfsDevice fff::condenseToFtpDevice(const FtpLogin& login) //noexcept startsWithAsciiNoCase(loginTmp.server, "ftps:" ) || startsWithAsciiNoCase(loginTmp.server, "sftp:" )) loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IfNotFoundReturn::none); - trim(loginTmp.server, true, true, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(loginTmp.server, TrimSide::both, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); return makeSharedRef<FtpFileSystem>(loginTmp); } @@ -2631,7 +2656,7 @@ AbstractPath fff::createItemPathFtp(const Zstring& itemPathPhrase) //noexcept if (startsWithAsciiNoCase(pathPhrase, ftpPrefix)) pathPhrase = pathPhrase.c_str() + strLength(ftpPrefix); - trim(pathPhrase, true, false, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); const ZstringView credentials = beforeFirst<ZstringView>(pathPhrase, Zstr('@'), IfNotFoundReturn::none); const ZstringView fullPathOpt = afterFirst<ZstringView>(pathPhrase, Zstr('@'), IfNotFoundReturn::all); diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index e82273cc..668a45db 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -475,7 +475,7 @@ GdriveAccessInfo gdriveAuthorizeAccess(const std::string& gdriveLoginHint, const &hints, //_In_opt_ const ADDRINFOA* pHints &servinfo); //_Outptr_ PADDRINFOA* ppResult if (rcGai != 0) - throw SysError(formatSystemError("getaddrinfo", replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(rcGai)), utfTo<std::wstring>(::gai_strerror(rcGai)))); + THROW_LAST_SYS_ERROR_GAI(rcGai); if (!servinfo) throw SysError(L"getaddrinfo: empty server info"); @@ -3332,11 +3332,10 @@ public: if (ps.existingType != GdriveItemType::folder) throw SysError(replaceCpy<std::wstring>(L"%x is not a folder.", L"%x", fmtPath(getItemName(folderPath)))); - warn_static("weird in combination with 'file attributes'") return Zstr("https://drive.google.com/drive/folders/") + utfTo<Zstring>(ps.existingItemId); } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString()); } } private: @@ -3401,7 +3400,7 @@ private: catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); } } - std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError { try { @@ -3419,7 +3418,7 @@ private: case GdriveItemType::shortcut: return ItemType::symlink; //*INDENT-ON* } - return ps.existingPath; + return std::nullopt; } catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString()); } } @@ -3707,13 +3706,19 @@ private: //symlink handling: follow //already existing: fail - void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + FolderCopyResult copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError { - if (copyFilePermissions) - throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); - //already existing: 1. fails or 2. creates duplicate (unlikely) AFS::createFolderPlain(targetPath); //throw FileError + + FolderCopyResult result; + try + { + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + catch (const FileError& e) { result.errorAttribs = e; } + return result; } //already existing: fail @@ -4066,7 +4071,7 @@ AbstractPath fff::createItemPathGdrive(const Zstring& itemPathPhrase) //noexcept if (startsWithAsciiNoCase(pathPhrase, gdrivePrefix)) pathPhrase = pathPhrase.c_str() + strLength(gdrivePrefix); - trim(pathPhrase, true, false, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); const ZstringView fullPath = beforeFirst<ZstringView>(pathPhrase, Zstr('|'), IfNotFoundReturn::all); const ZstringView options = afterFirst<ZstringView>(pathPhrase, Zstr('|'), IfNotFoundReturn::none); diff --git a/FreeFileSync/Source/afs/native.cpp b/FreeFileSync/Source/afs/native.cpp index 48fdcac2..1c23d688 100644 --- a/FreeFileSync/Source/afs/native.cpp +++ b/FreeFileSync/Source/afs/native.cpp @@ -448,18 +448,12 @@ private: return zenToAfsItemType(zen::getItemType(getNativePath(itemPath))); //throw FileError } - std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError { initComForThread(); //throw FileError - std::variant<zen::ItemType, Zstring /*last existing parent path*/> typeOrPath = zen::getItemTypeIfExists(getNativePath(itemPath)); //throw FileError - - if (const zen::ItemType* type = std::get_if<zen::ItemType>(&typeOrPath)) + if (const std::optional<zen::ItemType> type = zen::getItemTypeIfExists(getNativePath(itemPath))) //throw FileError return zenToAfsItemType(*type); - - const Zstring existingFolderPath = std::get<Zstring>(typeOrPath); - - assert(startsWith(existingFolderPath, rootPath_)); //"reverse" getNativePath() => safer than parsePathComponents()->relPath!? - return sanitizeDeviceRelativePath({existingFolderPath.begin() + rootPath_.size(), existingFolderPath.end()}); + return std::nullopt; } //---------------------------------------------------------------------------------------------------------------- @@ -590,7 +584,7 @@ private: //symlink handling: follow //already existing: fail - void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + FolderCopyResult copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError { initComForThread(); //throw FileError @@ -599,15 +593,16 @@ private: zen::createDirectory(targetPathNative); //throw FileError, ErrorTargetExisting - ZEN_ON_SCOPE_FAIL(try { removeDirectoryPlain(targetPathNative); } - catch (FileError&) {}); - warn_static("log it!") - - warn_static("implement properly + FileError should lead to a warning only:") - tryCopyDirectoryAttributes(sourcePathNative, targetPathNative); //throw FileError + FolderCopyResult result; + try + { + copyDirectoryAttributes(sourcePathNative, targetPathNative); //throw FileError - if (copyFilePermissions) - copyItemPermissions(sourcePathNative, targetPathNative, ProcSymlink::follow); //throw FileError + if (copyFilePermissions) + copyItemPermissions(sourcePathNative, targetPathNative, ProcSymlink::follow); //throw FileError + } + catch (const FileError& e) { result.errorAttribs = e; } + return result; } //already existing: fail diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index b284345d..1221db24 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -1602,7 +1602,7 @@ private: } } - std::variant<ItemType, AfsPath /*last existing parent path*/> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError + std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError { try { @@ -1621,9 +1621,7 @@ private: if (e.sftpErrorCode == LIBSSH2_FX_NO_SUCH_FILE || e.sftpErrorCode == LIBSSH2_FX_NO_SUCH_PATH) //-> not seen yet, but sounds reasonable { - const std::variant<ItemType, AfsPath /*last existing parent path*/> parentTypeOrPath = getItemTypeIfExists(*parentPath); //throw FileError - - if (const ItemType* parentType = std::get_if<ItemType>(&parentTypeOrPath)) + if (const std::optional<ItemType> parentType = getItemTypeIfExists(*parentPath)) //throw FileError { if (*parentType == ItemType::file /*obscure, but possible*/) throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); @@ -1637,11 +1635,8 @@ private: [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }); //- case-sensitive comparison! itemPath must be normalized! //- finding the item after getItemType() previously failed is exceptional - - return *parentPath; } - else - return parentTypeOrPath; + return std::nullopt; } else throw; @@ -1810,13 +1805,19 @@ private: //symlink handling: follow //already existing: fail - void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError + FolderCopyResult copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError { - if (copyFilePermissions) - throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); - //already existing: fail AFS::createFolderPlain(targetPath); //throw FileError + + FolderCopyResult result; + try + { + if (copyFilePermissions) + throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device.")); + } + catch (const FileError& e) { result.errorAttribs = e; } + return result; } //already existing: fail @@ -1950,14 +1951,12 @@ private: std::unique_ptr<RecycleSession> createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, RecycleBinUnavailable { - throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath))), - _("Operation not supported by device.")); + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath)))); } void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable { - throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath))), - _("Operation not supported by device.")); + throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath)))); } const SftpLogin login_; @@ -2059,7 +2058,7 @@ AfsDevice fff::condenseToSftpDevice(const SftpLogin& login) //noexcept startsWithAsciiNoCase(loginTmp.server, "ftps:" ) || startsWithAsciiNoCase(loginTmp.server, "sftp:" )) loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IfNotFoundReturn::none); - trim(loginTmp.server, true, true, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(loginTmp.server, TrimSide::both, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); return makeSharedRef<SftpFileSystem>(loginTmp); } @@ -2125,7 +2124,7 @@ AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept if (startsWithAsciiNoCase(pathPhrase, sftpPrefix)) pathPhrase = pathPhrase.c_str() + strLength(sftpPrefix); - trim(pathPhrase, true, false, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); + trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); }); const ZstringView credentials = beforeFirst<ZstringView>(pathPhrase, Zstr('@'), IfNotFoundReturn::none); const ZstringView fullPathOpt = afterFirst<ZstringView>(pathPhrase, Zstr('@'), IfNotFoundReturn::all); diff --git a/FreeFileSync/Source/application.cpp b/FreeFileSync/Source/application.cpp index 7a2d23fb..cb4c556b 100644 --- a/FreeFileSync/Source/application.cpp +++ b/FreeFileSync/Source/application.cpp @@ -41,6 +41,8 @@ using namespace fff; "I'd really like to know if there is some deep technical reason for it or if this is really as bloody stupid as it seems?" - vadz https://github.com/wxWidgets/wxWidgets/issues/18733#issuecomment-1011235902 + Show all available GTK backends: run FreeFileSync with env variable: GDK_BACKEND=help + => workaround: https://docs.gtk.org/gdk3/func.set_allowed_backends.html */ GLOBAL_RUN_ONCE(::gdk_set_allowed_backends("x11,*")); //call *before* gtk_init() #endif diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 543313bb..5c6450ef 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -415,15 +415,7 @@ void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifySta } -void releaseLock(const Zstring& lockFilePath) //noexcept -{ - try - { - removeFilePlain(lockFilePath); //throw FileError - } - catch (FileError&) {} - warn_static("log!!! at the very least") //https://freefilesync.org/forum/viewtopic.php?t=7655 -} +void releaseLock(const Zstring& lockFilePath) { removeFilePlain(lockFilePath); } //throw FileError bool tryLock(const Zstring& lockFilePath) //throw FileError @@ -484,7 +476,12 @@ public: lifeSignthread_.requestStop(); //thread lifetime is subset of this instances's life lifeSignthread_.join(); - ::releaseLock(lockFilePath_); //noexcept + try + { + ::releaseLock(lockFilePath_); //throw FileError + } + catch (FileError&) {} + warn_static("log!!! at the very least") //https://freefilesync.org/forum/viewtopic.php?t=7655 } private: diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index 105ac8bc..f87903ca 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -745,8 +745,8 @@ void copySymlink(const AbstractPath& sourcePath, const AbstractPath& targetPath, { parallelScope([sourcePath, targetPath, copyFilePermissions] { AFS::copySymlink(sourcePath, targetPath, copyFilePermissions); /*throw FileError*/ }, singleThread); } inline -void copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions, std::mutex& singleThread) //throw FileError -{ parallelScope([sourcePath, targetPath, copyFilePermissions] { AFS::copyNewFolder(sourcePath, targetPath, copyFilePermissions); /*throw FileError*/ }, singleThread); } +AFS::FolderCopyResult copyNewFolder(const AbstractPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions, std::mutex& singleThread) //throw FileError +{ return parallelScope([sourcePath, targetPath, copyFilePermissions] { return AFS::copyNewFolder(sourcePath, targetPath, copyFilePermissions); /*throw FileError*/ }, singleThread); } inline void removeFilePlain(const AbstractPath& filePath, std::mutex& singleThread) //throw FileError @@ -834,11 +834,11 @@ private: DeletionHandler (const DeletionHandler&) = delete; DeletionHandler& operator=(const DeletionHandler&) = delete; - //might not be needed => create lazily: - AFS::RecycleSession& getOrCreateRecyclerSession() //throw FileError, RecycleBinUnavailable + void moveToRecycleBinIfExists(const AbstractPath& itemPath, const Zstring& relPath, std::mutex& singleThread) //throw FileError, RecycleBinUnavailable { assert(deletionVariant_ == DeletionVariant::recycler); + //might not be needed => create lazily: if (!recyclerSession_ && !recyclerUnavailableExcept_) try { @@ -848,10 +848,16 @@ private: } catch (const RecycleBinUnavailable& e) { recyclerUnavailableExcept_ = e; } - if (recyclerUnavailableExcept_) - throw* recyclerUnavailableExcept_; //throw RecycleBinUnavailable - else - return *recyclerSession_; + if (recyclerUnavailableExcept_) //add context, or user might think we're removing baseFolderPath_! + throw RecycleBinUnavailable(replaceCpy(_("Unable to move %x to the recycle bin."), L"%x", fmtPath(AFS::getDisplayPath(itemPath))), + replaceCpy(recyclerUnavailableExcept_->toString(), L"\n\n", L'\n')); + /* "Unable to move "Z:\folder\file.txt" to the recycle bin. + + The recycle bin is not available for "Z:\". + + Ignore and delete permanently each time recycle bin is unavailable?" */ + + parallel::moveToRecycleBinIfExists(*recyclerSession_, itemPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable } //might not be needed => create lazily: @@ -967,7 +973,7 @@ void DeletionHandler::removeFileWithCallback(const FileDescriptor& fileDescr, co if (!beforeOverwrite) reportInfo(replaceCpy(txtDelFileRecycler_, L"%x", fmtPath(AFS::getDisplayPath(fileDescr.path))), statReporter); //throw ThreadStopRequest try { - parallel::moveToRecycleBinIfExists(getOrCreateRecyclerSession(), fileDescr.path, relPath, singleThread); //throw FileError, RecycleBinUnavailable + moveToRecycleBinIfExists(fileDescr.path, relPath, singleThread); //throw FileError, RecycleBinUnavailable } catch (const RecycleBinUnavailable& e) { @@ -1025,7 +1031,7 @@ void DeletionHandler::removeLinkWithCallback(const AbstractPath& linkPath, const if (!beforeOverwrite) reportInfo(replaceCpy(txtDelSymlinkRecycler_, L"%x", fmtPath(AFS::getDisplayPath(linkPath))), statReporter); //throw ThreadStopRequest try { - parallel::moveToRecycleBinIfExists(getOrCreateRecyclerSession(), linkPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable + moveToRecycleBinIfExists(linkPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable } catch (const RecycleBinUnavailable& e) { @@ -1084,7 +1090,7 @@ void DeletionHandler::removeDirWithCallback(const AbstractPath& folderPath, cons reportInfo(replaceCpy(txtDelFolderRecycler_, L"%x", fmtPath(AFS::getDisplayPath(folderPath))), statReporter); //throw ThreadStopRequest try { - parallel::moveToRecycleBinIfExists(getOrCreateRecyclerSession(), folderPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable + moveToRecycleBinIfExists(folderPath, relPath, singleThread); //throw FileError, RecycleBinUnavailable statReporter.reportDelta(1, 0); //moving to recycler is ONE logical operation, irrespective of the number of child elements! } catch (const RecycleBinUnavailable& e) @@ -2196,7 +2202,10 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy try { //already existing: fail - parallel::copyNewFolder(folder.getAbstractPath<sideSrc>(), targetPath, copyFilePermissions_, singleThread_); //throw FileError + AFS::FolderCopyResult result = parallel::copyNewFolder(folder.getAbstractPath<sideSrc>(), targetPath, copyFilePermissions_, singleThread_); //throw FileError + + if (result.errorAttribs) //log only; no popup + acb_.logMessage(result.errorAttribs->toString(), PhaseCallback::MsgType::warning); //throw ThreadStopRequest } catch (FileError&) { @@ -2426,7 +2435,10 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, Phas if (const std::optional<AbstractPath> parentPath = AFS::getParentPath(folderPath)) AFS::createFolderIfMissingRecursion(*parentPath); //throw FileError - AFS::copyNewFolder(baseFolder.getAbstractPath<sideSrc>(), folderPath, copyFilePermissions); //throw FileError + AFS::FolderCopyResult result = AFS::copyNewFolder(baseFolder.getAbstractPath<sideSrc>(), folderPath, copyFilePermissions); //throw FileError + + if (result.errorAttribs) //log only; no popup + callback.logMessage(result.errorAttribs->toString(), PhaseCallback::MsgType::warning); //throw X } else AFS::createFolderIfMissingRecursion(folderPath); //throw FileError diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp index 3efe0788..c3822d65 100644 --- a/FreeFileSync/Source/base/versioning.cpp +++ b/FreeFileSync/Source/base/versioning.cpp @@ -183,8 +183,7 @@ void moveExistingItemToVersioning(const AbstractPath& sourcePath, const Abstract void FileVersioner::revisionFile(const FileDescriptor& fileDescr, const Zstring& relativePath, const IoCallback& notifyUnbufferedIO /*throw X*/) const //throw FileError, X { - const std::variant<AFS::ItemType, AfsPath /*last existing parent path*/> typeOrPath = AFS::getItemTypeIfExists(fileDescr.path); //throw FileError - if (const AFS::ItemType* type = std::get_if<AFS::ItemType>(&typeOrPath)) + if (const std::optional<AFS::ItemType> type = AFS::getItemTypeIfExists(fileDescr.path)) //throw FileError { assert(*type != AFS::ItemType::symlink); @@ -250,8 +249,7 @@ void FileVersioner::revisionFolder(const AbstractPath& folderPath, const Zstring const IoCallback& notifyUnbufferedIO /*throw X*/) const { //no error situation if directory is not existing! manual deletion relies on it! - const std::variant<AFS::ItemType, AfsPath /*last existing parent path*/> typeOrPath = AFS::getItemTypeIfExists(folderPath); //throw FileError - if (const AFS::ItemType* type = std::get_if<AFS::ItemType>(&typeOrPath)) + if (const std::optional<AFS::ItemType> type = AFS::getItemTypeIfExists(folderPath)) //throw FileError { assert(*type != AFS::ItemType::symlink); diff --git a/FreeFileSync/Source/config.cpp b/FreeFileSync/Source/config.cpp index 98ebfe4a..49f988e9 100644 --- a/FreeFileSync/Source/config.cpp +++ b/FreeFileSync/Source/config.cpp @@ -10,7 +10,8 @@ #include <zen/file_io.h> #include <zen/time.h> #include <zen/process_exec.h> -#include <wx/intl.h> +//#include <wx/intl.h> +#include <wx/uilocale.h> #include "ffs_paths.h" #include "base_tools.h" #include "afs/native.h" @@ -101,18 +102,18 @@ void writeText(const wxLanguage& value, std::string& output) //use description as unique wxLanguage identifier, see localization.cpp //=> handle changes to wxLanguage enum between wxWidgets versions - const wxString& canonicalName = wxLocale::GetLanguageCanonicalName(value); + const wxString& canonicalName = wxUILocale::GetLanguageCanonicalName(value); assert(!canonicalName.empty()); if (!canonicalName.empty()) output = utfTo<std::string>(canonicalName); else - output = utfTo<std::string>(wxLocale::GetLanguageCanonicalName(wxLANGUAGE_ENGLISH_US)); + output = utfTo<std::string>(wxUILocale::GetLanguageCanonicalName(wxLANGUAGE_ENGLISH_US)); } template <> inline bool readText(const std::string& input, wxLanguage& value) { - if (const wxLanguageInfo* lngInfo = wxLocale::FindLanguageInfo(utfTo<wxString>(input))) + if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo<wxString>(input))) { value = static_cast<wxLanguage>(lngInfo->Language); return true; @@ -1455,7 +1456,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) cfg.programLanguage = wxLANGUAGE_NORWEGIAN; else if (lngName == "Portuguese (Brazilian)") cfg.programLanguage = wxLANGUAGE_PORTUGUESE_BRAZILIAN; - else if (const wxLanguageInfo* lngInfo = wxLocale::FindLanguageInfo(utfTo<wxString>(lngName))) + else if (const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo<wxString>(lngName))) cfg.programLanguage = static_cast<wxLanguage>(lngInfo->Language); } else diff --git a/FreeFileSync/Source/localization.cpp b/FreeFileSync/Source/localization.cpp index d1de6201..6bb2f376 100644 --- a/FreeFileSync/Source/localization.cpp +++ b/FreeFileSync/Source/localization.cpp @@ -5,27 +5,15 @@ // ***************************************************************************** #include "localization.h" -#include <unordered_map> #include <clocale> //setlocale -#include <map> -#include <list> -#include <iterator> -#include <zen/string_tools.h> #include <zen/file_traverser.h> #include <zen/file_io.h> -#include <zen/i18n.h> -#include <zen/format_unit.h> -#include <zen/perf.h> #include <wx/zipstrm.h> #include <wx/mstream.h> -#include <wx/intl.h> -#include <wx/log.h> +#include <wx/uilocale.h> #include "parse_plural.h" #include "parse_lng.h" - #include <wchar.h> //wcscasecmp - - using namespace zen; using namespace fff; @@ -154,7 +142,7 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi assert(!lngHeader.locale .empty()); assert(!lngHeader.flagFile .empty()); - const wxLanguageInfo* lngInfo = wxLocale::FindLanguageInfo(utfTo<wxString>(lngHeader.locale)); + const wxLanguageInfo* lngInfo = wxUILocale::FindLanguageInfo(utfTo<wxString>(lngHeader.locale)); assert(lngInfo && lngInfo->CanonicalName == utfTo<wxString>(lngHeader.locale)); if (lngInfo) translations.push_back( @@ -180,23 +168,23 @@ std::vector<TranslationInfo> loadTranslations(const Zstring& zipPath) //throw Fi } -/* Some ISO codes are used by multiple wxLanguage IDs which can lead to incorrect mapping by wxLocale::FindLanguageInfo()!!! +/* Some ISO codes are used by multiple wxLanguage IDs which can lead to incorrect mapping by wxUILocale::FindLanguageInfo()!!! => Identify by description, e.g. "Chinese (Traditional)". The following IDs are affected: - zh_TW: wxLANGUAGE_CHINESE_TAIWAN, wxLANGUAGE_CHINESE, wxLANGUAGE_CHINESE_TRADITIONAL_EXPLICIT - en_GB: wxLANGUAGE_ENGLISH_UK, wxLANGUAGE_ENGLISH - es_ES: wxLANGUAGE_SPANISH, wxLANGUAGE_SPANISH_SPAIN */ wxLanguage mapLanguageDialect(wxLanguage lng) { - if (const wxString& canonicalName = wxLocale::GetLanguageCanonicalName(lng); + if (const wxString& canonicalName = wxUILocale::GetLanguageCanonicalName(lng); !canonicalName.empty()) { assert(!contains(canonicalName, L'-')); - const std::string locale = beforeFirst(utfTo<std::string>(canonicalName), '@', IfNotFoundReturn::all); //e.g. "sr_RS@latin"; see wxLocale::InitLanguagesDB() + const std::string locale = beforeFirst(utfTo<std::string>(canonicalName), '@', IfNotFoundReturn::all); //e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB() const std::string lngCode = beforeFirst(locale, '_', IfNotFoundReturn::all); if (lngCode == "zh") { - if (lng == wxLANGUAGE_CHINESE) //wxWidgets assigns this to "zh" or "zh_TW" for some reason + if (lng == wxLANGUAGE_CHINESE) //wxWidgets assigns this to "zh_TW" for some reason return wxLANGUAGE_CHINESE_CHINA; for (const char* l : {"zh_HK", "zh_MO", "zh_TW"}) @@ -208,7 +196,7 @@ wxLanguage mapLanguageDialect(wxLanguage lng) if (lngCode == "en") { - if (lng == wxLANGUAGE_ENGLISH || //wxWidgets assigns this to "en" or "en_GB" for some reason + if (lng == wxLANGUAGE_ENGLISH || //wxWidgets assigns this to "en_GB" for some reason lng == wxLANGUAGE_ENGLISH_WORLD) return wxLANGUAGE_ENGLISH_US; @@ -227,7 +215,7 @@ wxLanguage mapLanguageDialect(wxLanguage lng) //all other cases: map to primary language code if (contains(locale, '_')) - if (const wxLanguageInfo* lngInfo2 = wxLocale::FindLanguageInfo(utfTo<wxString>(lngCode))) + if (const wxLanguageInfo* lngInfo2 = wxUILocale::FindLanguageInfo(utfTo<wxString>(lngCode))) return static_cast<wxLanguage>(lngInfo2->Language); } return lng; //including wxLANGUAGE_DEFAULT, wxLANGUAGE_UNKNOWN @@ -240,7 +228,7 @@ class MemoryTranslationLoader : public wxTranslationsLoader { public: MemoryTranslationLoader(wxLanguage langId, std::map<std::string, std::wstring>&& transMapping) : - canonicalName_(wxLocale::GetLanguageCanonicalName(langId)) + canonicalName_(wxUILocale::GetLanguageCanonicalName(langId)) { assert(!canonicalName_.empty()); static_assert(std::is_same_v<std::remove_cvref_t<decltype(transMapping)>, std::map<std::string, std::wstring>>); //translations *must* be sorted in MO file! @@ -280,15 +268,13 @@ public: wxMsgCatalog* LoadCatalog(const wxString& domain, const wxString& lang) override { - //"lang" is NOT (exactly) what we return from GetAvailableTranslations(), but has a little "extra", e.g.: de_DE.WINDOWS-1252 or ar.WINDOWS-1252 - auto extractIsoLangCode = [](wxString langCode) - { - langCode = beforeLast(langCode, L".", IfNotFoundReturn::all); - return beforeLast(langCode, L"_", IfNotFoundReturn::all); - }; + //"lang" is NOT (exactly) what we return from GetAvailableTranslations(), but has a little "extra" + //e.g.: de_DE.WINDOWS-1252 ar.WINDOWS-1252 zh_TW.MacRoman + auto extractIsoLangCode = [](wxString langCode) { return beforeLast(langCode, L".", IfNotFoundReturn::all); }; if (equalAsciiNoCase(extractIsoLangCode(lang), extractIsoLangCode(canonicalName_))) return wxMsgCatalog::CreateFromData(wxScopedCharBuffer::CreateNonOwned(moBuf_.ref().c_str(), moBuf_.ref().size()), domain); + assert(false); return nullptr; } @@ -306,108 +292,65 @@ private: }; -//global wxWidgets localization: sets up C locale as well! -class ZenLocale -{ -public: - static ZenLocale& getInstance() - { - static ZenLocale inst; - return inst; - } - - void init(wxLanguage lng) - { - lng_ = lng; - - if (const wxLanguageInfo* selLngInfo = wxLocale::GetLanguageInfo(lng)) - layoutDir_ = selLngInfo->LayoutDirection; - else - layoutDir_ = wxLayout_LeftToRight; - - /* use wxLANGUAGE_DEFAULT to preserve sub-language-specific rules (e.g. number and date format) - - beneficial even for Arabic locale: use user-specific date settings instead of Hijri calendar year 1441 (= Gregorian 2019) - - note when testing: format_unit.cpp::formatNumber() always uses LOCALE_NAME_USER_DEFAULT => test date format instead */ - if (!locale_) - { - //wxWidgets shows a modal dialog on error during wxLocale::Init() -> at least we can shut it up! - wxLog* oldLogTarget = wxLog::SetActiveTarget(new wxLogStderr); //transfer and receive ownership! - ZEN_ON_SCOPE_EXIT(delete wxLog::SetActiveTarget(oldLogTarget)); - - //locale_.reset(); //avoid global locale lifetime overlap! wxWidgets cannot handle this and will crash! - locale_ = std::make_unique<wxLocale>(wxLANGUAGE_DEFAULT, wxLOCALE_DONT_LOAD_DEFAULT /*we're not using wxwin.mo*/); - //wxLANGUAGE_DEFAULT => internally calls std::setlocale(LC_ALL, "" /*== user-preferred locale*/) on Windows/Linux (but not macOS) - /* => exactly what's needed on Windows/Linux - - but not needed on macOS; even detrimental: - - breaks wxWidgets file drag and drop! https://freefilesync.org/forum/viewtopic.php?t=8215 - - even wxWidgets knows: "under macOS C locale must not be changed, as doing this exposes bugs in the system" - https://docs.wxwidgets.org/trunk/classwx_u_i_locale.html - - reproduce: - std::setlocale(LC_ALL, ""); - - double-click the app (*) - - drag and drop folder named "アアアア" - - wxFileDropTarget::OnDropFiles() called with empty file array! - - *) CAVEAT: context matters! this yields a different user-preferred locale than running Contents/MacOS/FreeFileSync_main!!! - e.g. 1. locale after wxLocale creation is "en_US" - 2. call std::setlocale(LC_ALL, ""): - a) app was double-clicked: locale is "C" => drag/drop FAILS! - b) run Contents/MacOS/FreeFileSync_main: locale is "en_US.UTF-8" => drag/drop works! */ - assert(locale_->IsOk()); - - //const char* currentLocale = std::setlocale(LC_ALL, nullptr); - } - } +std::vector<TranslationInfo> globalTranslations; +wxLanguage globalLang = wxLANGUAGE_UNKNOWN; +wxLayoutDirection globalLayoutDir = wxLayout_Default; +} - void tearDown() { locale_.reset(); lng_ = wxLANGUAGE_UNKNOWN; layoutDir_ = wxLayout_Default; } - wxLanguage getLanguage() const { return lng_; } - wxLayoutDirection getLayoutDirection() const { return layoutDir_; } +void fff::localizationInit(const Zstring& zipPath) //throw FileError +{ + assert(globalTranslations.empty()); + globalTranslations = loadTranslations(zipPath); //throw FileError -private: - ZenLocale() {} - ~ZenLocale() { assert(!locale_); } - - wxLanguage lng_ = wxLANGUAGE_UNKNOWN; - wxLayoutDirection layoutDir_ = wxLayout_Default; - std::unique_ptr<wxLocale> locale_; - //use wxWidgets 3.1.6 wxUILocale? wxLocale already does *exactly* what we need, while wxLocale does a half-arsed job (as expected): - //setlocale() is only called on wxGTK, but not on Windows + wxTranslations is not initialized. -}; + /* wxLocale vs wxUILocale (since wxWidgets 3.1.6) + ------------------------------------------|-------------------- + calls setlocale() Windows, Linux, maCOS | Linux only + wxTranslations initialized | not initialized + caveat: setlocale() calls on macOS lead to bugs: + - breaks wxWidgets file drag and drop! https://freefilesync.org/forum/viewtopic.php?t=8215 + - "under macOS C locale must not be changed, as doing this exposes bugs in the system": https://docs.wxwidgets.org/trunk/classwx_u_i_locale.html -std::vector<TranslationInfo> globalTranslations; -} + reproduce: - std::setlocale(LC_ALL, ""); + - double-click the app (*) + - drag and drop folder named "アアアア" + - wxFileDropTarget::OnDropFiles() called with empty file array! + *) CAVEAT: context matters! this yields a different user-preferred locale than running Contents/MacOS/FreeFileSync_main!!! + e.g. 1. locale after wxLocale creation is "en_US" + 2. call std::setlocale(LC_ALL, ""): + a) app was double-clicked: locale is "C" => drag/drop FAILS! + b) run Contents/MacOS/FreeFileSync_main: locale is "en_US.UTF-8" => drag/drop works! */ + [[maybe_unused]] const bool rv = wxUILocale::UseDefault(); + assert(rv); -const std::vector<TranslationInfo>& fff::getAvailableTranslations() -{ - assert(!globalTranslations.empty()); //localizationInit() not called, or failed!? - return globalTranslations; -} + //const char* currentLocale = std::setlocale(LC_ALL, nullptr); + assert(!wxTranslations::Get()); + wxTranslations::Set(new wxTranslations() /*pass ownership*/); //implicitly done by wxLocale, but *not* wxUILocale -void fff::localizationInit(const Zstring& zipPath) //throw FileError -{ - assert(globalTranslations.empty()); - globalTranslations = loadTranslations(zipPath); //throw FileError setLanguage(getDefaultLanguage()); //throw FileError } void fff::localizationCleanup() { +#if 0 //good place for clean up rather than some time during static destruction: is this an actual benefit??? + globalLang = wxLANGUAGE_UNKNOWN; + globalLayoutDir = wxLayout_Default; + + setTranslator(nullptr); + assert(!globalTranslations.empty()); - ZenLocale::getInstance().tearDown(); - setTranslator(nullptr); //good place for clean up rather than some time during static destruction: is this an actual benefit??? globalTranslations.clear(); +#endif } void fff::setLanguage(wxLanguage lng) //throw FileError { - if (getLanguage() == lng) + if (globalLang == lng) return; //support polling //(try to) retrieve language file @@ -445,41 +388,44 @@ void fff::setLanguage(wxLanguage lng) //throw FileError { throw FileError(L"Invalid plural form definition: " + fmtPath(lngFileName)); //user should never see this! } + //------------------------------------------------------------ + + globalLang = lng; + + if (const wxLanguageInfo* selLngInfo = wxUILocale::GetLanguageInfo(lng)) + globalLayoutDir = selLngInfo->LayoutDirection; + else + globalLayoutDir = wxLayout_LeftToRight; - //handle RTL swapping: we need wxWidgets to do this - ZenLocale::getInstance().init(lng); //add translation for wxWidgets-internal strings: - assert(wxTranslations::Get()); //already initialized by wxLocale - if (wxTranslations* wxtrans = wxTranslations::Get()) + std::map<std::string, std::wstring> transMapping = { - std::map<std::string, std::wstring> transMapping = - { - }; - wxtrans->SetLanguage(lng); //!= wxLocale's language, which could be wxLANGUAGE_DEFAULT (see ZenLocale) - wxtrans->SetLoader(new MemoryTranslationLoader(lng, std::move(transMapping))); - [[maybe_unused]] const bool catalogAdded = wxtrans->AddCatalog(wxString()); - assert(catalogAdded || lng == wxLANGUAGE_ENGLISH_US); - } + }; + wxTranslations& wxtrans = *wxTranslations::Get(); //*assert* creation by localizationInit()! + wxtrans.SetLanguage(lng); //!= wxLocale's language, which could be wxLANGUAGE_DEFAULT (see ZenLocale) + wxtrans.SetLoader(new MemoryTranslationLoader(lng, std::move(transMapping))); + [[maybe_unused]] const bool catalogAdded = wxtrans.AddCatalog(wxString()); + assert(catalogAdded || lng == wxLANGUAGE_ENGLISH_US); +} + + +const std::vector<TranslationInfo>& fff::getAvailableTranslations() +{ + assert(!globalTranslations.empty()); //localizationInit() not called, or failed!? + return globalTranslations; } wxLanguage fff::getDefaultLanguage() { - static const wxLanguage defaultLng = mapLanguageDialect(static_cast<wxLanguage>(wxLocale::GetSystemLanguage())); + static const wxLanguage defaultLng = mapLanguageDialect(static_cast<wxLanguage>(wxUILocale::GetSystemLanguage())); //uses GetUserPreferredUILanguages() since wxWidgets 1.3.6, not GetUserDefaultUILanguage() anymore: // https://github.com/wxWidgets/wxWidgets/blob/master/src/common/intl.cpp return defaultLng; } -wxLanguage fff::getLanguage() -{ - return ZenLocale::getInstance().getLanguage(); -} - +wxLanguage fff::getLanguage() { return globalLang; } -wxLayoutDirection fff::getLayoutDirection() -{ - return ZenLocale::getInstance().getLayoutDirection(); -} +wxLayoutDirection fff::getLayoutDirection() { return globalLayoutDir; } diff --git a/FreeFileSync/Source/localization.h b/FreeFileSync/Source/localization.h index 3672e6f5..f52db495 100644 --- a/FreeFileSync/Source/localization.h +++ b/FreeFileSync/Source/localization.h @@ -9,7 +9,7 @@ #include <vector> #include <zen/file_error.h> -#include <wx/intl.h> +#include <wx/intl.h> //wxLayoutDirection #include <wx/language.h> diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index d0fd57ee..68967d1c 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -620,11 +620,11 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_staticTextProcessed = new wxStaticText( m_panelLog, wxID_ANY, _("Processed:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextProcessed->Wrap( -1 ); - ffgSizer11->Add( m_staticTextProcessed, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxRIGHT, 5 ); + ffgSizer11->Add( m_staticTextProcessed, 0, wxALIGN_RIGHT|wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); m_staticTextRemaining = new wxStaticText( m_panelLog, wxID_ANY, _("Remaining:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextRemaining->Wrap( -1 ); - ffgSizer11->Add( m_staticTextRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + ffgSizer11->Add( m_staticTextRemaining, 0, wxALIGN_RIGHT|wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); bSizer42->Add( ffgSizer11, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM, 10 ); @@ -643,7 +643,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer293 = new wxBoxSizer( wxHORIZONTAL ); m_bitmapItemStat = new wxStaticBitmap( m_panelItemStats, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer293->Add( m_bitmapItemStat, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 5 ); + bSizer293->Add( m_bitmapItemStat, 0, wxRIGHT|wxALIGN_CENTER_VERTICAL, 5 ); bSizer293->Add( 0, 0, 1, 0, 5 ); @@ -652,24 +652,24 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_staticTextItemsProcessed->Wrap( -1 ); m_staticTextItemsProcessed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_BOTTOM, 5 ); ffgSizer111->Add( bSizer293, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); m_staticTextBytesProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextBytesProcessed->Wrap( -1 ); - ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); m_staticTextItemsRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); m_staticTextItemsRemaining->Wrap( -1 ); m_staticTextItemsRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); m_staticTextBytesRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextBytesRemaining->Wrap( -1 ); - ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); bSizer291->Add( ffgSizer111, 0, wxALL, 5 ); @@ -703,7 +703,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_staticTextTimeElapsed->Wrap( -1 ); m_staticTextTimeElapsed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_BOTTOM, 5 ); ffgSizer112->Add( bSizer294, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); @@ -3474,24 +3474,24 @@ CompareProgressDlgGenerated::CompareProgressDlgGenerated( wxWindow* parent, wxWi m_staticTextItemsProcessed->Wrap( -1 ); m_staticTextItemsProcessed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer293->Add( m_staticTextItemsProcessed, 0, wxALIGN_BOTTOM, 5 ); ffgSizer111->Add( bSizer293, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); m_staticTextBytesProcessed = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextBytesProcessed->Wrap( -1 ); - ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + ffgSizer111->Add( m_staticTextBytesProcessed, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); m_staticTextItemsRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); m_staticTextItemsRemaining->Wrap( -1 ); m_staticTextItemsRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + ffgSizer111->Add( m_staticTextItemsRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); m_staticTextBytesRemaining = new wxStaticText( m_panelItemStats, wxID_ANY, _("dummy"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextBytesRemaining->Wrap( -1 ); - ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 5 ); + ffgSizer111->Add( m_staticTextBytesRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); bSizer291->Add( ffgSizer111, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); @@ -3525,7 +3525,7 @@ CompareProgressDlgGenerated::CompareProgressDlgGenerated( wxWindow* parent, wxWi m_staticTextTimeElapsed->Wrap( -1 ); m_staticTextTimeElapsed->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_CENTER_VERTICAL, 5 ); + bSizer294->Add( m_staticTextTimeElapsed, 0, wxALIGN_BOTTOM, 5 ); ffgSizer112->Add( bSizer294, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); @@ -3534,7 +3534,7 @@ CompareProgressDlgGenerated::CompareProgressDlgGenerated( wxWindow* parent, wxWi m_staticTextTimeRemaining->Wrap( -1 ); m_staticTextTimeRemaining->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - ffgSizer112->Add( m_staticTextTimeRemaining, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT, 5 ); + ffgSizer112->Add( m_staticTextTimeRemaining, 0, wxALIGN_RIGHT|wxALIGN_BOTTOM, 5 ); bSizer292->Add( ffgSizer112, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 3ebebeba..a5eab887 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -2628,14 +2628,14 @@ void MainDialog::addFilterPhrase(const Zstring& phrase, bool include, bool requi if (requireNewLine) { - trim(filterString, false, true, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n') || c == Zstr(' '); }); + trim(filterString, TrimSide::right, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n') || c == Zstr(' '); }); if (!filterString.empty()) filterString += Zstr('\n'); filterString += phrase; } else { - trim(filterString, false, true, [](Zchar c) { return c == Zstr('\n') || c == Zstr(' '); }); + trim(filterString, TrimSide::right, [](Zchar c) { return c == Zstr('\n') || c == Zstr(' '); }); if (contains(afterLast(filterString, Zstr('\n'), IfNotFoundReturn::all), FILTER_ITEM_SEPARATOR)) { @@ -4295,9 +4295,8 @@ void MainDialog::onCompare(wxCommandEvent& event) //mark selected cfg files as "in sync" when there is nothing to do: https://freefilesync.org/forum/viewtopic.php?t=4991 if (r.summary.syncResult == SyncResult::finishedSuccess) - { - const SyncStatistics st(folderCmp_); - if (st.createCount() + + if (const SyncStatistics st(folderCmp_); + st.createCount() + st.updateCount() + st.deleteCount() == 0) { @@ -4306,7 +4305,6 @@ void MainDialog::onCompare(wxCommandEvent& event) updateConfigLastRunStats(std::chrono::system_clock::to_time_t(r.summary.startTime), r.summary.syncResult, getNullPath() /*logFilePath*/); } - } //reset icon cache (IconBuffer) after *each* comparison! filegrid::setupIcons(*m_gridMainL, *m_gridMainC, *m_gridMainR, globalCfg_.mainDlg.showIcons, convert(globalCfg_.mainDlg.iconSize)); @@ -4780,6 +4778,8 @@ void MainDialog::setLastOperationLog(const ProcessSummary& summary, const std::s return loadImage("msg_error", getDefaultMenuIconSize()); if (logCount.warning > 0) return loadImage("msg_warning", getDefaultMenuIconSize()); + + //return loadImage("msg_success", getDefaultMenuIconSize()); -> too noisy? } return wxNullImage; }(); diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 6be25206..47e3ed99 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -58,7 +58,7 @@ private: void onOkay (wxCommandEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::accept)); } void onClose (wxCloseEvent& event) override { EndModal(static_cast<int>(ConfirmationButton::cancel)); } void onOpenForum(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/forum"); } - void onSendEmail(wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"mailto:zenju@" L"freefilesync.org"); } + void onSendEmail(wxCommandEvent& event) override { wxLaunchDefaultBrowser(wxString() + L"mailto:zenju@" + /*don't leave full email in either source or binary*/ L"freefilesync.org"); } void onDonate (wxCommandEvent& event) override { wxLaunchDefaultBrowser(L"https://freefilesync.org/donate"); } void onLocalKeyEvent(wxKeyEvent& event); @@ -75,8 +75,8 @@ AboutDlg::AboutDlg(wxWindow* parent) : AboutDlgGenerated(parent) setImage(*m_bitmapLogoLeft, loadImage("logo-left")); setBitmapTextLabel(*m_bpButtonForum, loadImage("ffs_forum"), L"FreeFileSync Forum"); - setBitmapTextLabel(*m_bpButtonEmail, loadImage("ffs_email"), L"zenju@" L"freefilesync.org"); - m_bpButtonEmail->SetToolTip(L"mailto:zenju@" L"freefilesync.org"); + setBitmapTextLabel(*m_bpButtonEmail, loadImage("ffs_email"), wxString() + L"zenju@" + /*don't leave full email in either source or binary*/ L"freefilesync.org"); + m_bpButtonEmail->SetToolTip( wxString() + L"mailto:zenju@" + /*don't leave full email in either source or binary*/ L"freefilesync.org"); wxString build = utfTo<wxString>(ffsVersion); diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index 65b1b645..ec79a65d 100644 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -127,73 +127,76 @@ bool sanitizeFilter(FilterConfig& filterCfg, const std::vector<AbstractPath>& ba removeDuplicates(folderPathsPf); } - if (!folderPathsPf.empty()) + + std::vector<std::pair<Zstring /*from*/, Zstring /*to*/>> replacements; + + auto replaceFullPaths = [&](Zstring& filterPhrase) { - std::vector<std::pair<Zstring /*from*/, Zstring /*to*/>> replacements; + Zstring filterPhraseNew; + const Zchar* itFilterOrig = filterPhrase.begin(); - auto replaceFullPaths = [&](Zstring& filterPhrase) + split2(filterPhrase, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n'); }, //delimiters + [&](const ZstringView phrase) { - Zstring filterPhraseNew; - const Zchar* itFilterOrig = filterPhrase.begin(); - - split2(filterPhrase, [](Zchar c) { return c == FILTER_ITEM_SEPARATOR || c == Zstr('\n'); }, //delimiters - [&](ZstringView phrase) + const ZstringView phraseTrm = trimCpy(phrase); + if (!phraseTrm.empty()) { - phrase = trimCpy(phrase); - if (!phrase.empty()) - { - const Zstring phraseNorm = normalizeForSearch(Zstring{phrase}); - - for (const Zstring& pathNormPf : folderPathsPf) - if (startsWith(phraseNorm, pathNormPf)) - { - //emulate a "normalized afterFirst()": - ptrdiff_t sepCount = std::count(pathNormPf.begin(), pathNormPf.end(), FILE_NAME_SEPARATOR); - assert(sepCount > 0); - - for (auto it = phrase.begin(); it != phrase.end(); ++it) - if (*it == Zstr('/') || - *it == Zstr('\\')) - if (--sepCount == 0) - { - const Zstring relPath(it, phrase.end()); //include first path separator - - filterPhraseNew.append(itFilterOrig, phrase.data()); - filterPhraseNew += relPath; - itFilterOrig = phrase.data() + phrase.size(); - - replacements.emplace_back(phrase, relPath); - return; //... to next block - } - throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); - } - } - }); - filterPhraseNew.append(itFilterOrig, filterPhrase.cend()); - - filterPhrase = filterPhraseNew; - }; - replaceFullPaths(filterCfg.includeFilter); - replaceFullPaths(filterCfg.excludeFilter); + const Zstring phraseNorm = normalizeForSearch(Zstring{phraseTrm}); + + for (const Zstring& pathNormPf : folderPathsPf) + if (startsWith(phraseNorm, pathNormPf)) + { + //emulate a "normalized afterFirst()": + ptrdiff_t sepCount = std::count(pathNormPf.begin(), pathNormPf.end(), FILE_NAME_SEPARATOR); + assert(sepCount > 0); + + for (auto it = phraseTrm.begin(); it != phraseTrm.end(); ++it) + if (*it == Zstr('/') || + *it == Zstr('\\')) + if (--sepCount == 0) + { + const Zstring relPath(it, phraseTrm.end()); //include first path separator + + filterPhraseNew.append(itFilterOrig, phraseTrm.data()); + filterPhraseNew += relPath; + itFilterOrig = phraseTrm.data() + phraseTrm.size(); + + replacements.emplace_back(phraseTrm, relPath); + return; //... to next block + } + throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!"); + } + } + }); - assert(!replacements.empty()); - if (!replacements.empty()) + if (itFilterOrig != filterPhrase.begin()) //perf!? { - std::wstring detailsMsg; - for (const auto& [from, to] : replacements) + filterPhraseNew.append(itFilterOrig, filterPhrase.cend()); + filterPhrase = std::move(filterPhraseNew); + } + }; + replaceFullPaths(filterCfg.includeFilter); + replaceFullPaths(filterCfg.excludeFilter); + + if (!replacements.empty()) + { + std::wstring detailsMsg; + for (const auto& [from, to] : replacements) + if (to.empty()) + detailsMsg += _("Remove:") + L' ' + utfTo<std::wstring>(from) + L'\n'; + else detailsMsg += utfTo<std::wstring>(from) + L' ' + arrowRight + L' ' + utfTo<std::wstring>(to) + L'\n'; - detailsMsg.pop_back(); + detailsMsg.pop_back(); - switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg(). - setMainInstructions(_("Each filter item must be a path relative to the selected folder pairs. The following changes are suggested:")). - setDetailInstructions(detailsMsg), _("&Change"))) - { - case ConfirmationButton::accept: //change - break; + switch (showConfirmationDialog(parent, DialogInfoType::info, PopupDialogCfg(). + setMainInstructions(_("Each filter item must be a path relative to the selected folder pairs. The following changes are suggested:")). + setDetailInstructions(detailsMsg), _("&Change"))) + { + case ConfirmationButton::accept: //change + break; - case ConfirmationButton::cancel: - return false; - } + case ConfirmationButton::cancel: + return false; } } return true; diff --git a/FreeFileSync/Source/ui/version_check.cpp b/FreeFileSync/Source/ui/version_check.cpp index 29b5ed2f..9af55432 100644 --- a/FreeFileSync/Source/ui/version_check.cpp +++ b/FreeFileSync/Source/ui/version_check.cpp @@ -26,6 +26,8 @@ #include "../version/version.h" #include "small_dlgs.h" + #include <gtk/gtk.h> + #include <wx/uilocale.h> #include <zen/symlink_target.h> @@ -91,8 +93,8 @@ std::wstring getIso639Language() { assert(runningOnMainThread()); //this function is not thread-safe: consider wxWidgets usage - std::wstring localeName(wxLocale::GetLanguageCanonicalName(wxLocale::GetSystemLanguage())); - localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxLocale::InitLanguagesDB() + std::wstring localeName(wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLanguage())); + localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB() if (!localeName.empty()) { @@ -111,8 +113,8 @@ std::wstring getIso3166Country() { assert(runningOnMainThread()); //this function is not thread-safe, consider wxWidgets usage - std::wstring localeName(wxLocale::GetLanguageCanonicalName(wxLocale::GetSystemLanguage())); - localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxLocale::InitLanguagesDB() + std::wstring localeName(wxUILocale::GetLanguageCanonicalName(wxUILocale::GetSystemLanguage())); + localeName = beforeFirst(localeName, L'@', IfNotFoundReturn::all); //the locale may contain an @, e.g. "sr_RS@latin"; see wxUILocale::InitLanguagesDB() if (contains(localeName, L'_')) { @@ -142,9 +144,9 @@ std::vector<std::pair<std::string, std::string>> geHttpPostParameters(wxWindow& const char* osArch = cpuArchName; params.emplace_back("os_arch", osArch); -#ifdef __WXGTK2__ +#if GTK_MAJOR_VERSION == 2 //wxWindow::GetContentScaleFactor() requires GTK3 or later -#elif defined __WXGTK3__ +#elif GTK_MAJOR_VERSION == 3 params.emplace_back("dip_scale", numberTo<std::string>(parent.GetContentScaleFactor())); #else #error unknown GTK version! diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 87e6db77..b4002e5c 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "12.1"; //internal linkage! +const char ffsVersion[] = "12.2"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/wx+/tooltip.cpp b/wx+/tooltip.cpp index 5ad5da31..f8d23c5d 100644 --- a/wx+/tooltip.cpp +++ b/wx+/tooltip.cpp @@ -13,6 +13,7 @@ #include "image_tools.h" #include "bitmap_button.h" #include "dc.h" +#include <gtk/gtk.h> using namespace zen; @@ -27,7 +28,8 @@ class Tooltip::TooltipDlgGenerated : public wxDialog { public: TooltipDlgGenerated(wxWindow* parent) : //Suse Linux/X11: needs parent window, else there are z-order issues - wxDialog(parent, wxID_ANY, L"" /*title*/, wxDefaultPosition, wxDefaultSize, 0 /*style*/) + wxDialog(parent, wxID_ANY, L"" /*title*/, wxDefaultPosition, wxDefaultSize, wxSIMPLE_BORDER /*style*/) + //wxSIMPLE_BORDER side effect: removes title bar on KDE { SetSizeHints(wxDefaultSize, wxDefaultSize); SetExtraStyle(this->GetExtraStyle() | wxWS_EX_TRANSIENT); @@ -101,13 +103,16 @@ void Tooltip::hide() { if (tipWindow_) { -#ifdef __WXGTK2__ //the tooltip sometimes turns blank or is not shown again after it was hidden: e.g. drag-selection on middle grid +#if GTK_MAJOR_VERSION == 2 //the tooltip sometimes turns blank or is not shown again after it was hidden: e.g. drag-selection on middle grid //=> no such issues on GTK3! tipWindow_->Destroy(); //apply brute force: tipWindow_ = nullptr; // lastUsedImg_ = wxNullImage; -#else + +#elif GTK_MAJOR_VERSION == 3 tipWindow_->Hide(); +#else +#error unknown GTK version! #endif } } diff --git a/zen/argon2.cpp b/zen/argon2.cpp index f48abe5e..1250e545 100644 --- a/zen/argon2.cpp +++ b/zen/argon2.cpp @@ -59,7 +59,7 @@ /* ---------------------------------------------------------------------- -/* + * * A sort of 'abstract base class' or 'interface' or 'trait' which is * the common feature of all types that want to accept data formatted * using the SSH binary conventions of uint32, string, mpint etc. diff --git a/zen/file_access.cpp b/zen/file_access.cpp index ef6cdc80..c5cbf095 100644 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -8,12 +8,14 @@ #include <map> #include <algorithm> #include <chrono> +#include <variant> #include "file_traverser.h" #include "scope_guard.h" #include "symlink_target.h" #include "file_io.h" #include "crc.h" #include "guid.h" +#include "ring_buffer.h" #include <sys/vfs.h> //statfs #ifdef HAVE_SELINUX @@ -51,72 +53,79 @@ ItemType getItemTypeImpl(const Zstring& itemPath) //throw SysErrorCode return ItemType::folder; return ItemType::file; //S_ISREG || S_ISCHR || S_ISBLK || S_ISFIFO || S_ISSOCK } -} -ItemType zen::getItemType(const Zstring& itemPath) //throw FileError +std::variant<ItemType, Zstring /*last existing parent path*/> getItemTypeIfExistsImpl(const Zstring& itemPath) //throw SysError { try { + //fast check: 1. perf 2. expected by getFolderStatusNonBlocking() return getItemTypeImpl(itemPath); //throw SysErrorCode } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), e.toString()); } -} - - -std::variant<ItemType, Zstring /*last existing parent path*/> zen::getItemTypeIfExists(const Zstring& itemPath) //throw FileError -{ - try + catch (const SysErrorCode& e) //let's dig deeper, but *only* if error code sounds like "not existing" { - try - { - //fast check: 1. perf 2. expected by getFolderStatusNonBlocking() - return getItemTypeImpl(itemPath); //throw SysErrorCode - } - catch (const SysErrorCode& e) //let's dig deeper, but *only* if error code sounds like "not existing" + const std::optional<Zstring> parentPath = getParentFolderPath(itemPath); + if (!parentPath) //device root => quick access test + throw; + if (e.errorCode == ENOENT) { - const std::optional<Zstring> parentPath = getParentFolderPath(itemPath); - if (!parentPath) //device root => quick access test - throw; - if (e.errorCode == ENOENT) - { - const std::variant<ItemType, Zstring /*last existing parent path*/> parentTypeOrPath = getItemTypeIfExists(*parentPath); //throw FileError + const std::variant<ItemType, Zstring /*last existing parent path*/> parentTypeOrPath = getItemTypeIfExistsImpl(*parentPath); //throw SysError - if (const ItemType* parentType = std::get_if<ItemType>(&parentTypeOrPath)) - { - if (*parentType == ItemType::file /*obscure, but possible*/) - throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); + if (const ItemType* parentType = std::get_if<ItemType>(&parentTypeOrPath)) + { + if (*parentType == ItemType::file /*obscure, but possible*/) + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath)))); - const Zstring itemName = getItemName(itemPath); - assert(!itemName.empty()); + const Zstring itemName = getItemName(itemPath); + assert(!itemName.empty()); + try + { traverseFolder(*parentPath, //throw FileError [&](const FileInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }, [&](const FolderInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }, [&](const SymlinkInfo& si) { if (si.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); }); //- case-sensitive comparison! itemPath must be normalized! //- finding the item after getItemType() previously failed is exceptional - - return *parentPath; } - else - return parentTypeOrPath; + catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } + + return *parentPath; } else - throw; + return parentTypeOrPath; } + else + throw; } - catch (const SysError& e) +} +} + + +ItemType zen::getItemType(const Zstring& itemPath) //throw FileError +{ + try { - throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), e.toString()); + return getItemTypeImpl(itemPath); //throw SysErrorCode } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), e.toString()); } } -bool zen::itemExists(const Zstring& itemPath) //throw FileError +std::optional<ItemType> zen::getItemTypeIfExists(const Zstring& itemPath) //throw FileError { - const std::variant<ItemType, Zstring /*last existing parent path*/> typeOrPath = getItemTypeIfExists(itemPath); //throw FileError - return std::get_if<ItemType>(&typeOrPath); + try + { + const std::variant<ItemType, Zstring /*last existing parent path*/> typeOrPath = getItemTypeIfExistsImpl(itemPath); //throw SysError + if (const ItemType* type = std::get_if<ItemType>(&typeOrPath)) + return *type; + else + return std::nullopt; + } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(itemPath)), e.toString()); + } } @@ -130,16 +139,16 @@ namespace //- folderPath does not need to exist (yet) int64_t zen::getFreeDiskSpace(const Zstring& folderPath) //throw FileError { - const Zstring existingPath = [&] - { - const std::variant<ItemType, Zstring /*last existing parent path*/> typeOrPath = getItemTypeIfExists(folderPath); //throw FileError - if (std::get_if<ItemType>(&typeOrPath)) - return folderPath; - else - return std::get<Zstring>(typeOrPath); - }(); try { + const Zstring existingPath = [&] + { + const std::variant<ItemType, Zstring /*last existing parent path*/> typeOrPath = getItemTypeIfExistsImpl(folderPath); //throw SysError + if (std::get_if<ItemType>(&typeOrPath)) + return folderPath; + else + return std::get<Zstring>(typeOrPath); + }(); struct statfs info = {}; if (::statfs(existingPath.c_str(), &info) != 0) //follows symlinks! THROW_LAST_SYS_ERROR("statfs"); @@ -253,10 +262,14 @@ void removeDirectoryImpl(const Zstring& folderPath) //throw FileError void zen::removeDirectoryPlainRecursion(const Zstring& dirPath) //throw FileError { - if (getItemType(dirPath) == ItemType::symlink) //throw FileError - removeSymlinkPlain(dirPath); //throw FileError - else - removeDirectoryImpl(dirPath); //throw FileError + try + { + if (getItemTypeImpl(dirPath) == ItemType::symlink) //throw SysErrorCode + removeSymlinkPlain(dirPath); //throw FileError + else + removeDirectoryImpl(dirPath); //throw FileError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(dirPath)), e.toString()); } } @@ -354,16 +367,15 @@ void setWriteTimeNative(const Zstring& itemPath, const timespec& modTime, ProcSy //https://freefilesync.org/forum/viewtopic.php?t=2803 => utimensat() works (but not for gvfs SFTP) if (::utimensat(AT_FDCWD, itemPath.c_str(), newTimes, procSl == ProcSymlink::asLink ? AT_SYMLINK_NOFOLLOW : 0) == 0) return; + const ErrorCode ecUtimensat = errno; try { if (procSl == ProcSymlink::asLink) - try - { - if (getItemType(itemPath) == ItemType::symlink) //throw FileError - THROW_LAST_SYS_ERROR("utimensat(AT_SYMLINK_NOFOLLOW)"); //use lutimes()? just a wrapper around utimensat()! - //else: fall back - } - catch (const FileError& e) { throw SysError(e.toString()); } + { + if (getItemTypeImpl(itemPath) == ItemType::symlink) //throw SysErrorCode + throw SysError(formatSystemError("utimensat(AT_SYMLINK_NOFOLLOW)", ecUtimensat)); //use lutimes()? just a wrapper around utimensat()! + //else: fall back + } //in other cases utimensat() returns EINVAL for CIFS/NTFS drives, but open+futimens works: https://freefilesync.org/forum/viewtopic.php?t=387 //2017-07-04: O_WRONLY | O_APPEND seems to avoid EOPNOTSUPP on gvfs SFTP! @@ -475,10 +487,14 @@ void zen::copyItemPermissions(const Zstring& sourcePath, const Zstring& targetPa if (::lchown(targetPath.c_str(), fileInfo.st_uid, fileInfo.st_gid) != 0) // may require admin rights! THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "lchown"); - const bool isSymlinkTarget = getItemType(targetPath) == ItemType::symlink; //throw FileError - if (!isSymlinkTarget && //setting access permissions doesn't make sense for symlinks on Linux: there is no lchmod() - ::chmod(targetPath.c_str(), fileInfo.st_mode) != 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), "chmod"); + try + { + if (getItemTypeImpl(targetPath) != ItemType::symlink && //throw SysErrorCode + //setting access permissions doesn't make sense for symlinks on Linux: there is no lchmod() + ::chmod(targetPath.c_str(), fileInfo.st_mode) != 0) + THROW_LAST_SYS_ERROR("chmod"); + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(targetPath)), e.toString()); } } } @@ -517,29 +533,47 @@ void zen::createDirectory(const Zstring& dirPath) //throw FileError, ErrorTarget void zen::createDirectoryIfMissingRecursion(const Zstring& dirPath) //throw FileError { - //expect that path already exists (see: versioning, base folder, log file path) => check first - const std::variant<ItemType, Zstring /*last existing parent path*/> typeOrPath = getItemTypeIfExists(dirPath); //throw FileError - - if (const ItemType* type = std::get_if<ItemType>(&typeOrPath)) + auto getItemType2 = [&](const Zstring& itemPath) //throw FileError { - if (*type == ItemType::file /*obscure, but possible*/) + try + { return getItemTypeImpl(itemPath); } //throw SysErrorCode + catch (const SysErrorCode& e) //need to add context! + { throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)), - replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(dirPath)))); - } - else - { - const Zstring existingDirPath = std::get<Zstring>(typeOrPath); - assert(startsWith(dirPath, existingDirPath)); + replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getParentFolderPath(itemPath) ? getItemName(itemPath) : itemPath)) + L'\n' + + e.toString()); + } + }; - const ZstringView relPath = makeStringView(dirPath.begin() + existingDirPath.size(), dirPath.end()); - const std::vector<ZstringView> namesMissing = splitCpy(relPath, FILE_NAME_SEPARATOR, SplitOnEmpty::skip); - assert(!namesMissing.empty()); + try + { + //- path most likely already exists (see: versioning, base folder, log file path) => check first + //- do NOT use getItemTypeIfExists()! race condition when multiple threads are calling createDirectoryIfMissingRecursion(): https://freefilesync.org/forum/viewtopic.php?t=10137#p38062 + //- find first existing + accessible parent folder (backwards iteration): + Zstring dirPathEx = dirPath; + RingBuffer<Zstring> dirNames; //caveat: 1. might have been created in the meantime 2. getItemType2() may have failed with access error + for (;;) + try + { + if (getItemType2(dirPathEx) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(dirPathEx)))); + break; + } + catch (FileError&) //not yet existing or access error + { + const std::optional<Zstring> parentPath = getParentFolderPath(dirPathEx); + if (!parentPath)//device root => quick access test + throw; + dirNames.push_front(getItemName(dirPathEx)); + dirPathEx = *parentPath; + } + //----------------------------------------------------------- - Zstring dirPathNew = existingDirPath; - for (const ZstringView folderName : namesMissing) + Zstring dirPathNew = dirPathEx; + for (const Zstring& dirName : dirNames) try { - dirPathNew = appendPath(dirPathNew, Zstring(folderName)); + dirPathNew = appendPath(dirPathNew, dirName); createDirectory(dirPathNew); //throw FileError } @@ -547,18 +581,24 @@ void zen::createDirectoryIfMissingRecursion(const Zstring& dirPath) //throw File { try { - if (getItemType(dirPathNew) != ItemType::file /*obscure, but possible*/) //throw FileError - continue; //already existing => possible, if createFolderIfMissingRecursion() is run in parallel + if (getItemType2(dirPathNew) == ItemType::file /*obscure, but possible*/) //throw FileError + throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(dirPathNew)))); + else + continue; //already existing => possible, if createDirectoryIfMissingRecursion() is run in parallel } catch (FileError&) {} //not yet existing or access error throw; } } + catch (const SysError& e) + { + throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(dirPath)), e.toString()); + } } -void zen::tryCopyDirectoryAttributes(const Zstring& sourcePath, const Zstring& targetPath) //throw FileError +void zen::copyDirectoryAttributes(const Zstring& sourcePath, const Zstring& targetPath) //throw FileError { //do NOT copy attributes for volume root paths which return as: FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_DIRECTORY //https://freefilesync.org/forum/viewtopic.php?t=5550 @@ -570,10 +610,11 @@ void zen::tryCopyDirectoryAttributes(const Zstring& sourcePath, const Zstring& t void zen::copySymlink(const Zstring& sourcePath, const Zstring& targetPath) //throw FileError { - const SymlinkRawContent linkContent = getSymlinkRawContent(sourcePath); //throw FileError; accept broken symlinks - + SymlinkRawContent linkContent{}; try //harmonize with NativeFileSystem::equalSymlinkContentForSameAfsType() { + linkContent = getSymlinkRawContent_impl(sourcePath); //throw SysError; accept broken symlinks + if (::symlink(linkContent.targetPath.c_str(), targetPath.c_str()) != 0) THROW_LAST_SYS_ERROR("symlink"); } diff --git a/zen/file_access.h b/zen/file_access.h index 5af8b879..f6ac3740 100644 --- a/zen/file_access.h +++ b/zen/file_access.h @@ -8,7 +8,6 @@ #define FILE_ACCESS_H_8017341345614857 #include <functional> -#include <variant> #include "file_path.h" #include "file_error.h" #include "serialize.h" //IoCallback @@ -39,9 +38,9 @@ ItemType getItemType(const Zstring& itemPath); //throw FileError //execute potentially SLOW folder traversal but distinguish error/not existing: // - all child item path parts must correspond to folder traversal // => we can conclude whether an item is *not* existing anymore by doing a *case-sensitive* name search => potentially SLOW! -std::variant<ItemType, Zstring /*last existing parent path*/> getItemTypeIfExists(const Zstring& itemPath); //throw FileError +std::optional<ItemType> getItemTypeIfExists(const Zstring& itemPath); //throw FileError -bool itemExists(const Zstring& itemPath); //throw FileError +inline bool itemExists(const Zstring& itemPath) { return static_cast<bool>(getItemTypeIfExists(itemPath)); } //throw FileError enum class ProcSymlink { @@ -80,8 +79,7 @@ void createDirectoryIfMissingRecursion(const Zstring& dirPath); //throw FileErro //symlink handling: follow //expects existing source/target directories -//reports "note-worthy" errors only -void tryCopyDirectoryAttributes(const Zstring& sourcePath, const Zstring& targetPath); //throw FileError +void copyDirectoryAttributes(const Zstring& sourcePath, const Zstring& targetPath); //throw FileError void copySymlink(const Zstring& sourcePath, const Zstring& targetPath); //throw FileError diff --git a/zen/file_path.cpp b/zen/file_path.cpp index 7ef78569..c9d6f236 100644 --- a/zen/file_path.cpp +++ b/zen/file_path.cpp @@ -23,7 +23,7 @@ std::optional<PathComponents> zen::parsePathComponents(const Zstring& itemPath) Zstring rootPath(itemPathPf.begin(), rootWithSep ? it + 1 : it); Zstring relPath(it + 1, itemPathPf.end()); - trim(relPath, true, true, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); + trim(relPath, TrimSide::both, [](Zchar c) { return c == FILE_NAME_SEPARATOR; }); return PathComponents{std::move(rootPath), std::move(relPath)}; } @@ -203,24 +203,11 @@ std::optional<Zstring> zen::getEnvironmentVar(const ZstringView name) envVars = globalEnvVars.get(); } - auto it = envVars->find(name); + const auto it = envVars->find(name); if (it == envVars->end()) return {}; - Zstring value = it->second; - - //some postprocessing (good idea!? Is this even needed!? - warn_static("let's find out!") -#if 0 - trim(value); //remove leading, trailing blanks - - //remove leading, trailing double-quotes - if (startsWith(value, Zstr('"')) && - endsWith (value, Zstr('"')) && - value.length() >= 2) - value = Zstring(value.c_str() + 1, value.length() - 2); -#endif - return value; + return it->second; } diff --git a/zen/open_ssl.cpp b/zen/open_ssl.cpp index af4306b2..5cfd7f12 100644 --- a/zen/open_ssl.cpp +++ b/zen/open_ssl.cpp @@ -387,7 +387,7 @@ void zen::verifySignature(const std::string_view message, const std::string_view bool zen::isPuttyKeyStream(const std::string_view keyStream) { - return startsWith(trimCpy(keyStream, true, false), "PuTTY-User-Key-File-"); + return startsWith(trimCpy(keyStream, TrimSide::left), "PuTTY-User-Key-File-"); } diff --git a/zen/resolve_path.cpp b/zen/resolve_path.cpp index daaf91ff..7bf50b12 100644 --- a/zen/resolve_path.cpp +++ b/zen/resolve_path.cpp @@ -149,7 +149,7 @@ namespace Zstring tryExpandVolumeName(Zstring pathPhrase) // [volname]:\folder [volname]\folder [volname]folder -> C:\folder { //we only expect the [.*] pattern at the beginning => do not touch dir names like "C:\somedir\[stuff]" - trim(pathPhrase, true, false); + trim(pathPhrase, TrimSide::left); if (startsWith(pathPhrase, Zstr('['))) { diff --git a/zen/shutdown.cpp b/zen/shutdown.cpp index e64e1e70..ee68b467 100644 --- a/zen/shutdown.cpp +++ b/zen/shutdown.cpp @@ -55,7 +55,7 @@ void zen::terminateProcess(int exitCode) for (;;) //why still here?? => crash deliberately! - *reinterpret_cast<volatile int*>(0) = 0; //crude but at least we'll get crash dumps if it ever happens + *reinterpret_cast<volatile int*>(0) = 0; //crude but at least we'll get crash dumps *if* it ever happens } diff --git a/zen/socket.h b/zen/socket.h index 4ccde190..f706daab 100644 --- a/zen/socket.h +++ b/zen/socket.h @@ -19,6 +19,42 @@ namespace zen do { const ErrorCode ecInternal = getLastError(); throw SysError(formatSystemError(functionName, ecInternal)); } while (false) +#define THROW_LAST_SYS_ERROR_GAI(rcGai) \ + do { \ + if (rcGai == EAI_SYSTEM) /*"check errno for details"*/ \ + THROW_LAST_SYS_ERROR("getaddrinfo"); \ + \ + throw SysError(formatSystemError("getaddrinfo", formatGaiErrorCode(rcGai), utfTo<std::wstring>(::gai_strerror(rcGai)))); \ + } while (false) + +inline +std::wstring formatGaiErrorCode(int ec) +{ + switch (ec) //codes used on both Linux and macOS + { + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_ADDRFAMILY); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_AGAIN); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_BADFLAGS); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_FAIL); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_FAMILY); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_MEMORY); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NODATA); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NONAME); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SERVICE); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SOCKTYPE); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_SYSTEM); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_OVERFLOW); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_INPROGRESS); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_CANCELED); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_NOTCANCELED); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_ALLDONE); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_INTR); + ZEN_CHECK_CASE_FOR_CONSTANT(EAI_IDN_ENCODE); + default: + return replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(ec)); + } +} + //patch up socket portability: using SocketType = int; const SocketType invalidSocket = -1; @@ -53,7 +89,7 @@ public: const int rcGai = ::getaddrinfo(server.c_str(), serviceName.c_str(), &hints, &servinfo); if (rcGai != 0) - throw SysError(formatSystemError("getaddrinfo", replaceCpy(_("Error code %x"), L"%x", numberTo<std::wstring>(rcGai)), utfTo<std::wstring>(::gai_strerror(rcGai)))); + THROW_LAST_SYS_ERROR_GAI(rcGai); if (!servinfo) throw SysError(formatSystemError("getaddrinfo", L"", L"Empty server info.")); diff --git a/zen/string_tools.h b/zen/string_tools.h index 03563d41..1cd8ef0d 100644 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -73,9 +73,15 @@ template <class S, class Char, class Function> void split(const S& str, Char del template <class S, class Function1, class Function2> void split2(const S& str, Function1 isDelimiter, Function2 onStringPart); template <class S, class Char> [[nodiscard]] std::vector<S> splitCpy(const S& str, Char delimiter, SplitOnEmpty soe); -template <class S> [[nodiscard]] S trimCpy(const S& str, bool fromLeft = true, bool fromRight = true); -template <class S> void trim (S& str, bool fromLeft = true, bool fromRight = true); -template <class S, class Function> void trim(S& str, bool fromLeft, bool fromRight, Function trimThisChar); +enum class TrimSide +{ + both, + left, + right, +}; +template <class S> [[nodiscard]] S trimCpy(const S& str, TrimSide side = TrimSide::both); +template <class S> void trim(S& str, TrimSide side = TrimSide::both); +template <class S, class Function> void trim(S& str, TrimSide side, Function trimThisChar); template <class S, class T, class U> [[nodiscard]] S replaceCpy(S str, const T& oldTerm, const U& newTerm); @@ -540,15 +546,13 @@ S replaceCpyAsciiNoCase(S str, const T& oldTerm, const U& newTerm) template <class Char, class Function> [[nodiscard]] inline -std::pair<Char*, Char*> trimCpy2(Char* first, Char* last, bool fromLeft, bool fromRight, Function trimThisChar) +std::pair<Char*, Char*> trimCpy2(Char* first, Char* last, TrimSide side, Function trimThisChar) { - assert(fromLeft || fromRight); - - if (fromRight) + if (side == TrimSide::right || side == TrimSide::both) while (first != last && trimThisChar(last[-1])) --last; - if (fromLeft) + if (side == TrimSide::left || side == TrimSide::both) while (first != last && trimThisChar(*first)) ++first; @@ -557,12 +561,10 @@ std::pair<Char*, Char*> trimCpy2(Char* first, Char* last, bool fromLeft, bool fr template <class S, class Function> inline -void trim(S& str, bool fromLeft, bool fromRight, Function trimThisChar) +void trim(S& str, TrimSide side, Function trimThisChar) { - assert(fromLeft || fromRight); - const auto* const oldBegin = strBegin(str); - const auto& [newBegin, newEnd] = trimCpy2(oldBegin, oldBegin + strLength(str), fromLeft, fromRight, trimThisChar); + const auto [newBegin, newEnd] = trimCpy2(oldBegin, oldBegin + strLength(str), side, trimThisChar); if (newBegin != oldBegin) str = S(newBegin, newEnd); //minor inefficiency: in case "str" is not shared, we could save an allocation and do a memory move only @@ -572,21 +574,21 @@ void trim(S& str, bool fromLeft, bool fromRight, Function trimThisChar) template <class S> inline -void trim(S& str, bool fromLeft, bool fromRight) +void trim(S& str, TrimSide side) { using CharType = GetCharTypeT<S>; - trim(str, fromLeft, fromRight, [](CharType c) { return isWhiteSpace(c); }); + trim(str, side, [](CharType c) { return isWhiteSpace(c); }); } template <class S> inline -S trimCpy(const S& str, bool fromLeft, bool fromRight) +S trimCpy(const S& str, TrimSide side) { using CharType = GetCharTypeT<S>; const auto* const oldBegin = strBegin(str); const auto* const oldEnd = oldBegin + strLength(str); - const auto& [newBegin, newEnd] = trimCpy2(oldBegin, oldEnd, fromLeft, fromRight, [](CharType c) { return isWhiteSpace(c); }); + const auto [newBegin, newEnd] = trimCpy2(oldBegin, oldEnd, side, [](CharType c) { return isWhiteSpace(c); }); if (newBegin == oldBegin && newEnd == oldEnd) return str; diff --git a/zen/symlink_target.h b/zen/symlink_target.h index 89c00571..083360af 100644 --- a/zen/symlink_target.h +++ b/zen/symlink_target.h @@ -38,7 +38,7 @@ Zstring getSymlinkResolvedPath(const Zstring& linkPath); //throw FileError namespace { //retrieve raw target data of symlink or junction -zen::SymlinkRawContent getSymlinkRawContent_impl(const Zstring& linkPath) //throw FileError +zen::SymlinkRawContent getSymlinkRawContent_impl(const Zstring& linkPath) //throw SysError { using namespace zen; const size_t bufSize = 10000; @@ -46,26 +46,22 @@ zen::SymlinkRawContent getSymlinkRawContent_impl(const Zstring& linkPath) //thro const ssize_t bytesWritten = ::readlink(linkPath.c_str(), buf.data(), bufSize); if (bytesWritten < 0) - THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), "readlink"); + THROW_LAST_SYS_ERROR("readlink"); if (makeUnsigned(bytesWritten) >= bufSize) //detect truncation; not an error for readlink! - throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), formatSystemError("readlink", L"", L"Buffer truncated.")); + throw SysError(formatSystemError("readlink", L"", L"Buffer truncated.")); return {.targetPath = Zstring(buf.data(), bytesWritten)}; //readlink does not append 0-termination! } -Zstring getResolvedSymlinkPath_impl(const Zstring& linkPath) //throw FileError +Zstring getResolvedSymlinkPath_impl(const Zstring& linkPath) //throw SysError { using namespace zen; - try - { - char* targetPath = ::realpath(linkPath.c_str(), nullptr); - if (!targetPath) - THROW_LAST_SYS_ERROR("realpath"); - ZEN_ON_SCOPE_EXIT(::free(targetPath)); - return targetPath; - } - catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(linkPath)), e.toString()); } + char* targetPath = ::realpath(linkPath.c_str(), nullptr); + if (!targetPath) + THROW_LAST_SYS_ERROR("realpath"); + ZEN_ON_SCOPE_EXIT(::free(targetPath)); + return targetPath; } } @@ -73,11 +69,25 @@ Zstring getResolvedSymlinkPath_impl(const Zstring& linkPath) //throw FileError namespace zen { inline -SymlinkRawContent getSymlinkRawContent(const Zstring& linkPath) { return getSymlinkRawContent_impl(linkPath); } //throw FileError +SymlinkRawContent getSymlinkRawContent(const Zstring& linkPath) +{ + try + { + return getSymlinkRawContent_impl(linkPath); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(linkPath)), e.toString()); } +} inline -Zstring getSymlinkResolvedPath(const Zstring& linkPath) { return getResolvedSymlinkPath_impl(linkPath); } //throw FileError +Zstring getSymlinkResolvedPath(const Zstring& linkPath) +{ + try + { + return getResolvedSymlinkPath_impl(linkPath); //throw SysError + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(linkPath)), e.toString()); } +} } diff --git a/zen/sys_info.cpp b/zen/sys_info.cpp index 55465711..2a32d247 100644 --- a/zen/sys_info.cpp +++ b/zen/sys_info.cpp @@ -131,8 +131,8 @@ ComputerModel zen::getComputerModel() //throw FileError cm.model = beforeFirst(cm.model, L'\u00ff', IfNotFoundReturn::all); //fix broken BIOS entries: cm.vendor = beforeFirst(cm.vendor, L'\u00ff', IfNotFoundReturn::all); //0xff can be considered 0 - trim(cm.model, false, true, [](wchar_t c) { return c == L'_'; }); //e.g. "CBX3___" or just "_" - trim(cm.vendor, false, true, [](wchar_t c) { return c == L'_'; }); //e.g. "DELL__" or just "_" + trim(cm.model, TrimSide::right, [](wchar_t c) { return c == L'_'; }); //e.g. "CBX3___" or just "_" + trim(cm.vendor, TrimSide::right, [](wchar_t c) { return c == L'_'; }); //e.g. "DELL__" or just "_" for (const char* dummyModel : { @@ -209,9 +209,13 @@ std::wstring zen::getOsDescription() //throw FileError Zstring zen::getRealProcessPath() //throw FileError { - return getSymlinkRawContent("/proc/self/exe").targetPath; //throw FileError - //path does not contain symlinks => no need for ::realpath() + try + { + return getSymlinkRawContent_impl("/proc/self/exe").targetPath; //throw SysError + //path does not contain symlinks => no need for ::realpath() + } + catch (const SysError& e) { throw FileError(_("Cannot get process information."), e.toString()); } } diff --git a/zen/sys_version.cpp b/zen/sys_version.cpp index b123ba07..e57c9b69 100644 --- a/zen/sys_version.cpp +++ b/zen/sys_version.cpp @@ -55,8 +55,8 @@ OsVersionDetail zen::getOsVersionDetail() //throw SysError osVersion = utfTo<std::wstring>(afterFirst(line, '=', IfNotFoundReturn::none)); //PRETTY_NAME? too wordy! e.g. "Fedora 17 (Beefy Miracle)" }); - trim(osName, true, true, [](char c) { return c == L'"' || c == L'\''; }); - trim(osVersion, true, true, [](char c) { return c == L'"' || c == L'\''; }); + trim(osName, TrimSide::both, [](char c) { return c == L'"' || c == L'\''; }); + trim(osVersion, TrimSide::both, [](char c) { return c == L'"' || c == L'\''; }); } if (osName.empty()) |