diff options
author | B Stack <bgstack15@gmail.com> | 2019-09-17 08:06:14 -0400 |
---|---|---|
committer | B Stack <bgstack15@gmail.com> | 2019-09-17 08:06:14 -0400 |
commit | ca250bf14979d359a82d5baf11ae9587d5605594 (patch) | |
tree | fc3041d022fee1abfc6df2ffd903fd7662b0b0d9 | |
parent | add upstream 10.15 (diff) | |
download | FreeFileSync-ca250bf14979d359a82d5baf11ae9587d5605594.tar.gz FreeFileSync-ca250bf14979d359a82d5baf11ae9587d5605594.tar.bz2 FreeFileSync-ca250bf14979d359a82d5baf11ae9587d5605594.zip |
add upstream 10.16
34 files changed, 1113 insertions, 694 deletions
@@ -2,22 +2,72 @@ When manually compiling FreeFileSync, you should also fix the following bugs in ---------------- -| libssh2 Bugs | +| libcurl Bugs | ---------------- __________________________________________________________________________________________________________ -Amazons SFTP server returns legitimate package sizes of ~100kb! -https://freefilesync.org/forum/viewtopic.php?t=5999 +/lib/setopt.c +https://github.com/curl/curl/pull/4321 -/src/sftp.c: +- if ((arg < CURLFTPMETHOD_DEFAULT) || (arg > CURLFTPMETHOD_SINGLECWD)) ++ if ((arg < CURLFTPMETHOD_DEFAULT) || (arg >= CURLFTPMETHOD_LAST)) -- #define LIBSSH2_SFTP_PACKET_MAXLEN 80000 -+ #define LIBSSH2_SFTP_PACKET_MAXLEN 160000 __________________________________________________________________________________________________________ +https://github.com/curl/curl/pull/4331 +/include/curl/curl.h + + CURLFTPMETHOD_SINGLECWD, /* one CWD to full dir, then work on file */ ++ CURLFTPMETHOD_FULLPATH, //AKA "CURLFTPMETHOD_NOCWD_BUT_THIS_TIME_FOR_REAL" + + +/lib/ftp.h + + FTPFILE_SINGLECWD = 3 /* make one CWD, then SIZE / RETR / STOR on the + file */ ++ FTPFILE_FULLPATH = 4 //AKA "FTPFILE_NOCWD_BUT_THIS_TIME_FOR_REAL" + + +/lib/ftp.c + +- if ((data->set.ftp_filemethod == FTPFILE_NOCWD) && ++ if ((data->set.ftp_filemethod == FTPFILE_NOCWD || ++ data->set.ftp_filemethod == FTPFILE_FULLPATH) && + + ++ if (data->set.ftp_filemethod == FTPFILE_FULLPATH) ++ { ++ //no CWDs happened => remember old working dir ++ //ftpc->prevmethod = ++ //ftpc->prevpath = ++ } ++ else ++ { + /* now store a copy of the directory we are in */ + free(ftpc->prevpath); + + [...] + + else + { + ftpc->prevpath = NULL; /* no path */ + free(path); + } + } + ++ } + + + switch (data->set.ftp_filemethod) + { + case FTPFILE_NOCWD: ++ case FTPFILE_FULLPATH: + + ++ if (data->set.ftp_filemethod == FTPFILE_FULLPATH) ++ ftpc->cwddone = TRUE; ++ else + if (ftpc->prevpath) ----------------- -| libcurl Bugs | ----------------- __________________________________________________________________________________________________________ https://github.com/curl/curl/issues/1455 @@ -51,63 +101,53 @@ Replace with: } if (skipIp) + __________________________________________________________________________________________________________ +/lib/ftp.c +https://github.com/curl/curl/pull/4332 -"wrong dir listing because libcurl remembers wrong CWD": https://github.com/curl/curl/issues/1782 +- else if (conn->bits.reuse && ftpc->entrypath) ++ else if (conn->bits.reuse && ftpc->entrypath && ++ !(ftpc->dirdepth && ftpc->dirs[0][0] == '/')) //no need to go to entrypath when we have an absolute path -=> "fixed" by adding only the "if (data->set.ftp_filemethod == FTPFILE_NOCWD)" below: https://github.com/curl/curl/issues/1811 -=> this is NOT enough! consider what happens for a reused connection that first used CURLFTPMETHOD_MULTICWD, now CURLFTPMETHOD_NOCWD: - - the code in ftp_state_cwd() will issue a CWD sequence that ends with "ftpc->cwdcount == 1"!!! See "if (++ftpc->cwdcount <= ftpc->dirdepth)" - => this skips the previous "fix" in https://github.com/curl/curl/issues/1718 with - if ((conn->data->set.ftp_filemethod == FTPFILE_NOCWD) && !ftpc->cwdcount) +__________________________________________________________________________________________________________ +/lib/ftp.c +https://github.com/curl/curl/pull/4348 -/lib/ftp.c: - if (ftpc->prevpath) - { -+ if (data->set.ftp_filemethod == FTPFILE_NOCWD) -+ { -+ /* -+ CURLFTPMETHOD_NOCWD -+ if the connection is used for the first time, *no* CWD takes place -+ if the connection is reused, ftp_state_cwd() issues a single "CWD ftpc->entrypath" before the operation -+ in both cases ftp_done() sets ftpc->prevpath to "" after a successfull FTP operation -+ ergo: "" corresponds to ftpc->entrypath, so we only ever need CWD if ftpc->prevpath != "" -+ => avoid needless "CWD /" and reduce folder traversal time with CURLFTPMETHOD_NOCWD by 15-20% -+ */ -+ if (strcmp(ftpc->prevpath, "") == 0) -+ { -+ infof(data, "Request has same path (\"%s\") as previous transfer\n", ftpc->prevpath); -+ ftpc->cwddone = TRUE; -+ } -+ } -+ else -+ { - /* prevpath is "raw" so we convert the input path before we compare the - strings */ - size_t dlen; - char* path; - CURLcode result = - Curl_urldecode(conn->data, data->state.path, 0, &path, &dlen, FALSE); - if (result) - { - freedirs(ftpc); - return result; - } - - dlen -= ftpc->file?strlen(ftpc->file):0; - if ((dlen == strlen(ftpc->prevpath)) && - !strncmp(path, ftpc->prevpath, dlen) && -- (ftpc->prevmethod == data->set.ftp_filemethod)) -+ true) //(ftpc->prevmethod == data->set.ftp_filemethod)) - { - infof(data, "Request has same path as previous transfer\n"); - ftpc->cwddone = TRUE; - } - free(path); -+ } - } +- size_t n = strlen(inpath); +- /* Check if path does not end with /, as then we cut off the file part */ +- if (inpath[n - 1] != '/') +- { +- /* chop off the file part if format is dir/dir/file */ +- slashPos = strrchr(inpath, '/'); +- n = slashPos - inpath; +- } + ++ /* chop off the file part if format is dir/file ++ otherwise remove the trailing slash for dir/dir/ ++ and full paths like %2f/ except for / */ ++ size_t n = strrchr(inpath, '/') - inpath; ++ if(n == 0) ++ ++n; +__________________________________________________________________________________________________________ + +/lib/vtls/openssl.c +https://github.com/curl/curl/issues/4329 + + case SSL_ERROR_ZERO_RETURN: /* no more data */ +- /* close_notify alert */ +- connclose(conn, "TLS close_notify"); +__________________________________________________________________________________________________________ + + +---------------- +| libssh2 Bugs | +---------------- +__________________________________________________________________________________________________________ +move the following constants from src/sftp.h to include/libssh2_sftp.h: + #define MAX_SFTP_OUTGOING_SIZE 30000 + #define MAX_SFTP_READ_SIZE 30000 __________________________________________________________________________________________________________ @@ -215,3 +255,16 @@ Backspace not working in filter dialog: http://www.freefilesync.org/forum/viewto +// g_source_attach(source, NULL); +// } __________________________________________________________________________________________________________ + +wxWidgets/GTK2 on some Linux systems incorrectly detects high DPI: https://freefilesync.org/forum/viewtopic.php?t=6114 +=> hack away high-DPI support for GTK2 (= pretend GTK2 has device independent pixels, which it clearly has not!) + +/include/wx/window.h: + + #include "wx/gtk/window.h" +- #ifdef __WXGTK3__ ++ //#ifdef __WXGTK3__ + #define wxHAVE_DPI_INDEPENDENT_PIXELS +- #endif ++ //#endif +__________________________________________________________________________________________________________ diff --git a/Changelog.txt b/Changelog.txt index 74415e95..9d1713f4 100755 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,25 @@ +FreeFileSync 10.16 [2019-09-16] +------------------------------- +Redesigned progress indicator graphs +Avoid needless HTTP delay prior to Google Drive upload +Skip redundant CWDs during FTP metadata updates +Fixed MLSD 501 syntax error on Serv-U FTP server +Check FTP server status using home directory instead of root +Avoid redundant TYPE changes during FTP directory listing +Access FTP files by full path and avoid CWDs +Support FTP home paths with non-ASCII chars +Workaround libcurl bug failing to buffer FTP TLS authentication +Skip redundant FTP SIZE check before downloading file +Use ISO 8601 week of the year definition for %week% macro +Show login prompt for disconnected NAS share +Force icon resolution to 96 DPI in GTK2 build (Linux) +Detect missing full disk access permission (macOS) +Fixed accessibility issue due to graph color inconsistency +Use short naming convention when deleting abandoned folder lock +Detect endless folder lock recursion on buggy file systems +Fixed Google Drive parsing error for invalid file time + + FreeFileSync 10.15 [2019-08-15] ------------------------------- Redesigned progress indicator stats diff --git a/FreeFileSync/Build/Resources/Icons.zip b/FreeFileSync/Build/Resources/Icons.zip Binary files differindex af35c0b3..3c78d303 100755 --- a/FreeFileSync/Build/Resources/Icons.zip +++ b/FreeFileSync/Build/Resources/Icons.zip diff --git a/FreeFileSync/Build/Resources/Languages/german.lng b/FreeFileSync/Build/Resources/Languages/german.lng index 45c61939..47ae6f4e 100755 --- a/FreeFileSync/Build/Resources/Languages/german.lng +++ b/FreeFileSync/Build/Resources/Languages/german.lng @@ -387,12 +387,12 @@ Tatsächlich: %y bytes <source>Database file is corrupted:</source> <target>Die Datenbankdatei ist beschädigt:</target> -<source>The database files do not yet contain information about the last synchronization.</source> -<target>Die Datenbankdateien beinhalten noch keine Informationen zur letzten Synchronisation.</target> - <source>Loading file %x...</source> <target>Lade Datei %x...</target> +<source>The database files do not yet contain information about the last synchronization.</source> +<target>Die Datenbankdateien beinhalten noch keine Informationen zur letzten Synchronisation.</target> + <source>Saving file %x...</source> <target>Speichere Datei %x...</target> @@ -477,6 +477,27 @@ Tatsächlich: %y bytes <source>Update attributes on right</source> <target>Aktualisiere Attribute des rechten Elements</target> +<source>Error parsing file %x, row %y, column %z.</source> +<target>Fehler beim Auswerten der Datei %x, Zeile %y, Spalte %z.</target> + +<source>Services</source> +<target>Dienste</target> + +<source>Show All</source> +<target>Alle einblenden</target> + +<source>Hide Others</source> +<target>Andere ausblenden</target> + +<source>Hide %x</source> +<target>%x ausblenden</target> + +<source>Quit %x</source> +<target>%x beenden</target> + +<source>Cannot set directory locks for the following folders:</source> +<target>Die Verzeichnissperren können für die folgenden Ordner nicht gesetzt werden:</target> + <source>Errors:</source> <target>Fehler:</target> @@ -501,27 +522,6 @@ Tatsächlich: %y bytes <source>Cleaning up log files:</source> <target>Bereinige Protokolldateien:</target> -<source>Error parsing file %x, row %y, column %z.</source> -<target>Fehler beim Auswerten der Datei %x, Zeile %y, Spalte %z.</target> - -<source>Services</source> -<target>Dienste</target> - -<source>Show All</source> -<target>Alle einblenden</target> - -<source>Hide Others</source> -<target>Andere ausblenden</target> - -<source>Hide %x</source> -<target>%x ausblenden</target> - -<source>Quit %x</source> -<target>%x beenden</target> - -<source>Cannot set directory locks for the following folders:</source> -<target>Die Verzeichnissperren können für die folgenden Ordner nicht gesetzt werden:</target> - <source> <pluralform>1 thread</pluralform> <pluralform>%x threads</pluralform> @@ -1441,6 +1441,21 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. <source>Highlight configurations that have not been run for more than the following number of days:</source> <target>Konfigurationen hervorheben, die seit mehr als die folgende Anzahl an Tagen nicht mehr ausgeführt wurden:</target> +<source>FreeFileSync requires access rights to avoid "Operation not permitted" errors when synchronizing your data (e.g. Mail, Messages, Calendars).</source> +<target>FreeFileSync benötigt Zugriffsrechte, um "Operation nicht erlaubt" Fehler bei der Synchronisation deiner Daten (z.B. E-Mail, Nachrichten, Kalender) zu vermeiden.</target> + +<source>Locate the FreeFileSync app</source> +<target>Die FreeFileSync App finden</target> + +<source>Open Security && Privacy</source> +<target>Systemeinstellungen/Sicherheit öffnen</target> + +<source>Click the lock to allow changes.</source> +<target>Das Schloss anklicken und Änderungen erlauben.</target> + +<source>Drag FreeFileSync into the panel.</source> +<target>FreeFileSync in das Fenster ziehen.</target> + <source>Synchronization Settings</source> <target>Synchronisationseinstellungen</target> @@ -1465,6 +1480,9 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. <source>Highlight Configurations</source> <target>Konfigurationen hervorheben</target> +<source>Grant Full Disk Access</source> +<target>Festplattenvollzugriff gewähren</target> + <source>Info</source> <target>Info</target> @@ -1525,33 +1543,6 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. <source>&Execute</source> <target>&Ausführen</target> -<source> -<pluralform>1 directory</pluralform> -<pluralform>%x directories</pluralform> -</source> -<target> -<pluralform>1 Verzeichnis</pluralform> -<pluralform>%x Verzeichnisse</pluralform> -</target> - -<source> -<pluralform>1 file</pluralform> -<pluralform>%x files</pluralform> -</source> -<target> -<pluralform>1 Datei</pluralform> -<pluralform>%x Dateien</pluralform> -</target> - -<source> -<pluralform>Showing %y of 1 row</pluralform> -<pluralform>Showing %y of %x rows</pluralform> -</source> -<target> -<pluralform>Zeige %y von 1 Zeile</pluralform> -<pluralform>Zeige %y von %x Zeilen</pluralform> -</target> - <source>Set direction:</source> <target>Setze Richtung:</target> @@ -1690,6 +1681,33 @@ Dadurch wird ein konsistenter Datenstand auch bei schweren Fehlern garantiert. <source>All files are in sync</source> <target>Alle Dateien sind synchron</target> +<source> +<pluralform>1 directory</pluralform> +<pluralform>%x directories</pluralform> +</source> +<target> +<pluralform>1 Verzeichnis</pluralform> +<pluralform>%x Verzeichnisse</pluralform> +</target> + +<source> +<pluralform>1 file</pluralform> +<pluralform>%x files</pluralform> +</source> +<target> +<pluralform>1 Datei</pluralform> +<pluralform>%x Dateien</pluralform> +</target> + +<source> +<pluralform>Showing %y of 1 row</pluralform> +<pluralform>Showing %y of %x rows</pluralform> +</source> +<target> +<pluralform>Zeige %y von 1 Zeile</pluralform> +<pluralform>Zeige %y von %x Zeilen</pluralform> +</target> + <source>Cannot find %x</source> <target>%x wurde nicht gefunden.</target> diff --git a/FreeFileSync/Build/Resources/cacert.pem b/FreeFileSync/Build/Resources/cacert.pem index 8e92f772..65be2181 100755 --- a/FreeFileSync/Build/Resources/cacert.pem +++ b/FreeFileSync/Build/Resources/cacert.pem @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Wed May 15 03:12:09 2019 GMT +## Certificate data from Mozilla as of: Wed Aug 28 03:12:10 2019 GMT ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates @@ -14,7 +14,7 @@ ## Just configure this file as the SSLCACertificateFile. ## ## Conversion done with mk-ca-bundle.pl version 1.27. -## SHA256: 61eaa79ac46d923f2f74dfe401189424e96fa8736102b47ba2cdb4ea19af2cc8 +## SHA256: fffa309937c3be940649293f749b8207fabc6eb224e50e4bb3f2c5e44e0d6a6b ## @@ -2613,37 +2613,6 @@ kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su -----END CERTIFICATE----- -Certinomis - Root CA -==================== ------BEGIN CERTIFICATE----- -MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjETMBEGA1UEChMK -Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAbBgNVBAMTFENlcnRpbm9taXMg -LSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMzMTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIx -EzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRD -ZXJ0aW5vbWlzIC0gUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQos -P5L2fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJflLieY6pOo -d5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQVWZUKxkd8aRi5pwP5ynap -z8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDFTKWrteoB4owuZH9kb/2jJZOLyKIOSY00 -8B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09x -RLWtwHkziOC/7aOgFLScCbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE -6OXWk6RiwsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJwx3t -FvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SGm/lg0h9tkQPTYKbV -PZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4F2iw4lNVYC2vPsKD2NkJK/DAZNuH -i5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZngWVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGj -YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I -6tNxIqSSaHh02TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF -AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/0KGRHCwPT5iV -WVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWwF6YSjNRieOpWauwK0kDDPAUw -Pk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZSg081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAX -lCOotQqSD7J6wWAsOMwaplv/8gzjqh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJ -y29SWwNyhlCVCNSNh4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9 -Iff/ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8Vbtaw5Bng -DwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwjY/M50n92Uaf0yKHxDHYi -I0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nM -cyrDflOR1m749fPH0FFNjkulW+YZFzvWgQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVr -hkIGuUE= ------END CERTIFICATE----- - OISTE WISeKey Global Root GB CA =============================== -----BEGIN CERTIFICATE----- diff --git a/FreeFileSync/Source/afs/abstract.h b/FreeFileSync/Source/afs/abstract.h index 535d4114..85916fcd 100644 --- a/FreeFileSync/Source/afs/abstract.h +++ b/FreeFileSync/Source/afs/abstract.h @@ -468,6 +468,7 @@ AbstractFileSystem::FinalizeResult AbstractFileSystem::OutputStream::finalize() replaceCpy(replaceCpy(_("Unexpected size of data stream.\nExpected: %x bytes\nActual: %y bytes"), L"%x", numberTo<std::wstring>(*bytesExpected_)), L"%y", numberTo<std::wstring>(bytesWrittenTotal_))); + warn_static("somehow indicate that this is about source, and not the file name presented") const FinalizeResult result = outStream_->finalize(); //throw FileError, X finalizeSucceeded_ = true; diff --git a/FreeFileSync/Source/afs/ftp.cpp b/FreeFileSync/Source/afs/ftp.cpp index 4901168d..af3605e4 100644 --- a/FreeFileSync/Source/afs/ftp.cpp +++ b/FreeFileSync/Source/afs/ftp.cpp @@ -25,6 +25,10 @@ namespace { Zstring concatenateFtpFolderPathPhrase(const FtpLoginInfo& login, const AfsPath& afsPath); //noexcept +/* + Extensions to FTP: https://tools.ietf.org/html/rfc3659 +*/ + const std::chrono::seconds FTP_SESSION_MAX_IDLE_TIME (20); const std::chrono::seconds FTP_SESSION_CLEANUP_INTERVAL(4); const int FTP_STREAM_BUFFER_SIZE = 512 * 1024; //unit: [byte] @@ -224,7 +228,7 @@ public: template <class Function> //expects non-empty range! std::string readRange(Function acceptChar) //throw SysError { - auto itEnd = std::find_if(it_, line_.end(), std::not_fn(acceptChar)); + auto itEnd = std::find_if_not(it_, line_.end(), acceptChar); std::string output(it_, itEnd); if (output.empty()) throw SysError(L"Expected char range not found."); @@ -241,37 +245,52 @@ private: //---------------------------------------------------------------------------------------------------------------- -std::wstring tryFormatFtpErrorCode(int ec) //https://en.wikipedia.org/wiki/List_of_FTP_server_return_codes +std::wstring formatFtpStatusCode(int sc) { - if (ec == 400) return L"The command was not accepted but the error condition is temporary."; - if (ec == 421) return L"Service not available, closing control connection."; - if (ec == 425) return L"Cannot open data connection."; - if (ec == 426) return L"Connection closed; transfer aborted."; - if (ec == 430) return L"Invalid username or password."; - if (ec == 431) return L"Need some unavailable resource to process security."; - if (ec == 434) return L"Requested host unavailable."; - if (ec == 450) return L"Requested file action not taken."; - if (ec == 451) return L"Local error in processing."; - if (ec == 452) return L"Insufficient storage space in system. File unavailable, e.g. file busy."; - if (ec == 500) return L"Syntax error, command unrecognized or command line too long."; - if (ec == 501) return L"Syntax error in parameters or arguments."; - if (ec == 502) return L"Command not implemented."; - if (ec == 503) return L"Bad sequence of commands."; - if (ec == 504) return L"Command not implemented for that parameter."; - if (ec == 521) return L"Data connection cannot be opened with this PROT setting."; - if (ec == 522) return L"Server does not support the requested network protocol."; - if (ec == 530) return L"User not logged in."; - if (ec == 532) return L"Need account for storing files."; - if (ec == 533) return L"Command protection level denied for policy reasons."; - if (ec == 534) return L"Could not connect to server; issue regarding SSL."; - if (ec == 535) return L"Failed security check."; - if (ec == 536) return L"Requested PROT level not supported by mechanism."; - if (ec == 537) return L"Command protection level not supported by security mechanism."; - if (ec == 550) return L"File unavailable, e.g. file not found, no access."; - if (ec == 551) return L"Requested action aborted. Page type unknown."; - if (ec == 552) return L"Requested file action aborted. Exceeded storage allocation."; - if (ec == 553) return L"File name not allowed."; - return L""; + const wchar_t* statusText = [&] //https://en.wikipedia.org/wiki/List_of_FTP_server_return_codes + { + switch (sc) + { + //*INDENT-OFF* + case 400: return L"The command was not accepted but the error condition is temporary."; + case 421: return L"Service not available, closing control connection."; + case 425: return L"Cannot open data connection."; + case 426: return L"Connection closed; transfer aborted."; + case 430: return L"Invalid username or password."; + case 431: return L"Need some unavailable resource to process security."; + case 434: return L"Requested host unavailable."; + case 450: return L"Requested file action not taken."; + case 451: return L"Local error in processing."; + case 452: return L"Insufficient storage space in system. File unavailable, e.g. file busy."; + + case 500: return L"Syntax error, command unrecognized or command line too long."; + case 501: return L"Syntax error in parameters or arguments."; + case 502: return L"Command not implemented."; + case 503: return L"Bad sequence of commands."; + case 504: return L"Command not implemented for that parameter."; + case 521: return L"Data connection cannot be opened with this PROT setting."; + case 522: return L"Server does not support the requested network protocol."; + case 530: return L"User not logged in."; + case 532: return L"Need account for storing files."; + case 533: return L"Command protection level denied for policy reasons."; + case 534: return L"Could not connect to server; issue regarding SSL."; + case 535: return L"Failed security check."; + case 536: return L"Requested PROT level not supported by mechanism."; + case 537: return L"Command protection level not supported by security mechanism."; + case 550: return L"File unavailable, e.g. file not found, no access."; + case 551: return L"Requested action aborted. Page type unknown."; + case 552: return L"Requested file action aborted. Exceeded storage allocation."; + case 553: return L"File name not allowed."; + + default: return L""; + //*INDENT-ON* + } + }(); + + if (strLength(statusText) == 0) + return trimCpy(replaceCpy<std::wstring>(L"FTP status %x.", L"%x", numberTo<std::wstring>(sc))); + else + return trimCpy(replaceCpy<std::wstring>(L"FTP status %x: ", L"%x", numberTo<std::wstring>(sc)) + statusText); } //================================================================================================================ @@ -309,7 +328,7 @@ public: }; //returns server response (header data) - std::string perform(const AfsPath* afsPath /*optional, use last-used path if null*/, bool isDir, + std::string perform(const AfsPath& afsPath, bool isDir, curl_ftpmethod pathMethod, const std::vector<Option>& extraOptions, bool requiresUtf8, int timeoutSec) //throw SysError { if (requiresUtf8) //avoid endless recursion @@ -319,7 +338,7 @@ public: { easyHandle_ = ::curl_easy_init(); if (!easyHandle_) - throw SysError(formatSystemError(L"curl_easy_init", formatCurlErrorRaw(CURLE_OUT_OF_MEMORY), std::wstring())); + throw SysError(formatSystemError(L"curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), std::wstring())); } else ::curl_easy_reset(easyHandle_); @@ -341,42 +360,29 @@ public: options.emplace_back(CURLOPT_HEADERDATA, &headerData_); options.emplace_back(CURLOPT_HEADERFUNCTION, onHeaderReceived); - std::string curlPath; //lifetime: keep alive until after curl_easy_setopt() below - if (std::any_of(extraOptions.begin(), extraOptions.end(), [](const Option& opt) { return opt.option == CURLOPT_FTP_FILEMETHOD && opt.value == CURLFTPMETHOD_NOCWD; })) - { - //CURLFTPMETHOD_NOCWD case => CURLOPT_URL will not be used for CWD but as argument, e.g., for MLSD - //curl was fixed to expect encoded paths in this case, too: https://github.com/curl/curl/issues/1974 - AfsPath targetPath; - bool targetPathisDir = true; - if (afsPath) - { - targetPath = *afsPath; - targetPathisDir = isDir; - } - curlPath = getCurlUrlPath(targetPath, targetPathisDir, timeoutSec); //throw SysError - workingDirPath_ = AfsPath(); - } - else - { - AfsPath currentPath; - bool currentPathisDir = true; - if (afsPath) - { - currentPath = *afsPath; - currentPathisDir = isDir; - } - else //try to use libcurl's last-used working dir and avoid excess CWD round trips - if (getActiveSocket()) //throw SysError - currentPath = workingDirPath_; - //what if our last curl_easy_perform() just deleted the working directory???? - //=> 1. libcurl recognizes last-used path and avoids the CWD accordingly 2. commands that depend on the working directory, e.g. PWD will fail on *some* servers - - curlPath = getCurlUrlPath(currentPath, currentPathisDir, timeoutSec); //throw SysError - workingDirPath_ = currentPathisDir ? currentPath : AfsPath(beforeLast(currentPath.value, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_NONE)); - //remember libcurl's working dir: path might not exist => make sure to clear if ::curl_easy_perform() fails! - } + //lifetime: keep alive until after curl_easy_setopt() below + const std::string curlPath = getCurlUrlPath(afsPath, isDir, timeoutSec); //throw SysError options.emplace_back(CURLOPT_URL, curlPath.c_str()); + options.emplace_back(CURLOPT_FTP_FILEMETHOD, pathMethod); + + assert(pathMethod != CURLFTPMETHOD_MULTICWD); //too slow! + assert(pathMethod != CURLFTPMETHOD_NOCWD); //too buggy! + /* "wrong dir listing because libcurl remembers wrong CWD": https://github.com/curl/curl/issues/1782 + + => "fixed" by adding only the "if (data->set.ftp_filemethod == FTPFILE_NOCWD)" below: https://github.com/curl/curl/issues/1811 + => this is NOT enough! consider what happens for a reused connection that first used CURLFTPMETHOD_MULTICWD, now CURLFTPMETHOD_NOCWD: + + the code in ftp_state_cwd() will issue a CWD sequence that ends with "ftpc->cwdcount == 1"!!! See "if (++ftpc->cwdcount <= ftpc->dirdepth)" + => this skips the previous "fix" in https://github.com/curl/curl/issues/1718 with + if ((conn->data->set.ftp_filemethod == FTPFILE_NOCWD) && !ftpc->cwdcount) + ------------------------------------------------------------ + workaround => use absolute paths only! + ------------------------------------------------------------ + CURLFTPMETHOD_NOCWD doesn't work as advertized: "CWD is sent despite CURLOPT_QUOTE/CURLOPT_NOBODY" https://github.com/curl/curl/issues/1443 + */ + + warn_static("what if server uses ansii encoding") const auto username = utfTo<std::string>(sessionId_.username); const auto password = utfTo<std::string>(sessionId_.password); if (!username.empty()) //else: libcurl handles anonymous login for us (including fake email as password) @@ -385,6 +391,15 @@ public: options.emplace_back(CURLOPT_PASSWORD, password.c_str()); } + + warn_static("remove after test") + //const auto username2 = utfToAnsiEncoding(sessionId_.username); + //options.emplace_back(CURLOPT_USERNAME, username2.c_str()); + + + + + if (sessionId_.port > 0) options.emplace_back(CURLOPT_PORT, static_cast<long>(sessionId_.port)); @@ -402,13 +417,6 @@ public: //CURLOPT_ACCEPTTIMEOUT_MS? => only relevant for "active" FTP connections - if (!std::any_of(extraOptions.begin(), extraOptions.end(), [](const Option& opt) { return opt.option == CURLOPT_FTP_FILEMETHOD; })) - options.emplace_back(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD); - //let's save these needless round trips!! most servers should support "CWD /folder/subfolder" - //=> 15% faster folder traversal time compared to CURLFTPMETHOD_MULTICWD! - //CURLFTPMETHOD_NOCWD? Already set in the MLSD case; but use for legacy servers, too? supported? - - //Use share interface? https://curl.haxx.se/libcurl/c/libcurl-share.html //perf test, 4 and 8 parallel threads: // CURL_LOCK_DATA_DNS => no measurable total time difference @@ -491,7 +499,7 @@ public: #endif if (sessionId_.useTls) //https://tools.ietf.org/html/rfc4217 { - options.emplace_back(CURLOPT_USE_SSL, CURLUSESSL_ALL); //require SSL for both control and data + options.emplace_back(CURLOPT_USE_SSL, CURLUSESSL_ALL); //require SSL for both control and data options.emplace_back(CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS); //try TLS first, then SSL (currently: CURLFTPAUTH_DEFAULT == CURLFTPAUTH_SSL) } @@ -507,14 +515,14 @@ public: const CURLcode rc = ::curl_easy_setopt(easyHandle_, opt.option, opt.value); if (rc != CURLE_OK) throw SysError(formatSystemError(L"curl_easy_setopt " + numberTo<std::wstring>(opt.option), - formatCurlErrorRaw(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); + formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); } //======================================================================================================= const CURLcode rcPerf = ::curl_easy_perform(easyHandle_); //WTF: curl_easy_perform() considers FTP response codes 4XX, 5XX as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!! - long ftpStatus = 0; //optional - /*const CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &ftpStatus); + long ftpStatusCode = 0; //optional + /*const CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_RESPONSE_CODE, &ftpStatusCode); //note: CURLOPT_FAILONERROR(default:off) is only available for HTTP //assert((rcPerf == CURLE_OK && 100 <= ftpStatus && ftpStatus < 400) || -> insufficient *FEAT can fail with 550, but still CURLE_OK because of * @@ -522,10 +530,7 @@ public: //======================================================================================================= if (rcPerf != CURLE_OK) - { - workingDirPath_ = AfsPath(); //not sure what went wrong; no idea where libcurl's working dir currently is => libcurl might even have closed the old session! - throw SysError(formatLastCurlError(L"curl_easy_perform", rcPerf, ftpStatus)); - } + throw SysError(formatLastCurlError(L"curl_easy_perform", rcPerf, ftpStatusCode)); lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); return headerData_; @@ -538,33 +543,83 @@ public: ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(quote)); quote = ::curl_slist_append(quote, ftpCmd.c_str()); - std::vector<FtpSession::Option> options = + return perform(AfsPath(), true /*isDir*/, CURLFTPMETHOD_FULLPATH, //really avoid needless CWDs unlike buggy(!) CURLFTPMETHOD_NOCWD { - FtpSession::Option(CURLOPT_NOBODY, 1L), - FtpSession::Option(CURLOPT_QUOTE, quote), - }; + { CURLOPT_NOBODY, 1L }, + { CURLOPT_QUOTE, quote }, + }, requiresUtf8, timeoutSec); //throw SysError + } - //observation: libcurl sends CWD *after* CURLOPT_QUOTE has run - //perf: we neither need nor want libcurl to send CWD - return perform(nullptr /*re-use last-used path*/, true /*isDir*/, options, requiresUtf8, timeoutSec); //throw SysError + void testConnection(int timeoutSec) //throw SysError + { + //FEAT: are there servers that don't support this command? fuck, yes: "550 FEAT: Operation not permitted" => buggy server not granting access, despite support! + if (supportsFeat(timeoutSec)) //throw SysError + runSingleFtpCommand("FEAT", false /*requiresUtf8*/, timeoutSec); //throw SysError + else + //PWD? will fail if last access deleted the working dir! + //"TYPE I"? might interfere with libcurls internal handling, but that's an improvement, right? right? :> + //=> but "HELP", and "NOOP" work, right?? https://en.wikipedia.org/wiki/List_of_FTP_commands + //Fuck my life: even "HELP" is not always implemented: https://freefilesync.org/forum/viewtopic.php?t=6002 + runSingleFtpCommand("HELP", false /*requiresUtf8*/, timeoutSec); //throw SysError + //=> are there servers supporting neither FEAT nor HELP? only time will tell... } AfsPath getHomePath(int timeoutSec) //throw SysError { - perform(nullptr /*re-use last-used path*/, true /*isDir*/, - { FtpSession::Option(CURLOPT_NOBODY, 1L) }, true /*requiresUtf8*/, timeoutSec); //throw SysError - assert(easyHandle_); + if (!homePathCached_) + homePathCached_ = [&] + { + if (!easyHandle_) + testConnection(timeoutSec); //throw SysError + assert(easyHandle_); + + const char* homePathCurl = nullptr; //not owned + /*CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_FTP_ENTRY_PATH, &homePathCurl); + + if (homePathCurl && isAsciiString(homePathCurl)) + return sanitizeRootRelativePath(utfTo<Zstring>(homePathCurl)); - const char* homePath = nullptr; //not owned - /*CURLcode rc =*/ ::curl_easy_getinfo(easyHandle_, CURLINFO_FTP_ENTRY_PATH, &homePath); + //home path with non-ASCII chars: libcurl issues PWD right after login *before* server was set up for UTF8 + //=> CURLINFO_FTP_ENTRY_PATH could be in any encoding => useless! + // Test case: Windows 10 IIS FTP with non-Ascii entry path + //=> start new FTP session and parse PWD *after* UTF8 is enabled: + if (easyHandle_) + { + ::curl_easy_cleanup(easyHandle_); + easyHandle_ = nullptr; + } - if (!homePath) - return AfsPath(); - return sanitizeRootRelativePath(utfTo<Zstring>(homePath)); + for (const std::string& line : splitFtpResponse(runSingleFtpCommand("PWD", true /*requiresUtf8*/, timeoutSec))) //throw SysError + if (startsWith(line, "257 ")) + { + /* 257<space>[rubbish]"<directory-name>"<space><commentary> according to libcurl + + "The directory name can contain any character; embedded double-quotes should be escaped by + double-quotes (the "quote-doubling" convention)." https://tools.ietf.org/html/rfc959 */ + auto itBegin = std::find(line.begin(), line.end(), '"'); + if (itBegin != line.end()) + for (auto it = ++itBegin; it != line.end(); ++it) + if (*it == '"') + { + if (it + 1 != line.end() && it[1] == '"') + ++it; //skip double quote + else + { + const std::string homePathRaw = replaceCpy<std::string>({ itBegin, it }, "\"\"", '"'); + const ServerEncoding enc = getServerEncoding(timeoutSec); //throw SysError + const Zstring homePathUtf = serverToUtfEncoding(homePathRaw, enc); //throw SysError + return sanitizeRootRelativePath(homePathUtf); + } + } + } + return AfsPath(); //error: home path could not be determined + }(); + return *homePathCached_; } //------------------------------------------------------------------------------------------------------------ + bool supportsFeat(int timeoutSec) { return getFeatureSupport(&Features::feat, timeoutSec); } // bool supportsMlsd(int timeoutSec) { return getFeatureSupport(&Features::mlsd, timeoutSec); } // bool supportsMfmt(int timeoutSec) { return getFeatureSupport(&Features::mfmt, timeoutSec); } //throw SysError bool supportsClnt(int timeoutSec) { return getFeatureSupport(&Features::clnt, timeoutSec); } // @@ -593,11 +648,9 @@ private: FtpSession (const FtpSession&) = delete; FtpSession& operator=(const FtpSession&) = delete; - std::string getCurlUrlPath(const AfsPath& afsPath, bool isDir, int timeoutSec) //throw SysError + std::string getCurlUrlPath(const AfsPath& afsPath /*optional*/, bool isDir, int timeoutSec) //throw SysError { - //Some FTP servers distinguish between user-home- and root-relative paths! e.g. FreeNAS: https://freefilesync.org/forum/viewtopic.php?t=6129 - //=> use root-relative paths (= same as expected by CURLOPT_QUOTE) - std::string curlRelPath = "/%2f"; //https://curl.haxx.se/docs/faq.html#How_do_I_list_the_root_dir_of_an + std::string curlRelPath; //libcurl expects encoded paths (except for '/' char!!!) for (const std::string& comp : split(getServerRelPathInternal(afsPath, timeoutSec), '/', SplitType::SKIP_EMPTY)) //throw SysError { @@ -606,16 +659,21 @@ private: throw SysError(replaceCpy<std::wstring>(L"curl_easy_escape: conversion failure (%x)", L"%x", utfTo<std::wstring>(comp))); ZEN_ON_SCOPE_EXIT(::curl_free(compFmt)); + if (!curlRelPath.empty()) + curlRelPath += '/'; curlRelPath += compFmt; - curlRelPath += '/'; } - if (endsWith(curlRelPath, '/')) - curlRelPath.pop_back(); - std::string path = utfTo<std::string>(Zstring(ftpPrefix) + Zstr("//") + sessionId_.server) + curlRelPath; + /* 1. FFS CURLFTPMETHOD_NOCWD is buggy (see comment FtpSession::perform()) => must use absolute, not home-relative paths! + 2. Support CURLFTPMETHOD_FULLPATH => must use absolute, not home-relative paths! + 3. Some FTP servers distinguish between user-home- and root-relative paths! e.g. FreeNAS: https://freefilesync.org/forum/viewtopic.php?t=6129 + => use root-relative paths (= same as expected by CURLOPT_QUOTE) https://curl.haxx.se/docs/faq.html#How_do_I_list_the_root_dir_of_an + => use // because /%2f had bugs (but they should be fixed: https://github.com/curl/curl/pull/4348) + */ + std::string path = utfTo<std::string>(Zstring(ftpPrefix) + Zstr("//") + sessionId_.server) + "//" + curlRelPath; if (isDir && !endsWith(path, '/')) //curl-FTP needs directory paths to end with a slash - path += "/"; + path += '/'; return path; } @@ -630,7 +688,7 @@ private: { //[!] supportsUtf8() is buffered! => FTP session might not yet exist (or was closed by libcurl after a failure) if (std::optional<curl_socket_t> currentSocket = getActiveSocket()) //throw SysError - if (*currentSocket == utf8EnabledSocket_) //caveat: a non-utf8-enabled session might already exist, e.g. from a previous call to supportsMlsd() + if (*currentSocket == utf8EnabledSocket_) //caveat: a non-UTF8-enabled session might already exist, e.g. from a previous call to supportsMlsd() return; //some servers even require "CLNT" before accepting "OPTS UTF8 ON": https://social.msdn.microsoft.com/Forums/en-US/d602574f-8a69-4d69-b337-52b6081902cf/problem-with-ftpwebrequestopts-utf8-on-501-please-clnt-first @@ -641,7 +699,6 @@ private: //-> ignore if server does not know this legacy command (but report all *other* issues; else getActiveSocket() below won't return value and hide real error!) runSingleFtpCommand("*OPTS UTF8 ON", false /*requiresUtf8*/, timeoutSec); //throw SysError - //make sure our unicode-enabled session is still there (== libcurl behaves as we expect) std::optional<curl_socket_t> currentSocket = getActiveSocket(); //throw SysError if (!currentSocket) @@ -658,7 +715,7 @@ private: curl_socket_t currentSocket = 0; const CURLcode rc = ::curl_easy_getinfo(easyHandle_, CURLINFO_ACTIVESOCKET, ¤tSocket); if (rc != CURLE_OK) - throw SysError(formatSystemError(L"curl_easy_getinfo: CURLINFO_ACTIVESOCKET", formatCurlErrorRaw(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); + throw SysError(formatSystemError(L"curl_easy_getinfo: CURLINFO_ACTIVESOCKET", formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); if (currentSocket != CURL_SOCKET_BAD) return currentSocket; } @@ -667,6 +724,7 @@ private: struct Features { + bool feat = false; //not always enabled (e.g. might be disabled because... who knows why) bool mlsd = false; bool mfmt = false; bool clnt = false; @@ -690,15 +748,10 @@ private: if (!featureCache_) { //ignore errors if server does not support FEAT (do those exist?), but fail for all others - const std::string featResponse = runSingleFtpCommand("*FEAT", false /*requiresUtf8*/, timeoutSec); //throw SysError + featureCache_ = parseFeatResponse(runSingleFtpCommand("*FEAT", false /*requiresUtf8*/, timeoutSec)); //throw SysError //used by sessionEnableUtf8()! => requiresUtf8 = false!!! - sf->access([&](FeatureList& feat) - { - auto& f = feat[sessionId_.server]; - f = parseFeatResponse(featResponse); - featureCache_ = f; - }); + sf->access([&](FeatureList& feat) { feat[sessionId_.server] = featureCache_; }); } } return (*featureCache_).*status; @@ -709,37 +762,41 @@ private: Features output; //FEAT command: https://tools.ietf.org/html/rfc2389#page-4 const std::vector<std::string> lines = splitFtpResponse(featResponse); - auto it = std::find_if(lines.begin(), lines.end(), [](const std::string& line) { return startsWith(line, "211-"); }); - if (it != lines.end()) ++it; - for (; it != lines.end(); ++it) + auto it = std::find_if(lines.begin(), lines.end(), [](const std::string& line) { return startsWith(line, "211-") || startsWith(line, "211 "); }); + if (it != lines.end()) { - const std::string& line = *it; - if ( equalAsciiNoCase(line, "211 End") || - startsWithAsciiNoCase(line, "211 End ")) //e.g. Serv-U: "211 End (for details use "HELP commmand" where command is the command of interest)" - break; - - //https://tools.ietf.org/html/rfc3659#section-7.8 - //"a server-FTP process that supports MLST, and MLSD [...] MUST indicate that this support exists" - //"there is no distinct FEAT output for MLSD. The presence of the MLST feature indicates that both MLST and MLSD are supported" - if (equalAsciiNoCase (line, " MLST") || - startsWithAsciiNoCase(line, " MLST ")) //SP "MLST" [SP factlist] CRLF - output.mlsd = true; - - //https://tools.ietf.org/html/draft-somers-ftp-mfxx-04#section-3.3 - //"Where a server-FTP process supports the MFMT command [...] it MUST include the response to the FEAT command" - else if (equalAsciiNoCase(line, " MFMT")) //SP "MFMT" CRLF - output.mfmt = true; - - else if (equalAsciiNoCase(line, " UTF8")) - output.utf8 = true; - - else if (equalAsciiNoCase(line, " CLNT")) - output.clnt = true; + output.feat = true; + ++it; + for (; it != lines.end(); ++it) + { + const std::string& line = *it; + if (equalAsciiNoCase(line, "211 End") || + startsWithAsciiNoCase(line, "211 End ")) //Serv-U: "211 End (for details use "HELP commmand" where command is the command of interest)" + break; //Home Ftp Server: "211 End of extentions." + + //https://tools.ietf.org/html/rfc3659#section-7.8 + //"a server-FTP process that supports MLST, and MLSD [...] MUST indicate that this support exists" + //"there is no distinct FEAT output for MLSD. The presence of the MLST feature indicates that both MLST and MLSD are supported" + if (equalAsciiNoCase (line, " MLST") || + startsWithAsciiNoCase(line, " MLST ")) //SP "MLST" [SP factlist] CRLF + output.mlsd = true; + + //https://tools.ietf.org/html/draft-somers-ftp-mfxx-04#section-3.3 + //"Where a server-FTP process supports the MFMT command [...] it MUST include the response to the FEAT command" + else if (equalAsciiNoCase(line, " MFMT")) //SP "MFMT" CRLF + output.mfmt = true; + + else if (equalAsciiNoCase(line, " UTF8")) + output.utf8 = true; + + else if (equalAsciiNoCase(line, " CLNT")) + output.clnt = true; + } } return output; } - std::wstring formatLastCurlError(const std::wstring& functionName, CURLcode ec, long ftpResponse) const + std::wstring formatLastCurlError(const std::wstring& functionName, CURLcode ec, long ftpStatusCode) const { std::wstring errorMsg; @@ -753,11 +810,7 @@ private: errorMsg += (errorMsg.empty() ? L"" : L"\n") + trimCpy(utfTo<std::wstring>(headerLines.back())); //that *should* be the servers error response } else //failed to get server response - { - const std::wstring descr = tryFormatFtpErrorCode(ftpResponse); - if (!descr.empty()) - errorMsg += (errorMsg.empty() ? L"" : L"\n") + numberTo<std::wstring>(ftpResponse) + L": " + descr; - } + errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatFtpStatusCode(ftpStatusCode); #if 0 //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html @@ -766,7 +819,7 @@ private: if (nativeErrorCode != 0) errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode); #endif - return formatSystemError(functionName, formatCurlErrorRaw(ec), errorMsg); + return formatSystemError(functionName, formatCurlStatusCode(ec), errorMsg); } const FtpSessionId sessionId_; @@ -774,11 +827,10 @@ private: char curlErrorBuf_[CURL_ERROR_SIZE] = {}; std::string headerData_; - AfsPath workingDirPath_; - curl_socket_t utf8EnabledSocket_ = 0; std::optional<Features> featureCache_; + std::optional<AfsPath> homePathCached_; std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_; std::chrono::steady_clock::time_point lastSuccessfulUseTime_; @@ -941,9 +993,10 @@ public: { std::vector<FtpSession::Option> options = { - FtpSession::Option(CURLOPT_WRITEDATA, &rawListing), - FtpSession::Option(CURLOPT_WRITEFUNCTION, onBytesReceived), + { CURLOPT_WRITEDATA, &rawListing }, + { CURLOPT_WRITEFUNCTION, onBytesReceived }, }; + curl_ftpmethod pathMethod = CURLFTPMETHOD_SINGLECWD; if (session.supportsMlsd(login.timeoutSec)) //throw SysError { @@ -968,12 +1021,12 @@ public: }(); if (!pathHasWildcards) - options.emplace_back(CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD); //16% faster traversal compared to CURLFTPMETHOD_SINGLECWD (35% faster than CURLFTPMETHOD_MULTICWD) + pathMethod = CURLFTPMETHOD_FULLPATH; //16% faster traversal compared to CURLFTPMETHOD_SINGLECWD (35% faster than CURLFTPMETHOD_MULTICWD) } //else: use "LIST" + CURLFTPMETHOD_SINGLECWD + //caveat: let's better not use LIST parameters: https://cr.yp.to/ftp/list.html - session.perform(&afsDirPath, true /*isDir*/, options, true /*requiresUtf8*/, login.timeoutSec); //throw SysError - + session.perform(afsDirPath, true /*isDir*/, pathMethod, options, true /*requiresUtf8*/, login.timeoutSec); //throw SysError const ServerEncoding encoding = session.getServerEncoding(login.timeoutSec); //throw SysError if (session.supportsMlsd(login.timeoutSec)) //throw SysError @@ -1048,16 +1101,15 @@ private: if (tc == TimeComp()) throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(modifyFact) + L")"); - time_t utcTime = utcToTimeT(tc); //returns -1 on error - if (utcTime == -1) + item.modTime = utcToTimeT(tc); //returns -1 on error + if (item.modTime == -1) { if (tc.year == 1600 || //FTP on Windows phone: zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" tc.year == 1601) // => is this also relevant in this context of MLST UTC time?? - utcTime = 0; + item.modTime = 0; else throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(modifyFact) + L")"); } - item.modTime = utcTime; } if (equalAsciiNoCase(typeFact, "cdir")) @@ -1196,13 +1248,13 @@ private: //------------------------------------------------------------------------------------ //user parser.readRange(std::not_fn(isWhiteSpace<char>)); //throw SysError - parser.readRange(&isWhiteSpace<char>); //throw SysError + parser.readRange(&isWhiteSpace<char>); //throw SysError //------------------------------------------------------------------------------------ //group if (haveGroup) { parser.readRange(std::not_fn(isWhiteSpace<char>)); //throw SysError - parser.readRange(&isWhiteSpace<char>); //throw SysError + parser.readRange(&isWhiteSpace<char>); //throw SysError } //------------------------------------------------------------------------------------ //file size (no separators) @@ -1539,11 +1591,12 @@ void ftpFileDownload(const FtpLoginInfo& login, const AfsPath& afsFilePath, //th { accessFtpSession(login, [&](FtpSession& session) //throw SysError { - session.perform(&afsFilePath, false /*isDir*/, //throw SysError + session.perform(afsFilePath, false /*isDir*/, CURLFTPMETHOD_FULLPATH, //are there any servers that require CURLFTPMETHOD_SINGLECWD? let's find out { - FtpSession::Option(CURLOPT_WRITEDATA, &onBytesReceived), - FtpSession::Option(CURLOPT_WRITEFUNCTION, onBytesReceivedWrapper), - }, true /*requiresUtf8*/, login.timeoutSec); + { CURLOPT_WRITEDATA, &onBytesReceived }, + { CURLOPT_WRITEFUNCTION, onBytesReceivedWrapper }, + { CURLOPT_IGNORE_CONTENT_LENGTH, 1L }, //skip FTP "SIZE" command before download (=> download until actual EOF if file size changes) + }, true /*requiresUtf8*/, login.timeoutSec); //throw SysError }); } catch (const SysError& e) @@ -1604,18 +1657,18 @@ void ftpFileUpload(const FtpLoginInfo& login, const AfsPath& afsFilePath, //thro //optimize fail-safe copy with RNFR/RNTO as CURLOPT_POSTQUOTE? -> even slightly *slower* than RNFR/RNTO as additional curl_easy_perform() */ - session.perform(&afsFilePath, false /*isDir*/, //throw SysError + session.perform(afsFilePath, false /*isDir*/, CURLFTPMETHOD_FULLPATH, //are there any servers that require CURLFTPMETHOD_SINGLECWD? let's find out { - FtpSession::Option(CURLOPT_UPLOAD, 1L), - //FtpSession::Option(CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(inputBuffer.size())), - //=> CURLOPT_INFILESIZE_LARGE does not issue a specific FTP command, but is used by libcurl only! + { CURLOPT_UPLOAD, 1L }, + { CURLOPT_READDATA, &getBytesToSend }, + { CURLOPT_READFUNCTION, getBytesToSendWrapper }, - FtpSession::Option(CURLOPT_READDATA, &getBytesToSend), - FtpSession::Option(CURLOPT_READFUNCTION, getBytesToSendWrapper), + //{ CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(inputBuffer.size()) }, + //=> CURLOPT_INFILESIZE_LARGE does not issue a specific FTP command, but is used by libcurl only! - //FtpSession::Option(CURLOPT_PREQUOTE, quote), - //FtpSession::Option(CURLOPT_POSTQUOTE, quote), - }, true /*requiresUtf8*/, login.timeoutSec); + //{ CURLOPT_PREQUOTE, quote }, + //{ CURLOPT_POSTQUOTE, quote }, + }, true /*requiresUtf8*/, login.timeoutSec); //throw SysError }); } catch (const SysError& e) @@ -1841,16 +1894,16 @@ private: //don't use MLST: broken for Pure-FTPd: https://freefilesync.org/forum/viewtopic.php?t=4287 const std::optional<AfsPath> parentAfsPath = getParentPath(afsPath); - if (!parentAfsPath) //device root => quick access tests: just see if the server responds at all! - { - //don't use PWD: if last access deleted the working dir, PWD will fail on some servers, e.g. https://freefilesync.org/forum/viewtopic.php?t=4314 - //FEAT: are there servers that don't support this command? fuck, yes: "550 FEAT: Operation not permitted" => buggy server not granting access, despite support! - //=> but "HELP", and "NOOP" work, right?? https://en.wikipedia.org/wiki/List_of_FTP_commands - //Fuck my life: even "HELP" is not always implemented: https://freefilesync.org/forum/viewtopic.php?t=6002 - //Screw this, just traverse the root folder: (only a single round-trip for FTP) - /*std::vector<FtpItem> items =*/ FtpDirectoryReader::execute(login_, afsPath); //throw FileError - return ItemType::FOLDER; - } + if (!parentAfsPath) //device root => do a quick access tests to see if the server responds at all! + try + { + accessFtpSession(login_, [&](FtpSession& session) //throw SysError + { + session.testConnection(login_.timeoutSec); //throw SysError + }); + return ItemType::FOLDER; + } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getCurlDisplayPath(login_.server, afsPath))), e.toString()); } const Zstring itemName = getItemName(afsPath); assert(!itemName.empty()); @@ -2073,11 +2126,11 @@ private: quote = ::curl_slist_append(quote, ("RNFR " + session.getServerRelPathInternal(pathFrom, login_.timeoutSec)).c_str()); //throw SysError quote = ::curl_slist_append(quote, ("RNTO " + session.getServerRelPathInternal(pathTo.afsPath, login_.timeoutSec)).c_str()); // - session.perform(nullptr /*re-use last-used path*/, true /*isDir*/, //throw SysError + session.perform(AfsPath(), true /*isDir*/, CURLFTPMETHOD_FULLPATH, //really avoid needless CWDs unlike buggy(!) CURLFTPMETHOD_NOCWD { - FtpSession::Option(CURLOPT_NOBODY, 1L), - FtpSession::Option(CURLOPT_QUOTE, quote), - }, true /*requiresUtf8*/, login_.timeoutSec); + { CURLOPT_NOBODY, 1L }, + { CURLOPT_QUOTE, quote }, + }, true /*requiresUtf8*/, login_.timeoutSec); //throw SysError }); } catch (const SysError& e) diff --git a/FreeFileSync/Source/afs/gdrive.cpp b/FreeFileSync/Source/afs/gdrive.cpp index b55350bf..9b885036 100644 --- a/FreeFileSync/Source/afs/gdrive.cpp +++ b/FreeFileSync/Source/afs/gdrive.cpp @@ -138,61 +138,6 @@ std::wstring formatGoogleErrorRaw(const std::string& serverResponse) return utfTo<std::wstring>(serverResponse); } - -std::wstring tryFormatHttpErrorCode(int ec) //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes -{ - if (ec == 300) return L"Multiple Choices."; - if (ec == 301) return L"Moved Permanently."; - if (ec == 302) return L"Moved temporarily."; - if (ec == 303) return L"See Other"; - if (ec == 304) return L"Not Modified."; - if (ec == 305) return L"Use Proxy."; - if (ec == 306) return L"Switch Proxy."; - if (ec == 307) return L"Temporary Redirect."; - if (ec == 308) return L"Permanent Redirect."; - - if (ec == 400) return L"Bad Request."; - if (ec == 401) return L"Unauthorized."; - if (ec == 402) return L"Payment Required."; - if (ec == 403) return L"Forbidden."; - if (ec == 404) return L"Not Found."; - if (ec == 405) return L"Method Not Allowed."; - if (ec == 406) return L"Not Acceptable."; - if (ec == 407) return L"Proxy Authentication Required."; - if (ec == 408) return L"Request Timeout."; - if (ec == 409) return L"Conflict."; - if (ec == 410) return L"Gone."; - if (ec == 411) return L"Length Required."; - if (ec == 412) return L"Precondition Failed."; - if (ec == 413) return L"Payload Too Large."; - if (ec == 414) return L"URI Too Long."; - if (ec == 415) return L"Unsupported Media Type."; - if (ec == 416) return L"Range Not Satisfiable."; - if (ec == 417) return L"Expectation Failed."; - if (ec == 418) return L"I'm a teapot."; - if (ec == 421) return L"Misdirected Request."; - if (ec == 422) return L"Unprocessable Entity."; - if (ec == 423) return L"Locked."; - if (ec == 424) return L"Failed Dependency."; - if (ec == 426) return L"Upgrade Required."; - if (ec == 428) return L"Precondition Required."; - if (ec == 429) return L"Too Many Requests."; - if (ec == 431) return L"Request Header Fields Too Large."; - if (ec == 451) return L"Unavailable For Legal Reasons."; - - if (ec == 500) return L"Internal Server Error."; - if (ec == 501) return L"Not Implemented."; - if (ec == 502) return L"Bad Gateway."; - if (ec == 503) return L"Service Unavailable."; - if (ec == 504) return L"Gateway Timeout."; - if (ec == 505) return L"HTTP Version Not Supported."; - if (ec == 506) return L"Variant Also Negotiates."; - if (ec == 507) return L"Insufficient Storage."; - if (ec == 508) return L"Loop Detected."; - if (ec == 510) return L"Not Extended."; - if (ec == 511) return L"Network Authentication Required."; - return L""; -} //---------------------------------------------------------------------------------------------------------------- Global<UniSessionCounter> httpSessionCount(createUniSessionCounter()); @@ -239,7 +184,7 @@ public: { easyHandle_ = ::curl_easy_init(); if (!easyHandle_) - throw SysError(formatSystemError(L"curl_easy_init", formatCurlErrorRaw(CURLE_OUT_OF_MEMORY), std::wstring())); + throw SysError(formatSystemError(L"curl_easy_init", formatCurlStatusCode(CURLE_OUT_OF_MEMORY), std::wstring())); } else ::curl_easy_reset(easyHandle_); @@ -339,9 +284,13 @@ public: //--------------------------------------------------- struct curl_slist* headers = nullptr; //"libcurl will not copy the entire list so you must keep it!" ZEN_ON_SCOPE_EXIT(::curl_slist_free_all(headers)); + for (const std::string& headerLine : extraHeaders) headers = ::curl_slist_append(headers, headerLine.c_str()); + //WTF!!! 1 sec delay when server doesn't support "Expect: 100-continue!! https://stackoverflow.com/questions/49670008/how-to-disable-expect-100-continue-in-libcurl + headers = ::curl_slist_append(headers, "Expect:"); //guess, what: www.googleapis.com doesn't support it! e.g. gdriveUploadFile() + if (headers) options.emplace_back(CURLOPT_HTTPHEADER, headers); //--------------------------------------------------- @@ -353,13 +302,14 @@ public: const CURLcode rc = ::curl_easy_setopt(easyHandle_, opt.option, opt.value); if (rc != CURLE_OK) throw SysError(formatSystemError(L"curl_easy_setopt " + numberTo<std::wstring>(opt.option), - formatCurlErrorRaw(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); + formatCurlStatusCode(rc), utfTo<std::wstring>(::curl_easy_strerror(rc)))); } //======================================================================================================= const CURLcode rcPerf = ::curl_easy_perform(easyHandle_); //WTF: curl_easy_perform() considers FTP response codes 4XX, 5XX as failure, but for HTTP response codes 4XX are considered success!! CONSISTENCY, people!!! //=> at least libcurl is aware: CURLOPT_FAILONERROR: "request failure on HTTP response >= 400"; default: "0, do not fail on error" + //https://curl.haxx.se/docs/faq.html#curl_doesn_t_return_error_for_HT //=> Curiously Google also screws up in their REST API design and returns HTTP 4XX status for domain-level errors! //=> let caller handle HTTP status to work around this mess! @@ -390,16 +340,15 @@ private: HttpSession (const HttpSession&) = delete; HttpSession& operator=(const HttpSession&) = delete; - std::wstring formatLastCurlError(const std::wstring& functionName, CURLcode ec, int httpStatusCode) const + std::wstring formatLastCurlError(const std::wstring& functionName, CURLcode ec, int httpStatusCode /*optional*/) const { std::wstring errorMsg; if (curlErrorBuf_[0] != 0) errorMsg = trimCpy(utfTo<std::wstring>(curlErrorBuf_)); - const std::wstring descr = tryFormatHttpErrorCode(httpStatusCode); - if (!descr.empty()) - errorMsg += (errorMsg.empty() ? L"" : L"\n") + numberTo<std::wstring>(httpStatusCode) + L": " + descr; + if (httpStatusCode != 0) //optional + errorMsg += (errorMsg.empty() ? L"" : L"\n") + formatHttpStatusCode(httpStatusCode); #if 0 //utfTo<std::wstring>(::curl_easy_strerror(ec)) is uninteresting //use CURLINFO_OS_ERRNO ?? https://curl.haxx.se/libcurl/c/CURLINFO_OS_ERRNO.html @@ -408,7 +357,7 @@ private: if (nativeErrorCode != 0) errorMsg += (errorMsg.empty() ? L"" : L"\n") + std::wstring(L"Native error code: ") + numberTo<std::wstring>(nativeErrorCode); #endif - return formatSystemError(functionName, formatCurlErrorRaw(ec), errorMsg); + return formatSystemError(functionName, formatCurlStatusCode(ec), errorMsg); } const HttpSessionId sessionId_; @@ -1042,9 +991,19 @@ std::vector<GoogleFileItem> readFolderContent(const std::string& folderId, const const uint64_t fileSize = size ? stringTo<uint64_t>(*size) : 0; //not available for folders //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - const time_t modTime = utcToTimeT(parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_ALL))); //returns -1 on error - if (modTime == -1 || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix - throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L")"); + const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_ALL)); + if (tc == TimeComp() || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix + throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L")"); + + time_t modTime = utcToTimeT(tc); //returns -1 on error + if (modTime == -1) + { + if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" + tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 + modTime = 0; + else + throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L")"); + } std::vector<std::string> parentIds; for (const auto& parentVal : parents->arrayVal) @@ -1141,9 +1100,19 @@ ChangesDelta getChangesDelta(const std::string& startPageToken, const std::strin itemDetails.fileSize = size ? stringTo<uint64_t>(*size) : 0; //not available for folders //RFC 3339 date-time: e.g. "2018-09-29T08:39:12.053Z" - itemDetails.modTime = utcToTimeT(parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_ALL))); //returns -1 on error - if (itemDetails.modTime == -1 || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix - throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L")"); + const TimeComp tc = parseTime("%Y-%m-%dT%H:%M:%S", beforeLast(*modifiedTime, '.', IF_MISSING_RETURN_ALL)); + if (tc == TimeComp() || !endsWith(*modifiedTime, 'Z')) //'Z' means "UTC" => it seems Google doesn't use the time-zone offset postfix + throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L")"); + + itemDetails.modTime = utcToTimeT(tc); //returns -1 on error + if (itemDetails.modTime == -1) + { + if (tc.year == 1600 || //zero-initialized FILETIME equals "December 31, 1600" or "January 1, 1601" + tc.year == 1601) // => yes, possible even on Google Drive: https://freefilesync.org/forum/viewtopic.php?t=6602 + itemDetails.modTime = 0; + else + throw SysError(L"Modification time could not be parsed. (" + utfTo<std::wstring>(*modifiedTime) + L")"); + } for (const auto& parentVal : parents->arrayVal) { @@ -1485,9 +1454,8 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri { //https://developers.google.com/drive/api/v3/folder#inserting_a_file_in_a_folder //https://developers.google.com/drive/api/v3/resumable-upload - try - { - //step 1: initiate resumable upload session + + //step 1: initiate resumable upload session std::string uploadUrlRelative; { std::string postBuf = "{\n"; @@ -1544,13 +1512,13 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri //step 2: upload file content //not officially documented, but Google Drive supports compressed file upload when "Content-Encoding: gzip" is set! :))) - InputStreamAsGzip gzipStream(readBlock); //throw ZlibInternalError + InputStreamAsGzip gzipStream(readBlock); //throw SysError - auto readBlockAsGzip = [&](void* buffer, size_t bytesToRead) { return gzipStream.read(buffer, bytesToRead); }; //throw ZlibInternalError, X + auto readBlockAsGzip = [&](void* buffer, size_t bytesToRead) { return gzipStream.read(buffer, bytesToRead); }; //throw SysError, X //returns "bytesToRead" bytes unless end of stream! => fits into "0 signals EOF: Posix read() semantics" std::string response; - googleHttpsRequest(uploadUrlRelative, { "Content-Encoding: gzip" }, {} /*extraOptions*/, //throw SysError, ZlibInternalError, X + googleHttpsRequest(uploadUrlRelative, { "Content-Encoding: gzip" }, {} /*extraOptions*/, //throw SysError, X [&](const void* buffer, size_t bytesToWrite) { response.append(static_cast<const char*>(buffer), bytesToWrite); }, readBlockAsGzip); JsonValue jresponse; @@ -1562,11 +1530,6 @@ std::string /*itemId*/ gdriveUploadFile(const Zstring& fileName, const std::stri throw SysError(formatGoogleErrorRaw(response)); return *itemId; - } - catch (ZlibInternalError&) - { - throw SysError(L"zlib internal error"); - } } @@ -2254,9 +2217,9 @@ private: ByteArray zstreamOut; try { - zstreamOut = compress(streamOut.ref(), 3 /*compression level: see db_file.cpp*/); //throw ZlibInternalError + zstreamOut = compress(streamOut.ref(), 3 /*compression level: see db_file.cpp*/); //throw SysError } - catch (ZlibInternalError&) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(dbFilePath)), L"zlib internal error"); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(dbFilePath)), e.toString()); } saveBinContainer(dbFilePath, zstreamOut, nullptr /*notifyUnbufferedIO*/); //throw FileError } @@ -2267,9 +2230,9 @@ private: ByteArray rawStream; try { - rawStream = decompress(zstream); //throw ZlibInternalError + rawStream = decompress(zstream); //throw SysError } - catch (ZlibInternalError&) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(dbFilePath)), L"Zlib internal error"); } + catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(dbFilePath)), e.toString()); } MemoryStreamIn<ByteArray> streamIn(rawStream); try diff --git a/FreeFileSync/Source/afs/libcurl/curl_wrap.h b/FreeFileSync/Source/afs/libcurl/curl_wrap.h index 7a5a4f45..670ad6f5 100644 --- a/FreeFileSync/Source/afs/libcurl/curl_wrap.h +++ b/FreeFileSync/Source/afs/libcurl/curl_wrap.h @@ -20,9 +20,9 @@ namespace zen { namespace { -std::wstring formatCurlErrorRaw(CURLcode ec) +std::wstring formatCurlStatusCode(CURLcode sc) { - switch (ec) + switch (sc) { ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_OK); ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_UNSUPPORTED_PROTOCOL); @@ -118,9 +118,12 @@ std::wstring formatCurlErrorRaw(CURLcode ec) ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_SSL_INVALIDCERTSTATUS); ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_HTTP2_STREAM); ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_RECURSIVE_API_CALL); + ZEN_CHECK_CASE_FOR_CONSTANT(CURLE_AUTH_ERROR); ZEN_CHECK_CASE_FOR_CONSTANT(CURL_LAST); } - return L"Unknown Curl error: " + numberTo<std::wstring>(ec); + static_assert(CURL_LAST == CURLE_AUTH_ERROR + 1); + + return replaceCpy<std::wstring>(L"Curl status %x.", L"%x", numberTo<std::wstring>(sc)); } } } diff --git a/FreeFileSync/Source/afs/sftp.cpp b/FreeFileSync/Source/afs/sftp.cpp index d1276d11..aa5de7eb 100644 --- a/FreeFileSync/Source/afs/sftp.cpp +++ b/FreeFileSync/Source/afs/sftp.cpp @@ -167,9 +167,9 @@ std::wstring getSftpDisplayPath(const Zstring& serverName, const AfsPath& afsPat //=========================================================================================================================== -std::wstring formatSshErrorRaw(int ec) +std::wstring formatSshStatusCode(int sc) { - switch (ec) + switch (sc) { ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_NONE); ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_SOCKET_NONE); @@ -218,77 +218,72 @@ std::wstring formatSshErrorRaw(int ec) ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_ENCRYPT); ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_BAD_SOCKET); ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_KNOWN_HOSTS); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_CHANNEL_WINDOW_FULL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_ERROR_KEYFILE_AUTH_FAILED); } - return L"Unknown SSH error: " + numberTo<std::wstring>(ec); + return replaceCpy<std::wstring>(L"SSH status %x.", L"%x", numberTo<std::wstring>(sc)); } -std::wstring formatSftpErrorRaw(unsigned long ec) +std::wstring formatSftpStatusCode(unsigned long sc) { - switch (ec) - { - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_OK); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_EOF); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_SUCH_FILE); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_PERMISSION_DENIED); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_FAILURE); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_BAD_MESSAGE); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_CONNECTION); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_CONNECTION_LOST); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_OP_UNSUPPORTED); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_INVALID_HANDLE); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_SUCH_PATH); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_FILE_ALREADY_EXISTS); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_WRITE_PROTECT); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_MEDIA); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_SPACE_ON_FILESYSTEM); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_QUOTA_EXCEEDED); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_UNKNOWN_PRINCIPAL); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_LOCK_CONFLICT); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_DIR_NOT_EMPTY); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NOT_A_DIRECTORY); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_INVALID_FILENAME); - ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_LINK_LOOP); + switch (sc) + { + //*INDENT-OFF* + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_OK); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_EOF); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_SUCH_FILE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_PERMISSION_DENIED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_FAILURE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_BAD_MESSAGE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_CONNECTION); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_CONNECTION_LOST); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_OP_UNSUPPORTED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_INVALID_HANDLE); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_SUCH_PATH); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_FILE_ALREADY_EXISTS); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_WRITE_PROTECT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_MEDIA); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NO_SPACE_ON_FILESYSTEM); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_QUOTA_EXCEEDED); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_UNKNOWN_PRINCIPAL); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_LOCK_CONFLICT); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_DIR_NOT_EMPTY); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_NOT_A_DIRECTORY); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_INVALID_FILENAME); + ZEN_CHECK_CASE_FOR_CONSTANT(LIBSSH2_FX_LINK_LOOP); //SFTP error codes missing from libssh2: https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1 - case 22: - return L"SSH_FX_CANNOT_DELETE"; - case 23: - return L"SSH_FX_INVALID_PARAMETER"; - case 24: - return L"SSH_FX_FILE_IS_A_DIRECTORY"; - case 25: - return L"SSH_FX_BYTE_RANGE_LOCK_CONFLICT"; - case 26: - return L"SSH_FX_BYTE_RANGE_LOCK_REFUSED"; - case 27: - return L"SSH_FX_DELETE_PENDING"; - case 28: - return L"SSH_FX_FILE_CORRUPT"; - case 29: - return L"SSH_FX_OWNER_INVALID"; - case 30: - return L"SSH_FX_GROUP_INVALID"; - case 31: - return L"SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK"; + case 22: return L"SSH_FX_CANNOT_DELETE"; + case 23: return L"SSH_FX_INVALID_PARAMETER"; + case 24: return L"SSH_FX_FILE_IS_A_DIRECTORY"; + case 25: return L"SSH_FX_BYTE_RANGE_LOCK_CONFLICT"; + case 26: return L"SSH_FX_BYTE_RANGE_LOCK_REFUSED"; + case 27: return L"SSH_FX_DELETE_PENDING"; + case 28: return L"SSH_FX_FILE_CORRUPT"; + case 29: return L"SSH_FX_OWNER_INVALID"; + case 30: return L"SSH_FX_GROUP_INVALID"; + case 31: return L"SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK"; + + default: return replaceCpy<std::wstring>(L"SFTP status %x.", L"%x", numberTo<std::wstring>(sc)); + //*INDENT-ON* } - return L"Unknown SFTP error: " + numberTo<std::wstring>(ec); } std::wstring formatLastSshError(const std::wstring& functionName, LIBSSH2_SESSION* sshSession, LIBSSH2_SFTP* sftpChannel /*optional*/) { char* lastErrorMsg = nullptr; //owned by "sshSession" - const int lastErrorCode = ::libssh2_session_last_error(sshSession, &lastErrorMsg, nullptr, false /*want_buf*/); + const int sshStatusCode = ::libssh2_session_last_error(sshSession, &lastErrorMsg, nullptr, false /*want_buf*/); assert(lastErrorMsg); std::wstring errorMsg; if (lastErrorMsg) errorMsg = trimCpy(utfTo<std::wstring>(lastErrorMsg)); - if (sftpChannel && lastErrorCode == LIBSSH2_ERROR_SFTP_PROTOCOL) - errorMsg += (errorMsg.empty() ? L"" : L" - ") + formatSftpErrorRaw(::libssh2_sftp_last_error(sftpChannel)); + if (sftpChannel && sshStatusCode == LIBSSH2_ERROR_SFTP_PROTOCOL) + errorMsg += (errorMsg.empty() ? L"" : L" - ") + formatSftpStatusCode(::libssh2_sftp_last_error(sftpChannel)); - return formatSystemError(functionName, formatSshErrorRaw(lastErrorCode), errorMsg); + return formatSystemError(functionName, formatSshStatusCode(sshStatusCode), errorMsg); } //=========================================================================================================================== @@ -325,14 +320,14 @@ public: sshSession_ = ::libssh2_session_init(); if (!sshSession_) //does not set ssh last error; source: only memory allocation may fail - throw SysError(formatSystemError(L"libssh2_session_init", formatSshErrorRaw(LIBSSH2_ERROR_ALLOC), std::wstring())); + throw SysError(formatSystemError(L"libssh2_session_init", formatSshStatusCode(LIBSSH2_ERROR_ALLOC), std::wstring())); /* => libssh2 using zlib crashes for Bitvise Servers: https://freefilesync.org/forum/viewtopic.php?t=2825 => Don't enable zlib compression: libssh2 also recommends this option disabled: http://comments.gmane.org/gmane.network.ssh.libssh2.devel/6203 const int rc = ::libssh2_session_flag(sshSession_, LIBSSH2_FLAG_COMPRESS, 1); //does not set ssh last error if (rc != 0) - throw SysError(formatSystemError(L"libssh2_session_flag", formatSshErrorRaw(rc), std::wstring())); + throw SysError(formatSystemError(L"libssh2_session_flag", formatSshStatusCode(rc), std::wstring())); => build libssh2 without LIBSSH2_HAVE_ZLIB */ @@ -570,7 +565,7 @@ public: { if (numeric::dist(std::chrono::steady_clock::now(), nbInfo.commandStartTime) > std::chrono::seconds(timeoutSec)) //consider SSH session corrupted! => isHealthy() will see pending command - throw FatalSshError(formatSystemError(functionName, formatSshErrorRaw(LIBSSH2_ERROR_TIMEOUT), + throw FatalSshError(formatSystemError(functionName, formatSshStatusCode(LIBSSH2_ERROR_TIMEOUT), _P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", timeoutSec))); return false; } @@ -1586,6 +1581,8 @@ public: { try { + warn_static("should we use ~ instead???") //https://curl.haxx.se/docs/faq.html#How_to_SFTP_from_my_user_s_home + //we never ever change the SFTP working directory, right? ...right? return getServerRealPath("."); //throw SysError } diff --git a/FreeFileSync/Source/base/db_file.cpp b/FreeFileSync/Source/base/db_file.cpp index 578b53f8..d27313bc 100644 --- a/FreeFileSync/Source/base/db_file.cpp +++ b/FreeFileSync/Source/base/db_file.cpp @@ -187,11 +187,11 @@ public: 7 12.54 3633 8 12.51 9032 9 12.50 19698 (maximal compression) */ - return compress(stream, 3); //throw ZlibInternalError + return compress(stream, 3); //throw SysError } - catch (ZlibInternalError&) + catch (const SysError& e) { - throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), L"zlib internal error"); + throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), e.toString()); } }; @@ -294,11 +294,11 @@ public: { try { - return decompress(stream); //throw ZlibInternalError + return decompress(stream); //throw SysError } - catch (ZlibInternalError&) + catch (const SysError& e) { - throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), L"Zlib internal error"); + throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayFilePathL + L"/" + displayFilePathR)), e.toString()); } }; @@ -336,7 +336,7 @@ public: const size_t size2ndPart = static_cast<size_t>(readNumber<uint64_t>(in2ndPart)); ByteArray tmpB; - tmpB.resize(size1stPart + size2ndPart); //throw bad_alloc + tmpB.resize(size1stPart + size2ndPart); //throw std::bad_alloc readArray(in1stPart, &*tmpB.begin(), size1stPart); //stream always non-empty readArray(in2ndPart, &*tmpB.begin() + size1stPart, size2ndPart); //throw UnexpectedEndOfStreamError @@ -359,7 +359,7 @@ public: const size_t sizePart2 = static_cast<size_t>(readNumber<uint64_t>(streamInPart2)); ByteArray buf; - buf.resize(sizePart1 + sizePart2); //throw bad_alloc + buf.resize(sizePart1 + sizePart2); //throw std::bad_alloc if (sizePart1 > 0) readArray(streamInPart1, &*buf.begin(), sizePart1); //throw UnexpectedEndOfStreamError if (sizePart2 > 0) readArray(streamInPart2, &*buf.begin() + sizePart1, sizePart2); // @@ -911,7 +911,7 @@ void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transa } else //some MTP devices don't even allow renaming files: https://freefilesync.org/forum/viewtopic.php?t=6531 { - warn_static("caveat: throw X leaves db file as deleted!") + warn_static("caveat: throw X leaves db file as deleted!") AFS::removeFileIfExists(dbPathL); //throw FileError saveStreams(streamsL, dbPathL, notifySaveL); //throw FileError, X @@ -937,14 +937,14 @@ void fff::saveLastSynchronousState(const BaseFolderPair& baseFolder, bool transa if (transactionalCopy && !AFS::hasNativeTransactionalCopy(dbPathL)) { - AbstractPath dbPathTmpL2 = AFS::appendRelPath(*AFS::getParentPath(dbPathL), AFS::getItemName(dbPathL) + Zstr('.') + shortGuid + AFS::TEMP_FILE_ENDING); + AbstractPath dbPathTmpL2 = AFS::appendRelPath(*AFS::getParentPath(dbPathL), AFS::getItemName(dbPathL) + Zstr('.') + shortGuid + AFS::TEMP_FILE_ENDING); saveStreams(streamsL, dbPathTmpL2, notifySaveL); //throw FileError, X dbPathTmpL = dbPathTmpL2; } if (transactionalCopy && !AFS::hasNativeTransactionalCopy(dbPathR)) { - AbstractPath dbPathTmpR2 = AFS::appendRelPath(*AFS::getParentPath(dbPathR), AFS::getItemName(dbPathR) + Zstr('.') + shortGuid + AFS::TEMP_FILE_ENDING); + AbstractPath dbPathTmpR2 = AFS::appendRelPath(*AFS::getParentPath(dbPathR), AFS::getItemName(dbPathR) + Zstr('.') + shortGuid + AFS::TEMP_FILE_ENDING); saveStreams(streamsR, dbPathTmpR2, notifySaveR); //throw FileError, X dbPathTmpR = dbPathTmpR2; } diff --git a/FreeFileSync/Source/base/dir_lock.cpp b/FreeFileSync/Source/base/dir_lock.cpp index 39245bae..43ffb4b2 100644 --- a/FreeFileSync/Source/base/dir_lock.cpp +++ b/FreeFileSync/Source/base/dir_lock.cpp @@ -31,8 +31,44 @@ const std::chrono::seconds DETECT_ABANDONED_INTERVAL(30); //assume abandoned loc const char LOCK_FORMAT_DESCR[] = "FreeFileSync"; const int LOCK_FORMAT_VER = 2; //lock file format version +const int ABANDONED_LOCK_LEVEL_MAX = 10; +} + + +Zstring fff::impl::getLockFilePathForAbandonedLock(const Zstring& lockFilePath) //throw FileError +{ + auto it = zen::findLast(lockFilePath.begin(), lockFilePath.end(), FILE_NAME_SEPARATOR); + if (it == lockFilePath.end()) + it = lockFilePath.begin(); + else + ++it; + + const Zstring prefix (lockFilePath.begin(), it); + /**/ Zstring fileName( it, lockFilePath.end()); + int level = 0; + + //recursive abandoned locks!? (almost) impossible, except for file system bugs: https://freefilesync.org/forum/viewtopic.php?t=6568 + if (startsWith(fileName, Zstr("Delete."))) //e.g. Delete.1.sync.ffs_lock + { + const Zstring tmp = afterFirst(fileName, Zstr('.'), IF_MISSING_RETURN_NONE); + + const Zstring levelStr = beforeFirst(tmp, Zstr('.'), IF_MISSING_RETURN_NONE); + if (!levelStr.empty() && std::all_of(levelStr.begin(), levelStr.end(), [](Zchar c) { return zen::isDigit(c); })) + { + fileName = afterFirst(tmp, Zstr('.'), IF_MISSING_RETURN_NONE); + level = stringTo<int>(levelStr) + 1; + + if (level >= ABANDONED_LOCK_LEVEL_MAX) + throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(lockFilePath)), L"Endless recursion."); + } + } + + return prefix + Zstr("Delete.") + numberTo<Zstring>(level) + Zstr(".") + fileName; //preserve lock file extension! +} +namespace +{ //worker thread class LifeSigns { @@ -69,16 +105,6 @@ private: }; -Zstring abandonedLockDeletionName(const Zstring& lockFilePath) //make sure to NOT change file ending! -{ - const size_t pos = lockFilePath.rfind(FILE_NAME_SEPARATOR); //search from end - return pos == Zstring::npos ? Zstr("Del.") + lockFilePath : - Zstring(lockFilePath.c_str(), pos + 1) + //include path separator - Zstr("Del.") + - afterLast(lockFilePath, FILE_NAME_SEPARATOR, IF_MISSING_RETURN_ALL); -} - - using ProcessId = pid_t; @@ -303,7 +329,7 @@ void waitOnDirLock(const Zstring& lockFilePath, const DirLockCallback& notifySta if (lockOwnderDead || //no need to wait any longer... lastCheckTime >= lastLifeSign + DETECT_ABANDONED_INTERVAL) { - DirLock guardDeletion(abandonedLockDeletionName(lockFilePath), notifyStatus, cbInterval); //throw FileError + DirLock guardDeletion(fff::impl::getLockFilePathForAbandonedLock(lockFilePath), notifyStatus, cbInterval); //throw FileError //now that the lock is in place check existence again: meanwhile another process may have deleted and created a new lock! std::string currentLockId; diff --git a/FreeFileSync/Source/base/dir_lock.h b/FreeFileSync/Source/base/dir_lock.h index 20795804..1895c0e7 100644 --- a/FreeFileSync/Source/base/dir_lock.h +++ b/FreeFileSync/Source/base/dir_lock.h @@ -31,13 +31,21 @@ using DirLockCallback = std::function<void(const std::wstring& msg)>; //throw X class DirLock { public: - DirLock(const Zstring& lockFilePath, const DirLockCallback& notifyStatus, std::chrono::milliseconds cbInterval); //throw FileError, callback only used during construction + DirLock(const Zstring& lockFilePath, //throw FileError + const DirLockCallback& notifyStatus, //callback only used during construction + std::chrono::milliseconds cbInterval); // private: class LockAdmin; class SharedDirLock; std::shared_ptr<SharedDirLock> sharedLock_; }; + + +namespace impl //declare for unit tests: +{ +Zstring getLockFilePathForAbandonedLock(const Zstring& lockFilePath); //throw FileError +} } #endif //DIR_LOCK_H_81740832174954356 diff --git a/FreeFileSync/Source/base/lock_holder.h b/FreeFileSync/Source/base/lock_holder.h index 7bc470ba..fb1679bb 100644 --- a/FreeFileSync/Source/base/lock_holder.h +++ b/FreeFileSync/Source/base/lock_holder.h @@ -24,17 +24,17 @@ public: { using namespace zen; - std::map<Zstring, FileError> failedLocks; + std::vector<std::pair<Zstring, FileError>> failedLocks; for (const Zstring& folderPath : folderPaths) try { - //lock file creation is synchronous and may block noticeably for very slow devices (usb sticks, mapped cloud storages) + //lock file creation is synchronous and may block noticeably for very slow devices (USB sticks, mapped cloud storage) lockHolder_.emplace_back(appendSeparator(folderPath) + Zstr("sync") + LOCK_FILE_ENDING, [&](const std::wstring& msg) { pcb.reportStatus(msg); /*throw X*/ }, UI_UPDATE_INTERVAL / 2); //throw FileError } - catch (const FileError& e) { failedLocks.emplace(folderPath, e); } + catch (const FileError& e) { failedLocks.emplace_back(folderPath, e); } if (!failedLocks.empty()) { @@ -42,8 +42,9 @@ public: for (const auto& [folderPath, error] : failedLocks) { - msg += L"\n\n" + fmtPath(folderPath); - msg += L"\n" + replaceCpy(error.toString(), L"\n\n", L"\n"); + msg += L"\n\n"; + //msg += fmtPath(folderPath) + L"\n" -> seems redundant + msg += 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 1853355a..df2839fb 100644 --- a/FreeFileSync/Source/base/parallel_scan.cpp +++ b/FreeFileSync/Source/base/parallel_scan.cpp @@ -17,6 +17,8 @@ using namespace fff; namespace { +const int FOLDER_TRAVERSAL_LEVEL_MAX = 100; + /* PERF NOTE --------------------------------------------- @@ -322,7 +324,7 @@ std::shared_ptr<AFS::TraverserCallback> DirCallback::onFolder(const AFS::FolderI cfg_.acb.incItemsScanned(); //add 1 element to the progress indicator //------------------------------------------------------------------------------------ - if (level_ > 100) //Win32 traverser: stack overflow approximately at level 1000 + if (level_ > FOLDER_TRAVERSAL_LEVEL_MAX) //Win32 traverser: stack overflow approximately at level 1000 //check after FolderContainer::addSubFolder() for (size_t retryNumber = 0;; ++retryNumber) switch (reportItemError(replaceCpy(_("Cannot read directory %x."), L"%x", AFS::getDisplayPath(AFS::appendRelPath(cfg_.baseFolderPath, relPath))) + diff --git a/FreeFileSync/Source/base/resolve_path.cpp b/FreeFileSync/Source/base/resolve_path.cpp index 89000583..1799acb5 100644 --- a/FreeFileSync/Source/base/resolve_path.cpp +++ b/FreeFileSync/Source/base/resolve_path.cpp @@ -107,10 +107,11 @@ std::optional<Zstring> tryResolveMacro(const Zstring& macro) //macro without %-c return true; }; + //https://en.cppreference.com/w/cpp/chrono/c/strftime if (resolveTimePhrase(Zstr("weekday"), Zstr("%A"))) return timeStr; if (resolveTimePhrase(Zstr("day" ), Zstr("%d"))) return timeStr; if (resolveTimePhrase(Zstr("month" ), Zstr("%m"))) return timeStr; - if (resolveTimePhrase(Zstr("week" ), Zstr("%U"))) return timeStr; + if (resolveTimePhrase(Zstr("week" ), Zstr("%V"))) return timeStr; //ISO 8601 week of the year if (resolveTimePhrase(Zstr("year" ), Zstr("%Y"))) return timeStr; if (resolveTimePhrase(Zstr("hour" ), Zstr("%H"))) return timeStr; if (resolveTimePhrase(Zstr("min" ), Zstr("%M"))) return timeStr; diff --git a/FreeFileSync/Source/base/synchronization.cpp b/FreeFileSync/Source/base/synchronization.cpp index bd407296..33923cc6 100644 --- a/FreeFileSync/Source/base/synchronization.cpp +++ b/FreeFileSync/Source/base/synchronization.cpp @@ -2533,7 +2533,7 @@ void fff::synchronize(const std::chrono::system_clock::time_point& syncStartTime L" " + AFS::getDisplayPath(baseFolder.getAbstractPath<RIGHT_SIDE>())); //------------------------------------------------------------------------------------------ - //checking a second time: (a long time may have passed since folder comparison!) + //checking a second time: (a long time may have passed since syncing the previous folder pairs!) if (baseFolderDrop< LEFT_SIDE>(baseFolder, callback) || baseFolderDrop<RIGHT_SIDE>(baseFolder, callback)) continue; diff --git a/FreeFileSync/Source/ui/gui_generated.cpp b/FreeFileSync/Source/ui/gui_generated.cpp index d8e104bc..4a7d822a 100644 --- a/FreeFileSync/Source/ui/gui_generated.cpp +++ b/FreeFileSync/Source/ui/gui_generated.cpp @@ -1832,7 +1832,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelFilterSettingsTab->SetSizer( bSizer278 ); m_panelFilterSettingsTab->Layout(); bSizer278->Fit( m_panelFilterSettingsTab ); - m_notebook->AddPage( m_panelFilterSettingsTab, _("dummy"), true ); + m_notebook->AddPage( m_panelFilterSettingsTab, _("dummy"), false ); m_panelSyncSettingsTab = new wxPanel( m_notebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); m_panelSyncSettingsTab->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); @@ -2347,7 +2347,7 @@ ConfigDlgGenerated::ConfigDlgGenerated( wxWindow* parent, wxWindowID id, const w m_panelSyncSettingsTab->SetSizer( bSizer276 ); m_panelSyncSettingsTab->Layout(); bSizer276->Fit( m_panelSyncSettingsTab ); - m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), false ); + m_notebook->AddPage( m_panelSyncSettingsTab, _("dummy"), true ); bSizer190->Add( m_notebook, 1, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 5 ); @@ -5341,3 +5341,116 @@ CfgHighlightDlgGenerated::CfgHighlightDlgGenerated( wxWindow* parent, wxWindowID CfgHighlightDlgGenerated::~CfgHighlightDlgGenerated() { } + +WarnAccessRightsMissingDlgGenerated::WarnAccessRightsMissingDlgGenerated( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxDialog( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxSize( -1, -1 ), wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_BTNFACE ) ); + + wxBoxSizer* bSizer330; + bSizer330 = new wxBoxSizer( wxHORIZONTAL ); + + m_bitmapGrantAccess = new wxStaticBitmap( this, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxDefaultSize, 0 ); + bSizer330->Add( m_bitmapGrantAccess, 0, wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM|wxLEFT, 10 ); + + wxBoxSizer* bSizer95; + bSizer95 = new wxBoxSizer( wxVERTICAL ); + + m_staticTextDescr = new wxStaticText( this, wxID_ANY, _("FreeFileSync requires access rights to avoid \"Operation not permitted\" errors when synchronizing your data (e.g. Mail, Messages, Calendars)."), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_staticTextDescr->Wrap( -1 ); + bSizer95->Add( m_staticTextDescr, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + m_staticline20 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer95->Add( m_staticline20, 0, wxEXPAND, 5 ); + + m_panel39 = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + m_panel39->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer166; + bSizer166 = new wxBoxSizer( wxVERTICAL ); + + ffgSizer11 = new wxFlexGridSizer( 0, 2, 5, 5 ); + ffgSizer11->SetFlexibleDirection( wxBOTH ); + ffgSizer11->SetNonFlexibleGrowMode( wxFLEX_GROWMODE_SPECIFIED ); + + m_staticTextStep1 = new wxStaticText( m_panel39, wxID_ANY, _("1."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep1->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep1, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_buttonLocateBundle = new wxButton( m_panel39, wxID_ANY, _("Locate the FreeFileSync app"), wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_buttonLocateBundle, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + m_staticTextStep2 = new wxStaticText( m_panel39, wxID_ANY, _("2."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep2->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep2, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_buttonOpenSecurity = new wxButton( m_panel39, wxID_ANY, _("Open Security && Privacy"), wxDefaultPosition, wxDefaultSize, 0 ); + ffgSizer11->Add( m_buttonOpenSecurity, 0, wxALIGN_CENTER_VERTICAL|wxEXPAND, 5 ); + + m_staticTextStep3 = new wxStaticText( m_panel39, wxID_ANY, _("3."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep3->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep3, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_staticTextAllowChanges = new wxStaticText( m_panel39, wxID_ANY, _("Click the lock to allow changes."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextAllowChanges->Wrap( -1 ); + ffgSizer11->Add( m_staticTextAllowChanges, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + m_staticTextStep4 = new wxStaticText( m_panel39, wxID_ANY, _("4."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextStep4->Wrap( -1 ); + ffgSizer11->Add( m_staticTextStep4, 0, wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL, 5 ); + + m_staticTextGrantAccess = new wxStaticText( m_panel39, wxID_ANY, _("Drag FreeFileSync into the panel."), wxDefaultPosition, wxDefaultSize, 0 ); + m_staticTextGrantAccess->Wrap( -1 ); + ffgSizer11->Add( m_staticTextGrantAccess, 0, wxALIGN_CENTER_VERTICAL, 5 ); + + + bSizer166->Add( ffgSizer11, 0, wxALL|wxALIGN_CENTER_HORIZONTAL, 10 ); + + + m_panel39->SetSizer( bSizer166 ); + m_panel39->Layout(); + bSizer166->Fit( m_panel39 ); + bSizer95->Add( m_panel39, 1, wxEXPAND, 5 ); + + m_staticline36 = new wxStaticLine( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL ); + bSizer95->Add( m_staticline36, 0, wxEXPAND, 5 ); + + wxBoxSizer* bSizer25; + bSizer25 = new wxBoxSizer( wxVERTICAL ); + + m_checkBoxDontShowAgain = new wxCheckBox( this, wxID_ANY, _("&Don't show this dialog again"), wxDefaultPosition, wxDefaultSize, 0 ); + bSizer25->Add( m_checkBoxDontShowAgain, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); + + bSizerStdButtons = new wxBoxSizer( wxHORIZONTAL ); + + m_buttonClose = new wxButton( this, wxID_OK, _("Close"), wxDefaultPosition, wxSize( -1, -1 ), 0 ); + m_buttonClose->SetDefault(); + bSizerStdButtons->Add( m_buttonClose, 0, wxALIGN_CENTER_VERTICAL|wxBOTTOM|wxRIGHT|wxLEFT, 5 ); + + + bSizer25->Add( bSizerStdButtons, 0, wxALIGN_RIGHT, 5 ); + + + bSizer95->Add( bSizer25, 0, wxEXPAND, 5 ); + + + bSizer330->Add( bSizer95, 1, wxEXPAND, 5 ); + + + this->SetSizer( bSizer330 ); + this->Layout(); + bSizer330->Fit( this ); + + this->Centre( wxBOTH ); + + // Connect Events + this->Connect( wxEVT_CLOSE_WINDOW, wxCloseEventHandler( WarnAccessRightsMissingDlgGenerated::OnClose ) ); + m_buttonLocateBundle->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::OnShowAppBundle ), NULL, this ); + m_buttonOpenSecurity->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::OnOpenSecuritySettings ), NULL, this ); + m_checkBoxDontShowAgain->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::OnCheckBoxClick ), NULL, this ); + m_buttonClose->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( WarnAccessRightsMissingDlgGenerated::OnOK ), NULL, this ); +} + +WarnAccessRightsMissingDlgGenerated::~WarnAccessRightsMissingDlgGenerated() +{ +} diff --git a/FreeFileSync/Source/ui/gui_generated.h b/FreeFileSync/Source/ui/gui_generated.h index 616cf0bb..41febc65 100644 --- a/FreeFileSync/Source/ui/gui_generated.h +++ b/FreeFileSync/Source/ui/gui_generated.h @@ -1295,4 +1295,45 @@ public: }; +/////////////////////////////////////////////////////////////////////////////// +/// Class WarnAccessRightsMissingDlgGenerated +/////////////////////////////////////////////////////////////////////////////// +class WarnAccessRightsMissingDlgGenerated : public wxDialog +{ +private: + +protected: + wxStaticBitmap* m_bitmapGrantAccess; + wxStaticText* m_staticTextDescr; + wxStaticLine* m_staticline20; + wxPanel* m_panel39; + wxFlexGridSizer* ffgSizer11; + wxStaticText* m_staticTextStep1; + wxButton* m_buttonLocateBundle; + wxStaticText* m_staticTextStep2; + wxButton* m_buttonOpenSecurity; + wxStaticText* m_staticTextStep3; + wxStaticText* m_staticTextAllowChanges; + wxStaticText* m_staticTextStep4; + wxStaticText* m_staticTextGrantAccess; + wxStaticLine* m_staticline36; + wxCheckBox* m_checkBoxDontShowAgain; + wxBoxSizer* bSizerStdButtons; + wxButton* m_buttonClose; + + // Virtual event handlers, overide them in your derived class + virtual void OnClose( wxCloseEvent& event ) { event.Skip(); } + virtual void OnShowAppBundle( wxCommandEvent& event ) { event.Skip(); } + virtual void OnOpenSecuritySettings( wxCommandEvent& event ) { event.Skip(); } + virtual void OnCheckBoxClick( wxCommandEvent& event ) { event.Skip(); } + virtual void OnOK( wxCommandEvent& event ) { event.Skip(); } + + +public: + + WarnAccessRightsMissingDlgGenerated( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = _("Grant Full Disk Access"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxDEFAULT_DIALOG_STYLE ); + ~WarnAccessRightsMissingDlgGenerated(); + +}; + #endif //__GUI_GENERATED_H__ diff --git a/FreeFileSync/Source/ui/main_dlg.cpp b/FreeFileSync/Source/ui/main_dlg.cpp index d4f3aa5c..1721245a 100644 --- a/FreeFileSync/Source/ui/main_dlg.cpp +++ b/FreeFileSync/Source/ui/main_dlg.cpp @@ -736,6 +736,7 @@ MainDialog::MainDialog(const Zstring& globalConfigFilePath, if (!selectedRows.empty()) m_gridCfgHistory->makeRowVisible(selectedRows.front()); + m_buttonCompare->SetFocus(); //---------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/FreeFileSync/Source/ui/progress_indicator.cpp b/FreeFileSync/Source/ui/progress_indicator.cpp index f3339193..394fb60e 100644 --- a/FreeFileSync/Source/ui/progress_indicator.cpp +++ b/FreeFileSync/Source/ui/progress_indicator.cpp @@ -193,7 +193,7 @@ CompareProgressDialog::Impl::Impl(wxFrame& parentWindow) : m_panelProgressGraph->setAttributes(Graph2D::MainAttributes().setMinY(0).setMaxY(2). setLabelX(Graph2D::LABEL_X_NONE). setLabelY(Graph2D::LABEL_Y_NONE). - setBackgroundColor(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)). + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)). setSelectionMode(Graph2D::SELECT_NONE)); m_panelProgressGraph->addCurve(curveDataBytes_, Graph2D::CurveAttributes().setLineWidth(1).fillPolygonArea(getColorBytes()).setColor(Graph2D::getBorderColor())); @@ -494,32 +494,34 @@ private: class CurveDataTotalBlock : public CurveData { public: - void setValue (double x, double y) { x_ = x; y_ = y; } - void setValueX(double x) { x_ = x; } - double getValueX() const { return x_; } + void setValue(double x1, double x2, double y) { x1_ = x1; x2_ = x2; y_ = y; } + void setTimes(double x1, double x2) { x1_ = x1; x2_ = x2; } + double getTotalTime() const { return x2_; } private: - std::pair<double, double> getRangeX() const override { return { x_, x_ }; } //conceptually just a vertical line! + std::pair<double, double> getRangeX() const override { return { x1_, x2_ }; } std::vector<CurvePoint> getPoints(double minX, double maxX, const wxSize& areaSizePx) const override { return { - { 0, y_ }, - { x_, y_ }, - { x_, 0 }, + { x1_, 0 }, + { x1_, y_ }, + { x2_, y_ }, + { x2_, 0 }, }; } - double x_ = 0; //time elapsed in seconds - double y_ = 0; //items/bytes processed + double x1_ = 0; //elapsed time [s] + double x2_ = 0; //total time [s] (estimated) + double y_ = 0; //items/bytes total }; class CurveDataProcessedBlock : public CurveData { public: - void setValue(double x1, double x2, double y) { x1_ = x1; x2_ = x2; y_ = y; } + void setValue(double x1, double x2, double y1, double y2) { x1_ = x1; x2_ = x2; y1_ = y1; y2_ = y2; } private: std::pair<double, double> getRangeX() const override { return { x1_, x2_ }; } @@ -528,17 +530,17 @@ private: { return { - { 0, y_ }, - { x1_, y_ }, - { x1_, 0 }, - { x1_, y_ }, - { x2_, y_ }, + { x1_, 0 }, + { x1_, y2_ }, + { x1_, y1_ }, + { x2_, y1_ }, }; } - double x1_ = 0; //time elapsed in seconds - double x2_ = 0; //total time (estimated) - double y_ = 0; //items/bytes processed + double x1_ = 0; //elapsed time [s] + double x2_ = 0; //total time [s] (estimated) + double y1_ = 0; //items/bytes processed + double y2_ = 0; //items/bytes total }; @@ -823,26 +825,26 @@ SyncProgressDialogImpl<TopLevelDialog>::SyncProgressDialogImpl(long style, //wxF const int xLabelHeight = this->GetCharHeight() + fastFromDIP(2) /*margin*/; //use same height for both graphs to make sure they stretch evenly const int yLabelWidth = fastFromDIP(70); pnl_.m_panelGraphBytes->setAttributes(Graph2D::MainAttributes(). - setLabelX(Graph2D::LABEL_X_TOP, xLabelHeight, std::make_shared<LabelFormatterTimeElapsed>(true)). - setLabelY(Graph2D::LABEL_Y_RIGHT, yLabelWidth, std::make_shared<LabelFormatterBytes>()). - setBackgroundColor(wxColor(208, 208, 208)). //light grey + setLabelX(Graph2D::LABEL_X_TOP, xLabelHeight, std::make_shared<LabelFormatterTimeElapsed>(true)). + setLabelY(Graph2D::LABEL_Y_RIGHT, yLabelWidth, std::make_shared<LabelFormatterBytes>()). + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)). setSelectionMode(Graph2D::SELECT_NONE)); pnl_.m_panelGraphItems->setAttributes(Graph2D::MainAttributes(). setLabelX(Graph2D::LABEL_X_BOTTOM, xLabelHeight, std::make_shared<LabelFormatterTimeElapsed>(true)). setLabelY(Graph2D::LABEL_Y_RIGHT, yLabelWidth, std::make_shared<LabelFormatterItemCount>()). - setBackgroundColor(wxColor(208, 208, 208)). //light grey + setBaseColors(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)). setSelectionMode(Graph2D::SELECT_NONE)); pnl_.m_panelGraphBytes->setCurve(curveDataBytesTotal_, Graph2D::CurveAttributes().setLineWidth(1).fillCurveArea(*wxWHITE).setColor(wxColor(192, 192, 192))); //medium grey pnl_.m_panelGraphItems->setCurve(curveDataItemsTotal_, Graph2D::CurveAttributes().setLineWidth(1).fillCurveArea(*wxWHITE).setColor(wxColor(192, 192, 192))); //medium grey - pnl_.m_panelGraphBytes->addCurve(curveDataBytesCurrent_, Graph2D::CurveAttributes().setLineWidth(1).fillCurveArea(getColorBytesBackground()).setColor(getColorBytesBackgroundRim())); - pnl_.m_panelGraphItems->addCurve(curveDataItemsCurrent_, Graph2D::CurveAttributes().setLineWidth(1).fillCurveArea(getColorItemsBackground()).setColor(getColorItemsBackgroundRim())); - pnl_.m_panelGraphBytes->addCurve(curveDataBytes_, Graph2D::CurveAttributes().setLineWidth(2).fillCurveArea(getColorBytes()).setColor(getColorBytesRim())); pnl_.m_panelGraphItems->addCurve(curveDataItems_, Graph2D::CurveAttributes().setLineWidth(2).fillCurveArea(getColorItems()).setColor(getColorItemsRim())); + pnl_.m_panelGraphBytes->addCurve(curveDataBytesCurrent_, Graph2D::CurveAttributes().setLineWidth(2).fillCurveArea(getColorBytesBackground()).setColor(getColorBytesBackgroundRim())); + pnl_.m_panelGraphItems->addCurve(curveDataItemsCurrent_, Graph2D::CurveAttributes().setLineWidth(2).fillCurveArea(getColorItemsBackground()).setColor(getColorItemsBackgroundRim())); + //graph legend: auto generateSquareBitmap = [&](const wxColor& fillCol, const wxColor& borderCol) { @@ -940,10 +942,10 @@ void SyncProgressDialogImpl<TopLevelDialog>::initNewPhase() updateStaticGui(); //evaluates "syncStat_->currentPhase()" //reset graphs (e.g. after binary comparison) - curveDataBytesTotal_ ->setValue(0, 0); - curveDataItemsTotal_ ->setValue(0, 0); - curveDataBytesCurrent_->setValue(0, 0, 0); - curveDataItemsCurrent_->setValue(0, 0, 0); + curveDataBytesTotal_ ->setTimes(0, 0); + curveDataItemsTotal_ ->setTimes(0, 0); + curveDataBytesCurrent_->setValue(0, 0, 0, 0); + curveDataItemsCurrent_->setValue(0, 0, 0, 0); curveDataBytes_ ->clear(); curveDataItems_ ->clear(); @@ -1057,15 +1059,15 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield) if (trayIcon_.get()) trayIcon_->setProgress(fractionTotal); if (taskbar_ .get()) taskbar_ ->setProgress(fractionTotal); - const double timeTotalSecTentative = bytesCurrent == bytesTotal ? timeElapsedDouble : std::max(curveDataBytesTotal_->getValueX(), timeElapsedDouble); + const double timeTotalSecTentative = bytesCurrent == bytesTotal ? timeElapsedDouble : std::max(curveDataBytesTotal_->getTotalTime(), timeElapsedDouble); //constant line graph - curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, bytesCurrent); - curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, itemsCurrent); + curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, bytesCurrent, bytesTotal); + curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSecTentative, itemsCurrent, itemsTotal); //tentatively update total time, may be improved on below: - curveDataBytesTotal_->setValue(timeTotalSecTentative, bytesTotal); - curveDataItemsTotal_->setValue(timeTotalSecTentative, itemsTotal); + curveDataBytesTotal_->setValue(timeElapsedDouble, timeTotalSecTentative, bytesTotal); + curveDataItemsTotal_->setValue(timeElapsedDouble, timeTotalSecTentative, itemsTotal); } //even though notifyProgressChange() already set the latest data, let's add another sample to have all curves consider "timeNowMs" @@ -1129,13 +1131,13 @@ void SyncProgressDialogImpl<TopLevelDialog>::updateProgressGui(bool allowYield) //update estimated total time marker with precision of "10% remaining time" only to avoid needless jumping around: const double timeRemainingSec = remTimeSec ? *remTimeSec : 0; const double timeTotalSec = timeElapsedDouble + timeRemainingSec; - if (numeric::dist(curveDataBytesTotal_->getValueX(), timeTotalSec) > 0.1 * timeRemainingSec) + if (numeric::dist(curveDataBytesTotal_->getTotalTime(), timeTotalSec) > 0.1 * timeRemainingSec) { - curveDataBytesTotal_->setValueX(timeTotalSec); - curveDataItemsTotal_->setValueX(timeTotalSec); + curveDataBytesTotal_->setTimes(timeElapsedDouble, timeTotalSec); + curveDataItemsTotal_->setTimes(timeElapsedDouble, timeTotalSec); //don't forget to update these, too: - curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSec, bytesCurrent); - curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSec, itemsCurrent); + curveDataBytesCurrent_->setValue(timeElapsedDouble, timeTotalSec, bytesCurrent, bytesTotal); + curveDataItemsCurrent_->setValue(timeElapsedDouble, timeTotalSec, itemsCurrent, itemsTotal); } } } diff --git a/FreeFileSync/Source/ui/small_dlgs.cpp b/FreeFileSync/Source/ui/small_dlgs.cpp index 97942754..44a4d7c2 100644 --- a/FreeFileSync/Source/ui/small_dlgs.cpp +++ b/FreeFileSync/Source/ui/small_dlgs.cpp @@ -45,11 +45,12 @@ - using namespace zen; using namespace fff; +namespace +{ class AboutDlg : public AboutDlgGenerated { public: @@ -167,16 +168,18 @@ void AboutDlg::onLocalKeyEvent(wxKeyEvent& event) //process key events without e { event.Skip(); } - +} void fff::showAboutDialog(wxWindow* parent) { - AboutDlg aboutDlg(parent); - aboutDlg.ShowModal(); + AboutDlg dlg(parent); + dlg.ShowModal(); } //######################################################################################## +namespace +{ class CloudSetupDlg : public CloudSetupDlgGenerated { public: @@ -661,16 +664,18 @@ void CloudSetupDlg::OnOkay(wxCommandEvent& event) EndModal(ReturnSmallDlg::BUTTON_OKAY); } - +} ReturnSmallDlg::ButtonPressed fff::showCloudSetupDialog(wxWindow* parent, Zstring& folderPathPhrase, size_t& parallelOps, const std::wstring* parallelOpsDisabledReason) { - CloudSetupDlg setupDlg(parent, folderPathPhrase, parallelOps, parallelOpsDisabledReason); - return static_cast<ReturnSmallDlg::ButtonPressed>(setupDlg.ShowModal()); + CloudSetupDlg dlg(parent, folderPathPhrase, parallelOps, parallelOpsDisabledReason); + return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); } //######################################################################################## +namespace +{ class CopyToDialog : public CopyToDlgGenerated { public: @@ -788,7 +793,7 @@ void CopyToDialog::OnOK(wxCommandEvent& event) EndModal(ReturnSmallDlg::BUTTON_OKAY); } - +} ReturnSmallDlg::ButtonPressed fff::showCopyToDialog(wxWindow* parent, std::span<const FileSystemObject* const> rowsOnLeft, @@ -811,6 +816,8 @@ ReturnSmallDlg::ButtonPressed fff::showCopyToDialog(wxWindow* parent, //######################################################################################## +namespace +{ class DeleteDialog : public DeleteDlgGenerated { public: @@ -921,19 +928,21 @@ void DeleteDialog::OnOK(wxCommandEvent& event) EndModal(ReturnSmallDlg::BUTTON_OKAY); } - +} ReturnSmallDlg::ButtonPressed fff::showDeleteDialog(wxWindow* parent, std::span<const FileSystemObject* const> rowsOnLeft, std::span<const FileSystemObject* const> rowsOnRight, bool& useRecycleBin) { - DeleteDialog confirmDeletion(parent, rowsOnLeft, rowsOnRight, useRecycleBin); - return static_cast<ReturnSmallDlg::ButtonPressed>(confirmDeletion.ShowModal()); + DeleteDialog dlg(parent, rowsOnLeft, rowsOnRight, useRecycleBin); + return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); } //######################################################################################## +namespace +{ class SyncConfirmationDlg : public SyncConfirmationDlgGenerated { public: @@ -1020,7 +1029,7 @@ void SyncConfirmationDlg::OnStartSync(wxCommandEvent& event) dontShowAgainOut_ = m_checkBoxDontShowAgain->GetValue(); EndModal(ReturnSmallDlg::BUTTON_OKAY); } - +} ReturnSmallDlg::ButtonPressed fff::showSyncConfirmationDlg(wxWindow* parent, bool syncSelection, @@ -1038,6 +1047,8 @@ ReturnSmallDlg::ButtonPressed fff::showSyncConfirmationDlg(wxWindow* parent, //######################################################################################## +namespace +{ class OptionsDlg : public OptionsDlgGenerated { public: @@ -1347,7 +1358,7 @@ void OptionsDlg::OnShowLogFolder(wxHyperlinkEvent& event) } catch (const FileError& e) { showNotificationDialog(this, DialogInfoType::error, PopupDialogCfg().setDetailInstructions(e.toString())); } } - +} ReturnSmallDlg::ButtonPressed fff::showOptionsDlg(wxWindow* parent, XmlGlobalSettings& globalCfg) { @@ -1357,6 +1368,8 @@ ReturnSmallDlg::ButtonPressed fff::showOptionsDlg(wxWindow* parent, XmlGlobalSet //######################################################################################## +namespace +{ class SelectTimespanDlg : public SelectTimespanDlgGenerated { public: @@ -1446,16 +1459,18 @@ void SelectTimespanDlg::OnOkay(wxCommandEvent& event) EndModal(ReturnSmallDlg::BUTTON_OKAY); } - +} ReturnSmallDlg::ButtonPressed fff::showSelectTimespanDlg(wxWindow* parent, time_t& timeFrom, time_t& timeTo) { - SelectTimespanDlg timeSpanDlg(parent, timeFrom, timeTo); - return static_cast<ReturnSmallDlg::ButtonPressed>(timeSpanDlg.ShowModal()); + SelectTimespanDlg dlg(parent, timeFrom, timeTo); + return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); } //######################################################################################## +namespace +{ class CfgHighlightDlg : public CfgHighlightDlgGenerated { public: @@ -1500,16 +1515,18 @@ void CfgHighlightDlg::OnOkay(wxCommandEvent& event) cfgHistSyncOverdueDaysOut_ = m_spinCtrlOverdueDays->GetValue(); EndModal(ReturnSmallDlg::BUTTON_OKAY); } - +} ReturnSmallDlg::ButtonPressed fff::showCfgHighlightDlg(wxWindow* parent, int& cfgHistSyncOverdueDays) { - CfgHighlightDlg cfgHighDlg(parent, cfgHistSyncOverdueDays); - return static_cast<ReturnSmallDlg::ButtonPressed>(cfgHighDlg.ShowModal()); + CfgHighlightDlg dlg(parent, cfgHistSyncOverdueDays); + return static_cast<ReturnSmallDlg::ButtonPressed>(dlg.ShowModal()); } //######################################################################################## +namespace +{ class ActivationDlg : public ActivationDlgGenerated { public: @@ -1580,7 +1597,7 @@ void ActivationDlg::OnActivateOffline(wxCommandEvent& event) manualActivationKeyOut_ = m_textCtrlOfflineActivationKey->GetValue(); EndModal(static_cast<int>(ReturnActivationDlg::ACTIVATE_OFFLINE)); } - +} ReturnActivationDlg fff::showActivationDialog(wxWindow* parent, const std::wstring& lastErrorMsg, const std::wstring& manualActivationUrl, std::wstring& manualActivationKey) { @@ -1670,3 +1687,6 @@ DownloadProgressWindow::~DownloadProgressWindow() { pimpl_->Destroy(); } void DownloadProgressWindow::notifyNewFile(const Zstring& filePath) { pimpl_->notifyNewFile(filePath); } void DownloadProgressWindow::notifyProgress(int64_t delta) { pimpl_->notifyProgress(delta); } void DownloadProgressWindow::requestUiRefresh() { pimpl_->requestUiRefresh(); } //throw CancelPressed + +//######################################################################################## + diff --git a/FreeFileSync/Source/ui/small_dlgs.h b/FreeFileSync/Source/ui/small_dlgs.h index 3607b593..f3636a0c 100644 --- a/FreeFileSync/Source/ui/small_dlgs.h +++ b/FreeFileSync/Source/ui/small_dlgs.h @@ -80,6 +80,8 @@ private: class Impl; Impl* const pimpl_; }; + + } #endif //SMALL_DLGS_H_8321790875018750245 diff --git a/FreeFileSync/Source/version/version.h b/FreeFileSync/Source/version/version.h index ad914ec1..aee4ad4d 100644 --- a/FreeFileSync/Source/version/version.h +++ b/FreeFileSync/Source/version/version.h @@ -3,7 +3,7 @@ namespace fff { -const char ffsVersion[] = "10.15"; //internal linkage! +const char ffsVersion[] = "10.16"; //internal linkage! const char FFS_VERSION_SEPARATOR = '.'; } @@ -12,24 +12,25 @@ #include <zen/basic_math.h> #include <wx/dcbuffer.h> //for macro: wxALWAYS_NATIVE_DOUBLE_BUFFER #include <wx/dcscreen.h> + #include <gtk/gtk.h> namespace zen { /* - 1. wxDCClipper does *not* stack: another fix for yet another poor wxWidgets implementation + 1. wxDCClipper does *not* stack: another fix for yet another poor wxWidgets implementation - class RecursiveDcClipper - { - RecursiveDcClipper(wxDC& dc, const wxRect& r) - }; + class RecursiveDcClipper + { + RecursiveDcClipper(wxDC& dc, const wxRect& r) + }; - 2. wxAutoBufferedPaintDC skips one pixel on left side when RTL layout is active: a fix for a poor wxWidgets implementation + 2. wxAutoBufferedPaintDC skips one pixel on left side when RTL layout is active: a fix for a poor wxWidgets implementation - class BufferedPaintDC - { - BufferedPaintDC(wxWindow& wnd, std::unique_ptr<wxBitmap>& buffer) - }; + class BufferedPaintDC + { + BufferedPaintDC(wxWindow& wnd, std::unique_ptr<wxBitmap>& buffer) + }; */ @@ -50,10 +51,12 @@ Standard DPI: inline int fastFromDIP(int d) //like wxWindow::FromDIP (but tied to primary monitor and buffered) { - #ifdef wxHAVE_DPI_INDEPENDENT_PIXELS //pulled from wx/window.h: https://github.com/wxWidgets/wxWidgets/blob/master/include/wx/window.h#L2029 return d; //e.g. macOS, GTK3 #else //https://github.com/wxWidgets/wxWidgets/blob/master/src/common/wincmn.cpp#L2865 + static_assert(GTK_MAJOR_VERSION == 2); + //GTK2 doesn't properly support high DPI: https://freefilesync.org/forum/viewtopic.php?t=6114 + //=> requires general fix at wxWidgets-level assert(wxTheApp); //only call after wxWidgets was initalized! static const int dpiY = wxScreenDC().GetPPI().y; //perf: buffering for calls to ::GetDeviceCaps() needed!? const int defaultDpi = 96; diff --git a/wx+/graph.cpp b/wx+/graph.cpp index 2440f77d..aac2cc10 100644 --- a/wx+/graph.cpp +++ b/wx+/graph.cpp @@ -151,7 +151,7 @@ void drawXLabel(wxDC& dc, double xMin, double xMax, int blockCount, const Conver return; wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... - wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); const double valRangePerBlock = (xMax - xMin) / blockCount; @@ -179,7 +179,7 @@ void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const Conver return; wxDCPenChanger dummy(dc, wxColor(192, 192, 192)); //light grey => not accessible! but no big deal... - wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); //use user setting for labels + wxDCTextColourChanger dummy2(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); const double valRangePerBlock = (yMax - yMin) / blockCount; @@ -200,7 +200,7 @@ void drawYLabel(wxDC& dc, double yMin, double yMax, int blockCount, const Conver } -void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos, const wxColor& backgroundColor) +void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Graph2D::PosCorner pos, const wxColor& colorText, const wxColor& colorBack) { if (txt.empty()) return; @@ -225,13 +225,13 @@ void drawCornerText(wxDC& dc, const wxRect& graphArea, const wxString& txt, Grap drawPos.y += graphArea.height - boxExtent.GetHeight(); break; } - { //add text shadow to improve readability: - wxDCTextColourChanger dummy(dc, backgroundColor); + wxDCTextColourChanger dummy(dc, colorBack); dc.DrawText(txt, drawPos + border + wxSize(fastFromDIP(1), fastFromDIP(1))); } - wxDCTextColourChanger dummy(dc, wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); + + wxDCTextColourChanger dummy(dc, colorText); dc.DrawText(txt, drawPos + border); } @@ -593,7 +593,7 @@ void Graph2D::render(wxDC& dc) const { //paint graph background (excluding label area) wxDCPenChanger dummy (dc, getBorderColor()); - wxDCBrushChanger dummy2(dc, attr_.backgroundColor); + wxDCBrushChanger dummy2(dc, attr_.colorBack); //accessibility: consider system text and background colors; small drawback: color of graphs is NOT connected to the background! => responsibility of client to use correct colors dc.DrawRectangle(graphArea); @@ -849,7 +849,7 @@ void Graph2D::render(wxDC& dc) const //5. draw corner texts for (const auto& [cornerPos, text] : attr_.cornerTexts) - drawCornerText(dc, graphArea, text, cornerPos, attr_.backgroundColor); + drawCornerText(dc, graphArea, text, cornerPos, attr_.colorText, attr_.colorBack); } } } diff --git a/wx+/graph.h b/wx+/graph.h index f1ae5d5a..f1f0c76c 100644 --- a/wx+/graph.h +++ b/wx+/graph.h @@ -264,7 +264,8 @@ public: MainAttributes& setCornerText(const wxString& txt, PosCorner pos) { cornerTexts[pos] = txt; return *this; } - MainAttributes& setBackgroundColor(const wxColor& col) { backgroundColor = col; return *this; } + //accessibility: always set both colors + MainAttributes& setBaseColors(const wxColor& text, const wxColor& back) { colorText = text; colorBack = back; return *this; } MainAttributes& setSelectionMode(SelMode mode) { mouseSelMode = mode; return *this; } @@ -287,10 +288,13 @@ public: std::map<PosCorner, wxString> cornerTexts; - wxColor backgroundColor = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + wxColor colorText = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + wxColor colorBack = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + SelMode mouseSelMode = SELECT_RECTANGLE; }; + void setAttributes(const MainAttributes& newAttr) { attr_ = newAttr; Refresh(); } MainAttributes getAttributes() const { return attr_; } diff --git a/zen/http.cpp b/zen/http.cpp index d4c30741..93651d0b 100644 --- a/zen/http.cpp +++ b/zen/http.cpp @@ -46,7 +46,7 @@ public: else //HTTP default port: 80, see %WINDIR%\system32\drivers\etc\services socket_ = std::make_unique<Socket>(server, Zstr("http")); //throw SysError - //we don't support "chunked and gzip transfer encoding" => HTTP 1.0 + //we don't support "chunked and gzip transfer encoding" => HTTP 1.0 std::map<std::string, std::string, LessAsciiNoCase> headers; headers["Host" ] = utfTo<std::string>(server); //only required for HTTP/1.1 but a few servers expect it even for HTTP/1.0 headers["User-Agent"] = utfTo<std::string>(userAgent); @@ -235,8 +235,8 @@ std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const Zstring& url, auto response = std::make_unique<HttpInputStream::Impl>(urlRed, postParams, false /*disableGetCache*/, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection - const int statusCode = response->getStatusCode(); - if (statusCode / 100 == 3) //e.g. 301, 302, 303, 307... we're not too greedy since we check location, too! + const int httpStatusCode = response->getStatusCode(); + if (httpStatusCode / 100 == 3) //e.g. 301, 302, 303, 307... we're not too greedy since we check location, too! { const std::string* value = response->getHeader("Location"); if (!value || value->empty()) @@ -246,9 +246,8 @@ std::unique_ptr<HttpInputStream::Impl> sendHttpRequestImpl(const Zstring& url, } else { - if (statusCode != 200) //HTTP_STATUS_OK - throw SysError(replaceCpy<std::wstring>(L"HTTP status code %x.", L"%x", numberTo<std::wstring>(statusCode))); - //e.g. 404 - HTTP_STATUS_NOT_FOUND + if (httpStatusCode != 200) //HTTP_STATUS_OK(200) + throw SysError(formatHttpStatusCode(httpStatusCode)); //e.g. HTTP_STATUS_NOT_FOUND(404) return response; } @@ -271,7 +270,7 @@ std::string urlencode(const std::string& str) out += c; else { - const auto [high, low] = hexify(c); + const auto [high, low] = hexify(c); out += '%'; out += high; out += low; @@ -327,7 +326,7 @@ std::vector<std::pair<std::string, std::string>> zen::xWwwFormUrlDecode(const st HttpInputStream zen::sendHttpPost(const Zstring& url, const std::vector<std::pair<std::string, std::string>>& postParams, - const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError + const Zstring& userAgent, const Zstring* caCertFilePath, const IOCallback& notifyUnbufferedIO) //throw SysError { return sendHttpRequestImpl(url, &postParams, userAgent, caCertFilePath, notifyUnbufferedIO); //throw SysError } @@ -357,3 +356,85 @@ bool zen::internetIsAlive() //noexcept } catch (SysError&) { return false; } } + + +std::wstring zen::formatHttpStatusCode(int sc) +{ + const wchar_t* statusText = [&] //https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + { + switch (sc) + { + //*INDENT-OFF* + case 300: return L"Multiple choices."; + case 301: return L"Moved permanently."; + case 302: return L"Moved temporarily."; + case 303: return L"See other"; + case 304: return L"Not modified."; + case 305: return L"Use proxy."; + case 306: return L"Switch proxy."; + case 307: return L"Temporary redirect."; + case 308: return L"Permanent redirect."; + + case 400: return L"Bad request."; + case 401: return L"Unauthorized."; + case 402: return L"Payment required."; + case 403: return L"Forbidden."; + case 404: return L"Not found."; + case 405: return L"Method not allowed."; + case 406: return L"Not acceptable."; + case 407: return L"Proxy authentication required."; + case 408: return L"Request timeout."; + case 409: return L"Conflict."; + case 410: return L"Gone."; + case 411: return L"Length required."; + case 412: return L"Precondition failed."; + case 413: return L"Payload too large."; + case 414: return L"URI too long."; + case 415: return L"Unsupported media type."; + case 416: return L"Range not satisfiable."; + case 417: return L"Expectation failed."; + case 418: return L"I'm a teapot."; + case 421: return L"Misdirected request."; + case 422: return L"Unprocessable entity."; + case 423: return L"Locked."; + case 424: return L"Failed dependency."; + case 425: return L"Too early."; + case 426: return L"Upgrade required."; + case 428: return L"Precondition required."; + case 429: return L"Too many requests."; + case 431: return L"Request header fields too large."; + case 451: return L"Unavailable for legal reasons."; + + case 500: return L"Internal server error."; + case 501: return L"Not implemented."; + case 502: return L"Bad gateway."; + case 503: return L"Service unavailable."; + case 504: return L"Gateway timeout."; + case 505: return L"HTTP version not supported."; + case 506: return L"Variant also negotiates."; + case 507: return L"Insufficient storage."; + case 508: return L"Loop detected."; + case 510: return L"Not extended."; + case 511: return L"Network authentication required."; + + //Cloudflare errors regarding origin server: + case 520: return L"Unknown error (Cloudflare)"; + case 521: return L"Web server is down (Cloudflare)"; + case 522: return L"Connection timed out (Cloudflare)"; + case 523: return L"Origin is unreachable (Cloudflare)"; + case 524: return L"A timeout occurred (Cloudflare)"; + case 525: return L"SSL handshake failed (Cloudflare)"; + case 526: return L"Invalid SSL certificate (Cloudflare)"; + case 527: return L"Railgun error (Cloudflare)"; + case 530: return L"Origin DNS error (Cloudflare)"; + + default: return L""; + //*INDENT-ON* + } + }(); + + if (strLength(statusText) == 0) + return trimCpy(replaceCpy<std::wstring>(L"HTTP status %x.", L"%x", numberTo<std::wstring>(sc))); + else + return trimCpy(replaceCpy<std::wstring>(L"HTTP status %x: ", L"%x", numberTo<std::wstring>(sc)) + statusText); +}
\ No newline at end of file @@ -47,6 +47,7 @@ HttpInputStream sendHttpPost(const Zstring& url, const Zstring* caCertFilePath /*optional: enable certificate validation*/, const IOCallback& notifyUnbufferedIO /*throw X*/); bool internetIsAlive(); //noexcept +std::wstring formatHttpStatusCode(int httpStatusCode); std::string xWwwFormUrlEncode(const std::vector<std::pair<std::string, std::string>>& paramPairs); std::vector<std::pair<std::string, std::string>> xWwwFormUrlDecode(const std::string& str); @@ -12,7 +12,8 @@ namespace zen { -//https://tools.ietf.org/html/rfc8259 +//Spec: https://tools.ietf.org/html/rfc8259 +//Test: http://seriot.ch/parsing_json.php struct JsonValue { enum class Type @@ -92,26 +93,31 @@ std::string jsonEscape(const std::string& str) { std::string output; for (const char c : str) - { - if (c == '"') output += "\\\""; //escaping mandatory - else if (c == '\\') output += "\\\\"; // - - else if (c == '\b') output += "\\b"; // - else if (c == '\f') output += "\\f"; // - else if (c == '\n') output += "\\n"; //prefer compact escaping - else if (c == '\r') output += "\\r"; // - else if (c == '\t') output += "\\t"; // - - else if (static_cast<unsigned char>(c) < 32) + switch (c) { - const auto [high, low] = hexify(c); - output += "\\u00"; - output += high; - output += low; + //*INDENT-OFF* + case '"': output += "\\\""; break; //escaping mandatory + case '\\': output += "\\\\"; break; // + + case '\b': output += "\\b"; break; // + case '\f': output += "\\f"; break; // + case '\n': output += "\\n"; break; //prefer compact escaping + case '\r': output += "\\r"; break; // + case '\t': output += "\\t"; break; // + + default: + if (static_cast<unsigned char>(c) < 32) + { + const auto [high, low] = hexify(c); + output += "\\u00"; + output += high; + output += low; + } + else + output += c; + break; + //*INDENT-ON* } - else - output += c; - } return output; } @@ -151,31 +157,36 @@ std::string jsonUnescape(const std::string& str) } const char c2 = *it; - if (c2 == '"' || - c2 == '\\' || - c2 == '/') - writeOut(c2); - else if (c2 == 'b') writeOut('\b'); - else if (c2 == 'f') writeOut('\f'); - else if (c2 == 'n') writeOut('\n'); - else if (c2 == 'r') writeOut('\r'); - else if (c2 == 't') writeOut('\t'); - - else if (c2 == 'u' && - str.end() - it >= 5 && - isHexDigit(it[1]) && - isHexDigit(it[2]) && - isHexDigit(it[3]) && - isHexDigit(it[4])) - { - utf16Buf += static_cast<impl::Char16>(static_cast<unsigned char>(unhexify(it[1], it[2])) * 256 + - static_cast<unsigned char>(unhexify(it[3], it[4]))); - it += 4; - } - else //unknown escape sequence! + switch (c2) { - writeOut(c); - writeOut(c2); + //*INDENT-OFF* + case '"': + case '\\': + case '/': writeOut(c2); break; + case 'b': writeOut('\b'); break; + case 'f': writeOut('\f'); break; + case 'n': writeOut('\n'); break; + case 'r': writeOut('\r'); break; + case 't': writeOut('\t'); break; + default: + if (c2 == 'u' && + str.end() - it >= 5 && + isHexDigit(it[1]) && + isHexDigit(it[2]) && + isHexDigit(it[3]) && + isHexDigit(it[4])) + { + utf16Buf += static_cast<impl::Char16>(static_cast<unsigned char>(unhexify(it[1], it[2])) * 256 + + static_cast<unsigned char>(unhexify(it[3], it[4]))); + it += 4; + } + else //unknown escape sequence! + { + writeOut(c); + writeOut(c2); + } + break; + //*INDENT-ON* } } else @@ -322,7 +333,7 @@ public: Token getNextToken() //throw JsonParsingError { //skip whitespace - pos_ = std::find_if(pos_, stream_.end(), std::not_fn(isJsonWhiteSpace)); + pos_ = std::find_if_not(pos_, stream_.end(), isJsonWhiteSpace); if (pos_ == stream_.end()) return Token::Type::eof; @@ -368,7 +379,7 @@ public: } //expect a number: - const auto itNumEnd = std::find_if(pos_, stream_.end(), std::not_fn(isJsonNumDigit)); + const auto itNumEnd = std::find_if_not(pos_, stream_.end(), isJsonNumDigit); if (itNumEnd == pos_) throw JsonParsingError(posRow(), posCol()); diff --git a/zen/shell_execute.h b/zen/shell_execute.h index 4875a039..56322236 100644 --- a/zen/shell_execute.h +++ b/zen/shell_execute.h @@ -72,7 +72,7 @@ void shellExecute(const Zstring& command, ExecutionType type, bool hideConsole) inline void openWithDefaultApplication(const Zstring& itemPath) //throw FileError { - shellExecute("xdg-open \"" + itemPath + '"', ExecutionType::ASYNC, false/*hideConsole*/); // + shellExecute("xdg-open \"" + itemPath + '"', ExecutionType::ASYNC, false /*hideConsole*/); //throw FileError } } diff --git a/zen/zlib_wrap.cpp b/zen/zlib_wrap.cpp index ff5799c3..8979efa6 100644 --- a/zen/zlib_wrap.cpp +++ b/zen/zlib_wrap.cpp @@ -9,17 +9,39 @@ //Linux/macOS: use zlib system header for both wxWidgets and libcurl (zlib is required for HTTP) // => don't compile wxWidgets with: --with-zlib=builtin #include <zlib.h> //https://www.zlib.net/manual.html +#include <zen/scope_guard.h> using namespace zen; +namespace +{ +std::wstring formatZlibStatusCode(int sc) +{ + switch (sc) + { + ZEN_CHECK_CASE_FOR_CONSTANT(Z_OK); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_STREAM_END); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_NEED_DICT); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_ERRNO); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_STREAM_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_DATA_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_MEM_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_BUF_ERROR); + ZEN_CHECK_CASE_FOR_CONSTANT(Z_VERSION_ERROR); + } + return replaceCpy<std::wstring>(L"zlib status %x.", L"%x", numberTo<std::wstring>(sc)); +} +} + + size_t zen::impl::zlib_compressBound(size_t len) { return ::compressBound(static_cast<uLong>(len)); //upper limit for buffer size, larger than input size!!! } -size_t zen::impl::zlib_compress(const void* src, size_t srcLen, void* trg, size_t trgLen, int level) //throw ZlibInternalError +size_t zen::impl::zlib_compress(const void* src, size_t srcLen, void* trg, size_t trgLen, int level) //throw SysError { uLongf bufferSize = static_cast<uLong>(trgLen); const int rv = ::compress2(static_cast<Bytef*>(trg), //Bytef* dest, @@ -31,12 +53,13 @@ size_t zen::impl::zlib_compress(const void* src, size_t srcLen, void* trg, size_ // Z_MEM_ERROR: not enough memory // Z_BUF_ERROR: not enough room in the output buffer if (rv != Z_OK || bufferSize > trgLen) - throw ZlibInternalError(); + throw SysError(formatSystemError(L"compress2", formatZlibStatusCode(rv), L"zlib error")); + return bufferSize; } -size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen) //throw ZlibInternalError +size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen) //throw SysError { uLongf bufferSize = static_cast<uLong>(trgLen); const int rv = ::uncompress(static_cast<Bytef*>(trg), //Bytef* dest, @@ -48,7 +71,8 @@ size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, siz // Z_BUF_ERROR: not enough room in the output buffer // Z_DATA_ERROR: input data was corrupted or incomplete if (rv != Z_OK || bufferSize > trgLen) - throw ZlibInternalError(); + throw SysError(formatSystemError(L"uncompress", formatZlibStatusCode(rv), L"zlib error")); + return bufferSize; } @@ -56,7 +80,7 @@ size_t zen::impl::zlib_decompress(const void* src, size_t srcLen, void* trg, siz class InputStreamAsGzip::Impl { public: - Impl(const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/) : //throw ZlibInternalError; returning 0 signals EOF: Posix read() semantics + Impl(const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/) : //throw SysError; returning 0 signals EOF: Posix read() semantics readBlock_(readBlock) { const int windowBits = MAX_WBITS + 16; //"add 16 to windowBits to write a simple gzip header" @@ -72,7 +96,7 @@ public: memLevel, //int memLevel Z_DEFAULT_STRATEGY); //int strategy if (rv != Z_OK) - throw ZlibInternalError(); + throw SysError(formatSystemError(L"deflateInit2", formatZlibStatusCode(rv), L"zlib error")); } ~Impl() @@ -81,7 +105,7 @@ public: assert(rv == Z_OK); } - size_t read(void* buffer, size_t bytesToRead) //throw ZlibInternalError, X; return "bytesToRead" bytes unless end of stream! + size_t read(void* buffer, size_t bytesToRead) //throw SysError, X; return "bytesToRead" bytes unless end of stream! { 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__)); @@ -107,7 +131,7 @@ public: if (rv == Z_STREAM_END) return bytesToRead - gzipStream_.avail_out; if (rv != Z_OK) - throw ZlibInternalError(); + throw SysError(formatSystemError(L"deflate", formatZlibStatusCode(rv), L"zlib error")); if (gzipStream_.avail_out == 0) return bytesToRead; @@ -122,6 +146,6 @@ private: }; -zen::InputStreamAsGzip::InputStreamAsGzip(const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/) : pimpl_(std::make_unique<Impl>(readBlock)) {} //throw ZlibInternalError +zen::InputStreamAsGzip::InputStreamAsGzip(const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/) : pimpl_(std::make_unique<Impl>(readBlock)) {} //throw SysError zen::InputStreamAsGzip::~InputStreamAsGzip() {} -size_t zen::InputStreamAsGzip::read(void* buffer, size_t bytesToRead) { return pimpl_->read(buffer, bytesToRead); } //throw ZlibInternalError, X +size_t zen::InputStreamAsGzip::read(void* buffer, size_t bytesToRead) { return pimpl_->read(buffer, bytesToRead); } //throw SysError, X diff --git a/zen/zlib_wrap.h b/zen/zlib_wrap.h index c8647baf..fbe26193 100644 --- a/zen/zlib_wrap.h +++ b/zen/zlib_wrap.h @@ -8,31 +8,30 @@ #define ZLIB_WRAP_H_428597064566 #include "serialize.h" +#include "sys_error.h" namespace zen { -class ZlibInternalError {}; - // compression level must be between 0 and 9: // 0: no compression // 9: best compression template <class BinContainer> //as specified in serialize.h -BinContainer compress(const BinContainer& stream, int level); //throw ZlibInternalError +BinContainer compress(const BinContainer& stream, int level); //throw SysError //caveat: output stream is physically larger than input! => strip additional reserved space if needed: "BinContainer(output.begin(), output.end())" template <class BinContainer> -BinContainer decompress(const BinContainer& stream); //throw ZlibInternalError +BinContainer decompress(const BinContainer& stream); //throw SysError class InputStreamAsGzip //convert input stream into gzip on the fly { public: - InputStreamAsGzip( //throw ZlibInternalError + InputStreamAsGzip( //throw SysError const std::function<size_t(void* buffer, size_t bytesToRead)>& readBlock /*throw X*/); //returning 0 signals EOF: Posix read() semantics ~InputStreamAsGzip(); - size_t read(void* buffer, size_t bytesToRead); //throw ZlibInternalError, X; return "bytesToRead" bytes unless end of stream! + size_t read(void* buffer, size_t bytesToRead); //throw SysError, X; return "bytesToRead" bytes unless end of stream! private: class Impl; @@ -49,13 +48,13 @@ private: namespace impl { size_t zlib_compressBound(size_t len); -size_t zlib_compress (const void* src, size_t srcLen, void* trg, size_t trgLen, int level); //throw ZlibInternalError -size_t zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen); //throw ZlibInternalError +size_t zlib_compress (const void* src, size_t srcLen, void* trg, size_t trgLen, int level); //throw SysError +size_t zlib_decompress(const void* src, size_t srcLen, void* trg, size_t trgLen); //throw SysError } template <class BinContainer> -BinContainer compress(const BinContainer& stream, int level) //throw ZlibInternalError +BinContainer compress(const BinContainer& stream, int level) //throw SysError { BinContainer contOut; if (!stream.empty()) //don't dereference iterator into empty container! @@ -73,7 +72,7 @@ BinContainer compress(const BinContainer& stream, int level) //throw ZlibInterna stream.size(), &*contOut.begin() + contOut.size() - bufferEstimate, bufferEstimate, - level); //throw ZlibInternalError + level); //throw SysError if (bytesWritten < bufferEstimate) contOut.resize(contOut.size() - (bufferEstimate - bytesWritten)); //caveat: unsigned arithmetics //caveat: physical memory consumption still *unchanged*! @@ -83,7 +82,7 @@ BinContainer compress(const BinContainer& stream, int level) //throw ZlibInterna template <class BinContainer> -BinContainer decompress(const BinContainer& stream) //throw ZlibInternalError +BinContainer decompress(const BinContainer& stream) //throw SysError { BinContainer contOut; if (!stream.empty()) //don't dereference iterator into empty container! @@ -91,30 +90,30 @@ BinContainer decompress(const BinContainer& stream) //throw ZlibInternalError //retrieve size of uncompressed data uint64_t uncompressedSize = 0; //use portable number type! if (stream.size() < sizeof(uncompressedSize)) - throw ZlibInternalError(); + throw SysError(L"zlib error: stream size < 8"); std::memcpy(&uncompressedSize, &*stream.begin(), sizeof(uncompressedSize)); //attention: contOut MUST NOT be empty! Else it will pass a nullptr to zlib_decompress() => Z_STREAM_ERROR although "uncompressedSize == 0"!!! //secondary bug: don't dereference iterator into empty container! if (uncompressedSize == 0) //cannot be 0: compress() directly maps empty -> empty container skipping zlib! - throw ZlibInternalError(); + throw SysError(L"zlib error: uncompressed size == 0"); try { contOut.resize(static_cast<size_t>(uncompressedSize)); //throw std::bad_alloc } - catch (std::bad_alloc&) //most likely due to data corruption! + catch (const std::bad_alloc& e) //most likely due to data corruption! { - throw ZlibInternalError(); + throw SysError(L"zlib error: " + _("Out of memory.") + L" " + utfTo<std::wstring>(e.what())); } const size_t bytesWritten = impl::zlib_decompress(&*stream.begin() + sizeof(uncompressedSize), stream.size() - sizeof(uncompressedSize), &*contOut.begin(), - static_cast<size_t>(uncompressedSize)); //throw ZlibInternalError + static_cast<size_t>(uncompressedSize)); //throw SysError if (bytesWritten != static_cast<size_t>(uncompressedSize)) - throw ZlibInternalError(); + throw SysError(L"zlib error: bytes written != uncompressed size"); } return contOut; } diff --git a/zenXml/zenxml/parser.h b/zenXml/zenxml/parser.h index f7604ebf..dbf5a8c4 100644 --- a/zenXml/zenxml/parser.h +++ b/zenXml/zenxml/parser.h @@ -79,31 +79,31 @@ std::string normalize(const std::string& str, Predicate pred) //pred: unary func { std::string output; for (const char c : str) - { - if (c == '&') // - output += "&"; - else if (c == '<') //normalization mandatory: https://www.w3.org/TR/xml/#syntax - output += "<"; - else if (c == '>') // - output += ">"; - else if (pred(c)) + switch (c) { - if (c == '\'') - output += "'"; - else if (c == '"') - output += """; - else - { - output += "&#x"; - const auto [high, low] = hexify(c); - output += high; - output += low; - output += ';'; - } + //*INDENT-OFF* + case '&': output += "&"; break; // + case '<': output += "<"; break; //normalization mandatory: https://www.w3.org/TR/xml/#syntax + case '>': output += ">"; break; // + default: + if (pred(c)) + { + if (c == '\'') output += "'"; + else if (c == '"') output += """; + else + { + output += "&#x"; + const auto [high, low] = hexify(c); + output += high; + output += low; + output += ';'; + } + } + else + output += c; + break; + //*INDENT-ON* } - else - output += c; - } return output; } |