1 /****************************************************************************
2 **
3 ** Copyright (C) 2021 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "filepath.h"
27 
28 #include "algorithm.h"
29 #include "commandline.h"
30 #include "environment.h"
31 #include "fileutils.h"
32 #include "hostosinfo.h"
33 #include "qtcassert.h"
34 #include "savefile.h"
35 
36 #include <QDataStream>
37 #include <QDateTime>
38 #include <QDebug>
39 #include <QFileInfo>
40 #include <QOperatingSystemVersion>
41 #include <QRegularExpression>
42 #include <QUrl>
43 #include <qplatformdefs.h>
44 
45 #ifdef Q_OS_WIN
46 #ifdef QTCREATOR_PCH_H
47 #define CALLBACK WINAPI
48 #endif
49 #include <qt_windows.h>
50 #include <shlobj.h>
51 #endif
52 
53 #ifdef Q_OS_OSX
54 #include "fileutils_mac.h"
55 #endif
56 
57 QT_BEGIN_NAMESPACE
operator <<(QDebug dbg,const Utils::FilePath & c)58 QDebug operator<<(QDebug dbg, const Utils::FilePath &c)
59 {
60     return dbg << c.toUserOutput();
61 }
62 
63 QT_END_NAMESPACE
64 
65 namespace Utils {
66 
67 static DeviceFileHooks s_deviceHooks;
68 
69 /*! \class Utils::FileUtils
70 
71   \brief The FileUtils class contains file and directory related convenience
72   functions.
73 
74 */
75 
removeRecursivelyLocal(const FilePath & filePath,QString * error)76 static bool removeRecursivelyLocal(const FilePath &filePath, QString *error)
77 {
78     QTC_ASSERT(!filePath.needsDevice(), return false);
79     QFileInfo fileInfo = filePath.toFileInfo();
80     if (!fileInfo.exists() && !fileInfo.isSymLink())
81         return true;
82     QFile::setPermissions(filePath.toString(), fileInfo.permissions() | QFile::WriteUser);
83     if (fileInfo.isDir()) {
84         QDir dir(filePath.toString());
85         dir.setPath(dir.canonicalPath());
86         if (dir.isRoot()) {
87             if (error) {
88                 *error = QCoreApplication::translate("Utils::FileUtils",
89                     "Refusing to remove root directory.");
90             }
91             return false;
92         }
93         if (dir.path() == QDir::home().canonicalPath()) {
94             if (error) {
95                 *error = QCoreApplication::translate("Utils::FileUtils",
96                     "Refusing to remove your home directory.");
97             }
98             return false;
99         }
100 
101         const QStringList fileNames = dir.entryList(
102                     QDir::Files | QDir::Hidden | QDir::System | QDir::Dirs | QDir::NoDotAndDotDot);
103         for (const QString &fileName : fileNames) {
104             if (!removeRecursivelyLocal(filePath / fileName, error))
105                 return false;
106         }
107         if (!QDir::root().rmdir(dir.path())) {
108             if (error) {
109                 *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove directory \"%1\".")
110                         .arg(filePath.toUserOutput());
111             }
112             return false;
113         }
114     } else {
115         if (!QFile::remove(filePath.toString())) {
116             if (error) {
117                 *error = QCoreApplication::translate("Utils::FileUtils", "Failed to remove file \"%1\".")
118                         .arg(filePath.toUserOutput());
119             }
120             return false;
121         }
122     }
123     return true;
124 }
125 
126 /*!
127   Copies the directory specified by \a srcFilePath recursively to \a tgtFilePath. \a tgtFilePath will contain
128   the target directory, which will be created. Example usage:
129 
130   \code
131     QString error;
132     book ok = Utils::FileUtils::copyRecursively("/foo/bar", "/foo/baz", &error);
133     if (!ok)
134       qDebug() << error;
135   \endcode
136 
137   This will copy the contents of /foo/bar into to the baz directory under /foo, which will be created in the process.
138 
139   \note The \a error parameter is optional.
140 
141   Returns whether the operation succeeded.
142 */
143 
copyRecursively(const FilePath & srcFilePath,const FilePath & tgtFilePath,QString * error)144 bool FileUtils::copyRecursively(const FilePath &srcFilePath, const FilePath &tgtFilePath, QString *error)
145 {
146     return copyRecursively(
147         srcFilePath, tgtFilePath, error, [](const QFileInfo &src, const QFileInfo &dest, QString *error) {
148             if (!QFile::copy(src.filePath(), dest.filePath())) {
149                 if (error) {
150                     *error = QCoreApplication::translate("Utils::FileUtils",
151                                                          "Could not copy file \"%1\" to \"%2\".")
152                                  .arg(FilePath::fromFileInfo(src).toUserOutput(),
153                                       FilePath::fromFileInfo(dest).toUserOutput());
154                 }
155                 return false;
156             }
157             return true;
158         });
159 }
160 
161 /*!
162   Copies a file specified by \a srcFilePath to \a tgtFilePath only if \a srcFilePath is different
163   (file contents and last modification time).
164 
165   Returns whether the operation succeeded.
166 */
167 
copyIfDifferent(const FilePath & srcFilePath,const FilePath & tgtFilePath)168 bool FileUtils::copyIfDifferent(const FilePath &srcFilePath, const FilePath &tgtFilePath)
169 {
170     QTC_ASSERT(srcFilePath.exists(), return false);
171     QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false);
172     QTC_ASSERT(srcFilePath.host() == tgtFilePath.host(), return false);
173 
174     if (tgtFilePath.exists()) {
175         const QDateTime srcModified = srcFilePath.lastModified();
176         const QDateTime tgtModified = tgtFilePath.lastModified();
177         if (srcModified == tgtModified) {
178             const QByteArray srcContents = srcFilePath.fileContents();
179             const QByteArray tgtContents = srcFilePath.fileContents();
180             if (srcContents == tgtContents)
181                 return true;
182         }
183         tgtFilePath.removeFile();
184     }
185 
186     return srcFilePath.copyFile(tgtFilePath);
187 }
188 
189 /*!
190   If this is a directory, the function will recursively check all files and return
191   true if one of them is newer than \a timeStamp. If this is a single file, true will
192   be returned if the file is newer than \a timeStamp.
193 
194   Returns whether at least one file in \a filePath has a newer date than
195   \a timeStamp.
196 */
isNewerThan(const QDateTime & timeStamp) const197 bool FilePath::isNewerThan(const QDateTime &timeStamp) const
198 {
199     if (!exists() || lastModified() >= timeStamp)
200         return true;
201     if (isDir()) {
202         const FilePaths dirContents = dirEntries(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
203         for (const FilePath &entry : dirContents) {
204             if (entry.isNewerThan(timeStamp))
205                 return true;
206         }
207     }
208     return false;
209 }
210 
caseSensitivity() const211 Qt::CaseSensitivity FilePath::caseSensitivity() const
212 {
213     if (m_scheme.isEmpty())
214         return HostOsInfo::fileNameCaseSensitivity();
215 
216     // FIXME: This could or possibly should the target device's file name case sensitivity
217     // into account by diverting to IDevice. However, as this is expensive and we are
218     // in time-critical path here, we go with "good enough" for now:
219     // The first approximation is "Anything unusual is not case sensitive"
220     return Qt::CaseSensitive;
221 }
222 
223 /*!
224   Recursively resolves symlinks if this is a symlink.
225   To resolve symlinks anywhere in the path, see canonicalPath.
226   Unlike QFileInfo::canonicalFilePath(), this function will still return the expected deepest
227   target file even if the symlink is dangling.
228 
229   \note Maximum recursion depth == 16.
230 
231   Returns the symlink target file path.
232 */
resolveSymlinks() const233 FilePath FilePath::resolveSymlinks() const
234 {
235     FilePath current = *this;
236     int links = 16;
237     while (links--) {
238         const FilePath target = current.symLinkTarget();
239         if (target.isEmpty())
240             return current;
241         current = target;
242     }
243     return current;
244 }
245 
246 /*!
247   Recursively resolves possibly present symlinks in this file name.
248   Unlike QFileInfo::canonicalFilePath(), this function will not return an empty
249   string if path doesn't exist.
250 
251   Returns the canonical path.
252 */
canonicalPath() const253 FilePath FilePath::canonicalPath() const
254 {
255     if (needsDevice()) {
256         // FIXME: Not a full solution, but it stays on the right device.
257         return *this;
258     }
259     const QString result = toFileInfo().canonicalFilePath();
260     if (result.isEmpty())
261         return *this;
262     return FilePath::fromString(result);
263 }
264 
operator /(const QString & str) const265 FilePath FilePath::operator/(const QString &str) const
266 {
267     return pathAppended(str);
268 }
269 
clear()270 void FilePath::clear()
271 {
272     m_data.clear();
273     m_host.clear();
274     m_scheme.clear();
275 }
276 
isEmpty() const277 bool FilePath::isEmpty() const
278 {
279     return m_data.isEmpty();
280 }
281 
282 /*!
283   Like QDir::toNativeSeparators(), but use prefix '~' instead of $HOME on unix systems when an
284   absolute path is given.
285 
286   Returns the possibly shortened path with native separators.
287 */
shortNativePath() const288 QString FilePath::shortNativePath() const
289 {
290     if (HostOsInfo::isAnyUnixHost()) {
291         const FilePath home = FileUtils::homePath();
292         if (isChildOf(home)) {
293             return QLatin1Char('~') + QDir::separator()
294                 + QDir::toNativeSeparators(relativeChildPath(home).toString());
295         }
296     }
297     return toUserOutput();
298 }
299 
fileSystemFriendlyName(const QString & name)300 QString FileUtils::fileSystemFriendlyName(const QString &name)
301 {
302     QString result = name;
303     result.replace(QRegularExpression(QLatin1String("\\W")), QLatin1String("_"));
304     result.replace(QRegularExpression(QLatin1String("_+")), QLatin1String("_")); // compact _
305     result.remove(QRegularExpression(QLatin1String("^_*"))); // remove leading _
306     result.remove(QRegularExpression(QLatin1String("_+$"))); // remove trailing _
307     if (result.isEmpty())
308         result = QLatin1String("unknown");
309     return result;
310 }
311 
indexOfQmakeUnfriendly(const QString & name,int startpos)312 int FileUtils::indexOfQmakeUnfriendly(const QString &name, int startpos)
313 {
314     static const QRegularExpression checkRegExp(QLatin1String("[^a-zA-Z0-9_.-]"));
315     return checkRegExp.match(name, startpos).capturedStart();
316 }
317 
qmakeFriendlyName(const QString & name)318 QString FileUtils::qmakeFriendlyName(const QString &name)
319 {
320     QString result = name;
321 
322     // Remove characters that might trip up a build system (especially qmake):
323     int pos = indexOfQmakeUnfriendly(result);
324     while (pos >= 0) {
325         result[pos] = QLatin1Char('_');
326         pos = indexOfQmakeUnfriendly(result, pos);
327     }
328     return fileSystemFriendlyName(result);
329 }
330 
makeWritable(const FilePath & path)331 bool FileUtils::makeWritable(const FilePath &path)
332 {
333     const QString filePath = path.toString();
334     return QFile::setPermissions(filePath, QFile::permissions(filePath) | QFile::WriteUser);
335 }
336 
337 // makes sure that capitalization of directories is canonical on Windows and OS X.
338 // This mimics the logic in QDeclarative_isFileCaseCorrect
normalizePathName(const QString & name)339 QString FileUtils::normalizePathName(const QString &name)
340 {
341 #ifdef Q_OS_WIN
342     const QString nativeSeparatorName(QDir::toNativeSeparators(name));
343     const auto nameC = reinterpret_cast<LPCTSTR>(nativeSeparatorName.utf16()); // MinGW
344     PIDLIST_ABSOLUTE file;
345     HRESULT hr = SHParseDisplayName(nameC, NULL, &file, 0, NULL);
346     if (FAILED(hr))
347         return name;
348     TCHAR buffer[MAX_PATH];
349     const bool success = SHGetPathFromIDList(file, buffer);
350     ILFree(file);
351     return success ? QDir::fromNativeSeparators(QString::fromUtf16(reinterpret_cast<const ushort *>(buffer)))
352                    : name;
353 #elif defined(Q_OS_OSX)
354     return Internal::normalizePathName(name);
355 #else // do not try to handle case-insensitive file systems on Linux
356     return name;
357 #endif
358 }
359 
isRelativePathHelper(const QString & path,OsType osType)360 static bool isRelativePathHelper(const QString &path, OsType osType)
361 {
362     if (path.startsWith('/'))
363         return false;
364     if (osType == OsType::OsTypeWindows) {
365         if (path.startsWith('\\'))
366             return false;
367         // Unlike QFileInfo, this won't accept a relative path with a drive letter.
368         // Such paths result in a royal mess anyway ...
369         if (path.length() >= 3 && path.at(1) == ':' && path.at(0).isLetter()
370                 && (path.at(2) == '/' || path.at(2) == '\\'))
371             return false;
372     }
373     return true;
374 }
375 
isRelativePath(const QString & path)376 bool FileUtils::isRelativePath(const QString &path)
377 {
378     return isRelativePathHelper(path, HostOsInfo::hostOs());
379 }
380 
isRelativePath() const381 bool FilePath::isRelativePath() const
382 {
383     return isRelativePathHelper(m_data, osType());
384 }
385 
resolvePath(const QString & fileName) const386 FilePath FilePath::resolvePath(const QString &fileName) const
387 {
388     if (FileUtils::isAbsolutePath(fileName))
389         return FilePath::fromString(QDir::cleanPath(fileName));
390     FilePath result = *this;
391     result.setPath(QDir::cleanPath(m_data + '/' + fileName));
392     return result;
393 }
394 
cleanPath() const395 FilePath FilePath::cleanPath() const
396 {
397     FilePath result = *this;
398     result.setPath(QDir::cleanPath(result.path()));
399     return result;
400 }
401 
commonPath(const FilePath & oldCommonPath,const FilePath & filePath)402 FilePath FileUtils::commonPath(const FilePath &oldCommonPath, const FilePath &filePath)
403 {
404     FilePath newCommonPath = oldCommonPath;
405     while (!newCommonPath.isEmpty() && !filePath.isChildOf(newCommonPath))
406         newCommonPath = newCommonPath.parentDir();
407     return newCommonPath.canonicalPath();
408 }
409 
homePath()410 FilePath FileUtils::homePath()
411 {
412     return FilePath::fromString(QDir::cleanPath(QDir::homePath()));
413 }
414 
renameFile(const FilePath & srcFilePath,const FilePath & tgtFilePath)415 bool FileUtils::renameFile(const FilePath &srcFilePath, const FilePath &tgtFilePath)
416 {
417     QTC_ASSERT(!srcFilePath.needsDevice(), return false);
418     QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false);
419     return QFile::rename(srcFilePath.path(), tgtFilePath.path());
420 }
421 
422 
423 /*! \class Utils::FilePath
424 
425     \brief The FilePath class is a light-weight convenience class for filenames.
426 
427     On windows filenames are compared case insensitively.
428 */
429 
FilePath()430 FilePath::FilePath()
431 {
432 }
433 
434 /// Constructs a FilePath from \a info
fromFileInfo(const QFileInfo & info)435 FilePath FilePath::fromFileInfo(const QFileInfo &info)
436 {
437     return FilePath::fromString(info.absoluteFilePath());
438 }
439 
440 /// \returns a QFileInfo
toFileInfo() const441 QFileInfo FilePath::toFileInfo() const
442 {
443     return QFileInfo(m_data);
444 }
445 
fromUrl(const QUrl & url)446 FilePath FilePath::fromUrl(const QUrl &url)
447 {
448     FilePath fn;
449     fn.m_scheme = url.scheme();
450     fn.m_host = url.host();
451     fn.m_data = url.path();
452     return fn;
453 }
454 
455 /// \returns a QString for passing on to QString based APIs
toString() const456 QString FilePath::toString() const
457 {
458     if (m_scheme.isEmpty())
459         return m_data;
460     if (m_data.startsWith('/'))
461         return m_scheme + "://" + m_host + m_data;
462     return m_scheme + "://" + m_host + "/./" + m_data;
463 }
464 
toUrl() const465 QUrl FilePath::toUrl() const
466 {
467     QUrl url;
468     url.setScheme(m_scheme);
469     url.setHost(m_host);
470     url.setPath(m_data);
471     return url;
472 }
473 
setDeviceFileHooks(const DeviceFileHooks & hooks)474 void FileUtils::setDeviceFileHooks(const DeviceFileHooks &hooks)
475 {
476     s_deviceHooks = hooks;
477 }
478 
479 /// \returns a QString to display to the user
480 /// Converts the separators to the native format
toUserOutput() const481 QString FilePath::toUserOutput() const
482 {
483     if (m_scheme.isEmpty())
484         return QDir::toNativeSeparators(m_data);
485     return toString();
486 }
487 
fileName() const488 QString FilePath::fileName() const
489 {
490     const QChar slash = QLatin1Char('/');
491     return m_data.mid(m_data.lastIndexOf(slash) + 1);
492 }
493 
fileNameWithPathComponents(int pathComponents) const494 QString FilePath::fileNameWithPathComponents(int pathComponents) const
495 {
496     if (pathComponents < 0)
497         return m_data;
498     const QChar slash = QLatin1Char('/');
499     int i = m_data.lastIndexOf(slash);
500     if (pathComponents == 0 || i == -1)
501         return m_data.mid(i + 1);
502     int component = i + 1;
503     // skip adjacent slashes
504     while (i > 0 && m_data.at(--i) == slash)
505         ;
506     while (i >= 0 && --pathComponents >= 0) {
507         i = m_data.lastIndexOf(slash, i);
508         component = i + 1;
509         while (i > 0 && m_data.at(--i) == slash)
510             ;
511     }
512 
513     // If there are no more slashes before the found one, return the entire string
514     if (i > 0 && m_data.lastIndexOf(slash, i) != -1)
515         return m_data.mid(component);
516     return m_data;
517 }
518 
519 /// \returns the base name of the file without the path.
520 ///
521 /// The base name consists of all characters in the file up to
522 /// (but not including) the first '.' character.
523 
baseName() const524 QString FilePath::baseName() const
525 {
526     const QString &name = fileName();
527     return name.left(name.indexOf('.'));
528 }
529 
530 /// \returns the complete base name of the file without the path.
531 ///
532 /// The complete base name consists of all characters in the file up to
533 /// (but not including) the last '.' character
534 
completeBaseName() const535 QString FilePath::completeBaseName() const
536 {
537     const QString &name = fileName();
538     return name.left(name.lastIndexOf('.'));
539 }
540 
541 /// \returns the suffix (extension) of the file.
542 ///
543 /// The suffix consists of all characters in the file after
544 /// (but not including) the last '.'.
545 
suffix() const546 QString FilePath::suffix() const
547 {
548     const QString &name = fileName();
549     const int index = name.lastIndexOf('.');
550     if (index >= 0)
551         return name.mid(index + 1);
552     return {};
553 }
554 
555 /// \returns the complete suffix (extension) of the file.
556 ///
557 /// The complete suffix consists of all characters in the file after
558 /// (but not including) the first '.'.
559 
completeSuffix() const560 QString FilePath::completeSuffix() const
561 {
562     const QString &name = fileName();
563     const int index = name.indexOf('.');
564     if (index >= 0)
565         return name.mid(index + 1);
566     return {};
567 }
568 
setScheme(const QString & scheme)569 void FilePath::setScheme(const QString &scheme)
570 {
571     QTC_CHECK(!scheme.contains('/'));
572     m_scheme = scheme;
573 }
574 
setHost(const QString & host)575 void FilePath::setHost(const QString &host)
576 {
577     QTC_CHECK(!host.contains('/'));
578     m_host = host;
579 }
580 
581 
582 /// \returns a bool indicating whether a file with this
583 /// FilePath exists.
exists() const584 bool FilePath::exists() const
585 {
586     if (needsDevice()) {
587         QTC_ASSERT(s_deviceHooks.exists, return false);
588         return s_deviceHooks.exists(*this);
589     }
590     return !isEmpty() && QFileInfo::exists(m_data);
591 }
592 
593 /// \returns a bool indicating whether a path is writable.
isWritableDir() const594 bool FilePath::isWritableDir() const
595 {
596     if (needsDevice()) {
597         QTC_ASSERT(s_deviceHooks.isWritableDir, return false);
598         return s_deviceHooks.isWritableDir(*this);
599     }
600     const QFileInfo fi{m_data};
601     return exists() && fi.isDir() && fi.isWritable();
602 }
603 
isWritableFile() const604 bool FilePath::isWritableFile() const
605 {
606     if (needsDevice()) {
607         QTC_ASSERT(s_deviceHooks.isWritableFile, return false);
608         return s_deviceHooks.isWritableFile(*this);
609     }
610     const QFileInfo fi{m_data};
611     return fi.exists() && fi.isWritable() && !fi.isDir();
612 }
613 
ensureWritableDir() const614 bool FilePath::ensureWritableDir() const
615 {
616     if (needsDevice()) {
617         QTC_ASSERT(s_deviceHooks.ensureWritableDir, return false);
618         return s_deviceHooks.ensureWritableDir(*this);
619     }
620     const QFileInfo fi{m_data};
621     if (exists() && fi.isDir() && fi.isWritable())
622         return true;
623     return QDir().mkpath(m_data);
624 }
625 
ensureExistingFile() const626 bool FilePath::ensureExistingFile() const
627 {
628     if (needsDevice()) {
629         QTC_ASSERT(s_deviceHooks.ensureExistingFile, return false);
630         return s_deviceHooks.ensureExistingFile(*this);
631     }
632     QFile f(m_data);
633     if (f.exists())
634         return true;
635     f.open(QFile::WriteOnly);
636     f.close();
637     return f.exists();
638 }
639 
isExecutableFile() const640 bool FilePath::isExecutableFile() const
641 {
642     if (needsDevice()) {
643         QTC_ASSERT(s_deviceHooks.isExecutableFile, return false);
644         return s_deviceHooks.isExecutableFile(*this);
645     }
646     const QFileInfo fi{m_data};
647     return fi.exists() && fi.isExecutable() && !fi.isDir();
648 }
649 
isReadableFile() const650 bool FilePath::isReadableFile() const
651 {
652     if (needsDevice()) {
653         QTC_ASSERT(s_deviceHooks.isReadableFile, return false);
654         return s_deviceHooks.isReadableFile(*this);
655     }
656     const QFileInfo fi{m_data};
657     return fi.exists() && fi.isReadable() && !fi.isDir();
658 }
659 
isReadableDir() const660 bool FilePath::isReadableDir() const
661 {
662     if (needsDevice()) {
663         QTC_ASSERT(s_deviceHooks.isReadableDir, return false);
664         return s_deviceHooks.isReadableDir(*this);
665     }
666     const QFileInfo fi{m_data};
667     return fi.exists() && fi.isReadable() && fi.isDir();
668 }
669 
isFile() const670 bool FilePath::isFile() const
671 {
672     if (needsDevice()) {
673         QTC_ASSERT(s_deviceHooks.isFile, return false);
674         return s_deviceHooks.isFile(*this);
675     }
676     const QFileInfo fi{m_data};
677     return fi.exists() && fi.isFile();
678 }
679 
isDir() const680 bool FilePath::isDir() const
681 {
682     if (needsDevice()) {
683         QTC_ASSERT(s_deviceHooks.isDir, return false);
684         return s_deviceHooks.isDir(*this);
685     }
686     const QFileInfo fi{m_data};
687     return fi.exists() && fi.isDir();
688 }
689 
createDir() const690 bool FilePath::createDir() const
691 {
692     if (needsDevice()) {
693         QTC_ASSERT(s_deviceHooks.createDir, return false);
694         return s_deviceHooks.createDir(*this);
695     }
696     QDir dir(m_data);
697     return dir.mkpath(dir.absolutePath());
698 }
699 
dirEntries(const QStringList & nameFilters,QDir::Filters filters,QDir::SortFlags sort) const700 FilePaths FilePath::dirEntries(const QStringList &nameFilters,
701                                QDir::Filters filters,
702                                QDir::SortFlags sort) const
703 {
704     if (needsDevice()) {
705         QTC_ASSERT(s_deviceHooks.dirEntries, return {});
706         return s_deviceHooks.dirEntries(*this, nameFilters, filters, sort);
707     }
708 
709     const QFileInfoList entryInfoList = QDir(m_data).entryInfoList(nameFilters, filters, sort);
710     return Utils::transform(entryInfoList, &FilePath::fromFileInfo);
711 }
712 
713 
dirEntries(QDir::Filters filters) const714 QList<FilePath> FilePath::dirEntries(QDir::Filters filters) const
715 {
716     return dirEntries({}, filters);
717 }
718 
fileContents(qint64 maxSize,qint64 offset) const719 QByteArray FilePath::fileContents(qint64 maxSize, qint64 offset) const
720 {
721     if (needsDevice()) {
722         QTC_ASSERT(s_deviceHooks.fileContents, return {});
723         return s_deviceHooks.fileContents(*this, maxSize, offset);
724     }
725 
726     const QString path = toString();
727     QFile f(path);
728     if (!f.exists())
729         return {};
730 
731     if (!f.open(QFile::ReadOnly))
732         return {};
733 
734     if (offset != 0)
735         f.seek(offset);
736 
737     if (maxSize != -1)
738         return f.read(maxSize);
739 
740     return f.readAll();
741 }
742 
writeFileContents(const QByteArray & data) const743 bool FilePath::writeFileContents(const QByteArray &data) const
744 {
745     if (needsDevice()) {
746         QTC_ASSERT(s_deviceHooks.writeFileContents, return {});
747         return s_deviceHooks.writeFileContents(*this, data);
748     }
749 
750     QFile file(path());
751     QTC_ASSERT(file.open(QFile::WriteOnly | QFile::Truncate), return false);
752     qint64 res = file.write(data);
753     return res == data.size();
754 }
755 
needsDevice() const756 bool FilePath::needsDevice() const
757 {
758     return !m_scheme.isEmpty();
759 }
760 
761 /// \returns an empty FilePath if this is not a symbolic linl
symLinkTarget() const762 FilePath FilePath::symLinkTarget() const
763 {
764     if (needsDevice()) {
765         QTC_ASSERT(s_deviceHooks.symLinkTarget, return {});
766         return s_deviceHooks.symLinkTarget(*this);
767     }
768     const QFileInfo info(m_data);
769     if (!info.isSymLink())
770         return {};
771     return FilePath::fromString(info.symLinkTarget());
772 }
773 
withExecutableSuffix() const774 FilePath FilePath::withExecutableSuffix() const
775 {
776     FilePath res = *this;
777     res.setPath(OsSpecificAspects::withExecutableSuffix(osType(), m_data));
778     return res;
779 }
780 
781 /// Find the parent directory of a given directory.
782 
783 /// Returns an empty FilePath if the current directory is already
784 /// a root level directory.
785 
786 /// \returns \a FilePath with the last segment removed.
parentDir() const787 FilePath FilePath::parentDir() const
788 {
789     const QString basePath = path();
790     if (basePath.isEmpty())
791         return FilePath();
792 
793     const QDir base(basePath);
794     if (base.isRoot())
795         return FilePath();
796 
797     const QString path = basePath + QLatin1String("/..");
798     const QString parent = QDir::cleanPath(path);
799     QTC_ASSERT(parent != path, return FilePath());
800 
801     FilePath result = *this;
802     result.setPath(parent);
803     return result;
804 }
805 
absolutePath() const806 FilePath FilePath::absolutePath() const
807 {
808     FilePath result = *this;
809     result.m_data = QFileInfo(m_data).absolutePath();
810     return result;
811 }
812 
absoluteFilePath() const813 FilePath FilePath::absoluteFilePath() const
814 {
815     FilePath result = *this;
816     result.m_data = QFileInfo(m_data).absoluteFilePath();
817     return result;
818 }
819 
absoluteFilePath(const FilePath & tail) const820 FilePath FilePath::absoluteFilePath(const FilePath &tail) const
821 {
822     if (isRelativePathHelper(tail.m_data, osType()))
823         return pathAppended(tail.m_data);
824     return tail;
825 }
826 
827 /// Constructs an absolute FilePath from this path which
828 /// is interpreted as being relative to \a anchor.
absoluteFromRelativePath(const FilePath & anchor) const829 FilePath FilePath::absoluteFromRelativePath(const FilePath &anchor) const
830 {
831     QDir anchorDir = QFileInfo(anchor.m_data).absoluteDir();
832     QString absoluteFilePath = QFileInfo(anchorDir, m_data).canonicalFilePath();
833     return FilePath::fromString(absoluteFilePath);
834 }
835 
836 /// Constructs a FilePath from \a filename
837 /// \a filename is not checked for validity.
fromString(const QString & filename)838 FilePath FilePath::fromString(const QString &filename)
839 {
840     FilePath fn;
841     if (filename.startsWith('/')) {
842         fn.m_data = filename;  // fast track: absolute local paths
843     } else {
844         int pos1 = filename.indexOf("://");
845         if (pos1 >= 0) {
846             fn.m_scheme = filename.left(pos1);
847             pos1 += 3;
848             int pos2 = filename.indexOf('/', pos1);
849             if (pos2 == -1) {
850                 fn.m_data = filename.mid(pos1);
851             } else {
852                 fn.m_host = filename.mid(pos1, pos2 - pos1);
853                 fn.m_data = filename.mid(pos2);
854             }
855             if (fn.m_data.startsWith("/./"))
856                 fn.m_data = fn.m_data.mid(3);
857         } else {
858             fn.m_data = filename; // treat everything else as local, too.
859         }
860     }
861     return fn;
862 }
863 
864 /// Constructs a FilePath from \a filePath. The \a defaultExtension is appended
865 /// to \a filename if that does not have an extension already.
866 /// \a filePath is not checked for validity.
fromStringWithExtension(const QString & filepath,const QString & defaultExtension)867 FilePath FilePath::fromStringWithExtension(const QString &filepath, const QString &defaultExtension)
868 {
869     if (filepath.isEmpty() || defaultExtension.isEmpty())
870         return FilePath::fromString(filepath);
871 
872     QString rc = filepath;
873     QFileInfo fi(filepath);
874     // Add extension unless user specified something else
875     const QChar dot = QLatin1Char('.');
876     if (!fi.fileName().contains(dot)) {
877         if (!defaultExtension.startsWith(dot))
878             rc += dot;
879         rc += defaultExtension;
880     }
881     return FilePath::fromString(rc);
882 }
883 
884 /// Constructs a FilePath from \a filePath
885 /// \a filePath is only passed through QDir::fromNativeSeparators
fromUserInput(const QString & filePath)886 FilePath FilePath::fromUserInput(const QString &filePath)
887 {
888     QString clean = QDir::fromNativeSeparators(filePath);
889     if (clean.startsWith(QLatin1String("~/")))
890         return FileUtils::homePath().pathAppended(clean.mid(2));
891     return FilePath::fromString(clean);
892 }
893 
894 /// Constructs a FilePath from \a filePath, which is encoded as UTF-8.
895 /// \a filePath is not checked for validity.
fromUtf8(const char * filename,int filenameSize)896 FilePath FilePath::fromUtf8(const char *filename, int filenameSize)
897 {
898     return FilePath::fromString(QString::fromUtf8(filename, filenameSize));
899 }
900 
fromVariant(const QVariant & variant)901 FilePath FilePath::fromVariant(const QVariant &variant)
902 {
903     if (variant.type() == QVariant::Url)
904         return FilePath::fromUrl(variant.toUrl());
905     return FilePath::fromString(variant.toString());
906 }
907 
toVariant() const908 QVariant FilePath::toVariant() const
909 {
910     return toString();
911 }
912 
toDir() const913 QDir FilePath::toDir() const
914 {
915     return QDir(m_data);
916 }
917 
operator ==(const FilePath & other) const918 bool FilePath::operator==(const FilePath &other) const
919 {
920     return QString::compare(m_data, other.m_data, caseSensitivity()) == 0
921         && m_host == other.m_host
922         && m_scheme == other.m_scheme;
923 }
924 
operator !=(const FilePath & other) const925 bool FilePath::operator!=(const FilePath &other) const
926 {
927     return !(*this == other);
928 }
929 
operator <(const FilePath & other) const930 bool FilePath::operator<(const FilePath &other) const
931 {
932     const int cmp = QString::compare(m_data, other.m_data, caseSensitivity());
933     if (cmp != 0)
934         return cmp < 0;
935     if (m_host != other.m_host)
936         return m_host < other.m_host;
937     return m_scheme < other.m_scheme;
938 }
939 
operator <=(const FilePath & other) const940 bool FilePath::operator<=(const FilePath &other) const
941 {
942     return !(other < *this);
943 }
944 
operator >(const FilePath & other) const945 bool FilePath::operator>(const FilePath &other) const
946 {
947     return other < *this;
948 }
949 
operator >=(const FilePath & other) const950 bool FilePath::operator>=(const FilePath &other) const
951 {
952     return !(*this < other);
953 }
954 
operator +(const QString & s) const955 FilePath FilePath::operator+(const QString &s) const
956 {
957     FilePath res = *this;
958     res.m_data += s;
959     return res;
960 }
961 
962 /// \returns whether FilePath is a child of \a s
isChildOf(const FilePath & s) const963 bool FilePath::isChildOf(const FilePath &s) const
964 {
965     if (s.isEmpty())
966         return false;
967     if (!m_data.startsWith(s.m_data, caseSensitivity()))
968         return false;
969     if (m_data.size() <= s.m_data.size())
970         return false;
971     // s is root, '/' was already tested in startsWith
972     if (s.m_data.endsWith(QLatin1Char('/')))
973         return true;
974     // s is a directory, next character should be '/' (/tmpdir is NOT a child of /tmp)
975     return m_data.at(s.m_data.size()) == QLatin1Char('/');
976 }
977 
978 /// \overload
isChildOf(const QDir & dir) const979 bool FilePath::isChildOf(const QDir &dir) const
980 {
981     return isChildOf(FilePath::fromString(dir.absolutePath()));
982 }
983 
984 /// \returns whether FilePath startsWith \a s
startsWith(const QString & s) const985 bool FilePath::startsWith(const QString &s) const
986 {
987     return m_data.startsWith(s, caseSensitivity());
988 }
989 
990 /// \returns whether FilePath endsWith \a s
endsWith(const QString & s) const991 bool FilePath::endsWith(const QString &s) const
992 {
993     return m_data.endsWith(s, caseSensitivity());
994 }
995 
996 /// \returns whether FilePath starts with a drive letter
997 /// \note defaults to \c false if it is a non-Windows host or represents a path on device
startsWithDriveLetter() const998 bool FilePath::startsWithDriveLetter() const
999 {
1000     if (needsDevice() || !HostOsInfo::isWindowsHost())
1001         return false;
1002     return m_data.length() >= 2 && m_data.at(0).isLetter() && m_data.at(1) == ':';
1003 }
1004 
1005 /// \returns the relativeChildPath of FilePath to parent if FilePath is a child of parent
1006 /// \note returns a empty FilePath if FilePath is not a child of parent
1007 /// That is, this never returns a path starting with "../"
relativeChildPath(const FilePath & parent) const1008 FilePath FilePath::relativeChildPath(const FilePath &parent) const
1009 {
1010     FilePath res;
1011     if (isChildOf(parent))
1012         res.m_data = m_data.mid(parent.m_data.size() + 1, -1);
1013     return res;
1014 }
1015 
1016 /// \returns the relativePath of FilePath to given \a anchor.
1017 /// Both, FilePath and anchor may be files or directories.
1018 /// Example usage:
1019 ///
1020 /// \code
1021 ///     FilePath filePath("/foo/b/ar/file.txt");
1022 ///     FilePath relativePath = filePath.relativePath("/foo/c");
1023 ///     qDebug() << relativePath
1024 /// \endcode
1025 ///
1026 /// The debug output will be "../b/ar/file.txt".
1027 ///
relativePath(const FilePath & anchor) const1028 FilePath FilePath::relativePath(const FilePath &anchor) const
1029 {
1030     const QFileInfo fileInfo(m_data);
1031     QString absolutePath;
1032     QString filename;
1033     if (fileInfo.isFile()) {
1034         absolutePath = fileInfo.absolutePath();
1035         filename = fileInfo.fileName();
1036     } else if (fileInfo.isDir()) {
1037         absolutePath = fileInfo.absoluteFilePath();
1038     } else {
1039         return {};
1040     }
1041     const QFileInfo anchorInfo(anchor.m_data);
1042     QString absoluteAnchorPath;
1043     if (anchorInfo.isFile())
1044         absoluteAnchorPath = anchorInfo.absolutePath();
1045     else if (anchorInfo.isDir())
1046         absoluteAnchorPath = anchorInfo.absoluteFilePath();
1047     else
1048         return {};
1049     QString relativeFilePath = calcRelativePath(absolutePath, absoluteAnchorPath);
1050     if (!filename.isEmpty()) {
1051         if (!relativeFilePath.isEmpty())
1052             relativeFilePath += '/';
1053         relativeFilePath += filename;
1054     }
1055     return FilePath::fromString(relativeFilePath);
1056 }
1057 
1058 /// \returns the relativePath of \a absolutePath to given \a absoluteAnchorPath.
1059 /// Both paths must be an absolute path to a directory. Example usage:
1060 ///
1061 /// \code
1062 ///     qDebug() << FilePath::calcRelativePath("/foo/b/ar", "/foo/c");
1063 /// \endcode
1064 ///
1065 /// The debug output will be "../b/ar".
1066 ///
1067 /// \see FilePath::relativePath
1068 ///
calcRelativePath(const QString & absolutePath,const QString & absoluteAnchorPath)1069 QString FilePath::calcRelativePath(const QString &absolutePath, const QString &absoluteAnchorPath)
1070 {
1071     if (absolutePath.isEmpty() || absoluteAnchorPath.isEmpty())
1072         return QString();
1073     // TODO using split() instead of parsing the strings by char index is slow
1074     // and needs more memory (but the easiest implementation for now)
1075     const QStringList splits1 = absolutePath.split('/');
1076     const QStringList splits2 = absoluteAnchorPath.split('/');
1077     int i = 0;
1078     while (i < splits1.count() && i < splits2.count() && splits1.at(i) == splits2.at(i))
1079         ++i;
1080     QString relativePath;
1081     int j = i;
1082     bool addslash = false;
1083     while (j < splits2.count()) {
1084         if (!splits2.at(j).isEmpty()) {
1085             if (addslash)
1086                 relativePath += '/';
1087             relativePath += "..";
1088             addslash = true;
1089         }
1090         ++j;
1091     }
1092     while (i < splits1.count()) {
1093         if (!splits1.at(i).isEmpty()) {
1094             if (addslash)
1095                 relativePath += '/';
1096             relativePath += splits1.at(i);
1097             addslash = true;
1098         }
1099         ++i;
1100     }
1101     return relativePath;
1102 }
1103 
1104 /*!
1105     Returns a path corresponding to the current object on the
1106     same device as \a deviceTemplate.
1107 
1108     Example usage:
1109     \code
1110         localDir = FilePath::fromString("/tmp/workingdir");
1111         executable = FilePath::fromUrl("docker://123/bin/ls")
1112         realDir = localDir.onDevice(executable)
1113         assert(realDir == FilePath::fromUrl("docker://123/tmp/workingdir"))
1114     \endcode
1115 */
onDevice(const FilePath & deviceTemplate) const1116 FilePath FilePath::onDevice(const FilePath &deviceTemplate) const
1117 {
1118     FilePath res;
1119     res.m_data = m_data;
1120     res.m_host = deviceTemplate.m_host;
1121     res.m_scheme = deviceTemplate.m_scheme;
1122     return res;
1123 }
1124 
1125 /*!
1126     Returns a FilePath with local path \a newPath on the same device
1127     as the current object.
1128 
1129     Example usage:
1130     \code
1131         devicePath = FilePath::fromString("docker://123/tmp");
1132         newPath = devicePath.withNewPath("/bin/ls");
1133         assert(realDir == FilePath::fromUrl("docker://123/bin/ls"))
1134     \endcode
1135 */
withNewPath(const QString & newPath) const1136 FilePath FilePath::withNewPath(const QString &newPath) const
1137 {
1138     FilePath res;
1139     res.m_data = newPath;
1140     res.m_host = m_host;
1141     res.m_scheme = m_scheme;
1142     return res;
1143 }
1144 
1145 /*!
1146     Searched a binary corresponding to this object in the PATH of
1147     the device implied by this object's scheme and host.
1148 
1149     Example usage:
1150     \code
1151         binary = FilePath::fromUrl("docker://123/./make);
1152         fullPath = binary.searchOnDevice();
1153         assert(fullPath == FilePath::fromUrl("docker://123/usr/bin/make"))
1154     \endcode
1155 */
searchOnDevice(const FilePaths & dirs) const1156 FilePath FilePath::searchOnDevice(const FilePaths &dirs) const
1157 {
1158     if (needsDevice()) {
1159         QTC_ASSERT(s_deviceHooks.searchInPath, return {});
1160         return s_deviceHooks.searchInPath(*this, dirs);
1161     }
1162     return Environment::systemEnvironment().searchInPath(path(), dirs);
1163 }
1164 
deviceEnvironment() const1165 Environment FilePath::deviceEnvironment() const
1166 {
1167     if (needsDevice()) {
1168         QTC_ASSERT(s_deviceHooks.environment, return {});
1169         return s_deviceHooks.environment(*this);
1170     }
1171     return Environment::systemEnvironment();
1172 }
1173 
formatFilePaths(const QList<FilePath> & files,const QString & separator)1174 QString FilePath::formatFilePaths(const QList<FilePath> &files, const QString &separator)
1175 {
1176     const QStringList nativeFiles = Utils::transform(files, &FilePath::toUserOutput);
1177     return nativeFiles.join(separator);
1178 }
1179 
removeDuplicates(QList<FilePath> & files)1180 void FilePath::removeDuplicates(QList<FilePath> &files)
1181 {
1182     // FIXME: Improve.
1183     QStringList list = Utils::transform<QStringList>(files, &FilePath::toString);
1184     list.removeDuplicates();
1185     files = Utils::transform(list, &FilePath::fromString);
1186 }
1187 
sort(QList<FilePath> & files)1188 void FilePath::sort(QList<FilePath> &files)
1189 {
1190     // FIXME: Improve.
1191     QStringList list = Utils::transform<QStringList>(files, &FilePath::toString);
1192     list.sort();
1193     files = Utils::transform(list, &FilePath::fromString);
1194 }
1195 
pathAppended(const QString & path) const1196 FilePath FilePath::pathAppended(const QString &path) const
1197 {
1198     FilePath fn = *this;
1199     if (path.isEmpty())
1200         return fn;
1201 
1202     if (fn.m_data.isEmpty()) {
1203         fn.m_data = path;
1204         return fn;
1205     }
1206 
1207     if (fn.m_data.endsWith('/')) {
1208         if (path.startsWith('/'))
1209             fn.m_data.append(path.mid(1));
1210         else
1211             fn.m_data.append(path);
1212     } else {
1213         if (path.startsWith('/'))
1214             fn.m_data.append(path);
1215         else
1216             fn.m_data.append('/').append(path);
1217     }
1218 
1219     return fn;
1220 }
1221 
stringAppended(const QString & str) const1222 FilePath FilePath::stringAppended(const QString &str) const
1223 {
1224     FilePath fn = *this;
1225     fn.m_data.append(str);
1226     return fn;
1227 }
1228 
hash(uint seed) const1229 uint FilePath::hash(uint seed) const
1230 {
1231     if (Utils::HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive)
1232         return qHash(m_data.toUpper(), seed);
1233     return qHash(m_data, seed);
1234 }
1235 
lastModified() const1236 QDateTime FilePath::lastModified() const
1237 {
1238     if (needsDevice()) {
1239         QTC_ASSERT(s_deviceHooks.lastModified, return {});
1240         return s_deviceHooks.lastModified(*this);
1241     }
1242     return toFileInfo().lastModified();
1243 }
1244 
permissions() const1245 QFile::Permissions FilePath::permissions() const
1246 {
1247     if (needsDevice()) {
1248         QTC_ASSERT(s_deviceHooks.permissions, return {});
1249         return s_deviceHooks.permissions(*this);
1250     }
1251     return toFileInfo().permissions();
1252 }
1253 
osType() const1254 OsType FilePath::osType() const
1255 {
1256     if (needsDevice()) {
1257         QTC_ASSERT(s_deviceHooks.osType, return {});
1258         return s_deviceHooks.osType(*this);
1259     }
1260     return HostOsInfo::hostOs();
1261 }
1262 
removeFile() const1263 bool FilePath::removeFile() const
1264 {
1265     if (needsDevice()) {
1266         QTC_ASSERT(s_deviceHooks.removeFile, return false);
1267         return s_deviceHooks.removeFile(*this);
1268     }
1269     return QFile::remove(path());
1270 }
1271 
1272 /*!
1273   Removes the directory this filePath refers too and its subdirectories recursively.
1274 
1275   \note The \a error parameter is optional.
1276 
1277   Returns whether the operation succeeded.
1278 */
removeRecursively(QString * error) const1279 bool FilePath::removeRecursively(QString *error) const
1280 {
1281     if (needsDevice()) {
1282         QTC_ASSERT(s_deviceHooks.removeRecursively, return false);
1283         return s_deviceHooks.removeRecursively(*this);
1284     }
1285     return removeRecursivelyLocal(*this, error);
1286 }
1287 
copyFile(const FilePath & target) const1288 bool FilePath::copyFile(const FilePath &target) const
1289 {
1290     if (needsDevice()) {
1291         QTC_ASSERT(s_deviceHooks.copyFile, return false);
1292         return s_deviceHooks.copyFile(*this, target);
1293     }
1294     return QFile::copy(path(), target.path());
1295 }
1296 
renameFile(const FilePath & target) const1297 bool FilePath::renameFile(const FilePath &target) const
1298 {
1299     if (needsDevice()) {
1300         QTC_ASSERT(s_deviceHooks.renameFile, return false);
1301         return s_deviceHooks.renameFile(*this, target);
1302     }
1303     return QFile::rename(path(), target.path());
1304 }
1305 
operator <<(QTextStream & s,const FilePath & fn)1306 QTextStream &operator<<(QTextStream &s, const FilePath &fn)
1307 {
1308     return s << fn.toString();
1309 }
1310 
1311 } // namespace Utils
1312 
1313 std::hash<Utils::FilePath>::result_type
operator ()(const std::hash<Utils::FilePath>::argument_type & fn) const1314     std::hash<Utils::FilePath>::operator()(const std::hash<Utils::FilePath>::argument_type &fn) const
1315 {
1316     if (fn.caseSensitivity() == Qt::CaseInsensitive)
1317         return hash<string>()(fn.toString().toUpper().toStdString());
1318     return hash<string>()(fn.toString().toStdString());
1319 }
1320