1 /************************************************************************
2 **
3 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford Ontario Canada
4 **  Copyright (C) 2016-2020 Doug Massay
5 **  Copyright (C) 2009-2011 Strahinja Markovic  <strahinja.markovic@gmail.com>
6 **
7 **  This file is part of Sigil.
8 **
9 **  Sigil is free software: you can redistribute it and/or modify
10 **  it under the terms of the GNU General Public License as published by
11 **  the Free Software Foundation, either version 3 of the License, or
12 **  (at your option) any later version.
13 **
14 **  Sigil is distributed in the hope that it will be useful,
15 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 **  GNU General Public License for more details.
18 **
19 **  You should have received a copy of the GNU General Public License
20 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
21 **
22 *************************************************************************/
23 
24 #ifdef _WIN32
25 #define NOMINMAX
26 #endif
27 
28 #include "unzip.h"
29 
30 #ifdef _WIN32
31 #include "iowin32.h"
32 #endif
33 
34 #include <stdio.h>
35 #include <time.h>
36 #include <string>
37 
38 #include <utility>
39 #include <vector>
40 
41 #include <QApplication>
42 #include <QtCore/QDir>
43 #include <QtCore/QFile>
44 #include <QtCore/QFileInfo>
45 #include <QtCore/QProcess>
46 #include <QtCore/QStandardPaths>
47 #include <QtCore/QStringList>
48 #include <QtCore/QStringRef>
49 #include <QtCore/QTextStream>
50 #include <QtCore/QtGlobal>
51 #include <QtCore/QUrl>
52 #include <QtCore/QUuid>
53 #include <QtWidgets/QMainWindow>
54 #include <QTextEdit>
55 #include <QMessageBox>
56 #include <QRegularExpression>
57 #include <QRegularExpressionMatch>
58 #include <QFile>
59 #include <QFileInfo>
60 #include <QCollator>
61 #include <QMenu>
62 #include <QSet>
63 #include <QVector>
64 #include <QDebug>
65 
66 #include "sigil_constants.h"
67 #include "sigil_exception.h"
68 #include "Misc/QCodePage437Codec.h"
69 #include "Misc/SettingsStore.h"
70 #include "Misc/SleepFunctions.h"
71 #include "MainUI/MainApplication.h"
72 
73 static const QString URL_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-/~";
74 
75 static const QString DARK_STYLE =
76     "<style>:root { background-color: %1; color: %2; } ::-webkit-scrollbar { display: none; }</style>"
77     "<link rel=\"stylesheet\" type=\"text/css\" href=\"%3\" />";
78 
79 #ifndef MAX_PATH
80 // Set Max length to 256 because that's the max path size on many systems.
81 #define MAX_PATH 256
82 #endif
83 // This is the same read buffer size used by Java and Perl.
84 #define BUFF_SIZE 8192
85 
86 static QCodePage437Codec *cp437 = 0;
87 
88 // Subclass QMessageBox for our StdWarningDialog to make any Details Resizable
89 class SigilMessageBox: public QMessageBox
90 {
91     public:
SigilMessageBox(QWidget * parent)92         SigilMessageBox(QWidget* parent) : QMessageBox(parent)
93         {
94             setSizeGripEnabled(true);
95         }
96     private:
resizeEvent(QResizeEvent * e)97         virtual void resizeEvent(QResizeEvent * e) {
98             QMessageBox::resizeEvent(e);
99             setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
100             if (QWidget *textEdit = findChild<QTextEdit *>()) {
101                 textEdit->setMaximumHeight(QWIDGETSIZE_MAX);
102             }
103         }
104 };
105 
106 #include "Misc/Utility.h"
107 
108 
109 // Define the user preferences location to be used
DefinePrefsDir()110 QString Utility::DefinePrefsDir()
111 {
112     // If the SIGIL_PREFS_DIR environment variable override exists; use it.
113     // It's up to the user to provide a directory they have permission to write to.
114     if (!SIGIL_PREFS_DIR.isEmpty()) {
115         return SIGIL_PREFS_DIR;
116     } else {
117         return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
118     }
119 }
120 
IsDarkMode()121 bool Utility::IsDarkMode()
122 {
123 #ifdef Q_OS_MAC
124     MainApplication *mainApplication = qobject_cast<MainApplication *>(qApp);
125     return mainApplication->isDarkMode();
126 #else
127     // Windows, Linux and Other platforms
128     QPalette app_palette = qApp->palette();
129     bool isdark = app_palette.color(QPalette::Active,QPalette::WindowText).lightness() > 128;
130     return isdark;
131 #endif
132 }
133 
IsWindowsSysDarkMode()134 bool Utility::IsWindowsSysDarkMode()
135 {
136     QSettings s("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", QSettings::NativeFormat);
137     if (s.status() == QSettings::NoError) {
138         // qDebug() << "Registry Value = " << s.value("AppsUseLightTheme");
139         return s.value("AppsUseLightTheme") == 0;
140     }
141     return false;
142 }
143 
WindowsShouldUseDarkMode()144 bool Utility::WindowsShouldUseDarkMode()
145 {
146     QString override(GetEnvironmentVar("SIGIL_USES_DARK_MODE"));
147     if (override.isEmpty()) {
148         //Env var unset - use system registry setting.
149         return IsWindowsSysDarkMode();
150     }
151     // Otherwise use the env var: anything other than "0" is true.
152     return (override == "0" ? false : true);
153 }
154 
155 #if !defined(Q_OS_WIN32) && !defined(Q_OS_MAC)
156 // Return correct path(s) for Linux hunspell dictionaries
LinuxHunspellDictionaryDirs()157 QStringList Utility::LinuxHunspellDictionaryDirs()
158 {
159     QStringList paths;
160     // prefer the directory specified by the env var SIGIL_DICTIONARIES above all else.
161     if (!hunspell_dicts_override.isEmpty()) {
162         // Handle multiple colon-delimited paths
163         foreach (QString s, hunspell_dicts_override.split(":")) {
164             paths << s.trimmed();
165         }
166     }
167     // else use the env var runtime overridden 'share/sigil/hunspell_dictionaries/' location.
168     else if (!sigil_extra_root.isEmpty()) {
169         paths.append(sigil_extra_root + "/hunspell_dictionaries/");
170     }
171     // Bundled dicts were not installed use standard system dictionary location.
172     else if (!dicts_are_bundled) {
173         paths.append("/usr/share/hunspell");
174         // Add additional hunspell dictionary directories. Provided at compile
175         // time via the cmake option EXTRA_DICT_DIRS (colon separated list).
176         if (!extra_dict_dirs.isEmpty()) {
177             foreach (QString s, extra_dict_dirs.split(":")) {
178                 paths << s.trimmed();
179             }
180         }
181     }
182     else {
183         // else use the standard build time 'share/sigil/hunspell_dictionaries/'location.
184         paths.append(sigil_share_root + "/hunspell_dictionaries/");
185     }
186     return paths;
187 }
188 #endif
189 
190 
191 // Uses QUuid to generate a random UUID but also removes
192 // the curly braces that QUuid::createUuid() adds
CreateUUID()193 QString Utility::CreateUUID()
194 {
195     return QUuid::createUuid().toString().remove("{").remove("}");
196 }
197 
198 
199 // Convert the casing of the text, returning the result.
ChangeCase(const QString & text,const Utility::Casing & casing)200 QString Utility::ChangeCase(const QString &text, const Utility::Casing &casing)
201 {
202     if (text.isEmpty()) {
203         return text;
204     }
205 
206     switch (casing) {
207         case Utility::Casing_Lowercase: {
208             return text.toLower();
209         }
210 
211         case Utility::Casing_Uppercase: {
212             return text.toUpper();
213         }
214 
215         case Utility::Casing_Titlecase: {
216             // This is a super crude algorithm, could be replaced by something more clever.
217             QString new_text = text.toLower();
218             // Skip past any leading spaces
219             int i = 0;
220 
221             while (i < text.length() && new_text.at(i).isSpace()) {
222                 i++;
223             }
224 
225             while (i < text.length()) {
226                 if (i == 0 || new_text.at(i - 1).isSpace()) {
227                     new_text.replace(i, 1, new_text.at(i).toUpper());
228                 }
229 
230                 i++;
231             }
232 
233             return new_text;
234         }
235 
236         case Utility::Casing_Capitalize: {
237             // This is a super crude algorithm, could be replaced by something more clever.
238             QString new_text = text.toLower();
239             // Skip past any leading spaces
240             int i = 0;
241 
242             while (i < text.length() && new_text.at(i).isSpace()) {
243                 i++;
244             }
245 
246             if (i < text.length()) {
247                 new_text.replace(i, 1, new_text.at(i).toUpper());
248             }
249 
250             return new_text;
251         }
252 
253         default:
254             return text;
255     }
256 }
257 
258 
259 // Returns true if the string is mixed case, false otherwise.
260 // For instance, "test" and "TEST" return false, "teSt" returns true.
261 // If the string is empty, returns false.
IsMixedCase(const QString & string)262 bool Utility::IsMixedCase(const QString &string)
263 {
264     if (string.isEmpty() || string.length() == 1) {
265         return false;
266     }
267 
268     bool first_char_lower = string[ 0 ].isLower();
269 
270     for (int i = 1; i < string.length(); ++i) {
271         if (string[ i ].isLower() != first_char_lower) {
272             return true;
273         }
274     }
275 
276     return false;
277 }
278 
279 // Returns a substring from a specified QStringRef;
280 // the characters included are in the interval:
281 // [ start_index, end_index >
Substring(int start_index,int end_index,const QStringRef & string)282 QString Utility::Substring(int start_index, int end_index, const QStringRef &string)
283 {
284     return string.mid(start_index, end_index - start_index).toString();
285 }
286 
287 
288 // Returns a substring of a specified string;
289 // the characters included are in the interval:
290 // [ start_index, end_index >
Substring(int start_index,int end_index,const QString & string)291 QString Utility::Substring(int start_index, int end_index, const QString &string)
292 {
293     return string.mid(start_index, end_index - start_index);
294 
295 }
296 
297 // Returns a substring of a specified string;
298 // the characters included are in the interval:
299 // [ start_index, end_index >
SubstringRef(int start_index,int end_index,const QString & string)300 QStringRef Utility::SubstringRef(int start_index, int end_index, const QString &string)
301 {
302     return string.midRef(start_index, end_index - start_index);
303 }
304 // Replace the first occurrence of string "before"
305 // with string "after" in string "string"
ReplaceFirst(const QString & before,const QString & after,const QString & string)306 QString Utility::ReplaceFirst(const QString &before, const QString &after, const QString &string)
307 {
308     int start_index = string.indexOf(before);
309     int end_index   = start_index + before.length();
310     return Substring(0, start_index, string) + after + Substring(end_index, string.length(), string);
311 }
312 
313 
314 // Copies every file and folder in the source folder
315 // to the destination folder; the paths to the folders are submitted;
316 // the destination folder needs to be created in advance
CopyFiles(const QString & fullfolderpath_source,const QString & fullfolderpath_destination)317 void Utility::CopyFiles(const QString &fullfolderpath_source, const QString &fullfolderpath_destination)
318 {
319     QDir folder_source(fullfolderpath_source);
320     QDir folder_destination(fullfolderpath_destination);
321     folder_source.setFilter(QDir::AllDirs |
322                             QDir::Files |
323                             QDir::NoDotAndDotDot |
324                             QDir::NoSymLinks |
325                             QDir::Hidden);
326     // Erase all the files in this folder
327     foreach(QFileInfo file, folder_source.entryInfoList()) {
328         if ((file.fileName() != ".") && (file.fileName() != "..")) {
329             // If it's a file, copy it
330             if (file.isFile()) {
331                 QString destination = fullfolderpath_destination + "/" + file.fileName();
332                 bool success = QFile::copy(file.absoluteFilePath(), destination);
333 
334                 if (!success) {
335                     std::string msg = file.absoluteFilePath().toStdString() + ": " + destination.toStdString();
336                     throw(CannotCopyFile(msg));
337                 }
338             }
339             // Else it's a directory, copy everything in it
340             // to a new folder of the same name in the destination folder
341             else {
342                 folder_destination.mkpath(file.fileName());
343                 CopyFiles(file.absoluteFilePath(), fullfolderpath_destination + "/" + file.fileName());
344             }
345         }
346     }
347 }
348 
349 
350 
351 //
352 //   Delete a directory along with all of its contents.
353 //
354 //   \param dirName Path of directory to remove.
355 //   \return true on success; false on error.
356 //
removeDir(const QString & dirName)357 bool Utility::removeDir(const QString &dirName)
358 {
359     bool result = true;
360     QDir dir(dirName);
361 
362     if (dir.exists(dirName)) {
363         Q_FOREACH(QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden  | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) {
364             if (info.isDir()) {
365                 result = removeDir(info.absoluteFilePath());
366             } else {
367                 result = SDeleteFile(info.absoluteFilePath());
368             }
369 
370             if (!result) {
371                 return result;
372             }
373         }
374         result = dir.rmdir(dirName);
375     }
376     return result;
377 }
378 
379 
380 
381 // Deletes the specified file if it exists
SDeleteFile(const QString & fullfilepath)382 bool Utility::SDeleteFile(const QString &fullfilepath)
383 {
384     // Make sure the path exists, otherwise very
385     // bad things could happen
386     if (!QFileInfo(fullfilepath).exists()) {
387         return false;
388     }
389 
390     QFile file(fullfilepath);
391     bool deleted = file.remove();
392     // Some multiple file deletion operations fail on Windows, so we try once more.
393     if (!deleted) {
394         qApp->processEvents();
395         SleepFunctions::msleep(100);
396         deleted = file.remove();
397     }
398     return deleted;
399 }
400 
401 
402 // Copies File from full Inpath to full OutPath with overwrite if needed
ForceCopyFile(const QString & fullinpath,const QString & fulloutpath)403 bool Utility::ForceCopyFile(const QString &fullinpath, const QString &fulloutpath)
404 {
405     if (!QFileInfo(fullinpath).exists()) {
406         return false;
407     }
408     if (QFileInfo::exists(fulloutpath)) {
409         Utility::SDeleteFile(fulloutpath);
410     }
411     return QFile::copy(fullinpath, fulloutpath);
412 }
413 
414 
415 // Needed to add the S to this routine name to prevent collisions on Windows
416 // We had to do the same thing for DeleteFile earlier
SMoveFile(const QString & oldfilepath,const QString & newfilepath)417 bool Utility::SMoveFile(const QString &oldfilepath, const QString &newfilepath)
418 {
419     // Make sure the path exists, otherwise very
420     // bad things could happen
421     if (!QFileInfo(oldfilepath).exists()) {
422         return false;
423     }
424 
425     // check if these are identical files on the file system
426     // and if so no copy and delete sequence is needed
427     if (QFileInfo(oldfilepath) == QFileInfo(newfilepath)) {
428         return true;
429     }
430 
431     // Ensure that the newfilepath doesn't already exist but due to case insenstive file systems
432     // check if we are actually moving to an identical path with a different case.
433     if (QFileInfo(newfilepath).exists() && QFileInfo(oldfilepath) != QFileInfo(newfilepath)) {
434         return false;
435     }
436 
437     // copy file from old file path to new file path
438     bool success = QFile::copy(oldfilepath, newfilepath);
439     // if and only if copy succeeds then delete old file
440     if (success) {
441         Utility::SDeleteFile(oldfilepath);
442     }
443     return success;
444 }
445 
446 
RenameFile(const QString & oldfilepath,const QString & newfilepath)447 bool Utility::RenameFile(const QString &oldfilepath, const QString &newfilepath)
448 {
449     // Make sure the path exists, otherwise very
450     // bad things could happen
451     if (!QFileInfo(oldfilepath).exists()) {
452         return false;
453     }
454 
455     // Ensure that the newfilepath doesn't already exist but due to case insenstive file systems
456     // check if we are actually renaming to an identical path with a different case.
457     if (QFileInfo(newfilepath).exists() && QFileInfo(oldfilepath) != QFileInfo(newfilepath)) {
458         return false;
459     }
460 
461     // On case insensitive file systems, QFile::rename fails when the new name is the
462     // same (case insensitive) to the old one. This is workaround for that issue.
463     int ret = -1;
464 #if defined(Q_OS_WIN32)
465     ret = _wrename(Utility::QStringToStdWString(oldfilepath).data(), Utility::QStringToStdWString(newfilepath).data());
466 #else
467     ret = rename(oldfilepath.toUtf8().data(), newfilepath.toUtf8().data());
468 #endif
469 
470     if (ret == 0) {
471         return true;
472     }
473 
474     return false;
475 }
476 
477 
GetTemporaryFileNameWithExtension(const QString & extension)478 QString Utility::GetTemporaryFileNameWithExtension(const QString &extension)
479 {
480     SettingsStore ss;
481     QString temp_path = ss.tempFolderHome();
482     if (temp_path == "<SIGIL_DEFAULT_TEMP_HOME>") {
483         temp_path = QDir::tempPath();
484     }
485     return temp_path +  "/sigil_" + Utility::CreateUUID() + extension;
486 }
487 
488 
489 // Returns true if the file can be read;
490 // shows an error dialog if it can't
491 // with a message elaborating what's wrong
IsFileReadable(const QString & fullfilepath)492 bool Utility::IsFileReadable(const QString &fullfilepath)
493 {
494     // Qt has <QFileInfo>.exists() and <QFileInfo>.isReadable()
495     // functions, but then we would have to create our own error
496     // message for each of those situations (and more). Trying to
497     // actually open the file enables us to retrieve the exact
498     // reason preventing us from reading the file in an error string.
499     QFile file(fullfilepath);
500 
501     // Check if we can open the file
502     if (!file.open(QFile::ReadOnly)) {
503         Utility::DisplayStdErrorDialog(
504             QObject::tr("Cannot read file %1:\n%2.")
505             .arg(fullfilepath)
506             .arg(file.errorString())
507         );
508         return false;
509     }
510 
511     file.close();
512     return true;
513 }
514 
515 
516 // Reads the text file specified with the full file path;
517 // text needs to be in UTF-8 or UTF-16; if the file cannot
518 // be read, an error dialog is shown and an empty string returned
ReadUnicodeTextFile(const QString & fullfilepath)519 QString Utility::ReadUnicodeTextFile(const QString &fullfilepath)
520 {
521     // TODO: throw an exception instead of
522     // returning an empty string
523     QFile file(fullfilepath);
524 
525     // Check if we can open the file
526     if (!file.open(QFile::ReadOnly)) {
527         std::string msg = fullfilepath.toStdString() + ": " + file.errorString().toStdString();
528         throw(CannotOpenFile(msg));
529     }
530 
531     QTextStream in(&file);
532     // Input should be UTF-8
533     in.setCodec("UTF-8");
534     // This will automatically switch reading from
535     // UTF-8 to UTF-16 if a BOM is detected
536     in.setAutoDetectUnicode(true);
537     return ConvertLineEndings(in.readAll());
538 }
539 
540 
541 // Writes the provided text variable to the specified
542 // file; if the file exists, it is truncated
WriteUnicodeTextFile(const QString & text,const QString & fullfilepath)543 void Utility::WriteUnicodeTextFile(const QString &text, const QString &fullfilepath)
544 {
545     QFile file(fullfilepath);
546 
547     if (!file.open(QIODevice::WriteOnly |
548                    QIODevice::Truncate  |
549                    QIODevice::Text
550                   )
551        ) {
552         std::string msg = file.fileName().toStdString() + ": " + file.errorString().toStdString();
553         throw(CannotOpenFile(msg));
554     }
555 
556     QTextStream out(&file);
557     // We ALWAYS output in UTF-8
558     out.setCodec("UTF-8");
559     out << text;
560 }
561 
562 
563 // Converts Mac and Windows style line endings to Unix style
564 // line endings that are expected throughout the Qt framework
ConvertLineEndings(const QString & text)565 QString Utility::ConvertLineEndings(const QString &text)
566 {
567     QString newtext(text);
568     return newtext.replace("\x0D\x0A", "\x0A").replace("\x0D", "\x0A");
569 }
570 
571 
572 // Decodes XML escaped string to normal text
573 // &amp; -> "&"    &apos; -> "'"  &quot; -> "\""   &lt; -> "<"  &gt; -> ">"
DecodeXML(const QString & text)574 QString Utility::DecodeXML(const QString &text)
575 {
576     QString newtext(text);
577     newtext.replace("&apos;", "'");
578     newtext.replace("&quot;", "\"");
579     newtext.replace("&lt;", "<");
580     newtext.replace("&gt;", ">");
581     newtext.replace("&amp;", "&");
582     return newtext;
583 }
584 
EncodeXML(const QString & text)585 QString Utility::EncodeXML(const QString &text)
586 {
587     QString newtext = Utility::DecodeXML(text);
588     return newtext.toHtmlEscaped();
589 }
590 
591 
592 
593 // From the IRI spec rfc3987
594 // iunreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~" / ucschar
595 //
596 //    ucschar        = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
597 //                   / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
598 //                   / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
599 //                   / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
600 //                   / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
601 //                   / %xD0000-DFFFD / %xE1000-EFFFD
602 // But currently nothing *after* the 0x30000 plane is even defined
603 
NeedToPercentEncode(uint32_t cp)604 bool Utility::NeedToPercentEncode(uint32_t cp)
605 {
606     // sequence matters for both correctness and speed
607     if (cp < 128) {
608         if (URL_SAFE.contains(QChar(cp))) return false;
609         return true;
610     }
611     if (cp < 0xA0) return true;
612     if (cp <= 0xD7FF) return false;
613     if (cp < 0xF900) return true;
614     if (cp <= 0xFDCF) return false;
615     if (cp < 0xFDF0) return true;
616     if (cp <= 0xFFEF) return false;
617     if (cp < 0x10000) return true;
618     if (cp <= 0x1FFFD) return false;
619     if (cp < 0x20000) return true;
620     if (cp <= 0x2FFFD) return false;
621     if (cp < 0x30000) return true;
622     if (cp <= 0x3FFFD) return false;
623     return true;
624 }
625 
626 // this is meant to work on paths, not paths and fragments and schemes
627 // therefore do not leave # chars unencoded
URLEncodePath(const QString & path)628 QString Utility::URLEncodePath(const QString &path)
629 {
630     // some very poorly written software uses xml escaping of the
631     // "&" instead of url encoding when building hrefs
632     // So run xmldecode first to convert them to normal characters before
633     // url encoding them
634     QString newpath = DecodeXML(path);
635 
636     // then undo any existing url encoding
637     newpath = URLDecodePath(newpath);
638 
639     QString result = "";
640     QVector<uint32_t> codepoints = newpath.toUcs4();
641     for (int i = 0; i < codepoints.size(); i++) {
642         uint32_t cp = codepoints.at(i);
643         QString s = QString::fromUcs4(&cp, 1);
644         if (NeedToPercentEncode(cp)) {
645             QByteArray b = s.toUtf8();
646             for (int j = 0; j < b.size(); j++) {
647                 uint8_t bval = b.at(j);
648                 QString val = QString::number(bval,16);
649                 val = val.toUpper();
650                 if (val.size() == 1) val.prepend("0");
651                 val.prepend("%");
652                 result.append(val);
653             }
654         } else {
655             result.append(s);
656         }
657     }
658     // qDebug() << "In Utility URLEncodePath: " << result;
659     // Previously was:
660     // encoded_url = QUrl::toPercentEncoding(newpath, QByteArray("/"), QByteArray("#"));
661     // encoded_path = scheme + QString::fromUtf8(encoded_url.constData(), encoded_url.count());
662     return result;
663 }
664 
665 
URLDecodePath(const QString & path)666 QString Utility::URLDecodePath(const QString &path)
667 {
668     QString apath(path);
669     // some very poorly written software uses xml-escape on hrefs
670     // instead of properly url encoding them, so look for the
671     // the "&" character which should *not* exist if properly
672     // url encoded and if found try to xml decode them first
673     apath = DecodeXML(apath);
674     return QUrl::fromPercentEncoding(apath.toUtf8());
675 }
676 
677 
DisplayExceptionErrorDialog(const QString & error_info)678 void Utility::DisplayExceptionErrorDialog(const QString &error_info)
679 {
680     QMessageBox message_box(QApplication::activeWindow());
681     message_box.setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
682     message_box.setModal(true);
683     message_box.setIcon(QMessageBox::Critical);
684     message_box.setWindowTitle("Sigil");
685     // Spaces are added to the end because otherwise the dialog is too small.
686     message_box.setText(QObject::tr("Sigil has encountered a problem.") % "                                                                                                       ");
687     message_box.setInformativeText(QObject::tr("Sigil may need to close."));
688     message_box.setStandardButtons(QMessageBox::Close);
689     QStringList detailed_text;
690     detailed_text << "Error info: "    + error_info
691                   << "Sigil version: " + QString(SIGIL_FULL_VERSION)
692                   << "Runtime Qt: "    + QString(qVersion())
693                   << "Compiled Qt: "   + QString(QT_VERSION_STR)
694                   << "System: "        + QSysInfo::prettyProductName()
695                   << "Architecture: "  + QSysInfo::currentCpuArchitecture();
696 
697     message_box.setDetailedText(detailed_text.join("\n"));
698     message_box.exec();
699 }
700 
701 
DisplayStdErrorDialog(const QString & error_message,const QString & detailed_text)702 void Utility::DisplayStdErrorDialog(const QString &error_message, const QString &detailed_text)
703 {
704     QMessageBox message_box(QApplication::activeWindow());
705     message_box.setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
706     message_box.setModal(true);
707     message_box.setIcon(QMessageBox::Critical);
708     message_box.setWindowTitle("Sigil");
709     message_box.setText(error_message);
710 
711     if (!detailed_text.isEmpty()) {
712         message_box.setDetailedText(detailed_text);
713     }
714 
715     message_box.setStandardButtons(QMessageBox::Close);
716     message_box.exec();
717 }
718 
719 
DisplayStdWarningDialog(const QString & warning_message,const QString & detailed_text)720 void Utility::DisplayStdWarningDialog(const QString &warning_message, const QString &detailed_text)
721 {
722     SigilMessageBox message_box(QApplication::activeWindow());
723     message_box.setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
724     message_box.setModal(true);
725     message_box.setIcon(QMessageBox::Warning);
726     message_box.setWindowTitle("Sigil");
727     message_box.setText(warning_message);
728     message_box.setTextFormat(Qt::RichText);
729 
730     if (!detailed_text.isEmpty()) {
731         message_box.setDetailedText(detailed_text);
732     }
733     message_box.setStandardButtons(QMessageBox::Ok);
734     message_box.exec();
735 }
736 
737 // Returns a value for the environment variable name passed;
738 // if the env var isn't set, it returns an empty string
GetEnvironmentVar(const QString & variable_name)739 QString Utility::GetEnvironmentVar(const QString &variable_name)
740 {
741 #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
742     // The only time this might fall down is on Linux when an
743     // environment variable holds bytedata. Don't use this
744     // utility function for retrieval if that's the case.
745     return qEnvironmentVariable(variable_name.toUtf8().constData(), "").trimmed();
746 #else
747     // This will typically only be used on older Qts on Linux
748     return QProcessEnvironment::systemEnvironment().value(variable_name, "").trimmed();
749 #endif
750 }
751 
752 
753 // Returns the same number, but rounded to one decimal place
RoundToOneDecimal(float number)754 float Utility::RoundToOneDecimal(float number)
755 {
756     return QString::number(number, 'f', 1).toFloat();
757 }
758 
759 
GetMainWindow()760 QWidget *Utility::GetMainWindow()
761 {
762     QWidget *parent_window = QApplication::activeWindow();
763     while (parent_window && !(qobject_cast<QMainWindow *>(parent_window))) {
764         parent_window = parent_window->parentWidget();
765     }
766 
767     return parent_window;
768 }
769 
770 
getSpellingSafeText(const QString & raw_text)771 QString Utility::getSpellingSafeText(const QString &raw_text)
772 {
773     // There is currently a problem with Hunspell if we attempt to pass
774     // words with smart apostrophes from the CodeView encoding.
775     // Hunspell dictionaries typically store their main wordlist using
776     // the dumb apostrophe variants only to save space and speed checking
777     QString text(raw_text);
778     text.replace(QChar(0x00ad),"");
779     return text.replace(QChar(0x2019),QChar(0x27));
780 }
781 
782 
has_non_ascii_chars(const QString & str)783 bool Utility::has_non_ascii_chars(const QString &str)
784 {
785     QRegularExpression not_ascii("[^\\x00-\\x7F]");
786     QRegularExpressionMatch mo = not_ascii.match(str);
787     return mo.hasMatch();
788 }
789 
use_filename_warning(const QString & filename)790 bool Utility::use_filename_warning(const QString &filename)
791 {
792     if (has_non_ascii_chars(filename)) {
793         return QMessageBox::Apply == QMessageBox::warning(QApplication::activeWindow(),
794                 tr("Sigil"),
795                 tr("The requested file name contains non-ASCII characters. "
796                    "You should only use ASCII characters in filenames. "
797                    "Using non-ASCII characters can prevent the EPUB from working "
798                    "with some readers.\n\n"
799                    "Continue using the requested filename?"),
800                 QMessageBox::Cancel|QMessageBox::Apply);
801     }
802     return true;
803 }
804 
805 #if defined(Q_OS_WIN32)
QStringToStdWString(const QString & str)806 std::wstring Utility::QStringToStdWString(const QString &str)
807 {
808     return std::wstring((const wchar_t *)str.utf16());
809 }
810 
stdWStringToQString(const std::wstring & str)811 QString Utility::stdWStringToQString(const std::wstring &str)
812 {
813     return QString::fromUtf16((const ushort *)str.c_str());
814 }
815 #endif
816 
817 
UnZip(const QString & zippath,const QString & destpath)818 bool Utility::UnZip(const QString &zippath, const QString &destpath)
819 {
820     int res = 0;
821     QDir dir(destpath);
822     if (!cp437) {
823         cp437 = new QCodePage437Codec();
824     }
825 #ifdef Q_OS_WIN32
826     zlib_filefunc64_def ffunc;
827     fill_win32_filefunc64W(&ffunc);
828     unzFile zfile = unzOpen2_64(Utility::QStringToStdWString(QDir::toNativeSeparators(zippath)).c_str(), &ffunc);
829 #else
830     unzFile zfile = unzOpen64(QDir::toNativeSeparators(zippath).toUtf8().constData());
831 #endif
832 
833     if ((zfile == NULL) || (!IsFileReadable(zippath)) || (!dir.exists())) {
834         return false;
835     }
836 
837     res = unzGoToFirstFile(zfile);
838 
839     if (res == UNZ_OK) {
840         do {
841             // Get the name of the file in the archive.
842             char file_name[MAX_PATH] = {0};
843             unz_file_info64 file_info;
844             unzGetCurrentFileInfo64(zfile, &file_info, file_name, MAX_PATH, NULL, 0, NULL, 0);
845             QString qfile_name;
846             QString cp437_file_name;
847             qfile_name = QString::fromUtf8(file_name);
848             if (!(file_info.flag & (1<<11))) {
849                 // General purpose bit 11 says the filename is utf-8 encoded. If not set then
850                 // IBM 437 encoding might be used.
851                 cp437_file_name = cp437->toUnicode(file_name);
852             }
853 
854             // If there is no file name then we can't do anything with it.
855             if (!qfile_name.isEmpty()) {
856 
857                 // for security reasons against maliciously crafted zip archives
858                 // we need the file path to always be inside the target folder
859                 // and not outside, so we will remove all illegal backslashes
860                 // and all relative upward paths segments "/../" from the zip's local
861                 // file name/path before prepending the target folder to create
862                 // the final path
863 
864                 QString original_path = qfile_name;
865                 bool evil_or_corrupt_epub = false;
866 
867                 if (qfile_name.contains("\\")) evil_or_corrupt_epub = true;
868                 qfile_name = "/" + qfile_name.replace("\\","");
869 
870                 if (qfile_name.contains("/../")) evil_or_corrupt_epub = true;
871                 qfile_name = qfile_name.replace("/../","/");
872 
873                 while(qfile_name.startsWith("/")) {
874                     qfile_name = qfile_name.remove(0,1);
875                 }
876 
877                 if (cp437_file_name.contains("\\")) evil_or_corrupt_epub = true;
878                 cp437_file_name = "/" + cp437_file_name.replace("\\","");
879 
880                 if (cp437_file_name.contains("/../")) evil_or_corrupt_epub = true;
881                 cp437_file_name = cp437_file_name.replace("/../","/");
882 
883                 while(cp437_file_name.startsWith("/")) {
884                   cp437_file_name = cp437_file_name.remove(0,1);
885                 }
886 
887                 if (evil_or_corrupt_epub) {
888                     unzCloseCurrentFile(zfile);
889                     unzClose(zfile);
890                     // throw (UNZIPLoadParseError(QString(QObject::tr("Possible evil or corrupt zip file name: %1")).arg(original_path).toStdString()));
891                     return false;
892                 }
893 
894                 // We use the dir object to create the path in the temporary directory.
895                 // Unfortunately, we need a dir ojbect to do this as it's not a static function.
896                 // Full file path in the temporary directory.
897                 QString file_path = destpath + "/" + qfile_name;
898                 QFileInfo qfile_info(file_path);
899 
900                 // Is this entry a directory?
901                 if (file_info.uncompressed_size == 0 && qfile_name.endsWith('/')) {
902                     dir.mkpath(qfile_name);
903                     continue;
904                 } else {
905                     if (!qfile_info.path().isEmpty()) dir.mkpath(qfile_info.path());
906                 }
907 
908                 // Open the file entry in the archive for reading.
909                 if (unzOpenCurrentFile(zfile) != UNZ_OK) {
910                     unzClose(zfile);
911                     return false;
912                 }
913 
914                 // Open the file on disk to write the entry in the archive to.
915                 QFile entry(file_path);
916 
917                 if (!entry.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
918                     unzCloseCurrentFile(zfile);
919                     unzClose(zfile);
920                     return false;
921                 }
922 
923                 // Buffered reading and writing.
924                 char buff[BUFF_SIZE] = {0};
925                 int read = 0;
926 
927                 while ((read = unzReadCurrentFile(zfile, buff, BUFF_SIZE)) > 0) {
928                     entry.write(buff, read);
929                 }
930 
931                 entry.close();
932 
933                 // Read errors are marked by a negative read amount.
934                 if (read < 0) {
935                     unzCloseCurrentFile(zfile);
936                     unzClose(zfile);
937                     return false;
938                 }
939 
940                 // The file was read but the CRC did not match.
941                 // We don't check the read file size vs the uncompressed file size
942                 // because if they're different there should be a CRC error.
943                 if (unzCloseCurrentFile(zfile) == UNZ_CRCERROR) {
944                     unzClose(zfile);
945                     return false;
946                 }
947 
948                 if (!cp437_file_name.isEmpty() && cp437_file_name != qfile_name) {
949                     QString cp437_file_path = destpath + "/" + cp437_file_name;
950                     QFile::copy(file_path, cp437_file_path);
951                 }
952             }
953         } while ((res = unzGoToNextFile(zfile)) == UNZ_OK);
954     }
955 
956     if (res != UNZ_END_OF_LIST_OF_FILE) {
957         unzClose(zfile);
958         return false;
959     }
960 
961     unzClose(zfile);
962     return true;
963 }
964 
ZipInspect(const QString & zippath)965 QStringList Utility::ZipInspect(const QString &zippath)
966 {
967     QStringList filelist;
968     int res = 0;
969 
970     if (!cp437) {
971         cp437 = new QCodePage437Codec();
972     }
973 #ifdef Q_OS_WIN32
974     zlib_filefunc64_def ffunc;
975     fill_win32_filefunc64W(&ffunc);
976     unzFile zfile = unzOpen2_64(Utility::QStringToStdWString(QDir::toNativeSeparators(zippath)).c_str(), &ffunc);
977 #else
978     unzFile zfile = unzOpen64(QDir::toNativeSeparators(zippath).toUtf8().constData());
979 #endif
980 
981     if ((zfile == NULL) || (!IsFileReadable(zippath))) {
982         return filelist;
983     }
984     res = unzGoToFirstFile(zfile);
985     if (res == UNZ_OK) {
986         do {
987             // Get the name of the file in the archive.
988             char file_name[MAX_PATH] = {0};
989             unz_file_info64 file_info;
990             unzGetCurrentFileInfo64(zfile, &file_info, file_name, MAX_PATH, NULL, 0, NULL, 0);
991             QString qfile_name;
992             QString cp437_file_name;
993             qfile_name = QString::fromUtf8(file_name);
994             if (!(file_info.flag & (1<<11))) {
995                 cp437_file_name = cp437->toUnicode(file_name);
996             }
997 
998             // If there is no file name then we can't do anything with it.
999             if (!qfile_name.isEmpty()) {
1000                 if (!cp437_file_name.isEmpty() && cp437_file_name != qfile_name) {
1001                     filelist.append(cp437_file_name);
1002                 } else {
1003                     filelist.append(qfile_name);
1004                 }
1005             }
1006         } while ((res = unzGoToNextFile(zfile)) == UNZ_OK);
1007     }
1008     unzClose(zfile);
1009     return filelist;
1010 }
1011 
1012 // some utilities for working with absolute and book relative paths
1013 
longestCommonPath(const QStringList & filepaths,const QString & sep)1014 QString Utility::longestCommonPath(const QStringList& filepaths, const QString& sep)
1015 {
1016     if (filepaths.isEmpty()) return QString();
1017     if (filepaths.length() == 1) return QFileInfo(filepaths.at(0)).absolutePath() + sep;
1018     QStringList fpaths(filepaths);
1019     fpaths.sort();
1020     const QStringList segs1 = fpaths.first().split(sep);
1021     const QStringList segs2 = fpaths.last().split(sep);
1022     QStringList res;
1023     int i = 0;
1024     while((i < segs1.length()) && (i < segs2.length()) && (segs1.at(i) == segs2.at(i))) {
1025         res.append(segs1.at(i));
1026         i++;
1027     }
1028     if (res.length() == 0) return sep;
1029     return res.join(sep) + sep;
1030 }
1031 
1032 
1033 // works with absolute paths and book (internal to epub) paths
resolveRelativeSegmentsInFilePath(const QString & file_path,const QString & sep)1034 QString Utility::resolveRelativeSegmentsInFilePath(const QString& file_path, const QString &sep)
1035 {
1036     const QStringList segs = file_path.split(sep);
1037     QStringList res;
1038     for (int i = 0; i < segs.length(); i++) {
1039         // FIXME skip empty segments but not at the front when windows
1040         if (segs.at(i) == ".") continue;
1041         if (segs.at(i) == "..") {
1042             if (!res.isEmpty()) {
1043                 res.removeLast();
1044             } else {
1045                 qDebug() << "Error resolving relative path segments";
1046                 qDebug() << "original file path: " << file_path;
1047             }
1048         } else {
1049             res << segs.at(i);
1050         }
1051     }
1052     return res.join(sep);
1053 }
1054 
1055 
1056 // Generate relative path to destination from starting directory path
1057 // Both paths should be cannonical
relativePath(const QString & destination,const QString & start_dir)1058 QString Utility::relativePath(const QString & destination, const QString & start_dir)
1059 {
1060     QString dest(destination);
1061     QString start(start_dir);
1062 
1063     // first handle the special case
1064     if (start_dir.isEmpty()) return destination;
1065 
1066     QChar sep = '/';
1067 
1068     // remove any trailing path separators from both paths
1069     while (dest.endsWith(sep)) dest.chop(1);
1070     while (start.endsWith(sep)) start.chop(1);
1071 
1072     QStringList dsegs = dest.split(sep, QString::KeepEmptyParts);
1073     QStringList ssegs = start.split(sep, QString::KeepEmptyParts);
1074     QStringList res;
1075     int i = 0;
1076     int nd = dsegs.size();
1077     int ns = ssegs.size();
1078     // skip over starting common path segments in both paths
1079     while (i < ns && i < nd && (dsegs.at(i) == ssegs.at(i))) {
1080         i++;
1081     }
1082     // now "move up" for each remaining path segment in the starting directory
1083     int p = i;
1084     while (p < ns) {
1085         res.append("..");
1086         p++;
1087     }
1088     // And append the remaining path segments from the destination
1089     p = i;
1090     while(p < nd) {
1091         res.append(dsegs.at(p));
1092         p++;
1093     }
1094     return res.join(sep);
1095 }
1096 
1097 // dest_relpath is the relative path to the destination file
1098 // start_folder is the *book path* (path internal to the epub) to the starting folder
buildBookPath(const QString & dest_relpath,const QString & start_folder)1099 QString Utility::buildBookPath(const QString& dest_relpath, const QString& start_folder)
1100 {
1101     QString bookpath(start_folder);
1102     while (bookpath.endsWith("/")) bookpath.chop(1);
1103     if (!bookpath.isEmpty()) {
1104         bookpath = bookpath + "/" + dest_relpath;
1105     } else {
1106         bookpath = dest_relpath;
1107     }
1108     bookpath = resolveRelativeSegmentsInFilePath(bookpath, "/");
1109     return bookpath;
1110 }
1111 
1112 // no ending path separator
startingDir(const QString & file_bookpath)1113 QString Utility::startingDir(const QString &file_bookpath)
1114 {
1115     QString start_dir(file_bookpath);
1116     int pos = start_dir.lastIndexOf('/');
1117     if (pos > -1) {
1118         start_dir = start_dir.left(pos);
1119     } else {
1120         start_dir = "";
1121     }
1122     return start_dir;
1123 }
1124 
1125 // This is the equivalent of Resource.cpp's GetRelativePathFromResource but using book paths
buildRelativePath(const QString & from_file_bkpath,const QString & to_file_bkpath)1126 QString Utility::buildRelativePath(const QString &from_file_bkpath, const QString & to_file_bkpath)
1127 {
1128     // handle special case of "from" and "to" being identical
1129     if (from_file_bkpath == to_file_bkpath) return "";
1130 
1131     // convert start_file_bkpath to start_dir by stripping off existing filename component
1132     return relativePath(to_file_bkpath, startingDir(from_file_bkpath));
1133 }
1134 
1135 // return fully decoded path and fragment (if any) from a raw relative href string.
1136 // any fragment will start with '#', use QUrl to handle parsing and Percent Decoding
parseRelativeHREF(const QString & relative_href)1137 std::pair<QString, QString> Utility::parseRelativeHREF(const QString &relative_href)
1138 {
1139     QUrl href(relative_href);
1140     Q_ASSERT(href.isRelative());
1141     Q_ASSERT(!href.hasQuery());
1142     QString attpath = href.path();
1143     QString fragment = href.fragment();
1144     // fragment will include any # if fragment exists
1145     if (relative_href.indexOf("#") != -1) {
1146         fragment = "#" + fragment;
1147     }
1148     if (attpath.startsWith("./")) attpath = attpath.mid(2,-1);
1149     return std::make_pair(attpath, fragment);
1150 }
1151 
1152 // return a url encoded string for given decoded path and fragment (if any)
1153 // Note: Any fragment will start with a "#" ! to allow links to root as just "#"
buildRelativeHREF(const QString & apath,const QString & afrag)1154 QString Utility::buildRelativeHREF(const QString &apath, const QString &afrag)
1155 {
1156     QString newhref = URLEncodePath(apath);
1157     QString id = afrag;
1158     if (!id.isEmpty()) {
1159         if (id.startsWith("#")) {
1160             id = id.mid(1, -1);
1161         } else {
1162             qDebug() << "Warning: buildRelativeHREF has fragment that does not start with #" << afrag;
1163         }
1164         // technically fragments should be percent encoded if needed
1165         id = URLEncodePath(id);
1166         newhref = newhref + "#" + id;
1167     }
1168     return newhref;
1169 }
1170 
sort_pair_in_reverse(const std::pair<int,QString> & a,const std::pair<int,QString> & b)1171 bool Utility::sort_pair_in_reverse(const std::pair<int,QString> &a, const std::pair<int,QString> &b)
1172 {
1173     return (a.first > b.first);
1174 }
1175 
sortByCounts(const QStringList & folderlst,const QList<int> & countlst)1176 QStringList Utility::sortByCounts(const QStringList &folderlst, const QList<int> &countlst)
1177 {
1178     std::vector< std::pair<int , QString> > vec;
1179     int i = 0;
1180     foreach(QString afolder, folderlst) {
1181         vec.push_back(std::make_pair(countlst.at(i++), afolder));
1182     }
1183     std::sort(vec.begin(), vec.end(), sort_pair_in_reverse);
1184     QStringList sortedlst;
1185     for(unsigned int j=0; j < vec.size(); j++) {
1186         sortedlst << vec[j].second;
1187     }
1188     return sortedlst;
1189 }
1190 
LocaleAwareSort(const QStringList & names)1191 QStringList Utility::LocaleAwareSort(const QStringList &names)
1192 {
1193     SettingsStore ss;
1194     QStringList nlist(names);
1195     QLocale uiLocale(ss.uiLanguage());
1196     QCollator uiCollator(uiLocale);
1197     uiCollator.setCaseSensitivity(Qt::CaseInsensitive);
1198     // use uiCollator.compare(s1, s2)
1199     std::sort(nlist.begin(), nlist.end(), uiCollator);
1200     return nlist;
1201 }
1202 
1203 
AddDarkCSS(const QString & html)1204 QString Utility::AddDarkCSS(const QString &html)
1205 {
1206     QString text = html;
1207     int endheadpos = text.indexOf("</head>");
1208     if (endheadpos == -1) return text;
1209     QPalette pal = qApp->palette();
1210     QString back = pal.color(QPalette::Base).name();
1211     QString fore = pal.color(QPalette::Text).name();
1212 #ifdef Q_OS_MAC
1213     // on macOS the Base role is used for the background not the Window role
1214     QString dark_css_url = "qrc:///dark/mac_dark_scrollbar.css";
1215 #elif defined(Q_OS_WIN32)
1216     QString dark_css_url = "qrc:///dark/win_dark_scrollbar.css";
1217 #else
1218     QString dark_css_url = "qrc:///dark/lin_dark_scrollbar.css";
1219 #endif
1220     QString inject_dark_style = DARK_STYLE.arg(back).arg(fore).arg(dark_css_url);
1221     // qDebug() << "Injecting dark style: ";
1222     text.insert(endheadpos, inject_dark_style);
1223     return text;
1224 }
1225 
1226 
WebViewBackgroundColor(bool followpref)1227 QColor Utility::WebViewBackgroundColor(bool followpref)
1228 {
1229     QColor back_color = Qt::white;
1230     if (IsDarkMode()) {
1231         if (followpref) {
1232             SettingsStore ss;
1233             if (!ss.previewDark()) {
1234                 return back_color;
1235             }
1236         }
1237         QPalette pal = qApp->palette();
1238         back_color = pal.color(QPalette::Base);
1239     }
1240     return back_color;
1241 }
1242 
ValidationResultBrush(const Val_Msg_Type & valres)1243 QBrush Utility::ValidationResultBrush(const Val_Msg_Type &valres)
1244 {
1245     if (Utility::IsDarkMode()) {
1246         switch (valres) {
1247             case Utility::INFO_BRUSH: {
1248                return QBrush(QColor(114, 165, 212));
1249             }
1250             case Utility::WARNING_BRUSH: {
1251                 return QBrush(QColor(212, 165, 114));
1252             }
1253             case Utility::ERROR_BRUSH: {
1254                 return QBrush(QColor(222, 94, 94));
1255             }
1256             default:
1257                 QPalette pal = qApp->palette();
1258                 return QBrush(pal.color(QPalette::Text));
1259         }
1260     } else {
1261         switch (valres) {
1262             case Utility::INFO_BRUSH: {
1263                 return QBrush(QColor(224, 255, 255));;
1264             }
1265             case Utility::WARNING_BRUSH: {
1266                 return QBrush(QColor(255, 255, 230));
1267             }
1268             case Utility::ERROR_BRUSH: {
1269                 return QBrush(QColor(255, 230, 230));
1270             }
1271             default:
1272                 QPalette pal = qApp->palette();
1273                 return QBrush(pal.color(QPalette::Window));
1274         }
1275     }
1276 }
1277 
1278 
createCSVLine(const QStringList & data)1279 QString Utility::createCSVLine(const QStringList &data)
1280 {
1281     QStringList csvline;
1282     foreach(QString val, data) {
1283         bool need_quotes = val.contains(',');
1284         QString cval = "";
1285         if (need_quotes) cval.append('"');
1286         foreach(QChar c, val) {
1287             if (c == '"') cval.append('"');
1288             cval.append(c);
1289         }
1290         if (need_quotes) cval.append('"');
1291         csvline.append(cval);
1292     }
1293     return csvline.join(',');
1294 }
1295 
1296 
parseCSVLine(const QString & data)1297 QStringList Utility::parseCSVLine(const QString &data)
1298 {
1299     auto unquote_val = [](const QString &av) {
1300         QString nv(av);
1301         if (nv.startsWith('"')) nv = nv.mid(1);
1302         if (nv.endsWith('"')) nv = nv.mid(0, nv.length()-1);
1303         return nv;
1304     };
1305 
1306     bool in_quote = false;
1307     QStringList vals;
1308     QString v;
1309     int n = data.size();
1310     int i = 0;
1311     while(i < n) {
1312         QChar c = data.at(i);
1313         if (!in_quote) {
1314             if (c == ',') {
1315                 vals.append(unquote_val(v.trimmed()));
1316                 v = "";
1317             } else  {
1318                 v.append(c);
1319                 if (c == '"') in_quote = true;
1320             }
1321         } else {
1322             v.append(c);
1323             if (c == '"') {
1324                 if ((i+1 < n) && (data.at(i+1) == '"')) {
1325                     i++;
1326                 } else {
1327                     in_quote = false;
1328                 }
1329             }
1330         }
1331         i++;
1332     }
1333     if (!v.isEmpty()) vals.append(unquote_val(v.trimmed()));
1334     return vals;
1335 }
1336 
1337 
GenerateUniqueId(const QString & id,const QSet<QString> & Used)1338 QString Utility::GenerateUniqueId(const QString &id, const QSet<QString>& Used)
1339 {
1340     int cnt = 1;
1341     QString new_id = id + "_" + QString::number(cnt);
1342     while (Used.contains(new_id)) {
1343         cnt++;
1344         new_id = id + "_" + QString::number(cnt);
1345     }
1346     return new_id;
1347 }
1348