diff --git a/src/base/path.cpp b/src/base/path.cpp index 14950d4739cd..b6895f4c82bf 100644 --- a/src/base/path.cpp +++ b/src/base/path.cpp @@ -54,6 +54,7 @@ namespace { QString cleanPath(const QString &path) { + const bool hasSeparator = std::any_of(path.cbegin(), path.cend(), [](const QChar c) { return (c == u'/') || (c == u'\\'); @@ -74,8 +75,18 @@ namespace static_assert(Stringable); Path::Path(const QString &pathStr) - : m_pathStr {cleanPath(pathStr)} { +#if defined(Q_OS_WIN) + if (validateUNCPath(pathStr)) + { + auto [rootPath, filename] = splitUNCPath(pathStr); + isUNCPath = true; + rootStr = rootPath; + m_pathStr = cleanPath(filename).replace(u"/"_s, u"\\"_s); + return; + } +#endif + m_pathStr = cleanPath(pathStr); } Path::Path(const std::string &pathStr) @@ -85,13 +96,14 @@ Path::Path(const std::string &pathStr) bool Path::isValid() const { - // does not support UNC path - if (isEmpty()) return false; // https://stackoverflow.com/a/31976060 #if defined(Q_OS_WIN) + if (isUNCPath) + return true; + QStringView view = m_pathStr; if (hasDriveLetter(view)) view = view.mid(3); @@ -109,7 +121,7 @@ bool Path::isValid() const bool Path::isEmpty() const { - return m_pathStr.isEmpty(); + return isUNCPath ? false : m_pathStr.isEmpty(); } bool Path::isAbsolute() const @@ -128,6 +140,36 @@ bool Path::isRelative() const return QDir::isRelativePath(m_pathStr); } +#if defined(Q_OS_WIN) +bool Path::validateUNCPath() const +{ + return validateUNCPath(data()); +} + +bool Path::validateUNCPath(const QString &pathStr) const +{ + const QRegularExpression forbidden {u"[\\0-\\37:?\"*<>|:/]"_s}; // no drive letter allowed C:/ + const QRegularExpression pattern{uR"(^\\(\\[^\\]+){2,}[\\]*$)"_s}; // need raw \\ to match back slash + return pattern.match(pathStr).hasMatch() && !pathStr.contains(forbidden); +} + +QPair Path::splitUNCPath(const QString &pathStr) const +{ + // only call this only if validateUNCPath() returns true + int slashCount = 0; + int index = 0; + for (index = 0; index < pathStr.size(); ++index) + { + if (pathStr[index] == u'\\') + { + if (++slashCount == 4) + break; + } + } + return {pathStr.left(index), pathStr.right(pathStr.size() - index)}; +} +#endif + bool Path::exists() const { return !isEmpty() && QFileInfo::exists(m_pathStr); @@ -135,8 +177,10 @@ bool Path::exists() const Path Path::rootItem() const { - // does not support UNC path - +#ifdef Q_OS_WIN + if (isUNCPath) + return createUnchecked(rootStr, true); +#endif const int slashIndex = m_pathStr.indexOf(u'/'); if (slashIndex < 0) return *this; @@ -154,8 +198,14 @@ Path Path::rootItem() const Path Path::parentPath() const { - // does not support UNC path - +#ifdef Q_OS_WIN + if (isUNCPath) + { + const int backSlashIndex = m_pathStr.lastIndexOf(u'\\'); + const QString parent = rootStr + m_pathStr.left(backSlashIndex); + return createUnchecked(parent, true); + } +#endif const int slashIndex = m_pathStr.lastIndexOf(u'/'); if (slashIndex == -1) return {}; @@ -241,12 +291,12 @@ Path Path::removedExtension(const QStringView ext) const QString Path::data() const { - return m_pathStr; + return (isUNCPath ? (rootStr + m_pathStr) : m_pathStr); } QString Path::toString() const { - return QDir::toNativeSeparators(m_pathStr); + return QDir::toNativeSeparators(data()); } std::filesystem::path Path::toStdFsPath() const @@ -333,11 +383,11 @@ void Path::addRootFolder(PathList &filePaths, const Path &rootFolder) filePath = rootFolder / filePath; } -Path Path::createUnchecked(const QString &pathStr) +Path Path::createUnchecked(const QString &pathStr, const bool isUNC) { Path path; path.m_pathStr = pathStr; - + path.isUNCPath = isUNC; return path; } diff --git a/src/base/path.h b/src/base/path.h index 8e222bc64ca6..08fee73def7d 100644 --- a/src/base/path.h +++ b/src/base/path.h @@ -50,6 +50,12 @@ class Path final bool isEmpty() const; bool isAbsolute() const; bool isRelative() const; + bool isUNCPath = false; + +#if defined(Q_OS_WIN) + bool validateUNCPath() const; + bool validateUNCPath(const QString &pathStr) const; +#endif bool exists() const; @@ -86,9 +92,14 @@ class Path final private: // this constructor doesn't perform any checks // so it's intended for internal use only - static Path createUnchecked(const QString &pathStr); + static Path createUnchecked(const QString &pathStr, const bool isUNCPath = false); QString m_pathStr; + +#if defined(Q_OS_WIN) + QString rootStr; + QPair splitUNCPath(const QString &pathStr) const; +#endif }; Q_DECLARE_METATYPE(Path) diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp index 4dd02e3ded9a..ec3cbfc36822 100644 --- a/src/base/utils/fs.cpp +++ b/src/base/utils/fs.cpp @@ -209,6 +209,17 @@ Path Utils::Fs::toValidPath(const QString &name, const QString &pad) qint64 Utils::Fs::freeDiskSpaceOnPath(const Path &path) { +#if defined(Q_OS_WIN) + const auto wStrPath = path.data().toStdWString(); + if (path.isUNCPath) + { + ULARGE_INTEGER FreeBytesAvailable = {0}; + const BOOL ok = GetDiskFreeSpaceEx(wStrPath.c_str(), &FreeBytesAvailable, nullptr, nullptr); + if (ok) + return FreeBytesAvailable.QuadPart; + } +#endif + return QStorageInfo(path.data()).bytesAvailable(); } diff --git a/test/testpath.cpp b/test/testpath.cpp index 6449a8071437..46bb181e4412 100644 --- a/test/testpath.cpp +++ b/test/testpath.cpp @@ -63,6 +63,10 @@ private slots: QVERIFY(Path(uR"(\\?\C:\)"_s) == Path(std::string(R"(\\?\C:\)"))); QVERIFY(Path(uR"(\\?\C:\abc)"_s) == Path(std::string(R"(\\?\C:\abc)"))); + + QVERIFY(Path(uR"(\\nas01\drive)"_s) == Path(std::string(R"(\\nas01\drive)"))); + QVERIFY(Path(uR"(\\nas01\drive\xxx)"_s) == Path(std::string(R"(\\nas01\drive\xxx)"))); + QVERIFY(Path(uR"(\\nas01\drive\xxx\\)"_s) == Path(std::string(R"(\\nas01\drive\xxx)"))); #endif } @@ -109,11 +113,31 @@ private slots: QCOMPARE(Path(u"<"_s).isValid(), false); QCOMPARE(Path(u">"_s).isValid(), false); QCOMPARE(Path(u"|"_s).isValid(), false); + + QCOMPARE(Path(uR"(\\nas01\drive)"_s).isValid(), true); + QCOMPARE(Path(uR"(\\nas01\drive\xxx)"_s).isValid(), true); + QCOMPARE(Path(uR"(\\nas01\drive\xxx\\)"_s).isValid(), true); #else QCOMPARE(Path(u"\0"_s).isValid(), false); #endif } +#ifdef Q_OS_WIN + void testIsUNCPath() const + { + QCOMPARE(Path(uR"(\\)"_s).isUNCPath, false); + QCOMPARE(Path(uR"(\\\)"_s).isUNCPath, false); + + QCOMPARE(Path(uR"(\\nas01\drive)"_s).isUNCPath, true); + QCOMPARE(Path(uR"(\\nas01\drive\)"_s).isUNCPath, true); + QCOMPARE(Path(uR"(\\nas01\drive\xxx)"_s).isUNCPath, true); + QCOMPARE(Path(uR"(\\nas01\drive\xxx\)"_s).isUNCPath, true); + + QCOMPARE(Path(uR"(\\C:\\drive\xxx)"_s).isUNCPath, false); + QCOMPARE(Path(uR"(\\nas01\drive\?)"_s).isUNCPath, false); + QCOMPARE(Path(uR"(\\nas01\?\xxx)"_s).isUNCPath, false); + } +#endif void testIsEmpty() const { QCOMPARE(Path().isEmpty(), true); @@ -247,6 +271,10 @@ private slots: QCOMPARE(Path(uR"(c:\)"_s).rootItem(), Path(uR"(c:/)"_s)); QCOMPARE(Path(uR"(c:\a)"_s).rootItem(), Path(uR"(c:\)"_s)); QCOMPARE(Path(uR"(c:\a\b)"_s).rootItem(), Path(uR"(c:\)"_s)); + + QCOMPARE(Path(uR"(\\nas01\drive)"_s).rootItem(), Path(uR"(\\nas01\drive)"_s)); + QCOMPARE(Path(uR"(\\nas01\drive\xxx)"_s).rootItem(), Path(uR"(\\nas01\drive)"_s)); + QCOMPARE(Path(uR"(\\nas01\drive\xxx\yyy)"_s).rootItem(), Path(uR"(\\nas01\drive)"_s)); #else QCOMPARE(Path(uR"(\a)"_s).rootItem(), Path(uR"(\a)"_s)); QCOMPARE(Path(uR"(\\a)"_s).rootItem(), Path(uR"(\\a)"_s)); @@ -280,6 +308,10 @@ private slots: QCOMPARE(Path(uR"(c:\)"_s).parentPath(), Path()); QCOMPARE(Path(uR"(c:\a)"_s).parentPath(), Path(uR"(c:\)"_s)); QCOMPARE(Path(uR"(c:\a\b)"_s).parentPath(), Path(uR"(c:\a)"_s)); + + QCOMPARE(Path(uR"(\\nas01\drive)"_s).parentPath(), Path(uR"(\\nas01\drive)"_s)); + QCOMPARE(Path(uR"(\\nas01\drive\xxx)"_s).parentPath(), Path(uR"(\\nas01\drive)"_s)); + QCOMPARE(Path(uR"(\\nas01\drive\xxx\yyy)"_s).parentPath(), Path(uR"(\\nas01\drive\xxx)"_s)); #else QCOMPARE(Path(uR"(\a)"_s).parentPath(), Path()); QCOMPARE(Path(uR"(\\a)"_s).parentPath(), Path());