Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support UNC path capacity on Windows #22202

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

DoubleSpicy
Copy link
Contributor

@DoubleSpicy DoubleSpicy commented Jan 24, 2025

Add UNC path capacity calculation capability by invoking Windows API directly.

For #21954,
Can we get a confirmation on linux as well?

@DoubleSpicy
Copy link
Contributor Author

QT seems doesn't have full support of UNC paths in general, a workaround is to mount it as a drive, mentioned at PCSX2/pcsx2#6258 and https://forum.qt.io/topic/96418/how-to-create-a-folder-and-write-a-file-in-it-using-the-unc-path/2

@glassez glassez changed the title Support UNC path capacity on windows Support UNC path capacity on Windows Jan 25, 2025
src/gui/addnewtorrentdialog.cpp Outdated Show resolved Hide resolved
src/gui/addnewtorrentdialog.cpp Outdated Show resolved Hide resolved
src/gui/addnewtorrentdialog.cpp Outdated Show resolved Hide resolved
src/gui/addnewtorrentdialog.cpp Outdated Show resolved Hide resolved
src/gui/addnewtorrentdialog.cpp Outdated Show resolved Hide resolved
src/gui/addnewtorrentdialog.cpp Outdated Show resolved Hide resolved
@DoubleSpicy DoubleSpicy force-pushed the UNC branch 7 times, most recently from c66a932 to 4a58443 Compare January 28, 2025 19:39
@DoubleSpicy
Copy link
Contributor Author

hi @glassez I have reworked this feature and added a bunch of testcases to avoid impacting normal path handling.

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to further simplify this PR?
I suppose you only need the following to make it work:

  1. bool Path::isUNCPath() const
    It will check if the path is a valid UNC path.
  2. This code at Utils::Fs::freeDiskSpaceOnPath()

I especially would like to retain the old QString cleanPath(const QString &path) code if it won't block/get in the way of UNC paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree perhaps that the cleanPath(path) can be done in an more elegant way. Something like path.toStdWString().find_last_not_of(u"\\"_s)+1 can replace the loops.

Few tricky things to consider when distinguishing UNC paths from normal paths:

  1. QDir::cleanPath is designed for "normal paths", it normalizes all back slashes into forward slashes and try to eliminate extra slashes.
    \\host\drive\folder will become //host/drive/folder

  2. PathIsUNCW() from win32 api in my 1st iteration is not reliable as well, it returns true whenever the string starts with double back slashes \\

  3. Path::rootItem() and Path::parentPath() is required in queryFreeDiskSpace(const Path &path) in order to traverse through folders for capacity and create new folders when input path does not exist, but the current version depends on forward slashes /

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to just declare UNC paths officially unsupported?

src/base/path.cpp Outdated Show resolved Hide resolved
@@ -51,6 +51,11 @@ class Path final
bool isAbsolute() const;
bool isRelative() const;

#if defined(Q_OS_WIN)
bool isUNCPath() const;
QString getUNCRootPathStr() const;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is part of implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is part of implementation.

So?

src/base/path.h Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved
src/base/utils/fs.cpp Outdated Show resolved Hide resolved
src/base/path.cpp Outdated Show resolved Hide resolved

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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why \\nas01\drive is root item?

Copy link
Contributor Author

@DoubleSpicy DoubleSpicy Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the ordinary usecase where we mount a network drive on windows, are there usecases where a UNC path is usable in form of \\host without specifying the share component?

I don't have a nas at home handy, the test is done by mounting 2 folder as network drive

C:\Users\Z>net use
New connections will be remembered.


Status       Local     Remote                    Network

-------------------------------------------------------------------------------
OK           X:        \\mydesktop\mount1
                                                Microsoft Windows Network
OK           Y:        \\mydesktop\mount2
                                                Microsoft Windows Network
The command completed successfully.

edit: Found a doc from microsoft stating that it must have >= 2 components: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsc/149a3039-98ce-491a-9268-2f5ddef08192

Copy link
Member

@xavier2k6 xavier2k6 Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't done any tests with this PR yet, but here's what net use returns on my setup (xxx.xxx.xxx.xxx = IP) with 3x NAS connected.

C:\WINDOWS\system32>net use
New connections will be remembered.


Status       Local     Remote                    Network

-------------------------------------------------------------------------------
OK           T:        \\xxx.xxx.xxx.xxx\Movies   Microsoft Windows Network
OK           V:        \\xxx.xxx.xxx.xxx\NAS3     Microsoft Windows Network
OK           W:        \\xxx.xxx.xxx.xxx\Tv Shows
                                                Microsoft Windows Network
The command completed successfully.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why \\nas01\drive is root item?

Can \\servername\share not be used like the docs show?

Copy link
Member

@Chocobo1 Chocobo1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ps. Some preliminary review. Haven't done a full round.

Comment on lines +124 to +125
if (isUNCPath())
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isUNCPath())
return true;
#if defined(Q_OS_WIN)
if (isUNCPath())
return true;
#endif

QString cleanPath(const QString &path)
{
#if defined(Q_OS_WIN)
// QT's cleanPath does not support UNC path
if (isQStrUNCPath(path))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this is full validation which is too expensive here. You should use fast/lightweight checks instead. This function doesn't need validation for constructing paths.

bool isQStrUNCPath(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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use QStringView::startsWith

// 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'\\');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no auto here please.

const auto lastSlash = path.lastIndexOf(u'\\');
if ((lastSlash - 1) <= 2)
return false;
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|:/]"_s}; // no drive letter allowed C:/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|:/]"_s}; // no drive letter allowed C:/
static const QRegularExpression regex {u"[\\0-\\37:?\"*<>|:/]"_s}; // no drive letter allowed C:/

if ((lastSlash - 1) <= 2)
return false;
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|:/]"_s}; // no drive letter allowed C:/
return !regex.match(path.data()).hasMatch();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use QStringView::contains

QString cleanPath(const QString &path)
{
#if defined(Q_OS_WIN)
// QT's cleanPath does not support UNC path
Copy link
Member

@Chocobo1 Chocobo1 Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea.
You could try to split the UNC path into 2 fields. First is host-name and the second is the rest of the path. host-name will have its own private QString variable in Path class and the second part will be stored in m_pathStr. The second part will look like a relative path.
This way you could reuse existing code and you only need to concat the host-name back when full path is expected in various methods.

https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/62e862f4-2a51-452e-8eeb-dc4ff5ee33cc

@@ -52,8 +52,35 @@ const int PATHLIST_TYPEID = qRegisterMetaType<PathList>("PathList");

namespace
{
#if defined(Q_OS_WIN)
bool isQStrUNCPath(const QString &path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to use QStringView as input parameter type.

Comment on lines +75 to +81
// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this way. All paths are QDir::cleanPath processed except UNC paths. This creates discrepancy behavior which might create unforeseen surprises in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo it can be removed with the idea you posted of storing \\host\share root as a string, it also helps to handle extra slashes like \\host\share\xxx\\\\yyy

Comment on lines +54 to +57
#if defined(Q_OS_WIN)
bool isUNCPath() const;
QString getUNCRootPathStr() const;
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move them to the bottom of public: section. To right before private: section.

@@ -52,8 +52,35 @@ const int PATHLIST_TYPEID = qRegisterMetaType<PathList>("PathList");

namespace
{
#if defined(Q_OS_WIN)
bool isQStrUNCPath(const QString &path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool isQStrUNCPath(const QString &path)
bool isUNCPath(const QString &path)

@Neustradamus
Copy link

WinDirStat supports it, maybe you can look how it has been done:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants