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