Support UNC path capacity on windows

This commit is contained in:
DoubleSpicy 2025-01-25 05:47:14 +08:00
commit 1cfeecd4aa
4 changed files with 117 additions and 13 deletions

View file

@ -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::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<QString, QString> 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;
}

View file

@ -50,6 +50,12 @@ public:
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 @@ public:
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<QString, QString> splitUNCPath(const QString &pathStr) const;
#endif
};
Q_DECLARE_METATYPE(Path)

View file

@ -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();
}

View file

@ -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());