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 // & -> "&" ' -> "'" " -> "\"" < -> "<" > -> ">"
DecodeXML(const QString & text)574 QString Utility::DecodeXML(const QString &text)
575 {
576 QString newtext(text);
577 newtext.replace("'", "'");
578 newtext.replace(""", "\"");
579 newtext.replace("<", "<");
580 newtext.replace(">", ">");
581 newtext.replace("&", "&");
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