diff --git a/src/base/path.cpp b/src/base/path.cpp index 66270154c..51bea7694 100644 --- a/src/base/path.cpp +++ b/src/base/path.cpp @@ -59,6 +59,14 @@ namespace }); 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) @@ -73,15 +81,24 @@ 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) - const QRegularExpression regex {u"[:?\"*<>|]"_qs}; + 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}; + const QRegularExpression regex {u"\\0"_qs}; #endif return !m_pathStr.contains(regex); } @@ -93,11 +110,17 @@ bool Path::isEmpty() const 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); } @@ -108,6 +131,8 @@ bool Path::exists() const Path Path::rootItem() const { + // does not support UNC path + const int slashIndex = m_pathStr.indexOf(u'/'); if (slashIndex < 0) return *this; @@ -125,6 +150,8 @@ Path Path::rootItem() const Path Path::parentPath() const { + // does not support UNC path + const int slashIndex = m_pathStr.lastIndexOf(u'/'); if (slashIndex == -1) return {}; @@ -135,8 +162,8 @@ Path Path::parentPath() const #ifdef Q_OS_WIN // should be `c:/` instead of `c:` // Windows "drive letter" is limited to one alphabet - if ((slashIndex == 2) && (m_pathStr.at(1) == u':')) - return createUnchecked(m_pathStr.left(slashIndex + 1)); + 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)); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 266e42040..ad00a4bdc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,6 +12,7 @@ include_directories("../src") set(testFiles testalgorithm.cpp testorderedset.cpp + testpath.cpp testutilscompare.cpp testutilsgzip.cpp testutilsstring.cpp diff --git a/test/testpath.cpp b/test/testpath.cpp new file mode 100644 index 000000000..7dd7f3b6e --- /dev/null +++ b/test/testpath.cpp @@ -0,0 +1,293 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Mike Tzou (Chocobo1) + * + * 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 +#include + +#include "base/global.h" +#include "base/path.h" + +class TestPath final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(TestPath) + +public: + TestPath() = default; + +private slots: + void testConstructors() const + { + QVERIFY(Path(u""_qs) == Path(std::string(""))); + QVERIFY(Path(u"abc"_qs) == Path(std::string("abc"))); + QVERIFY(Path(u"/abc"_qs) == Path(std::string("/abc"))); + QVERIFY(Path(uR"(\abc)"_qs) == Path(std::string(R"(\abc)"))); + +#ifdef Q_OS_WIN + QVERIFY(Path(uR"(c:)"_qs) == Path(std::string(R"(c:)"))); + QVERIFY(Path(uR"(c:/)"_qs) == Path(std::string(R"(c:/)"))); + QVERIFY(Path(uR"(c:/)"_qs) == Path(std::string(R"(c:\)"))); + QVERIFY(Path(uR"(c:\)"_qs) == Path(std::string(R"(c:/)"))); + QVERIFY(Path(uR"(c:\)"_qs) == Path(std::string(R"(c:\)"))); + + QVERIFY(Path(uR"(\\?\C:)"_qs) == Path(std::string(R"(\\?\C:)"))); + QVERIFY(Path(uR"(\\?\C:/)"_qs) == Path(std::string(R"(\\?\C:/)"))); + QVERIFY(Path(uR"(\\?\C:/)"_qs) == Path(std::string(R"(\\?\C:\)"))); + QVERIFY(Path(uR"(\\?\C:\)"_qs) == Path(std::string(R"(\\?\C:/)"))); + QVERIFY(Path(uR"(\\?\C:\)"_qs) == Path(std::string(R"(\\?\C:\)"))); + + QVERIFY(Path(uR"(\\?\C:\abc)"_qs) == Path(std::string(R"(\\?\C:\abc)"))); +#endif + } + + void testIsValid() const + { + QCOMPARE(Path().isValid(), false); + QCOMPARE(Path(u""_qs).isValid(), false); + + QCOMPARE(Path(u"/"_qs).isValid(), true); + QCOMPARE(Path(uR"(\)"_qs).isValid(), true); + + QCOMPARE(Path(u"a"_qs).isValid(), true); + QCOMPARE(Path(u"/a"_qs).isValid(), true); + QCOMPARE(Path(uR"(\a)"_qs).isValid(), true); + + QCOMPARE(Path(u"a/b"_qs).isValid(), true); + QCOMPARE(Path(uR"(a\b)"_qs).isValid(), true); + QCOMPARE(Path(u"/a/b"_qs).isValid(), true); + QCOMPARE(Path(uR"(/a\b)"_qs).isValid(), true); + QCOMPARE(Path(uR"(\a/b)"_qs).isValid(), true); + QCOMPARE(Path(uR"(\a\b)"_qs).isValid(), true); + + QCOMPARE(Path(u"//"_qs).isValid(), true); + QCOMPARE(Path(uR"(\\)"_qs).isValid(), true); + QCOMPARE(Path(u"//a"_qs).isValid(), true); + QCOMPARE(Path(uR"(\\a)"_qs).isValid(), true); + +#if defined Q_OS_MACOS + QCOMPARE(Path(u"\0"_qs).isValid(), false); + QCOMPARE(Path(u":"_qs).isValid(), false); +#elif defined Q_OS_WIN + QCOMPARE(Path(u"c:"_qs).isValid(), false); + QCOMPARE(Path(u"c:/"_qs).isValid(), true); + QCOMPARE(Path(uR"(c:\)"_qs).isValid(), true); + QCOMPARE(Path(uR"(c:\a)"_qs).isValid(), true); + QCOMPARE(Path(uR"(c:\a\b)"_qs).isValid(), true); + + for (int i = 0; i <= 31; ++i) + QCOMPARE(Path(QChar(i)).isValid(), false); + QCOMPARE(Path(u":"_qs).isValid(), false); + QCOMPARE(Path(u"?"_qs).isValid(), false); + QCOMPARE(Path(u"\""_qs).isValid(), false); + QCOMPARE(Path(u"*"_qs).isValid(), false); + QCOMPARE(Path(u"<"_qs).isValid(), false); + QCOMPARE(Path(u">"_qs).isValid(), false); + QCOMPARE(Path(u"|"_qs).isValid(), false); +#else + QCOMPARE(Path(u"\0"_qs).isValid(), false); +#endif + } + + void testIsEmpty() const + { + QCOMPARE(Path().isEmpty(), true); + QCOMPARE(Path(u""_qs).isEmpty(), true); + + QCOMPARE(Path(u"\0"_qs).isEmpty(), false); + + QCOMPARE(Path(u"/"_qs).isEmpty(), false); + QCOMPARE(Path(uR"(\)"_qs).isEmpty(), false); + + QCOMPARE(Path(u"a"_qs).isEmpty(), false); + QCOMPARE(Path(u"/a"_qs).isEmpty(), false); + QCOMPARE(Path(uR"(\a)"_qs).isEmpty(), false); + + QCOMPARE(Path(uR"(c:)"_qs).isEmpty(), false); + QCOMPARE(Path(uR"(c:/)"_qs).isEmpty(), false); + QCOMPARE(Path(uR"(c:\)"_qs).isEmpty(), false); + } + + void testIsAbsolute() const + { + QCOMPARE(Path().isAbsolute(), false); + QCOMPARE(Path(u""_qs).isAbsolute(), false); + +#ifdef Q_OS_WIN + QCOMPARE(Path(u"/"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\)"_qs).isAbsolute(), true); + + QCOMPARE(Path(u"a"_qs).isAbsolute(), false); + QCOMPARE(Path(u"/a"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\a)"_qs).isAbsolute(), true); + + QCOMPARE(Path(u"//"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\)"_qs).isAbsolute(), true); + QCOMPARE(Path(u"//a"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\a)"_qs).isAbsolute(), true); + + QCOMPARE(Path(uR"(c:)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(c:/)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(c:\)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(c:\a)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(c:\a\b)"_qs).isAbsolute(), true); + + QCOMPARE(Path(uR"(\\?\C:)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\?\C:/)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\?\C:\)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\?\C:\a)"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\?\C:\a\b)"_qs).isAbsolute(), true); +#else + QCOMPARE(Path(u"/"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\)"_qs).isAbsolute(), false); + + QCOMPARE(Path(u"a"_qs).isAbsolute(), false); + QCOMPARE(Path(u"/a"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\a)"_qs).isAbsolute(), false); + + QCOMPARE(Path(u"//"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\)"_qs).isAbsolute(), false); + QCOMPARE(Path(u"//a"_qs).isAbsolute(), true); + QCOMPARE(Path(uR"(\\a)"_qs).isAbsolute(), false); +#endif + } + + void testIsRelative() const + { + QCOMPARE(Path().isRelative(), true); + QCOMPARE(Path(u""_qs).isRelative(), true); + +#if defined Q_OS_WIN + QCOMPARE(Path(u"/"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\)"_qs).isRelative(), false); + + QCOMPARE(Path(u"a"_qs).isRelative(), true); + QCOMPARE(Path(u"/a"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\a)"_qs).isRelative(), false); + + QCOMPARE(Path(u"//"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\)"_qs).isRelative(), false); + QCOMPARE(Path(u"//a"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\a)"_qs).isRelative(), false); + + QCOMPARE(Path(uR"(c:)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(c:/)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(c:\)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(c:\a)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(c:\a\b)"_qs).isRelative(), false); + + QCOMPARE(Path(uR"(\\?\C:)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\?\C:/)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\?\C:\)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\?\C:\a)"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\?\C:\a\b)"_qs).isRelative(), false); +#else + QCOMPARE(Path(u"/"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\)"_qs).isRelative(), true); + + QCOMPARE(Path(u"a"_qs).isRelative(), true); + QCOMPARE(Path(u"/a"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\a)"_qs).isRelative(), true); + + QCOMPARE(Path(u"//"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\)"_qs).isRelative(), true); + QCOMPARE(Path(u"//a"_qs).isRelative(), false); + QCOMPARE(Path(uR"(\\a)"_qs).isRelative(), true); +#endif + } + + void testRootItem() const + { + QCOMPARE(Path().rootItem(), Path()); + QCOMPARE(Path(u""_qs).rootItem(), Path()); + + QCOMPARE(Path(u"/"_qs).rootItem(), Path(u"/"_qs)); + QCOMPARE(Path(uR"(\)"_qs).rootItem(), Path(uR"(\)"_qs)); + + QCOMPARE(Path(u"a"_qs).rootItem(), Path(u"a"_qs)); + QCOMPARE(Path(u"/a"_qs).rootItem(), Path(u"/"_qs)); + QCOMPARE(Path(u"/a/b"_qs).rootItem(), Path(u"/"_qs)); + + QCOMPARE(Path(u"//"_qs).rootItem(), Path(u"/"_qs)); + QCOMPARE(Path(uR"(\\)"_qs).rootItem(), Path(uR"(\\)"_qs)); + QCOMPARE(Path(u"//a"_qs).rootItem(), Path(u"/"_qs)); + +#ifdef Q_OS_WIN + QCOMPARE(Path(uR"(\a)"_qs).rootItem(), Path(uR"(\)"_qs)); + QCOMPARE(Path(uR"(\\a)"_qs).rootItem(), Path(uR"(\)"_qs)); + + QCOMPARE(Path(uR"(c:)"_qs).rootItem(), Path(uR"(c:)"_qs)); + QCOMPARE(Path(uR"(c:/)"_qs).rootItem(), Path(uR"(c:/)"_qs)); + QCOMPARE(Path(uR"(c:\)"_qs).rootItem(), Path(uR"(c:\)"_qs)); + QCOMPARE(Path(uR"(c:\)"_qs).rootItem(), Path(uR"(c:/)"_qs)); + QCOMPARE(Path(uR"(c:\a)"_qs).rootItem(), Path(uR"(c:\)"_qs)); + QCOMPARE(Path(uR"(c:\a\b)"_qs).rootItem(), Path(uR"(c:\)"_qs)); +#else + QCOMPARE(Path(uR"(\a)"_qs).rootItem(), Path(uR"(\a)"_qs)); + QCOMPARE(Path(uR"(\\a)"_qs).rootItem(), Path(uR"(\\a)"_qs)); +#endif + } + + void testParentPath() const + { + QCOMPARE(Path().parentPath(), Path()); + QCOMPARE(Path(u""_qs).parentPath(), Path()); + + QCOMPARE(Path(u"/"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(\)"_qs).parentPath(), Path()); + + QCOMPARE(Path(u"a"_qs).parentPath(), Path()); + QCOMPARE(Path(u"/a"_qs).parentPath(), Path(u"/"_qs)); + + QCOMPARE(Path(u"//"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(\\)"_qs).parentPath(), Path()); + QCOMPARE(Path(u"//a"_qs).parentPath(), Path(u"/"_qs)); + + QCOMPARE(Path(u"a/b"_qs).parentPath(), Path(u"a"_qs)); + +#ifdef Q_OS_WIN + QCOMPARE(Path(uR"(\a)"_qs).parentPath(), Path(uR"(\)"_qs)); + QCOMPARE(Path(uR"(\\a)"_qs).parentPath(), Path(uR"(\)"_qs)); + QCOMPARE(Path(uR"(a\b)"_qs).parentPath(), Path(u"a"_qs)); + + QCOMPARE(Path(uR"(c:)"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(c:/)"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(c:\)"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(c:\a)"_qs).parentPath(), Path(uR"(c:\)"_qs)); + QCOMPARE(Path(uR"(c:\a\b)"_qs).parentPath(), Path(uR"(c:\a)"_qs)); +#else + QCOMPARE(Path(uR"(\a)"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(\\a)"_qs).parentPath(), Path()); + QCOMPARE(Path(uR"(a\b)"_qs).parentPath(), Path()); +#endif + } + + // TODO: add tests for remaining methods +}; + +QTEST_APPLESS_MAIN(TestPath) +#include "testpath.moc"