From 2addfa635d2f666d18ffc818f76eb673a6c787d1 Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Tue, 17 Sep 2024 10:11:06 +0200 Subject: [PATCH] QUrl: Keep one trailing slash when normalizing "file:///b/." Removing the slash would be a drastic change in behavior. We can expect many people to rely on the trailing slash to string-append further path segments without inserting an extra slash. Amends commit e7bcf41c0b23d83cfb31f966454945c705589a99. Pick-to: 6.8 6.7 6.5 Fixes: QTBUG-128940 Change-Id: I71dfc957f383e4648b3b47a40aeb796d7237fb00 Reviewed-by: Thiago Macieira --- src/corelib/io/qdir.cpp | 26 ++++++++++++++++++++++--- src/corelib/io/qdir_p.h | 3 ++- src/corelib/io/qurl.cpp | 8 ++++++-- tests/auto/corelib/io/qurl/tst_qurl.cpp | 4 +++- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/corelib/io/qdir.cpp b/src/corelib/io/qdir.cpp index 2495ae93c73..80592cb64d7 100644 --- a/src/corelib/io/qdir.cpp +++ b/src/corelib/io/qdir.cpp @@ -2215,11 +2215,17 @@ bool QDir::match(const QString &filter, const QString &fileName) 3) a sequence of "//" is treated as multiple path levels ("a/b//.." becomes "a/b/" and "a/b//../.." becomes "a/"), which matches the behavior observed in web browsers. + + However, QUrl also uses local path mode for local URLs; with one exception: + Even in local mode we leave one trailing slash for paths ending in "/." if + the KeepLocalTrailingSlash flag is given. This reflects how QUrl needs to + treat local URLs due to compatibility constraints. */ bool qt_normalizePathSegments(QString *path, QDirPrivate::PathNormalizations flags) { const bool allowUncPaths = flags.testAnyFlag(QDirPrivate::AllowUncPaths); const bool isRemote = flags.testAnyFlag(QDirPrivate::RemotePath); + const bool keepLocalTrailingSlash = flags.testAnyFlags(QDirPrivate::KeepLocalTrailingSlash); const qsizetype prefixLength = rootLength(*path, allowUncPaths); // RFC 3986 says: "The input buffer is initialized with the now-appended @@ -2331,11 +2337,25 @@ bool qt_normalizePathSegments(QString *path, QDirPrivate::PathNormalizations fla } if (out > start) { - // backtrack one or all the slashes (so "/tmp///" -> "/tmp/") + // Always backtrack one slash if (out[-1] == u'/' && in != end) --out; - while (!isRemote && out > start && out[-1] == u'/') - --out; + + if (!isRemote) { + bool removedAnySlashes = false; + + // Backtrack all slashes ... + while (out > start && out[-1] == u'/') { + --out; + removedAnySlashes = true; + } + + // ... except a trailing one if it exists and flag given + if (removedAnySlashes && keepLocalTrailingSlash && out > start) { + ++out; + break; + } + } } if (out == start) { // We've reached the root. Make sure we don't turn a relative path diff --git a/src/corelib/io/qdir_p.h b/src/corelib/io/qdir_p.h index 748b4143143..3641d003bec 100644 --- a/src/corelib/io/qdir_p.h +++ b/src/corelib/io/qdir_p.h @@ -30,7 +30,8 @@ class QDirPrivate : public QSharedData enum PathNormalization { DefaultNormalization = 0x00, AllowUncPaths = 0x01, - RemotePath = 0x02 + RemotePath = 0x02, + KeepLocalTrailingSlash = 0x04, }; Q_DECLARE_FLAGS(PathNormalizations, PathNormalization) Q_FLAGS(PathNormalizations) diff --git a/src/corelib/io/qurl.cpp b/src/corelib/io/qurl.cpp index e867b2d27ea..2b34c6b8bcb 100644 --- a/src/corelib/io/qurl.cpp +++ b/src/corelib/io/qurl.cpp @@ -911,7 +911,9 @@ inline void QUrlPrivate::appendPath(QString &appendTo, QUrl::FormattingOptions o { QString thePath = path; if (options & QUrl::NormalizePathSegments) { - qt_normalizePathSegments(&thePath, isLocalFile() ? QDirPrivate::DefaultNormalization : QDirPrivate::RemotePath); + qt_normalizePathSegments( + &thePath, + isLocalFile() ? QDirPrivate::KeepLocalTrailingSlash : QDirPrivate::RemotePath); } QStringView thePathView(thePath); @@ -2712,7 +2714,9 @@ QUrl QUrl::resolved(const QUrl &relative) const else t.d->sectionIsPresent &= ~QUrlPrivate::Fragment; - qt_normalizePathSegments(&t.d->path, isLocalFile() ? QDirPrivate::DefaultNormalization : QDirPrivate::RemotePath); + qt_normalizePathSegments( + &t.d->path, + isLocalFile() ? QDirPrivate::KeepLocalTrailingSlash : QDirPrivate::RemotePath); if (!t.d->hasAuthority()) fixupNonAuthorityPath(&t.d->path); diff --git a/tests/auto/corelib/io/qurl/tst_qurl.cpp b/tests/auto/corelib/io/qurl/tst_qurl.cpp index 30c2c61d855..ea751b586ee 100644 --- a/tests/auto/corelib/io/qurl/tst_qurl.cpp +++ b/tests/auto/corelib/io/qurl/tst_qurl.cpp @@ -1497,7 +1497,9 @@ void tst_QUrl::fromLocalFileNormalize_data() QTest::newRow("relative-dot-dot") << QString::fromLatin1("././a.txt") << QString::fromLatin1("file:././a.txt") << QString::fromLatin1("file:a.txt"); QTest::newRow("relative-path-dotdot") << QString::fromLatin1("b/../a.txt") << QString::fromLatin1("file:b/../a.txt") << QString::fromLatin1("file:a.txt"); QTest::newRow("absolute-path-dotdot") << QString::fromLatin1("/b/../a.txt") << QString::fromLatin1("file:///b/../a.txt") << QString::fromLatin1("file:///a.txt"); - QTest::newRow("absolute-path-dot") << QString::fromLatin1("/b/.") << QString::fromLatin1("file:///b/.") << QString::fromLatin1("file:///b"); + QTest::newRow("absolute-path-slash") << QString::fromLatin1("/b/") << QString::fromLatin1("file:///b/") << QString::fromLatin1("file:///b/"); + QTest::newRow("absolute-path-slahs-dot") << QString::fromLatin1("/b/.") << QString::fromLatin1("file:///b/.") << QString::fromLatin1("file:///b/"); + QTest::newRow("absolute-path-slahs-dot-slash") << QString::fromLatin1("/b/./") << QString::fromLatin1("file:///b/./") << QString::fromLatin1("file:///b/"); } void tst_QUrl::fromLocalFileNormalize()