// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "fileutils.h" #include "savefile.h" #include "algorithm.h" #include "commandline.h" #include "qtcassert.h" #include "hostosinfo.h" #include "fsengine/fileiconprovider.h" #include "fsengine/fsengine.h" #include #include #include #include #include #include #include #include #include #ifdef QT_GUI_LIB #include #include #include #endif #ifdef Q_OS_WIN #ifdef QTCREATOR_PCH_H #define CALLBACK WINAPI #endif #include #include #endif #ifdef Q_OS_OSX #include "fileutils_mac.h" #endif namespace Utils { // FileReader QByteArray FileReader::fetchQrc(const QString &fileName) { QTC_ASSERT(fileName.startsWith(':'), return QByteArray()); QFile file(fileName); bool ok = file.open(QIODevice::ReadOnly); QTC_ASSERT(ok, qWarning() << fileName << "not there!"; return QByteArray()); return file.readAll(); } bool FileReader::fetch(const FilePath &filePath, QIODevice::OpenMode mode) { QTC_ASSERT(!(mode & ~(QIODevice::ReadOnly | QIODevice::Text)), return false); if (filePath.needsDevice()) { const std::optional contents = filePath.fileContents(); if (!contents) { m_errorString = tr("Cannot read %1").arg(filePath.toUserOutput()); return false; } m_data = *contents; return true; } QFile file(filePath.toString()); if (!file.open(QIODevice::ReadOnly | mode)) { m_errorString = tr("Cannot open %1 for reading: %2").arg( filePath.toUserOutput(), file.errorString()); return false; } m_data = file.readAll(); if (file.error() != QFile::NoError) { m_errorString = tr("Cannot read %1: %2").arg( filePath.toUserOutput(), file.errorString()); return false; } return true; } bool FileReader::fetch(const FilePath &filePath, QIODevice::OpenMode mode, QString *errorString) { if (fetch(filePath, mode)) return true; if (errorString) *errorString = m_errorString; return false; } #ifdef QT_GUI_LIB bool FileReader::fetch(const FilePath &filePath, QIODevice::OpenMode mode, QWidget *parent) { if (fetch(filePath, mode)) return true; if (parent) QMessageBox::critical(parent, tr("File Error"), m_errorString); return false; } #endif // QT_GUI_LIB // FileSaver FileSaverBase::FileSaverBase() = default; FileSaverBase::~FileSaverBase() = default; bool FileSaverBase::finalize() { m_file->close(); setResult(m_file->error() == QFile::NoError); m_file.reset(); return !m_hasError; } bool FileSaverBase::finalize(QString *errStr) { if (finalize()) return true; if (errStr) *errStr = errorString(); return false; } #ifdef QT_GUI_LIB bool FileSaverBase::finalize(QWidget *parent) { if (finalize()) return true; QMessageBox::critical(parent, tr("File Error"), errorString()); return false; } #endif // QT_GUI_LIB bool FileSaverBase::write(const char *data, int len) { if (m_hasError) return false; return setResult(m_file->write(data, len) == len); } bool FileSaverBase::write(const QByteArray &bytes) { if (m_hasError) return false; return setResult(m_file->write(bytes) == bytes.count()); } bool FileSaverBase::setResult(bool ok) { if (!ok && !m_hasError) { if (!m_file->errorString().isEmpty()) { m_errorString = tr("Cannot write file %1: %2") .arg(m_filePath.toUserOutput(), m_file->errorString()); } else { m_errorString = tr("Cannot write file %1. Disk full?").arg(m_filePath.toUserOutput()); } m_hasError = true; } return ok; } bool FileSaverBase::setResult(QTextStream *stream) { stream->flush(); return setResult(stream->status() == QTextStream::Ok); } bool FileSaverBase::setResult(QDataStream *stream) { return setResult(stream->status() == QDataStream::Ok); } bool FileSaverBase::setResult(QXmlStreamWriter *stream) { return setResult(!stream->hasError()); } // FileSaver FileSaver::FileSaver(const FilePath &filePath, QIODevice::OpenMode mode) { m_filePath = filePath; // Workaround an assert in Qt -- and provide a useful error message, too: if (m_filePath.osType() == OsType::OsTypeWindows) { // Taken from: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx static const QStringList reservedNames = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}; const QString fn = filePath.baseName().toUpper(); if (reservedNames.contains(fn)) { m_errorString = tr("%1: Is a reserved filename on Windows. Cannot save.") .arg(filePath.toUserOutput()); m_hasError = true; return; } } if (filePath.needsDevice()) { // Write to a local temporary file first. Actual saving to the selected location // is done via m_filePath.writeFileContents() in finalize() m_isSafe = false; auto tf = new QTemporaryFile(QDir::tempPath() + "/remotefilesaver-XXXXXX"); tf->setAutoRemove(false); m_file.reset(tf); } else if (mode & (QIODevice::ReadOnly | QIODevice::Append)) { m_file.reset(new QFile{filePath.path()}); m_isSafe = false; } else { m_file.reset(new SaveFile{filePath.path()}); m_isSafe = true; } if (!m_file->open(QIODevice::WriteOnly | mode)) { QString err = filePath.exists() ? tr("Cannot overwrite file %1: %2") : tr("Cannot create file %1: %2"); m_errorString = err.arg(filePath.toUserOutput(), m_file->errorString()); m_hasError = true; } } bool FileSaver::finalize() { if (m_filePath.needsDevice()) { m_file->close(); m_file->open(QIODevice::ReadOnly); const QByteArray data = m_file->readAll(); const bool res = m_filePath.writeFileContents(data); m_file->remove(); m_file.reset(); return res; } if (!m_isSafe) return FileSaverBase::finalize(); auto sf = static_cast(m_file.get()); if (m_hasError) { if (sf->isOpen()) sf->rollback(); } else { setResult(sf->commit()); } m_file.reset(); return !m_hasError; } TempFileSaver::TempFileSaver(const QString &templ) { m_file.reset(new QTemporaryFile{}); auto tempFile = static_cast(m_file.get()); if (!templ.isEmpty()) tempFile->setFileTemplate(templ); tempFile->setAutoRemove(false); if (!tempFile->open()) { m_errorString = tr("Cannot create temporary file in %1: %2").arg( QDir::toNativeSeparators(QFileInfo(tempFile->fileTemplate()).absolutePath()), tempFile->errorString()); m_hasError = true; } m_filePath = FilePath::fromString(tempFile->fileName()); } TempFileSaver::~TempFileSaver() { m_file.reset(); if (m_autoRemove) QFile::remove(m_filePath.toString()); } /*! \class Utils::FileUtils \brief The FileUtils class contains file and directory related convenience functions. */ #ifdef QT_GUI_LIB FileUtils::CopyAskingForOverwrite::CopyAskingForOverwrite(QWidget *dialogParent, const std::function &postOperation) : m_parent(dialogParent) , m_postOperation(postOperation) {} bool FileUtils::CopyAskingForOverwrite::operator()(const FilePath &src, const FilePath &dest, QString *error) { bool copyFile = true; if (dest.exists()) { if (m_skipAll) copyFile = false; else if (!m_overwriteAll) { const int res = QMessageBox::question( m_parent, QCoreApplication::translate("Utils::FileUtils", "Overwrite File?"), QCoreApplication::translate("Utils::FileUtils", "Overwrite existing file \"%1\"?") .arg(dest.toUserOutput()), QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel); if (res == QMessageBox::Cancel) { return false; } else if (res == QMessageBox::No) { copyFile = false; } else if (res == QMessageBox::NoToAll) { m_skipAll = true; copyFile = false; } else if (res == QMessageBox::YesToAll) { m_overwriteAll = true; } if (copyFile) dest.removeFile(); } } if (copyFile) { dest.parentDir().ensureWritableDir(); if (!src.copyFile(dest)) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Could not copy file \"%1\" to \"%2\".") .arg(src.toUserOutput(), dest.toUserOutput()); } return false; } if (m_postOperation) m_postOperation(dest); } m_files.append(dest.absoluteFilePath()); return true; } FilePaths FileUtils::CopyAskingForOverwrite::files() const { return m_files; } #endif // QT_GUI_LIB // Copied from qfilesystemengine_win.cpp #ifdef Q_OS_WIN // File ID for Windows up to version 7. static inline QByteArray fileIdWin7(HANDLE handle) { BY_HANDLE_FILE_INFORMATION info; if (GetFileInformationByHandle(handle, &info)) { char buffer[sizeof "01234567:0123456701234567\0"]; qsnprintf(buffer, sizeof(buffer), "%lx:%08lx%08lx", info.dwVolumeSerialNumber, info.nFileIndexHigh, info.nFileIndexLow); return QByteArray(buffer); } return QByteArray(); } // File ID for Windows starting from version 8. static QByteArray fileIdWin8(HANDLE handle) { QByteArray result; FILE_ID_INFO infoEx; if (GetFileInformationByHandleEx(handle, static_cast(18), // FileIdInfo in Windows 8 &infoEx, sizeof(FILE_ID_INFO))) { result = QByteArray::number(infoEx.VolumeSerialNumber, 16); result += ':'; // Note: MinGW-64's definition of FILE_ID_128 differs from the MSVC one. result += QByteArray(reinterpret_cast(&infoEx.FileId), int(sizeof(infoEx.FileId))).toHex(); } return result; } static QByteArray fileIdWin(HANDLE fHandle) { return QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8 ? fileIdWin8(HANDLE(fHandle)) : fileIdWin7(HANDLE(fHandle)); } #endif FilePath FileUtils::commonPath(const FilePaths &paths) { if (paths.isEmpty()) return {}; if (paths.count() == 1) return paths.constFirst(); const FilePath &first = paths.constFirst(); const FilePaths others = paths.mid(1); FilePath result; // Common scheme const QStringView commonScheme = first.scheme(); auto sameScheme = [&commonScheme] (const FilePath &fp) { return commonScheme == fp.scheme(); }; if (!allOf(others, sameScheme)) return result; result.setParts(commonScheme, {}, {}); // Common host const QStringView commonHost = first.host(); auto sameHost = [&commonHost] (const FilePath &fp) { return commonHost == fp.host(); }; if (!allOf(others, sameHost)) return result; result.setParts(commonScheme, commonHost, {}); // Common path QString commonPath; auto sameBasePath = [&commonPath] (const FilePath &fp) { return QString(fp.path() + '/').startsWith(commonPath); }; const QStringList pathSegments = first.path().split('/'); for (const QString &segment : pathSegments) { commonPath += segment + '/'; if (!allOf(others, sameBasePath)) return result; result.setParts(commonScheme, commonHost, commonPath.chopped(1)); } return result; } QByteArray FileUtils::fileId(const FilePath &fileName) { QByteArray result; #ifdef Q_OS_WIN const HANDLE handle = CreateFile((wchar_t*)fileName.toUserOutput().utf16(), 0, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if (handle != INVALID_HANDLE_VALUE) { result = fileIdWin(handle); CloseHandle(handle); } #else // Copied from qfilesystemengine_unix.cpp if (Q_UNLIKELY(fileName.isEmpty())) return result; QT_STATBUF statResult; if (QT_STAT(fileName.toString().toLocal8Bit().constData(), &statResult)) return result; result = QByteArray::number(quint64(statResult.st_dev), 16); result += ':'; result += QByteArray::number(quint64(statResult.st_ino)); #endif return result; } #ifdef Q_OS_WIN template <> void withNtfsPermissions(const std::function &task) { qt_ntfs_permission_lookup++; task(); qt_ntfs_permission_lookup--; } #endif #ifdef QT_WIDGETS_LIB static std::function s_dialogParentGetter; void FileUtils::setDialogParentGetter(const std::function &getter) { s_dialogParentGetter = getter; } static QWidget *dialogParent(QWidget *parent) { return parent ? parent : s_dialogParentGetter ? s_dialogParentGetter() : nullptr; } FilePath qUrlToFilePath(const QUrl &url) { if (url.isLocalFile()) return FilePath::fromString(url.toLocalFile()); return FilePath::fromUrl(url); } QUrl filePathToQUrl(const FilePath &filePath) { return QUrl::fromLocalFile(filePath.toFSPathString()); } void prepareNonNativeDialog(QFileDialog &dialog) { // Checking QFileDialog::itemDelegate() seems to be the only way to determine // whether the dialog is native or not. if (dialog.itemDelegate()) { QList sideBarUrls; for (const FilePath &path : FSEngine::registeredDeviceRoots()) { if (path.exists()) sideBarUrls.append(filePathToQUrl(path)); } dialog.setSidebarUrls(sideBarUrls); dialog.setIconProvider(Utils::FileIconProvider::iconProvider()); } } FilePaths getFilePaths(QWidget *parent, const QString &caption, const FilePath &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, const QStringList &supportedSchemes, const bool forceNonNativeDialog, QFileDialog::FileMode fileMode, QFileDialog::AcceptMode acceptMode) { QFileDialog dialog(parent, caption, dir.toFSPathString(), filter); dialog.setFileMode(fileMode); if (forceNonNativeDialog) options.setFlag(QFileDialog::DontUseNativeDialog); dialog.setOptions(options); prepareNonNativeDialog(dialog); dialog.setSupportedSchemes(supportedSchemes); dialog.setAcceptMode(acceptMode); if (selectedFilter && !selectedFilter->isEmpty()) dialog.selectNameFilter(*selectedFilter); if (dialog.exec() == QDialog::Accepted) { if (selectedFilter) *selectedFilter = dialog.selectedNameFilter(); return Utils::transform(dialog.selectedUrls(), &qUrlToFilePath); } return {}; } FilePath firstOrEmpty(const FilePaths &filePaths) { return filePaths.isEmpty() ? FilePath() : filePaths.first(); } FilePath FileUtils::getOpenFilePath(QWidget *parent, const QString &caption, const FilePath &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, bool fromDeviceIfShiftIsPressed) { bool forceNonNativeDialog = dir.needsDevice(); #ifdef QT_GUI_LIB if (fromDeviceIfShiftIsPressed && qApp->queryKeyboardModifiers() & Qt::ShiftModifier) { forceNonNativeDialog = true; } #endif const QStringList schemes = QStringList(QStringLiteral("file")); return firstOrEmpty(getFilePaths(dialogParent(parent), caption, dir, filter, selectedFilter, options, schemes, forceNonNativeDialog, QFileDialog::ExistingFile, QFileDialog::AcceptOpen)); } FilePath FileUtils::getSaveFilePath(QWidget *parent, const QString &caption, const FilePath &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { bool forceNonNativeDialog = dir.needsDevice(); const QStringList schemes = QStringList(QStringLiteral("file")); return firstOrEmpty(getFilePaths(dialogParent(parent), caption, dir, filter, selectedFilter, options, schemes, forceNonNativeDialog, QFileDialog::AnyFile, QFileDialog::AcceptSave)); } FilePath FileUtils::getExistingDirectory(QWidget *parent, const QString &caption, const FilePath &dir, QFileDialog::Options options) { bool forceNonNativeDialog = dir.needsDevice(); const QStringList schemes = QStringList(QStringLiteral("file")); return firstOrEmpty(getFilePaths(dialogParent(parent), caption, dir, {}, nullptr, options, schemes, forceNonNativeDialog, QFileDialog::Directory, QFileDialog::AcceptOpen)); } FilePaths FileUtils::getOpenFilePaths(QWidget *parent, const QString &caption, const FilePath &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { bool forceNonNativeDialog = dir.needsDevice(); const QStringList schemes = QStringList(QStringLiteral("file")); return getFilePaths(dialogParent(parent), caption, dir, filter, selectedFilter, options, schemes, forceNonNativeDialog, QFileDialog::ExistingFiles, QFileDialog::AcceptOpen); } #endif // QT_WIDGETS_LIB // Used on 'ls' output on unix-like systems. static void iterateLsOutput(const FilePath &base, const QStringList &entries, const FileFilter &filter, const FilePath::IterateDirCallback &callBack) { const QList nameRegexps = transform(filter.nameFilters, [](const QString &filter) { QRegularExpression re; re.setPattern(QRegularExpression::wildcardToRegularExpression(filter)); QTC_CHECK(re.isValid()); return re; }); const auto nameMatches = [&nameRegexps](const QString &fileName) { for (const QRegularExpression &re : nameRegexps) { const QRegularExpressionMatch match = re.match(fileName); if (match.hasMatch()) return true; } return nameRegexps.isEmpty(); }; // FIXME: Handle filters. For now bark on unsupported options. QTC_CHECK(filter.fileFilters == QDir::NoFilter); for (const QString &entry : entries) { if (!nameMatches(entry)) continue; const FilePath current = base.pathAppended(entry); bool res = false; if (callBack.index() == 0) res = std::get<0>(callBack)(current); else res = std::get<1>(callBack)(current, current.filePathInfo()); if (!res) break; } } FilePathInfo::FileFlags fileInfoFlagsfromStatRawModeHex(const QString &hexString) { bool ok = false; uint mode = hexString.toUInt(&ok, 16); QTC_ASSERT(ok, return {}); FilePathInfo::FileFlags result; if (mode & 0x100) // S_IRUSR result |= FilePathInfo::ReadOwnerPerm; if (mode & 0x80) // S_IWUSR result |= FilePathInfo::WriteOwnerPerm; if (mode & 0x40) // S_IXUSR result |= FilePathInfo::ExeOwnerPerm; if (mode & 0x20) // S_IRGRP result |= FilePathInfo::ReadGroupPerm; if (mode & 0x10) // S_IWGRP result |= FilePathInfo::WriteGroupPerm; if (mode & 0x8) // S_IXGRP result |= FilePathInfo::ExeGroupPerm; if (mode & 0x4) // S_IROTH result |= FilePathInfo::ReadOtherPerm; if (mode & 0x2) // S_IWOTH result |= FilePathInfo::WriteOtherPerm; if (mode & 0x1) // S_IXOTH result |= FilePathInfo::ExeOtherPerm; if (mode & 0xa000) // S_IFLNK result |= FilePathInfo::LinkType; if (mode & 0x8000) // S_IFREG result |= FilePathInfo::FileType; if (mode & 0x4000) // S_IFDIR result |= FilePathInfo::DirectoryType; if (mode & 0x6000) // S_IFBLK result |= FilePathInfo::LocalDiskFlag; if (result != 0) // There is no Exist flag, but if anything was set before, it must exist. result |= FilePathInfo::ExistsFlag; return result; } FilePathInfo FileUtils::filePathInfoFromTriple(const QString &infos) { const QStringList parts = infos.split(' ', Qt::SkipEmptyParts); if (parts.size() != 3) return {}; FilePathInfo::FileFlags flags = fileInfoFlagsfromStatRawModeHex(parts[0]); const QDateTime dt = QDateTime::fromSecsSinceEpoch(parts[1].toLongLong(), Qt::UTC); qint64 size = parts[2].toLongLong(); return {size, flags, dt}; } static bool iterateWithFindHelper( const FilePath &filePath, const FileFilter &filter, const std::function &runInShell, const std::function callBack, const QString &extraArguments) { QTC_CHECK(filePath.isAbsolutePath()); const QStringList arguments = filter.asFindArguments(filePath.path()); CommandLine cmdLine{"find", arguments}; if (!extraArguments.isEmpty()) cmdLine.addArgs(extraArguments, CommandLine::Raw); const RunResult result = runInShell(cmdLine); const QString out = QString::fromUtf8(result.stdOut); if (result.exitCode != 0) { // Find returns non-zero exit code for any error it encounters, even if it finds some files. if (!out.startsWith('"' + filePath.path())) { if (!filePath.exists()) // File does not exist, so no files to find. return true; // If the output does not start with the path we are searching in, find has failed. // Possibly due to unknown options. return false; } } QStringList entries = out.split("\n", Qt::SkipEmptyParts); if (entries.isEmpty()) return true; // Remove the first line, it is always the directory we are searching in. // as long as we do not specify "mindepth > 0" if (entries.size() > 0) entries.pop_front(); for (const QString &entry : entries) { if (!callBack(entry)) break; } return true; } // returns whether 'find' could be used. static bool iterateWithFind( const FilePath &filePath, const FileFilter &filter, const std::function &runInShell, const FilePath::IterateDirCallback &callBack) { const auto toFilePath = [&filePath, &callBack](const QString &entry) { if (callBack.index() == 0) return std::get<0>(callBack)(filePath.withNewPath(entry)); const QString fileName = entry.mid(1, entry.lastIndexOf('\"') - 1); const QString infos = entry.mid(fileName.length() + 3); const FilePathInfo fi = FileUtils::filePathInfoFromTriple(infos); if (!fi.fileFlags) return true; const FilePath fp = filePath.withNewPath(fileName); return std::get<1>(callBack)(fp, fi); }; // TODO: Using stat -L will always return the link target, not the link itself. // We may wan't to add the information that it is a link at some point. QString infoArgs; if (callBack.index() == 1) infoArgs = R"(-exec echo -n \"{}\"" " \; -exec stat -L -c "%f %Y %s" "{}" \;)"; return iterateWithFindHelper(filePath, filter, runInShell, toFilePath, infoArgs); } static void findUsingLs(const QString ¤t, const FileFilter &filter, const std::function &runInShell, QStringList *found) { const RunResult result = runInShell({"ls", {"-1", "-p", "--", current}}); const QStringList entries = QString::fromUtf8(result.stdOut).split('\n', Qt::SkipEmptyParts); for (QString entry : entries) { const QChar last = entry.back(); if (last == '/') { entry.chop(1); if (filter.iteratorFlags.testFlag(QDirIterator::Subdirectories)) findUsingLs(current + '/' + entry, filter, runInShell, found); } found->append(entry); } } void FileUtils::iterateUnixDirectory(const FilePath &filePath, const FileFilter &filter, bool *useFind, const std::function &runInShell, const FilePath::IterateDirCallback &callBack) { // We try to use 'find' first, because that can filter better directly. // Unfortunately, it's not installed on all devices by default. if (useFind && *useFind) { if (iterateWithFind(filePath, filter, runInShell, callBack)) return; *useFind = false; // remember the failure for the next time and use the 'ls' fallback below. } // if we do not have find - use ls as fallback QStringList entries; findUsingLs(filePath.path(), filter, runInShell, &entries); iterateLsOutput(filePath, entries, filter, callBack); } /*! Copies the directory specified by \a srcFilePath recursively to \a tgtFilePath. \a tgtFilePath will contain the target directory, which will be created. Example usage: \code QString error; bool ok = Utils::FileUtils::copyRecursively("/foo/bar", "/foo/baz", &error); if (!ok) qDebug() << error; \endcode This will copy the contents of /foo/bar into to the baz directory under /foo, which will be created in the process. \note The \a error parameter is optional. Returns whether the operation succeeded. */ bool FileUtils::copyRecursively(const FilePath &srcFilePath, const FilePath &tgtFilePath, QString *error) { return copyRecursively( srcFilePath, tgtFilePath, error, [](const FilePath &src, const FilePath &dest, QString *error) { if (!src.copyFile(dest)) { if (error) { *error = QCoreApplication::translate("Utils::FileUtils", "Could not copy file \"%1\" to \"%2\".") .arg(src.toUserOutput(), dest.toUserOutput()); } return false; } return true; }); } /*! Copies a file specified by \a srcFilePath to \a tgtFilePath only if \a srcFilePath is different (file contents and last modification time). Returns whether the operation succeeded. */ bool FileUtils::copyIfDifferent(const FilePath &srcFilePath, const FilePath &tgtFilePath) { QTC_ASSERT(srcFilePath.exists(), return false); QTC_ASSERT(srcFilePath.scheme() == tgtFilePath.scheme(), return false); QTC_ASSERT(srcFilePath.host() == tgtFilePath.host(), return false); if (tgtFilePath.exists()) { const QDateTime srcModified = srcFilePath.lastModified(); const QDateTime tgtModified = tgtFilePath.lastModified(); if (srcModified == tgtModified) { const std::optional srcContents = srcFilePath.fileContents(); const std::optional tgtContents = srcFilePath.fileContents(); if (srcContents == tgtContents) return true; } tgtFilePath.removeFile(); } return srcFilePath.copyFile(tgtFilePath); } QString FileUtils::fileSystemFriendlyName(const QString &name) { QString result = name; result.replace(QRegularExpression(QLatin1String("\\W")), QLatin1String("_")); result.replace(QRegularExpression(QLatin1String("_+")), QLatin1String("_")); // compact _ result.remove(QRegularExpression(QLatin1String("^_*"))); // remove leading _ result.remove(QRegularExpression(QLatin1String("_+$"))); // remove trailing _ if (result.isEmpty()) result = QLatin1String("unknown"); return result; } int FileUtils::indexOfQmakeUnfriendly(const QString &name, int startpos) { static const QRegularExpression checkRegExp(QLatin1String("[^a-zA-Z0-9_.-]")); return checkRegExp.match(name, startpos).capturedStart(); } QString FileUtils::qmakeFriendlyName(const QString &name) { QString result = name; // Remove characters that might trip up a build system (especially qmake): int pos = indexOfQmakeUnfriendly(result); while (pos >= 0) { result[pos] = QLatin1Char('_'); pos = indexOfQmakeUnfriendly(result, pos); } return fileSystemFriendlyName(result); } bool FileUtils::makeWritable(const FilePath &path) { return path.setPermissions(path.permissions() | QFile::WriteUser); } // makes sure that capitalization of directories is canonical on Windows and macOS. // This mimics the logic in QDeclarative_isFileCaseCorrect QString FileUtils::normalizedPathName(const QString &name) { #ifdef Q_OS_WIN const QString nativeSeparatorName(QDir::toNativeSeparators(name)); const auto nameC = reinterpret_cast(nativeSeparatorName.utf16()); // MinGW PIDLIST_ABSOLUTE file; HRESULT hr = SHParseDisplayName(nameC, NULL, &file, 0, NULL); if (FAILED(hr)) return name; TCHAR buffer[MAX_PATH]; const bool success = SHGetPathFromIDList(file, buffer); ILFree(file); return success ? QDir::fromNativeSeparators(QString::fromUtf16(reinterpret_cast(buffer))) : name; #elif defined(Q_OS_MACOS) return Internal::normalizePathName(name); #else // do not try to handle case-insensitive file systems on Linux return name; #endif } bool isRelativePathHelper(const QString &path, OsType osType) { if (path.startsWith('/')) return false; if (osType == OsType::OsTypeWindows) { if (path.startsWith('\\')) return false; // Unlike QFileInfo, this won't accept a relative path with a drive letter. // Such paths result in a royal mess anyway ... if (path.length() >= 3 && path.at(1) == ':' && path.at(0).isLetter() && (path.at(2) == '/' || path.at(2) == '\\')) return false; } return true; } bool FileUtils::isRelativePath(const QString &path) { return isRelativePathHelper(path, HostOsInfo::hostOs()); } FilePath FileUtils::commonPath(const FilePath &oldCommonPath, const FilePath &filePath) { FilePath newCommonPath = oldCommonPath; while (!newCommonPath.isEmpty() && !filePath.isChildOf(newCommonPath)) newCommonPath = newCommonPath.parentDir(); return newCommonPath.canonicalPath(); } FilePath FileUtils::homePath() { return FilePath::fromString(doCleanPath(QDir::homePath())); } FilePaths FileUtils::toFilePathList(const QStringList &paths) { return transform(paths, [](const QString &path) { return FilePath::fromString(path); }); } qint64 FileUtils::bytesAvailableFromDFOutput(const QByteArray &dfOutput) { const auto lines = filtered(dfOutput.split('\n'), [](const QByteArray &line) { return line.size() > 0; }); QTC_ASSERT(lines.size() == 2, return -1); const auto headers = filtered(lines[0].split(' '), [](const QByteArray &field) { return field.size() > 0; }); QTC_ASSERT(headers.size() >= 4, return -1); QTC_ASSERT(headers[3] == QByteArray("Available"), return -1); const auto fields = filtered(lines[1].split(' '), [](const QByteArray &field) { return field.size() > 0; }); QTC_ASSERT(fields.size() >= 4, return -1); bool ok = false; const quint64 result = QString::fromUtf8(fields[3]).toULongLong(&ok); if (ok) return result; return -1; } } // namespace Utils