1 /*
2  * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
3  * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18  */
19 #include "config.h"
20 
21 #include "common/utility.h"
22 #include "common/filesystembase.h"
23 #include "version.h"
24 
25 // Note:  This file must compile without QtGui
26 #include <QCoreApplication>
27 #include <QSettings>
28 #include <QTextStream>
29 #include <QDir>
30 #include <QFile>
31 #include <QUrl>
32 #include <QProcess>
33 #include <QObject>
34 #include <QThread>
35 #include <QDateTime>
36 #include <QSysInfo>
37 #include <QStandardPaths>
38 #include <QCollator>
39 #include <QSysInfo>
40 #include <qrandom.h>
41 
42 
43 #ifdef Q_OS_UNIX
44 #include <sys/statvfs.h>
45 #include <sys/types.h>
46 #include <unistd.h>
47 #endif
48 
49 #include <cmath>
50 #include <cstdarg>
51 #include <cstring>
52 
53 #if defined(Q_OS_WIN)
54 #include "utility_win.cpp"
55 #elif defined(Q_OS_MAC)
56 #include "utility_mac.cpp"
57 #else
58 #include "utility_unix.cpp"
59 #endif
60 
61 namespace OCC {
62 
63 Q_LOGGING_CATEGORY(lcUtility, "nextcloud.sync.utility", QtInfoMsg)
64 
writeRandomFile(const QString & fname,int size)65 bool Utility::writeRandomFile(const QString &fname, int size)
66 {
67     int maxSize = 10 * 10 * 1024;
68 
69     if (size == -1)
70         size = rand() % maxSize;
71 
72     QString randString;
73     for (int i = 0; i < size; i++) {
74         int r = rand() % 128;
75         randString.append(QChar(r));
76     }
77 
78     QFile file(fname);
79     if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
80         QTextStream out(&file);
81         out << randString;
82         // optional, as QFile destructor will already do it:
83         file.close();
84         return true;
85     }
86     return false;
87 }
88 
formatFingerprint(const QByteArray & fmhash,bool colonSeparated)89 QString Utility::formatFingerprint(const QByteArray &fmhash, bool colonSeparated)
90 {
91     QByteArray hash;
92     int steps = fmhash.length() / 2;
93     for (int i = 0; i < steps; i++) {
94         hash.append(fmhash[i * 2]);
95         hash.append(fmhash[i * 2 + 1]);
96         hash.append(' ');
97     }
98 
99     QString fp = QString::fromLatin1(hash.trimmed());
100     if (colonSeparated) {
101         fp.replace(QLatin1Char(' '), QLatin1Char(':'));
102     }
103 
104     return fp;
105 }
106 
setupFavLink(const QString & folder)107 void Utility::setupFavLink(const QString &folder)
108 {
109     setupFavLink_private(folder);
110 }
111 
removeFavLink(const QString & folder)112 void Utility::removeFavLink(const QString &folder)
113 {
114     removeFavLink_private(folder);
115 }
116 
octetsToString(qint64 octets)117 QString Utility::octetsToString(qint64 octets)
118 {
119 #define THE_FACTOR 1024
120     static const qint64 kb = THE_FACTOR;
121     static const qint64 mb = THE_FACTOR * kb;
122     static const qint64 gb = THE_FACTOR * mb;
123 
124     QString s;
125     qreal value = octets;
126 
127     // Whether we care about decimals: only for GB/MB and only
128     // if it's less than 10 units.
129     bool round = true;
130 
131     // do not display terra byte with the current units, as when
132     // the MB, GB and KB units were made, there was no TB,
133     // see the JEDEC standard
134     // https://en.wikipedia.org/wiki/JEDEC_memory_standards
135     if (octets >= gb) {
136         s = QCoreApplication::translate("Utility", "%L1 GB");
137         value /= gb;
138         round = false;
139     } else if (octets >= mb) {
140         s = QCoreApplication::translate("Utility", "%L1 MB");
141         value /= mb;
142         round = false;
143     } else if (octets >= kb) {
144         s = QCoreApplication::translate("Utility", "%L1 KB");
145         value /= kb;
146     } else {
147         s = QCoreApplication::translate("Utility", "%L1 B");
148     }
149 
150     if (value > 9.95)
151         round = true;
152 
153     if (round)
154         return s.arg(qRound(value));
155 
156     return s.arg(value, 0, 'g', 2);
157 }
158 
159 // Qtified version of get_platforms() in csync_owncloud.c
platform()160 static QLatin1String platform()
161 {
162 #if defined(Q_OS_WIN)
163     return QLatin1String("Windows");
164 #elif defined(Q_OS_MAC)
165     return QLatin1String("Macintosh");
166 #elif defined(Q_OS_LINUX)
167     return QLatin1String("Linux");
168 #elif defined(__DragonFly__) // Q_OS_FREEBSD also defined
169     return QLatin1String("DragonFlyBSD");
170 #elif defined(Q_OS_FREEBSD) || defined(Q_OS_FREEBSD_KERNEL)
171     return QLatin1String("FreeBSD");
172 #elif defined(Q_OS_NETBSD)
173     return QLatin1String("NetBSD");
174 #elif defined(Q_OS_OPENBSD)
175     return QLatin1String("OpenBSD");
176 #elif defined(Q_OS_SOLARIS)
177     return QLatin1String("Solaris");
178 #else
179     return QSysInfo::productType();
180 #endif
181 }
182 
userAgentString()183 QByteArray Utility::userAgentString()
184 {
185     return QStringLiteral("Mozilla/5.0 (%1) mirall/%2 (%3, %4-%5 ClientArchitecture: %6 OsArchitecture: %7)")
186         .arg(platform(),
187             QStringLiteral(MIRALL_VERSION_STRING),
188             qApp->applicationName(),
189             QSysInfo::productType(),
190             QSysInfo::kernelVersion(),
191             QSysInfo::buildCpuArchitecture(),
192             QSysInfo::currentCpuArchitecture())
193         .toLatin1();
194 }
195 
friendlyUserAgentString()196 QByteArray Utility::friendlyUserAgentString()
197 {
198     const auto pattern = QStringLiteral("%1 (Desktop Client - %2)");
199     const auto userAgent = pattern.arg(QSysInfo::machineHostName(), platform());
200     return userAgent.toUtf8();
201 }
202 
hasSystemLaunchOnStartup(const QString & appName)203 bool Utility::hasSystemLaunchOnStartup(const QString &appName)
204 {
205 #if defined(Q_OS_WIN)
206     return hasSystemLaunchOnStartup_private(appName);
207 #else
208     Q_UNUSED(appName)
209     return false;
210 #endif
211 }
212 
hasLaunchOnStartup(const QString & appName)213 bool Utility::hasLaunchOnStartup(const QString &appName)
214 {
215     return hasLaunchOnStartup_private(appName);
216 }
217 
setLaunchOnStartup(const QString & appName,const QString & guiName,bool enable)218 void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable)
219 {
220     setLaunchOnStartup_private(appName, guiName, enable);
221 }
222 
freeDiskSpace(const QString & path)223 qint64 Utility::freeDiskSpace(const QString &path)
224 {
225 #if defined(Q_OS_MAC) || defined(Q_OS_FREEBSD) || defined(Q_OS_FREEBSD_KERNEL) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD)
226     struct statvfs stat;
227     if (statvfs(path.toLocal8Bit().data(), &stat) == 0) {
228         return (qint64)stat.f_bavail * stat.f_frsize;
229     }
230 #elif defined(Q_OS_UNIX)
231     struct statvfs64 stat;
232     if (statvfs64(path.toLocal8Bit().data(), &stat) == 0) {
233         return (qint64)stat.f_bavail * stat.f_frsize;
234     }
235 #elif defined(Q_OS_WIN)
236     ULARGE_INTEGER freeBytes;
237     freeBytes.QuadPart = 0L;
238     if (GetDiskFreeSpaceEx(reinterpret_cast<const wchar_t *>(FileSystem::longWinPath(path).utf16()), &freeBytes, nullptr, nullptr)) {
239         return freeBytes.QuadPart;
240     }
241 #endif
242     return -1;
243 }
244 
compactFormatDouble(double value,int prec,const QString & unit)245 QString Utility::compactFormatDouble(double value, int prec, const QString &unit)
246 {
247     QLocale locale = QLocale::system();
248     QChar decPoint = locale.decimalPoint();
249     QString str = locale.toString(value, 'f', prec);
250     while (str.endsWith(QLatin1Char('0')) || str.endsWith(decPoint)) {
251         if (str.endsWith(decPoint)) {
252             str.chop(1);
253             break;
254         }
255         str.chop(1);
256     }
257     if (!unit.isEmpty())
258         str += (QLatin1Char(' ') + unit);
259     return str;
260 }
261 
escape(const QString & in)262 QString Utility::escape(const QString &in)
263 {
264     return in.toHtmlEscaped();
265 }
266 
rand()267 int Utility::rand()
268 {
269     return QRandomGenerator::global()->bounded(0, RAND_MAX);
270 }
271 
sleep(int sec)272 void Utility::sleep(int sec)
273 {
274     QThread::sleep(sec);
275 }
276 
usleep(int usec)277 void Utility::usleep(int usec)
278 {
279     QThread::usleep(usec);
280 }
281 
282 // This can be overriden from the tests
__anonbc40b8130102() 283 OCSYNC_EXPORT bool fsCasePreserving_override = []() -> bool {
284     QByteArray env = qgetenv("OWNCLOUD_TEST_CASE_PRESERVING");
285     if (!env.isEmpty())
286         return env.toInt();
287     return Utility::isWindows() || Utility::isMac();
288 }();
289 
fsCasePreserving()290 bool Utility::fsCasePreserving()
291 {
292     return fsCasePreserving_override;
293 }
294 
fileNamesEqual(const QString & fn1,const QString & fn2)295 bool Utility::fileNamesEqual(const QString &fn1, const QString &fn2)
296 {
297     const QDir fd1(fn1);
298     const QDir fd2(fn2);
299 
300     // Attention: If the path does not exist, canonicalPath returns ""
301     // ONLY use this function with existing pathes.
302     const QString a = fd1.canonicalPath();
303     const QString b = fd2.canonicalPath();
304     bool re = !a.isEmpty() && QString::compare(a, b, fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive) == 0;
305     return re;
306 }
307 
qDateTimeFromTime_t(qint64 t)308 QDateTime Utility::qDateTimeFromTime_t(qint64 t)
309 {
310     return QDateTime::fromMSecsSinceEpoch(t * 1000);
311 }
312 
qDateTimeToTime_t(const QDateTime & t)313 qint64 Utility::qDateTimeToTime_t(const QDateTime &t)
314 {
315     return t.toMSecsSinceEpoch() / 1000;
316 }
317 
318 namespace {
319     struct Period
320     {
321         const char *name;
322         quint64 msec;
323 
descriptionOCC::__anonbc40b8130211::Period324         QString description(quint64 value) const
325         {
326             return QCoreApplication::translate("Utility", name, nullptr, value);
327         }
328     };
329 // QTBUG-3945 and issue #4855: QT_TRANSLATE_NOOP does not work with plural form because lupdate
330 // limitation unless we fake more arguments
331 // (it must be in the form ("context", "source", "comment", n)
332 #undef QT_TRANSLATE_NOOP
333 #define QT_TRANSLATE_NOOP(ctx, str, ...) str
334     Q_DECL_CONSTEXPR Period periods[] = {
335         { QT_TRANSLATE_NOOP("Utility", "%n year(s)", 0, _), 365 * 24 * 3600 * 1000LL },
336         { QT_TRANSLATE_NOOP("Utility", "%n month(s)", 0, _), 30 * 24 * 3600 * 1000LL },
337         { QT_TRANSLATE_NOOP("Utility", "%n day(s)", 0, _), 24 * 3600 * 1000LL },
338         { QT_TRANSLATE_NOOP("Utility", "%n hour(s)", 0, _), 3600 * 1000LL },
339         { QT_TRANSLATE_NOOP("Utility", "%n minute(s)", 0, _), 60 * 1000LL },
340         { QT_TRANSLATE_NOOP("Utility", "%n second(s)", 0, _), 1000LL },
341         { nullptr, 0 }
342     };
343 } // anonymous namespace
344 
durationToDescriptiveString2(quint64 msecs)345 QString Utility::durationToDescriptiveString2(quint64 msecs)
346 {
347     int p = 0;
348     while (periods[p + 1].name && msecs < periods[p].msec) {
349         p++;
350     }
351 
352     auto firstPart = periods[p].description(int(msecs / periods[p].msec));
353 
354     if (!periods[p + 1].name) {
355         return firstPart;
356     }
357 
358     quint64 secondPartNum = qRound(double(msecs % periods[p].msec) / periods[p + 1].msec);
359 
360     if (secondPartNum == 0) {
361         return firstPart;
362     }
363 
364     return QCoreApplication::translate("Utility", "%1 %2").arg(firstPart, periods[p + 1].description(secondPartNum));
365 }
366 
durationToDescriptiveString1(quint64 msecs)367 QString Utility::durationToDescriptiveString1(quint64 msecs)
368 {
369     int p = 0;
370     while (periods[p + 1].name && msecs < periods[p].msec) {
371         p++;
372     }
373 
374     quint64 amount = qRound(double(msecs) / periods[p].msec);
375     return periods[p].description(amount);
376 }
377 
fileNameForGuiUse(const QString & fName)378 QString Utility::fileNameForGuiUse(const QString &fName)
379 {
380     if (isMac()) {
381         QString n(fName);
382         return n.replace(QLatin1Char(':'), QLatin1Char('/'));
383     }
384     return fName;
385 }
386 
normalizeEtag(QByteArray etag)387 QByteArray Utility::normalizeEtag(QByteArray etag)
388 {
389     /* strip "XXXX-gzip" */
390     if (etag.startsWith('"') && etag.endsWith("-gzip\"")) {
391         etag.chop(6);
392         etag.remove(0, 1);
393     }
394     /* strip trailing -gzip */
395     if (etag.endsWith("-gzip")) {
396         etag.chop(5);
397     }
398     /* strip normal quotes */
399     if (etag.startsWith('"') && etag.endsWith('"')) {
400         etag.chop(1);
401         etag.remove(0, 1);
402     }
403     etag.squeeze();
404     return etag;
405 }
406 
hasDarkSystray()407 bool Utility::hasDarkSystray()
408 {
409     return hasDarkSystray_private();
410 }
411 
412 
platformName()413 QString Utility::platformName()
414 {
415     return QSysInfo::prettyProductName();
416 }
417 
crash()418 void Utility::crash()
419 {
420     volatile int *a = (int *)nullptr;
421     *a = 1;
422 }
423 
424 // Use this functions to retrieve uint/int (often required by Qt and WIN32) from size_t
425 // without compiler warnings about possible truncation
convertSizeToUint(size_t & convertVar)426 uint Utility::convertSizeToUint(size_t &convertVar)
427 {
428     if (convertVar > UINT_MAX) {
429         //throw std::bad_cast();
430         convertVar = UINT_MAX; // intentionally default to wrong value here to not crash: exception handling TBD
431     }
432     return static_cast<uint>(convertVar);
433 }
434 
convertSizeToInt(size_t & convertVar)435 int Utility::convertSizeToInt(size_t &convertVar)
436 {
437     if (convertVar > INT_MAX) {
438         //throw std::bad_cast();
439         convertVar = INT_MAX; // intentionally default to wrong value here to not crash: exception handling TBD
440     }
441     return static_cast<int>(convertVar);
442 }
443 
444 // read the output of the owncloud --version command from the owncloud
445 // version that is on disk. This works for most versions of the client,
446 // because clients that do not yet know the --version flag return the
447 // version in the first line of the help output :-)
448 //
449 // This version only delivers output on linux, as Mac and Win get their
450 // restarting from the installer.
versionOfInstalledBinary(const QString & command)451 QByteArray Utility::versionOfInstalledBinary(const QString &command)
452 {
453     QByteArray re;
454     if (isLinux()) {
455         QString binary(command);
456         if (binary.isEmpty()) {
457             binary = qApp->arguments()[0];
458         }
459         QStringList params;
460         params << QStringLiteral("--version");
461         QProcess process;
462         process.start(binary, params);
463         process.waitForFinished(); // sets current thread to sleep and waits for pingProcess end
464         re = process.readAllStandardOutput();
465         int newline = re.indexOf('\n');
466         if (newline > 0) {
467             re.truncate(newline);
468         }
469     }
470     return re;
471 }
472 
timeAgoInWords(const QDateTime & dt,const QDateTime & from)473 QString Utility::timeAgoInWords(const QDateTime &dt, const QDateTime &from)
474 {
475     QDateTime now = QDateTime::currentDateTimeUtc();
476 
477     if (from.isValid()) {
478         now = from;
479     }
480 
481     if (dt.daysTo(now) == 1) {
482         return QObject::tr("%n day ago", "", dt.daysTo(now));
483     } else if (dt.daysTo(now) > 1) {
484         return QObject::tr("%n days ago", "", dt.daysTo(now));
485     } else {
486         qint64 secs = dt.secsTo(now);
487         if (secs < 0) {
488             return QObject::tr("in the future");
489         }
490 
491         if (floor(secs / 3600.0) > 0) {
492             int hours = floor(secs / 3600.0);
493             if (hours == 1) {
494                 return (QObject::tr("%n hour ago", "", hours));
495             } else {
496                 return (QObject::tr("%n hours ago", "", hours));
497             }
498         } else {
499             int minutes = qRound(secs / 60.0);
500 
501             if (minutes == 0) {
502                 if (secs < 5) {
503                     return QObject::tr("now");
504                 } else {
505                     return QObject::tr("Less than a minute ago");
506                 }
507 
508             } else if (minutes == 1) {
509                 return (QObject::tr("%n minute ago", "", minutes));
510             } else {
511                 return (QObject::tr("%n minutes ago", "", minutes));
512             }
513         }
514     }
515     return QObject::tr("Some time ago");
516 }
517 
518 /* --------------------------------------------------------------------------- */
519 
520 static const char STOPWATCH_END_TAG[] = "_STOPWATCH_END";
521 
start()522 void Utility::StopWatch::start()
523 {
524     _startTime = QDateTime::currentDateTimeUtc();
525     _timer.start();
526 }
527 
stop()528 quint64 Utility::StopWatch::stop()
529 {
530     addLapTime(QLatin1String(STOPWATCH_END_TAG));
531     quint64 duration = _timer.elapsed();
532     _timer.invalidate();
533     return duration;
534 }
535 
reset()536 void Utility::StopWatch::reset()
537 {
538     _timer.invalidate();
539     _startTime.setMSecsSinceEpoch(0);
540     _lapTimes.clear();
541 }
542 
addLapTime(const QString & lapName)543 quint64 Utility::StopWatch::addLapTime(const QString &lapName)
544 {
545     if (!_timer.isValid()) {
546         start();
547     }
548     quint64 re = _timer.elapsed();
549     _lapTimes[lapName] = re;
550     return re;
551 }
552 
startTime() const553 QDateTime Utility::StopWatch::startTime() const
554 {
555     return _startTime;
556 }
557 
timeOfLap(const QString & lapName) const558 QDateTime Utility::StopWatch::timeOfLap(const QString &lapName) const
559 {
560     quint64 t = durationOfLap(lapName);
561     if (t) {
562         QDateTime re(_startTime);
563         return re.addMSecs(t);
564     }
565 
566     return QDateTime();
567 }
568 
durationOfLap(const QString & lapName) const569 quint64 Utility::StopWatch::durationOfLap(const QString &lapName) const
570 {
571     return _lapTimes.value(lapName, 0);
572 }
573 
sortFilenames(QStringList & fileNames)574 void Utility::sortFilenames(QStringList &fileNames)
575 {
576     QCollator collator;
577     collator.setNumericMode(true);
578     collator.setCaseSensitivity(Qt::CaseInsensitive);
579     std::sort(fileNames.begin(), fileNames.end(), collator);
580 }
581 
concatUrlPath(const QUrl & url,const QString & concatPath,const QUrlQuery & queryItems)582 QUrl Utility::concatUrlPath(const QUrl &url, const QString &concatPath,
583     const QUrlQuery &queryItems)
584 {
585     QString path = url.path();
586     if (!concatPath.isEmpty()) {
587         // avoid '//'
588         if (path.endsWith(QLatin1Char('/')) && concatPath.startsWith(QLatin1Char('/'))) {
589             path.chop(1);
590         } // avoid missing '/'
591         else if (!path.endsWith(QLatin1Char('/')) && !concatPath.startsWith(QLatin1Char('/'))) {
592             path += QLatin1Char('/');
593         }
594         path += concatPath; // put the complete path together
595     }
596 
597     QUrl tmpUrl = url;
598     tmpUrl.setPath(path);
599     tmpUrl.setQuery(queryItems);
600     return tmpUrl;
601 }
602 
makeConflictFileName(const QString & fn,const QDateTime & dt,const QString & user)603 QString Utility::makeConflictFileName(
604     const QString &fn, const QDateTime &dt, const QString &user)
605 {
606     QString conflictFileName(fn);
607     // Add conflict tag before the extension.
608     int dotLocation = conflictFileName.lastIndexOf(QLatin1Char('.'));
609     // If no extension, add it at the end  (take care of cases like foo/.hidden or foo.bar/file)
610     if (dotLocation <= conflictFileName.lastIndexOf(QLatin1Char('/')) + 1) {
611         dotLocation = conflictFileName.size();
612     }
613 
614     QString conflictMarker = QStringLiteral(" (conflicted copy ");
615     if (!user.isEmpty()) {
616         // Don't allow parens in the user name, to ensure
617         // we can find the beginning and end of the conflict tag.
618         const auto userName = sanitizeForFileName(user).replace(QLatin1Char('('), QLatin1Char('_')).replace(QLatin1Char(')'), QLatin1Char('_'));;
619         conflictMarker += userName + QLatin1Char(' ');
620     }
621     conflictMarker += dt.toString(QStringLiteral("yyyy-MM-dd hhmmss")) + QLatin1Char(')');
622 
623     conflictFileName.insert(dotLocation, conflictMarker);
624     return conflictFileName;
625 }
626 
isConflictFile(const char * name)627 bool Utility::isConflictFile(const char *name)
628 {
629     const char *bname = std::strrchr(name, '/');
630     if (bname) {
631         bname += 1;
632     } else {
633         bname = name;
634     }
635 
636     // Old pattern
637     if (std::strstr(bname, "_conflict-"))
638         return true;
639 
640     // New pattern
641     if (std::strstr(bname, "(conflicted copy"))
642         return true;
643 
644     return false;
645 }
646 
isConflictFile(const QString & name)647 bool Utility::isConflictFile(const QString &name)
648 {
649     auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
650 
651     if (bname.contains(QStringLiteral("_conflict-")))
652         return true;
653 
654     if (bname.contains(QStringLiteral("(conflicted copy")))
655         return true;
656 
657     return false;
658 }
659 
conflictFileBaseNameFromPattern(const QByteArray & conflictName)660 QByteArray Utility::conflictFileBaseNameFromPattern(const QByteArray &conflictName)
661 {
662     // This function must be able to deal with conflict files for conflict files.
663     // To do this, we scan backwards, for the outermost conflict marker and
664     // strip only that to generate the conflict file base name.
665     auto startOld = conflictName.lastIndexOf("_conflict-");
666 
667     // A single space before "(conflicted copy" is considered part of the tag
668     auto startNew = conflictName.lastIndexOf("(conflicted copy");
669     if (startNew > 0 && conflictName[startNew - 1] == ' ')
670         startNew -= 1;
671 
672     // The rightmost tag is relevant
673     auto tagStart = qMax(startOld, startNew);
674     if (tagStart == -1)
675         return "";
676 
677     // Find the end of the tag
678     auto tagEnd = conflictName.size();
679     auto dot = conflictName.lastIndexOf('.'); // dot could be part of user name for new tag!
680     if (dot > tagStart)
681         tagEnd = dot;
682     if (tagStart == startNew) {
683         auto paren = conflictName.indexOf(')', tagStart);
684         if (paren != -1)
685             tagEnd = paren + 1;
686     }
687     return conflictName.left(tagStart) + conflictName.mid(tagEnd);
688 }
689 
isPathWindowsDrivePartitionRoot(const QString & path)690 bool Utility::isPathWindowsDrivePartitionRoot(const QString &path)
691 {
692     Q_UNUSED(path)
693 #ifdef Q_OS_WIN
694     // should be 2 or 3 characters length
695     if (!(path.size() >= 2 && path.size() <= 3)) {
696         return false;
697     }
698 
699     // must mutch a pattern "[A-Za-z]:"
700     if (!(path.at(1) == QLatin1Char(':') && path.at(0).isLetter())) {
701         return false;
702     }
703 
704     // final check - last character should be either slash/backslash, or, it should be missing
705     return path.size() < 3 || path.at(2) == QLatin1Char('/') || path.at(2) == QLatin1Char('\\');
706 #endif
707     return false;
708 }
709 
sanitizeForFileName(const QString & name)710 QString Utility::sanitizeForFileName(const QString &name)
711 {
712     const auto invalid = QStringLiteral(R"(/?<>\:*|")");
713     QString result;
714     result.reserve(name.size());
715     for (const auto c : name) {
716         if (!invalid.contains(c)
717             && c.category() != QChar::Other_Control
718             && c.category() != QChar::Other_Format) {
719             result.append(c);
720         }
721     }
722     return result;
723 }
724 
725 } // namespace OCC
726