diff options
author | B Stack <bgstack15@gmail.com> | 2018-10-16 17:33:51 -0400 |
---|---|---|
committer | B Stack <bgstack15@gmail.com> | 2018-10-16 17:33:51 -0400 |
commit | 878a41d3be13da2a654df74f2a35ea8b295c8a13 (patch) | |
tree | 89b2a018482c164bdd8ecac5c76b19a08f420dec | |
parent | Merge branch '10.4' into 'master' (diff) | |
download | FreeFileSync-878a41d3be13da2a654df74f2a35ea8b295c8a13.tar.gz FreeFileSync-878a41d3be13da2a654df74f2a35ea8b295c8a13.tar.bz2 FreeFileSync-878a41d3be13da2a654df74f2a35ea8b295c8a13.zip |
10.5
102 files changed, 2197 insertions, 1216 deletions
diff --git a/Changelog.txt b/Changelog.txt index 90955af9..004aca9d 100755 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,22 @@ +FreeFileSync 10.5 [2018-10-11] +------------------------------ +New file matching algorithm considering Unicode normalization +User-configurable timeout for FTP and SFTP connections +Obsoleted old CHM manual in favor of PDF +Ignore case sensitivity during filter matching (Linux) +Unicode-normalized and faster case-insensitive grid search +New button to save current view filter settings as default +Both slash and backslash can be used in filter expressions +Improved Unicode case conversion routines +Keyboard shortcuts for swap sides (F10) and view category (F11) +Don't steal input focus when closing progress dialog (macOS) +Fixed shutdown crash when accessing already destroyed state +Fixed file grid column order not being preserved +Fixed manual activation input fields being disabled (macOS) +Fixed FTP parsing error due to invalid folder time +Fixed statistics boxes background distortion (macOS) + + FreeFileSync 10.4 [2018-09-09] ------------------------------ Allow overriding log folder path for gui and batch runs diff --git a/FreeFileSync/Build/Languages/german.lng b/FreeFileSync/Build/Languages/german.lng index 51d22174..4b79745a 100755 --- a/FreeFileSync/Build/Languages/german.lng +++ b/FreeFileSync/Build/Languages/german.lng @@ -7,11 +7,20 @@ <plural_definition>n == 1 ? 0 : 1</plural_definition> </header> -<source>Default log path:</source> -<target>Standardprotokollpfad:</target> +<source>Access timeout (in seconds):</source> +<target>Zeitlimit für Zugriff(in Sekunden):</target> -<source>&Override default log path:</source> -<target>&Standardprotokollpfad überschreiben:</target> +<source>The server returned an error:</source> +<target>Der Server hat einen Fehler zurückgegeben:</target> + +<source>You may close this page now and continue with FreeFileSync.</source> +<target>Sie können diese Seite nun schließen und mit FreeFileSync fortfahren.</target> + +<source>Authentication failed.</source> +<target>Authentifizierung fehlgeschlagen.</target> + +<source>Authentication completed.</source> +<target>Authentifizierung abgeschlossen.</target> <source>Both sides have changed since last synchronization.</source> <target>Beide Seiten wurden seit der letzten Synchronisation verändert.</target> @@ -193,9 +202,6 @@ <source>File time tolerance</source> <target>Dateizeittoleranz</target> -<source>Folder access timeout</source> -<target>Zeitlimit für Ordnerzugriff</target> - <source>Run with background priority</source> <target>Mit Hintergrundpriorität ausführen</target> @@ -591,6 +597,12 @@ Tatsächlich: %y bytes <source>Unable to move %x to the recycle bin.</source> <target>%x kann nicht in den Papierkorb verschoben werden.</target> +<source>Unable to access %x.</source> +<target>Auf %x kann nicht zugegriffen werden.</target> + +<source>Cannot determine free disk space for %x.</source> +<target>Der freie Speicherplatz für %x konnte nicht ermittelt werden.</target> + <source>Cannot find %x.</source> <target>%x wurde nicht gefunden.</target> @@ -603,18 +615,12 @@ Tatsächlich: %y bytes <source>Cannot delete symbolic link %x.</source> <target>Die symbolischen Verknüpfung %x kann nicht gelöscht werden.</target> -<source>Cannot determine free disk space for %x.</source> -<target>Der freie Speicherplatz für %x konnte nicht ermittelt werden.</target> - <source>Incorrect command line:</source> <target>Ungültige Befehlszeile:</target> <source>The server does not support authentication via %x.</source> <target>Der Server unterstützt keine Authentifizierung über %x.</target> -<source>Unable to access %x.</source> -<target>Auf %x kann nicht zugegriffen werden.</target> - <source> <pluralform>Operation timed out after 1 second.</pluralform> <pluralform>Operation timed out after %x seconds.</pluralform> @@ -1144,6 +1150,9 @@ Die Befehlszeile wird ausgelöst, wenn: <source>Last x days:</source> <target>Letzte x Tage:</target> +<source>&Override default log path:</source> +<target>&Standardprotokollpfad überschreiben:</target> + <source>Run a command after synchronization:</source> <target>Befehl nach Synchronisation ausführen:</target> @@ -1309,6 +1318,9 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. <source>Show all permanently hidden dialogs and warning messages again</source> <target>Alle dauerhaft versteckten Fenster und Warnmeldungen wieder anzeigen</target> +<source>Default log path:</source> +<target>Standardprotokollpfad:</target> + <source>Remove old log files after x days:</source> <target>Alte Protokolldateien nach x Tagen entfernen:</target> @@ -1954,12 +1966,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. <source>Checking recycle bin failed for folder %x.</source> <target>Die Prüfung des Papierkorbs für Ordner %x ist fehlgeschlagen.</target> -<source>The following XML elements could not be read:</source> -<target>Die folgenden XML-Elemente konnten nicht gelesen werden:</target> - -<source>Configuration file %x is incomplete. The missing elements will be set to their default values.</source> -<target>Die Konfigurationsdatei %x ist unvollständig. Die fehlenden Elemente werden auf ihre Standardwerte gesetzt.</target> - <source>Prepare installation</source> <target>Installation vorbereiten</target> diff --git a/FreeFileSync/Source/Makefile b/FreeFileSync/Source/Makefile index efc73825..bd274138 100755 --- a/FreeFileSync/Source/Makefile +++ b/FreeFileSync/Source/Makefile @@ -68,14 +68,13 @@ CPP_FILES+=ui/gui_generated.cpp CPP_FILES+=ui/gui_status_handler.cpp CPP_FILES+=ui/main_dlg.cpp CPP_FILES+=ui/progress_indicator.cpp -CPP_FILES+=ui/search.cpp +CPP_FILES+=ui/search_grid.cpp CPP_FILES+=ui/small_dlgs.cpp CPP_FILES+=ui/sync_cfg.cpp CPP_FILES+=ui/taskbar.cpp CPP_FILES+=ui/tray_icon.cpp CPP_FILES+=ui/triple_splitter.cpp CPP_FILES+=ui/version_check.cpp -CPP_FILES+=../../zen/xml_io.cpp CPP_FILES+=../../zen/recycler.cpp CPP_FILES+=../../zen/file_access.cpp CPP_FILES+=../../zen/file_io.cpp @@ -118,11 +117,11 @@ install: mkdir -p $(APPSHAREDIR) cp -R ../Build/Languages/ \ - ../Build/Help/ \ ../Build/ding.wav \ ../Build/gong.wav \ ../Build/harp.wav \ ../Build/Resources.zip \ + "../Build/User Manual.pdf" \ $(APPSHAREDIR) mkdir -p $(DOCSHAREDIR) diff --git a/FreeFileSync/Source/RealTimeSync/Makefile b/FreeFileSync/Source/RealTimeSync/Makefile index 61aee2cb..900d2d22 100755 --- a/FreeFileSync/Source/RealTimeSync/Makefile +++ b/FreeFileSync/Source/RealTimeSync/Makefile @@ -23,7 +23,6 @@ CPP_FILES+=folder_selector2.cpp CPP_FILES+=../base/localization.cpp CPP_FILES+=../base/resolve_path.cpp CPP_FILES+=../base/ffs_paths.cpp -CPP_FILES+=../../../zen/xml_io.cpp CPP_FILES+=../../../zen/dir_watcher.cpp CPP_FILES+=../../../zen/file_access.cpp CPP_FILES+=../../../zen/file_io.cpp diff --git a/FreeFileSync/Source/RealTimeSync/application.cpp b/FreeFileSync/Source/RealTimeSync/application.cpp index 69f7475f..6047468f 100755 --- a/FreeFileSync/Source/RealTimeSync/application.cpp +++ b/FreeFileSync/Source/RealTimeSync/application.cpp @@ -17,7 +17,7 @@ #include "../base/localization.h" #include "../base/ffs_paths.h" #include "../base/return_codes.h" -#include "../base/error_log.h" +#include "../base/fatal_error.h" #include "../base/help_provider.h" #include "../base/resolve_path.h" diff --git a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp index 0f895246..298d55b6 100755 --- a/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp +++ b/FreeFileSync/Source/RealTimeSync/folder_selector2.cpp @@ -36,7 +36,7 @@ void setFolderPath(const Zstring& dirpath, wxTextCtrl* txtCtrl, wxWindow& toolti tooltipWnd.SetToolTip(utfTo<wxString>(folderPathFmt)); if (staticText) //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway - staticText->SetLabel(equalFilePath(appendSeparator(trimCpy(dirpath)), appendSeparator(folderPathFmt)) ? wxString(_("Drag && drop")) : utfTo<wxString>(folderPathFmt)); + staticText->SetLabel(equalLocalPath(appendSeparator(trimCpy(dirpath)), appendSeparator(folderPathFmt)) ? wxString(_("Drag && drop")) : utfTo<wxString>(folderPathFmt)); } } @@ -150,9 +150,7 @@ void FolderSelector2::onSelectDir(wxCommandEvent& event) Zstring FolderSelector2::getPath() const { - Zstring path = utfTo<Zstring>(folderPathCtrl_.GetValue()); - - return path; + return utfTo<Zstring>(folderPathCtrl_.GetValue()); } diff --git a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp index 3ee8956b..e4dfcb56 100755 --- a/FreeFileSync/Source/RealTimeSync/main_dlg.cpp +++ b/FreeFileSync/Source/RealTimeSync/main_dlg.cpp @@ -18,7 +18,6 @@ #include "tray_menu.h" #include "app_icon.h" #include "../base/help_provider.h" -//#include "../base/process_xml.h" #include "../base/ffs_paths.h" #include "../version/version.h" @@ -35,8 +34,8 @@ namespace std::wstring extractJobName(const Zstring& cfgFilePath) { - const Zstring shortName = afterLast(cfgFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); - const Zstring jobName = beforeLast(shortName, Zstr('.'), IF_MISSING_RETURN_ALL); + const Zstring fileName = afterLast(cfgFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); + const Zstring jobName = beforeLast(fileName, Zstr('.'), IF_MISSING_RETURN_ALL); return utfTo<std::wstring>(jobName); } @@ -213,7 +212,7 @@ void MainDialog::OnStart(wxCommandEvent& event) Hide(); XmlRealConfig currentCfg = getConfiguration(); - const Zstring activeCfgFilePath = !equalFilePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + const Zstring activeCfgFilePath = !equalLocalPath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); switch (runFolderMonitor(currentCfg, ::extractJobName(activeCfgFilePath))) { @@ -232,16 +231,16 @@ void MainDialog::OnStart(wxCommandEvent& event) void MainDialog::OnConfigSave(wxCommandEvent& event) { - Zstring defaultFilePath = !activeConfigFile_.empty() && !equalFilePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstr("Realtime.ffs_real"); + const Zstring defaultFilePath = !activeConfigFile_.empty() && !equalLocalPath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstr("Realtime.ffs_real"); + auto defaultFolder = utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); + auto defaultFileName = utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)); + //attention: currentConfigFileName may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - if (endsWith(defaultFilePath, Zstr(".ffs_batch"), CmpFilePath())) - defaultFilePath = beforeLast(defaultFilePath, Zstr("."), IF_MISSING_RETURN_NONE) + Zstr(".ffs_real"); + defaultFileName = beforeLast(defaultFileName, L'.', IF_MISSING_RETURN_ALL) + L".ffs_real"; wxFileDialog filePicker(this, wxString(), - //OS X really needs dir/file separated like this: - utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)), //default dir - utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)), //default file + defaultFolder, defaultFileName, //OS X really needs dir/file separated like this wxString(L"RealTimeSync (*.ffs_real)|*.ffs_real") + L"|" +_("All files") + L" (*.*)|*", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (filePicker.ShowModal() != wxID_OK) @@ -291,7 +290,7 @@ void MainDialog::setLastUsedConfig(const Zstring& filepath) { activeConfigFile_ = filepath; - const Zstring activeCfgFilePath = !equalFilePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + const Zstring activeCfgFilePath = !equalLocalPath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); if (!activeCfgFilePath.empty()) SetTitle(utfTo<wxString>(activeCfgFilePath)); @@ -309,7 +308,7 @@ void MainDialog::OnConfigNew(wxCommandEvent& event) void MainDialog::OnConfigLoad(wxCommandEvent& event) { - const Zstring activeCfgFilePath = !equalFilePath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); + const Zstring activeCfgFilePath = !equalLocalPath(activeConfigFile_, lastRunConfigPath_) ? activeConfigFile_ : Zstring(); wxFileDialog filePicker(this, wxString(), diff --git a/FreeFileSync/Source/RealTimeSync/monitor.cpp b/FreeFileSync/Source/RealTimeSync/monitor.cpp index 84e3e363..8d12080b 100755 --- a/FreeFileSync/Source/RealTimeSync/monitor.cpp +++ b/FreeFileSync/Source/RealTimeSync/monitor.cpp @@ -22,14 +22,14 @@ const std::chrono::seconds FOLDER_EXISTENCE_CHECK_INTERVAL(1); //wait until all directories become available (again) + logs in network share -std::set<Zstring, LessFilePath> waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError - const std::function<void(const Zstring& folderPath)>& requestUiRefresh, std::chrono::milliseconds cbInterval) +std::set<Zstring, LessLocalPath> waitForMissingDirs(const std::vector<Zstring>& folderPathPhrases, //throw FileError + const std::function<void(const Zstring& folderPath)>& requestUiRefresh, std::chrono::milliseconds cbInterval) { //early failure! check for unsupported folder paths: for (const Zstring& protoName : { Zstr("FTP"), Zstr("SFTP"), Zstr("MTP") }) for (const Zstring& phrase : folderPathPhrases) //hopefully clear enough now: https://freefilesync.org/forum/viewtopic.php?t=4302 - if (startsWith(trimCpy(phrase), protoName + Zstr(":"), CmpAsciiNoCase())) + if (startsWithAsciiNoCase(trimCpy(phrase), protoName + Zstr(":"))) throw FileError(replaceCpy(_("The %x protocol does not support directory monitoring:"), L"%x", utfTo<std::wstring>(protoName)) + L"\n\n" + fmtPath(phrase)); for (;;) @@ -39,7 +39,7 @@ std::set<Zstring, LessFilePath> waitForMissingDirs(const std::vector<Zstring>& f Zstring folderPathPhrase; std::future<bool> folderAvailable; }; - std::map<Zstring, FolderInfo, LessFilePath> folderInfos; //folderPath => FolderInfo + std::map<Zstring, FolderInfo, LessLocalPath> folderInfos; //folderPath => FolderInfo for (const Zstring& phrase : folderPathPhrases) { @@ -50,12 +50,11 @@ std::set<Zstring, LessFilePath> waitForMissingDirs(const std::vector<Zstring>& f folderInfos[folderPath] = { phrase, runAsync([folderPath]{ return dirAvailable(folderPath); }) }; } - std::set<Zstring, LessFilePath> availablePaths; - std::set<Zstring, LessFilePath> missingPathPhrases; - for (auto& item : folderInfos) + std::set<Zstring, LessLocalPath> availablePaths; + std::set<Zstring, LessLocalPath> missingPathPhrases; + for (auto& [folderPath, folderInfo] : folderInfos) { - const Zstring& folderPath = item.first; - std::future<bool>& folderAvailable = item.second.folderAvailable; + std::future<bool>& folderAvailable = folderInfo.folderAvailable; while (folderAvailable.wait_for(cbInterval) != std::future_status::ready) requestUiRefresh(folderPath); //throw X @@ -63,7 +62,7 @@ std::set<Zstring, LessFilePath> waitForMissingDirs(const std::vector<Zstring>& f if (folderAvailable.get()) availablePaths.insert(folderPath); else - missingPathPhrases.insert(item.second.folderPathPhrase); + missingPathPhrases.insert(folderInfo.folderPathPhrase); } if (missingPathPhrases.empty()) return availablePaths; //only return when all folders were found on *first* try! @@ -119,7 +118,7 @@ struct WaitResult }; -WaitResult waitForChanges(const std::set<Zstring, LessFilePath>& folderPaths, //throw FileError +WaitResult waitForChanges(const std::set<Zstring, LessLocalPath>& folderPaths, //throw FileError const std::function<void(bool readyForSync)>& requestUiRefresh, std::chrono::milliseconds cbInterval) { assert(std::all_of(folderPaths.begin(), folderPaths.end(), [](const Zstring& folderPath) { return dirAvailable(folderPath); })); @@ -154,19 +153,16 @@ WaitResult waitForChanges(const std::set<Zstring, LessFilePath>& folderPaths, // return false; }(); - for (const auto& item : watches) + for (const auto& [folderPath, watcher] : watches) { - const Zstring& folderPath = item.first; - DirWatcher& watcher = *item.second; - //IMPORTANT CHECK: DirWatcher has problems detecting removal of top watched directories! if (checkDirNow) if (!dirAvailable(folderPath)) //catch errors related to directory removal, e.g. ERROR_NETNAME_DELETED return WaitResult(folderPath); try { - std::vector<DirWatcher::Entry> changedItems = watcher.getChanges([&] { requestUiRefresh(false /*readyForSync*/); /*throw X*/ }, - cbInterval); //throw FileError + std::vector<DirWatcher::Entry> changedItems = watcher->getChanges([&] { requestUiRefresh(false /*readyForSync*/); /*throw X*/ }, + cbInterval); //throw FileError erase_if(changedItems, [](const DirWatcher::Entry& e) { return @@ -226,7 +222,7 @@ void rts::monitorDirectories(const std::vector<Zstring>& folderPathPhrases, std: for (;;) try { - std::set<Zstring, LessFilePath> folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiRefresh(&folderPath); }, cbInterval); //throw FileError + std::set<Zstring, LessLocalPath> folderPaths = waitForMissingDirs(folderPathPhrases, [&](const Zstring& folderPath) { requestUiRefresh(&folderPath); }, cbInterval); //throw FileError //schedule initial execution (*after* all directories have arrived) auto nextExecTime = std::chrono::steady_clock::now() + delay; diff --git a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp index ddc5ea1c..667b480d 100755 --- a/FreeFileSync/Source/RealTimeSync/tray_menu.cpp +++ b/FreeFileSync/Source/RealTimeSync/tray_menu.cpp @@ -114,7 +114,7 @@ private: realtimeIcon.CopyFromBitmap(bmp); wxString tooltip = L"RealTimeSync\n" + statusTxt; if (!jobName_.empty()) - tooltip += L"\n\"" + jobName_ + L"\""; + tooltip += L"\n\"" + jobName_ + L'"'; SetIcon(realtimeIcon, tooltip); } diff --git a/FreeFileSync/Source/RealTimeSync/xml_proc.cpp b/FreeFileSync/Source/RealTimeSync/xml_proc.cpp index dd933e0d..1e8ca476 100755 --- a/FreeFileSync/Source/RealTimeSync/xml_proc.cpp +++ b/FreeFileSync/Source/RealTimeSync/xml_proc.cpp @@ -6,6 +6,7 @@ #include "xml_proc.h" #include <zen/file_access.h> +#include <zenxml/xml.h> #include <wx/intl.h> #include "../base/ffs_paths.h" #include "../base/localization.h" @@ -75,7 +76,7 @@ void writeConfig(const XmlRealConfig& config, XmlOut& out) template <class ConfigType> void readConfig(const Zstring& filePath, RtsXmlType type, ConfigType& cfg, std::wstring& warningMsg) //throw FileError { - XmlDoc doc = loadXmlDocument(filePath); //throw FileError + XmlDoc doc = loadXml(filePath); //throw FileError if (getXmlTypeNoThrow(doc) != type) //noexcept throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); @@ -85,7 +86,7 @@ void readConfig(const Zstring& filePath, RtsXmlType type, ConfigType& cfg, std:: try { - checkForMappingErrors(in, filePath); //throw FileError + checkXmlMappingErrors(in, filePath); //throw FileError } catch (const FileError& e) { warningMsg = e.toString(); } } @@ -106,14 +107,14 @@ void rts::writeConfig(const XmlRealConfig& config, const Zstring& filepath) //th XmlOut out(doc); ::writeConfig(config, out); - saveXmlDocument(doc, filepath); //throw FileError + saveXml(doc, filepath); //throw FileError } void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& config, std::wstring& warningMsg) //throw FileError { - //do NOT use zen::loadStream as it will needlessly load even huge files! - XmlDoc doc = loadXmlDocument(filePath); //throw FileError; quick exit if file is not an FFS XML + XmlDoc doc = loadXml(filePath); //throw FileError + //quick exit if file is not an FFS XML const RtsXmlType xmlType = ::getXmlTypeNoThrow(doc); @@ -123,7 +124,7 @@ void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& config, XmlIn in(doc); //read folder pairs - std::set<Zstring, LessFilePath> uniqueFolders; + std::set<Zstring, LessLocalPath> uniqueFolders; for (XmlIn inPair = in["FolderPairs"]["Pair"]; inPair; inPair.next()) { @@ -137,13 +138,13 @@ void rts::readRealOrBatchConfig(const Zstring& filePath, XmlRealConfig& config, } //don't consider failure a warning only: - checkForMappingErrors(in, filePath); //throw FileError + checkXmlMappingErrors(in, filePath); //throw FileError //--------------------------------------------------------------------------------------- erase_if(uniqueFolders, [](const Zstring& str) { return trimCpy(str).empty(); }); config.directories.assign(uniqueFolders.begin(), uniqueFolders.end()); - config.commandline = Zstr("\"") + fff::getFreeFileSyncLauncherPath() + Zstr("\" \"") + filePath + Zstr("\""); + config.commandline = Zstr('"') + fff::getFreeFileSyncLauncherPath() + Zstr("\" \"") + filePath + Zstr('"'); } else return readConfig(filePath, config, warningMsg); //throw FileError @@ -157,7 +158,7 @@ wxLanguage rts::getProgramLanguage() //throw FileError XmlDoc doc; try { - doc = loadXmlDocument(filePath); //throw FileError + doc = loadXml(filePath); //throw FileError } catch (FileError&) { @@ -174,6 +175,6 @@ wxLanguage rts::getProgramLanguage() //throw FileError wxLanguage lng = wxLANGUAGE_UNKNOWN; in["General"]["Language"].attribute("Name", lng); - checkForMappingErrors(in, filePath); //throw FileError + checkXmlMappingErrors(in, filePath); //throw FileError return lng; } diff --git a/FreeFileSync/Source/RealTimeSync/xml_proc.h b/FreeFileSync/Source/RealTimeSync/xml_proc.h index 305ca76d..ebad3c7e 100755 --- a/FreeFileSync/Source/RealTimeSync/xml_proc.h +++ b/FreeFileSync/Source/RealTimeSync/xml_proc.h @@ -8,7 +8,6 @@ #define XML_PROC_H_0813748158321813490 #include <vector> -#include <zen/xml_io.h> #include <zen/zstring.h> #include <wx/language.h> diff --git a/FreeFileSync/Source/base/algorithm.cpp b/FreeFileSync/Source/base/algorithm.cpp index 044349b3..ff1bc787 100755 --- a/FreeFileSync/Source/base/algorithm.cpp +++ b/FreeFileSync/Source/base/algorithm.cpp @@ -192,13 +192,6 @@ bool fff::allElementsEqual(const FolderComparison& folderCmp) namespace { template <SelectedSide side> inline -const InSyncDescrFile& getDescriptor(const InSyncFile& dbFile) { return dbFile.left; } - -template <> inline -const InSyncDescrFile& getDescriptor<RIGHT_SIDE>(const InSyncFile& dbFile) { return dbFile.right; } - - -template <SelectedSide side> inline bool matchesDbEntry(const FilePair& file, const InSyncFolder::FileList::value_type* dbFile, const std::vector<unsigned int>& ignoreTimeShiftMinutes) { if (file.isEmpty<side>()) @@ -206,10 +199,10 @@ bool matchesDbEntry(const FilePair& file, const InSyncFolder::FileList::value_ty else if (!dbFile) return false; - const Zstring& shortNameDb = dbFile->first; - const InSyncDescrFile& descrDb = getDescriptor<side>(dbFile->second); + const Zstring& fileNameDb = dbFile->first; + const InSyncDescrFile& descrDb = SelectParam<side>::ref(dbFile->second.left, dbFile->second.right); - return file.getItemName<side>() == shortNameDb && //detect changes in case (windows) + return getUnicodeNormalForm(file.getItemName<side>()) == getUnicodeNormalForm(fileNameDb) && //detect changes in case (but ignore Unicode normal forms) //respect 2 second FAT/FAT32 precision! copying a file to a FAT32 drive changes it's modification date by up to 2 seconds //we're not interested in "fileTimeTolerance" here! sameFileTime(file.getLastWriteTime<side>(), descrDb.modTime, 2, ignoreTimeShiftMinutes) && @@ -244,13 +237,6 @@ bool stillInSync(const InSyncFile& dbFile, CompareVariant compareVar, int fileTi //-------------------------------------------------------------------- -template <SelectedSide side> inline -const InSyncDescrLink& getDescriptor(const InSyncSymlink& dbLink) { return dbLink.left; } - -template <> inline -const InSyncDescrLink& getDescriptor<RIGHT_SIDE>(const InSyncSymlink& dbLink) { return dbLink.right; } - - //check whether database entry and current item match: *irrespective* of current comparison settings template <SelectedSide side> inline bool matchesDbEntry(const SymlinkPair& symlink, const InSyncFolder::SymlinkList::value_type* dbSymlink, const std::vector<unsigned int>& ignoreTimeShiftMinutes) @@ -260,10 +246,10 @@ bool matchesDbEntry(const SymlinkPair& symlink, const InSyncFolder::SymlinkList: else if (!dbSymlink) return false; - const Zstring& shortNameDb = dbSymlink->first; - const InSyncDescrLink& descrDb = getDescriptor<side>(dbSymlink->second); + const Zstring& linkNameDb = dbSymlink->first; + const InSyncDescrLink& descrDb = SelectParam<side>::ref(dbSymlink->second.left, dbSymlink->second.right); - return symlink.getItemName<side>() == shortNameDb && + return getUnicodeNormalForm(symlink.getItemName<side>()) == getUnicodeNormalForm(linkNameDb) && //respect 2 second FAT/FAT32 precision! copying a file to a FAT32 drive changes its modification date by up to 2 seconds sameFileTime(symlink.getLastWriteTime<side>(), descrDb.modTime, 2, ignoreTimeShiftMinutes); } @@ -297,14 +283,14 @@ bool stillInSync(const InSyncSymlink& dbLink, CompareVariant compareVar, int fil template <SelectedSide side> inline bool matchesDbEntry(const FolderPair& folder, const InSyncFolder::FolderList::value_type* dbFolder) { - if (folder.isEmpty<side>()) - return !dbFolder || dbFolder->second.status == InSyncFolder::DIR_STATUS_STRAW_MAN; - else if (!dbFolder || dbFolder->second.status == InSyncFolder::DIR_STATUS_STRAW_MAN) + if (!dbFolder || dbFolder->second.status == InSyncFolder::DIR_STATUS_STRAW_MAN) + return folder.isEmpty<side>(); + else if (folder.isEmpty<side>()) return false; - const Zstring& shortNameDb = dbFolder->first; + const Zstring& folderNameDb = dbFolder->first; - return folder.getItemName<side>() == shortNameDb; + return getUnicodeNormalForm(folder.getItemName<side>()) == getUnicodeNormalForm(folderNameDb); } @@ -344,7 +330,7 @@ private: { if (dbFolder) { - auto it = dbFolder->files.find(file.getPairItemName()); + auto it = dbFolder->files.find(file.getItemNameAny()); if (it != dbFolder->files.end()) return &it->second; } @@ -382,7 +368,7 @@ private: const InSyncFolder* dbSubFolder = nullptr; //try to find corresponding database entry if (dbFolder) { - auto it = dbFolder->folders.find(folder.getPairItemName()); + auto it = dbFolder->folders.find(folder.getItemNameAny()); if (it != dbFolder->folders.end()) dbSubFolder = &it->second; } @@ -393,18 +379,18 @@ private: void detectMovePairs(const InSyncFolder& container) const { - for (auto& dbFile : container.files) - findAndSetMovePair(dbFile.second); + for (const auto& [fileName, dbAttrib] : container.files) + findAndSetMovePair(dbAttrib); - for (auto& dbFolder : container.folders) - detectMovePairs(dbFolder.second); + for (const auto& [folderName, subFolder] : container.folders) + detectMovePairs(subFolder); } template <SelectedSide side> static bool sameSizeAndDate(const FilePair& file, const InSyncFile& dbFile) { return file.getFileSize<side>() == dbFile.fileSize && - sameFileTime(file.getLastWriteTime<side>(), getDescriptor<side>(dbFile).modTime, 2, {}); + sameFileTime(file.getLastWriteTime<side>(), SelectParam<side>::ref(dbFile.left, dbFile.right).modTime, 2, {}); //- respect 2 second FAT/FAT32 precision! not user-configurable! //- "ignoreTimeShiftMinutes" may lead to false positive move detections => let's be conservative and not allow it // (time shift is only ever required during FAT DST switches) @@ -427,7 +413,7 @@ private: //- note: exOneSideById isn't filled in this case, see recurse() } - const AFS::FileId fileId = getDescriptor<side>(dbFile).fileId; + const AFS::FileId fileId = SelectParam<side>::ref(dbFile.left, dbFile.right).fileId; if (!fileId.empty()) { auto it = exOneSideById.find(fileId); @@ -484,7 +470,7 @@ private: FAT caveat: File Ids are generally not stable when file is either moved or renamed! => 1. Move/rename operations on FAT cannot be detected reliably. - => 2. database generally contains wrong file ID on FAT after renaming from .ffs_tmp files => correct file Ids in database only after next sync + => 2. database generally contains wrong file ID on FAT after renaming from .ffs_tmp files => correct file IDs in database only after next sync => 3. even exFAT screws up (but less than FAT) and changes IDs after file move. Did they learn nothing from the past? */ }; @@ -535,7 +521,7 @@ private: const InSyncFolder::FileList::value_type* dbEntry = nullptr; if (dbFolder) { - auto it = dbFolder->files.find(file.getPairItemName()); + auto it = dbFolder->files.find(file.getItemNameAny()); if (it != dbFolder->files.end()) dbEntry = &*it; } @@ -571,7 +557,7 @@ private: const InSyncFolder::SymlinkList::value_type* dbEntry = nullptr; if (dbFolder) { - auto it = dbFolder->symlinks.find(symlink.getPairItemName()); + auto it = dbFolder->symlinks.find(symlink.getItemNameAny()); if (it != dbFolder->symlinks.end()) dbEntry = &*it; } @@ -612,7 +598,7 @@ private: const InSyncFolder::FolderList::value_type* dbEntry = nullptr; if (dbFolder) { - auto it = dbFolder->folders.find(folder.getPairItemName()); + auto it = dbFolder->folders.find(folder.getItemNameAny()); if (it != dbFolder->folders.end()) dbEntry = &*it; } @@ -890,19 +876,19 @@ private: void processFile(FilePair& file) const { if (Eval<strategy>::process(file)) - file.setActive(filterProc.passFileFilter(file.getPairRelativePath())); + file.setActive(filterProc.passFileFilter(file.getRelativePathAny())); } void processLink(SymlinkPair& symlink) const { if (Eval<strategy>::process(symlink)) - symlink.setActive(filterProc.passFileFilter(symlink.getPairRelativePath())); + symlink.setActive(filterProc.passFileFilter(symlink.getRelativePathAny())); } void processDir(FolderPair& folder) const { bool childItemMightMatch = true; - const bool filterPassed = filterProc.passDirFilter(folder.getPairRelativePath(), &childItemMightMatch); + const bool filterPassed = filterProc.passDirFilter(folder.getRelativePathAny(), &childItemMightMatch); if (Eval<strategy>::process(folder)) folder.setActive(filterPassed); @@ -1138,7 +1124,7 @@ std::optional<PathDependency> fff::getPathDependency(const AbstractPath& basePat const auto& relPathP = leftParent ? relPathL : relPathR; const auto& relPathC = leftParent ? relPathR : relPathL; - if (std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalFilePath(lhs, rhs); })) + if (std::equal(relPathP.begin(), relPathP.end(), relPathC.begin(), [](const Zstring& lhs, const Zstring& rhs) { return equalNoCase(lhs, rhs); })) { Zstring relDirPath; std::for_each(relPathC.begin() + relPathP.size(), relPathC.end(), [&](const Zstring& itemName) @@ -1591,9 +1577,9 @@ void fff::deleteFromGridAndHD(const std::vector<FileSystemObject*>& rowsToDelete { std::wstring msg = _("The recycle bin is not supported by the following folders. Deleted or overwritten files will not be able to be restored:") + L"\n"; - for (const auto& item : recyclerSupported) - if (!item.second) - msg += L"\n" + AFS::getDisplayPath(item.first); + for (const auto& [folderPath, supported] : recyclerSupported) + if (!supported) + msg += L"\n" + AFS::getDisplayPath(folderPath); callback.reportWarning(msg, warnRecyclerMissing); //throw? } diff --git a/FreeFileSync/Source/base/algorithm.h b/FreeFileSync/Source/base/algorithm.h index 7e073945..a8facb9d 100755 --- a/FreeFileSync/Source/base/algorithm.h +++ b/FreeFileSync/Source/base/algorithm.h @@ -81,7 +81,7 @@ struct FileDescriptor }; bool operator<(const FileDescriptor& lhs, const FileDescriptor& rhs); -//get native Win32 paths or create temporary copy for SFTP/MTP, ect. +//get native Win32 paths or create temporary copy for SFTP/MTP, etc. class TempFileBuffer { public: diff --git a/FreeFileSync/Source/base/application.cpp b/FreeFileSync/Source/base/application.cpp index a160ff06..4707fd98 100755 --- a/FreeFileSync/Source/base/application.cpp +++ b/FreeFileSync/Source/base/application.cpp @@ -18,16 +18,14 @@ #include "synchronization.h" #include "help_provider.h" #include "process_xml.h" -#include "error_log.h" +#include "fatal_error.h" #include "resolve_path.h" #include "generate_logfile.h" #include "../ui/batch_status_handler.h" #include "../ui/main_dlg.h" -//#include "../fs/concrete.h" #include <gtk/gtk.h> - using namespace zen; using namespace fff; @@ -181,27 +179,27 @@ void Application::launch(const std::vector<Zstring>& commandArgs) if (it == arg.begin()) return false; //require at least one prefix character const Zstring argTmp(it, arg.end()); - return strEqual(argTmp, Zstr("help"), CmpAsciiNoCase()) || - strEqual(argTmp, Zstr("h"), CmpAsciiNoCase()) || + return equalAsciiNoCase(argTmp, Zstr("help")) || + equalAsciiNoCase(argTmp, Zstr("h")) || argTmp == Zstr("?"); }; auto isCommandLineOption = [&](const Zstring& arg) { - return strEqual(arg, optionEdit, CmpAsciiNoCase()) || - strEqual(arg, optionLeftDir, CmpAsciiNoCase()) || - strEqual(arg, optionRightDir, CmpAsciiNoCase()) || - strEqual(arg, optionDirPair, CmpAsciiNoCase()) || - strEqual(arg, optionSendTo, CmpAsciiNoCase()) || + return equalAsciiNoCase(arg, optionEdit ) || + equalAsciiNoCase(arg, optionLeftDir ) || + equalAsciiNoCase(arg, optionRightDir) || + equalAsciiNoCase(arg, optionDirPair ) || + equalAsciiNoCase(arg, optionSendTo ) || syntaxHelpRequested(arg); }; for (auto it = commandArgs.begin(); it != commandArgs.end(); ++it) if (syntaxHelpRequested(*it)) return showSyntaxHelp(); - else if (strEqual(*it, optionEdit, CmpAsciiNoCase())) + else if (equalAsciiNoCase(*it, optionEdit)) openForEdit = true; - else if (strEqual(*it, optionLeftDir, CmpAsciiNoCase())) + else if (equalAsciiNoCase(*it, optionLeftDir)) { if (++it == commandArgs.end() || isCommandLineOption(*it)) { @@ -210,7 +208,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) } dirPathPhrasesLeft.push_back(*it); } - else if (strEqual(*it, optionRightDir, CmpAsciiNoCase())) + else if (equalAsciiNoCase(*it, optionRightDir)) { if (++it == commandArgs.end() || isCommandLineOption(*it)) { @@ -219,7 +217,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) } dirPathPhrasesRight.push_back(*it); } - else if (strEqual(*it, optionDirPair, CmpAsciiNoCase())) + else if (equalAsciiNoCase(*it, optionDirPair)) { if (++it == commandArgs.end() || isCommandLineOption(*it)) { @@ -235,7 +233,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) } dirPathPhrasePairs.back().second = *it; } - else if (strEqual(*it, optionSendTo, CmpAsciiNoCase())) + else if (equalAsciiNoCase(*it, optionSendTo)) { for (size_t i = 0; ; ++i) { @@ -245,7 +243,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) break; } - if (i < 2) //-SendTo with more than 2 paths? Doesn't make any sense, does it!? + if (i < 2) //else: -SendTo with more than 2 paths? Doesn't make any sense, does it!? { //for -SendTo we expect a list of full native paths, not "phrases" that need to be resolved! auto getFolderPath = [](Zstring itemPath) @@ -266,7 +264,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) else { const Zstring folderPath = getFolderPath(*it); - if (!equalFilePath(dirPathPhrasePairs.back().first, folderPath)) //user accidentally sending to two files, which each time yield the same parent folder + if (dirPathPhrasePairs.back().first != folderPath) //else: user accidentally sending to two files, which each time yield the same parent folder dirPathPhrasePairs.back().second = folderPath; } } @@ -439,15 +437,15 @@ void Application::launch(const std::vector<Zstring>& commandArgs) return; } - std::vector<Zstring> filepaths; - for (const auto& item : configFiles) - filepaths.push_back(item.first); + std::vector<Zstring> filePaths; + for (const auto& [filePath, xmlType] : configFiles) + filePaths.push_back(filePath); XmlGuiConfig guiCfg; //structure to receive gui settings with default values try { std::wstring warningMsg; - readAnyConfig(filepaths, guiCfg, warningMsg); //throw FileError + readAnyConfig(filePaths, guiCfg, warningMsg); //throw FileError if (!warningMsg.empty()) showNotificationDialog(nullptr, DialogInfoType::WARNING, PopupDialogCfg().setDetailInstructions(warningMsg)); @@ -458,7 +456,7 @@ void Application::launch(const std::vector<Zstring>& commandArgs) notifyFatalError(e.toString(), _("Error")); return; } - runGuiMode(globalConfigFilePath, guiCfg, filepaths, !openForEdit /*startComparison*/); + runGuiMode(globalConfigFilePath, guiCfg, filePaths, !openForEdit /*startComparison*/); } } @@ -580,7 +578,6 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat globalCfg.fileTimeTolerance, showPopupAllowed, //allowUserInteraction globalCfg.runWithBackgroundPriority, - globalCfg.folderAccessTimeout, globalCfg.createLockFile, dirLocks, extractCompareCfg(batchCfg.mainCfg), @@ -593,7 +590,6 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat globalCfg.copyFilePermissions, globalCfg.failSafeFileCopy, globalCfg.runWithBackgroundPriority, - globalCfg.folderAccessTimeout, extractSyncCfg(batchCfg.mainCfg), cmpResult, deviceParallelOps, @@ -609,7 +605,7 @@ void runBatchMode(const Zstring& globalConfigFilePath, const XmlBatchConfig& bat //update last sync stats for the selected cfg file for (ConfigFileItem& cfi : globalCfg.gui.mainDlg.cfgFileHistory) - if (equalFilePath(cfi.cfgFilePath, cfgFilePath)) + if (equalLocalPath(cfi.cfgFilePath, cfgFilePath)) { if (r.finalStatus != SyncResult::ABORTED) cfi.lastSyncTime = std::chrono::system_clock::to_time_t(syncStartTime); diff --git a/FreeFileSync/Source/base/comparison.cpp b/FreeFileSync/Source/base/comparison.cpp index 84b392cf..ce601ac8 100755 --- a/FreeFileSync/Source/base/comparison.cpp +++ b/FreeFileSync/Source/base/comparison.cpp @@ -69,7 +69,6 @@ struct ResolvedBaseFolders ResolvedBaseFolders initializeBaseFolders(const std::vector<FolderPairCfg>& fpCfgList, const std::map<AbstractPath, size_t>& deviceParallelOps, - std::chrono::seconds folderAccessTimeout, bool allowUserInteraction, bool& warnFolderNotExisting, ProcessCallback& callback /*throw X*/) @@ -95,19 +94,19 @@ ResolvedBaseFolders initializeBaseFolders(const std::vector<FolderPairCfg>& fpCf } const FolderStatus status = getFolderStatusNonBlocking(uniqueBaseFolders, deviceParallelOps, //re-check *all* directories on each try! - folderAccessTimeout, allowUserInteraction, callback); //throw X + allowUserInteraction, callback); //throw X output.existingBaseFolders = status.existing; notExisting = status.notExisting; if (!status.failedChecks.empty()) { std::wstring msg = _("Cannot find the following folders:") + L"\n"; - for (const auto& fc : status.failedChecks) - msg += L"\n" + AFS::getDisplayPath(fc.first); + for (const auto& [folderPath, error] : status.failedChecks) + msg += L"\n" + AFS::getDisplayPath(folderPath); msg += L"\n___________________________________________"; - for (const auto& fc : status.failedChecks) - msg += L"\n\n" + replaceCpy(fc.second.toString(), L"\n\n", L"\n"); + for (const auto& [folderPath, error] : status.failedChecks) + msg += L"\n\n" + replaceCpy(error.toString(), L"\n\n", L"\n"); throw FileError(msg); } @@ -262,7 +261,8 @@ void categorizeSymlinkByTime(SymlinkPair& symlink) //1. SYMLINK_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp //2. harmonize with "bool stillInSync()" in algorithm.cpp - if (symlink.getItemName<LEFT_SIDE>() == symlink.getItemName<RIGHT_SIDE>()) + if (getUnicodeNormalForm(symlink.getItemName<LEFT_SIDE >()) == + getUnicodeNormalForm(symlink.getItemName<RIGHT_SIDE>())) symlink.setCategory<FILE_EQUAL>(); else symlink.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(symlink)); @@ -311,7 +311,8 @@ std::shared_ptr<BaseFolderPair> ComparisonBuffer::compareByTimeSize(const Resolv //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h if (file->getFileSize<LEFT_SIDE>() == file->getFileSize<RIGHT_SIDE>()) { - if (file->getItemName<LEFT_SIDE>() == file->getItemName<RIGHT_SIDE>()) + if (getUnicodeNormalForm(file->getItemName<LEFT_SIDE >()) == + getUnicodeNormalForm(file->getItemName<RIGHT_SIDE>())) file->setCategory<FILE_EQUAL>(); else file->setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(*file)); @@ -368,7 +369,8 @@ void categorizeSymlinkByContent(SymlinkPair& symlink, ProcessCallback& callback) //2. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h //symlinks have same "content" - if (symlink.getItemName<LEFT_SIDE>() != symlink.getItemName<RIGHT_SIDE>()) + if (getUnicodeNormalForm(symlink.getItemName<LEFT_SIDE >()) != + getUnicodeNormalForm(symlink.getItemName<RIGHT_SIDE>())) symlink.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(symlink)); //else if (!sameFileTime(symlink.getLastWriteTime<LEFT_SIDE>(), // symlink.getLastWriteTime<RIGHT_SIDE>(), symlink.base().getFileTimeTolerance(), symlink.base().getIgnoredTimeShift())) @@ -404,7 +406,8 @@ std::shared_ptr<BaseFolderPair> ComparisonBuffer::compareBySize(const ResolvedFo //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h if (file->getFileSize<LEFT_SIDE>() == file->getFileSize<RIGHT_SIDE>()) { - if (file->getItemName<LEFT_SIDE>() == file->getItemName<RIGHT_SIDE>()) + if (getUnicodeNormalForm(file->getItemName< LEFT_SIDE>()) == + getUnicodeNormalForm(file->getItemName<RIGHT_SIDE>())) file->setCategory<FILE_EQUAL>(); else file->setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(*file)); @@ -433,7 +436,7 @@ namespace { void categorizeFileByContent(FilePair& file, const std::wstring& txtComparingContentOfFiles, AsyncCallback& acb, std::mutex& singleThread) //throw ThreadInterruption { - acb.reportStatus(replaceCpy(txtComparingContentOfFiles, L"%x", fmtPath(file.getPairRelativePath()))); //throw ThreadInterruption + acb.reportStatus(replaceCpy(txtComparingContentOfFiles, L"%x", fmtPath(file.getRelativePathAny()))); //throw ThreadInterruption bool haveSameContent = false; const std::wstring errMsg = tryReportingError([&] @@ -462,7 +465,8 @@ void categorizeFileByContent(FilePair& file, const std::wstring& txtComparingCon //1. FILE_EQUAL may only be set if short names match in case: InSyncFolder's mapping tables use short name as a key! see db_file.cpp //2. FILE_EQUAL is expected to mean identical file sizes! See InSyncFile //3. harmonize with "bool stillInSync()" in algorithm.cpp, FilePair::setSyncedTo() in file_hierarchy.h - if (file.getItemName<LEFT_SIDE>() != file.getItemName<RIGHT_SIDE>()) + if (getUnicodeNormalForm(file.getItemName< LEFT_SIDE>()) != + getUnicodeNormalForm(file.getItemName<RIGHT_SIDE>())) file.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(file)); #if 0 //don't synchronize modtime only see FolderPairSyncer::synchronizeFileInt(), SO_COPY_METADATA_TO_* else if (!sameFileTime(file.getLastWriteTime<LEFT_SIDE>(), @@ -520,12 +524,12 @@ std::list<std::shared_ptr<BaseFolderPair>> ComparisonBuffer::compareByContent(co const Zstringw txtConflictSkippedBinaryComparison = getConflictSkippedBinaryComparison(); //avoid premature pess.: save memory via ref-counted string - for (const auto& w : workLoad) + for (const auto& [folderPair, fpCfg] : workLoad) { std::vector<FilePair*> undefinedFiles; std::vector<SymlinkPair*> uncategorizedLinks; //run basis scan and retrieve candidates for binary comparison (files existing on both sides) - output.push_back(performComparison(w.first, w.second, undefinedFiles, uncategorizedLinks)); + output.push_back(performComparison(folderPair, fpCfg, undefinedFiles, uncategorizedLinks)); RingBuffer<FilePair*> filesToCompareBytewise; //content comparison of file content happens AFTER finding corresponding files and AFTER filtering @@ -638,19 +642,19 @@ std::list<std::shared_ptr<BaseFolderPair>> ComparisonBuffer::compareByContent(co class MergeSides { public: - MergeSides(const std::map<Zstring, Zstringw, LessFilePath>& failedItemReads, + MergeSides(const std::map<ZstringNoCase, Zstringw>& errorsByRelPath, std::vector<FilePair*>& undefinedFilesOut, std::vector<SymlinkPair*>& undefinedSymlinksOut) : - failedItemReads_(failedItemReads), + errorsByRelPath_(errorsByRelPath), undefinedFiles_(undefinedFilesOut), undefinedSymlinks_(undefinedSymlinksOut) {} void execute(const FolderContainer& lhs, const FolderContainer& rhs, ContainerObject& output) { - auto it = failedItemReads_.find(Zstring()); //empty path if read-error for whole base directory + auto it = errorsByRelPath_.find(Zstring()); //empty path if read-error for whole base directory mergeTwoSides(lhs, rhs, - it != failedItemReads_.end() ? &it->second : nullptr, + it != errorsByRelPath_.end() ? &it->second : nullptr, output); } @@ -662,8 +666,8 @@ private: const Zstringw* checkFailedRead(FileSystemObject& fsObj, const Zstringw* errorMsg); - const std::map<Zstring, Zstringw, LessFilePath>& failedItemReads_; //base-relative paths or empty if read-error for whole base directory - std::vector<FilePair*>& undefinedFiles_; + const std::map<ZstringNoCase, Zstringw>& errorsByRelPath_; //base-relative paths or empty if read-error for whole base directory + std::vector<FilePair*>& undefinedFiles_; std::vector<SymlinkPair*>& undefinedSymlinks_; }; @@ -673,8 +677,8 @@ const Zstringw* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstri { if (!errorMsg) { - auto it = failedItemReads_.find(fsObj.getPairRelativePath()); - if (it != failedItemReads_.end()) + auto it = errorsByRelPath_.find(fsObj.getRelativePathAny()); + if (it != errorsByRelPath_.end()) errorMsg = &it->second; } @@ -691,65 +695,106 @@ const Zstringw* MergeSides::checkFailedRead(FileSystemObject& fsObj, const Zstri template <SelectedSide side> void MergeSides::fillOneSide(const FolderContainer& folderCont, const Zstringw* errorMsg, ContainerObject& output) { - for (const auto& file : folderCont.files) + for (const auto& [fileName, attrib] : folderCont.files) { - FilePair& newItem = output.addSubFile<side>(file.first, file.second); + FilePair& newItem = output.addSubFile<side>(fileName, attrib); checkFailedRead(newItem, errorMsg); } - for (const auto& symlink : folderCont.symlinks) + for (const auto& [linkName, attrib] : folderCont.symlinks) { - SymlinkPair& newItem = output.addSubLink<side>(symlink.first, symlink.second); + SymlinkPair& newItem = output.addSubLink<side>(linkName, attrib); checkFailedRead(newItem, errorMsg); } - for (const auto& dir : folderCont.folders) + for (const auto& [folderName, attrAndSub] : folderCont.folders) { - FolderPair& newFolder = output.addSubFolder<side>(dir.first, dir.second.first); + FolderPair& newFolder = output.addSubFolder<side>(folderName, attrAndSub.first); const Zstringw* errorMsgNew = checkFailedRead(newFolder, errorMsg); - fillOneSide<side>(dir.second.second, errorMsgNew, newFolder); //recurse + fillOneSide<side>(attrAndSub.second, errorMsgNew, newFolder); //recurse } } -//perf: 70% faster than traversing over left and right containers + more natural default sequence -//- 2 x lessKey vs 1 x cmpFilePath() => no significant difference -//- simplify loop by placing the eob check at the beginning => slightly slower template <class MapType, class ProcessLeftOnly, class ProcessRightOnly, class ProcessBoth> inline -void linearMerge(const MapType& mapLeft, const MapType& mapRight, ProcessLeftOnly lo, ProcessRightOnly ro, ProcessBoth bo) +void matchFolders(const MapType& mapLeft, const MapType& mapRight, ProcessLeftOnly lo, ProcessRightOnly ro, ProcessBoth bo) { - auto itL = mapLeft .begin(); - auto itR = mapRight.begin(); + struct FileRef + { + Zstring normalName; //buffer expensive makeUpperCopy() calls!! + const typename MapType::value_type* ref; + bool leftSide; + }; + std::vector<FileRef> fileList; + fileList.reserve(mapLeft.size() + mapRight.size()); //perf: ~5% shorter runtime - auto finishLeft = [&] { std::for_each(itL, mapLeft .end(), lo); }; - auto finishRight = [&] { std::for_each(itR, mapRight.end(), ro); }; + for (const auto& item : mapLeft ) fileList.push_back({ makeUpperCopy(item.first), &item, true }); + for (const auto& item : mapRight) fileList.push_back({ makeUpperCopy(item.first), &item, false }); - if (itL == mapLeft .end()) return finishRight(); - if (itR == mapRight.end()) return finishLeft (); + std::sort(fileList.begin(), fileList.end(), [&](const FileRef& lhs, const FileRef& rhs) + { + int rv = compareString(lhs.normalName, rhs.normalName); + if (rv != 0) + return rv < 0; //primary sort key: ignore unicode normal form and case - const auto lessKey = typename MapType::key_compare(); + //perf: sorting by secondary/tertiary key here costs about 7% additional runtime + rv = compareString(getUnicodeNormalForm(lhs.ref->first), getUnicodeNormalForm(rhs.ref->first)); + if (rv != 0) + return rv < 0; //secondary sort key: ignore unicode normal - for (;;) - if (lessKey(itL->first, itR->first)) - { - lo(*itL); - if (++itL == mapLeft.end()) - return finishRight(); - } - else if (lessKey(itR->first, itL->first)) - { - ro(*itR); - if (++itR == mapRight.end()) - return finishLeft(); - } - else + return lhs.ref->first < rhs.ref->first; //tertiary sort key: raw string value + }); + //bonus: natural default sequence on file guid UI + + auto tryMatchRange = [&](auto it, auto itLast) + { + const size_t equalCountL = std::count_if(it, itLast, [](const FileRef& fr) { return fr.leftSide; }); + const size_t equalCountR = itLast - it - equalCountL; + + if (equalCountL == 1 && equalCountR == 1) //we have a match { - bo(*itL, *itR); - ++itL; // - ++itR; //increment BOTH before checking for end of range! - if (itL == mapLeft .end()) return finishRight(); - if (itR == mapRight.end()) return finishLeft (); + if (it->leftSide) + bo(*it[0].ref, *it[1].ref); + else + bo(*it[1].ref, *it[0].ref); } + else if (equalCountL == 0) //no match + std::for_each(it, itLast, [&](const FileRef& fr) { ro(*fr.ref); }); + else if (equalCountR == 0) //no match + std::for_each(it, itLast, [&](const FileRef& fr) { lo(*fr.ref); }); + else //ambiguous + return false; + return true; + }; + + for (auto it = fileList.begin(); it != fileList.end();) + { + //find equal range: ignore case, ignore Unicode normalization + auto itEndEq = std::find_if(it + 1, fileList.end(), [&](const FileRef& fr) { return fr.normalName != it->normalName; }); + if (!tryMatchRange(it, itEndEq)) + for (auto itCase = it; itCase != itEndEq;) + { + //find equal range: respect case, ignore Unicode normalization + auto itEndCase = std::find_if(itCase + 1, itEndEq, [&](const FileRef& fr) { return getUnicodeNormalForm(fr.ref->first) != getUnicodeNormalForm(itCase->ref->first); }); + if (!tryMatchRange(itCase, itEndCase)) + for (auto itRaw = itCase; itRaw != itEndCase;) + { + //find equal range: respect case, respect Unicode normalization + auto itEndRaw = std::find_if(itRaw + 1, itEndCase, [&](const FileRef& fr) { return fr.ref->first != itRaw->ref->first; }); + if (!tryMatchRange(itRaw, itEndRaw)) + std::for_each(itRaw, itEndRaw, [&](const FileRef& fr) + { + if (fr.leftSide) + lo(*fr.ref); + else + ro(*fr.ref); + }); + itRaw = itEndRaw; + } + itCase = itEndCase; + } + it = itEndEq; + } } @@ -757,7 +802,7 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer { using FileData = FolderContainer::FileList::value_type; - linearMerge(lhs.files, rhs.files, + matchFolders(lhs.files, rhs.files, [&](const FileData& fileLeft ) { FilePair& newItem = output.addSubFile< LEFT_SIDE>(fileLeft .first, fileLeft .second); checkFailedRead(newItem, errorMsg); }, //left only [&](const FileData& fileRight) { FilePair& newItem = output.addSubFile<RIGHT_SIDE>(fileRight.first, fileRight.second); checkFailedRead(newItem, errorMsg); }, //right only @@ -776,7 +821,7 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer //----------------------------------------------------------------------------------------------- using SymlinkData = FolderContainer::SymlinkList::value_type; - linearMerge(lhs.symlinks, rhs.symlinks, + matchFolders(lhs.symlinks, rhs.symlinks, [&](const SymlinkData& symlinkLeft ) { SymlinkPair& newItem = output.addSubLink< LEFT_SIDE>(symlinkLeft .first, symlinkLeft .second); checkFailedRead(newItem, errorMsg); }, //left only [&](const SymlinkData& symlinkRight) { SymlinkPair& newItem = output.addSubLink<RIGHT_SIDE>(symlinkRight.first, symlinkRight.second); checkFailedRead(newItem, errorMsg); }, //right only @@ -794,8 +839,8 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer //----------------------------------------------------------------------------------------------- using FolderData = FolderContainer::FolderList::value_type; - linearMerge(lhs.folders, rhs.folders, - [&](const FolderData& dirLeft) //left only + matchFolders(lhs.folders, rhs.folders, + [&](const FolderData& dirLeft) //left only { FolderPair& newFolder = output.addSubFolder<LEFT_SIDE>(dirLeft.first, dirLeft.second.first); const Zstringw* errorMsgNew = checkFailedRead(newFolder, errorMsg); @@ -814,7 +859,8 @@ void MergeSides::mergeTwoSides(const FolderContainer& lhs, const FolderContainer const Zstringw* errorMsgNew = checkFailedRead(newFolder, errorMsg); if (!errorMsgNew) - if (dirLeft.first != dirRight.first) + if (getUnicodeNormalForm(dirLeft.first) != + getUnicodeNormalForm(dirRight.first)) newFolder.setCategoryDiffMetadata(getDescrDiffMetaShortnameCase(newFolder)); mergeTwoSides(dirLeft.second.second, dirRight.second.second, errorMsgNew, newFolder); //recurse @@ -836,7 +882,7 @@ void stripExcludedDirectories(ContainerObject& hierObj, const HardFilter& filter hierObj.refSubFolders().remove_if([&](FolderPair& folder) { - const bool included = filterProc.passDirFilter(folder.getPairRelativePath(), nullptr); //childItemMightMatch is false, child items were already excluded during scanning + const bool included = filterProc.passDirFilter(folder.getRelativePathAny(), nullptr); //childItemMightMatch is false, child items were already excluded during scanning if (!included) //falsify only! (e.g. might already be inactive due to read error!) folder.setActive(false); @@ -867,15 +913,16 @@ std::shared_ptr<BaseFolderPair> ComparisonBuffer::performComparison(const Resolv const DirectoryValue* bufValueLeft = getDirValue(fp.folderPathLeft); const DirectoryValue* bufValueRight = getDirValue(fp.folderPathRight); - std::map<Zstring, Zstringw, LessFilePath> failedReads; //base-relative paths or empty if read-error for whole base directory + std::map<ZstringNoCase, Zstringw> failedReads; //base-relative paths or empty if read-error for whole base directory { - auto append = [&](const std::map<Zstring, std::wstring, LessFilePath>& c) + auto append = [&](const std::map<Zstring, std::wstring>& c) { - for (const auto& item : c) - failedReads.emplace(item.first, copyStringTo<Zstringw>(item.second)); + for (const auto& [relPath, errorMsg] : c) + failedReads.emplace(relPath, copyStringTo<Zstringw>(errorMsg)); }; + //mix failedFolderReads with failedItemReads: - //mark directory errors already at directory-level (instead for child items only) to show on GUI! See "MergeSides" + //associate folder traversing errors with folder (instead of child items only) to show on GUI! See "MergeSides" //=> minor pessimization for "excludefilterFailedRead" which needlessly excludes parent folders, too if (bufValueLeft ) append(bufValueLeft ->failedFolderReads); if (bufValueRight) append(bufValueRight->failedFolderReads); @@ -888,8 +935,8 @@ std::shared_ptr<BaseFolderPair> ComparisonBuffer::performComparison(const Resolv if (failedReads.find(Zstring()) != failedReads.end()) //empty path if read-error for whole base directory excludefilterFailedRead += Zstr("*\n"); else - for (const auto& item : failedReads) - excludefilterFailedRead += item.first + Zstr("\n"); //exclude item AND (potential) child items! + for (const auto& [relPath, errorMsg] : failedReads) + excludefilterFailedRead += relPath.upperCase + Zstr("\n"); //exclude item AND (potential) child items! std::shared_ptr<BaseFolderPair> output = std::make_shared<BaseFolderPair>(fp.folderPathLeft, bufValueLeft != nullptr, //dir existence must be checked only once: available iff buffer entry exists! @@ -939,9 +986,6 @@ void fff::logNonDefaultSettings(const XmlGlobalSettings& activeSettings, Process if (activeSettings.fileTimeTolerance != defaultSettings.fileTimeTolerance) changedSettingsMsg += L"\n " + _("File time tolerance") + L" - " + numberTo<std::wstring>(activeSettings.fileTimeTolerance); - if (activeSettings.folderAccessTimeout != defaultSettings.folderAccessTimeout) - changedSettingsMsg += L"\n " + _("Folder access timeout") + L" - " + numberTo<std::wstring>(activeSettings.folderAccessTimeout.count()); - if (activeSettings.runWithBackgroundPriority != defaultSettings.runWithBackgroundPriority) changedSettingsMsg += L"\n " + _("Run with background priority") + L" - " + (activeSettings.runWithBackgroundPriority ? _("Enabled") : _("Disabled")); @@ -960,7 +1004,6 @@ FolderComparison fff::compare(WarningDialogs& warnings, int fileTimeTolerance, bool allowUserInteraction, bool runWithBackgroundPriority, - std::chrono::seconds folderAccessTimeout, bool createDirLocks, std::unique_ptr<LockHolder>& dirLocks, const std::vector<FolderPairCfg>& fpCfgList, @@ -999,7 +1042,7 @@ FolderComparison fff::compare(WarningDialogs& warnings, callback.reportInfo(e.toString()); //throw X } - const ResolvedBaseFolders& resInfo = initializeBaseFolders(fpCfgList, deviceParallelOps, folderAccessTimeout, allowUserInteraction, warnings.warnFolderNotExisting, callback); //throw X + const ResolvedBaseFolders& resInfo = initializeBaseFolders(fpCfgList, deviceParallelOps, allowUserInteraction, warnings.warnFolderNotExisting, callback); //throw X //directory existence only checked *once* to avoid race conditions! if (resInfo.resolvedPairs.size() != fpCfgList.size()) throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); @@ -1034,13 +1077,13 @@ FolderComparison fff::compare(WarningDialogs& warnings, { std::wstring msg; - for (const auto& w : workLoad) - if (std::optional<PathDependency> pd = getPathDependency(w.first.folderPathLeft, *w.second.filter.nameFilter, - w.first.folderPathRight, *w.second.filter.nameFilter)) + for (const auto& [folderPair, fpCfg] : workLoad) + if (std::optional<PathDependency> pd = getPathDependency(folderPair.folderPathLeft, *fpCfg.filter.nameFilter, + folderPair.folderPathRight, *fpCfg.filter.nameFilter)) { msg += L"\n\n" + - AFS::getDisplayPath(w.first.folderPathLeft) + L"\n" + - AFS::getDisplayPath(w.first.folderPathRight); + AFS::getDisplayPath(folderPair.folderPathLeft) + L"\n" + + AFS::getDisplayPath(folderPair.folderPathRight); if (!pd->relPath.empty()) msg += L"\n" + _("Exclude:") + L" " + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); } @@ -1068,12 +1111,12 @@ FolderComparison fff::compare(WarningDialogs& warnings, //------------------- fill directory buffer --------------------------------------------------- std::set<DirectoryKey> foldersToRead; - for (const auto& w : workLoad) + for (const auto& [folderPair, fpCfg] : workLoad) { - if (basefolderExisting(w.first.folderPathLeft)) //only traverse *currently existing* folders: at this point user is aware that non-ex + empty string are seen as empty folder! - foldersToRead.emplace(DirectoryKey({ w.first.folderPathLeft, w.second.filter.nameFilter, w.second.handleSymlinks })); - if (basefolderExisting(w.first.folderPathRight)) - foldersToRead.emplace(DirectoryKey({ w.first.folderPathRight, w.second.filter.nameFilter, w.second.handleSymlinks })); + if (basefolderExisting(folderPair.folderPathLeft)) //only traverse *currently existing* folders: at this point user is aware that non-ex + empty string are seen as empty folder! + foldersToRead.emplace(DirectoryKey({ folderPair.folderPathLeft, fpCfg.filter.nameFilter, fpCfg.handleSymlinks })); + if (basefolderExisting(folderPair.folderPathRight)) + foldersToRead.emplace(DirectoryKey({ folderPair.folderPathRight, fpCfg.filter.nameFilter, fpCfg.handleSymlinks })); } FolderComparison output; @@ -1087,21 +1130,21 @@ FolderComparison fff::compare(WarningDialogs& warnings, //process binary comparison as one junk std::vector<std::pair<ResolvedFolderPair, FolderPairCfg>> workLoadByContent; - for (const auto& w : workLoad) - if (w.second.compareVar == CompareVariant::CONTENT) - workLoadByContent.push_back(w); + for (const auto& [folderPair, fpCfg] : workLoad) + if (fpCfg.compareVar == CompareVariant::CONTENT) + workLoadByContent.push_back({ folderPair, fpCfg }); std::list<std::shared_ptr<BaseFolderPair>> outputByContent = cmpBuff.compareByContent(workLoadByContent); //write output in expected order - for (const auto& w : workLoad) - switch (w.second.compareVar) + for (const auto& [folderPair, fpCfg] : workLoad) + switch (fpCfg.compareVar) { case CompareVariant::TIME_SIZE: - output.push_back(cmpBuff.compareByTimeSize(w.first, w.second)); + output.push_back(cmpBuff.compareByTimeSize(folderPair, fpCfg)); break; case CompareVariant::SIZE: - output.push_back(cmpBuff.compareBySize(w.first, w.second)); + output.push_back(cmpBuff.compareBySize(folderPair, fpCfg)); break; case CompareVariant::CONTENT: assert(!outputByContent.empty()); diff --git a/FreeFileSync/Source/base/comparison.h b/FreeFileSync/Source/base/comparison.h index 4c93a3ba..b956019b 100755 --- a/FreeFileSync/Source/base/comparison.h +++ b/FreeFileSync/Source/base/comparison.h @@ -55,7 +55,6 @@ FolderComparison compare(WarningDialogs& warnings, int fileTimeTolerance, bool allowUserInteraction, bool runWithBackgroundPriority, - std::chrono::seconds folderAccessTimeout, bool createDirLocks, std::unique_ptr<LockHolder>& dirLocks, //out const std::vector<FolderPairCfg>& fpCfgList, diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index 5945918c..2dc88402 100755 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -182,7 +182,7 @@ public: { try { - /* Zlib: optimal level - testcase 1 million files + /* Zlib: optimal level - test case 1 million files level|size [MB]|time [ms] 0 49.54 272 (uncompressed) 1 14.53 1013 @@ -552,19 +552,16 @@ private: void recurse(const ContainerObject& hierObj, InSyncFolder& dbFolder) { - process(hierObj.refSubFiles (), hierObj.getPairRelativePath(), dbFolder.files); - process(hierObj.refSubLinks (), hierObj.getPairRelativePath(), dbFolder.symlinks); - process(hierObj.refSubFolders(), hierObj.getPairRelativePath(), dbFolder.folders); + process(hierObj.refSubFiles (), hierObj.getRelativePathAny(), dbFolder.files); + process(hierObj.refSubLinks (), hierObj.getRelativePathAny(), dbFolder.symlinks); + process(hierObj.refSubFolders(), hierObj.getRelativePathAny(), dbFolder.folders); } template <class M, class V> static V& mapAddOrUpdate(M& map, const Zstring& key, V&& value) { -#if defined ZEN_LINUX && !defined __cpp_lib_map_try_emplace - auto rv = map.emplace(key, value); //C++11 emplace will move r-value arguments => don't use std::forward! -#else //C++17's map::try_emplace() is faster than map::emplace() if key is already existing - auto rv = map.try_emplace(key, std::forward<V>(value)); //and does NOT MOVE r-value arguments in this case! -#endif + //C++17's map::try_emplace() is faster than map::emplace() if key is already existing + auto rv = map.try_emplace(key, std::forward<V>(value)); //and does NOT MOVE r-value arguments unlike map::emplace()! if (rv.second) return rv.first->second; @@ -582,12 +579,12 @@ private: { //Caveat: If FILE_EQUAL, we *implicitly* assume equal left and right short names matching case: InSyncFolder's mapping tables use short name as a key! //This makes us silently dependent from code in algorithm.h!!! - assert(file.getItemName<LEFT_SIDE>() == file.getItemName<RIGHT_SIDE>()); + assert(getUnicodeNormalForm(file.getItemName<LEFT_SIDE>()) == getUnicodeNormalForm(file.getItemName<RIGHT_SIDE>())); //this should be taken for granted: assert(file.getFileSize<LEFT_SIDE>() == file.getFileSize<RIGHT_SIDE>()); //create or update new "in-sync" state - InSyncFile& dbFile = mapAddOrUpdate(dbFiles, file.getPairItemName(), + InSyncFile& dbFile = mapAddOrUpdate(dbFiles, file.getItemNameAny(), InSyncFile(InSyncDescrFile(file.getLastWriteTime< LEFT_SIDE>(), file.getFileId < LEFT_SIDE>()), InSyncDescrFile(file.getLastWriteTime<RIGHT_SIDE>(), @@ -598,7 +595,7 @@ private: } else //not in sync: preserve last synchronous state { - auto it = dbFiles.find(file.getPairItemName()); + auto it = dbFiles.find(file.getItemNameAny()); if (it != dbFiles.end()) toPreserve.insert(&it->second); } @@ -625,10 +622,10 @@ private: { if (symlink.getLinkCategory() == SYMLINK_EQUAL) //data in sync: write current state { - assert(symlink.getItemName<LEFT_SIDE>() == symlink.getItemName<RIGHT_SIDE>()); + assert(getUnicodeNormalForm(symlink.getItemName<LEFT_SIDE>()) == getUnicodeNormalForm(symlink.getItemName<RIGHT_SIDE>())); //create or update new "in-sync" state - InSyncSymlink& dbSymlink = mapAddOrUpdate(dbSymlinks, symlink.getPairItemName(), + InSyncSymlink& dbSymlink = mapAddOrUpdate(dbSymlinks, symlink.getItemNameAny(), InSyncSymlink(InSyncDescrLink(symlink.getLastWriteTime<LEFT_SIDE>()), InSyncDescrLink(symlink.getLastWriteTime<RIGHT_SIDE>()), activeCmpVar_)); @@ -636,7 +633,7 @@ private: } else //not in sync: preserve last synchronous state { - auto it = dbSymlinks.find(symlink.getPairItemName()); + auto it = dbSymlinks.find(symlink.getItemNameAny()); if (it != dbSymlinks.end()) toPreserve.insert(&it->second); } @@ -663,10 +660,10 @@ private: { case DIR_EQUAL: { - assert(folder.getItemName<LEFT_SIDE>() == folder.getItemName<RIGHT_SIDE>()); + assert(getUnicodeNormalForm(folder.getItemName<LEFT_SIDE>()) == getUnicodeNormalForm(folder.getItemName<RIGHT_SIDE>())); //update directory entry only (shallow), but do *not touch* exising child elements!!! - const Zstring& key = folder.getPairItemName(); + const Zstring& key = folder.getItemNameAny(); auto insertResult = dbFolders.emplace(key, InSyncFolder(InSyncFolder::DIR_STATUS_IN_SYNC)); //get or create auto it = insertResult.first; @@ -684,7 +681,7 @@ private: //Example: directories on left and right differ in case while sub-files are equal { //reuse last "in-sync" if available or insert strawman entry (do not try to update and thereby remove child elements!!!) - InSyncFolder& dbFolder = dbFolders.emplace(folder.getPairItemName(), InSyncFolder(InSyncFolder::DIR_STATUS_STRAW_MAN)).first->second; + InSyncFolder& dbFolder = dbFolders.emplace(folder.getItemNameAny(), InSyncFolder(InSyncFolder::DIR_STATUS_STRAW_MAN)).first->second; toPreserve.insert(&dbFolder); recurse(folder, dbFolder); //unconditional recursion without filter check! => no problem since "childItemMightMatch" is optional!!! } @@ -694,7 +691,7 @@ private: case DIR_LEFT_SIDE_ONLY: case DIR_RIGHT_SIDE_ONLY: { - auto it = dbFolders.find(folder.getPairItemName()); + auto it = dbFolders.find(folder.getItemNameAny()); if (it != dbFolders.end()) { toPreserve.insert(&it->second); @@ -926,11 +923,11 @@ void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, const std:: //operation finished: rename temp files -> this should work (almost) transactionally: //if there were no write access, creation of temp files would have failed - AFS::removeFileIfExists(dbPathLeft); //throw FileError - AFS::renameItem(dbPathLeftTmp, dbPathLeft); //throw FileError, (ErrorDifferentVolume) + AFS::removeFileIfExists(dbPathLeft); //throw FileError + AFS::moveAndRenameItem(dbPathLeftTmp, dbPathLeft); //throw FileError, (ErrorDifferentVolume) guardTmpL.dismiss(); - AFS::removeFileIfExists(dbPathRight); // - AFS::renameItem(dbPathRightTmp, dbPathRight); // + AFS::removeFileIfExists(dbPathRight); // + AFS::moveAndRenameItem(dbPathRightTmp, dbPathRight); // guardTmpR.dismiss(); } diff --git a/FreeFileSync/Source/base/db_file.h b/FreeFileSync/Source/base/db_file.h index 17409c2b..ebbb0c01 100755 --- a/FreeFileSync/Source/base/db_file.h +++ b/FreeFileSync/Source/base/db_file.h @@ -75,19 +75,19 @@ struct InSyncFolder SymlinkList symlinks; //non-followed symlinks //convenience - InSyncFolder& addFolder(const Zstring& shortName, InSyncStatus st) + InSyncFolder& addFolder(const Zstring& folderName, InSyncStatus st) { - return folders.emplace(shortName, InSyncFolder(st)).first->second; + return folders.emplace(folderName, InSyncFolder(st)).first->second; } - void addFile(const Zstring& shortName, const InSyncDescrFile& dataL, const InSyncDescrFile& dataR, CompareVariant cmpVar, uint64_t fileSize) + void addFile(const Zstring& fileName, const InSyncDescrFile& dataL, const InSyncDescrFile& dataR, CompareVariant cmpVar, uint64_t fileSize) { - files.emplace(shortName, InSyncFile(dataL, dataR, cmpVar, fileSize)); + files.emplace(fileName, InSyncFile(dataL, dataR, cmpVar, fileSize)); } - void addSymlink(const Zstring& shortName, const InSyncDescrLink& dataL, const InSyncDescrLink& dataR, CompareVariant cmpVar) + void addSymlink(const Zstring& linkName, const InSyncDescrLink& dataL, const InSyncDescrLink& dataR, CompareVariant cmpVar) { - symlinks.emplace(shortName, InSyncSymlink(dataL, dataR, cmpVar)); + symlinks.emplace(linkName, InSyncSymlink(dataL, dataR, cmpVar)); } }; diff --git a/FreeFileSync/Source/base/dir_exist_async.h b/FreeFileSync/Source/base/dir_exist_async.h index acd79a69..c445a665 100755 --- a/FreeFileSync/Source/base/dir_exist_async.h +++ b/FreeFileSync/Source/base/dir_exist_async.h @@ -16,6 +16,8 @@ namespace fff { +const int DEFAULT_FOLDER_ACCESS_TIME_OUT_SEC = 20; //consider CD-ROM insert or hard disk spin up time from sleep + namespace { //directory existence checking may hang for non-existent network drives => run asynchronously and update UI! @@ -30,8 +32,7 @@ struct FolderStatus }; FolderStatus getFolderStatusNonBlocking(const std::set<AbstractPath>& folderPaths, const std::map<AbstractPath, size_t>& deviceParallelOps, - std::chrono::seconds folderAccessTimeout, bool allowUserInteraction, - ProcessCallback& procCallback /*throw X*/) + bool allowUserInteraction, ProcessCallback& procCallback /*throw X*/) { using namespace zen; @@ -45,20 +46,19 @@ FolderStatus getFolderStatusNonBlocking(const std::set<AbstractPath>& folderPath std::vector<std::pair<AbstractPath, std::future<bool>>> futureInfo; std::vector<ThreadGroup<std::packaged_task<bool()>>> perDeviceThreads; - for (const auto& item : perDevicePaths) + for (const auto& [rootPath, deviceFolderPaths] : perDevicePaths) { - const AbstractPath& rootPath = item.first; const size_t parallelOps = getDeviceParallelOps(deviceParallelOps, rootPath); perDeviceThreads.emplace_back(parallelOps, "DirExist: " + utfTo<std::string>(AFS::getDisplayPath(rootPath))); auto& threadGroup = perDeviceThreads.back(); threadGroup.detach(); //don't wait on threads hanging longer than "folderAccessTimeout" - for (const AbstractPath& folderPath : item.second) + for (const AbstractPath& folderPath : deviceFolderPaths) { std::packaged_task<bool()> pt([folderPath, allowUserInteraction] //AbstractPath is thread-safe like an int! :) { - //1. login to network share, open FTP connection, ect. + //1. login to network share, open FTP connection, etc. AFS::connectNetworkFolder(folderPath, allowUserInteraction); //throw FileError //2. check dir existence @@ -77,30 +77,31 @@ FolderStatus getFolderStatusNonBlocking(const std::set<AbstractPath>& folderPath FolderStatus output; - for (auto& fi : futureInfo) + for (auto& [folderPath, future] : futureInfo) { - const std::wstring& displayPathFmt = fmtPath(AFS::getDisplayPath(fi.first)); + const std::wstring& displayPathFmt = fmtPath(AFS::getDisplayPath(folderPath)); procCallback.reportStatus(replaceCpy(_("Searching for folder %x..."), L"%x", displayPathFmt)); //throw X - while (std::chrono::steady_clock::now() < startTime + folderAccessTimeout && - fi.second.wait_for(UI_UPDATE_INTERVAL / 2) != std::future_status::ready) + const int deviceTimeOut = AFS::geAccessTimeout(folderPath); //0 if no timeout in force + const auto timeoutTime = startTime + std::chrono::seconds(deviceTimeOut > 0 ? deviceTimeOut : DEFAULT_FOLDER_ACCESS_TIME_OUT_SEC); + + while (std::chrono::steady_clock::now() < timeoutTime && + future.wait_for(UI_UPDATE_INTERVAL / 2) != std::future_status::ready) procCallback.requestUiRefresh(); //throw X - if (isReady(fi.second)) - { + if (!isReady(future)) + output.failedChecks.emplace(folderPath, FileError(replaceCpy(_("Timeout while searching for folder %x."), L"%x", displayPathFmt))); + else try { //call future::get() only *once*! otherwise: undefined behavior! - if (fi.second.get()) //throw FileError - output.existing.insert(fi.first); + if (future.get()) //throw FileError + output.existing.insert(folderPath); else - output.notExisting.insert(fi.first); + output.notExisting.insert(folderPath); } - catch (const FileError& e) { output.failedChecks.emplace(fi.first, e); } - } - else - output.failedChecks.emplace(fi.first, FileError(replaceCpy(_("Timeout while searching for folder %x."), L"%x", displayPathFmt))); + catch (const FileError& e) { output.failedChecks.emplace(folderPath, e); } } return output; } diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 161fb35e..86006337 100755 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -80,8 +80,6 @@ Zstring abandonedLockDeletionName(const Zstring& lockFilePath) //make sure to NO } -#if 0 -#endif using ProcessId = pid_t; @@ -160,7 +158,7 @@ LockInformation getLockInfoFromCurrentProcess() //throw FileError LockInformation unserialize(MemoryStreamIn<ByteArray>& stream) //throw UnexpectedEndOfStreamError { char tmp[sizeof(LOCK_FORMAT_DESCR)] = {}; - readArray(stream, &tmp, sizeof(tmp)); //file format header + readArray(stream, &tmp, sizeof(tmp)); //file format header const int lockFileVersion = readNumber<int32_t>(stream); // if (!std::equal(std::begin(tmp), std::end(tmp), std::begin(LOCK_FORMAT_DESCR)) || diff --git a/FreeFileSync/Source/base/fatal_error.h b/FreeFileSync/Source/base/fatal_error.h new file mode 100755 index 00000000..a27e423b --- /dev/null +++ b/FreeFileSync/Source/base/fatal_error.h @@ -0,0 +1,45 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef ERROR_LOG_H_89734181783491324134 +#define ERROR_LOG_H_89734181783491324134 + +#include <cassert> +#include <zen/file_io.h> +#include <zen/time.h> +#include "ffs_paths.h" + + +namespace fff +{ +//write error message to a file (even with corrupted stack)- call in desperate situations when no other means of error handling is available +void logFatalError(const std::string& msg); //noexcept + + + + + + + + + +//##################### implementation ############################ +inline +void logFatalError(const std::string& msg) //noexcept +{ + using namespace zen; + + assert(false); //this is stuff we like to debug + const std::string logEntry = "[" + formatTime<std::string>(FORMAT_DATE) + " " + formatTime<std::string>(FORMAT_TIME) + "] " + msg; + try + { + saveBinContainer(getConfigDirPathPf() + Zstr("LastError.log"), logEntry, nullptr /*notifyUnbufferedIO*/); //throw FileError + } + catch (FileError&) {} +} +} + +#endif //ERROR_LOG_H_89734181783491324134 diff --git a/FreeFileSync/Source/base/file_hierarchy.cpp b/FreeFileSync/Source/base/file_hierarchy.cpp index 88f7f152..d6e65ebd 100755 --- a/FreeFileSync/Source/base/file_hierarchy.cpp +++ b/FreeFileSync/Source/base/file_hierarchy.cpp @@ -27,7 +27,7 @@ std::wstring fff::getShortDisplayNameForFolderPair(const AbstractPath& itemPathL const Zstring itemNameL = AFS::getItemName(tmpPathL); const Zstring itemNameR = AFS::getItemName(tmpPathR); - if (!strEqual(itemNameL, itemNameR, CmpNaturalSort())) //let's compare case-insensitively even on Linux! + if (!equalNoCase(itemNameL, itemNameR)) //let's compare case-insensitively even on Linux! break; tmpPathL = *parentPathL; @@ -358,7 +358,7 @@ const wchar_t arrowRight[] = L"->"; std::wstring fff::getCategoryDescription(const FileSystemObject& fsObj) { - const std::wstring footer = L"\n[" + utfTo<std::wstring>(fsObj. getPairItemName()) + L"]"; + const std::wstring footer = L"\n[" + utfTo<std::wstring>(fsObj. getItemNameAny()) + L"]"; const CompareFilesResult cmpRes = fsObj.getCategory(); switch (cmpRes) @@ -439,7 +439,7 @@ std::wstring fff::getSyncOpDescription(SyncOperation op) std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) { - const std::wstring footer = L"\n[" + utfTo<std::wstring>(fsObj. getPairItemName()) + L"]"; + const std::wstring footer = L"\n[" + utfTo<std::wstring>(fsObj. getItemNameAny()) + L"]"; const SyncOperation op = fsObj.getSyncOperation(); switch (op) @@ -458,15 +458,16 @@ std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) case SO_COPY_METADATA_TO_RIGHT: //harmonize with synchronization.cpp::FolderPairSyncer::synchronizeFileInt, ect!! { - Zstring shortNameOld = fsObj.getItemName<RIGHT_SIDE>(); - Zstring shortNameNew = fsObj.getItemName< LEFT_SIDE>(); + Zstring itemNameOld = fsObj.getItemName<RIGHT_SIDE>(); + Zstring itemNameNew = fsObj.getItemName< LEFT_SIDE>(); if (op == SO_COPY_METADATA_TO_LEFT) - std::swap(shortNameOld, shortNameNew); + std::swap(itemNameOld, itemNameNew); - if (shortNameOld != shortNameNew) //detected change in case + if (getUnicodeNormalForm(itemNameOld) != + getUnicodeNormalForm(itemNameNew)) //detected change in case return getSyncOpDescription(op) + L"\n" + - fmtPath(shortNameOld) + L" " + arrowRight + L"\n" + //show short name only - fmtPath(shortNameNew) /*+ footer -> redundant */; + fmtPath(itemNameOld) + L" " + arrowRight + L"\n" + //show short name only + fmtPath(itemNameNew) /*+ footer -> redundant */; } return getSyncOpDescription(op) + footer; //fallback @@ -491,8 +492,8 @@ std::wstring fff::getSyncOpDescription(const FileSystemObject& fsObj) //attention: ::SetWindowText() doesn't handle tab characters correctly in combination with certain file names, so don't use them return getSyncOpDescription(op) + L"\n" + - (equalFilePath(beforeLast(relSource, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE), - beforeLast(relTarget, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)) ? + (beforeLast(relSource, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE) == + beforeLast(relTarget, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE) ? //detected pure "rename" fmtPath(afterLast(relSource, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)) + L" " + arrowRight + L"\n" + //show short name only fmtPath(afterLast(relTarget, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)) : diff --git a/FreeFileSync/Source/base/file_hierarchy.h b/FreeFileSync/Source/base/file_hierarchy.h index 0ea4a3bb..4d06f73b 100755 --- a/FreeFileSync/Source/base/file_hierarchy.h +++ b/FreeFileSync/Source/base/file_hierarchy.h @@ -107,9 +107,9 @@ std::wstring getShortDisplayNameForFolderPair(const AbstractPath& itemPathL, con struct FolderContainer { //------------------------------------------------------------------ - using FolderList = std::map<Zstring, std::pair<FolderAttributes, FolderContainer>, LessFilePath>; // - using FileList = std::map<Zstring, FileAttributes, LessFilePath>; //key: file name - using SymlinkList = std::map<Zstring, LinkAttributes, LessFilePath>; // + using FolderList = std::map<Zstring, std::pair<FolderAttributes, FolderContainer>>; // + using FileList = std::map<Zstring, FileAttributes>; //key: raw file name, without any (Unicode) normalization, preserving original upper-/lower-case + using SymlinkList = std::map<Zstring, LinkAttributes>; // //------------------------------------------------------------------ FolderContainer() = default; @@ -123,7 +123,7 @@ struct FolderContainer void addSubFile(const Zstring& itemName, const FileAttributes& attr) { auto rv = files.emplace(itemName, attr); - if (!rv.second) //update entry if already existing (e.g. during folder traverser "retry") => does not handle different item name case (irrelvant!..) + if (!rv.second) //update entry if already existing (e.g. during folder traverser "retry") rv.first->second = attr; } @@ -174,8 +174,7 @@ struct PathInformation //diamond-shaped inheritence! template <SelectedSide side> AbstractPath getAbstractPath() const; template <SelectedSide side> Zstring getRelativePath() const; //get path relative to base sync dir (without leading/trailing FILE_NAME_SEPARATOR) - - Zstring getPairRelativePath() const; + Zstring getRelativePathAny() const { return getRelativePathL(); } //side doesn't matter private: virtual AbstractPath getAbstractPathL() const = 0; //implemented by FileSystemObject + BaseFolderPair @@ -191,8 +190,6 @@ template <> inline AbstractPath PathInformation::getAbstractPath<RIGHT_SIDE>() c template <> inline Zstring PathInformation::getRelativePath<LEFT_SIDE >() const { return getRelativePathL(); } template <> inline Zstring PathInformation::getRelativePath<RIGHT_SIDE>() const { return getRelativePathR(); } -inline Zstring PathInformation::getPairRelativePath() const { return getRelativePathL(); } //side doesn't matter - //------------------------------------------------------------------ class ContainerObject : public virtual PathInformation @@ -421,12 +418,12 @@ class FileSystemObject : public ObjectMgr<FileSystemObject>, public virtual Path public: virtual void accept(FSObjectVisitor& visitor) const = 0; - Zstring getPairItemName() const; //like getItemName() but without bias to which side is returned bool isPairEmpty() const; //true, if both sides are empty + template <SelectedSide side> bool isEmpty() const; //path getters always return valid values, even if isEmpty<side>()! + Zstring getItemNameAny() const; //like getItemName() but without bias to which side is returned template <SelectedSide side> Zstring getItemName() const; //case sensitive! - template <SelectedSide side> bool isEmpty() const; //comparison result CompareFilesResult getCategory() const { return cmpResult_; } @@ -794,12 +791,12 @@ Zstring FileSystemObject::getItemName() const const Zstring& itemName = SelectParam<side>::ref(itemNameL_, itemNameR_); //empty if not existing if (!itemName.empty()) //avoid ternary-WTF! (implicit copy-constructor call!!!!!!) return itemName; - return SelectParam<OtherSide<side>::value>::ref(itemNameL_, itemNameR_); //empty if not existing + return SelectParam<OtherSide<side>::value>::ref(itemNameL_, itemNameR_); } inline -Zstring FileSystemObject::getPairItemName() const +Zstring FileSystemObject::getItemNameAny() const { return getItemName<LEFT_SIDE>(); //side doesn't matter } diff --git a/FreeFileSync/Source/base/generate_logfile.cpp b/FreeFileSync/Source/base/generate_logfile.cpp index cbad8a4c..dbb8e42a 100755 --- a/FreeFileSync/Source/base/generate_logfile.cpp +++ b/FreeFileSync/Source/base/generate_logfile.cpp @@ -77,7 +77,7 @@ void streamToLogFile(const ProcessSummary& summary, //throw FileError const std::wstring& finalStatusLabel, AFS::OutputStream& streamOut) { - auto fmtForTxtFile = [needLbReplace = !strEqual(LINE_BREAK, '\n')](const std::wstring& str) + auto fmtForTxtFile = [needLbReplace = !equalString(LINE_BREAK, '\n')](const std::wstring& str) { std::string utfStr = utfTo<std::string>(str); if (needLbReplace) @@ -196,7 +196,7 @@ std::vector<LogFileInfo> getLogFiles(const AbstractPath& logFolderPath) //throw //"2013-09-15 015052.123 [Error].log" static_assert(TIME_STAMP_LENGTH == 21); - if (endsWith(fi.itemName, Zstr(".log"), CmpFilePath())) + if (endsWith(fi.itemName, Zstr(".log"))) //case-sensitive: e.g. ".LOG" is not from FFS, right? { auto tsBegin = fi.itemName.begin(); auto tsEnd = fi.itemName.end() - 4; diff --git a/FreeFileSync/Source/base/hard_filter.cpp b/FreeFileSync/Source/base/hard_filter.cpp index 1c735c85..dda78b6b 100755 --- a/FreeFileSync/Source/base/hard_filter.cpp +++ b/FreeFileSync/Source/base/hard_filter.cpp @@ -35,7 +35,12 @@ static_assert(FILE_NAME_SEPARATOR == '/'); void addFilterEntry(const Zstring& filterPhrase, std::vector<Zstring>& masksFileFolder, std::vector<Zstring>& masksFolder) { - const Zstring& filterFmt = filterPhrase; //Linux DOES distinguish between upper/lower-case: nothing to do here + warn_static("3. ignore path separator => bug regarding copyFilterAddingExclusion() after failed directory reads when dir has path separator from other OS in name") + + //normalize filter input: 1. ignore Unicode normalization form 2. ignore case 3. ignore path separator + Zstring filterFmt = makeUpperCopy(filterPhrase); + if constexpr (FILE_NAME_SEPARATOR != Zstr('/' )) replace(filterFmt, Zstr('/'), FILE_NAME_SEPARATOR); + if constexpr (FILE_NAME_SEPARATOR != Zstr('\\')) replace(filterFmt, Zstr('\\'), FILE_NAME_SEPARATOR); /* phrase | action +---------+-------- @@ -218,12 +223,12 @@ bool matchesMaskBegin(const Zstring& name, const std::vector<Zstring>& masks) } -std::vector<Zstring> fff::splitByDelimiter(const Zstring& filterString) +std::vector<Zstring> fff::splitByDelimiter(const Zstring& filterPhrase) { //delimiters may be FILTER_ITEM_SEPARATOR or '\n' std::vector<Zstring> output; - for (const Zstring& str : split(filterString, FILTER_ITEM_SEPARATOR, SplitType::SKIP_EMPTY)) //split by less common delimiter first (create few, large strings) + for (const Zstring& str : split(filterPhrase, FILTER_ITEM_SEPARATOR, SplitType::SKIP_EMPTY)) //split by less common delimiter first (create few, large strings) for (Zstring entry : split(str, Zstr('\n'), SplitType::SKIP_EMPTY)) { trim(entry); @@ -261,7 +266,9 @@ void NameFilter::addExclusion(const Zstring& excludePhrase) bool NameFilter::passFileFilter(const Zstring& relFilePath) const { assert(!startsWith(relFilePath, FILE_NAME_SEPARATOR)); - const Zstring& pathFmt = relFilePath; //nothing to do here + + //normalize input: 1. ignore Unicode normalization form 2. ignore case + const Zstring& pathFmt = makeUpperCopy(relFilePath); if (matchesMask<AnyMatch >(pathFmt, excludeMasksFileFolder) || //either full match on file or partial match on any parent folder matchesMask<ParentFolderMatch>(pathFmt, excludeMasksFolder)) //partial match on any parent folder only @@ -277,7 +284,8 @@ bool NameFilter::passDirFilter(const Zstring& relDirPath, bool* childItemMightMa assert(!startsWith(relDirPath, FILE_NAME_SEPARATOR)); assert(!childItemMightMatch || *childItemMightMatch); //check correct usage - const Zstring& pathFmt = relDirPath; //nothing to do here + //normalize input: 1. ignore Unicode normalization form 2. ignore case + const Zstring& pathFmt = makeUpperCopy(relDirPath); if (matchesMask<AnyMatch>(pathFmt, excludeMasksFileFolder) || matchesMask<AnyMatch>(pathFmt, excludeMasksFolder)) diff --git a/FreeFileSync/Source/base/hard_filter.h b/FreeFileSync/Source/base/hard_filter.h index 5e1f1702..682c2502 100755 --- a/FreeFileSync/Source/base/hard_filter.h +++ b/FreeFileSync/Source/base/hard_filter.h @@ -62,7 +62,7 @@ HardFilter::FilterRef combineFilters(const HardFilter::FilterRef& first, const HardFilter::FilterRef& second); -class NullFilter : public HardFilter //no filtering at all +class NullFilter : public HardFilter //no filtering at all { public: bool passFileFilter(const Zstring& relFilePath) const override { return true; } @@ -75,7 +75,7 @@ private: }; -class NameFilter : public HardFilter //standard filter by filepath +class NameFilter : public HardFilter //filter by base-relative file path { public: NameFilter(const Zstring& includePhrase, const Zstring& excludePhrase); @@ -93,7 +93,7 @@ private: bool cmpLessSameType(const HardFilter& other) const override; std::vector<Zstring> includeMasksFileFolder; // - std::vector<Zstring> includeMasksFolder; //upper case (windows) + unique items by construction + std::vector<Zstring> includeMasksFolder; //upper-case + Unicode-normalized by construction std::vector<Zstring> excludeMasksFileFolder; // std::vector<Zstring> excludeMasksFolder; // }; @@ -235,7 +235,7 @@ HardFilter::FilterRef constructFilter(const Zstring& includePhrase, } -std::vector<Zstring> splitByDelimiter(const Zstring& filterString); //keep external linkage for unit test +std::vector<Zstring> splitByDelimiter(const Zstring& filterPhrase); //keep external linkage for unit test } #endif //HARD_FILTER_H_825780275842758345 diff --git a/FreeFileSync/Source/base/icon_buffer.cpp b/FreeFileSync/Source/base/icon_buffer.cpp index a784f9e0..39b5459b 100755 --- a/FreeFileSync/Source/base/icon_buffer.cpp +++ b/FreeFileSync/Source/base/icon_buffer.cpp @@ -283,7 +283,7 @@ struct IconBuffer::Impl InterruptibleThread worker; //------------------------- //------------------------- - std::map<Zstring, wxBitmap, LessFilePath> extensionIcons; //no item count limit!? Test case C:\ ~ 3800 unique file extensions + std::map<Zstring, wxBitmap, LessAsciiNoCase> extensionIcons; //no item count limit!? Test case C:\ ~ 3800 unique file extensions }; diff --git a/FreeFileSync/Source/base/localization.cpp b/FreeFileSync/Source/base/localization.cpp index 28f19e33..de1b7087 100755 --- a/FreeFileSync/Source/base/localization.cpp +++ b/FreeFileSync/Source/base/localization.cpp @@ -89,24 +89,23 @@ FFSTranslation::FFSTranslation(const Zstring& lngFilePath, wxLanguage langId) : pluralParser_ = std::make_unique<plural::PluralForm>(header.pluralDefinition); //throw plural::ParsingError - for (const auto& item : transUtf) - { - std::wstring original = utfTo<std::wstring>(item.first); - std::wstring translation = utfTo<std::wstring>(item.second); - - transMapping_.emplace(std::move(original), std::move(translation)); - } + for (const auto& [original, translation] : transUtf) + transMapping_.emplace(utfTo<std::wstring>(original), + utfTo<std::wstring>(translation)); - for (const auto& item : transPluralUtf) + for (const auto& [singAndPlural, pluralForms] : transPluralUtf) { - std::wstring engSingular = utfTo<std::wstring>(item.first.first); - std::wstring engPlural = utfTo<std::wstring>(item.first.second); + std::vector<std::wstring> transPluralForms; + for (const std::string& pf : pluralForms) + transPluralForms.push_back(utfTo<std::wstring>(pf)); - std::vector<std::wstring> pluralForms; - for (const std::string& pf : item.second) - pluralForms.push_back(utfTo<std::wstring>(pf)); - - transMappingPl_.insert({ { std::move(engSingular), std::move(engPlural) }, std::move(pluralForms) }); + transMappingPl_.insert( + { + { + utfTo<std::wstring>(singAndPlural.first), + utfTo<std::wstring>(singAndPlural.second) + }, + std::move(transPluralForms) }); } } diff --git a/FreeFileSync/Source/base/lock_holder.h b/FreeFileSync/Source/base/lock_holder.h index cfe6a4fb..d4e3371c 100755 --- a/FreeFileSync/Source/base/lock_holder.h +++ b/FreeFileSync/Source/base/lock_holder.h @@ -20,7 +20,7 @@ class LockHolder public: LockHolder(const std::set<Zstring, LessFilePath>& dirPathsExisting, //resolved paths bool& warnDirectoryLockFailed, - ProcessCallback& pcb) + ProcessCallback& pcb /*throw X*/) { using namespace zen; @@ -40,10 +40,10 @@ public: { std::wstring msg = _("Cannot set directory locks for the following folders:"); - for (const auto& fl : failedLocks) + for (const auto& [folderPath, error] : failedLocks) { - msg += L"\n\n" + fmtPath(fl.first); - msg += L"\n" + replaceCpy(fl.second.toString(), L"\n\n", L"\n"); + msg += L"\n\n" + fmtPath(folderPath); + msg += L"\n" + replaceCpy(error.toString(), L"\n\n", L"\n"); } pcb.reportWarning(msg, warnDirectoryLockFailed); //throw X diff --git a/FreeFileSync/Source/base/parallel_scan.cpp b/FreeFileSync/Source/base/parallel_scan.cpp index cc432462..9ffb5f67 100755 --- a/FreeFileSync/Source/base/parallel_scan.cpp +++ b/FreeFileSync/Source/base/parallel_scan.cpp @@ -116,18 +116,18 @@ DiskInfo retrieveDiskInfo(const Zstring& itemPath) /* PERF NOTE --------------------------------------------- -|Testcase: Reading from two different disks| --------------------------------------------- +--------------------------------------------- +|Test case: Reading from two different disks| +--------------------------------------------- Windows 7: 1st(unbuffered) |2nd (OS buffered) ---------------------------------- 1 Thread: 57s | 8s 2 Threads: 39s | 7s --------------------------------------------------- -|Testcase: Reading two directories from same disk| --------------------------------------------------- +--------------------------------------------------- +|Test case: Reading two directories from same disk| +--------------------------------------------------- Windows 7: Windows XP: 1st(unbuffered) |2nd (OS buffered) 1st(unbuffered) |2nd (OS buffered) ---------------------------------- ---------------------------------- @@ -278,8 +278,8 @@ private: { std::lock_guard<std::mutex> dummy(lockCurrentStatus_); - for (const auto& item : activeThreadIdxs_) - parallelOpsTotal += item.second; + for (const auto& [threadIdx, parallelOps] : activeThreadIdxs_) + parallelOpsTotal += parallelOps; filePath = currentFile_; } @@ -319,8 +319,8 @@ struct TraverserConfig const HardFilter::FilterRef filter; //always bound! const SymLinkHandling handleSymlinks; - std::map<Zstring, std::wstring, LessFilePath>& failedDirReads; - std::map<Zstring, std::wstring, LessFilePath>& failedItemReads; + std::map<Zstring, std::wstring>& failedDirReads; + std::map<Zstring, std::wstring>& failedItemReads; AsyncCallback& acb; const int threadIdx; @@ -543,18 +543,17 @@ void fff::parallelDeviceTraversal(const std::set<DirectoryKey>& foldersToRead, ZEN_ON_SCOPE_FAIL( for (InterruptibleThread& wt : worker) wt.interrupt(); ); //interrupt all first, then join //init worker threads - for (const auto& item : perDeviceFolders) + for (const auto& [rootPath, dirKeys] : perDeviceFolders) { - const AbstractPath& rootPath = item.first; const int threadIdx = static_cast<int>(worker.size()); const size_t parallelOps = getDeviceParallelOps(deviceParallelOps, rootPath); std::map<DirectoryKey, DirectoryValue*> workload; - for (const DirectoryKey& key : item.second) + for (const DirectoryKey& key : dirKeys) workload.emplace(key, &output[key]); //=> DirectoryValue* unshared for lock-free worker-thread access - worker.emplace_back([rootPath, workload, threadIdx, &acb, parallelOps]() mutable + worker.emplace_back([rootPath = rootPath /*clang bug :>*/, workload, threadIdx, &acb, parallelOps]() mutable { setCurrentThreadName(("Comp Worker[" + numberTo<std::string>(threadIdx) + "]").c_str()); @@ -565,11 +564,11 @@ void fff::parallelDeviceTraversal(const std::set<DirectoryKey>& foldersToRead, AFS::TraverserWorkload travWorkload; - for (auto& wl : workload) + for (auto& [folderKey, folderVal] : workload) { - const std::vector<Zstring> relPath = split(AFS::getRootRelativePath(wl.first.folderPath), FILE_NAME_SEPARATOR, SplitType::SKIP_EMPTY); - assert(AFS::getRootPath(wl.first.folderPath) == rootPath); - travWorkload.emplace_back(relPath, std::make_shared<BaseDirCallback>(wl.first, *wl.second, acb, threadIdx, lastReportTime)); + const std::vector<Zstring> relPath = split(AFS::getRootRelativePath(folderKey.folderPath), FILE_NAME_SEPARATOR, SplitType::SKIP_EMPTY); + assert(AFS::getRootPath(folderKey.folderPath) == rootPath); + travWorkload.emplace_back(relPath, std::make_shared<BaseDirCallback>(folderKey, *folderVal, acb, threadIdx, lastReportTime)); } AFS::traverseFolderRecursive(rootPath, travWorkload, parallelOps); //throw ThreadInterruption }); diff --git a/FreeFileSync/Source/base/parallel_scan.h b/FreeFileSync/Source/base/parallel_scan.h index bbe1071f..dbb0c1d9 100755 --- a/FreeFileSync/Source/base/parallel_scan.h +++ b/FreeFileSync/Source/base/parallel_scan.h @@ -44,10 +44,10 @@ struct DirectoryValue FolderContainer folderCont; //relative paths (or empty string for root) for directories that could not be read (completely), e.g. access denied, or temporary network drop - std::map<Zstring, std::wstring, LessFilePath> failedFolderReads; //with corresponding error message + std::map<Zstring, std::wstring> failedFolderReads; //with corresponding error message //relative paths (never empty) for failure to read single file/dir/symlink with corresponding error message - std::map<Zstring, std::wstring, LessFilePath> failedItemReads; + std::map<Zstring, std::wstring> failedItemReads; }; diff --git a/FreeFileSync/Source/base/parse_lng.h b/FreeFileSync/Source/base/parse_lng.h index b48af8b2..c282c2de 100755 --- a/FreeFileSync/Source/base/parse_lng.h +++ b/FreeFileSync/Source/base/parse_lng.h @@ -127,7 +127,7 @@ public: template <class Function, class Function2> void visitItems(Function onTrans, Function2 onPluralTrans) const //onTrans takes (const TranslationMap::value_type&), onPluralTrans takes (const TranslationPluralMap::value_type&) { - for (const auto& item : sequence_) + for (const std::shared_ptr<Item>& item : sequence_) if (auto regular = dynamic_cast<const RegularItem*>(item.get())) onTrans(regular->value); else if (auto plural = dynamic_cast<const PluralItem*>(item.get())) @@ -183,8 +183,8 @@ struct Token }; Token(Type t) : type(t) {} - Type type; + Type type; std::string text; }; @@ -196,19 +196,19 @@ public: using TokenMap = std::map<Token::Type, std::string>; - const TokenMap& getList() const { return tokens; } + const TokenMap& getList() const { return tokens_; } std::string text(Token::Type t) const { - auto it = tokens.find(t); - if (it != tokens.end()) + auto it = tokens_.find(t); + if (it != tokens_.end()) return it->second; assert(false); return std::string(); } private: - const TokenMap tokens = + const TokenMap tokens_ = { //header information { Token::TK_HEADER_BEGIN, "<header>" }, @@ -246,19 +246,19 @@ public: pos_ += zen::strLength(zen::BYTE_ORDER_MARK_UTF8); } - Token nextToken() + Token getNextToken() { //skip whitespace - pos_ = std::find_if(pos_, stream_.end(), [](char c) { return !zen::isWhiteSpace(c); }); + pos_ = std::find_if(pos_, stream_.end(), std::not_fn(zen::isWhiteSpace<char>)); if (pos_ == stream_.end()) return Token(Token::TK_END); - for (const auto& token : tokens_.getList()) - if (startsWith(token.second)) + for (const auto& [tokenEnum, tokenString] : tokens_.getList()) + if (startsWith(tokenString)) { - pos_ += token.second.size(); - return Token(token.first); + pos_ += tokenString.size(); + return Token(tokenEnum); } //rest must be "text" @@ -268,7 +268,7 @@ public: std::string text(itBegin, pos_); - normalize(text); //remove whitespace from end ect. + normalize(text); //remove whitespace from end etc. if (text.empty() && pos_ == stream_.end()) return Token(Token::TK_END); @@ -308,9 +308,7 @@ private: bool startsWith(const std::string& prefix) const { - if (stream_.end() - pos_ < static_cast<ptrdiff_t>(prefix.size())) - return false; - return std::equal(prefix.begin(), prefix.end(), pos_); + return zen::startsWith(zen::StringRef<const char>(pos_, stream_.end()), prefix); } static void normalize(std::string& text) @@ -335,7 +333,7 @@ private: class LngParser { public: - LngParser(const std::string& fileStream) : scn_(fileStream), tk_(scn_.nextToken()) {} + LngParser(const std::string& fileStream) : scn_(fileStream), tk_(scn_.getNextToken()) {} void parse(TranslationMap& out, TranslationPluralMap& pluralOut, TransHeader& header) { @@ -357,54 +355,54 @@ public: void parseHeader(TransHeader& header) { - consumeToken(Token::TK_HEADER_BEGIN); + consumeToken(Token::TK_HEADER_BEGIN); //throw ParsingError - consumeToken(Token::TK_LANG_NAME_BEGIN); + consumeToken(Token::TK_LANG_NAME_BEGIN); //throw ParsingError header.languageName = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_LANG_NAME_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_LANG_NAME_END); // - consumeToken(Token::TK_TRANS_NAME_BEGIN); + consumeToken(Token::TK_TRANS_NAME_BEGIN); //throw ParsingError header.translatorName = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_TRANS_NAME_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_TRANS_NAME_END); // - consumeToken(Token::TK_LOCALE_NAME_BEGIN); + consumeToken(Token::TK_LOCALE_NAME_BEGIN); //throw ParsingError header.localeName = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_LOCALE_NAME_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_LOCALE_NAME_END); // - consumeToken(Token::TK_FLAG_FILE_BEGIN); + consumeToken(Token::TK_FLAG_FILE_BEGIN); //throw ParsingError header.flagFile = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_FLAG_FILE_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_FLAG_FILE_END); // - consumeToken(Token::TK_PLURAL_COUNT_BEGIN); + consumeToken(Token::TK_PLURAL_COUNT_BEGIN); //throw ParsingError header.pluralCount = zen::stringTo<int>(token().text); - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_PLURAL_COUNT_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_PLURAL_COUNT_END); // - consumeToken(Token::TK_PLURAL_DEF_BEGIN); + consumeToken(Token::TK_PLURAL_DEF_BEGIN); //throw ParsingError header.pluralDefinition = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_PLURAL_DEF_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_PLURAL_DEF_END); // - consumeToken(Token::TK_HEADER_END); + consumeToken(Token::TK_HEADER_END); //throw ParsingError } private: void parseRegular(TranslationMap& out, TranslationPluralMap& pluralOut, const plural::PluralFormInfo& pluralInfo) { - consumeToken(Token::TK_SRC_BEGIN); + consumeToken(Token::TK_SRC_BEGIN); //throw ParsingError if (token().type == Token::TK_PLURAL_BEGIN) return parsePlural(pluralOut, pluralInfo); std::string original = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_SRC_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_SRC_END); // - consumeToken(Token::TK_TRG_BEGIN); + consumeToken(Token::TK_TRG_BEGIN); //throw ParsingError std::string translation; if (token().type == Token::TK_TEXT) { @@ -412,7 +410,7 @@ private: nextToken(); } validateTranslation(original, translation); //throw ParsingError - consumeToken(Token::TK_TRG_END); + consumeToken(Token::TK_TRG_END); // out.emplace(original, translation); } @@ -421,32 +419,32 @@ private: { //Token::TK_SRC_BEGIN already consumed - consumeToken(Token::TK_PLURAL_BEGIN); + consumeToken(Token::TK_PLURAL_BEGIN); //throw ParsingError std::string engSingular = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_PLURAL_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_PLURAL_END); // - consumeToken(Token::TK_PLURAL_BEGIN); + consumeToken(Token::TK_PLURAL_BEGIN); //throw ParsingError std::string engPlural = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_PLURAL_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_PLURAL_END); // - consumeToken(Token::TK_SRC_END); + consumeToken(Token::TK_SRC_END); //throw ParsingError const SingularPluralPair original(engSingular, engPlural); - consumeToken(Token::TK_TRG_BEGIN); + consumeToken(Token::TK_TRG_BEGIN); //throw ParsingError PluralForms pluralList; while (token().type == Token::TK_PLURAL_BEGIN) { - consumeToken(Token::TK_PLURAL_BEGIN); + consumeToken(Token::TK_PLURAL_BEGIN); //throw ParsingError std::string pluralForm = token().text; - consumeToken(Token::TK_TEXT); - consumeToken(Token::TK_PLURAL_END); + consumeToken(Token::TK_TEXT); //throw ParsingError + consumeToken(Token::TK_PLURAL_END); // pluralList.push_back(pluralForm); } validateTranslation(original, pluralList, pluralInfo); - consumeToken(Token::TK_TRG_END); + consumeToken(Token::TK_TRG_END); //throw ParsingError pluralOut.emplace(original, pluralList); } @@ -678,14 +676,9 @@ private: } - void nextToken() { tk_ = scn_.nextToken(); } const Token& token() const { return tk_; } - void consumeToken(Token::Type t) //throw ParsingError - { - expectToken(t); //throw ParsingError - nextToken(); - } + void nextToken() { tk_ = scn_.getNextToken(); } void expectToken(Token::Type t) //throw ParsingError { @@ -693,6 +686,12 @@ private: throw ParsingError({ L"Unexpected token", scn_.posRow(), scn_.posCol() }); } + void consumeToken(Token::Type t) //throw ParsingError + { + expectToken(t); //throw ParsingError + nextToken(); + } + Scanner scn_; Token tk_; }; diff --git a/FreeFileSync/Source/base/parse_plural.h b/FreeFileSync/Source/base/parse_plural.h index 8a9173e3..e735c421 100755 --- a/FreeFileSync/Source/base/parse_plural.h +++ b/FreeFileSync/Source/base/parse_plural.h @@ -208,22 +208,22 @@ class Scanner public: Scanner(const std::string& stream) : stream_(stream), pos_(stream_.begin()) {} - Token nextToken() + Token getNextToken() //throw ParsingError { //skip whitespace - pos_ = std::find_if(pos_, stream_.end(), [](char c) { return !zen::isWhiteSpace(c); }); + pos_ = std::find_if(pos_, stream_.end(), std::not_fn(zen::isWhiteSpace<char>)); if (pos_ == stream_.end()) return Token::TK_END; - for (const auto& item : tokens_) - if (startsWith(item.first)) + for (const auto& [tokenString, tokenEnum] : tokens_) + if (startsWith(tokenString)) { - pos_ += item.first.size(); - return Token(item.second); + pos_ += tokenString.size(); + return Token(tokenEnum); } - auto digitEnd = std::find_if(pos_, stream_.end(), [](char c) { return !zen::isDigit(c); }); + auto digitEnd = std::find_if(pos_, stream_.end(), std::not_fn(zen::isDigit<char>)); if (pos_ == digitEnd) throw ParsingError(); //unknown token @@ -235,9 +235,7 @@ public: private: bool startsWith(const std::string& prefix) const { - if (stream_.end() - pos_ < static_cast<ptrdiff_t>(prefix.size())) - return false; - return std::equal(prefix.begin(), prefix.end(), pos_); + return zen::startsWith(zen::StringRef<const char>(pos_, stream_.end()), prefix); } using TokenList = std::vector<std::pair<std::string, Token::Type>>; @@ -271,7 +269,7 @@ class Parser public: Parser(const std::string& stream, int64_t& n) : scn_(stream), - tk_(scn_.nextToken()), + tk_(scn_.getNextToken()), //throw ParsingError n_(n) {} std::shared_ptr<Expr<int64_t>> parse() //throw ParsingError; return value always bound! @@ -279,7 +277,7 @@ public: auto e = std::dynamic_pointer_cast<Expr<int64_t>>(parseExpression()); //throw ParsingError if (!e) throw ParsingError(); - expectToken(Token::TK_END); + expectToken(Token::TK_END); //throw ParsingError return e; } @@ -292,13 +290,12 @@ private: if (token().type == Token::TK_TERNARY_QUEST) { - nextToken(); + nextToken(); //throw ParsingError auto ifExp = std::dynamic_pointer_cast<Expr<bool>>(e); auto thenExp = std::dynamic_pointer_cast<Expr<int64_t>>(parseExpression()); //associativity: <- - expectToken(Token::TK_TERNARY_COLON); - nextToken(); + consumeToken(Token::TK_TERNARY_COLON); //throw ParsingError auto elseExp = std::dynamic_pointer_cast<Expr<int64_t>>(parseExpression()); // if (!ifExp || !thenExp || !elseExp) @@ -313,7 +310,7 @@ private: std::shared_ptr<Expression> e = parseLogicalAnd(); while (token().type == Token::TK_OR) //associativity: -> { - nextToken(); + nextToken(); //throw ParsingError std::shared_ptr<Expression> rhs = parseLogicalAnd(); e = makeBiExp<std::logical_or<>, bool>(e, rhs); //throw ParsingError @@ -326,7 +323,7 @@ private: std::shared_ptr<Expression> e = parseEquality(); while (token().type == Token::TK_AND) //associativity: -> { - nextToken(); + nextToken(); //throw ParsingError std::shared_ptr<Expression> rhs = parseEquality(); e = makeBiExp<std::logical_and<>, bool>(e, rhs); //throw ParsingError @@ -342,7 +339,7 @@ private: if (t == Token::TK_EQUAL || //associativity: n/a t == Token::TK_NOT_EQUAL) { - nextToken(); + nextToken(); //throw ParsingError std::shared_ptr<Expression> rhs = parseRelational(); if (t == Token::TK_EQUAL) return makeBiExp<std::equal_to<>, int64_t>(e, rhs); //throw ParsingError @@ -361,7 +358,7 @@ private: t == Token::TK_GREATER || t == Token::TK_GREATER_EQUAL) { - nextToken(); + nextToken(); //throw ParsingError std::shared_ptr<Expression> rhs = parseMultiplicative(); if (t == Token::TK_LESS) return makeBiExp<std::less <>, int64_t>(e, rhs); // @@ -378,7 +375,7 @@ private: while (token().type == Token::TK_MODULUS) //associativity: -> { - nextToken(); + nextToken(); //throw ParsingError std::shared_ptr<Expression> rhs = parsePrimary(); //"compile-time" check: n % 0 @@ -395,37 +392,44 @@ private: { if (token().type == Token::TK_VARIABLE_N) { - nextToken(); + nextToken(); //throw ParsingError return std::make_shared<VariableNumberNExp>(n_); } else if (token().type == Token::TK_CONST_NUMBER) { const int64_t number = token().number; - nextToken(); + nextToken(); //throw ParsingError return std::make_shared<ConstNumberExp>(number); } else if (token().type == Token::TK_BRACKET_LEFT) { - nextToken(); + nextToken(); //throw ParsingError std::shared_ptr<Expression> e = parseExpression(); - expectToken(Token::TK_BRACKET_RIGHT); - nextToken(); + expectToken(Token::TK_BRACKET_RIGHT); //throw ParsingError + nextToken(); // return e; } else throw ParsingError(); } - void nextToken() { tk_ = scn_.nextToken(); } const Token& token() const { return tk_; } + void nextToken() { tk_ = scn_.getNextToken(); } //throw ParsingError + void expectToken(Token::Type t) //throw ParsingError { if (token().type != t) throw ParsingError(); } + void consumeToken(Token::Type t) //throw ParsingError + { + expectToken(t); //throw ParsingError + nextToken(); + } + Scanner scn_; Token tk_; int64_t& n_; diff --git a/FreeFileSync/Source/base/perf_check.h b/FreeFileSync/Source/base/perf_check.h index a00aae84..2e9ccc6d 100755 --- a/FreeFileSync/Source/base/perf_check.h +++ b/FreeFileSync/Source/base/perf_check.h @@ -9,8 +9,8 @@ #include <map> #include <chrono> +#include <optional> #include <string> -#include <zen/legacy_compiler.h> //#includes <optional> namespace fff diff --git a/FreeFileSync/Source/base/process_xml.cpp b/FreeFileSync/Source/base/process_xml.cpp index da7fa2e7..e3d1b89f 100755 --- a/FreeFileSync/Source/base/process_xml.cpp +++ b/FreeFileSync/Source/base/process_xml.cpp @@ -8,7 +8,6 @@ #include <zenxml/xml.h> #include <zen/file_access.h> #include <zen/file_io.h> -#include <zen/xml_io.h> #include <zen/time.h> #include <wx/intl.h> #include "ffs_paths.h" @@ -49,8 +48,8 @@ XmlType getXmlTypeNoThrow(const XmlDoc& doc) //throw() XmlType fff::getXmlType(const Zstring& filePath) //throw FileError { - //do NOT use zen::loadStream as it will needlessly load even huge files! - XmlDoc doc = loadXmlDocument(filePath); //throw FileError; quick exit if file is not an FFS XML + //quick exit if file is not an XML + XmlDoc doc = loadXml(filePath); //throw FileError return ::getXmlTypeNoThrow(doc); } @@ -1020,8 +1019,8 @@ void readConfig(const XmlIn& in, SyncConfig& syncCfg, std::map<AbstractPath, siz if (syncCfg.versioningStyle == VersioningStyle::REPLACE) { - if (endsWith(syncCfg.versioningFolderPhrase, Zstr("/%timestamp%"), CmpAsciiNoCase()) || - endsWith(syncCfg.versioningFolderPhrase, Zstr("\\%timestamp%"), CmpAsciiNoCase())) + if (endsWithAsciiNoCase(syncCfg.versioningFolderPhrase, Zstr("/%timestamp%")) || + endsWithAsciiNoCase(syncCfg.versioningFolderPhrase, Zstr("\\%timestamp%"))) { syncCfg.versioningFolderPhrase.resize(syncCfg.versioningFolderPhrase.size() - strLength(Zstr("/%timestamp%"))); syncCfg.versioningStyle = VersioningStyle::TIMESTAMP_FOLDER; @@ -1101,8 +1100,8 @@ void readConfig(const XmlIn& in, LocalPairConfig& lpc, std::map<AbstractPath, si { auto getParallelOps = [&](const Zstring& folderPathPhrase, size_t& parallelOps) { - if (startsWith(folderPathPhrase, Zstr("sftp:"), CmpAsciiNoCase()) || - startsWith(folderPathPhrase, Zstr( "ftp:"), CmpAsciiNoCase())) + if (startsWithAsciiNoCase(folderPathPhrase, Zstr("sftp:")) || + startsWithAsciiNoCase(folderPathPhrase, Zstr( "ftp:"))) { for (const Zstring& optPhrase : split(folderPathPhrase, Zstr("|"), SplitType::SKIP_EMPTY)) if (startsWith(optPhrase, Zstr("con="))) @@ -1129,7 +1128,7 @@ void readConfig(const XmlIn& in, LocalPairConfig& lpc, std::map<AbstractPath, si setParallelOps(lpc.folderPathPhraseRight, parallelOpsR); //TODO: remove after migration - 2016-07-24 - auto ciReplace = [](Zstring& pathPhrase, const Zstring& oldTerm, const Zstring& newTerm) { pathPhrase = ciReplaceCpy(pathPhrase, oldTerm, newTerm); }; + auto ciReplace = [](Zstring& pathPhrase, const Zstring& oldTerm, const Zstring& newTerm) { pathPhrase = replaceCpyAsciiNoCase(pathPhrase, oldTerm, newTerm); }; ciReplace(lpc.folderPathPhraseLeft, Zstr("%csidl_MyDocuments%"), Zstr("%csidl_Documents%")); ciReplace(lpc.folderPathPhraseLeft, Zstr("%csidl_MyMusic%" ), Zstr("%csidl_Music%")); ciReplace(lpc.folderPathPhraseLeft, Zstr("%csidl_MyPictures%" ), Zstr("%csidl_Pictures%")); @@ -1278,7 +1277,7 @@ void readConfig(const XmlIn& in, XmlGuiConfig& cfg, int formatVer) cfg.mainCfg.ignoreErrors = str == "Ignore"; str = trimCpy(utfTo<std::string>(cfg.mainCfg.postSyncCommand)); - if (strEqual(str, "Close progress dialog", CmpAsciiNoCase())) + if (equalAsciiNoCase(str, "Close progress dialog")) cfg.mainCfg.postSyncCommand.clear(); } } @@ -1355,7 +1354,7 @@ void readConfig(const XmlIn& in, XmlBatchConfig& cfg, int formatVer) cfg.mainCfg.ignoreErrors = str == "Ignore"; str = trimCpy(utfTo<std::string>(cfg.mainCfg.postSyncCommand)); - if (strEqual(str, "Close progress dialog", CmpAsciiNoCase())) + if (equalAsciiNoCase(str, "Close progress dialog")) { cfg.batchExCfg.autoCloseSummary = true; cfg.mainCfg.postSyncCommand.clear(); @@ -1363,7 +1362,7 @@ void readConfig(const XmlIn& in, XmlBatchConfig& cfg, int formatVer) else if (str == "rundll32.exe powrprof.dll,SetSuspendState Sleep" || str == "rundll32.exe powrprof.dll,SetSuspendState" || str == "systemctl suspend" || - str == "osascript -e \'tell application \"System Events\" to sleep\'") + str == "osascript -e 'tell application \"System Events\" to sleep'") { cfg.batchExCfg.postSyncAction = PostSyncAction::SLEEP; cfg.mainCfg.postSyncCommand.clear(); @@ -1371,7 +1370,7 @@ void readConfig(const XmlIn& in, XmlBatchConfig& cfg, int formatVer) else if (str == "shutdown /s /t 60" || str == "shutdown -s -t 60" || str == "systemctl poweroff" || - str == "osascript -e \'tell application \"System Events\" to shut down\'") + str == "osascript -e 'tell application \"System Events\" to shut down'") { cfg.batchExCfg.postSyncAction = PostSyncAction::SHUTDOWN; cfg.mainCfg.postSyncCommand.clear(); @@ -1396,7 +1395,6 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) inGeneral["CopyLockedFiles" ].attribute("Enabled", cfg.copyLockedFiles); inGeneral["CopyFilePermissions" ].attribute("Enabled", cfg.copyFilePermissions); inGeneral["FileTimeTolerance" ].attribute("Seconds", cfg.fileTimeTolerance); - inGeneral["FolderAccessTimeout" ].attribute("Seconds", cfg.folderAccessTimeout); inGeneral["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority); inGeneral["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile); inGeneral["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy); @@ -1673,8 +1671,8 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) if (inGui["ExternalApps"](extApps)) { cfg.gui.externalApps.clear(); - for (const auto& item : extApps) - cfg.gui.externalApps.push_back({ item.first, item.second }); + for (const auto& [description, cmdLine] : extApps) + cfg.gui.externalApps.push_back({ description, cmdLine }); } } else @@ -1683,7 +1681,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) //TODO: remove macro migration after some time! 2016-06-30 if (formatVer < 3) - for (auto& item : cfg.gui.externalApps) + for (ExternalApp& item : cfg.gui.externalApps) { replace(item.cmdLine, Zstr("%item2_path%"), Zstr("%item_path2%")); replace(item.cmdLine, Zstr("%item_folder%"), Zstr("%folder_path%")); @@ -1703,7 +1701,7 @@ void readConfig(const XmlIn& in, XmlGlobalSettings& cfg, int formatVer) } } //TODO: remove macro migration after some time! 2016-07-18 - for (auto& item : cfg.gui.externalApps) + for (ExternalApp& item : cfg.gui.externalApps) replace(item.cmdLine, Zstr("%item_folder%"), Zstr("%folder_path%")); //last update check @@ -1740,7 +1738,7 @@ int getConfigFormatVersion(const XmlDoc& doc) template <class ConfigType> void readConfig(const Zstring& filePath, XmlType type, ConfigType& cfg, int currentXmlFormatVer, std::wstring& warningMsg) //throw FileError { - XmlDoc doc = loadXmlDocument(filePath); //throw FileError + XmlDoc doc = loadXml(filePath); //throw FileError if (getXmlTypeNoThrow(doc) != type) //noexcept throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); @@ -1752,7 +1750,7 @@ void readConfig(const Zstring& filePath, XmlType type, ConfigType& cfg, int curr try { - checkForMappingErrors(in, filePath); //throw FileError + checkXmlMappingErrors(in, filePath); //throw FileError //(try to) migrate old configuration automatically if (formatVer < currentXmlFormatVer) @@ -1795,7 +1793,7 @@ XmlCfg parseConfig(const XmlDoc& doc, const Zstring& filePath, int currentXmlFor try { - checkForMappingErrors(in, filePath); //throw FileError + checkXmlMappingErrors(in, filePath); //throw FileError //(try to) migrate old configuration if needed if (formatVer < currentXmlFormatVer) @@ -1824,7 +1822,7 @@ void fff::readAnyConfig(const std::vector<Zstring>& filePaths, XmlGuiConfig& cfg const Zstring& filePath = *it; const bool firstItem = it == filePaths.begin(); //init all non-"mainCfg" settings with first config file - XmlDoc doc = loadXmlDocument(filePath); //throw FileError + XmlDoc doc = loadXml(filePath); //throw FileError switch (getXmlTypeNoThrow(doc)) { @@ -2041,7 +2039,6 @@ void writeConfig(const XmlGlobalSettings& cfg, XmlOut& out) outGeneral["CopyLockedFiles" ].attribute("Enabled", cfg.copyLockedFiles); outGeneral["CopyFilePermissions" ].attribute("Enabled", cfg.copyFilePermissions); outGeneral["FileTimeTolerance" ].attribute("Seconds", cfg.fileTimeTolerance); - outGeneral["FolderAccessTimeout" ].attribute("Seconds", cfg.folderAccessTimeout); outGeneral["RunWithBackgroundPriority"].attribute("Enabled", cfg.runWithBackgroundPriority); outGeneral["LockDirectoriesDuringSync"].attribute("Enabled", cfg.createLockFile); outGeneral["VerifyCopiedFiles" ].attribute("Enabled", cfg.verifyFileCopy); @@ -2167,7 +2164,7 @@ void writeConfig(const ConfigType& cfg, XmlType type, int xmlFormatVer, const Zs XmlOut out(doc); writeConfig(cfg, out); - saveXmlDocument(doc, filePath); //throw FileError + saveXml(doc, filePath); //throw FileError } } @@ -2191,7 +2188,7 @@ void fff::writeConfig(const XmlGlobalSettings& cfg, const Zstring& filePath) std::wstring fff::extractJobName(const Zstring& cfgFilePath) { - const Zstring shortName = afterLast(cfgFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); - const Zstring jobName = beforeLast(shortName, Zstr('.'), IF_MISSING_RETURN_ALL); + const Zstring fileName = afterLast(cfgFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); + const Zstring jobName = beforeLast(fileName, Zstr('.'), IF_MISSING_RETURN_ALL); return utfTo<std::wstring>(jobName); } diff --git a/FreeFileSync/Source/base/process_xml.h b/FreeFileSync/Source/base/process_xml.h index 17cb884a..85777a5c 100755 --- a/FreeFileSync/Source/base/process_xml.h +++ b/FreeFileSync/Source/base/process_xml.h @@ -7,7 +7,6 @@ #ifndef PROCESS_XML_H_28345825704254262435 #define PROCESS_XML_H_28345825704254262435 -#include <zen/xml_io.h> #include <wx/gdicmn.h> #include "localization.h" #include "structures.h" @@ -175,7 +174,6 @@ struct XmlGlobalSettings bool copyFilePermissions = false; int fileTimeTolerance = 2; //max. allowed file time deviation; < 0 means unlimited tolerance; default 2s: FAT vs NTFS - std::chrono::seconds folderAccessTimeout{20}; //consider CD-ROM insert or hard disk spin up time from sleep bool runWithBackgroundPriority = false; bool createLockFile = true; bool verifyFileCopy = false; diff --git a/FreeFileSync/Source/base/resolve_path.cpp b/FreeFileSync/Source/base/resolve_path.cpp index f8eae4d9..49699fa4 100755 --- a/FreeFileSync/Source/base/resolve_path.cpp +++ b/FreeFileSync/Source/base/resolve_path.cpp @@ -29,8 +29,8 @@ std::optional<Zstring> getEnvironmentVar(const Zstring& name) trim(value); //remove leading, trailing blanks //remove leading, trailing double-quotes - if (startsWith(value, Zstr('\"')) && - endsWith (value, Zstr('\"')) && + if (startsWith(value, Zstr('"')) && + endsWith (value, Zstr('"')) && value.length() >= 2) value = Zstring(value.c_str() + 1, value.length() - 2); @@ -77,23 +77,24 @@ Zstring resolveRelativePath(const Zstring& relativePath) + //returns value if resolved std::optional<Zstring> tryResolveMacro(const Zstring& macro) //macro without %-characters { //there exist environment variables named %TIME%, %DATE% so check for our internal macros first! - if (strEqual(macro, Zstr("time"), CmpAsciiNoCase())) + if (equalAsciiNoCase(macro, Zstr("time"))) return formatTime<Zstring>(Zstr("%H%M%S")); - if (strEqual(macro, Zstr("date"), CmpAsciiNoCase())) + if (equalAsciiNoCase(macro, Zstr("date"))) return formatTime<Zstring>(FORMAT_ISO_DATE); - if (strEqual(macro, Zstr("timestamp"), CmpAsciiNoCase())) + if (equalAsciiNoCase(macro, Zstr("timestamp"))) return formatTime<Zstring>(Zstr("%Y-%m-%d %H%M%S")); //e.g. "2012-05-15 131513" Zstring timeStr; auto resolveTimePhrase = [&](const Zchar* phrase, const Zchar* format) -> bool { - if (!strEqual(macro, phrase, CmpAsciiNoCase())) + if (!equalAsciiNoCase(macro, phrase)) return false; timeStr = formatTime<Zstring>(format); @@ -188,12 +189,10 @@ void getDirectoryAliasesRecursive(const Zstring& pathPhrase, std::set<Zstring, L addEnvVar("HOME"); //Linux: /home/<user> Mac: /Users/<user> //addEnvVar("USER"); -> any benefit? //substitute paths by symbolic names - for (const auto& item : macroList) + for (const auto& [macroName, macroPath] : macroList) { - const Zstring& macroName = item.first; - const Zstring& macroPath = item.second; - - const Zstring pathSubst = ciReplaceCpy(pathPhrase, macroPath, MACRO_SEP + macroName + MACRO_SEP); //ci on Linux, too? okay + //should use a replaceCpy() that considers "local path" case-sensitivity (if only we had one...) + const Zstring pathSubst = replaceCpyAsciiNoCase(pathPhrase, macroPath, MACRO_SEP + macroName + MACRO_SEP); if (pathSubst != pathPhrase) output.insert(pathSubst); } diff --git a/FreeFileSync/Source/base/status_handler.h b/FreeFileSync/Source/base/status_handler.h index 9a3c0948..59d5c80c 100755 --- a/FreeFileSync/Source/base/status_handler.h +++ b/FreeFileSync/Source/base/status_handler.h @@ -86,7 +86,7 @@ public: //implement parts of ProcessCallback void initNewPhase(int itemsTotal, int64_t bytesTotal, Phase phase) override //(throw X) { - assert(itemsTotal < 0 == bytesTotal < 0); + assert((itemsTotal < 0) == (bytesTotal < 0)); currentPhase_ = phase; refStats(statsTotal_, currentPhase_) = { itemsTotal, bytesTotal }; } diff --git a/FreeFileSync/Source/base/status_handler_impl.h b/FreeFileSync/Source/base/status_handler_impl.h index 21f108de..af578b82 100755 --- a/FreeFileSync/Source/base/status_handler_impl.h +++ b/FreeFileSync/Source/base/status_handler_impl.h @@ -131,9 +131,9 @@ public: void notifyTaskBegin(size_t prio) //noexcept { assert(!zen::runningMainThread()); - assert(!getThreadStatus()); const uint64_t threadId = zen::getThreadId(); std::lock_guard<std::mutex> dummy(lockCurrentStatus_); + assert(!getThreadStatus()); //const size_t taskIdx = [&]() -> size_t //{ @@ -160,7 +160,7 @@ public: const uint64_t threadId = zen::getThreadId(); std::lock_guard<std::mutex> dummy(lockCurrentStatus_); - for (auto& sbp : statusByPriority_) + for (std::vector<ThreadStatus>& sbp : statusByPriority_) for (ThreadStatus& ts : sbp) if (ts.threadId == threadId) { @@ -196,7 +196,7 @@ private: assert(!zen::runningMainThread()); const uint64_t threadId = zen::getThreadId(); - for (auto& sbp : statusByPriority_) + for (std::vector<ThreadStatus>& sbp : statusByPriority_) for (ThreadStatus& ts : sbp) //thread count is (hopefully) small enough so that linear search won't hurt perf if (ts.threadId == threadId) return &ts; @@ -250,7 +250,7 @@ private: statusMsg = [&] { - for (const auto& sbp : statusByPriority_) + for (const std::vector<ThreadStatus>& sbp : statusByPriority_) for (const ThreadStatus& ts : sbp) if (!ts.statusMsg.empty()) return ts.statusMsg; @@ -386,10 +386,7 @@ struct ParallelContext const AddTaskCallback& scheduleExtraTask; //throw ThreadInterruption }; -#ifdef __GNUC__ //ugly, but we won't put the function into a cpp nor make it inline - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wunused-function" -#endif + namespace { void massParallelExecute(const std::vector<std::pair<AbstractPath, ParallelWorkItem>>& workload, @@ -423,13 +420,12 @@ void massParallelExecute(const std::vector<std::pair<AbstractPath, ParallelWorkI //--------------------------------------------------------------------------------------------------------- //Attention: carefully orchestrate access to deviceThreadGroups and its contained worker threads! e.g. synchronize potential access during ~DeviceThreadGroup! - for (const auto& devItems : perDeviceWorkload) + for (const auto& [rootPath, wl] : perDeviceWorkload) { - const AbstractPath& rootPath = devItems.first; const size_t parallelOps = getDeviceParallelOps(deviceParallelOps, rootPath); const size_t statusPrio = deviceThreadGroups.size(); - auto scheduleExtraTask = [&acb, &deviceThreadGroupsShared, rootPath](const AfsPath& afsPath, const ParallelWorkItem& task) + auto scheduleExtraTask = [&acb, &deviceThreadGroupsShared, rootPath = rootPath /*clang bug :>*/](const AfsPath& afsPath, const ParallelWorkItem& task) { const AbstractPath& itemPath = AFS::appendRelPath(rootPath, afsPath.value); @@ -459,12 +455,11 @@ void massParallelExecute(const std::vector<std::pair<AbstractPath, ParallelWorkI //[!] deviceThreadGroups is shared with worker threads from here on! ZEN_ON_SCOPE_EXIT(deviceThreadGroupsShared.access([&](auto*& deviceThreadGroupsPtr) { deviceThreadGroupsPtr = nullptr; })); - for (const auto& devItems : perDeviceWorkload) + for (const auto& [rootPath, wl] : perDeviceWorkload) { - const AbstractPath& rootPath = devItems.first; ThreadGroupContext& ctx = deviceThreadGroups.find(rootPath)->second; //exists after construction above! - for (const std::pair<AbstractPath, ParallelWorkItem>* item : devItems.second) + for (const std::pair<AbstractPath, ParallelWorkItem>* item : wl) ctx.threadGroup.run([&acb, statusPrio = ctx.statusPrio, &itemPath = item->first, &task = item->second, &scheduleExtraTask = ctx.scheduleExtraTask] { acb.notifyTaskBegin(statusPrio); @@ -487,9 +482,6 @@ void massParallelExecute(const std::vector<std::pair<AbstractPath, ParallelWorkI acb.waitUntilDone(UI_UPDATE_INTERVAL / 2 /*every ~50 ms*/, callback); //throw X } } -#ifdef __GNUC__ - #pragma GCC diagnostic pop -#endif //===================================================================================================================== diff --git a/FreeFileSync/Source/base/structures.cpp b/FreeFileSync/Source/base/structures.cpp index 19c39aac..e39084ea 100755 --- a/FreeFileSync/Source/base/structures.cpp +++ b/FreeFileSync/Source/base/structures.cpp @@ -605,8 +605,8 @@ MainConfiguration fff::merge(const std::vector<MainConfiguration>& mainCfgs) std::map<AbstractPath, size_t> mergedParallelOps; for (const MainConfiguration& mainCfg : mainCfgs) - for (const auto& item : mainCfg.deviceParallelOps) - mergedParallelOps[item.first] = std::max(mergedParallelOps[item.first], item.second); + for (const auto& [rootPath, parallelOps] : mainCfg.deviceParallelOps) + mergedParallelOps[rootPath] = std::max(mergedParallelOps[rootPath], parallelOps); //final assembly MainConfiguration cfgOut; diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index d55a57a8..25723ce2 100755 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -124,7 +124,7 @@ void SyncStatistics::processFile(const FilePair& file) break; case SO_UNRESOLVED_CONFLICT: - conflictMsgs_.push_back({ file.getPairRelativePath(), file.getSyncOpConflict() }); + conflictMsgs_.push_back({ file.getRelativePathAny(), file.getSyncOpConflict() }); break; case SO_COPY_METADATA_TO_LEFT: @@ -178,7 +178,7 @@ void SyncStatistics::processLink(const SymlinkPair& link) break; case SO_UNRESOLVED_CONFLICT: - conflictMsgs_.push_back({ link.getPairRelativePath(), link.getSyncOpConflict() }); + conflictMsgs_.push_back({ link.getRelativePathAny(), link.getSyncOpConflict() }); break; case SO_MOVE_LEFT_FROM: @@ -217,7 +217,7 @@ void SyncStatistics::processFolder(const FolderPair& folder) break; case SO_UNRESOLVED_CONFLICT: - conflictMsgs_.push_back({ folder.getPairRelativePath(), folder.getSyncOpConflict() }); + conflictMsgs_.push_back({ folder.getRelativePathAny(), folder.getSyncOpConflict() }); break; case SO_OVERWRITE_LEFT: @@ -497,8 +497,8 @@ bool removeSymlinkIfExists(const AbstractPath& ap, std::mutex& singleThread) //t { return parallelScope([ap] { return AFS::removeSymlinkIfExists(ap); /*throw FileError*/ }, singleThread); } inline -void renameItem(const AbstractPath& apSource, const AbstractPath& apTarget, std::mutex& singleThread) //throw FileError, ErrorDifferentVolume -{ parallelScope([apSource, apTarget] { AFS::renameItem(apSource, apTarget); /*throw FileError, ErrorDifferentVolume*/ }, singleThread); } +void moveAndRenameItem(const AbstractPath& apSource, const AbstractPath& apTarget, std::mutex& singleThread) //throw FileError, ErrorDifferentVolume +{ parallelScope([apSource, apTarget] { AFS::moveAndRenameItem(apSource, apTarget); /*throw FileError, ErrorDifferentVolume*/ }, singleThread); } inline AbstractPath getSymlinkResolvedPath(const AbstractPath& ap, std::mutex& singleThread) //throw FileError @@ -733,6 +733,7 @@ void DeletionHandler::removeDirWithCallback(const AbstractPath& folderPath,//thr statReporter.reportStatus(replaceCpy(statusText, L"%x", fmtPath(displayPath))); //throw ThreadInterruption statReporter.reportDelta(1, 0); //it would be more correct to report *after* work was done! //OTOH: ThreadInterruption must not happen after last deletion was successful: allow for transactional file model update! + warn_static("=> indeed; fix!?") }; static_assert(std::is_const_v<decltype(txtRemovingFile_)>, "callbacks better be thread-safe!"); auto onBeforeFileDeletion = [&](const std::wstring& displayPath) { notifyDeletion(txtRemovingFile_, displayPath); }; @@ -754,6 +755,7 @@ void DeletionHandler::removeDirWithCallback(const AbstractPath& folderPath,//thr { statReporter.reportStatus(replaceCpy(replaceCpy(statusText, L"%x", L"\n" + fmtPath(displayPathFrom)), L"%y", L"\n" + fmtPath(displayPathTo))); //throw ThreadInterruption statReporter.reportDelta(1, 0); //it would be more correct to report *after* work was done! + warn_static("=> indeed; fix!?") }; static_assert(std::is_const_v<decltype(txtMovingFileXtoY_)>, "callbacks better be thread-safe!"); auto onBeforeFileMove = [&](const std::wstring& displayPathFrom, const std::wstring& displayPathTo) { notifyMove(txtMovingFileXtoY_, displayPathFrom, displayPathTo); }; @@ -1056,7 +1058,7 @@ void FolderPairSyncer::runPass(PassNo pass, SyncCtx& syncCtx, BaseFolderPair& ba workload.addWorkItems(fps.getFolderLevelWorkItems(pass, baseFolder, workload)); //initial workload: set *before* threads get access! std::vector<InterruptibleThread> worker; - ZEN_ON_SCOPE_EXIT( for (InterruptibleThread& wt : worker) wt.join (); ); + ZEN_ON_SCOPE_EXIT( for (InterruptibleThread& wt : worker) wt.join (); ); // ZEN_ON_SCOPE_EXIT( for (InterruptibleThread& wt : worker) wt.interrupt(); ); //interrupt all first, then join for (size_t threadIdx = 0; threadIdx < threadCount; ++threadIdx) @@ -1164,10 +1166,10 @@ III) c -> d caveat: move-sequence needs to be processed in correct order! */ template <class List> inline -bool haveNameClash(const Zstring& shortname, const List& m) +bool haveNameClash(const Zstring& itemName, const List& m) { return std::any_of(m.begin(), m.end(), - [&](const typename List::value_type& obj) { return equalFilePath(obj.getPairItemName(), shortname); }); + [&](const typename List::value_type& obj) { return equalNoCase(obj.getItemNameAny(), itemName); }); //equalNoCase: when in doubt => assume name clash! } @@ -1192,7 +1194,7 @@ void FolderPairSyncer::setup2StepMove(FilePair& sourceFile, //throw FileError, T AFS::getDisplayPath(sourceFile.getAbstractPath<side>()), AFS::getDisplayPath(sourcePathTmp)); - parallel::renameItem(sourceFile.getAbstractPath<side>(), sourcePathTmp, singleThread_); //throw FileError, (ErrorDifferentVolume) + parallel::moveAndRenameItem(sourceFile.getAbstractPath<side>(), sourcePathTmp, singleThread_); //throw FileError, (ErrorDifferentVolume) //TODO: prepare2StepMove: consider ErrorDifferentVolume! e.g. symlink aliasing! @@ -1226,9 +1228,9 @@ auto FolderPairSyncer::createMoveTargetFolder(FileSystemObject& fsObj) -> CmtfSt return cmtfs; //detect (and try to resolve) file type conflicts: 1. symlinks 2. files - const Zstring& shortname = parentFolder->getPairItemName(); - if (haveNameClash(shortname, parentFolder->parent().refSubLinks()) || - haveNameClash(shortname, parentFolder->parent().refSubFiles())) + const Zstring& folderName = parentFolder->getItemNameAny(); + if (haveNameClash(folderName, parentFolder->parent().refSubLinks()) || + haveNameClash(folderName, parentFolder->parent().refSubFiles())) return CmtfStatus::NAME_CLASH; //-------- create parent folder if needed -------------- @@ -1266,7 +1268,7 @@ auto FolderPairSyncer::createMoveTargetFolder(FileSystemObject& fsObj) -> CmtfSt } else //source deleted meanwhile... { - //attention when fixing statistics due to missing folder: child items may be scheduled for move, so deletion will have move references flip back to copy + delete! + //attention when fixing statistics due to missing folder: child items may be scheduled for move, so deletion will have move-references flip back to copy + delete! const SyncStatistics statsBefore(parentFolder->base()); //=> don't bother considering move operations, just calculate over the whole tree parentFolder->removeObject<sideSrc>(); //DON'T physically delete child objects while we're still evaluating them, e.g. fsObj, and in caller code!!! const SyncStatistics statsAfter(parentFolder->base()); @@ -1339,8 +1341,8 @@ void FolderPairSyncer::resolveMoveConflicts(FilePair& sourceFile, //throw FileEr auto haveNameClash = [](const FilePair& file) { - return ::haveNameClash(file.getPairItemName(), file.parent().refSubLinks()) || - ::haveNameClash(file.getPairItemName(), file.parent().refSubFolders()); + return ::haveNameClash(file.getItemNameAny(), file.parent().refSubLinks()) || + ::haveNameClash(file.getItemNameAny(), file.parent().refSubFolders()); }; if (sourceWillBeDeleted || haveNameClash(sourceFile)) @@ -1450,7 +1452,7 @@ bool FolderPairSyncer::needZeroPass(const FilePair& file) //1st, 2nd pass requirements: // - avoid disk space shortage: 1. delete files, 2. overwrite big with small files first -// - support change in type: overwrite file by directory, symlink by file, ect. +// - support change in type: overwrite file by directory, symlink by file, etc. inline FolderPairSyncer::PassNo FolderPairSyncer::getPass(const FilePair& file) @@ -1585,7 +1587,6 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) if (parentFolder->isEmpty<sideTrg>()) //BaseFolderPair OTOH is always non-empty and existing in this context => else: fatal error in zen::synchronize() return; //if parent directory creation failed, there's no reason to show more errors! - //can't use "getAbstractPath<sideTrg>()" as file name is not available! const AbstractPath targetPath = file.getAbstractPath<sideTrg>(); reportInfo(txtCreatingFile_, AFS::getDisplayPath(targetPath)); //throw ThreadInterruption @@ -1636,7 +1637,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) AsyncItemStatReporter statReporter(1, 0, acb_); delHandlerTrg.removeFileWithCallback({ file.getAbstractPath<sideTrg>(), file.getAttributes<sideTrg>() }, - file.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X + file.getRelativePath<sideTrg>(), statReporter, singleThread_); //throw FileError, X file.removeObject<sideTrg>(); //update FilePair } break; @@ -1660,7 +1661,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) //TODO: synchronizeFileInt: consider ErrorDifferentVolume! e.g. symlink aliasing! - parallel::renameItem(pathFrom, pathTo, singleThread_); //throw FileError, (ErrorDifferentVolume) + parallel::moveAndRenameItem(pathFrom, pathTo, singleThread_); //throw FileError, (ErrorDifferentVolume) statReporter.reportDelta(1, 0); @@ -1694,8 +1695,9 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) AsyncItemStatReporter statReporter(1, file.getFileSize<sideSrc>(), acb_); if (file.isFollowedSymlink<sideTrg>()) //since we follow the link, we need to sync case sensitivity of the link manually! - if (file.getItemName<sideTrg>() != file.getItemName<sideSrc>()) //have difference in case? - parallel::renameItem(file.getAbstractPath<sideTrg>(), targetPathLogical, singleThread_); //throw FileError, (ErrorDifferentVolume) + if (getUnicodeNormalForm(file.getItemName<sideTrg>()) != + getUnicodeNormalForm(file.getItemName<sideSrc>())) //have difference in case? + parallel::moveAndRenameItem(file.getAbstractPath<sideTrg>(), targetPathLogical, singleThread_); //throw FileError, (ErrorDifferentVolume) auto onDeleteTargetFile = [&] //delete target at appropriate time { @@ -1704,7 +1706,7 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) FileAttributes followedTargetAttr = file.getAttributes<sideTrg>(); followedTargetAttr.isFollowedSymlink = false; - delHandlerTrg.removeFileWithCallback({ targetPathResolvedOld, followedTargetAttr }, file.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X + delHandlerTrg.removeFileWithCallback({ targetPathResolvedOld, followedTargetAttr }, file.getRelativePath<sideTrg>(), statReporter, singleThread_); //throw FileError, X //no (logical) item count update desired - but total byte count may change, e.g. move(copy) old file to versioning dir statReporter.reportDelta(-1, 0); //undo item stats reporting within DeletionHandler::removeFileWithCallback() @@ -1742,10 +1744,12 @@ void FolderPairSyncer::synchronizeFileInt(FilePair& file, SyncOperation syncOp) { AsyncItemStatReporter statReporter(1, 0, acb_); - assert(file.getItemName<sideTrg>() != file.getItemName<sideSrc>()); - if (file.getItemName<sideTrg>() != file.getItemName<sideSrc>()) //have difference in case? - parallel::renameItem(file.getAbstractPath<sideTrg>(), //throw FileError, (ErrorDifferentVolume) - AFS::appendRelPath(file.parent().getAbstractPath<sideTrg>(), file.getItemName<sideSrc>()), singleThread_); + if (getUnicodeNormalForm(file.getItemName<sideTrg>()) != + getUnicodeNormalForm(file.getItemName<sideSrc>())) //have difference in case? + parallel::moveAndRenameItem(file.getAbstractPath<sideTrg>(), //throw FileError, (ErrorDifferentVolume) + AFS::appendRelPath(file.parent().getAbstractPath<sideTrg>(), file.getItemName<sideSrc>()), singleThread_); + else + assert(false); #if 0 //changing file time without copying content is not justified after CompareVariant::SIZE finds "equal" files! similar issue with CompareVariant::TIME_SIZE and FileTimeTolerance == -1 //Bonus: some devices don't support setting (precise) file times anyway, e.g. FAT or MTP! @@ -1852,7 +1856,7 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy { AsyncItemStatReporter statReporter(1, 0, acb_); - delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X + delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getRelativePath<sideTrg>(), statReporter, singleThread_); //throw FileError, X symlink.removeObject<sideTrg>(); //update SymlinkPair } @@ -1865,7 +1869,7 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy AsyncItemStatReporter statReporter(1, 0, acb_); //reportStatus(delHandlerTrg.getTxtRemovingSymLink(), AFS::getDisplayPath(symlink.getAbstractPath<sideTrg>())); - delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X + delHandlerTrg.removeLinkWithCallback(symlink.getAbstractPath<sideTrg>(), symlink.getRelativePath<sideTrg>(), statReporter, singleThread_); //throw FileError, X statReporter.reportDelta(-1, 0); //undo item stats reporting within DeletionHandler::removeLinkWithCallback() //symlink.removeObject<sideTrg>(); -> "symlink, sideTrg" evaluated below! @@ -1892,9 +1896,12 @@ void FolderPairSyncer::synchronizeLinkInt(SymlinkPair& symlink, SyncOperation sy { AsyncItemStatReporter statReporter(1, 0, acb_); - if (symlink.getItemName<sideTrg>() != symlink.getItemName<sideSrc>()) //have difference in case? - parallel::renameItem(symlink.getAbstractPath<sideTrg>(), //throw FileError, (ErrorDifferentVolume) - AFS::appendRelPath(symlink.parent().getAbstractPath<sideTrg>(), symlink.getItemName<sideSrc>()), singleThread_); + if (getUnicodeNormalForm(symlink.getItemName<sideTrg>()) != + getUnicodeNormalForm(symlink.getItemName<sideSrc>())) //have difference in case? + parallel::moveAndRenameItem(symlink.getAbstractPath<sideTrg>(), //throw FileError, (ErrorDifferentVolume) + AFS::appendRelPath(symlink.parent().getAbstractPath<sideTrg>(), symlink.getItemName<sideSrc>()), singleThread_); + else + assert(false); //if (symlink.getLastWriteTime<sideTrg>() != symlink.getLastWriteTime<sideSrc>()) // //- no need to call sameFileTime() or respect 2 second FAT/FAT32 precision in this comparison @@ -1984,7 +1991,7 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy } else //source deleted meanwhile... { - //attention when fixing statistics due to missing folder: child items may be scheduled for move, so deletion will have move references flip back to copy + delete! + //attention when fixing statistics due to missing folder: child items may be scheduled for move, so deletion will have move-references flip back to copy + delete! const SyncStatistics statsBefore(folder.base()); //=> don't bother considering move operations, just calculate over the whole tree folder.refSubFiles ().clear(); // folder.refSubLinks ().clear(); //update FolderPair @@ -2007,7 +2014,7 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy const SyncStatistics subStats(folder); //counts sub-objects only! AsyncItemStatReporter statReporter(1 + getCUD(subStats), subStats.getBytesToProcess(), acb_); - delHandlerTrg.removeDirWithCallback(folder.getAbstractPath<sideTrg>(), folder.getPairRelativePath(), statReporter, singleThread_); //throw FileError, X + delHandlerTrg.removeDirWithCallback(folder.getAbstractPath<sideTrg>(), folder.getRelativePath<sideTrg>(), statReporter, singleThread_); //throw FileError, X //TODO: implement parallel folder deletion @@ -2026,10 +2033,12 @@ void FolderPairSyncer::synchronizeFolderInt(FolderPair& folder, SyncOperation sy { AsyncItemStatReporter statReporter(1, 0, acb_); - assert(folder.getItemName<sideTrg>() != folder.getItemName<sideSrc>()); - if (folder.getItemName<sideTrg>() != folder.getItemName<sideSrc>()) //have difference in case? - parallel::renameItem(folder.getAbstractPath<sideTrg>(), //throw FileError, (ErrorDifferentVolume) - AFS::appendRelPath(folder.parent().getAbstractPath<sideTrg>(), folder.getItemName<sideSrc>()), singleThread_); + if (getUnicodeNormalForm(folder.getItemName<sideTrg>()) != + getUnicodeNormalForm(folder.getItemName<sideSrc>())) //have difference in case? + parallel::moveAndRenameItem(folder.getAbstractPath<sideTrg>(), //throw FileError, (ErrorDifferentVolume) + AFS::appendRelPath(folder.parent().getAbstractPath<sideTrg>(), folder.getItemName<sideSrc>()), singleThread_); + else + assert(false); //copyFileTimes -> useless: modification time changes with each child-object creation/deletion statReporter.reportDelta(1, 0); @@ -2109,7 +2118,7 @@ AFS::FileCopyResult FolderPairSyncer::copyFileWithCallback(const FileDescriptor& //########################################################################################### template <SelectedSide side> -bool baseFolderDrop(BaseFolderPair& baseFolder, std::chrono::seconds folderAccessTimeout, ProcessCallback& callback) +bool baseFolderDrop(BaseFolderPair& baseFolder, ProcessCallback& callback) { const AbstractPath folderPath = baseFolder.getAbstractPath<side>(); @@ -2118,7 +2127,7 @@ bool baseFolderDrop(BaseFolderPair& baseFolder, std::chrono::seconds folderAcces const std::wstring errMsg = tryReportingError([&] { const FolderStatus status = getFolderStatusNonBlocking({ folderPath }, {} /*deviceParallelOps*/, - folderAccessTimeout, false /*allowUserInteraction*/, callback); + false /*allowUserInteraction*/, callback); static_assert(std::is_same_v<decltype(status.failedChecks.begin()->second), FileError>); if (!status.failedChecks.empty()) @@ -2137,7 +2146,7 @@ bool baseFolderDrop(BaseFolderPair& baseFolder, std::chrono::seconds folderAcces template <SelectedSide side> //create base directories first (if not yet existing) -> no symlink or attribute copying! -bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, std::chrono::seconds folderAccessTimeout, ProcessCallback& callback) //return false if fatal error occurred +bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, ProcessCallback& callback) //return false if fatal error occurred { static const SelectedSide sideSrc = OtherSide<side>::value; const AbstractPath baseFolderPath = baseFolder.getAbstractPath<side>(); @@ -2151,7 +2160,7 @@ bool createBaseFolder(BaseFolderPair& baseFolder, bool copyFilePermissions, std: const std::wstring errMsg = tryReportingError([&] { const FolderStatus status = getFolderStatusNonBlocking({ baseFolderPath }, {} /*deviceParallelOps*/, - folderAccessTimeout, false /*allowUserInteraction*/, callback); + false /*allowUserInteraction*/, callback); static_assert(std::is_same_v<decltype(status.failedChecks.begin()->second), FileError>); if (!status.failedChecks.empty()) @@ -2206,7 +2215,6 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime bool copyFilePermissions, bool failSafeFileCopy, bool runWithBackgroundPriority, - std::chrono::seconds folderAccessTimeout, const std::vector<FolderPairSyncCfg>& syncConfig, FolderComparison& folderCmp, const std::map<AbstractPath, size_t>& deviceParallelOps, @@ -2329,8 +2337,8 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //check for network drops after comparison // - convenience: exit sync right here instead of showing tons of errors during file copy // - early failure! there's no point in evaluating subsequent warnings - if (baseFolderDrop< LEFT_SIDE>(baseFolder, folderAccessTimeout, callback) || - baseFolderDrop<RIGHT_SIDE>(baseFolder, folderAccessTimeout, callback)) + if (baseFolderDrop< LEFT_SIDE>(baseFolder, callback) || + baseFolderDrop<RIGHT_SIDE>(baseFolder, callback)) { jobType[folderIndex] = FolderPairJobType::SKIP; continue; @@ -2456,10 +2464,10 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime { std::wstring msg = _("The following folders are significantly different. Please check that the correct folders are selected for synchronization."); - for (const auto& item : significantDiffPairs) + for (const auto& [folderPathL, folderPathR] : significantDiffPairs) msg += L"\n\n" + - AFS::getDisplayPath(item.first) + L" <-> " + L"\n" + - AFS::getDisplayPath(item.second); + AFS::getDisplayPath(folderPathL) + L" <-> " + L"\n" + + AFS::getDisplayPath(folderPathR); callback.reportWarning(msg, warnings.warnSignificantDifference); } @@ -2469,10 +2477,10 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime { std::wstring msg = _("Not enough free disk space available in:"); - for (const auto& item : diskSpaceMissing) - msg += L"\n\n" + AFS::getDisplayPath(item.first) + L"\n" + - _("Required:") + L" " + formatFilesizeShort(item.second.first) + L"\n" + - _("Available:") + L" " + formatFilesizeShort(item.second.second); + for (const auto& [folderPath, space] : diskSpaceMissing) + msg += L"\n\n" + AFS::getDisplayPath(folderPath) + L"\n" + + _("Required:") + L" " + formatFilesizeShort(space.first) + L"\n" + + _("Available:") + L" " + formatFilesizeShort(space.second); callback.reportWarning(msg, warnings.warnNotEnoughDiskSpace); } @@ -2480,9 +2488,9 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //windows: check if recycle bin really exists; if not, Windows will silently delete, which is wrong { std::wstring msg; - for (const auto& item : recyclerSupported) - if (!item.second) - msg += L"\n" + AFS::getDisplayPath(item.first); + for (const auto& [folderPath, supported] : recyclerSupported) + if (!supported) + msg += L"\n" + AFS::getDisplayPath(folderPath); if (!msg.empty()) callback.reportWarning(_("The recycle bin is not supported by the following folders. Deleted or overwritten files will not be able to be restored:") + L"\n" + msg, @@ -2524,18 +2532,18 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime { std::map<AbstractPath, std::wstring> uniqueMsgs; //=> at most one msg per base folder (*and* per versioningFolderPath) - for (const auto& item : verCheckBaseFolderPaths) //may contain duplicate paths, but with *different* hard filter! - if (std::optional<PathDependency> pd = getPathDependency(versioningFolderPath, NullFilter(), item.first, *item.second)) + for (const auto& [folderPath, filter] : verCheckBaseFolderPaths) //may contain duplicate paths, but with *different* hard filter! + if (std::optional<PathDependency> pd = getPathDependency(versioningFolderPath, NullFilter(), folderPath, *filter)) { std::wstring line = L"\n\n" + _("Versioning folder:") + L" \t" + AFS::getDisplayPath(versioningFolderPath) + - L"\n" + _("Base folder:") + L" \t" + AFS::getDisplayPath(item.first); - if (AFS::equalAbstractPath(pd->basePathParent, item.first) && !pd->relPath.empty()) + L"\n" + _("Base folder:") + L" \t" + AFS::getDisplayPath(folderPath); + if (AFS::equalAbstractPath(pd->basePathParent, folderPath) && !pd->relPath.empty()) line += L"\n" + _("Exclude:") + L" \t" + utfTo<std::wstring>(FILE_NAME_SEPARATOR + pd->relPath + FILE_NAME_SEPARATOR); - uniqueMsgs[item.first] = line; + uniqueMsgs[folderPath] = line; } - for (const auto& item : uniqueMsgs) - msg += item.second; + for (const auto& [folderPath, perFolderMsg] : uniqueMsgs) + msg += perFolderMsg; } if (!msg.empty()) callback.reportWarning(_("The versioning folder is contained in a base folder.") + L"\n" + @@ -2568,14 +2576,14 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //------------------------------------------------------------------------------------------ //checking a second time: (a long time may have passed since folder comparison!) - if (baseFolderDrop< LEFT_SIDE>(baseFolder, folderAccessTimeout, callback) || - baseFolderDrop<RIGHT_SIDE>(baseFolder, folderAccessTimeout, callback)) + if (baseFolderDrop< LEFT_SIDE>(baseFolder, callback) || + baseFolderDrop<RIGHT_SIDE>(baseFolder, callback)) continue; //create base folders if not yet existing if (folderPairStat.createCount() > 0 || folderPairCfg.saveSyncDB) //else: temporary network drop leading to deletions already caught by "sourceFolderMissing" check! - if (!createBaseFolder< LEFT_SIDE>(baseFolder, copyFilePermissions, folderAccessTimeout, callback) || //+ detect temporary network drop!! - !createBaseFolder<RIGHT_SIDE>(baseFolder, copyFilePermissions, folderAccessTimeout, callback)) // + if (!createBaseFolder< LEFT_SIDE>(baseFolder, copyFilePermissions, callback) || //+ detect temporary network drop!! + !createBaseFolder<RIGHT_SIDE>(baseFolder, copyFilePermissions, callback)) // continue; //------------------------------------------------------------------------------------------ @@ -2699,7 +2707,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime //----------------------------------------------------------------------------------------------------- - applyVersioningLimit(versionLimitFolders, folderAccessTimeout, deviceParallelOps, callback); //throw X + applyVersioningLimit(versionLimitFolders, deviceParallelOps, callback); //throw X //------------------- show warnings after end of synchronization -------------------------------------- diff --git a/FreeFileSync/Source/base/synchronization.h b/FreeFileSync/Source/base/synchronization.h index 3ac1291e..7bd5db5e 100755 --- a/FreeFileSync/Source/base/synchronization.h +++ b/FreeFileSync/Source/base/synchronization.h @@ -92,7 +92,6 @@ void synchronize(const std::chrono::system_clock::time_point& syncStartTime, bool copyFilePermissions, bool failSafeFileCopy, bool runWithBackgroundPriority, - std::chrono::seconds folderAccessTimeout, const std::vector<FolderPairSyncCfg>& syncConfig, //CONTRACT: syncConfig and folderCmp correspond row-wise! FolderComparison& folderCmp, // const std::map<AbstractPath, size_t>& deviceParallelOps, diff --git a/FreeFileSync/Source/base/versioning.cpp b/FreeFileSync/Source/base/versioning.cpp index c5bc2aa6..64536076 100755 --- a/FreeFileSync/Source/base/versioning.cpp +++ b/FreeFileSync/Source/base/versioning.cpp @@ -37,7 +37,7 @@ std::pair<time_t, Zstring> fff::impl::parseVersionedFileName(const Zstring& file const auto itExt1 = fileName.end() - (2 * ext.length() + 18); const auto itTs = itExt1 + ext.length(); - if (!strEqual(ext, StringRef<const Zchar>(itExt1, itTs), CmpFilePath())) + if (!equalString(ext, StringRef<const Zchar>(itExt1, itTs))) return {}; const TimeComp tc = parseTime(Zstr(" %Y-%m-%d %H%M%S"), StringRef<const Zchar>(itTs, itTs + 18)); //returns TimeComp() on error @@ -102,7 +102,7 @@ template <class Function> void moveExistingItemToVersioning(const AbstractPath& sourcePath, const AbstractPath& targetPath, //throw FileError Function copyNewItemPlain /*throw FileError*/) { - //start deleting existing target as required by copyFileTransactional()/renameItem(): + //start deleting existing target as required by copyFileTransactional()/moveAndRenameItem(): //best amortized performance if "target existing" is the most common case std::exception_ptr deletionError; try { AFS::removeFilePlain(targetPath); /*throw FileError*/ } @@ -152,7 +152,7 @@ void moveExistingItemToVersioning(const AbstractPath& sourcePath, const Abstract try //first try to move directly without copying { - AFS::renameItem(sourcePath, targetPath); //throw FileError, ErrorDifferentVolume + AFS::moveAndRenameItem(sourcePath, targetPath); //throw FileError, ErrorDifferentVolume //great, we get away cheaply! } catch (ErrorDifferentVolume&) @@ -177,7 +177,7 @@ void moveExistingItemToVersioning(const AbstractPath& sourcePath, const Abstract try //retry { - AFS::renameItem(sourcePath, targetPath); //throw FileError, ErrorDifferentVolume + AFS::moveAndRenameItem(sourcePath, targetPath); //throw FileError, ErrorDifferentVolume } catch (ErrorDifferentVolume&) { @@ -283,7 +283,7 @@ void FileVersioner::revisionFolderImpl(const AbstractPath& folderPath, const Zst const Zstring relPathPf = appendSeparator(relativePath); - for (const auto& fileInfo : files) + for (const AFS::FileInfo& fileInfo : files) { const FileDescriptor fileDescr{ AFS::appendRelPath(folderPath, fileInfo.itemName), FileAttributes(fileInfo.modTime, fileInfo.fileSize, fileInfo.fileId, false /*isSymlink*/)}; @@ -291,12 +291,12 @@ void FileVersioner::revisionFolderImpl(const AbstractPath& folderPath, const Zst revisionFileImpl(fileDescr, relPathPf + fileInfo.itemName, onBeforeFileMove, notifyUnbufferedIO); //throw FileError } - for (const auto& linkInfo : symlinks) + for (const AFS::SymlinkInfo& linkInfo : symlinks) revisionSymlinkImpl(AFS::appendRelPath(folderPath, linkInfo.itemName), relPathPf + linkInfo.itemName, onBeforeFileMove); //throw FileError //move folders recursively - for (const auto& folderInfo : folders) + for (const AFS::FolderInfo& folderInfo : folders) revisionFolderImpl(AFS::appendRelPath(folderPath, folderInfo.itemName), //throw FileError relPathPf + folderInfo.itemName, onBeforeFileMove, onBeforeFolderMove, notifyUnbufferedIO); @@ -348,23 +348,21 @@ void findFileVersions(VersionInfoMap& versions, } }; - for (const auto& item : folderCont.files) - extractFileVersion(item.first, false /*isSymlink*/); + for (const auto& [fileName, attr] : folderCont.files) + extractFileVersion(fileName, false /*isSymlink*/); - for (const auto& item : folderCont.symlinks) - extractFileVersion(item.first, true /*isSymlink*/); + for (const auto& [linkName, attr] : folderCont.symlinks) + extractFileVersion(linkName, true /*isSymlink*/); - for (const auto& item : folderCont.folders) + for (const auto& [folderName, attrAndSub] : folderCont.folders) { - const Zstring& folderName = item.first; - if (relPathOrigParent.empty() && !versionTimeParent) //VersioningStyle::TIMESTAMP_FOLDER? { assert(!versionTimeParent); const time_t versionTime = fff::impl::parseVersionedFolderName(folderName); if (versionTime != 0) { - findFileVersions(versions, item.second.second, + findFileVersions(versions, attrAndSub.second, AFS::appendRelPath(parentFolderPath, folderName), Zstring(), //[!] skip time-stamped folder &versionTime); @@ -372,7 +370,7 @@ void findFileVersions(VersionInfoMap& versions, } } - findFileVersions(versions, item.second.second, + findFileVersions(versions, attrAndSub.second, AFS::appendRelPath(parentFolderPath, folderName), AFS::appendPaths(relPathOrigParent, folderName, FILE_NAME_SEPARATOR), versionTimeParent); @@ -387,8 +385,8 @@ void getFolderItemCount(std::map<AbstractPath, size_t>& folderItemCount, const F //theoretically possible that the same folder is found in one case with items, in another case empty (due to an error) //e.g. "subfolder" for versioning folders c:\folder and c:\folder\subfolder - for (const auto& item : folderCont.folders) - getFolderItemCount(folderItemCount, item.second.second, AFS::appendRelPath(parentFolderPath, item.first)); + for (const auto& [folderName, attrAndSub] : folderCont.folders) + getFolderItemCount(folderItemCount, attrAndSub.second, AFS::appendRelPath(parentFolderPath, folderName)); } } @@ -413,7 +411,6 @@ bool fff::operator<(const VersioningLimitFolder& lhs, const VersioningLimitFolde void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolders, - std::chrono::seconds folderAccessTimeout, const std::map<AbstractPath, size_t>& deviceParallelOps, ProcessCallback& callback /*throw X*/) { @@ -429,7 +426,7 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde tryReportingError([&] { const FolderStatus status = getFolderStatusNonBlocking(folderPaths, deviceParallelOps, //re-check *all* directories on each try! - folderAccessTimeout, false /*allowUserInteraction*/, callback); //throw X + false /*allowUserInteraction*/, callback); //throw X foldersToRead.clear(); for (const AbstractPath& folderPath : status.existing) @@ -439,12 +436,12 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde { std::wstring msg = _("Cannot find the following folders:") + L"\n"; - for (const auto& fc : status.failedChecks) - msg += L"\n" + AFS::getDisplayPath(fc.first); + for (const auto& [folderPath, error] : status.failedChecks) + msg += L"\n" + AFS::getDisplayPath(folderPath); msg += L"\n___________________________________________"; - for (const auto& fc : status.failedChecks) - msg += L"\n\n" + replaceCpy(fc.second.toString(), L"\n\n", L"\n"); + for (const auto& [folderPath, error] : status.failedChecks) + msg += L"\n\n" + replaceCpy(error.toString(), L"\n\n", L"\n"); throw FileError(msg); } @@ -484,28 +481,29 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde std::map<AbstractPath, VersionInfoMap> versionDetails; //versioningFolderPath => <version details> std::map<AbstractPath, size_t> folderItemCount; //<folder path> => <item count> for determination of empty folders - for (const auto& item : folderBuf) + for (const auto& [folderKey, folderVal] : folderBuf) { - const AbstractPath versioningFolderPath = item.first.folderPath; - const DirectoryValue& dirVal = item.second; + const AbstractPath versioningFolderPath = folderKey.folderPath; assert(versionDetails.find(versioningFolderPath) == versionDetails.end()); findFileVersions(versionDetails[versioningFolderPath], - dirVal.folderCont, + folderVal.folderCont, versioningFolderPath, Zstring() /*relPathOrigParent*/, nullptr /*versionTimeParent*/); //determine item count per folder for later detection and removal of empty folders: - getFolderItemCount(folderItemCount, dirVal.folderCont, versioningFolderPath); + getFolderItemCount(folderItemCount, folderVal.folderCont, versioningFolderPath); //make sure the versioning folder is never found empty and is not deleted: ++folderItemCount[versioningFolderPath]; + warn_static("TODO: unicode normalization, case-sensitivity!") + //similarly, failed folder traversal should not make folders look empty: - for (const auto& item2 : dirVal.failedFolderReads) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, item2.first)]; - for (const auto& item2 : dirVal.failedItemReads ) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, beforeLast(item2.first, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE))]; + for (const auto& [relPath, errorMsg] : folderVal.failedFolderReads) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, relPath)]; + for (const auto& [relPath, errorMsg] : folderVal.failedItemReads ) ++folderItemCount[AFS::appendRelPath(versioningFolderPath, beforeLast(relPath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE))]; } //--------- calculate excess file versions --------- @@ -524,10 +522,8 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde { auto it = versionDetails.find(vlf.versioningFolderPath); if (it != versionDetails.end()) - for (auto& item : it->second) + for (auto& [versioningFolderPath, versions] : it->second) { - std::vector<VersionInfo>& versions = item.second; - size_t versionsToKeep = versions.size(); if (vlf.versionMaxAgeDays > 0) { @@ -581,12 +577,12 @@ void fff::applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolde std::vector<std::pair<AbstractPath, ParallelWorkItem>> parallelWorkload; - for (const auto& item : folderItemCount) - if (item.second == 0) - parallelWorkload.emplace_back(item.first, deleteEmptyFolderTask); + for (const auto& [folderPath, itemCount] : folderItemCount) + if (itemCount == 0) + parallelWorkload.emplace_back(folderPath, deleteEmptyFolderTask); - for (const auto& item : itemsToDelete) - parallelWorkload.emplace_back(item.first, [isSymlink = item.second, &textRemoving, &folderItemCountShared, &deleteEmptyFolderTask](ParallelContext& ctx) //throw ThreadInterruption + for (const auto& [itemPath, isSymlink] : itemsToDelete) + parallelWorkload.emplace_back(itemPath, [isSymlink = isSymlink /*=> clang bug :>*/, &textRemoving, &folderItemCountShared, &deleteEmptyFolderTask](ParallelContext& ctx) //throw ThreadInterruption { const std::wstring errMsg = tryReportingError([&] //throw ThreadInterruption { diff --git a/FreeFileSync/Source/base/versioning.h b/FreeFileSync/Source/base/versioning.h index a2743e94..f1cefcc5 100755 --- a/FreeFileSync/Source/base/versioning.h +++ b/FreeFileSync/Source/base/versioning.h @@ -47,7 +47,7 @@ public: throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); if (timeStamp_.size() != 17) //formatTime() returns empty string on error; unexpected length: e.g. problem in year 10,000! - throw FileError(_("Unable to create time stamp for versioning:") + L" \"" + utfTo<std::wstring>(timeStamp_) + L"\""); + throw FileError(_("Unable to create time stamp for versioning:") + L" \"" + utfTo<std::wstring>(timeStamp_) + L'"'); } //multi-threaded access: internally synchronized! @@ -103,7 +103,6 @@ bool operator<(const VersioningLimitFolder& lhs, const VersioningLimitFolder& rh void applyVersioningLimit(const std::set<VersioningLimitFolder>& limitFolders, - std::chrono::seconds folderAccessTimeout, const std::map<AbstractPath, size_t>& deviceParallelOps, ProcessCallback& callback /*throw X*/); diff --git a/FreeFileSync/Source/fs/abstract.cpp b/FreeFileSync/Source/fs/abstract.cpp index e43de243..33b6ec0e 100755 --- a/FreeFileSync/Source/fs/abstract.cpp +++ b/FreeFileSync/Source/fs/abstract.cpp @@ -39,8 +39,7 @@ int AFS::compareAbstractPath(const AbstractPath& lhs, const AbstractPath& rhs) if (rv != 0) return rv; - return CmpFilePath()(lhs.afsPath.value.c_str(), lhs.afsPath.value.size(), - rhs.afsPath.value.c_str(), rhs.afsPath.value.size()); + return compareFilePath(lhs.afsPath.value, rhs.afsPath.value); } @@ -65,17 +64,17 @@ std::optional<AfsPath> AFS::getParentAfsPath(const AfsPath& afsPath) void AFS::traverseFolderRecursive(const AbstractPath& basePath, const AFS::TraverserWorkload& workload, size_t parallelOps) { TraverserWorkloadImpl wlImpl; - for (const auto& item : workload) + for (const auto& [relPathComponents, cb] : workload) { AfsPath afsPath = basePath.afsPath; - for (const Zstring& itemName : item.first) + for (const Zstring& itemName : relPathComponents) { assert(!contains(itemName, FILE_NAME_SEPARATOR)); if (!afsPath.value.empty()) afsPath.value += FILE_NAME_SEPARATOR; afsPath.value += itemName; } - wlImpl.emplace_back(afsPath, item.second); + wlImpl.emplace_back(afsPath, cb); } basePath.afs->traverseFolderRecursive(wlImpl, parallelOps); //throw } @@ -156,7 +155,7 @@ AFS::FileCopyResult AFS::copyFileAsStream(const AfsPath& afsPathSource, const St Native: no, needed for functional correctness, see file_access.cpp MTP: maybe a minor one (need to determine objectId one more time) SFTP: no, needed for functional correctness (synology server), just as for Native - FTP: maybe a minor one: could set modtime via CURLOPT_POSTQUOTE (but this would internally trigger an extra round-trip anyway!) + FTP: no: could set modtime via CURLOPT_POSTQUOTE (but this would internally trigger an extra round-trip anyway!) */ setModTime(apTarget, attrSourceNew.modTime); //throw FileError, follows symlinks } @@ -210,6 +209,9 @@ AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& apSource, con if (transactionalCopy) { + warn_static("doesnt make sense for Google Drive") + + std::optional<AbstractPath> parentPath = AFS::getParentFolderPath(apTarget); if (!parentPath) throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(AFS::getDisplayPath(apTarget))), L"Path is device root."); @@ -236,7 +238,7 @@ AFS::FileCopyResult AFS::copyFileTransactional(const AbstractPath& apSource, con onDeleteTargetFile(); //throw X //perf: this call is REALLY expensive on unbuffered volumes! ~40% performance decrease on FAT USB stick! - renameItem(apTargetTmp, apTarget); //throw FileError, (ErrorDifferentVolume) + moveAndRenameItem(apTargetTmp, apTarget); //throw FileError, (ErrorDifferentVolume) /* CAVEAT on FAT/FAT32: the sequence of deleting the target file and renaming "file.txt.ffs_tmp" to "file.txt" does diff --git a/FreeFileSync/Source/fs/abstract.h b/FreeFileSync/Source/fs/abstract.h index 887f6b8f..d5de09c7 100755 --- a/FreeFileSync/Source/fs/abstract.h +++ b/FreeFileSync/Source/fs/abstract.h @@ -69,6 +69,11 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t static Zstring getRootRelativePath(const AbstractPath& ap) { return ap.afsPath.value; } //---------------------------------------------------------------------------------------------------------------- + static void connectNetworkFolder(const AbstractPath& ap, bool allowUserInteraction) { return ap.afs->connectNetworkFolder(ap.afsPath, allowUserInteraction); } //throw FileError + + static int geAccessTimeout(const AbstractPath& ap) { return ap.afs->getAccessTimeout(); } //returns "0" if no timeout in force + //---------------------------------------------------------------------------------------------------------------- + enum class ItemType { FILE, @@ -115,8 +120,6 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t //noexcept; optional return value: static zen::ImageHolder getFileIcon (const AbstractPath& ap, int pixelSize) { return ap.afs->getFileIcon (ap.afsPath, pixelSize); } static zen::ImageHolder getThumbnailImage(const AbstractPath& ap, int pixelSize) { return ap.afs->getThumbnailImage(ap.afsPath, pixelSize); } - - static void connectNetworkFolder(const AbstractPath& ap, bool allowUserInteraction) { return ap.afs->connectNetworkFolder(ap.afsPath, allowUserInteraction); } //throw FileError //---------------------------------------------------------------------------------------------------------------- using FileId = zen::Zbase<char>; @@ -234,7 +237,7 @@ struct AbstractFileSystem //THREAD-SAFETY: "const" member functions must model t static bool supportPermissionCopy(const AbstractPath& apSource, const AbstractPath& apTarget); //throw FileError //target existing: undefined behavior! (fail/overwrite/auto-rename) - static void renameItem(const AbstractPath& apSource, const AbstractPath& apTarget); //throw FileError, ErrorDifferentVolume + static void moveAndRenameItem(const AbstractPath& apSource, const AbstractPath& apTarget); //throw FileError, ErrorDifferentVolume //Note: it MAY happen that copyFileTransactional() leaves temp files behind, e.g. temporary network drop. // => clean them up at an appropriate time (automatically set sync directions to delete them). They have the following ending: @@ -363,7 +366,7 @@ private: virtual bool supportsPermissions(const AfsPath& afsPath) const = 0; //throw FileError //target existing: undefined behavior! (fail/overwrite/auto-rename) - virtual void renameItemForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget) const = 0; //throw FileError, ErrorDifferentVolume + virtual void moveAndRenameItemForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget) const = 0; //throw FileError, ErrorDifferentVolume //symlink handling: follow link! //target existing: undefined behavior! (fail/overwrite/auto-rename) @@ -384,6 +387,8 @@ private: virtual zen::ImageHolder getThumbnailImage(const AfsPath& afsPath, int pixelSize) const = 0; // virtual void connectNetworkFolder(const AfsPath& afsPath, bool allowUserInteraction) const = 0; //throw FileError + + virtual int getAccessTimeout() const = 0; //returns "0" if no timeout in force //---------------------------------------------------------------------------------------------------------------- virtual uint64_t getFreeDiskSpace(const AfsPath& afsPath) const = 0; //throw FileError, returns 0 if not available @@ -508,12 +513,12 @@ bool AbstractFileSystem::supportPermissionCopy(const AbstractPath& apSource, con inline -void AbstractFileSystem::renameItem(const AbstractPath& apSource, const AbstractPath& apTarget) //throw FileError, ErrorDifferentVolume +void AbstractFileSystem::moveAndRenameItem(const AbstractPath& apSource, const AbstractPath& apTarget) //throw FileError, ErrorDifferentVolume { using namespace zen; if (typeid(*apSource.afs) == typeid(*apTarget.afs)) - return apSource.afs->renameItemForSameAfsType(apSource.afsPath, apTarget); //throw FileError, ErrorDifferentVolume + return apSource.afs->moveAndRenameItemForSameAfsType(apSource.afsPath, apTarget); //throw FileError, ErrorDifferentVolume throw ErrorDifferentVolume(replaceCpy(replaceCpy(_("Cannot move file %x to %y."), L"%x", L"\n" + fmtPath(getDisplayPath(apSource))), diff --git a/FreeFileSync/Source/fs/concrete_impl.h b/FreeFileSync/Source/fs/concrete_impl.h index e08d9574..08e075a8 100755 --- a/FreeFileSync/Source/fs/concrete_impl.h +++ b/FreeFileSync/Source/fs/concrete_impl.h @@ -144,7 +144,7 @@ class GenericDirTraverser public: using Function1 = zen::GetFirstOfT<Functions...>; - GenericDirTraverser(std::vector<Task<TravContext, Function1>>&& initialTasks, size_t parallelOps, const std::string& threadGroupName) : + GenericDirTraverser(std::vector<Task<TravContext, Function1>>&& initialTasks /*throw X*/, size_t parallelOps, const std::string& threadGroupName) : scheduler_(parallelOps, threadGroupName) { //set the initial work load @@ -163,18 +163,18 @@ private: GenericDirTraverser& operator=(const GenericDirTraverser&) = delete; template <class Function> - void evalResultList(std::vector<TaskResult<TravContext, Function>>& results) //throw X + void evalResultList(std::vector<TaskResult<TravContext, Function>>& results /*throw X*/) { for (TaskResult<TravContext, Function>& result : results) evalResult(result); //throw X } template <class Function> - void evalResult(TaskResult<TravContext, Function>& result); //throw X + void evalResult(TaskResult<TravContext, Function>& result /*throw X*/); //specialize! template <class Function> - void evalResultValue(const typename Function::Result& r, std::shared_ptr<AbstractFileSystem::TraverserCallback>& cb); //throw X + void evalResultValue(const typename Function::Result& r, std::shared_ptr<AbstractFileSystem::TraverserCallback>& cb /*throw X*/); TaskScheduler<TravContext, Functions...> scheduler_; }; @@ -182,7 +182,7 @@ private: template <class... Functions> template <class Function> -void GenericDirTraverser<Functions...>::evalResult(TaskResult<TravContext, Function>& result) //throw X +void GenericDirTraverser<Functions...>::evalResult(TaskResult<TravContext, Function>& result /*throw X*/) { auto& cb = result.wi.ctx.cb; try diff --git a/FreeFileSync/Source/fs/native.cpp b/FreeFileSync/Source/fs/native.cpp index fc260d7f..c924777e 100755 --- a/FreeFileSync/Source/fs/native.cpp +++ b/FreeFileSync/Source/fs/native.cpp @@ -96,8 +96,33 @@ std::vector<FsItemRaw> getDirContentFlat(const Zstring& dirPath) //throw FileErr if (itemNameRaw[0] == '.' && (itemNameRaw[1] == 0 || (itemNameRaw[1] == '.' && itemNameRaw[2] == 0))) continue; + + /* + Unicode normalization is file-system-dependent: + + OS Accepts Gives back + ---------- ------- ---------- + macOS (HFS+) all NFD + Linux all <input> + Windows (NTFS, FAT) all <input> + + some file systems return precomposed others decomposed UTF8: http://developer.apple.com/library/mac/#qa/qa1173/_index.html + - OS X edit controls and text fields may return precomposed UTF as directly received by keyboard or decomposed UTF that was copy & pasted in! + - Posix APIs require decomposed form: https://freefilesync.org/forum/viewtopic.php?t=2480 + + => General recommendation: always preserve input UNCHANGED (both unicode normalization and case sensitivity) + => normalize only when needed during string comparison + + Create sample files on Linux: touch decomposed-$'\x6f\xcc\x81'.txt + touch precomposed-$'\xc3\xb3'.txt + + - SMB sharing case-sensitive or NFD file names is fundamentally broken on macOS: + => the macOS SMB manager internally buffers file names as case-insensitive and NFC (= just like NTFS on Windows) + => test: create SMB share from Linux => *boom* on macOS: "Error Code 2: No such file or directory [lstat]" + or WORSE: folders "test" and "Test" *both* incorrectly return the content of one of the two + */ const Zstring& itemName = itemNameRaw; - if (itemName.empty()) //checks result of normalizeUtfForPosix, too! + if (itemName.empty()) throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(dirPath)), L"readdir: Data corruption; item with empty name."); const Zstring& itemPath = appendSeparator(dirPath) + itemName; @@ -126,7 +151,7 @@ ItemDetailsRaw getItemDetails(const Zstring& itemPath) //throw FileError else if (S_ISDIR(statData.st_mode)) //a directory return { ItemType::FOLDER, statData.st_mtime, 0, extractFileId(statData) }; - else //a file or named pipe, ect. => dont't check using S_ISREG(): see comment in file_traverser.cpp + else //a file or named pipe, etc. => dont't check using S_ISREG(): see comment in file_traverser.cpp return { ItemType::FILE, statData.st_mtime, makeUnsigned(statData.st_size), extractFileId(statData) }; } @@ -138,7 +163,7 @@ ItemDetailsRaw getSymlinkTargetDetails(const Zstring& linkPath) //throw FileErro if (S_ISDIR(statData.st_mode)) //a directory return { ItemType::FOLDER, statData.st_mtime, 0, extractFileId(statData) }; - else //a file or named pipe, ect. + else //a file or named pipe, etc. return { ItemType::FILE, statData.st_mtime, makeUnsigned(statData.st_size), extractFileId(statData) }; } @@ -198,13 +223,13 @@ private: }; -void traverseFolderRecursiveNative(const std::vector<std::pair<Zstring, std::shared_ptr<AFS::TraverserCallback>>>& initialTasks, size_t parallelOps) //throw X +void traverseFolderRecursiveNative(const std::vector<std::pair<Zstring, std::shared_ptr<AFS::TraverserCallback>>>& initialTasks /*throw X*/, size_t parallelOps) { std::vector<Task<TravContext, GetDirDetails>> genItems; - for (const auto& item : initialTasks) - genItems.push_back({ GetDirDetails(item.first), - TravContext{ Zstring() /*errorItemName*/, 0 /*errorRetryCount*/, item.second /*TraverserCallback*/ }}); + for (const auto& [folderPath, cb] : initialTasks) + genItems.push_back({ GetDirDetails(folderPath), + TravContext{ Zstring() /*errorItemName*/, 0 /*errorRetryCount*/, cb /*TraverserCallback*/ }}); GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>(std::move(genItems), parallelOps, "Native Traverser"); //throw X } @@ -214,7 +239,7 @@ namespace fff //specialization in original namespace needed by GCC 6 { template <> template <> -void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::evalResultValue<GetDirDetails>(const GetDirDetails::Result& r, std::shared_ptr<AFS::TraverserCallback>& cb) //throw X +void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::evalResultValue<GetDirDetails>(const GetDirDetails::Result& r, std::shared_ptr<AFS::TraverserCallback>& cb /*throw X*/) { for (const FsItemRaw& rawItem : r) scheduler_.run<GetItemDetails>({ GetItemDetails(rawItem), TravContext{ rawItem.itemName, 0 /*errorRetryCount*/, cb }}); @@ -223,7 +248,7 @@ void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::e template <> template <> -void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::evalResultValue<GetItemDetails>(const GetItemDetails::Result& r, std::shared_ptr<AFS::TraverserCallback>& cb) //throw X +void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::evalResultValue<GetItemDetails>(const GetItemDetails::Result& r, std::shared_ptr<AFS::TraverserCallback>& cb /*throw X*/) { switch (r.details.type) { @@ -253,7 +278,7 @@ void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::e template <> template <> -void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::evalResultValue<GetLinkTargetDetails>(const GetLinkTargetDetails::Result& r, std::shared_ptr<AFS::TraverserCallback>& cb) //throw X +void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::evalResultValue<GetLinkTargetDetails>(const GetLinkTargetDetails::Result& r, std::shared_ptr<AFS::TraverserCallback>& cb /*throw X*/) { assert(r.link.type == ItemType::SYMLINK && r.target.type != ItemType::SYMLINK); @@ -264,7 +289,7 @@ void GenericDirTraverser<GetDirDetails, GetItemDetails, GetLinkTargetDetails>::e if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb->onFolder({ r.raw.itemName, &linkInfo })) //throw X scheduler_.run<GetDirDetails>({ GetDirDetails(r.raw.itemPath), TravContext{ Zstring() /*errorItemName*/, 0 /*errorRetryCount*/, std::move(cbSub) }}); } - else //a file or named pipe, ect. + else //a file or named pipe, etc. cb->onFile({ r.raw.itemName, r.target.fileSize, r.target.modTime, convertToAbstractFileId(r.target.fileId), &linkInfo }); //throw X } } @@ -376,8 +401,7 @@ private: { const Zstring& rootPathRhs = static_cast<const NativeFileSystem&>(afsRhs).rootPath_; - return CmpFilePath()(rootPath_ .c_str(), rootPath_ .size(), - rootPathRhs.c_str(), rootPathRhs.size()); + return compareLocalPath(rootPath_, rootPathRhs); } //---------------------------------------------------------------------------------------------------------------- @@ -481,8 +505,8 @@ private: //initComForThread() -> done on traverser worker threads std::vector<std::pair<Zstring, std::shared_ptr<TraverserCallback>>> initialWorkItems; - for (const auto& item : workload) - initialWorkItems.emplace_back(getNativePath(item.first), item.second); + for (const auto& [folderPath, cb] : workload) + initialWorkItems.emplace_back(getNativePath(folderPath), cb); traverseFolderRecursiveNative(initialWorkItems, parallelOps); //throw X } @@ -540,7 +564,7 @@ private: } //target existing: undefined behavior! (fail/overwrite/auto-rename) => Native will fail and give a clear error message - void renameItemForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget) const override //throw FileError, ErrorDifferentVolume + void moveAndRenameItemForSameAfsType(const AfsPath& afsPathSource, const AbstractPath& apTarget) const override //throw FileError, ErrorDifferentVolume { //perf test: detecting different volumes by path is ~30 times faster than having MoveFileEx fail with ERROR_NOT_SAME_DEVICE (6µs vs 190µs) //=> maybe we can even save some actual I/O in some cases? @@ -588,6 +612,7 @@ private: } + int getAccessTimeout() const override { return 0; } //returns "0" if no timeout in force //---------------------------------------------------------------------------------------------------------------- uint64_t getFreeDiskSpace(const AfsPath& afsPath) const override //throw FileError, returns 0 if not available diff --git a/FreeFileSync/Source/ui/batch_status_handler.cpp b/FreeFileSync/Source/ui/batch_status_handler.cpp index a4e45c50..8b52bacf 100755 --- a/FreeFileSync/Source/ui/batch_status_handler.cpp +++ b/FreeFileSync/Source/ui/batch_status_handler.cpp @@ -323,7 +323,8 @@ ProcessCallback::Response BatchStatusHandler::reportError(const std::wstring& ms if (retryNumber < automaticRetryCount_) { errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO); - delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); }); + delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L" " + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), + automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); }); return ProcessCallback::RETRY; } diff --git a/FreeFileSync/Source/ui/cfg_grid.cpp b/FreeFileSync/Source/ui/cfg_grid.cpp index 72acf4a6..db49bee4 100755 --- a/FreeFileSync/Source/ui/cfg_grid.cpp +++ b/FreeFileSync/Source/ui/cfg_grid.cpp @@ -32,12 +32,12 @@ std::vector<ConfigFileItem> ConfigView::get() const { std::map<int, ConfigFileItem, std::greater<>> itemsSorted; //sort by last use; put most recent items *first* (looks better in XML than reverted) - for (const auto& item : cfgList_) - itemsSorted.emplace(item.second.lastUseIndex, item.second.cfgItem); + for (const auto& [filePath, details] : cfgList_) + itemsSorted.emplace(details.lastUseIndex, details.cfgItem); std::vector<ConfigFileItem> cfgHistory; - for (const auto& item : itemsSorted) - cfgHistory.emplace_back(item.second); + for (const auto& [lastUseIndex, cfgItem] : itemsSorted) + cfgHistory.emplace_back(cfgItem); return cfgHistory; } @@ -70,8 +70,8 @@ void ConfigView::addCfgFiles(const std::vector<Zstring>& filePaths) { //determine highest "last use" index number of m_listBoxHistory int lastUseIndexMax = 0; - for (const auto& item : cfgList_) - lastUseIndexMax = std::max(lastUseIndexMax, item.second.lastUseIndex); + for (const auto& [filePath, details] : cfgList_) + lastUseIndexMax = std::max(lastUseIndexMax, details.lastUseIndex); for (const Zstring& filePath : filePaths) { @@ -84,14 +84,14 @@ void ConfigView::addCfgFiles(const std::vector<Zstring>& filePaths) std::tie(detail.name, detail.cfgType, detail.isLastRunCfg) = [&] { - if (equalFilePath(filePath, lastRunConfigPath_)) + if (equalLocalPath(filePath, lastRunConfigPath_)) return std::make_tuple(utfTo<Zstring>(L"<" + _("Last session") + L">"), Details::CFG_TYPE_GUI, true); const Zstring fileName = afterLast(filePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); - if (endsWith(fileName, Zstr(".ffs_gui"), CmpFilePath())) + if (endsWithAsciiNoCase(fileName, Zstr(".ffs_gui"))) return std::make_tuple(beforeLast(fileName, Zstr('.'), IF_MISSING_RETURN_NONE), Details::CFG_TYPE_GUI, false); - else if (endsWith(fileName, Zstr(".ffs_batch"), CmpFilePath())) + else if (endsWithAsciiNoCase(fileName, Zstr(".ffs_batch"))) return std::make_tuple(beforeLast(fileName, Zstr('.'), IF_MISSING_RETURN_NONE), Details::CFG_TYPE_BATCH, false); else return std::make_tuple(fileName, Details::CFG_TYPE_NONE, false); @@ -110,7 +110,7 @@ void ConfigView::addCfgFiles(const std::vector<Zstring>& filePaths) void ConfigView::removeItems(const std::vector<Zstring>& filePaths) { - const std::set<Zstring, LessFilePath> pathsSorted(filePaths.begin(), filePaths.end()); + const std::set<Zstring, LessLocalPath> pathsSorted(filePaths.begin(), filePaths.end()); erase_if(cfgListView_, [&](auto it) { return pathsSorted.find(it->first) != pathsSorted.end(); }); @@ -581,7 +581,7 @@ void cfggrid::addAndSelect(Grid& grid, const std::vector<Zstring>& filePaths, bo grid.clearSelection(GridEventPolicy::DENY); - const std::set<Zstring, LessFilePath> pathsSorted(filePaths.begin(), filePaths.end()); + const std::set<Zstring, LessLocalPath> pathsSorted(filePaths.begin(), filePaths.end()); std::optional<size_t> selectionTopRow; for (size_t i = 0; i < grid.getRowCount(); ++i) diff --git a/FreeFileSync/Source/ui/cfg_grid.h b/FreeFileSync/Source/ui/cfg_grid.h index 37947867..66cd4dd8 100755 --- a/FreeFileSync/Source/ui/cfg_grid.h +++ b/FreeFileSync/Source/ui/cfg_grid.h @@ -134,7 +134,7 @@ private: const Zstring lastRunConfigPath_ = getLastRunConfigPath(); //let's not use another static... - using CfgFileList = std::map<Zstring /*file path*/, Details, LessFilePath>; + using CfgFileList = std::map<Zstring /*file path*/, Details, LessLocalPath>; CfgFileList cfgList_; std::vector<CfgFileList::iterator> cfgListView_; //sorted view on cfgList_ diff --git a/FreeFileSync/Source/ui/command_box.cpp b/FreeFileSync/Source/ui/command_box.cpp index 81a0aa6f..9d8c7a9b 100755 --- a/FreeFileSync/Source/ui/command_box.cpp +++ b/FreeFileSync/Source/ui/command_box.cpp @@ -22,7 +22,7 @@ inline std::wstring getSeparationLine() { return std::wstring(50, EM_DASH); } //no space between dashes! -std::vector<std::pair<std::wstring, Zstring>> getDefaultCommands() //(gui name/command) pairs +std::vector<std::pair<std::wstring, Zstring>> getDefaultCommands() //(description/command) pairs { return { @@ -63,21 +63,21 @@ CommandBox::CommandBox(wxWindow* parent, void CommandBox::addItemHistory() { - const Zstring command = trimCpy(getValue()); + const Zstring newCommand = trimCpy(getValue()); - if (command == utfTo<Zstring>(getSeparationLine()) || //do not add sep. line - command.empty()) + if (newCommand == utfTo<Zstring>(getSeparationLine()) || //do not add sep. line + newCommand.empty()) return; //do not add built-in commands to history - for (const auto& item : defaultCommands_) - if (command == utfTo<Zstring>(item.first) || - equalFilePath(command, item.second)) + for (const auto& [description, cmd] : defaultCommands_) + if (newCommand == utfTo<Zstring>(description) || + equalNoCase(newCommand, cmd)) return; - erase_if(history_, [&](const Zstring& item) { return equalFilePath(command, item); }); + erase_if(history_, [&](const Zstring& item) { return equalNoCase(newCommand, item); }); - history_.insert(history_.begin(), command); + history_.insert(history_.begin(), newCommand); if (history_.size() > historyMax_) history_.resize(historyMax_); @@ -104,8 +104,8 @@ void CommandBox::setValueAndUpdateList(const std::wstring& value) std::deque<std::wstring> items; //1. built in commands - for (const auto& item : defaultCommands_) - items.push_back(item.first); + for (const auto& [description, cmd] : defaultCommands_) + items.push_back(description); //2. history elements auto histSorted = history_; @@ -154,9 +154,9 @@ void CommandBox::OnValidateSelection(wxCommandEvent& event) if (value == getSeparationLine()) return setValueAndUpdateList(std::wstring()); - for (const auto& item : defaultCommands_) - if (item.first == value) - return setValueAndUpdateList(utfTo<std::wstring>(item.second)); //replace GUI name by actual command string + for (const auto& [description, cmd] : defaultCommands_) + if (description == value) + return setValueAndUpdateList(utfTo<std::wstring>(cmd)); //replace GUI name by actual command string } diff --git a/FreeFileSync/Source/ui/file_grid.cpp b/FreeFileSync/Source/ui/file_grid.cpp index 1f634cd4..a1af13a3 100755 --- a/FreeFileSync/Source/ui/file_grid.cpp +++ b/FreeFileSync/Source/ui/file_grid.cpp @@ -1084,9 +1084,9 @@ private: case ColumnTypeCenter::CHECKBOX: break; case ColumnTypeCenter::CMP_CATEGORY: - return _("Category") + L" (F10)"; + return _("Category") + L" (F11)"; case ColumnTypeCenter::SYNC_ACTION: - return _("Action") + L" (F10)"; + return _("Action") + L" (F11)"; } return std::wstring(); } @@ -1531,7 +1531,7 @@ private: return mainWin.GetVirtualSize().GetWidth() > mainWin.GetClientSize().GetWidth(); //assuming Grid::updateWindowSizes() does its job well, this should suffice! //CAVEAT: if horizontal and vertical scrollbar are circular dependent from each other - //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, ect...) + //(h-scrollbar is shown due to v-scrollbar consuming horizontal width, etc...) //while in fact both are NOT needed, this special case results in a bogus need for scrollbars! //see https://sourceforge.net/tracker/?func=detail&aid=3514183&group_id=234430&atid=1093083 // => since we're outside the Grid abstraction, we should not duplicate code to handle this special case as it seems to be insignificant @@ -1636,8 +1636,8 @@ private: //last inserted items are processed first in icon buffer: std::vector<AbstractPath> newLoad; - for (const auto& item : prefetchLoad) - newLoad.push_back(item.second); + for (const auto& [priority, filePath] : prefetchLoad) + newLoad.push_back(filePath); provRight_.updateNewAndGetUnbufferedIcons(newLoad); provLeft_ .updateNewAndGetUnbufferedIcons(newLoad); diff --git a/FreeFileSync/Source/ui/file_view.cpp b/FreeFileSync/Source/ui/file_view.cpp index 1d602540..fdf23760 100755 --- a/FreeFileSync/Source/ui/file_view.cpp +++ b/FreeFileSync/Source/ui/file_view.cpp @@ -282,7 +282,7 @@ private: Test case: 690.000 item pairs, Windows 7 x64 (C:\ vs I:\) ---------------------- CmpNaturalSort: 850 ms - CmpFilePath: 233 ms + CmpLocalPath: 233 ms CmpAsciiNoCase: 189 ms No sorting: 30 ms */ @@ -293,7 +293,7 @@ private: for (ItemPair& item : itemList) output.push_back(&item); - std::sort(output.begin(), output.end(), [](const ItemPair* lhs, const ItemPair* rhs) { return LessNaturalSort()(lhs->getPairItemName(), rhs->getPairItemName()); }); + std::sort(output.begin(), output.end(), [](const ItemPair* lhs, const ItemPair* rhs) { return LessNaturalSort()(lhs->getItemNameAny(), rhs->getItemNameAny()); }); return output; } #endif diff --git a/FreeFileSync/Source/ui/folder_history_box.h b/FreeFileSync/Source/ui/folder_history_box.h index 0252d7cd..bcbe343b 100755 --- a/FreeFileSync/Source/ui/folder_history_box.h +++ b/FreeFileSync/Source/ui/folder_history_box.h @@ -41,7 +41,7 @@ public: const Zstring nameTmp = zen::trimCpy(folderPathPhrase); //insert new folder or put it to the front if already existing - zen::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalFilePath(item, nameTmp); }); + zen::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalNoCase(item, nameTmp); }); folderPathPhrases_.insert(folderPathPhrases_.begin(), nameTmp); @@ -49,7 +49,7 @@ public: folderPathPhrases_.resize(maxSize_); } - void delItem(const Zstring& folderPathPhrase) { zen::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalFilePath(item, folderPathPhrase); }); } + void delItem(const Zstring& folderPathPhrase) { zen::erase_if(folderPathPhrases_, [&](const Zstring& item) { return equalNoCase(item, folderPathPhrase); }); } private: size_t maxSize_ = 0; diff --git a/FreeFileSync/Source/ui/folder_selector.cpp b/FreeFileSync/Source/ui/folder_selector.cpp index 0d68ccad..c050994b 100755 --- a/FreeFileSync/Source/ui/folder_selector.cpp +++ b/FreeFileSync/Source/ui/folder_selector.cpp @@ -44,7 +44,7 @@ void setFolderPathPhrase(const Zstring& folderPathPhrase, FolderHistoryBox* comb if (staticText) { //change static box label only if there is a real difference to what is shown in wxTextCtrl anyway - staticText->SetLabel(equalFilePath(appendSeparator(trimCpy(folderPathPhrase)), appendSeparator(folderPathPhraseFmt)) ? + staticText->SetLabel(equalNoCase(appendSeparator(trimCpy(folderPathPhrase)), appendSeparator(folderPathPhraseFmt)) ? wxString(_("Drag && drop")) : utfTo<wxString>(folderPathPhraseFmt)); } } @@ -230,9 +230,7 @@ void FolderSelector::onSelectAltFolder(wxCommandEvent& event) Zstring FolderSelector::getPath() const { - Zstring path = utfTo<Zstring>(folderComboBox_.GetValue()); - - return path; + return utfTo<Zstring>(folderComboBox_.GetValue()); } diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index 00b3ff64..24c86623 100755 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -312,7 +312,7 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizer1771 = new wxBoxSizer( wxVERTICAL ); m_bpButtonSwapSides = new wxBitmapButton( m_panelTopCenter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( -1, -1 ), wxBU_AUTODRAW ); - m_bpButtonSwapSides->SetToolTip( _("Swap sides") ); + m_bpButtonSwapSides->SetToolTip( _("dummy") ); bSizer1771->Add( m_bpButtonSwapSides, 0, wxEXPAND, 5 ); @@ -859,6 +859,11 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const bSizerViewFilter->Add( 0, 0, 1, wxEXPAND, 5 ); + m_bpButtonViewFilterSave = new wxBitmapButton( m_panelViewFilter, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, wxBU_AUTODRAW ); + m_bpButtonViewFilterSave->SetToolTip( _("Save as default") ); + + bSizerViewFilter->Add( m_bpButtonViewFilterSave, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + m_staticTextSelectView = new wxStaticText( m_panelViewFilter, wxID_ANY, _("Select view:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticTextSelectView->Wrap( -1 ); bSizerViewFilter->Add( m_staticTextSelectView, 0, wxALL|wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); @@ -1169,35 +1174,21 @@ MainDialogGenerated::MainDialogGenerated( wxWindow* parent, wxWindowID id, const m_bpButtonShowLog->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnShowLog ), NULL, this ); m_bpButtonViewTypeSyncAction->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewType ), NULL, this ); m_bpButtonShowExcluded->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowExcluded->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); + m_bpButtonViewFilterSave->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnViewFilterSave ), NULL, this ); m_bpButtonShowDeleteLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowDeleteLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowUpdateLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowUpdateLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowCreateLeft->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowCreateLeft->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowLeftOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowLeftOnly->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowLeftNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowLeftNewer->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowEqual->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowEqual->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowDoNothing->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowDoNothing->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowDifferent->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowDifferent->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowRightNewer->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowRightNewer->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowRightOnly->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowRightOnly->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowCreateRight->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowCreateRight->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowUpdateRight->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowUpdateRight->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowDeleteRight->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowDeleteRight->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); m_bpButtonShowConflict->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( MainDialogGenerated::OnToggleViewButton ), NULL, this ); - m_bpButtonShowConflict->Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( MainDialogGenerated::OnViewButtonRightClick ), NULL, this ); } MainDialogGenerated::~MainDialogGenerated() @@ -2645,23 +2636,54 @@ CloudSetupDlgGenerated::CloudSetupDlgGenerated( wxWindow* parent, wxWindowID id, m_staticline581 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); bSizer185->Add( m_staticline581, 0, wxEXPAND, 5 ); - wxBoxSizer* bSizer217; - bSizer217 = new wxBoxSizer( wxHORIZONTAL ); + wxBoxSizer* bSizer269; + bSizer269 = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* bSizer270; + bSizer270 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapServerDir = new wxStaticBitmap( m_panel41, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer270->Add( m_bitmapServerDir, 0, wxALIGN_BOTTOM|wxTOP|wxBOTTOM|wxLEFT, 5 ); m_staticText1232 = new wxStaticText( m_panel41, wxID_ANY, _("Directory on server:"), wxDefaultPosition, wxDefaultSize, 0 ); m_staticText1232->Wrap( -1 ); - bSizer217->Add( m_staticText1232, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + bSizer270->Add( m_staticText1232, 1, wxALL|wxALIGN_BOTTOM, 5 ); + + m_staticline72 = new wxStaticLine( m_panel41, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_VERTICAL ); + bSizer270->Add( m_staticline72, 0, wxEXPAND|wxLEFT, 5 ); + + wxBoxSizer* bSizer271; + bSizer271 = new wxBoxSizer( wxHORIZONTAL ); + + m_staticTextTimeout = new wxStaticText( m_panel41, wxID_ANY, _("Access timeout (in seconds):"), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextTimeout->Wrap( -1 ); + bSizer271->Add( m_staticTextTimeout, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + + m_spinCtrlTimeout = new wxSpinCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize( -1, -1 ), wxSP_ARROW_KEYS, 1, 2000000000, 1 ); + bSizer271->Add( m_spinCtrlTimeout, 0, wxALL|wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer270->Add( bSizer271, 0, wxALL, 5 ); + + + bSizer269->Add( bSizer270, 0, wxEXPAND|wxLEFT, 5 ); + + wxBoxSizer* bSizer217; + bSizer217 = new wxBoxSizer( wxHORIZONTAL ); m_textCtrlServerPath = new wxTextCtrl( m_panel41, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0 ); - bSizer217->Add( m_textCtrlServerPath, 1, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 5 ); + bSizer217->Add( m_textCtrlServerPath, 1, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxLEFT, 5 ); m_buttonSelectFolder = new wxButton( m_panel41, wxID_ANY, _("Browse"), wxDefaultPosition, wxDefaultSize, 0 ); m_buttonSelectFolder->SetToolTip( _("Select a folder") ); - bSizer217->Add( m_buttonSelectFolder, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxRIGHT, 5 ); + bSizer217->Add( m_buttonSelectFolder, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT, 5 ); + + + bSizer269->Add( bSizer217, 0, wxEXPAND|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); - bSizer185->Add( bSizer217, 0, wxALL|wxEXPAND, 5 ); + bSizer185->Add( bSizer269, 0, wxEXPAND, 5 ); m_panel41->SetSizer( bSizer185 ); diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h index 757634e8..f0b05c75 100755 --- a/FreeFileSync/Source/ui/gui_generated.h +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -173,6 +173,7 @@ protected: wxStaticText* m_staticTextViewType; zen::ToggleButton* m_bpButtonViewTypeSyncAction; zen::ToggleButton* m_bpButtonShowExcluded; + wxBitmapButton* m_bpButtonViewFilterSave; wxStaticText* m_staticTextSelectView; zen::ToggleButton* m_bpButtonShowDeleteLeft; zen::ToggleButton* m_bpButtonShowUpdateLeft; @@ -244,7 +245,7 @@ protected: virtual void OnSearchGridEnter( wxCommandEvent& event ) { event.Skip(); } virtual void OnToggleViewType( wxCommandEvent& event ) { event.Skip(); } virtual void OnToggleViewButton( wxCommandEvent& event ) { event.Skip(); } - virtual void OnViewButtonRightClick( wxMouseEvent& event ) { event.Skip(); } + virtual void OnViewFilterSave( wxCommandEvent& event ) { event.Skip(); } public: @@ -578,7 +579,11 @@ protected: wxTextCtrl* m_textCtrlPasswordHidden; wxCheckBox* m_checkBoxShowPassword; wxStaticLine* m_staticline581; + wxStaticBitmap* m_bitmapServerDir; wxStaticText* m_staticText1232; + wxStaticLine* m_staticline72; + wxStaticText* m_staticTextTimeout; + wxSpinCtrl* m_spinCtrlTimeout; wxTextCtrl* m_textCtrlServerPath; wxButton* m_buttonSelectFolder; wxBoxSizer* bSizer255; diff --git a/FreeFileSync/Source/ui/gui_status_handler.cpp b/FreeFileSync/Source/ui/gui_status_handler.cpp index 53391ea3..0a52e18a 100755 --- a/FreeFileSync/Source/ui/gui_status_handler.cpp +++ b/FreeFileSync/Source/ui/gui_status_handler.cpp @@ -90,6 +90,7 @@ StatusHandlerTemporaryPanel::StatusHandlerTemporaryPanel(MainDialog& dlg, statusPanel.Show(); mainDlg_.auiMgr_.Update(); + mainDlg_.compareStatus_->getAsWindow()->Refresh(); //macOS: fix background corruption for the statistics boxes (call *after* wxAuiManager::Update() } mainDlg_.Update(); //don't wait until idle event! @@ -214,7 +215,8 @@ ProcessCallback::Response StatusHandlerTemporaryPanel::reportError(const std::ws if (retryNumber < automaticRetryCount_) { errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO); - delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); }); + delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L" " + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), + automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); }); return ProcessCallback::RETRY; } @@ -565,7 +567,8 @@ ProcessCallback::Response StatusHandlerFloatingDialog::reportError(const std::ws if (retryNumber < automaticRetryCount_) { errorLog_.logMsg(msg + L"\n-> " + _("Automatic retry"), MSG_TYPE_INFO); - delayAndCountDown(_("Automatic retry"), automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); }); + delayAndCountDown(_("Automatic retry") + (automaticRetryCount_ <= 1 ? L"" : L" " + numberTo<std::wstring>(retryNumber + 1) + L"/" + numberTo<std::wstring>(automaticRetryCount_)), + automaticRetryDelay_, [&](const std::wstring& statusMsg) { this->reportStatus(_("Error") + L": " + statusMsg); }); return ProcessCallback::RETRY; } diff --git a/FreeFileSync/Source/ui/gui_status_handler.h b/FreeFileSync/Source/ui/gui_status_handler.h index 768d4d2e..7ba3878b 100755 --- a/FreeFileSync/Source/ui/gui_status_handler.h +++ b/FreeFileSync/Source/ui/gui_status_handler.h @@ -53,7 +53,7 @@ private: //StatusHandlerFloatingDialog(SyncProgressDialog) will internally process Window messages! disable GUI controls to avoid unexpected callbacks! -class StatusHandlerFloatingDialog : public StatusHandler //throw AbortProcess +class StatusHandlerFloatingDialog : public StatusHandler { public: StatusHandlerFloatingDialog(wxFrame* parentDlg, @@ -65,7 +65,7 @@ public: const Zstring& soundFileSyncComplete, const Zstring& postSyncCommand, PostSyncCondition postSyncCondition, - bool& autoCloseDialog); + bool& autoCloseDialog); //noexcept! ~StatusHandlerFloatingDialog(); void initNewPhase (int itemsTotal, int64_t bytesTotal, Phase phaseID) override; // diff --git a/FreeFileSync/Source/ui/log_panel.cpp b/FreeFileSync/Source/ui/log_panel.cpp index 7f84a9ed..fc7810e0 100755 --- a/FreeFileSync/Source/ui/log_panel.cpp +++ b/FreeFileSync/Source/ui/log_panel.cpp @@ -81,7 +81,7 @@ public: return {}; } - void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, ect. see error_log.h + void updateView(int includedTypes) //MSG_TYPE_INFO | MSG_TYPE_WARNING, etc. see error_log.h { viewRef_.clear(); diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index 5d608955..1cb67b60 100755 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -32,7 +32,7 @@ #include "small_dlgs.h" #include "progress_indicator.h" #include "folder_pair.h" -#include "search.h" +#include "search_grid.h" #include "batch_config.h" #include "triple_splitter.h" #include "app_icon.h" @@ -79,8 +79,8 @@ bool acceptDialogFileDrop(const std::vector<Zstring>& shellItemPaths) return std::any_of(shellItemPaths.begin(), shellItemPaths.end(), [](const Zstring& shellItemPath) { const Zstring ext = getFileExtension(shellItemPath); - return strEqual(ext, Zstr("ffs_gui"), CmpFilePath()) || - strEqual(ext, Zstr("ffs_batch"), CmpFilePath()); + return equalAsciiNoCase(ext, Zstr("ffs_gui")) || + equalAsciiNoCase(ext, Zstr("ffs_batch")); }); } } @@ -403,6 +403,8 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, m_bpButtonHideSearch ->SetBitmapLabel(getResourceImage(L"close_panel")); m_bpButtonShowLog ->SetBitmapLabel(getResourceImage(L"log_file")); + m_bpButtonViewFilterSave->SetBitmapLabel(getResourceImage(L"file_save_sicon")); + m_textCtrlSearchTxt->SetMinSize(wxSize(fastFromDIP(220), -1)); initViewFilterButtons(); @@ -563,6 +565,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, m_bpButtonCmpConfig ->SetToolTip(replaceCpy(_("C&omparison settings"), L"&", L"") + L" (F6)"); // m_bpButtonSyncConfig->SetToolTip(replaceCpy(_("S&ynchronization settings"), L"&", L"") + L" (F8)"); // m_buttonSync ->SetToolTip(replaceCpy(_("Start &synchronization"), L"&", L"") + L" (F9)"); // + m_bpButtonSwapSides ->SetToolTip(_("Swap sides") + L" (F10)"); m_bpButtonCmpContext ->SetToolTip(m_bpButtonCmpConfig ->GetToolTipText()); m_bpButtonSyncContext->SetToolTip(m_bpButtonSyncConfig->GetToolTipText()); @@ -957,8 +960,8 @@ void MainDialog::setGlobalCfgOnInit(const XmlGlobalSettings& globalSettings) auiMgr_.LoadPerspective(globalSettings.gui.mainDlg.guiPerspectiveLast); //restore original captions - for (const auto& item : captionNameMap) - auiMgr_.GetPane(item.second).Caption(item.first); + for (const auto& [caption, name] : captionNameMap) + auiMgr_.GetPane(name).Caption(caption); //-------------------------------------------------------------------------------- //if MainDialog::onQueryEndSession() is called while comparison is active, this panel is saved and restored as "visible" @@ -1945,6 +1948,13 @@ void MainDialog::onLocalKeyEvent(wxKeyEvent& event) //process key events without //return; //-> swallow event! case WXK_F10: + { + wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); + m_bpButtonSwapSides->Command(dummy); //simulate click + } + return; //-> swallow event! + + case WXK_F11: setViewTypeSyncAction(!m_bpButtonViewTypeSyncAction->isActive()); return; //-> swallow event! @@ -2103,13 +2113,13 @@ void MainDialog::onTreeGridContext(GridClickEvent& event) const bool isFolder = dynamic_cast<const FolderPair*>(selection[0]) != nullptr; //by short name - Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getPairItemName(); + Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getItemNameAny(); if (isFolder) labelShort += FILE_NAME_SEPARATOR; submenu.addItem(utfTo<wxString>(labelShort), [this, &selection, include] { filterShortname(*selection[0], include); }); //by relative path - Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getPairRelativePath(); + Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getRelativePathAny(); if (isFolder) labelRel += FILE_NAME_SEPARATOR; submenu.addItem(utfTo<wxString>(labelRel), [this, &selection, include] { filterItems(selection, include); }); @@ -2231,20 +2241,20 @@ void MainDialog::onMainGridContextRim(bool leftSide) //by extension if (!isFolder) { - const Zstring extension = getFileExtension(selection[0]->getPairItemName()); + const Zstring extension = getFileExtension(selection[0]->getItemNameAny()); if (!extension.empty()) submenu.addItem(L"*." + utfTo<wxString>(extension), [this, extension, include] { filterExtension(extension, include); }); } //by short name - Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getPairItemName(); + Zstring labelShort = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + selection[0]->getItemNameAny(); if (isFolder) labelShort += FILE_NAME_SEPARATOR; submenu.addItem(utfTo<wxString>(labelShort), [this, &selection, include] { filterShortname(*selection[0], include); }); //by relative path - Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getPairRelativePath(); + Zstring labelRel = FILE_NAME_SEPARATOR + selection[0]->getRelativePathAny(); if (isFolder) labelRel += FILE_NAME_SEPARATOR; submenu.addItem(utfTo<wxString>(labelRel), [this, &selection, include] { filterItems(selection, include); }); @@ -2380,7 +2390,7 @@ void MainDialog::filterExtension(const Zstring& extension, bool include) void MainDialog::filterShortname(const FileSystemObject& fsObj, bool include) { - Zstring phrase = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + fsObj.getPairItemName(); + Zstring phrase = Zstring(Zstr("*")) + FILE_NAME_SEPARATOR + fsObj.getItemNameAny(); const bool isFolder = dynamic_cast<const FolderPair*>(&fsObj) != nullptr; if (isFolder) phrase += FILE_NAME_SEPARATOR; @@ -2402,7 +2412,7 @@ void MainDialog::filterItems(const std::vector<FileSystemObject*>& selection, bo phrase += Zstr("\n"); //#pragma warning(suppress: 6011) -> fsObj bound in this context! - phrase += FILE_NAME_SEPARATOR + fsObj->getPairRelativePath(); + phrase += FILE_NAME_SEPARATOR + fsObj->getRelativePathAny(); const bool isFolder = dynamic_cast<const FolderPair*>(fsObj) != nullptr; if (isFolder) @@ -2418,8 +2428,8 @@ void MainDialog::onGridLabelContextC(GridLabelClickEvent& event) ContextMenu menu; const bool actionView = m_bpButtonViewTypeSyncAction->isActive(); - menu.addRadio(_("Category") + (actionView ? L"\tF10" : L""), [&] { setViewTypeSyncAction(false); }, !actionView); - menu.addRadio(_("Action") + (!actionView ? L"\tF10" : L""), [&] { setViewTypeSyncAction(true ); }, actionView); + menu.addRadio(_("Category") + (actionView ? L"\tF11" : L""), [&] { setViewTypeSyncAction(false); }, !actionView); + menu.addRadio(_("Action") + (!actionView ? L"\tF11" : L""), [&] { setViewTypeSyncAction(true ); }, actionView); menu.popup(*this); } @@ -2723,7 +2733,7 @@ void MainDialog::cfgHistoryRemoveObsolete(const std::vector<Zstring>& filePaths) void MainDialog::updateUnsavedCfgStatus() { - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); const bool haveUnsavedCfg = lastSavedCfg_ != getConfig(); @@ -2770,7 +2780,7 @@ void MainDialog::updateUnsavedCfgStatus() void MainDialog::OnConfigSave(wxCommandEvent& event) { - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); //if we work on a single named configuration document: save directly if changed //else: always show file dialog @@ -2824,16 +2834,16 @@ bool MainDialog::trySaveConfig(const Zstring* guiFilename) //return true if save } else { - Zstring defaultFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstr("SyncSettings.ffs_gui"); + const Zstring defaultFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstr("SyncSettings.ffs_gui"); + auto defaultFolder = utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); + auto defaultFileName = utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)); + //attention: activeConfigFiles may be an imported *.ffs_batch file! We don't want to overwrite it with a GUI config! - if (endsWith(defaultFilePath, Zstr(".ffs_batch"), CmpFilePath())) - defaultFilePath = beforeLast(defaultFilePath, Zstr("."), IF_MISSING_RETURN_NONE) + Zstr(".ffs_gui"); + defaultFileName = beforeLast(defaultFileName, L'.', IF_MISSING_RETURN_ALL) + L".ffs_gui"; wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! wxString(), - //OS X really needs dir/file separated like this: - utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)), //default dir - utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)), //default file + defaultFolder, defaultFileName, //OS X really needs dir/file separated like this wxString(L"FreeFileSync (*.ffs_gui)|*.ffs_gui") + L"|" +_("All files") + L" (*.*)|*", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (filePicker.ShowModal() != wxID_OK) @@ -2863,7 +2873,7 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchFileToUpdate) { //essentially behave like trySaveConfig(): the collateral damage of not saving GUI-only settings "m_bpButtonViewTypeSyncAction" is negligible - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); //prepare batch config: reuse existing batch-specific settings from file if available BatchExclusiveConfig batchExCfg; @@ -2907,16 +2917,16 @@ bool MainDialog::trySaveBatchConfig(const Zstring* batchFileToUpdate) return false; updateUnsavedCfgStatus(); //nothing else to update on GUI! - Zstring defaultFilePath = !activeCfgFilePath.empty() ? activeCfgFilePath : Zstr("BatchRun.ffs_batch"); + const Zstring defaultFilePath = !activeCfgFilePath.empty() ? activeCfgFilePath : Zstr("BatchRun.ffs_batch"); + auto defaultFolder = utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); + auto defaultFileName = utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)); + //attention: activeConfigFiles may be a *.ffs_gui file! We don't want to overwrite it with a BATCH config! - if (endsWith(defaultFilePath, Zstr(".ffs_gui"), CmpFilePath())) - defaultFilePath = beforeLast(defaultFilePath, Zstr("."), IF_MISSING_RETURN_NONE) + Zstr(".ffs_batch"); + defaultFileName = beforeLast(defaultFileName, L'.', IF_MISSING_RETURN_ALL) + L".ffs_batch"; wxFileDialog filePicker(this, //put modal dialog on stack: creating this on freestore leads to memleak! wxString(), - //OS X really needs dir/file separated like this: - utfTo<wxString>(beforeLast(defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)), //default dir - utfTo<wxString>(afterLast (defaultFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL)), //default file + defaultFolder, defaultFileName, //OS X really needs dir/file separated like this _("FreeFileSync batch") + L" (*.ffs_batch)|*.ffs_batch" + L"|" +_("All files") + L" (*.*)|*", wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if (filePicker.ShowModal() != wxID_OK) @@ -2947,7 +2957,7 @@ bool MainDialog::saveOldConfig() //return false on user abort { if (lastSavedCfg_ != getConfig()) { - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); //notify user about changed settings if (globalCfg_.confirmDlgs.popupOnConfigChange) @@ -3005,7 +3015,7 @@ bool MainDialog::saveOldConfig() //return false on user abort void MainDialog::OnConfigLoad(wxCommandEvent& event) { - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); wxFileDialog filePicker(this, wxString(), @@ -3594,7 +3604,7 @@ void MainDialog::setViewFilterDefault() } -void MainDialog::OnViewButtonRightClick(wxMouseEvent& event) +void MainDialog::OnViewFilterSave(wxCommandEvent& event) { auto setButtonDefault = [](const ToggleButton* tb, bool& defaultValue) { @@ -3690,7 +3700,6 @@ void MainDialog::OnCompare(wxCommandEvent& event) globalCfg_.fileTimeTolerance, true, //allowUserInteraction globalCfg_.runWithBackgroundPriority, - globalCfg_.folderAccessTimeout, globalCfg_.createLockFile, dirLocks, extractCompareCfg(guiCfg.mainCfg), @@ -3850,7 +3859,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) wxCommandEvent dummy(wxEVT_COMMAND_BUTTON_CLICKED); m_buttonCompare->Command(dummy); //simulate click - if (folderCmp_.empty()) //check if user aborted or error occurred, ect... + if (folderCmp_.empty()) //check if user aborted or error occurred, etc... return; } @@ -3876,7 +3885,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) for (const ConfigFileItem& item : cfggrid::getDataView(*m_gridCfgHistory).get()) logFilePathsToKeep.insert(item.logFilePath); - const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalFilePath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); + const Zstring activeCfgFilePath = activeConfigFiles_.size() == 1 && !equalLocalPath(activeConfigFiles_[0], lastRunConfigPath_) ? activeConfigFiles_[0] : Zstring(); const std::chrono::system_clock::time_point syncStartTime = std::chrono::system_clock::now(); bool exitAfterSync = false; @@ -3886,7 +3895,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) //run this->enableAllElements() BEFORE "exitAfterSync" buf AFTER StatusHandlerFloatingDialog::reportFinalStatus() //class handling status updates and error messages - StatusHandlerFloatingDialog statusHandler(this, //throw AbortProcess + StatusHandlerFloatingDialog statusHandler(this, syncStartTime, guiCfg.mainCfg.ignoreErrors, guiCfg.mainCfg.automaticRetryCount, @@ -3906,7 +3915,7 @@ void MainDialog::OnStartSync(wxCommandEvent& event) //wxBusyCursor dummy; -> redundant: progress already shown in progress dialog! - //GUI mode: place directory locks on directories isolated(!) during both comparison and synchronization + //GUI mode: end directory lock lifetime after comparion and start new locking right before sync std::unique_ptr<LockHolder> dirLocks; if (globalCfg_.createLockFile) { @@ -3931,7 +3940,6 @@ void MainDialog::OnStartSync(wxCommandEvent& event) globalCfg_.copyFilePermissions, globalCfg_.failSafeFileCopy, globalCfg_.runWithBackgroundPriority, - globalCfg_.folderAccessTimeout, extractSyncCfg(guiCfg.mainCfg), folderCmp_, deviceParallelOps, @@ -4099,7 +4107,9 @@ void MainDialog::showLogPanel(bool show) } logPane.Hide(); } + auiMgr_.Update(); + m_panelLog->Refresh(); //macOS: fix background corruption for the statistics boxes (call *after* wxAuiManager::Update() } @@ -4329,6 +4339,7 @@ void MainDialog::updateGridViewData() m_staticTextViewType ->Show(anyViewButtonShown); m_bpButtonViewTypeSyncAction->Show(anyViewButtonShown); m_staticTextSelectView ->Show(anySelectViewButtonShown); + m_bpButtonViewFilterSave ->Show(anySelectViewButtonShown); m_panelViewFilter->Layout(); @@ -4454,8 +4465,7 @@ void MainDialog::hideFindPanel() void MainDialog::startFindNext(bool searchAscending) //F3 or ENTER in m_textCtrlSearchTxt { - Zstring searchString = utfTo<Zstring>(trimCpy(m_textCtrlSearchTxt->GetValue())); - + const std::wstring& searchString = utfTo<std::wstring>(trimCpy(m_textCtrlSearchTxt->GetValue())); if (searchString.empty()) showFindPanel(); @@ -4871,7 +4881,7 @@ void MainDialog::OnMenuExportFileList(wxCommandEvent& event) std::string&& tmp = utfTo<std::string>(val); if (contains(tmp, CSV_SEP)) - return '\"' + tmp + '\"'; + return '"' + tmp + '"'; else return std::move(tmp); }; @@ -5080,7 +5090,7 @@ void MainDialog::setViewTypeSyncAction(bool value) //if (m_bpButtonViewTypeSyncAction->isActive() == value) return; support polling -> what about initialization? m_bpButtonViewTypeSyncAction->setActive(value); - m_bpButtonViewTypeSyncAction->SetToolTip((value ? _("Action") : _("Category")) + L" (F10)"); + m_bpButtonViewTypeSyncAction->SetToolTip((value ? _("Action") : _("Category")) + L" (F11)"); //toggle display of sync preview in middle grid filegrid::highlightSyncAction(*m_gridMainC, value); diff --git a/FreeFileSync/Source/ui/main_dlg.h b/FreeFileSync/Source/ui/main_dlg.h index 70a5fdb7..dbb0d136 100755 --- a/FreeFileSync/Source/ui/main_dlg.h +++ b/FreeFileSync/Source/ui/main_dlg.h @@ -153,7 +153,7 @@ private: void OnSyncSettingsContext(wxEvent& event); void OnGlobalFilterContext(wxEvent& event); - void OnViewButtonRightClick(wxMouseEvent& event) override; + void OnViewFilterSave(wxCommandEvent& event) override; void applyCompareConfig(bool setDefaultViewType); diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp index 6737996c..a2cfa0fa 100755 --- a/FreeFileSync/Source/ui/progress_indicator.cpp +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -325,7 +325,7 @@ void CompareProgressDialog::Impl::updateProgressGui() warn_static("harmonize phase handling!") - //write status information to taskbar, parent title ect. + //write status information to taskbar, parent title etc. switch (syncStat_->currentPhase()) { case ProcessCallback::PHASE_NONE: @@ -922,12 +922,13 @@ SyncProgressDialogImpl<TopLevelDialog>::~SyncProgressDialogImpl() parentFrame_->Disconnect(wxEVT_CHAR_HOOK, wxKeyEventHandler(SyncProgressDialogImpl::onParentKeyEvent), nullptr, this); parentFrame_->SetTitle(parentTitleBackup_); //restore title text - + //make sure main dialog is shown again if still "minimized to systray"! see SyncProgressDialog::closeDirectly() parentFrame_->Show(); //if (parentFrame_->IsIconized()) //caveat: if window is maximized calling Iconize(false) will erroneously un-maximize! // parentFrame_->Iconize(false); } + //else: don't call "TransformProcessType": consider "switch to main dialog" option during silent batch run //our client is NOT expecting a second call via notifyWindowTerminate_()! } @@ -1014,14 +1015,14 @@ template <class TopLevelDialog> void SyncProgressDialogImpl<TopLevelDialog>::setExternalStatus(const wxString& status, const wxString& progress) //progress may be empty! { //sys tray: order "top-down": jobname, status, progress - wxString systrayTooltip = jobName_.empty() ? status : L"\"" + jobName_ + L"\"\n" + status; + wxString systrayTooltip = jobName_.empty() ? status : L'"' + jobName_ + L"\"\n" + status; if (!progress.empty()) systrayTooltip += L" " + progress; //window caption/taskbar; inverse order: progress, status, jobname wxString title = progress.empty() ? status : progress + SPACED_DASH + status; if (!jobName_.empty()) - title += wxString(SPACED_DASH) + L"\"" + jobName_ + L"\""; + title += wxString(SPACED_DASH) + L'"' + jobName_ + L'"'; //systray tooltip, if window is minimized if (trayIcon_.get()) diff --git a/FreeFileSync/Source/ui/search_grid.cpp b/FreeFileSync/Source/ui/search_grid.cpp new file mode 100755 index 00000000..50ee3ab0 --- /dev/null +++ b/FreeFileSync/Source/ui/search_grid.cpp @@ -0,0 +1,135 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#include "search_grid.h" +#include <zen/zstring.h> +#include <zen/utf.h> +#include <zen/perf.h> + +using namespace zen; +using namespace fff; + + +namespace +{ +inline std::wstring getUnicodeNormalFormWide(const std::wstring& str) { return utfTo<std::wstring>(getUnicodeNormalForm(utfTo<Zstring>(str))); } +inline std::wstring makeUpperCopyWide (const std::wstring& str) { return utfTo<std::wstring>(makeUpperCopy (utfTo<Zstring>(str))); } + + +template <bool respectCase> +class MatchFound +{ +public: + MatchFound(const std::wstring& textToFind) : textToFind_(getUnicodeNormalFormWide(textToFind)) {} + bool operator()(const std::wstring& phrase) const + { + if (isAsciiString(phrase.c_str())) //perf: save Zstring conversion for getUnicodeNormalFormWide() when not needed + return contains(phrase, textToFind_); + else + return contains(getUnicodeNormalFormWide(phrase), textToFind_); + } + +private: + const std::wstring textToFind_; +}; + + +template <> +class MatchFound<false> +{ +public: + MatchFound(const std::wstring& textToFind) : textToFind_(makeUpperCopyWide(textToFind)) {} + bool operator()(std::wstring&& phrase) const + { + if (isAsciiString(phrase.c_str())) //perf: save Zstring conversion for makeUpperCopyWide() when not needed + { + for (wchar_t& c : phrase) c = asciiToUpper(c); + return contains(phrase, textToFind_); + } + else + return contains(makeUpperCopyWide(phrase), textToFind_); //getUnicodeNormalForm() is implied by makeUpperCopy() + } + +private: + const std::wstring textToFind_; +}; + +//########################################################################################### + +template <bool respectCase> +ptrdiff_t findRow(const Grid& grid, //return -1 if no matching row found + const std::wstring& searchString, + bool searchAscending, + size_t rowFirst, //specify area to search: + size_t rowLast) // [rowFirst, rowLast) +{ + if (auto prov = grid.getDataProvider()) + { + std::vector<Grid::ColAttributes> colAttr = grid.getColumnConfig(); + erase_if(colAttr, [](const Grid::ColAttributes& ca) { return !ca.visible; }); + if (!colAttr.empty()) + { + const MatchFound<respectCase> matchFound(searchString); + + if (searchAscending) + { + for (size_t row = rowFirst; row < rowLast; ++row) + for (const Grid::ColAttributes& ca : colAttr) + if (matchFound(prov->getValue(row, ca.type))) + return row; + } + else + for (size_t row = rowLast; row-- > rowFirst;) + for (const Grid::ColAttributes& ca : colAttr) + if (matchFound(prov->getValue(row, ca.type))) + return row; + } + } + return -1; +} +} + + +std::pair<const Grid*, ptrdiff_t> fff::findGridMatch(const Grid& grid1, const Grid& grid2, const std::wstring& searchString, bool respectCase, bool searchAscending) +{ + //PERF_START + + const size_t rowCount1 = grid1.getRowCount(); + const size_t rowCount2 = grid2.getRowCount(); + + size_t cursorRow1 = grid1.getGridCursor(); + if (cursorRow1 >= rowCount1) + cursorRow1 = 0; + + std::pair<const Grid*, ptrdiff_t> result(nullptr, -1); + + auto finishSearch = [&](const Grid& grid, size_t rowFirst, size_t rowLast) + { + const ptrdiff_t targetRow = respectCase ? + findRow<true >(grid, searchString, searchAscending, rowFirst, rowLast) : + findRow<false>(grid, searchString, searchAscending, rowFirst, rowLast); + if (targetRow >= 0) + { + result = { &grid, targetRow }; + return true; + } + return false; + }; + + if (searchAscending) + { + if (!finishSearch(grid1, cursorRow1 + 1, rowCount1)) + if (!finishSearch(grid2, 0, rowCount2)) + finishSearch(grid1, 0, cursorRow1 + 1); + } + else + { + if (!finishSearch(grid1, 0, cursorRow1)) + if (!finishSearch(grid2, 0, rowCount2)) + finishSearch(grid1, cursorRow1, rowCount1); + } + return result; +} diff --git a/FreeFileSync/Source/ui/search_grid.h b/FreeFileSync/Source/ui/search_grid.h new file mode 100755 index 00000000..81560f7c --- /dev/null +++ b/FreeFileSync/Source/ui/search_grid.h @@ -0,0 +1,19 @@ +// ***************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * +// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * +// ***************************************************************************** + +#ifndef SEARCH_H_423905762345342526587 +#define SEARCH_H_423905762345342526587 + +#include <wx+/grid.h> + + +namespace fff +{ +std::pair<const zen::Grid*, ptrdiff_t> findGridMatch(const zen::Grid& grid1, const zen::Grid& grid2, const std::wstring& searchString, bool respectCase, bool searchAscending); +//returns (grid/row) where the value was found, (nullptr, -1) if not found +} + +#endif //SEARCH_H_423905762345342526587 diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 08eefc02..7b71fe1e 100755 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -31,6 +31,7 @@ #include "../base/hard_filter.h" #include "../base/status_handler.h" //updateUiIsAllowed() #include "../base/generate_logfile.h" +#include "../base/icon_buffer.h" #include "../version/version.h" diff --git a/FreeFileSync/Source/ui/sorting.h b/FreeFileSync/Source/ui/sorting.h index 04bdc743..e9432907 100755 --- a/FreeFileSync/Source/ui/sorting.h +++ b/FreeFileSync/Source/ui/sorting.h @@ -76,17 +76,16 @@ bool lessRelativeFolder(const FileSystemObject& a, const FileSystemObject& b) { const bool isDirectoryA = isDirectoryPair(a); const Zstring& relFolderA = isDirectoryA ? - a.getPairRelativePath() : - a.parent().getPairRelativePath(); + a.getRelativePathAny() : + a.parent().getRelativePathAny(); const bool isDirectoryB = isDirectoryPair(b); const Zstring& relFolderB = isDirectoryB ? - b.getPairRelativePath() : - b.parent().getPairRelativePath(); + b.getRelativePathAny() : + b.parent().getRelativePathAny(); //compare relative names without filepaths first - const int rv = CmpNaturalSort()(relFolderA.c_str(), relFolderA.size(), - relFolderB.c_str(), relFolderB.size()); + const int rv = compareNatural(relFolderA, relFolderB); if (rv != 0) return zen::makeSortDirection(std::less<int>(), std::bool_constant<ascending>())(rv, 0); @@ -96,7 +95,7 @@ bool lessRelativeFolder(const FileSystemObject& a, const FileSystemObject& b) else if (isDirectoryA) return true; - return zen::makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(a.getPairItemName(), b.getPairItemName()); + return zen::makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(a.getItemNameAny(), b.getItemNameAny()); } diff --git a/FreeFileSync/Source/ui/sync_cfg.cpp b/FreeFileSync/Source/ui/sync_cfg.cpp index f5bb6399..e3b6a7bc 100755 --- a/FreeFileSync/Source/ui/sync_cfg.cpp +++ b/FreeFileSync/Source/ui/sync_cfg.cpp @@ -687,9 +687,8 @@ void ConfigDialog::onFilterKeyEvent(wxKeyEvent& event) FilterConfig ConfigDialog::getFilterConfig() const { - Zstring includeFilter = utfTo<Zstring>(m_textCtrlInclude->GetValue()); - Zstring exludeFilter = utfTo<Zstring>(m_textCtrlExclude->GetValue()); - + const Zstring& includeFilter = utfTo<Zstring>(m_textCtrlInclude->GetValue()); + const Zstring& exludeFilter = utfTo<Zstring>(m_textCtrlExclude->GetValue()); return FilterConfig(includeFilter, exludeFilter, m_spinCtrlTimespan->GetValue(), diff --git a/FreeFileSync/Source/ui/tree_grid.cpp b/FreeFileSync/Source/ui/tree_grid.cpp index 268cb6e4..bf3833b5 100755 --- a/FreeFileSync/Source/ui/tree_grid.cpp +++ b/FreeFileSync/Source/ui/tree_grid.cpp @@ -132,16 +132,16 @@ void calcPercentage(std::vector<std::pair<uint64_t, int*>>& workList) if (total == 0U) //this case doesn't work with the error minimizing algorithm below { - for (auto& pair : workList) - *pair.second = 0; + for (auto& [bytes, percent] : workList) + *percent = 0; return; } int remainingPercent = 100; - for (auto& pair : workList) + for (auto& [bytes, percent] : workList) { - *pair.second = static_cast<int>(pair.first * 100U / total); //round down - remainingPercent -= *pair.second; + *percent = static_cast<int>(bytes * 100U / total); //round down + remainingPercent -= *percent; } assert(remainingPercent >= 0); assert(remainingPercent < static_cast<int>(workList.size())); @@ -193,7 +193,7 @@ struct TreeView::LessShortName else if (!folderR) return true; - return makeSortDirection(LessNaturalSort() /*even on Linux*/, std::bool_constant<ascending>())(folderL->getPairItemName(), folderR->getPairItemName()); + return makeSortDirection(LessNaturalSort(), std::bool_constant<ascending>())(folderL->getItemNameAny(), folderR->getItemNameAny()); } case TreeView::TYPE_FILES: @@ -755,7 +755,7 @@ private: if (const TreeView::RootNode* root = dynamic_cast<const TreeView::RootNode*>(node.get())) return root->displayName; else if (const TreeView::DirNode* dir = dynamic_cast<const TreeView::DirNode*>(node.get())) - return utfTo<std::wstring>(dir->folder.getPairItemName()); + return utfTo<std::wstring>(dir->folder.getItemNameAny()); else if (dynamic_cast<const TreeView::FilesNode*>(node.get())) return _("Files"); break; diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index 0c47fff0..0a978b6c 100755 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "10.4"; //internal linkage! +const char ffsVersion[] = "10.5"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } diff --git a/wx+/async_task.h b/wx+/async_task.h index d1f30fec..df4c3ec6 100755 --- a/wx+/async_task.h +++ b/wx+/async_task.h @@ -95,7 +95,7 @@ public: return false; }); - for (auto& task : readyTasks) + for (std::unique_ptr<Task>& task : readyTasks) task->evaluateResult(); } } diff --git a/wx+/choice_enum.h b/wx+/choice_enum.h index 2c424b9f..f2c93927 100755 --- a/wx+/choice_enum.h +++ b/wx+/choice_enum.h @@ -103,11 +103,11 @@ Enum getEnumVal(const EnumDescrList<Enum>& mapping, const wxChoice& ctrl) template <class Enum> void updateTooltipEnumVal(const EnumDescrList<Enum>& mapping, wxChoice& ctrl) { - const Enum value = getEnumVal(mapping, ctrl); + const Enum currentValue = getEnumVal(mapping, ctrl); - for (const auto& item : mapping.descrList) - if (item.first == value) - ctrl.SetToolTip(item.second.second); + for (const auto& [enumValue, textAndTooltip] : mapping.descrList) + if (currentValue == enumValue) + ctrl.SetToolTip(textAndTooltip.second); } } diff --git a/wx+/context_menu.h b/wx+/context_menu.h index 7f459aca..d856db03 100755 --- a/wx+/context_menu.h +++ b/wx+/context_menu.h @@ -72,8 +72,8 @@ public: void popup(wxWindow& wnd, const wxPoint& pos = wxDefaultPosition) //show popup menu + process lambdas { //eventually all events from submenu items will be received by this menu - for (const auto& item : commandList_) - menu_->Connect(item.first, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(ContextMenu::onSelection), new GenericCommand(item.second) /*pass ownership*/, this); + for (const auto& [itemId, command] : commandList_) + menu_->Connect(itemId, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(ContextMenu::onSelection), new GenericCommand(command) /*pass ownership*/, this); wnd.PopupMenu(menu_.get(), pos); wxTheApp->ProcessPendingEvents(); //make sure lambdas are evaluated before going out of scope; @@ -8,6 +8,7 @@ #define DC_H_4987123956832143243214 #include <unordered_map> +#include <optional> #include <zen/basic_math.h> #include <wx/dcbuffer.h> //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER #include <wx/dcscreen.h> diff --git a/wx+/graph.cpp b/wx+/graph.cpp index 38846523..e00bed86 100755 --- a/wx+/graph.cpp +++ b/wx+/graph.cpp @@ -846,8 +846,8 @@ void Graph2D::render(wxDC& dc) const } //5. draw corner texts - for (const auto& ct : attr_.cornerTexts) - drawCornerText(dc, graphArea, ct.second, ct.first, attr_.backgroundColor); + for (const auto& [cornerPos, text] : attr_.cornerTexts) + drawCornerText(dc, graphArea, text, cornerPos, attr_.backgroundColor); } } } @@ -9,9 +9,11 @@ #include <memory> #include <numeric> +#include <optional> +#include <set> #include <vector> -#include <wx/scrolwin.h> #include <zen/basic_math.h> +#include <wx/scrolwin.h> //a user-friendly, extensible and high-performance grid control @@ -362,10 +364,18 @@ private: template <class ColAttrReal> std::vector<ColAttrReal> makeConsistent(const std::vector<ColAttrReal>& attribs, const std::vector<ColAttrReal>& defaults) { - std::vector<ColAttrReal> output = attribs; - //make sure each type is existing! - output.insert(output.end(), defaults.begin(), defaults.end()); - removeDuplicates(output, [](const ColAttrReal& lhs, const ColAttrReal& rhs) { return lhs.type < rhs.type; }); + using ColTypeReal = decltype(ColAttrReal().type); + std::vector<ColAttrReal> output; + + std::set<ColTypeReal> usedTypes; //remove duplicates + auto appendUnique = [&](const std::vector<ColAttrReal>& attr) + { + std::copy_if(attr.begin(), attr.end(), std::back_inserter(output), + [&](const ColAttrReal& a) { return usedTypes.insert(a.type).second; }); + }; + appendUnique(attribs); + appendUnique(defaults); //make sure each type is existing! + return output; } diff --git a/wx+/image_resources.cpp b/wx+/image_resources.cpp index d0449fe5..d09a188b 100755 --- a/wx+/image_resources.cpp +++ b/wx+/image_resources.cpp @@ -122,14 +122,12 @@ public: result_.access([&](std::vector<std::pair<std::wstring, ImageHolder>>& r) { - for (auto& item : r) + for (auto& [imageName, ih] : r) { - ImageHolder& ih = item.second; - wxImage img(ih.getWidth(), ih.getHeight(), ih.releaseRgb(), false /*static_data*/); //pass ownership img.SetAlpha(ih.releaseAlpha(), false /*static_data*/); - output.emplace(item.first, std::move(img)); + output.emplace(imageName, std::move(img)); } }); return output; @@ -171,7 +169,8 @@ class GlobalBitmaps public: static std::shared_ptr<GlobalBitmaps> instance() { - static Global<GlobalBitmaps> inst(std::make_unique<GlobalBitmaps>()); + static FunStatGlobal<GlobalBitmaps> inst; + inst.initOnce([] { return std::make_unique<GlobalBitmaps>(); }); assert(runningMainThread()); //wxWidgets is not thread-safe! return inst.get(); } @@ -179,7 +178,7 @@ public: GlobalBitmaps() {} ~GlobalBitmaps() { assert(bitmaps_.empty() && anims_.empty()); } //don't leave wxWidgets objects for static destruction! - void init(const Zstring& filepath); + void init(const Zstring& filePath); void cleanup() { bitmaps_.clear(); @@ -239,6 +238,8 @@ void GlobalBitmaps::init(const Zstring& filePath) } else if (endsWith(name, L".gif")) loadAnimFromZip(streamIn, anims_[name]); + else + assert(false); } } } diff --git a/wx+/image_tools.cpp b/wx+/image_tools.cpp index 4748a590..b314e801 100755 --- a/wx+/image_tools.cpp +++ b/wx+/image_tools.cpp @@ -141,10 +141,10 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const int maxWidth = 0; int lineHeight = 0; - for (const auto& li : lineInfo) + for (const auto& [lineText, lineSize] : lineInfo) { - maxWidth = std::max(maxWidth, li.second.GetWidth()); - lineHeight = std::max(lineHeight, li.second.GetHeight()); //wxWidgets comment "GetTextExtent will return 0 for empty string" + maxWidth = std::max(maxWidth, lineSize.GetWidth()); + lineHeight = std::max(lineHeight, lineSize.GetHeight()); //wxWidgets comment "GetTextExtent will return 0 for empty string" } if (maxWidth == 0 || lineHeight == 0) return wxImage(); @@ -160,19 +160,19 @@ wxImage zen::createImageFromText(const wxString& text, const wxFont& font, const dc.SetFont(font); int posY = 0; - for (const auto& li : lineInfo) + for (const auto& [lineText, lineSize] : lineInfo) { - if (!li.first.empty()) + if (!lineText.empty()) switch (textAlign) { case ImageStackAlignment::LEFT: - dc.DrawText(li.first, wxPoint(0, posY)); + dc.DrawText(lineText, wxPoint(0, posY)); break; case ImageStackAlignment::RIGHT: - dc.DrawText(li.first, wxPoint(maxWidth - li.second.GetWidth(), posY)); + dc.DrawText(lineText, wxPoint(maxWidth - lineSize.GetWidth(), posY)); break; case ImageStackAlignment::CENTER: - dc.DrawText(li.first, wxPoint((maxWidth - li.second.GetWidth()) / 2, posY)); + dc.DrawText(lineText, wxPoint((maxWidth - lineSize.GetWidth()) / 2, posY)); break; } diff --git a/wx+/zlib_wrap.cpp b/wx+/zlib_wrap.cpp index cb6e3083..fbbe2f09 100755 --- a/wx+/zlib_wrap.cpp +++ b/wx+/zlib_wrap.cpp @@ -5,9 +5,10 @@ // ***************************************************************************** #include "zlib_wrap.h" -//include the SAME zlib version that wxWidgets is linking against! - //#include <wx/../../../../../Source/src/zlib/zlib.h> //wxWidgets compiled with: --with-zlib=builtin - #include <zlib.h> //use same library as used by Curl (zlib is required for HTTP) +//Windows: use the SAME zlib version that wxWidgets is linking against! //C:\Data\Projects\wxWidgets\Source\src\zlib\zlib.h +//Linux/macOS: use zlib system header for both wxWidgets and Curl (zlib is required for HTTP) +// => don't compile wxWidgets with: --with-zlib=builtin +#include <zlib.h> using namespace zen; diff --git a/zen/file_access.cpp b/zen/file_access.cpp index a81fdae0..88b70b14 100755 --- a/zen/file_access.cpp +++ b/zen/file_access.cpp @@ -381,7 +381,7 @@ void zen::renameFile(const Zstring& pathSource, const Zstring& pathTarget) //thr const Zstring parentPathSrc = beforeLast(pathSource, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE); const Zstring parentPathTrg = beforeLast(pathTarget, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE); //some (broken) devices may fail to rename case directly: - if (equalFilePath(parentPathSrc, parentPathTrg)) + if (equalLocalPath(parentPathSrc, parentPathTrg)) { if (fileNameSrc == fileNameTrg) return; //non-sensical request diff --git a/zen/file_error.h b/zen/file_error.h index 086d0998..101d6543 100755 --- a/zen/file_error.h +++ b/zen/file_error.h @@ -45,7 +45,7 @@ DEFINE_NEW_FILE_ERROR(ErrorDifferentVolume); //----------- facilitate usage of std::wstring for error messages -------------------- -inline std::wstring fmtPath(const std::wstring& displayPath) { return L'\"' + displayPath + L'\"'; } +inline std::wstring fmtPath(const std::wstring& displayPath) { return L'"' + displayPath + L'"'; } inline std::wstring fmtPath(const Zstring& displayPath) { return fmtPath(utfTo<std::wstring>(displayPath)); } inline std::wstring fmtPath(const wchar_t* displayPath) { return fmtPath(std::wstring(displayPath)); } //resolve overload ambiguity } diff --git a/zen/file_traverser.cpp b/zen/file_traverser.cpp index e342c8ec..cc6e0c0b 100755 --- a/zen/file_traverser.cpp +++ b/zen/file_traverser.cpp @@ -78,7 +78,7 @@ void zen::traverseFolder(const Zstring& dirPath, if (onFolder) onFolder({ itemName, itemPath }); } - else //a file or named pipe, ect. + else //a file or named pipe, etc. { if (onFile) onFile({ itemName, itemPath, makeUnsigned(statData.st_size), statData.st_mtime }); diff --git a/zen/globals.h b/zen/globals.h index 10975414..024147fa 100755 --- a/zen/globals.h +++ b/zen/globals.h @@ -14,14 +14,22 @@ namespace zen { -//solve static destruction order fiasco by providing shared ownership and serialized access to global variables +/* +Solve static destruction order fiasco by providing shared ownership and serialized access to global variables + +=>there may be accesses to "Global<T>::get()" during process shutdown e.g. _("") used by message in debug_minidump.cpp or by some detached thread assembling an error message! +=> use trivially-destructible POD only!!! + +ATTENTION: function-static globals have the compiler generate "magic statics" == compiler-genenerated locking code which will crash or leak memory when accessed after global is "dead" + => "solved" by FunStatGlobal, but we can't have "too many" of these... +*/ template <class T> -class Global +class Global //don't use for function-scope statics! { public: Global() { - static_assert(std::is_trivially_destructible_v<Pod>, "this memory needs to live forever"); + static_assert(std::is_trivially_constructible_v<Pod>&& std::is_trivially_destructible_v<Pod>, "this memory needs to live forever"); assert(!pod_.inst && !pod_.spinLock); //we depend on static zero-initialization! } @@ -52,16 +60,131 @@ public: } private: - //avoid static destruction order fiasco: there may be accesses to "Global<T>::get()" during process shutdown - //e.g. _("") used by message in debug_minidump.cpp or by some detached thread assembling an error message! - //=> use trivially-destructible POD only!!! struct Pod { + std::atomic<bool> spinLock; // { false }; rely entirely on static zero-initialization! => avoid potential contention with worker thread during Global<> construction! + //serialize access; can't use std::mutex: has non-trival destructor std::shared_ptr<T>* inst; // = nullptr; + } pod_; +}; + +//=================================================================================================================== +//=================================================================================================================== + +struct CleanUpEntry +{ + using CleanUpFunction = void (*)(void* callbackData); + CleanUpFunction cleanUpFun; + void* callbackData; + CleanUpEntry* prev; +}; +void registerGlobalForDestruction(CleanUpEntry& entry); + + +template <class T> +class FunStatGlobal +{ +public: + //No FunStatGlobal() or ~FunStatGlobal()! + + std::shared_ptr<T> get() + { + static_assert(std::is_trivially_constructible_v<FunStatGlobal>&& + std::is_trivially_destructible_v<FunStatGlobal>, "this class must not generate code for magic statics!"); + + while (pod_.spinLock.exchange(true)) ; + ZEN_ON_SCOPE_EXIT(pod_.spinLock = false); + if (pod_.inst) + return *pod_.inst; + return nullptr; + } + + void set(std::unique_ptr<T>&& newInst) + { + std::shared_ptr<T>* tmpInst = nullptr; + if (newInst) + tmpInst = new std::shared_ptr<T>(std::move(newInst)); + { + while (pod_.spinLock.exchange(true)) ; + ZEN_ON_SCOPE_EXIT(pod_.spinLock = false); + + std::swap(pod_.inst, tmpInst); + registerDestruction(); + } + delete tmpInst; + } + + void initOnce(std::unique_ptr<T> (*getInitialValue)()) + { + while (pod_.spinLock.exchange(true)) ; + ZEN_ON_SCOPE_EXIT(pod_.spinLock = false); + + if (!pod_.cleanUpEntry.cleanUpFun) + { + assert(!pod_.inst); + if (std::unique_ptr<T> newInst = (*getInitialValue)()) + pod_.inst = new std::shared_ptr<T>(std::move(newInst)); + registerDestruction(); + } + } + +private: + //call while holding pod_.spinLock + void registerDestruction() + { + assert(pod_.spinLock); + + if (!pod_.cleanUpEntry.cleanUpFun) + { + pod_.cleanUpEntry.callbackData = this; + pod_.cleanUpEntry.cleanUpFun = [](void* callbackData) + { + auto thisPtr = static_cast<FunStatGlobal*>(callbackData); + thisPtr->set(nullptr); + }; + + registerGlobalForDestruction(pod_.cleanUpEntry); + } + } + + struct Pod + { std::atomic<bool> spinLock; // { false }; rely entirely on static zero-initialization! => avoid potential contention with worker thread during Global<> construction! //serialize access; can't use std::mutex: has non-trival destructor + std::shared_ptr<T>* inst; // = nullptr; + CleanUpEntry cleanUpEntry; } pod_; }; + + +inline +void registerGlobalForDestruction(CleanUpEntry& entry) +{ + static struct + { + std::atomic<bool> spinLock; + CleanUpEntry* head; + } cleanUpList; + + static_assert(std::is_trivially_constructible_v<decltype(cleanUpList)>&& + std::is_trivially_destructible_v<decltype(cleanUpList)>, "we must not generate code for magic statics!"); + + while (cleanUpList.spinLock.exchange(true)) ; + ZEN_ON_SCOPE_EXIT(cleanUpList.spinLock = false); + + std::atexit([] + { + while (cleanUpList.spinLock.exchange(true)) ; + ZEN_ON_SCOPE_EXIT(cleanUpList.spinLock = false); + + (*cleanUpList.head->cleanUpFun)(cleanUpList.head->callbackData); + cleanUpList.head = cleanUpList.head->prev; //nicely clean up in reverse order of construction + }); + + entry.prev = cleanUpList.head; + cleanUpList.head = &entry; + +} } #endif //GLOBALS_H_8013740213748021573485 @@ -9,6 +9,7 @@ #include <fcntl.h> //open #include <unistd.h> //close + #include <zen/sys_error.h> //#include <uuid/uuid.h> -> uuid_generate(), uuid_unparse(); avoid additional dependency for "sudo apt-get install uuid-dev" diff --git a/zen/http.cpp b/zen/http.cpp index d06d3309..1f89bf20 100755 --- a/zen/http.cpp +++ b/zen/http.cpp @@ -26,9 +26,9 @@ public: const bool useTls = [&] { - if (startsWith(url, Zstr("http://"), CmpAsciiNoCase())) + if (startsWithAsciiNoCase(url, Zstr("http://"))) return false; - if (startsWith(url, Zstr("https://"), CmpAsciiNoCase())) + if (startsWithAsciiNoCase(url, Zstr("https://"))) return true; throw SysError(L"URL uses unexpected protocol."); }(); @@ -57,35 +57,16 @@ public: //https://www.w3.org/Protocols/HTTP/1.0/spec.html#Request-Line std::string msg = (postParams ? "POST " : "GET ") + utfTo<std::string>(page) + " HTTP/1.0\r\n"; - for (const auto& item : headers) - msg += item.first + ": " + item.second + "\r\n"; + for (const auto& [name, value] : headers) + msg += name + ": " + value + "\r\n"; msg += "\r\n"; msg += postBuf; //send request for (size_t bytesToSend = msg.size(); bytesToSend > 0;) - { - int bytesSent = 0; - for (;;) - { - bytesSent = ::send(socket_->get(), //_In_ SOCKET s, - &*(msg.end() - bytesToSend), //_In_ const char *buf, - static_cast<int>(bytesToSend), //_In_ int len, - 0); //_In_ int flags - if (bytesSent >= 0 || errno != EINTR) - break; - } - if (bytesSent < 0) - THROW_LAST_SYS_ERROR_WSA(L"send"); - if (bytesSent > static_cast<int>(bytesToSend)) - throw SysError(L"send: buffer overflow."); - if (bytesSent == 0) - throw SysError(L"send: zero bytes processed"); - - bytesToSend -= bytesSent; - } - if (::shutdown(socket_->get(), SHUT_WR) != 0) - THROW_LAST_SYS_ERROR_WSA(L"shutdown"); + bytesToSend -= tryWriteSocket(socket_->get(), &*(msg.end() - bytesToSend), bytesToSend); //throw SysError + + shutdownSocketSend(socket_->get()); //throw SysError //receive response: std::string headBuf; @@ -116,7 +97,7 @@ public: const std::vector<std::string> statusItems = split(statusBuf, ' ', SplitType::ALLOW_EMPTY); //HTTP-Version SP Status-Code SP Reason-Phrase CRLF if (statusItems.size() < 2 || !startsWith(statusItems[0], "HTTP/")) - throw SysError(L"Invalid HTTP response: \"" + utfTo<std::wstring>(statusBuf) + L"\""); + throw SysError(L"Invalid HTTP response: \"" + utfTo<std::wstring>(statusBuf) + L'"'); statusCode_ = stringTo<int>(statusItems[1]); @@ -175,8 +156,6 @@ public: private: size_t tryRead(void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF! { - if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! - throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); assert(bytesToRead <= getBlockSize()); //block size might be 1000 while reading HTTP header if (contentRemaining_ >= 0) @@ -185,21 +164,7 @@ private: return 0; bytesToRead = static_cast<size_t>(std::min(static_cast<int64_t>(bytesToRead), contentRemaining_)); //[!] contentRemaining_ > 4 GB possible! } - int bytesReceived = 0; - for (;;) - { - bytesReceived = ::recv(socket_->get(), //_In_ SOCKET s, - static_cast<char*>(buffer), //_Out_ char *buf, - static_cast<int>(bytesToRead), //_In_ int len, - 0); //_In_ int flags - if (bytesReceived >= 0 || errno != EINTR) - break; - } - if (bytesReceived < 0) - THROW_LAST_SYS_ERROR_WSA(L"recv"); - if (static_cast<size_t>(bytesReceived) > bytesToRead) //better safe than sorry - throw SysError(L"HttpInputStream::tryRead: buffer overflow."); - + const size_t bytesReceived = tryReadSocket(socket_->get(), buffer, bytesToRead); //throw SysError; may return short, only 0 means EOF! if (contentRemaining_ >= 0) contentRemaining_ -= bytesReceived; @@ -325,8 +290,8 @@ std::string urldecode(const std::string& str) std::string zen::xWwwFormUrlEncode(const std::vector<std::pair<std::string, std::string>>& paramPairs) { std::string output; - for (const auto& pair : paramPairs) - output += urlencode(pair.first) + '=' + urlencode(pair.second) + '&'; + for (const auto& [name, value] : paramPairs) + output += urlencode(name) + '=' + urlencode(value) + '&'; //encode both key and value: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 if (!output.empty()) output.pop_back(); @@ -59,10 +59,10 @@ std::shared_ptr<const TranslationHandler> getTranslator(); namespace impl { inline -Global<const TranslationHandler>& refGlobalTranslationHandler() +FunStatGlobal<const TranslationHandler>& refGlobalTranslationHandler() { //getTranslator() may be called even after static objects of this translation unit are destroyed! - static Global<const TranslationHandler> inst; //external linkage even in header! + static FunStatGlobal<const TranslationHandler> inst; //external linkage even in header! return inst; } } diff --git a/zen/legacy_compiler.h b/zen/legacy_compiler.h index e9d50b97..16d87c53 100755 --- a/zen/legacy_compiler.h +++ b/zen/legacy_compiler.h @@ -7,14 +7,11 @@ #ifndef LEGACY_COMPILER_H_839567308565656789 #define LEGACY_COMPILER_H_839567308565656789 - #include <optional> - namespace std { //https://gcc.gnu.org/onlinedocs/libstdc++/manual/status.html //https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations - } #endif //LEGACY_COMPILER_H_839567308565656789 diff --git a/zen/scope_guard.h b/zen/scope_guard.h index d056bb2a..9eff6c1f 100755 --- a/zen/scope_guard.h +++ b/zen/scope_guard.h @@ -10,6 +10,7 @@ #include <cassert> #include <exception> #include "type_traits.h" +#include "legacy_compiler.h" //std::uncaught_exceptions //best of Zen, Loki and C++17 @@ -103,8 +104,8 @@ auto makeGuard(F&& fun) { return ScopeGuard<runMode, std::decay_t<F>>(std::forwa #define ZEN_CHECK_CASE_FOR_CONSTANT_IMPL(X) L ## X -#define ZEN_ON_SCOPE_EXIT(X) auto ZEN_CONCAT(dummy, __LINE__) = zen::makeGuard<zen::ScopeGuardRunMode::ON_EXIT >([&]{ X; }); (void)ZEN_CONCAT(dummy, __LINE__); -#define ZEN_ON_SCOPE_FAIL(X) auto ZEN_CONCAT(dummy, __LINE__) = zen::makeGuard<zen::ScopeGuardRunMode::ON_FAIL >([&]{ X; }); (void)ZEN_CONCAT(dummy, __LINE__); -#define ZEN_ON_SCOPE_SUCCESS(X) auto ZEN_CONCAT(dummy, __LINE__) = zen::makeGuard<zen::ScopeGuardRunMode::ON_SUCCESS>([&]{ X; }); (void)ZEN_CONCAT(dummy, __LINE__); +#define ZEN_ON_SCOPE_EXIT(X) auto ZEN_CONCAT(scopeGuard, __LINE__) = zen::makeGuard<zen::ScopeGuardRunMode::ON_EXIT >([&]{ X; }); (void)ZEN_CONCAT(scopeGuard, __LINE__); +#define ZEN_ON_SCOPE_FAIL(X) auto ZEN_CONCAT(scopeGuard, __LINE__) = zen::makeGuard<zen::ScopeGuardRunMode::ON_FAIL >([&]{ X; }); (void)ZEN_CONCAT(scopeGuard, __LINE__); +#define ZEN_ON_SCOPE_SUCCESS(X) auto ZEN_CONCAT(scopeGuard, __LINE__) = zen::makeGuard<zen::ScopeGuardRunMode::ON_SUCCESS>([&]{ X; }); (void)ZEN_CONCAT(scopeGuard, __LINE__); #endif //SCOPE_GUARD_H_8971632487321434 diff --git a/zen/shell_execute.h b/zen/shell_execute.h index 0802fbcb..98824d70 100755 --- a/zen/shell_execute.h +++ b/zen/shell_execute.h @@ -39,7 +39,7 @@ void shellExecute(const Zstring& command, ExecutionType type) //throw FileError if (type == ExecutionType::SYNC) { //Posix ::system() - execute a shell command - const int rv = ::system(command.c_str()); //do NOT use std::system as its documentation says nothing about "WEXITSTATUS(rv)", ect... + const int rv = ::system(command.c_str()); //do NOT use std::system as its documentation says nothing about "WEXITSTATUS(rv)", etc... if (rv == -1 || WEXITSTATUS(rv) == 127) throw FileError(_("Incorrect command line:") + L"\n" + utfTo<std::wstring>(command)); //http://linux.die.net/man/3/system "In case /bin/sh could not be executed, the exit status will be that of a command that does exit(127)" @@ -73,7 +73,7 @@ void shellExecute(const Zstring& command, ExecutionType type) //throw FileError inline void openWithDefaultApplication(const Zstring& itemPath) //throw FileError { - shellExecute("xdg-open \"" + itemPath + "\"", ExecutionType::ASYNC); // + shellExecute("xdg-open \"" + itemPath + '"', ExecutionType::ASYNC); // } } diff --git a/zen/socket.h b/zen/socket.h index e551a5ba..33ac2e50 100755 --- a/zen/socket.h +++ b/zen/socket.h @@ -20,6 +20,12 @@ namespace zen do { const ErrorCode ecInternal = getLastError(); throw SysError(formatSystemError(functionName, ecInternal)); } while (false) +//patch up socket portability: +using SocketType = int; +const SocketType invalidSocket = -1; +inline void closeSocket(SocketType s) { ::close(s); } + + //Winsock needs to be initialized before calling any of these functions! (WSAStartup/WSACleanup) class Socket //throw SysError @@ -67,18 +73,78 @@ public: ~Socket() { closeSocket(socket_); } - using SocketType = int; SocketType get() const { return socket_; } private: Socket (const Socket&) = delete; Socket& operator=(const Socket&) = delete; - static const SocketType invalidSocket = -1; - static void closeSocket(SocketType s) { ::close(s); } - SocketType socket_ = invalidSocket; }; + + +//more socket helper functions: +namespace +{ +size_t tryReadSocket(SocketType socket, void* buffer, size_t bytesToRead) //throw SysError; may return short, only 0 means EOF! +{ + if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check! + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); + + int bytesReceived = 0; + for (;;) + { + bytesReceived = ::recv(socket, //_In_ SOCKET s, + static_cast<char*>(buffer), //_Out_ char *buf, + static_cast<int>(bytesToRead), //_In_ int len, + 0); //_In_ int flags + if (bytesReceived >= 0 || errno != EINTR) + break; + } + if (bytesReceived < 0) + THROW_LAST_SYS_ERROR_WSA(L"recv"); + + if (static_cast<size_t>(bytesReceived) > bytesToRead) //better safe than sorry + throw SysError(L"HttpInputStream::tryRead: buffer overflow."); + + return bytesReceived; //"zero indicates end of file" +} + + +size_t tryWriteSocket(SocketType socket, const void* buffer, size_t bytesToWrite) //throw SysError; may return short! CONTRACT: bytesToWrite > 0 +{ + if (bytesToWrite == 0) + throw std::logic_error("Contract violation! " + std::string(__FILE__) + ":" + numberTo<std::string>(__LINE__)); + + int bytesWritten = 0; + for (;;) + { + bytesWritten = ::send(socket, //_In_ SOCKET s, + static_cast<const char*>(buffer), //_In_ const char *buf, + static_cast<int>(bytesToWrite), //_In_ int len, + 0); //_In_ int flags + if (bytesWritten >= 0 || errno != EINTR) + break; + } + if (bytesWritten < 0) + THROW_LAST_SYS_ERROR_WSA(L"send"); + if (bytesWritten > static_cast<int>(bytesToWrite)) + throw SysError(L"send: buffer overflow."); + if (bytesWritten == 0) + throw SysError(L"send: zero bytes processed"); + + return bytesWritten; +} +} + + +inline +void shutdownSocketSend(SocketType socket) //throw SysError +{ + if (::shutdown(socket, SHUT_WR) != 0) + THROW_LAST_SYS_ERROR_WSA(L"shutdown"); +} + } #endif //SOCKET_H_23498325972583947678456437 diff --git a/zen/stl_tools.h b/zen/stl_tools.h index be9bf710..c3a9bf8f 100755 --- a/zen/stl_tools.h +++ b/zen/stl_tools.h @@ -12,6 +12,7 @@ #include <vector> #include <memory> #include <algorithm> +#include <optional> #include "string_traits.h" #include "build_info.h" diff --git a/zen/string_tools.h b/zen/string_tools.h index 8746722a..657c70d5 100755 --- a/zen/string_tools.h +++ b/zen/string_tools.h @@ -26,32 +26,30 @@ template <class Char> bool isWhiteSpace(Char c); template <class Char> bool isDigit (Char c); //not exactly the same as "std::isdigit" -> we consider '0'-'9' only! template <class Char> bool isHexDigit (Char c); template <class Char> bool isAsciiAlpha(Char c); +template <class Char> bool isAsciiString(const Char* str); template <class Char> Char asciiToLower(Char c); template <class Char> Char asciiToUpper(Char c); -//case-sensitive comparison (compile-time correctness: use different number of arguments as STL comparison predicates!) -struct CmpBinary { template <class Char> int operator()(const Char* lhs, size_t lhsLen, const Char* rhs, size_t rhsLen) const; }; +//both S and T can be strings or char/wchar_t arrays or single char/wchar_t +template <class S, class T> bool contains(const S& str, const T& term); -//basic case-insensitive comparison (considering A-Z only!) -struct CmpAsciiNoCase { template <class Char> int operator()(const Char* lhs, size_t lhsLen, const Char* rhs, size_t rhsLen) const; }; +template <class S, class T> bool startsWith (const S& str, const T& prefix); +template <class S, class T> bool startsWithAsciiNoCase(const S& str, const T& prefix); -struct LessAsciiNoCase -{ - template <class S> //don't support heterogenous input! => use as container predicate only! - bool operator()(const S& lhs, const S& rhs) const { return CmpAsciiNoCase()(strBegin(lhs), strLength(lhs), strBegin(rhs), strLength(rhs)) < 0; } -}; +template <class S, class T> bool endsWith (const S& str, const T& postfix); +template <class S, class T> bool endsWithAsciiNoCase(const S& str, const T& postfix); -//both S and T can be strings or char/wchar_t arrays or simple char/wchar_t -template <class S, class T> bool contains(const S& str, const T& term); +template <class S, class T> bool equalString (const S& lhs, const T& rhs); +template <class S, class T> bool equalAsciiNoCase(const S& lhs, const T& rhs); -template <class S, class T> bool startsWith(const S& str, const T& prefix); -template <class S, class T, class Function> bool startsWith(const S& str, const T& prefix, Function cmpStringFun); +template <class S, class T> int compareString (const S& lhs, const T& rhs); +template <class S, class T> int compareAsciiNoCase(const S& lhs, const T& rhs); //basic case-insensitive comparison (considering A-Z only!) -template <class S, class T> bool endsWith (const S& str, const T& postfix); -template <class S, class T, class Function> bool endsWith (const S& str, const T& postfix, Function cmpStringFun); +struct LessAsciiNoCase //STL container predicate +{ + template <class S> bool operator()(const S& lhs, const S& rhs) const { return compareAsciiNoCase(lhs, rhs) < 0; } +}; -template <class S, class T> bool strEqual(const S& lhs, const T& rhs); -template <class S, class T, class Function> bool strEqual(const S& lhs, const T& rhs, Function cmpStringFun); enum FailureReturnVal { @@ -152,6 +150,17 @@ bool isAsciiAlpha(Char c) template <class Char> inline +bool isAsciiString(const Char* str) +{ + static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>); + for (Char c = *str; c != 0; c = *++str) + if (zen::makeUnsigned(c) >= 128) + return false; + return true; +} + + +template <class Char> inline Char asciiToLower(Char c) { if (static_cast<Char>('A') <= c && c <= static_cast<Char>('Z')) @@ -169,41 +178,103 @@ Char asciiToUpper(Char c) } -template <class S, class T, class Function> inline -bool startsWith(const S& str, const T& prefix, Function cmpStringFun) +namespace impl +{ +inline int strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std:: memcmp(ptr1, ptr2, num); } //support embedded 0, unlike strncmp/wcsncmp! +inline int strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num); } // + + +template <class Char> inline +int strcmpAsciiNoCase(const Char* lhs, const Char* rhs, size_t len) +{ + while (len-- > 0) + { + const Char charL = asciiToLower(*lhs++); //ordering: lower-case chars have higher code points than uppper-case + const Char charR = asciiToLower(*rhs++); // + if (charL != charR) + return static_cast<unsigned int>(charL) - static_cast<unsigned int>(charR); //unsigned char-comparison is the convention! + //unsigned underflow is well-defined! + } + return 0; +} +} + + +template <class S, class T> inline +bool startsWith(const S& str, const T& prefix) { const size_t pfLen = strLength(prefix); - if (strLength(str) < pfLen) - return false; + return strLength(str) >= pfLen && impl::strcmpWithNulls(strBegin(str), strBegin(prefix), pfLen) == 0; +} + - return cmpStringFun(strBegin(str), pfLen, - strBegin(prefix), pfLen) == 0; +template <class S, class T> inline +bool startsWithAsciiNoCase(const S& str, const T& prefix) +{ + const size_t pfLen = strLength(prefix); + return strLength(str) >= pfLen && impl::strcmpAsciiNoCase(strBegin(str), strBegin(prefix), pfLen) == 0; } -template <class S, class T, class Function> inline -bool endsWith(const S& str, const T& postfix, Function cmpStringFun) +template <class S, class T> inline +bool endsWith(const S& str, const T& postfix) { const size_t strLen = strLength(str); const size_t pfLen = strLength(postfix); - if (strLen < pfLen) - return false; + return strLen >= pfLen && impl::strcmpWithNulls(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen) == 0; +} + + +template <class S, class T> inline +bool endsWithAsciiNoCase(const S& str, const T& postfix) +{ + const size_t strLen = strLength(str); + const size_t pfLen = strLength(postfix); + return strLen >= pfLen && impl::strcmpAsciiNoCase(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen) == 0; +} + - return cmpStringFun(strBegin(str) + strLen - pfLen, pfLen, - strBegin(postfix), pfLen) == 0; +template <class S, class T> inline +bool equalString(const S& lhs, const T& rhs) +{ + const size_t lhsLen = strLength(lhs); + return lhsLen == strLength(rhs) && impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), lhsLen) == 0; } -template <class S, class T, class Function> inline -bool strEqual(const S& lhs, const T& rhs, Function cmpStringFun) +template <class S, class T> inline +bool equalAsciiNoCase(const S& lhs, const T& rhs) +{ + const size_t lhsLen = strLength(lhs); + return lhsLen == strLength(rhs) && impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), lhsLen) == 0; +} + + +template <class S, class T> inline +int compareString(const S& lhs, const T& rhs) { - return cmpStringFun(strBegin(lhs), strLength(lhs), strBegin(rhs), strLength(rhs)) == 0; + const size_t lhsLen = strLength(lhs); + const size_t rhsLen = strLength(rhs); + + //length check *after* strcmpWithNulls(): we do care about natural ordering: e.g. for "compareString(makeUpperCopy(lhs), makeUpperCopy(rhs))" + const int rv = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + if (rv != 0) + return rv; + return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); } -template <class S, class T> inline bool startsWith(const S& str, const T& prefix ) { return startsWith(str, prefix, CmpBinary()); } -template <class S, class T> inline bool endsWith (const S& str, const T& postfix) { return endsWith (str, postfix, CmpBinary()); } -template <class S, class T> inline bool strEqual (const S& lhs, const T& rhs ) { return strEqual (lhs, rhs, CmpBinary()); } +template <class S, class T> inline +int compareAsciiNoCase(const S& lhs, const T& rhs) +{ + const size_t lhsLen = strLength(lhs); + const size_t rhsLen = strLength(rhs); + + const int rv = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen)); + if (rv != 0) + return rv; + return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); +} template <class S, class T> inline @@ -464,42 +535,12 @@ struct CopyStringToString<T, T> //perf: we don't need a deep copy if string type template <class S> T copy(S&& str) const { return std::forward<S>(str); } }; - -inline int strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std::memcmp (ptr1, ptr2, num); } -inline int strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num); } } template <class T, class S> inline T copyStringTo(S&& str) { return impl::CopyStringToString<std::decay_t<S>, T>().copy(std::forward<S>(str)); } -template <class Char> inline -int CmpBinary::operator()(const Char* lhs, size_t lhsLen, const Char* rhs, size_t rhsLen) const -{ - //support embedded 0, unlike strncmp/wcsncmp! - const int rv = impl::strcmpWithNulls(lhs, rhs, std::min(lhsLen, rhsLen)); - if (rv != 0) - return rv; - return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); -} - - -template <class Char> inline -int CmpAsciiNoCase::operator()(const Char* lhs, size_t lhsLen, const Char* rhs, size_t rhsLen) const -{ - const auto* const lhsLast = lhs + std::min(lhsLen, rhsLen); - while (lhs != lhsLast) - { - const Char charL = asciiToLower(*lhs++); //ordering: lower-case chars have higher code points than uppper-case - const Char charR = asciiToLower(*rhs++); // - if (charL != charR) - return static_cast<unsigned int>(charL) - static_cast<unsigned int>(charR); //unsigned char-comparison is the convention! - //unsigned underflow is well-defined! - } - return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); -} - - namespace impl { template <class Num> inline diff --git a/zen/thread.cpp b/zen/thread.cpp index 8016d4a9..08bfaa25 100755 --- a/zen/thread.cpp +++ b/zen/thread.cpp @@ -34,10 +34,7 @@ uint64_t getThreadIdNative() } -struct InitMainThreadIdOnStartup -{ - InitMainThreadIdOnStartup() { getMainThreadId(); } -} startupInitMainThreadId; +const uint64_t globalMainThreadId = getThreadId(); //avoid code-gen for "magic static"! } @@ -50,6 +47,9 @@ uint64_t zen::getThreadId() uint64_t zen::getMainThreadId() { - static const uint64_t mainThreadId = getThreadId(); - return mainThreadId; + //don't make this a function-scope static (avoid code-gen for "magic static") + if (globalMainThreadId == 0) //might be called during static initialization + return getThreadId(); + + return globalMainThreadId; } @@ -327,13 +327,13 @@ TimeComp parseTime(const String& format, const String2& str, UserDefinedFormatTa const CharType* itStr = strBegin(str); const CharType* const strLast = itStr + strLength(str); - auto extractNumber = [&](int& result, size_t digitCount) -> bool + auto extractNumber = [&](int& result, size_t digitCount) { if (strLast - itStr < makeSigned(digitCount)) return false; - if (std::any_of(itStr, itStr + digitCount, [](CharType c) { return !isDigit(c); })) - return false; + if (!std::all_of(itStr, itStr + digitCount, isDigit<CharType>)) + return false; result = zen::stringTo<int>(StringRef<const CharType>(itStr, itStr + digitCount)); itStr += digitCount; diff --git a/zen/type_traits.h b/zen/type_traits.h index 2d4e7a97..8783cb6a 100755 --- a/zen/type_traits.h +++ b/zen/type_traits.h @@ -8,7 +8,6 @@ #define TYPE_TRAITS_H_3425628658765467 #include <type_traits> -#include "legacy_compiler.h" //http://en.cppreference.com/w/cpp/header/type_traits @@ -192,7 +192,7 @@ public: std::optional<CodePoint> getNext() { if (it_ == last_) - return std::nullopt; //GCC 8.2 bug: -Wmaybe-uninitialized for "return {};" + return std::nullopt; const Char8 ch = *it_++; CodePoint cp = ch; @@ -313,7 +313,7 @@ bool isValidUtf(const UtfString& str) using namespace impl; UtfDecoder<GetCharTypeT<UtfString>> decoder(strBegin(str), strLength(str)); - while (std::optional<CodePoint> cp = decoder.getNext()) + while (const std::optional<CodePoint> cp = decoder.getNext()) if (*cp == REPLACEMENT_CHAR) return false; @@ -367,7 +367,7 @@ TargetString utfTo(const SourceString& str, std::false_type) TargetString output; UtfDecoder<CharSrc> decoder(strBegin(str), strLength(str)); - while (std::optional<CodePoint> cp = decoder.getNext()) + while (const std::optional<CodePoint> cp = decoder.getNext()) codePointToUtf<CharTrg>(*cp, [&](CharTrg c) { output += c; }); return output; diff --git a/zen/zstring.cpp b/zen/zstring.cpp index 8bf77a0b..68609030 100755 --- a/zen/zstring.cpp +++ b/zen/zstring.cpp @@ -8,9 +8,102 @@ #include <stdexcept> #include "utf.h" + #include <gtk/gtk.h> + #include "sys_error.h" using namespace zen; + +Zstring makeUpperCopy(const Zstring& str) +{ + //fast pre-check: + if (isAsciiString(str.c_str())) //perf: in the range of 3.5ns + { + Zstring output = str; + for (Zchar& c : output) c = asciiToUpper(c); + return output; + } + + Zstring strNorm = getUnicodeNormalForm(str); + try + { + static_assert(sizeof(impl::CodePoint) == sizeof(gunichar)); + Zstring output; + output.reserve(strNorm.size()); + + impl::UtfDecoder<char> decoder(strNorm.c_str(), strNorm.size()); + while (const std::optional<impl::CodePoint> cp = decoder.getNext()) + impl::codePointToUtf<char>(::g_unichar_toupper(*cp), [&](char c) { output += c; }); //don't use std::towupper: *incomplete* and locale-dependent! + + return output; + + } + catch (const SysError& e) + { + (void)e; + assert(false); + return str; + } +} + + +Zstring getUnicodeNormalForm(const Zstring& str) +{ + //fast pre-check: + if (isAsciiString(str.c_str())) //perf: in the range of 3.5ns + return str; //god bless our ref-counting! => save output string memory consumption! + + //Example: const char* decomposed = "\x6f\xcc\x81"; + // const char* precomposed = "\xc3\xb3"; + try + { + gchar* outStr = ::g_utf8_normalize (str.c_str(), str.length(), G_NORMALIZE_DEFAULT_COMPOSE); + if (!outStr) + throw SysError(L"g_utf8_normalize: conversion failed. (" + utfTo<std::wstring>(str) + L")"); + ZEN_ON_SCOPE_EXIT(::g_free(outStr)); + return outStr; + + } + catch (const SysError& e) + { + (void)e; + assert(false); + return str; + } +} + + +Zstring replaceCpyAsciiNoCase(const Zstring& str, const Zstring& oldTerm, const Zstring& newTerm) +{ + if (oldTerm.empty()) + return str; + + Zstring strU = str; + Zstring oldU = oldTerm; + + for (Zchar& c : strU) c = asciiToUpper(c); //can't use makeUpperCopy(): input/output sizes may differ! + for (Zchar& c : oldU) c = asciiToUpper(c); // + + Zstring output; + + for (size_t pos = 0;;) + { + const size_t posFound = strU.find(oldU, pos); + if (posFound == Zstring::npos) + { + if (pos == 0) //optimize "oldTerm not found": return ref-counted copy + return str; + output.append(str.begin() + pos, str.end()); + return output; + } + + output.append(str.begin() + pos, str.begin() + posFound); + output += newTerm; + pos = posFound + oldTerm.size(); + } +} + + /* MSDN "Handling Sorting in Your Applications": https://msdn.microsoft.com/en-us/library/windows/desktop/dd318144 @@ -33,8 +126,14 @@ OS X (UTF8 char) ________________________ time per call | function */ +int compareLocalPath(const Zstring& lhs, const Zstring& rhs) +{ + assert(lhs.find(Zchar('\0')) == Zstring::npos); //don't expect embedded nulls! + assert(rhs.find(Zchar('\0')) == Zstring::npos); // + return compareString(lhs, rhs); +} namespace @@ -43,7 +142,7 @@ int compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rh { //- strncasecmp implements ASCII CI-comparsion only! => signature is broken for UTF8-input; toupper() similarly doesn't support Unicode //- wcsncasecmp: https://opensource.apple.com/source/Libc/Libc-763.12/string/wcsncasecmp-fbsd.c - // => re-implement comparison based on towlower() to avoid memory allocations + // => re-implement comparison based on g_unichar_tolower() to avoid memory allocations impl::UtfDecoder<char> decL(lhs, lhsLen); impl::UtfDecoder<char> decR(rhs, rhsLen); @@ -54,23 +153,35 @@ int compareNoCaseUtf8(const char* lhs, size_t lhsLen, const char* rhs, size_t rh if (!cpL || !cpR) return static_cast<int>(!cpR) - static_cast<int>(!cpL); - //support unit-testing on Windows: CodePoint is truncated to wchar_t - static_assert(sizeof(wchar_t) == sizeof(impl::CodePoint)); + static_assert(sizeof(gunichar) == sizeof(impl::CodePoint)); - const wchar_t charL = ::towlower(static_cast<wchar_t>(*cpL)); //ordering: towlower() converts to higher code points than towupper() - const wchar_t charR = ::towlower(static_cast<wchar_t>(*cpR)); //uses LC_CTYPE category of current locale + const gunichar charL = ::g_unichar_toupper(*cpL); //note: tolower can be ambiguous, so don't use: + const gunichar charR = ::g_unichar_toupper(*cpR); //e.g. "Σ" (upper case) can be lower-case "ς" in the end of the word or "σ" in the middle. if (charL != charR) + //ordering: "to lower" converts to higher code points than "to upper" return static_cast<unsigned int>(charL) - static_cast<unsigned int>(charR); //unsigned char-comparison is the convention! //unsigned underflow is well-defined! } } + } -int cmpStringNaturalLinuxTest(const char* lhs, size_t lhsLen, const char* rhs, size_t rhsLen) +int compareNatural(const Zstring& lhs, const Zstring& rhs) { - const char* const lhsEnd = lhs + lhsLen; - const char* const rhsEnd = rhs + rhsLen; + //Unicode normal forms: + // Windows: CompareString() already ignores NFD/NFC differences: nice... + // Linux: g_unichar_toupper() can't ignore differences + // macOS: CFStringCompare() considers differences + + const Zstring& lhsNorm = getUnicodeNormalForm(lhs); + const Zstring& rhsNorm = getUnicodeNormalForm(rhs); + + const char* strL = lhsNorm.c_str(); + const char* strR = rhsNorm.c_str(); + + const char* const strEndL = strL + lhsNorm.size(); + const char* const strEndR = strR + rhsNorm.size(); /* - compare strings after conceptually creating blocks of whitespace/numbers/text - implement strict weak ordering! @@ -84,43 +195,43 @@ int cmpStringNaturalLinuxTest(const char* lhs, size_t lhsLen, const char* rhs, s */ for (;;) { - if (lhs == lhsEnd || rhs == rhsEnd) - return static_cast<int>(lhs != lhsEnd) - static_cast<int>(rhs != rhsEnd); //"nothing" before "something" + if (strL == strEndL || strR == strEndR) + return static_cast<int>(strL != strEndL) - static_cast<int>(strR != strEndR); //"nothing" before "something" //note: "something" never would have been condensed to "nothing" further below => can finish evaluation here - const bool wsL = isWhiteSpace(*lhs); - const bool wsR = isWhiteSpace(*rhs); + const bool wsL = isWhiteSpace(*strL); + const bool wsR = isWhiteSpace(*strR); if (wsL != wsR) return static_cast<int>(!wsL) - static_cast<int>(!wsR); //whitespace before non-ws! if (wsL) { - ++lhs, ++rhs; - while (lhs != lhsEnd && isWhiteSpace(*lhs)) ++lhs; - while (rhs != rhsEnd && isWhiteSpace(*rhs)) ++rhs; + ++strL, ++strR; + while (strL != strEndL && isWhiteSpace(*strL)) ++strL; + while (strR != strEndR && isWhiteSpace(*strR)) ++strR; continue; } - const bool digitL = isDigit(*lhs); - const bool digitR = isDigit(*rhs); + const bool digitL = isDigit(*strL); + const bool digitR = isDigit(*strR); if (digitL != digitR) return static_cast<int>(!digitL) - static_cast<int>(!digitR); //number before chars! if (digitL) { - while (lhs != lhsEnd && *lhs == '0') ++lhs; - while (rhs != rhsEnd && *rhs == '0') ++rhs; + while (strL != strEndL && *strL == '0') ++strL; + while (strR != strEndR && *strR == '0') ++strR; int rv = 0; - for (;; ++lhs, ++rhs) + for (;; ++strL, ++strR) { - const bool endL = lhs == lhsEnd || !isDigit(*lhs); - const bool endR = rhs == rhsEnd || !isDigit(*rhs); + const bool endL = strL == strEndL || !isDigit(*strL); + const bool endR = strR == strEndR || !isDigit(*strR); if (endL != endR) return static_cast<int>(!endL) - static_cast<int>(!endR); //more digits means bigger number if (endL) break; //same number of digits - if (rv == 0 && *lhs != *rhs) - rv = *lhs - *rhs; //found first digit difference comparing from left + if (rv == 0 && *strL != *strR) + rv = *strL - *strR; //found first digit difference comparing from left } if (rv != 0) return rv; @@ -128,28 +239,19 @@ int cmpStringNaturalLinuxTest(const char* lhs, size_t lhsLen, const char* rhs, s } //compare full junks of text: consider unicode encoding! - const char* textBeginL = lhs++; - const char* textBeginR = rhs++; //current char is neither white space nor digit at this point! - while (lhs != lhsEnd && !isWhiteSpace(*lhs) && !isDigit(*lhs)) ++lhs; - while (rhs != rhsEnd && !isWhiteSpace(*rhs) && !isDigit(*rhs)) ++rhs; + const char* textBeginL = strL++; + const char* textBeginR = strR++; //current char is neither white space nor digit at this point! + while (strL != strEndL && !isWhiteSpace(*strL) && !isDigit(*strL)) ++strL; + while (strR != strEndR && !isWhiteSpace(*strR) && !isDigit(*strR)) ++strR; - const int rv = compareNoCaseUtf8(textBeginL, lhs - textBeginL, textBeginR, rhs - textBeginR); + const int rv = compareNoCaseUtf8(textBeginL, strL - textBeginL, textBeginR, strR - textBeginR); if (rv != 0) return rv; } -} - -namespace -{ } -int CmpNaturalSort::operator()(const Zchar* lhs, size_t lhsLen, const Zchar* rhs, size_t rhsLen) const -{ - //auto strL = utfTo<std::string>(Zstring(lhs, lhsLen)); - //auto strR = utfTo<std::string>(Zstring(rhs, rhsLen)); - //return cmpStringNaturalLinux(strL.c_str(), strL.size(), strR.c_str(), strR.size()); - return cmpStringNaturalLinux(lhs, lhsLen, rhs, rhsLen); - -}
\ No newline at end of file +warn_static("clean up implementation of these two:") +//template <> inline bool isWhiteSpace(char c) +//template <> inline bool isWhiteSpace(wchar_t c) diff --git a/zen/zstring.h b/zen/zstring.h index 7fa21335..20cf968d 100755 --- a/zen/zstring.h +++ b/zen/zstring.h @@ -14,6 +14,7 @@ #define Zstr(x) x const Zchar FILE_NAME_SEPARATOR = '/'; + //"The reason for all the fuss above" - Loki/SmartPtr //a high-performance string for interfacing with native OS APIs in multithreaded contexts using Zstring = zen::Zbase<Zchar>; @@ -22,43 +23,71 @@ using Zstring = zen::Zbase<Zchar>; using Zstringw = zen::Zbase<wchar_t>; -//Compare filepaths: Windows/OS X does NOT distinguish between upper/lower-case, while Linux DOES -struct CmpFilePath -{ - int operator()(const Zchar* lhs, size_t lhsLen, const Zchar* rhs, size_t rhsLen) const; -}; +//Caveat: don't expect input/output string sizes to match: +// - different UTF-8 encoding length of upper-case chars +// - different number of upper case chars (e.g. "ß" => "SS" on macOS) +// - output is Unicode-normalized +Zstring makeUpperCopy(const Zstring& str); -struct CmpNaturalSort -{ - int operator()(const Zchar* lhs, size_t lhsLen, const Zchar* rhs, size_t rhsLen) const; -}; +//Windows, Linux: precomposed +//macOS: decomposed +Zstring getUnicodeNormalForm(const Zstring& str); +Zstring replaceCpyAsciiNoCase(const Zstring& str, const Zstring& oldTerm, const Zstring& newTerm); -struct LessFilePath -{ - template <class S> //don't support heterogenous input! => use as container predicate only! - bool operator()(const S& lhs, const S& rhs) const { using namespace zen; return CmpFilePath()(strBegin(lhs), strLength(lhs), strBegin(rhs), strLength(rhs)) < 0; } -}; +//------------------------------------------------------------------------------------------ +//inline +//int compareNoCase(const Zstring& lhs, const Zstring& rhs) +//{ +// return zen::compareString(makeUpperCopy(lhs), makeUpperCopy(rhs)); +// //avoid eager optimization bugs: e.g. "if (isAsciiString()) compareAsciiNoCase()" might model a different order! +//} + +inline bool equalNoCase(const Zstring& lhs, const Zstring& rhs) { return makeUpperCopy(lhs) == makeUpperCopy(rhs); } -struct LessNaturalSort +struct ZstringNoCase //use as STL container key: avoid needless upper-case conversions during std::map<>::find() { - template <class S> //don't support heterogenous input! => use as container predicate only! - bool operator()(const S& lhs, const S& rhs) const { using namespace zen; return CmpNaturalSort()(strBegin(lhs), strLength(lhs), strBegin(rhs), strLength(rhs)) < 0; } + ZstringNoCase(const Zstring& str) : upperCase(makeUpperCopy(str)) {} + Zstring upperCase; }; +inline bool operator<(const ZstringNoCase& lhs, const ZstringNoCase& rhs) { return lhs.upperCase < rhs.upperCase; } + +//struct LessNoCase { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareNoCase(lhs, rhs) < 0; } }; + +//------------------------------------------------------------------------------------------ + +//Compare *local* file paths: +// Windows: igore case +// Linux: byte-wise comparison +// macOS: igore case + Unicode normalization forms +int compareLocalPath(const Zstring& lhs, const Zstring& rhs); + +inline bool equalLocalPath(const Zstring& lhs, const Zstring& rhs) { return compareLocalPath(lhs, rhs) == 0; } +struct LessLocalPath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareLocalPath(lhs, rhs) < 0; } }; -template <class S> -S makeUpperCopy(S str); +//------------------------------------------------------------------------------------------ +int compareNatural(const Zstring& lhs, const Zstring& rhs); +struct LessNaturalSort { bool operator()(const Zstring& lhs, const Zstring rhs) const { return compareNatural(lhs, rhs) < 0; } }; +//------------------------------------------------------------------------------------------ + +warn_static("get rid:") +inline int compareFilePath(const Zstring& lhs, const Zstring& rhs) { return compareLocalPath(lhs, rhs); } + +inline bool equalFilePath(const Zstring& lhs, const Zstring& rhs) { return compareLocalPath(lhs, rhs) == 0; } + +struct LessFilePath { bool operator()(const Zstring& lhs, const Zstring& rhs) const { return compareLocalPath(lhs, rhs) < 0; } }; +//------------------------------------------------------------------------------------------ -template <class S, class T> inline -bool equalFilePath(const S& lhs, const T& rhs) { using namespace zen; return strEqual(lhs, rhs, CmpFilePath()); } inline Zstring appendSeparator(Zstring path) //support rvalue references! { - return zen::endsWith(path, FILE_NAME_SEPARATOR) ? path : (path += FILE_NAME_SEPARATOR); //returning a by-value parameter implicitly converts to r-value! + if (!zen::endsWith(path, FILE_NAME_SEPARATOR)) + path += FILE_NAME_SEPARATOR; + return path; //returning a by-value parameter => RVO if possible, r-value otherwise! } @@ -82,12 +111,7 @@ Zstring getFileExtension(const Zstring& filePath) } -template <class S, class T, class U> -S ciReplaceCpy(const S& str, const T& oldTerm, const U& newTerm); - - - -//common unicode sequences +//common unicode characters const wchar_t EM_DASH = L'\u2014'; const wchar_t EN_DASH = L'\u2013'; const wchar_t* const SPACED_DASH = L" \u2013 "; //using 'EN DASH' @@ -99,89 +123,6 @@ const wchar_t MULT_SIGN = L'\u00D7'; //fancy "x" - - -//################################# inline implementation ######################################## -inline -void makeUpperInPlace(wchar_t* str, size_t strLen) -{ - std::for_each(str, str + strLen, [](wchar_t& c) { c = std::towupper(c); }); //locale-dependent! -} - - -inline -void makeUpperInPlace(char* str, size_t strLen) -{ - std::for_each(str, str + strLen, [](char& c) { c = std::toupper(static_cast<unsigned char>(c)); }); //locale-dependent! - //result of toupper() is an unsigned char mapped to int range: the char representation is in the last 8 bits and we need not care about signedness! - //this should work for UTF-8, too: all chars >= 128 are mapped upon themselves! -} - - -template <class S> inline -S makeUpperCopy(S str) -{ - const size_t len = str.length(); //we assert S is a string type! - if (len > 0) - makeUpperInPlace(&*str.begin(), len); - - return str; -} - - -inline -int CmpFilePath::operator()(const Zchar* lhs, size_t lhsLen, const Zchar* rhs, size_t rhsLen) const -{ - assert(std::find(lhs, lhs + lhsLen, 0) == lhs + lhsLen); //don't expect embedded nulls! - assert(std::find(rhs, rhs + rhsLen, 0) == rhs + rhsLen); // - - const int rv = std::strncmp(lhs, rhs, std::min(lhsLen, rhsLen)); - if (rv != 0) - return rv; - return static_cast<int>(lhsLen) - static_cast<int>(rhsLen); -} - - -template <class S, class T, class U> inline -S ciReplaceCpy(const S& str, const T& oldTerm, const U& newTerm) -{ - using namespace zen; - static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>); - static_assert(std::is_same_v<GetCharTypeT<T>, GetCharTypeT<U>>); - const size_t oldLen = strLength(oldTerm); - if (oldLen == 0) - return str; - - const S strU = makeUpperCopy(str); //S required to be a string class - const S oldU = makeUpperCopy<S>(oldTerm); //[!] T not required to be a string class - assert(strLength(strU) == strLength(str )); - assert(strLength(oldU) == strLength(oldTerm)); - - const auto* const newBegin = strBegin(newTerm); - const auto* const newEnd = newBegin + strLength(newTerm); - - S output; - - for (size_t pos = 0;;) - { - const auto itFound = std::search(strU.begin() + pos, strU.end(), - oldU.begin(), oldU.end()); - if (itFound == strU.end() && pos == 0) - return str; //optimize "oldTerm not found": return ref-counted copy - - impl::stringAppend(output, str.begin() + pos, str.begin() + (itFound - strU.begin())); - if (itFound == strU.end()) - return output; - - impl::stringAppend(output, newBegin, newEnd); - pos = (itFound - strU.begin()) + oldLen; - } -} - -//expose for unit tests -int cmpStringNaturalLinuxTest(const char* lhs, size_t lhsLen, const char* rhs, size_t rhsLen); -inline int cmpStringNaturalLinux(const char* lhs, size_t lhsLen, const char* rhs, size_t rhsLen) { return cmpStringNaturalLinuxTest(lhs, lhsLen, rhs, rhsLen); } - //--------------------------------------------------------------------------- //ZEN macro consistency checks: diff --git a/zenXml/zenxml/cvrt_text.h b/zenXml/zenxml/cvrt_text.h index c6dab8c2..51b23173 100755 --- a/zenXml/zenxml/cvrt_text.h +++ b/zenXml/zenxml/cvrt_text.h @@ -22,9 +22,9 @@ It is \b not required to call these functions directly. They are implicitly used zen::XmlElement::setValue(), zen::XmlElement::getAttribute() and zen::XmlElement::setAttribute(). \n\n Conversions for the following user types are supported by default: - - strings - std::string, std::wstring, char*, wchar_t*, char, wchar_t, ect..., all STL-compatible-string-classes - - numbers - int, double, float, bool, long, ect..., all built-in numbers - - STL containers - std::map, std::set, std::vector, std::list, ect..., all STL-compatible-containers + - strings - std::string, std::wstring, char*, wchar_t*, char, wchar_t, etc..., all STL-compatible-string-classes + - numbers - int, double, float, bool, long, etc..., all built-in numbers + - STL containers - std::map, std::set, std::vector, std::list, etc..., all STL-compatible-containers - std::pair You can add support for additional types via template specialization. \n\n diff --git a/zenXml/zenxml/parser.h b/zenXml/zenxml/parser.h index a3975297..a90d163a 100755 --- a/zenXml/zenxml/parser.h +++ b/zenXml/zenxml/parser.h @@ -11,7 +11,6 @@ #include <cstddef> //ptrdiff_t; req. on Linux #include <zen/string_tools.h> #include "dom.h" -#include "error.h" namespace zen @@ -28,12 +27,13 @@ namespace zen \param indent Indentation, default: four space characters \return Output byte stream */ -std::string serialize(const XmlDoc& doc, - const std::string& lineBreak = "\r\n", - const std::string& indent = " "); //noexcept +std::string serializeXml(const XmlDoc& doc, + const std::string& lineBreak = "\r\n", + const std::string& indent = " "); //noexcept + ///Exception thrown due to an XML parsing error -struct XmlParsingError : public XmlError +struct XmlParsingError { XmlParsingError(size_t rowNo, size_t colNo) : row(rowNo), col(colNo) {} ///Input file row where the parsing error occured (zero-based) @@ -42,14 +42,13 @@ struct XmlParsingError : public XmlError const size_t col; // }; - ///Load XML document from a byte stream /** \param stream Input byte stream \returns Output XML document \throw XmlParsingError */ -XmlDoc parse(const std::string& stream); //throw XmlParsingError +XmlDoc parseXml(const std::string& stream); //throw XmlParsingError @@ -71,9 +70,9 @@ XmlDoc parse(const std::string& stream); //throw XmlParsingError //---------------------------- implementation ---------------------------- -//see: http://www.w3.org/TR/xml/ +//see: https://www.w3.org/TR/xml/ -namespace impl +namespace xml_impl { template <class Predicate> inline std::string normalize(const std::string& str, Predicate pred) //pred: unary function taking a char, return true if value shall be encoded as hex @@ -91,7 +90,7 @@ std::string normalize(const std::string& str, Predicate pred) //pred: unary func { if (c == '\'') output += "'"; - else if (c == '\"') + else if (c == '"') output += """; else { @@ -111,7 +110,7 @@ std::string normalize(const std::string& str, Predicate pred) //pred: unary func inline std::string normalizeName(const std::string& str) { - const std::string nameFmt = normalize(str, [](char c) { return isWhiteSpace(c) || c == '=' || c == '/' || c == '\'' || c == '\"'; }); + const std::string nameFmt = normalize(str, [](char c) { return isWhiteSpace(c) || c == '=' || c == '/' || c == '\'' || c == '"'; }); assert(!nameFmt.empty()); return nameFmt; } @@ -125,7 +124,7 @@ std::string normalizeElementValue(const std::string& str) inline std::string normalizeAttribValue(const std::string& str) { - return normalize(str, [](char c) { return static_cast<unsigned char>(c) < 32 || c == '\'' || c == '\"'; }); + return normalize(str, [](char c) { return static_cast<unsigned char>(c) < 32 || c == '\'' || c == '"'; }); } @@ -163,10 +162,12 @@ std::string denormalize(const std::string& str) else if (checkEntity(it, str.end(), "'")) output += '\''; else if (checkEntity(it, str.end(), """)) - output += '\"'; + output += '"'; else if (str.end() - it >= 6 && it[1] == '#' && it[2] == 'x' && + isHexDigit(it[3]) && + isHexDigit(it[4]) && it[5] == ';') { output += unhexify(it[3], it[4]); @@ -203,7 +204,7 @@ void serialize(const XmlElement& element, std::string& stream, auto attr = element.getAttributes(); for (auto it = attr.first; it != attr.second; ++it) - stream += ' ' + normalizeName(it->name) + "=\"" + normalizeAttribValue(it->value) + '\"'; + stream += ' ' + normalizeName(it->name) + "=\"" + normalizeAttribValue(it->value) + '"'; auto iterPair = element.getChildren(); if (iterPair.first != iterPair.second) //structured element @@ -229,34 +230,30 @@ void serialize(const XmlElement& element, std::string& stream, stream += "/>" + lineBreak; } } +} +} -std::string serialize(const XmlDoc& doc, - const std::string& lineBreak, - const std::string& indent) +inline +std::string serializeXml(const XmlDoc& doc, + const std::string& lineBreak, + const std::string& indent) { std::string version = doc.getVersionAs<std::string>(); if (!version.empty()) - version = " version=\"" + normalizeAttribValue(version) + '\"'; + version = " version=\"" + xml_impl::normalizeAttribValue(version) + '"'; std::string encoding = doc.getEncodingAs<std::string>(); if (!encoding.empty()) - encoding = " encoding=\"" + normalizeAttribValue(encoding) + '\"'; + encoding = " encoding=\"" + xml_impl::normalizeAttribValue(encoding) + '"'; std::string standalone = doc.getStandaloneAs<std::string>(); if (!standalone.empty()) - standalone = " standalone=\"" + normalizeAttribValue(standalone) + '\"'; + standalone = " standalone=\"" + xml_impl::normalizeAttribValue(standalone) + '"'; std::string output = "<?xml" + version + encoding + standalone + "?>" + lineBreak; - serialize(doc.root(), output, lineBreak, indent, 0); + xml_impl::serialize(doc.root(), output, lineBreak, indent, 0); return output; } -} -} - -inline -std::string serialize(const XmlDoc& doc, - const std::string& lineBreak, - const std::string& indent) { return impl::serialize(doc, lineBreak, indent); } /* Grammar for XML parser @@ -282,7 +279,7 @@ pm-expression: element-list-expression */ -namespace impl +namespace xml_impl { struct Token { @@ -310,53 +307,53 @@ struct Token class Scanner { public: - Scanner(const std::string& stream) : stream_(stream), pos(stream_.begin()) + Scanner(const std::string& stream) : stream_(stream), pos_(stream_.begin()) { if (zen::startsWith(stream_, BYTE_ORDER_MARK_UTF8)) - pos += strLength(BYTE_ORDER_MARK_UTF8); + pos_ += strLength(BYTE_ORDER_MARK_UTF8); } - Token nextToken() //throw XmlParsingError + Token getNextToken() //throw XmlParsingError { //skip whitespace - pos = std::find_if(pos, stream_.end(), [](char c) { return !zen::isWhiteSpace(c); }); + pos_ = std::find_if(pos_, stream_.end(), std::not_fn(isWhiteSpace<char>)); - if (pos == stream_.end()) + if (pos_ == stream_.end()) return Token::TK_END; //skip XML comments - if (startsWith(xmlCommentBegin)) + if (startsWith(xmlCommentBegin_)) { - auto it = std::search(pos + xmlCommentBegin.size(), stream_.end(), xmlCommentEnd.begin(), xmlCommentEnd.end()); + auto it = std::search(pos_ + xmlCommentBegin_.size(), stream_.end(), xmlCommentEnd_.begin(), xmlCommentEnd_.end()); if (it != stream_.end()) { - pos = it + xmlCommentEnd.size(); - return nextToken(); + pos_ = it + xmlCommentEnd_.size(); + return getNextToken(); //throw XmlParsingError } } - for (auto it = tokens.begin(); it != tokens.end(); ++it) + for (auto it = tokens_.begin(); it != tokens_.end(); ++it) if (startsWith(it->first)) { - pos += it->first.size(); + pos_ += it->first.size(); return it->second; } - auto nameEnd = std::find_if(pos, stream_.end(), [](char c) + const auto itNameEnd = std::find_if(pos_, stream_.end(), [](char c) { return c == '<' || c == '>' || c == '=' || c == '/' || c == '\'' || - c == '\"' || - zen::isWhiteSpace(c); + c == '"' || + isWhiteSpace(c); }); - if (nameEnd != pos) + if (itNameEnd != pos_) { - std::string name(&*pos, nameEnd - pos); - pos = nameEnd; + std::string name(pos_, itNameEnd); + pos_ = itNameEnd; return denormalize(name); } @@ -366,34 +363,34 @@ public: std::string extractElementValue() { - auto it = std::find_if(pos, stream_.end(), [](char c) + auto it = std::find_if(pos_, stream_.end(), [](char c) { return c == '<' || c == '>'; }); - std::string output(pos, it); - pos = it; + std::string output(pos_, it); + pos_ = it; return denormalize(output); } std::string extractAttributeValue() { - auto it = std::find_if(pos, stream_.end(), [](char c) + auto it = std::find_if(pos_, stream_.end(), [](char c) { return c == '<' || c == '>' || c == '\'' || - c == '\"'; + c == '"'; }); - std::string output(pos, it); - pos = it; + std::string output(pos_, it); + pos_ = it; return denormalize(output); } size_t posRow() const //current row beginning with 0 { - const size_t crSum = std::count(stream_.begin(), pos, '\r'); //carriage returns - const size_t nlSum = std::count(stream_.begin(), pos, '\n'); //new lines + const size_t crSum = std::count(stream_.begin(), pos_, '\r'); //carriage returns + const size_t nlSum = std::count(stream_.begin(), pos_, '\n'); //new lines assert(crSum == 0 || nlSum == 0 || crSum == nlSum); return std::max(crSum, nlSum); //be compatible with Linux/Mac/Win } @@ -401,13 +398,13 @@ public: size_t posCol() const //current col beginning with 0 { //seek beginning of line - for (auto it = pos; it != stream_.begin(); ) + for (auto it = pos_; it != stream_.begin(); ) { --it; if (*it == '\r' || *it == '\n') - return pos - it - 1; + return pos_ - it - 1; } - return pos - stream_.begin(); + return pos_ - stream_.begin(); } private: @@ -416,13 +413,11 @@ private: bool startsWith(const std::string& prefix) const { - if (stream_.end() - pos < static_cast<ptrdiff_t>(prefix.size())) - return false; - return std::equal(prefix.begin(), prefix.end(), pos); + return zen::startsWith(StringRef<const char>(pos_, stream_.end()), prefix); } using TokenList = std::vector<std::pair<std::string, Token::Type>>; - const TokenList tokens + const TokenList tokens_ { { "<?xml", Token::TK_DECL_BEGIN }, { "?>", Token::TK_DECL_END }, @@ -435,11 +430,11 @@ private: { "\'", Token::TK_QUOTE }, }; - const std::string xmlCommentBegin = "<!--"; - const std::string xmlCommentEnd = "-->"; + const std::string xmlCommentBegin_ = "<!--"; + const std::string xmlCommentEnd_ = "-->"; const std::string stream_; - std::string::const_iterator pos; + std::string::const_iterator pos_; }; @@ -448,7 +443,7 @@ class XmlParser public: XmlParser(const std::string& stream) : scn_(stream), - tk_(scn_.nextToken()) {} + tk_(scn_.getNextToken()) {} //throw XmlParsingError XmlDoc parse() //throw XmlParsingError { @@ -457,19 +452,19 @@ public: //declaration (optional) if (token().type == Token::TK_DECL_BEGIN) { - nextToken(); + nextToken(); //throw XmlParsingError while (token().type == Token::TK_NAME) { std::string attribName = token().name; - nextToken(); + nextToken(); //throw XmlParsingError - consumeToken(Token::TK_EQUAL); - expectToken(Token::TK_QUOTE); + consumeToken(Token::TK_EQUAL); //throw XmlParsingError + expectToken (Token::TK_QUOTE); // std::string attribValue = scn_.extractAttributeValue(); - nextToken(); + nextToken(); //throw XmlParsingError - consumeToken(Token::TK_QUOTE); + consumeToken(Token::TK_QUOTE); //throw XmlParsingError if (attribName == "version") doc.setVersion(attribValue); @@ -478,7 +473,7 @@ public: else if (attribName == "standalone") doc.setStandalone(attribValue); } - consumeToken(Token::TK_DECL_END); + consumeToken(Token::TK_DECL_END); //throw XmlParsingError } XmlElement dummy; @@ -488,7 +483,7 @@ public: if (itPair.first != itPair.second) doc.root().swapSubtree(*itPair.first); - expectToken(Token::TK_END); + expectToken(Token::TK_END); //throw XmlParsingError return doc; } @@ -500,11 +495,11 @@ private: { while (token().type == Token::TK_LESS) { - nextToken(); + nextToken(); //throw XmlParsingError - expectToken(Token::TK_NAME); + expectToken(Token::TK_NAME); //throw XmlParsingError std::string elementName = token().name; - nextToken(); + nextToken(); //throw XmlParsingError XmlElement& newElement = parent.addChild(elementName); @@ -512,28 +507,28 @@ private: if (token().type == Token::TK_SLASH_GREATER) //empty element { - nextToken(); + nextToken(); //throw XmlParsingError continue; } - expectToken(Token::TK_GREATER); + expectToken(Token::TK_GREATER); //throw XmlParsingError std::string elementValue = scn_.extractElementValue(); - nextToken(); + nextToken(); //throw XmlParsingError //no support for mixed-mode content - if (token().type == Token::TK_LESS) //structured element + if (token().type == Token::TK_LESS) //structure-element parseChildElements(newElement); - else //value element + else //value-element newElement.setValue(elementValue); - consumeToken(Token::TK_LESS_SLASH); + consumeToken(Token::TK_LESS_SLASH); //throw XmlParsingError - if (token().type != Token::TK_NAME || - elementName != token().name) + expectToken(Token::TK_NAME); //throw XmlParsingError + if (token().name != elementName) throw XmlParsingError(scn_.posRow(), scn_.posCol()); - nextToken(); + nextToken(); //throw XmlParsingError - consumeToken(Token::TK_GREATER); + consumeToken(Token::TK_GREATER); //throw XmlParsingError } } @@ -542,26 +537,21 @@ private: while (token().type == Token::TK_NAME) { std::string attribName = token().name; - nextToken(); + nextToken(); //throw XmlParsingError - consumeToken(Token::TK_EQUAL); - expectToken(Token::TK_QUOTE); + consumeToken(Token::TK_EQUAL); //throw XmlParsingError + expectToken (Token::TK_QUOTE); // std::string attribValue = scn_.extractAttributeValue(); - nextToken(); + nextToken(); //throw XmlParsingError - consumeToken(Token::TK_QUOTE); + consumeToken(Token::TK_QUOTE); //throw XmlParsingError element.setAttribute(attribName, attribValue); } } const Token& token() const { return tk_; } - void nextToken() { tk_ = scn_.nextToken(); } - void consumeToken(Token::Type t) //throw XmlParsingError - { - expectToken(t); //throw XmlParsingError - nextToken(); - } + void nextToken() { tk_ = scn_.getNextToken(); } //throw XmlParsingError void expectToken(Token::Type t) //throw XmlParsingError { @@ -569,15 +559,21 @@ private: throw XmlParsingError(scn_.posRow(), scn_.posCol()); } + void consumeToken(Token::Type t) //throw XmlParsingError + { + expectToken(t); //throw XmlParsingError + nextToken(); // + } + Scanner scn_; Token tk_; }; } inline -XmlDoc parse(const std::string& stream) //throw XmlParsingError +XmlDoc parseXml(const std::string& stream) //throw XmlParsingError { - return impl::XmlParser(stream).parse(); //throw XmlParsingError + return xml_impl::XmlParser(stream).parse(); //throw XmlParsingError } } diff --git a/zenXml/zenxml/xml.h b/zenXml/zenxml/xml.h index 9510645a..5eddc462 100755 --- a/zenXml/zenxml/xml.h +++ b/zenXml/zenxml/xml.h @@ -1,4 +1,4 @@ -// ***************************************************************************** +// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * @@ -7,10 +7,444 @@ #ifndef XML_H_349578228034572457454554 #define XML_H_349578228034572457454554 -#include "bind.h" +#include <set> +#include <zen/file_io.h> +#include <zen/file_access.h> +#include "cvrt_struc.h" +#include "parser.h" /// The zen::Xml namespace -namespace zen {} +namespace zen +{ +/** +\file +\brief Save and load byte streams from files +*/ + +///Load XML document from a file +/** +Load and parse XML byte stream. Quick-exit if (potentially large) input file is not an XML. + +\param filePath Input file path +\returns The loaded XML document +\throw FileError +*/ +namespace +{ +XmlDoc loadXml(const Zstring& filePath) //throw FileError +{ + FileInput fileIn(filePath, nullptr /*notifyUnbufferedIO*/); //throw FileError, ErrorFileLocked + const size_t blockSize = fileIn.getBlockSize(); + const std::string xmlPrefix = "<?xml version="; + bool xmlPrefixChecked = false; + + std::string buffer; + for (;;) + { + buffer.resize(buffer.size() + blockSize); + const size_t bytesRead = fileIn.read(&*(buffer.end() - blockSize), blockSize); //throw FileError, ErrorFileLocked, (X); return "bytesToRead" bytes unless end of stream! + buffer.resize(buffer.size() - blockSize + bytesRead); //caveat: unsigned arithmetics + + //quick test whether input is an XML: avoid loading large binary files up front! + if (!xmlPrefixChecked && buffer.size() >= xmlPrefix.size() + strLength(BYTE_ORDER_MARK_UTF8)) + { + xmlPrefixChecked = true; + if (!startsWith(buffer, xmlPrefix) && + !startsWith(buffer, BYTE_ORDER_MARK_UTF8 + xmlPrefix)) //allow BOM! + throw FileError(replaceCpy(_("File %x does not contain a valid configuration."), L"%x", fmtPath(filePath))); + } + + if (bytesRead < blockSize) //end of file + break; + } + + try + { + return parseXml(buffer); //throw XmlParsingError + } + catch (const XmlParsingError& e) + { + throw FileError( + replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), + L"%x", fmtPath(filePath)), + L"%y", numberTo<std::wstring>(e.row + 1)), + L"%z", numberTo<std::wstring>(e.col + 1))); + } +} +} + + +///Save XML document to a file +/** +Serialize XML to byte stream and save to file. + +\param doc The XML document to save +\param filePath Output file path +\throw FileError +*/ +inline +void saveXml(const XmlDoc& doc, const Zstring& filePath) //throw FileError +{ + const std::string stream = serializeXml(doc); //noexcept + + try //only update XML file if there are changes + { + if (getFileSize(filePath) == stream.size()) //throw FileError + if (loadBinContainer<std::string>(filePath, nullptr /*notifyUnbufferedIO*/) == stream) //throw FileError + return; + } + catch (FileError&) {} + + saveBinContainer(filePath, stream, nullptr /*notifyUnbufferedIO*/); //throw FileError +} + + +///Proxy class to conveniently convert user data into XML structure +class XmlOut +{ +public: + ///Construct an output proxy for an XML document + /** + \code + zen::XmlDoc doc; + + zen::XmlOut out(doc); + out["elem1"]( 1); // + out["elem2"]( 2); //write data into XML elements + out["elem3"](-3); // + + saveXml(doc, "out.xml"); //throw FileError + \endcode + Output: + \verbatim + <?xml version="1.0" encoding="utf-8"?> + <Root> + <elem1>1</elem1> + <elem2>2</elem2> + <elem3>-3</elem3> + </Root> + \endverbatim + */ + XmlOut(XmlDoc& doc) : ref_(&doc.root()) {} + ///Construct an output proxy for a single XML element + /** + \sa XmlOut(XmlDoc& doc) + */ + XmlOut(XmlElement& element) : ref_(&element) {} + + ///Retrieve a handle to an XML child element for writing + /** + The child element will be created if it is not yet existing. + \tparam String Arbitrary string-like type: e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... + \param name The name of the child element + */ + template <class String> + XmlOut operator[](const String& name) const + { + const std::string utf8name = utfTo<std::string>(name); + XmlElement* child = ref_->getChild(utf8name); + return child ? *child : ref_->addChild(utf8name); + } + + ///Write user data to the underlying XML element + /** + This conversion requires a specialization of zen::writeText() or zen::writeStruc() for type T. + \tparam T User type that is converted into an XML element value. + */ + template <class T> + void operator()(const T& value) { writeStruc(value, *ref_); } + + ///Write user data to an XML attribute + /** + This conversion requires a specialization of zen::writeText() for type T. + \code + zen::XmlDoc doc; + + zen::XmlOut out(doc); + out["elem"].attribute("attr1", 1); // + out["elem"].attribute("attr2", 2); //write data into XML attributes + out["elem"].attribute("attr3", -3); // + + saveXml(doc, "out.xml"); //throw FileError + \endcode + Output: + \verbatim + <?xml version="1.0" encoding="utf-8"?> + <Root> + <elem attr1="1" attr2="2" attr3="-3"/> + </Root> + \endverbatim + + \tparam String Arbitrary string-like type: e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... + \tparam T String-convertible user data type: e.g. any string-like type, all built-in arithmetic numbers + \sa XmlElement::setAttribute() + */ + template <class String, class T> + void attribute(const String& name, const T& value) { ref_->setAttribute(name, value); } + + ///Return a reference to the underlying Xml element + XmlElement& ref() { return *ref_; } + +private: + XmlElement* ref_; //always bound! +}; + + +///Proxy class to conveniently convert XML structure to user data +class XmlIn +{ + class ErrorLog; + +public: + ///Construct an input proxy for an XML document + /** + \code + zen::XmlDoc doc; + ... //load document + zen::XmlIn in(doc); + in["elem1"](value1); // + in["elem2"](value2); //read data from XML elements into variables "value1", "value2", "value3" + in["elem3"](value3); // + \endcode + */ + XmlIn(const XmlDoc& doc) { refList_.push_back(&doc.root()); } + ///Construct an input proxy for a single XML element, may be nullptr + /** + \sa XmlIn(const XmlDoc& doc) + */ + XmlIn(const XmlElement* element) { refList_.push_back(element); } + ///Construct an input proxy for a single XML element + /** + \sa XmlIn(const XmlDoc& doc) + */ + XmlIn(const XmlElement& element) { refList_.push_back(&element); } + + ///Retrieve a handle to an XML child element for reading + /** + It is \b not an error if the child element does not exist, but only later if a conversion to user data is attempted. + \tparam String Arbitrary string-like type: e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... + \param name The name of the child element + */ + template <class String> + XmlIn operator[](const String& name) const + { + std::vector<const XmlElement*> childList; + + if (refIndex_ < refList_.size()) + { + auto iterPair = refList_[refIndex_]->getChildren(name); + std::for_each(iterPair.first, iterPair.second, + [&](const XmlElement& child) { childList.push_back(&child); }); + } + + return XmlIn(childList, childList.empty() ? getChildNameFormatted(name) : std::string(), log_); + } + + ///Refer to next sibling element with the same name + /** + <b>Example:</b> Loop over all XML child elements named "Item" + \verbatim + <?xml version="1.0" encoding="utf-8"?> + <Root> + <Item>1</Item> + <Item>3</Item> + <Item>5</Item> + </Root> + \endverbatim + + \code + zen::XmlIn in(doc); + ... + for (zen::XmlIn child = in["Item"]; child; child.next()) + { + ... + } + \endcode + */ + void next() { ++refIndex_; } + + ///Read user data from the underlying XML element + /** + This conversion requires a specialization of zen::readText() or zen::readStruc() for type T. + \tparam T User type that receives the data + \return "true" if data was read successfully + */ + template <class T> + bool operator()(T& value) const + { + if (refIndex_ < refList_.size()) + { + bool success = readStruc(*refList_[refIndex_], value); + if (!success) + log_->notifyConversionError(getNameFormatted()); + return success; + } + else + { + log_->notifyMissingElement(getNameFormatted()); + return false; + } + } + + ///Read user data from an XML attribute + /** + This conversion requires a specialization of zen::readText() for type T. + + \code + zen::XmlDoc doc; + ... //load document + zen::XmlIn in(doc); + in["elem"].attribute("attr1", value1); // + in["elem"].attribute("attr2", value2); //read data from XML attributes into variables "value1", "value2", "value3" + in["elem"].attribute("attr3", value3); // + \endcode + + \tparam String Arbitrary string-like type: e.g. std::string, wchar_t*, char[], wchar_t, wxString, MyStringClass, ... + \tparam T String-convertible user data type: e.g. any string-like type, all built-in arithmetic numbers + \returns "true" if the attribute was found and the conversion to the output value was successful. + \sa XmlElement::getAttribute() + */ + template <class String, class T> + bool attribute(const String& name, T& value) const + { + if (refIndex_ < refList_.size()) + { + bool success = refList_[refIndex_]->getAttribute(name, value); + if (!success) + log_->notifyMissingAttribute(getNameFormatted(), utfTo<std::string>(name)); + return success; + } + else + { + log_->notifyMissingElement(getNameFormatted()); + return false; + } + } + + ///Return a pointer to the underlying Xml element, may be nullptr + const XmlElement* get() const { return refIndex_ < refList_.size() ? refList_[refIndex_] : nullptr; } + + ///Test whether the underlying XML element exists + /** + \code + XmlIn in(doc); + XmlIn child = in["elem1"]; + if (child) + ... + \endcode + Use member pointer as implicit conversion to bool (C++ Templates - Vandevoorde/Josuttis; chapter 20) + */ + explicit operator bool() const { return get() != nullptr; } + + ///Notifies errors while mapping the XML to user data + /** + Error logging is shared by each hiearchy of XmlIn proxy instances that are created from each other. Consequently it doesn't matter which instance you query for errors: + \code + XmlIn in(doc); + XmlIn inItem = in["item1"]; + + int value = 0; + inItem(value); //let's assume this conversion failed + + assert(in.haveErrors() == inItem.haveErrors()); + assert(in.getErrorsAs<std::string>() == inItem.getErrorsAs<std::string>()); + \endcode + + Note that error logging is \b NOT global, but owned by all instances of a hierarchy of XmlIn proxies. + Therefore it's safe to use unrelated XmlIn proxies in multiple threads. + \n\n + However be aware that the chain of connected proxy instances will be broken once you call XmlIn::get() to retrieve the underlying pointer. + Errors that occur when working with this pointer are not logged by the original set of related instances. + */ + bool haveErrors() const { return !log_->elementList().empty(); } + + ///Get a list of XML element and attribute names which failed to convert to user data. + /** + \tparam String Arbitrary string class: e.g. std::string, std::wstring, wxString, MyStringClass, ... + \returns A list of XML element and attribute names, empty list if no errors occured. + */ + template <class String> + std::vector<String> getErrorsAs() const + { + std::vector<String> output; + const auto& elements = log_->elementList(); + std::transform(elements.begin(), elements.end(), std::back_inserter(output), [](const std::string& str) { return utfTo<String>(str); }); + return output; + } + +private: + XmlIn(const std::vector<const XmlElement*>& siblingList, const std::string& elementNameFmt, const std::shared_ptr<ErrorLog>& sharedlog) : + refList_(siblingList), formattedName_(elementNameFmt), log_(sharedlog) + { assert((!siblingList.empty() && elementNameFmt.empty()) || (siblingList.empty() && !elementNameFmt.empty())); } + + static std::string getNameFormatted(const XmlElement& elem) //"<Root> <Level1> <Level2>" + { + return (elem.parent() ? getNameFormatted(*elem.parent()) + " " : std::string()) + "<" + elem.getNameAs<std::string>() + ">"; + } + + std::string getNameFormatted() const + { + if (refIndex_ < refList_.size()) + { + assert(formattedName_.empty()); + return getNameFormatted(*refList_[refIndex_]); + } + else + return formattedName_; + } + + std::string getChildNameFormatted(const std::string& childName) const + { + std::string parentName = getNameFormatted(); + return (parentName.empty() ? std::string() : (parentName + " ")) + "<" + childName + ">"; + } + + class ErrorLog + { + public: + void notifyConversionError (const std::string& displayName) { insert(displayName); } + void notifyMissingElement (const std::string& displayName) { insert(displayName); } + void notifyMissingAttribute(const std::string& displayName, const std::string& attribName) { insert(displayName + " @" + attribName); } + + const std::vector<std::string>& elementList() const { return failedElements; } + + private: + void insert(const std::string& newVal) + { + if (usedElements.insert(newVal).second) + failedElements.push_back(newVal); + } + + std::vector<std::string> failedElements; //unique list of failed elements + std::set<std::string> usedElements; + }; + + std::vector<const XmlElement*> refList_; //all sibling elements with same name (all pointers bound!) + size_t refIndex_ = 0; //this sibling's index in refList_ + std::string formattedName_; //contains full and formatted element name if (and only if) refList_ is empty + std::shared_ptr<ErrorLog> log_{ std::make_shared<ErrorLog>() }; //always bound +}; + + +///Check XML input proxy for errors and map to FileError exception +/** +\param xmlInput XML input proxy +\param filePath Input file path +\throw FileError +*/ +inline +void checkXmlMappingErrors(const XmlIn& xmlInput, const Zstring& filePath) //throw FileError +{ + if (xmlInput.haveErrors()) + { + std::wstring msg = _("The following XML elements could not be read:") + L"\n"; + for (const std::wstring& elem : xmlInput.getErrorsAs<std::wstring>()) + msg += L"\n" + elem; + + throw FileError(replaceCpy(_("Configuration file %x is incomplete. The missing elements will be set to their default values."), L"%x", fmtPath(filePath)) + L"\n\n" + msg); + } +} +} #endif //XML_H_349578228034572457454554 |