From 19f3b8d454c2dd88f3baaff64fce10ae840d463c Mon Sep 17 00:00:00 2001
From: DoubleSpicy <stven3au@gmail.com>
Date: Sat, 25 Jan 2025 05:47:14 +0800
Subject: [PATCH] Support UNC path capacity on windows

---
 src/base/path.cpp     | 74 +++++++++++++++++++++++++++++++++++++++----
 src/base/path.h       |  9 ++++++
 src/base/utils/fs.cpp | 11 +++++++
 test/testpath.cpp     | 28 ++++++++++++++++
 4 files changed, 116 insertions(+), 6 deletions(-)

diff --git a/src/base/path.cpp b/src/base/path.cpp
index 14950d4739cd..f4162e040c37 100644
--- a/src/base/path.cpp
+++ b/src/base/path.cpp
@@ -54,6 +54,19 @@ namespace
 {
     QString cleanPath(const QString &path)
     {
+#if defined(Q_OS_WIN)
+        // QT's cleanPath does not support UNC path
+        if (isQStrUNCPath(path))
+        {
+            // only trim off trailing \ which is meaningless
+            int index = path.size();
+            while (path[index - 1] == u'\\')
+                --index;
+            if (index <= (path.size() - 1))
+                return path.left(index);
+            return path;
+        }
+#endif
         const bool hasSeparator = std::any_of(path.cbegin(), path.cend(), [](const QChar c)
         {
             return (c == u'/') || (c == u'\\');
@@ -85,8 +98,6 @@ Path::Path(const std::string &pathStr)
 
 bool Path::isValid() const
 {
-    // does not support UNC path
-
     if (isEmpty())
         return false;
 
@@ -96,6 +107,9 @@ bool Path::isValid() const
     if (hasDriveLetter(view))
         view = view.mid(3);
 
+    if (isUNCPath())
+        return true;
+
     // \\37 is using base-8 number system
     const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s};
     return !regex.match(view).hasMatch();
@@ -128,6 +142,29 @@ bool Path::isRelative() const
     return QDir::isRelativePath(m_pathStr);
 }
 
+#if defined(Q_OS_WIN)
+bool Path::isUNCPath() const
+{
+    return isQStrUNCPath(data());
+}
+
+QString Path::getUNCRootPathStr() const
+{
+    // only call this with isUNCPath() guard
+    int slashCount = 0;
+    int index = 0;
+    for (index = 0; index < m_pathStr.size(); ++index)
+    {
+        if (m_pathStr[index] == u'\\')
+        {
+            if (++slashCount == 4)
+                break;
+        }
+    }
+    return m_pathStr.left(index);
+}
+#endif
+
 bool Path::exists() const
 {
     return !isEmpty() && QFileInfo::exists(m_pathStr);
@@ -135,8 +172,10 @@ bool Path::exists() const
 
 Path Path::rootItem() const
 {
-    // does not support UNC path
-
+#ifdef Q_OS_WIN
+    if (isUNCPath())
+        return createUnchecked(getUNCRootPathStr());
+#endif
     const int slashIndex = m_pathStr.indexOf(u'/');
     if (slashIndex < 0)
         return *this;
@@ -154,8 +193,17 @@ Path Path::rootItem() const
 
 Path Path::parentPath() const
 {
-    // does not support UNC path
-
+#ifdef Q_OS_WIN
+    if (isUNCPath())
+    {
+        // UNC path checks guarantee path in \\host\drive or \\host\drive\xxx format
+        const auto rootPath = getUNCRootPathStr();
+        if (rootPath == m_pathStr)
+            return createUnchecked(m_pathStr);
+        const int slashIndex = m_pathStr.lastIndexOf(u'\\');
+        return createUnchecked(m_pathStr.left(slashIndex));
+    }
+#endif
     const int slashIndex = m_pathStr.lastIndexOf(u'/');
     if (slashIndex == -1)
         return {};
@@ -380,3 +428,17 @@ std::size_t qHash(const Path &key, const std::size_t seed)
 {
     return ::qHash(key.data(), seed);
 }
+
+#if defined(Q_OS_WIN)
+bool isUNCPath(const QString &path)
+{
+    // strict format of \\host\drive\folder with no forbidden char to avoid conflict with normal path
+    if (path.left(2) != uR"(\\)"_s)
+        return false;
+    const auto lastSlash = path.lastIndexOf(u'\\');
+    if ((lastSlash - 1) <= 2)
+        return false;
+    const QRegularExpression regex {u"[\\0-\\37:?\"*<>|:/]"_s}; // no drive letter allowed C:/
+    return !regex.match(path.data()).hasMatch();
+}
+#endif
diff --git a/src/base/path.h b/src/base/path.h
index 8e222bc64ca6..4fd127a38337 100644
--- a/src/base/path.h
+++ b/src/base/path.h
@@ -51,6 +51,11 @@ class Path final
     bool isAbsolute() const;
     bool isRelative() const;
 
+#if defined(Q_OS_WIN)
+    bool isUNCPath() const;
+    QString getUNCRootPathStr() const;
+#endif
+
     bool exists() const;
 
     Path rootItem() const;
@@ -100,3 +105,7 @@ QDataStream &operator<<(QDataStream &out, const Path &path);
 QDataStream &operator>>(QDataStream &in, Path &path);
 
 std::size_t qHash(const Path &key, std::size_t seed = 0);
+
+#if defined(Q_OS_WIN)
+bool isQStrUNCPath(const QString &path);
+#endif
diff --git a/src/base/utils/fs.cpp b/src/base/utils/fs.cpp
index 4dd02e3ded9a..8094faedae2f 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)
+    std::wstring 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..a96ad0e16c50 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,27 @@ 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\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 +267,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 +304,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());