/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "path.h" #include #include #include #include #include #include #include #include #include "base/global.h" #if defined(Q_OS_WIN) const Qt::CaseSensitivity CASE_SENSITIVITY = Qt::CaseInsensitive; #else const Qt::CaseSensitivity CASE_SENSITIVITY = Qt::CaseSensitive; #endif const int PATHLIST_TYPEID = qRegisterMetaType("PathList"); namespace { QString cleanPath(const QString &path) { const bool hasSeparator = std::any_of(path.cbegin(), path.cend(), [](const QChar c) { return (c == u'/') || (c == u'\\'); }); return hasSeparator ? QDir::cleanPath(path) : path; } #ifdef Q_OS_WIN bool hasDriveLetter(const QStringView path) { const QRegularExpression driveLetterRegex {u"^[A-Za-z]:/"_qs}; return driveLetterRegex.match(path).hasMatch(); } #endif } Path::Path(const QString &pathStr) : m_pathStr {cleanPath(pathStr)} { } Path::Path(const std::string &pathStr) : Path(QString::fromStdString(pathStr)) { } bool Path::isValid() const { // does not support UNC path if (isEmpty()) return false; // https://stackoverflow.com/a/31976060 #if defined(Q_OS_WIN) QStringView view = m_pathStr; if (hasDriveLetter(view)) view = view.mid(3); // \\37 is using base-8 number system const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_qs}; return !regex.match(view).hasMatch(); #elif defined(Q_OS_MACOS) const QRegularExpression regex {u"[\\0:]"_qs}; #else const QRegularExpression regex {u"\\0"_qs}; #endif return !m_pathStr.contains(regex); } bool Path::isEmpty() const { return m_pathStr.isEmpty(); } bool Path::isAbsolute() const { // `QDir::isAbsolutePath` treats `:` as a path to QResource, so handle it manually if (m_pathStr.startsWith(u':')) return false; return QDir::isAbsolutePath(m_pathStr); } bool Path::isRelative() const { // `QDir::isRelativePath` treats `:` as a path to QResource, so handle it manually if (m_pathStr.startsWith(u':')) return true; return QDir::isRelativePath(m_pathStr); } bool Path::exists() const { return !isEmpty() && QFileInfo::exists(m_pathStr); } Path Path::rootItem() const { // does not support UNC path const int slashIndex = m_pathStr.indexOf(u'/'); if (slashIndex < 0) return *this; if (slashIndex == 0) // *nix absolute path return createUnchecked(u"/"_qs); #ifdef Q_OS_WIN // should be `c:/` instead of `c:` if ((slashIndex == 2) && hasDriveLetter(m_pathStr)) return createUnchecked(m_pathStr.left(slashIndex + 1)); #endif return createUnchecked(m_pathStr.left(slashIndex)); } Path Path::parentPath() const { // does not support UNC path const int slashIndex = m_pathStr.lastIndexOf(u'/'); if (slashIndex == -1) return {}; if (slashIndex == 0) // *nix absolute path return (m_pathStr.size() == 1) ? Path() : createUnchecked(u"/"_qs); #ifdef Q_OS_WIN // should be `c:/` instead of `c:` // Windows "drive letter" is limited to one alphabet if ((slashIndex == 2) && hasDriveLetter(m_pathStr)) return (m_pathStr.size() == 3) ? Path() : createUnchecked(m_pathStr.left(slashIndex + 1)); #endif return createUnchecked(m_pathStr.left(slashIndex)); } QString Path::filename() const { const int slashIndex = m_pathStr.lastIndexOf(u'/'); if (slashIndex == -1) return m_pathStr; return m_pathStr.mid(slashIndex + 1); } QString Path::extension() const { const QString suffix = QMimeDatabase().suffixForFileName(m_pathStr); if (!suffix.isEmpty()) return (u"." + suffix); const int slashIndex = m_pathStr.lastIndexOf(u'/'); const auto filename = QStringView(m_pathStr).mid(slashIndex + 1); const int dotIndex = filename.lastIndexOf(u'.', -2); return ((dotIndex == -1) ? QString() : filename.mid(dotIndex).toString()); } bool Path::hasExtension(const QStringView ext) const { Q_ASSERT(ext.startsWith(u'.') && (ext.size() >= 2)); return m_pathStr.endsWith(ext, Qt::CaseInsensitive); } bool Path::hasAncestor(const Path &other) const { if (other.isEmpty() || (m_pathStr.size() <= other.m_pathStr.size())) return false; return (m_pathStr[other.m_pathStr.size()] == u'/') && m_pathStr.startsWith(other.m_pathStr, CASE_SENSITIVITY); } Path Path::relativePathOf(const Path &childPath) const { // If both paths are relative, we assume that they have the same base path if (isRelative() && childPath.isRelative()) return Path(QDir(QDir::home().absoluteFilePath(m_pathStr)).relativeFilePath(QDir::home().absoluteFilePath(childPath.data()))); return Path(QDir(m_pathStr).relativeFilePath(childPath.data())); } void Path::removeExtension() { m_pathStr.chop(extension().size()); } Path Path::removedExtension() const { return createUnchecked(m_pathStr.chopped(extension().size())); } void Path::removeExtension(const QStringView ext) { if (hasExtension(ext)) m_pathStr.chop(ext.size()); } Path Path::removedExtension(const QStringView ext) const { return (hasExtension(ext) ? createUnchecked(m_pathStr.chopped(ext.size())) : *this); } QString Path::data() const { return m_pathStr; } QString Path::toString() const { return QDir::toNativeSeparators(m_pathStr); } std::filesystem::path Path::toStdFsPath() const { #ifdef Q_OS_WIN return {data().toStdWString(), std::filesystem::path::format::generic_format}; #else return {data().toStdString(), std::filesystem::path::format::generic_format}; #endif } Path &Path::operator/=(const Path &other) { *this = *this / other; return *this; } Path &Path::operator+=(const QStringView str) { *this = *this + str; return *this; } Path Path::commonPath(const Path &left, const Path &right) { if (left.isEmpty() || right.isEmpty()) return {}; const QList leftPathItems = QStringView(left.m_pathStr).split(u'/'); const QList rightPathItems = QStringView(right.m_pathStr).split(u'/'); int commonItemsCount = 0; qsizetype commonPathSize = 0; while ((commonItemsCount < leftPathItems.size()) && (commonItemsCount < rightPathItems.size())) { const QStringView leftPathItem = leftPathItems[commonItemsCount]; const QStringView rightPathItem = rightPathItems[commonItemsCount]; if (leftPathItem.compare(rightPathItem, CASE_SENSITIVITY) != 0) break; ++commonItemsCount; commonPathSize += leftPathItem.size(); } if (commonItemsCount > 0) commonPathSize += (commonItemsCount - 1); // size of intermediate separators return Path::createUnchecked(left.m_pathStr.left(commonPathSize)); } Path Path::findRootFolder(const PathList &filePaths) { Path rootFolder; for (const Path &filePath : filePaths) { const auto filePathElements = QStringView(filePath.m_pathStr).split(u'/'); // if at least one file has no root folder, no common root folder exists if (filePathElements.count() <= 1) return {}; if (rootFolder.isEmpty()) rootFolder.m_pathStr = filePathElements.at(0).toString(); else if (rootFolder.m_pathStr != filePathElements.at(0)) return {}; } return rootFolder; } void Path::stripRootFolder(PathList &filePaths) { const Path commonRootFolder = findRootFolder(filePaths); if (commonRootFolder.isEmpty()) return; for (Path &filePath : filePaths) filePath.m_pathStr = filePath.m_pathStr.mid(commonRootFolder.m_pathStr.size() + 1); } void Path::addRootFolder(PathList &filePaths, const Path &rootFolder) { Q_ASSERT(!rootFolder.isEmpty()); for (Path &filePath : filePaths) filePath = rootFolder / filePath; } Path Path::createUnchecked(const QString &pathStr) { Path path; path.m_pathStr = pathStr; return path; } bool operator==(const Path &lhs, const Path &rhs) { return (lhs.data().compare(rhs.data(), CASE_SENSITIVITY) == 0); } bool operator!=(const Path &lhs, const Path &rhs) { return !(lhs == rhs); } Path operator/(const Path &lhs, const Path &rhs) { if (rhs.isEmpty()) return lhs; if (lhs.isEmpty()) return rhs; return Path(lhs.m_pathStr + u'/' + rhs.m_pathStr); } Path operator+(const Path &lhs, const QStringView rhs) { return Path(lhs.data() + rhs); } QDataStream &operator<<(QDataStream &out, const Path &path) { out << path.data(); return out; } QDataStream &operator>>(QDataStream &in, Path &path) { QString pathStr; in >> pathStr; path = Path(pathStr); return in; } #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) std::size_t qHash(const Path &key, const std::size_t seed) #else uint qHash(const Path &key, const uint seed) #endif { return ::qHash(key.data(), seed); }