KDE Dolphin が SMB 共有に表示されるサーバにアクセスしようとするとクラッシュする(その1)
Dolphin 使ってると
- リモート > ネットワーク > 共有フォルダ(SMB)
で表示された Windows マシンのアイコンをダブルクリックしファイル共有を参照しようとすると、認証終わったあたりで SEGV くらうのだよね。
落ちている個所は KIO(KDE Input/Output) というユーザースペースの VFS(Virtual File System) 実装の中。
$ gdb --quiet /usr/bin/dolphin
Reading symbols from /usr/bin/dolphin...
Reading symbols from /home/tnozaki/.cache/debuginfod_client/3d88faec9fb21d6b0c44b4c6dd9f7428bc88e28c/debuginfo...
(gdb) run smb://
Starting program: /usr/bin/dolphin smb://
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7fffeffa16c0 (LWP 10500)]
[New Thread 0x7fffef7a06c0 (LWP 10501)]
[New Thread 0x7fffee5ae6c0 (LWP 10504)]
[New Thread 0x7fffedd786c0 (LWP 10505)]
[Thread 0x7fffedd786c0 (LWP 10505) exited]
[New Thread 0x7fffedd786c0 (LWP 10508)]
[New Thread 0x7fffe55ff6c0 (LWP 10509)]
[New Thread 0x7fffd5bff6c0 (LWP 10510)]
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: Could not load default global viewproperties
[Detaching after fork from child process 10514]
[Detaching after fork from child process 10517]
[Detaching after fork from child process 10519]
[New Thread 0x7fffc7bff6c0 (LWP 10522)]
[New Thread 0x7fffc73fe6c0 (LWP 10523)]
[Detaching after fork from child process 10531]
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: Could not load default global viewproperties
[Detaching after fork from child process 10540]
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: could not find entry for charset= "その他のエンコーディング ()"
org.kde.dolphin: Could not load default global viewproperties
org.kde.dolphin: Could not load default global viewproperties
kf.kio.core: Internal error: itemsInUse did not contain QUrl("smb://Administrator@hpmsrv01.local/")
Thread 1 "dolphin" received signal SIGSEGV, Segmentation fault.
KCoreDirListerCache::slotUpdateResult (this=0x55555590d4a0, j=<optimized out>) at /usr/src/debug/kio-6.17.0/src/core/kcoredirlister.cpp:1731
1731 for (const KFileItem &item : std::as_const(dir->lstItems)) {
Missing separate debuginfos, use: zypper install libopenh264-8-debuginfo-2.6.0-2.suse1699.10.x86_64
(gdb) bt
#0 KCoreDirListerCache::slotUpdateResult (this=0x55555590d4a0, j=<optimized out>) at /usr/src/debug/kio-6.17.0/src/core/kcoredirlister.cpp:1731
#1 0x00007ffff52308b4 in QtPrivate::QSlotObjectBase::call (this=<optimized out>, r=<optimized out>, a=<optimized out>, this=<optimized out>, r=<optimized out>,
a=<optimized out>) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobjectdefs_impl.h:461
#2 doActivate<false> (sender=0x555556042ec0, signal_index=6, argv=0x7fffffffcb50) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobject.cpp:4157
#3 0x00007ffff6d8cc81 in QMetaObject::activate<void, KJob*, KJob::QPrivateSignal> (sender=0x555556042ec0, mo=<optimized out>, local_signal_index=3, ret=0x0)
at /usr/include/qt6/QtCore/qobjectdefs.h:306
#4 KJob::result (this=this@entry=0x555556042ec0, _t1=<optimized out>, _t1@entry=0x555556042ec0, _t2=...)
at /usr/src/debug/kcoreaddons-6.17.0/build/src/lib/KF6CoreAddons_autogen/include/moc_kjob.cpp:475
#5 0x00007ffff6d9275b in KJob::finishJob (this=0x555556042ec0, emitResult=<optimized out>) at /usr/src/debug/kcoreaddons-6.17.0/src/lib/jobs/kjob.cpp:115
#6 0x00007ffff52308b4 in QtPrivate::QSlotObjectBase::call (this=<optimized out>, r=<optimized out>, a=<optimized out>, this=<optimized out>, r=<optimized out>,
a=<optimized out>) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobjectdefs_impl.h:461
#7 doActivate<false> (sender=0x555556008db0, signal_index=7, argv=0x7fffffffcc18) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobject.cpp:4157
#8 0x00007ffff7907d41 in KIO::WorkerInterface::finished (this=0x555556008db0)
at /usr/src/debug/kio-6.17.0/build/src/core/KF6KIOCore_autogen/include/moc_workerinterface_p.cpp:341
#9 KIO::WorkerInterface::dispatch (this=0x555556008db0, _cmd=104, rawdata=...) at /usr/src/debug/kio-6.17.0/src/core/workerinterface.cpp:127
#10 0x00007ffff79002c4 in KIO::WorkerInterface::dispatch (this=0x555556008db0) at /usr/src/debug/kio-6.17.0/src/core/workerinterface.cpp:58
#11 0x00007ffff7903cf0 in KIO::Worker::gotInput (this=0x555556008db0) at /usr/src/debug/kio-6.17.0/src/core/worker.cpp:262
#12 0x00007ffff52308b4 in QtPrivate::QSlotObjectBase::call (this=<optimized out>, r=<optimized out>, a=<optimized out>, this=<optimized out>, r=<optimized out>,
a=<optimized out>) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobjectdefs_impl.h:461
#13 doActivate<false> (sender=0x55555603c530, signal_index=3, argv=0x7fffffffcff8) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobject.cpp:4157
#14 0x00007ffff521d9d4 in QObject::event (this=<optimized out>, e=<optimized out>) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qobject.cpp:1432
#15 0x00007ffff63e51c8 in QApplicationPrivate::notify_helper (this=<optimized out>, receiver=0x55555603c530, e=0x555555f2db70)
at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/widgets/kernel/qapplication.cpp:3300
#16 0x00007ffff51c9138 in QCoreApplication::notifyInternal2 (receiver=0x55555603c530, event=0x555555f2db70)
at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qcoreapplication.cpp:1106
#17 0x00007ffff51c917d in QCoreApplication::sendEvent (receiver=<optimized out>, event=<optimized out>)
at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qcoreapplication.cpp:1546
#18 0x00007ffff51cb567 in QCoreApplicationPrivate::sendPostedEvents (receiver=0x0, event_type=0, data=0x5555556f73c0)
at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qcoreapplication.cpp:1891
#19 0x00007ffff547fc17 in postEventSourceDispatch (s=s@entry=0x555555744090) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qeventdispatcher_glib.cpp:246
#20 0x00007ffff2a630b6 in g_main_dispatch (context=0x7fffe8000f70) at ../glib/gmain.c:3398
#21 g_main_context_dispatch_unlocked (context=context@entry=0x7fffe8000f70) at ../glib/gmain.c:4249
#22 0x00007ffff2a64ee8 in g_main_context_iterate_unlocked (context=context@entry=0x7fffe8000f70, block=block@entry=1, dispatch=dispatch@entry=1, self=<optimized out>)
at ../glib/gmain.c:4314
#23 0x00007ffff2a6572c in g_main_context_iteration (context=0x7fffe8000f70, may_block=1) at ../glib/gmain.c:4379
#24 0x00007ffff547d868 in QEventDispatcherGlib::processEvents (this=0x55555580ecd0, flags=...)
at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qeventdispatcher_glib.cpp:399
#25 0x00007ffff51d6ab3 in QEventLoop::exec (this=0x7fffffffd420, flags=...) at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/global/qflags.h:77
#26 0x00007ffff51cda63 in QCoreApplication::exec () at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/corelib/kernel/qcoreapplication.cpp:1449
#27 0x00007ffff5a21250 in QGuiApplication::exec () at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/gui/kernel/qguiapplication.cpp:1986
#28 0x00007ffff63dff29 in QApplication::exec () at /usr/src/debug/qtbase-everywhere-src-6.9.2/src/widgets/kernel/qapplication.cpp:2567
#29 0x00005555555be0bf in main (argc=<optimized out>, argv=<optimized out>) at /usr/src/debug/dolphin-25.08.0/src/main.cpp:257
(gdb)
同様の現象が Bug 451050: Dolphin crashing when connecting SMB share で報告されており、この コミット で修正され KIO 6.16 としてリリースされている。
しかし openSUSE Tumbleweed に含まれるバージョンは
- Dolphin(25.08.0-1.2)
- libKF6KIO6(6.17.0-1.2)
- kf6-kio(6.17.0-1.2)
- kio-extras(25.08.0-1.2)
はそれより新しいものにもかかわらずクラッシュする、これは KDE Neon 6 の
- Dolphin(25.08.0-0zneon+24.04+noble+releas+build24)
- kf6-kio(6.17.0-0zneon+24.04+noble+releas+build54)
- kio-extras(25.08.0-0zneon+24.04+noble+releas+build3)
でも同様なんで、直した(直ってない)案件なんだろうかね…
履歴を追ってみたら 6.17 で Bug 507278: Can not access remote root directory (/) via SFTP の コミット で実質 revert されてることが判明した、うーんこの。
どうりで openSUSE Leap 16.0 RC では問題が発生しないわけである、こっちは
- Dolphin(25.04.3-bp160.1.2)
- libKF6KIO6(6.16.0-bp160.1.2)
- kf6-kio(6.16.0-bp160.1.2)
- kio-extra(25.04.3-bp160.1.2)
と revert される前のバージョンだもんな、まあ前述の通り sftp の方で問題が出るんだろうけど。
この記事を書いてる間に 6.19 までバージョン上がってるんだが、差分みる限り直ってなさそう。
そもそもの話、同じ KIO をバックエンドにするインターネットブラウザ兼ファイルマネージャの Konqueror では問題発生していないので、修正が必要なのは普通に考えたら Dolphin だと思うんだよね。
しかたねえ少しはやる気出してソース読むか。
1664 void KCoreDirListerCache::slotUpdateResult(KJob *j)
1665 {
1666 Q_ASSERT(j);
1667 KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
1668
1669 QUrl jobUrl(joburl(job));
1670 jobUrl = cleanUpTrailingSlash(jobUrl); // need remove trailing slashes again, in case of redirections
...
1709 DirItem *dir = itemsInUse.value(jobUrl, nullptr);
1710 if (!dir) {
1711 qCWarning(KIO_CORE) << "Internal error: itemsInUse did not contain" << jobUrl;
1712#ifndef NDEBUG
1713 printDebug();
1714#endif
1715 Q_ASSERT(dir);
1716 } else {
1717 dir->complete = true;
1718 }
…
1729 // Fill the hash from the old list of items. We'll remove entries as we see them
1730 // in the new listing, and the resulting hash entries will be the deleted items.
1731 for (const KFileItem &item : std::as_const(dir->lstItems)) {
1732 fileItems.insert(item.name(), item);
1733 }
コンソールに 1711 行目の
Internal error: itemsInUse did not contain QUrl("smb://Administrator@hpmsrv01.local/")
が表示されているので、1709 行目の DirItem *dir は nullptr で -DNDEBUG つきでコンパイルされてれば 1715 行目の Q_ASSERT(dir) で死んでたはず。
ところが想定外のまま動き続けて 1731 行目で nullptr にアクセスしてしまい SEGV ということ。
まず Bug 451050 の差分を眺めてみよう。
diff --git a/src/core/kcoredirlister.cpp b/src/core/kcoredirlister.cpp
index 3a07c13e97cd4ebd9960ce19c50a78f179dc1975..0968ecec0343d5058bc7c6d482f7b20b7efa94ea 100644
--- a/src/core/kcoredirlister.cpp
+++ b/src/core/kcoredirlister.cpp
@@ -89,7 +89,7 @@ bool KCoreDirListerCache::listDir(KCoreDirLister *lister, const QUrl &dirUrl, bo
_url.setPath(QDir::cleanPath(_url.path())); // kill consecutive slashes
// like this we don't have to worry about trailing slashes any further
- _url = _url.adjusted(QUrl::StripTrailingSlash);
+ _url = cleanUpTrailingSlash(_url);
QString resolved;
if (_url.isLocalFile()) {
@@ -378,7 +378,7 @@ void KCoreDirListerCache::stop(KCoreDirLister *lister, bool silent)
void KCoreDirListerCache::stopListingUrl(KCoreDirLister *lister, const QUrl &_u, bool silent)
{
QUrl url(_u);
- url = url.adjusted(QUrl::StripTrailingSlash);
+ url = cleanUpTrailingSlash(url);
KCoreDirListerPrivate::CachedItemsJob *cachedItemsJob = lister->d->cachedItemsJobForUrl(url);
if (cachedItemsJob) {
@@ -486,7 +486,7 @@ void KCoreDirListerCache::forgetDirs(KCoreDirLister *lister, const QUrl &_url, b
{
qCDebug(KIO_CORE_DIRLISTER) << lister << " _url: " << _url;
- const QUrl url = _url.adjusted(QUrl::StripTrailingSlash);
+ const QUrl url = cleanUpTrailingSlash(_url);
DirectoryDataHash::iterator dit = directoryData.find(url);
if (dit == directoryData.end()) {
@@ -588,9 +588,9 @@ void KCoreDirListerCache::updateDirectory(const QUrl &_dir)
{
qCDebug(KIO_CORE_DIRLISTER) << _dir;
- QUrl dir = _dir.adjusted(QUrl::StripTrailingSlash);
+ QUrl dir = cleanUpTrailingSlash(_dir);
if (!checkUpdate(dir)) {
- auto parentDir = dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ auto parentDir = cleanUpTrailingSlash(dir.adjusted(QUrl::RemoveFilename));
if (checkUpdate(parentDir)) {
// if the parent is in use, update it instead
dir = parentDir;
@@ -710,7 +710,7 @@ KFileItem KCoreDirListerCache::itemForUrl(const QUrl &url) const
KCoreDirListerCache::DirItem *KCoreDirListerCache::dirItemForUrl(const QUrl &dir) const
{
- const QUrl url = dir.adjusted(QUrl::StripTrailingSlash);
+ const QUrl url = cleanUpTrailingSlash(dir);
DirItem *item = itemsInUse.value(url);
if (!item) {
item = itemsCached[url];
@@ -748,9 +748,9 @@ KFileItem KCoreDirListerCache::findByName(const KCoreDirLister *lister, const QS
KFileItem KCoreDirListerCache::findByUrl(const KCoreDirLister *lister, const QUrl &_u) const
{
QUrl url(_u);
- url = url.adjusted(QUrl::StripTrailingSlash);
+ url = cleanUpTrailingSlash(url);
- const QUrl parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ const QUrl parentDir = cleanUpTrailingSlash(url.adjusted(QUrl::RemoveFilename));
DirItem *dirItem = dirItemForUrl(parentDir);
if (dirItem) {
// If lister is set, check that it contains this dir
@@ -816,7 +816,7 @@ void KCoreDirListerCache::slotFilesRemoved(const QList<QUrl> &fileList)
}
}
- const QUrl parentDir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ const QUrl parentDir = cleanUpTrailingSlash(url.adjusted(QUrl::RemoveFilename));
const QList<QUrl> parentDirUrls = directoriesForCanonicalPath(parentDir);
for (const QUrl &dir : parentDirUrls) {
DirItem *dirItem = dirItemForUrl(dir);
@@ -872,7 +872,7 @@ void KCoreDirListerCache::slotFilesChanged(const QStringList &fileList) // from
pendingRemoteUpdates.insert(fileitem);
// For remote files, we won't be able to figure out the new information,
// we have to do a update (directory listing)
- const QUrl dir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ const QUrl dir = cleanUpTrailingSlash(url.adjusted(QUrl::RemoveFilename));
if (!dirsToUpdate.contains(dir)) {
dirsToUpdate.prepend(dir);
}
@@ -897,7 +897,7 @@ void KCoreDirListerCache::slotFileRenamed(const QString &_src, const QString &_d
printDebug();
#endif
- QUrl oldurl = src.adjusted(QUrl::StripTrailingSlash);
+ QUrl oldurl = cleanUpTrailingSlash(src);
KFileItem fileitem = findByUrl(nullptr, oldurl);
if (fileitem.isNull()) {
qCDebug(KIO_CORE_DIRLISTER) << "Item not found:" << oldurl;
@@ -968,7 +968,7 @@ std::set<KCoreDirLister *> KCoreDirListerCache::emitRefreshItem(const KFileItem
{
qCDebug(KIO_CORE_DIRLISTER) << "old:" << oldItem.name() << oldItem.url() << "new:" << fileitem.name() << fileitem.url();
// Look whether this item was shown in any view, i.e. held by any dirlister
- const QUrl parentDir = oldItem.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ const QUrl parentDir = cleanUpTrailingSlash(oldItem.url().adjusted(QUrl::RemoveFilename));
DirectoryDataHash::iterator dit = directoryData.find(parentDir);
std::set<KCoreDirLister *> listers;
if (dit != directoryData.end()) {
@@ -994,7 +994,7 @@ std::set<KCoreDirLister *> KCoreDirListerCache::emitRefreshItem(const KFileItem
lister->d->rootFileItem = fileitem;
lister->d->addRefreshItem(directoryUrl, oldRootItem, fileitem);
} else {
- directoryUrl = directoryUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ directoryUrl = cleanUpTrailingSlash(directoryUrl.adjusted(QUrl::RemoveFilename));
lister->d->addRefreshItem(directoryUrl, oldItem, fileitem);
}
}
@@ -1027,7 +1027,7 @@ QList<QUrl> KCoreDirListerCache::directoriesForCanonicalPath(const QUrl &dir) co
void KCoreDirListerCache::slotFileDirty(const QString &path)
{
qCDebug(KIO_CORE_DIRLISTER) << path;
- QUrl url = QUrl::fromLocalFile(path).adjusted(QUrl::StripTrailingSlash);
+ QUrl url = cleanUpTrailingSlash(QUrl::fromLocalFile(path));
// File or dir?
bool isDir;
const KFileItem item = itemForUrl(url);
@@ -1049,7 +1049,7 @@ void KCoreDirListerCache::slotFileDirty(const QString &path)
}
}
// Also do this for dirs, e.g. to handle permission changes
- const QList<QUrl> urls = directoriesForCanonicalPath(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
+ const QList<QUrl> urls = directoriesForCanonicalPath(cleanUpTrailingSlash(url.adjusted(QUrl::RemoveFilename)));
for (const QUrl &dir : urls) {
QUrl aliasUrl(dir);
aliasUrl.setPath(Utils::concatPaths(aliasUrl.path(), url.fileName()));
@@ -1090,7 +1090,7 @@ void KCoreDirListerCache::handleFileDirty(const QUrl &url)
{
// A file: do we know about it already?
const KFileItem &existingItem = findByUrl(nullptr, url);
- const QUrl dir = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+ const QUrl dir = cleanUpTrailingSlash(url.adjusted(QUrl::RemoveFilename));
if (existingItem.isNull()) {
// No - update the parent dir then
handleDirDirty(dir);
@@ -1112,7 +1112,7 @@ void KCoreDirListerCache::slotFileCreated(const QString &path) // from KDirWatch
// XXX: how to avoid a complete rescan here?
// We'd need to stat that one file separately and refresh the item(s) for it.
QUrl fileUrl(QUrl::fromLocalFile(path));
- itemsAddedInDirectory(fileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
+ itemsAddedInDirectory(cleanUpTrailingSlash(fileUrl.adjusted(QUrl::RemoveFilename)));
}
void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch
@@ -1121,7 +1121,7 @@ void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch
const QString fileName = QFileInfo(path).fileName();
QUrl dirUrl(QUrl::fromLocalFile(path));
QStringList fileUrls;
- const QList<QUrl> urls = directoriesForCanonicalPath(dirUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
+ const QList<QUrl> urls = directoriesForCanonicalPath(cleanUpTrailingSlash(dirUrl.adjusted(QUrl::RemoveFilename)));
for (const QUrl &url : urls) {
QUrl urlInfo(url);
urlInfo.setPath(Utils::concatPaths(urlInfo.path(), fileName));
@@ -1133,7 +1133,7 @@ void KCoreDirListerCache::slotFileDeleted(const QString &path) // from KDirWatch
void KCoreDirListerCache::slotEntries(KIO::Job *job, const KIO::UDSEntryList &entries)
{
QUrl url(joburl(static_cast<KIO::ListJob *>(job)));
- url = url.adjusted(QUrl::StripTrailingSlash);
+ url = cleanUpTrailingSlash(url);
qCDebug(KIO_CORE_DIRLISTER) << "new entries for " << url;
@@ -1245,7 +1245,7 @@ void KCoreDirListerCache::slotResult(KJob *j)
runningListJobs.remove(job);
QUrl jobUrl(joburl(job));
- jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections
+ jobUrl = cleanUpTrailingSlash(jobUrl); // need remove trailing slashes again, in case of redirections
qCDebug(KIO_CORE_DIRLISTER) << "finished listing" << jobUrl;
@@ -1337,8 +1337,8 @@ void KCoreDirListerCache::slotRedirection(KIO::Job *j, const QUrl &url)
QUrl newUrl(url);
// strip trailing slashes
- oldUrl = oldUrl.adjusted(QUrl::StripTrailingSlash);
- newUrl = newUrl.adjusted(QUrl::StripTrailingSlash);
+ oldUrl = cleanUpTrailingSlash(oldUrl);
+ newUrl = cleanUpTrailingSlash(newUrl);
if (oldUrl == newUrl) {
qCDebug(KIO_CORE_DIRLISTER) << "New redirection url same as old, giving up.";
@@ -1546,7 +1546,7 @@ void KCoreDirListerCache::renameDir(const QUrl &oldUrl, const QUrl &newUrl)
// Update URL in dir item and in itemsInUse
dir->redirect(newDirUrl);
- itemsToChange.emplace_back(oldDirUrl.adjusted(QUrl::StripTrailingSlash), newDirUrl.adjusted(QUrl::StripTrailingSlash), dir);
+ itemsToChange.emplace_back(cleanUpTrailingSlash(oldDirUrl), cleanUpTrailingSlash(newDirUrl), dir);
// Rename all items under that dir
// If all items of the directory change the same part of their url, the order is not
// changed, therefore just change it in the list.
@@ -1590,8 +1590,8 @@ void KCoreDirListerCache::renameDir(const QUrl &oldUrl, const QUrl &newUrl)
void KCoreDirListerCache::emitRedirections(const QUrl &_oldUrl, const QUrl &_newUrl)
{
qCDebug(KIO_CORE_DIRLISTER) << _oldUrl << "->" << _newUrl;
- const QUrl oldUrl = _oldUrl.adjusted(QUrl::StripTrailingSlash);
- const QUrl newUrl = _newUrl.adjusted(QUrl::StripTrailingSlash);
+ const QUrl oldUrl = cleanUpTrailingSlash(_oldUrl);
+ const QUrl newUrl = cleanUpTrailingSlash(_newUrl);
KIO::ListJob *job = jobForUrl(oldUrl);
if (job) {
@@ -1664,7 +1664,7 @@ void KCoreDirListerCache::slotUpdateResult(KJob *j)
KIO::ListJob *job = static_cast<KIO::ListJob *>(j);
QUrl jobUrl(joburl(job));
- jobUrl = jobUrl.adjusted(QUrl::StripTrailingSlash); // need remove trailing slashes again, in case of redirections
+ jobUrl = cleanUpTrailingSlash(jobUrl); // need remove trailing slashes again, in case of redirections
qCDebug(KIO_CORE_DIRLISTER) << "finished update" << jobUrl;
@@ -1860,7 +1860,7 @@ KIO::ListJob *KCoreDirListerCache::jobForUrl(const QUrl &url, KIO::ListJob *not_
{
for (auto it = runningListJobs.cbegin(); it != runningListJobs.cend(); ++it) {
KIO::ListJob *job = it.key();
- const QUrl jobUrl = joburl(job).adjusted(QUrl::StripTrailingSlash);
+ const QUrl jobUrl = cleanUpTrailingSlash(joburl(job));
if (jobUrl == url && job != not_job) {
return job;
@@ -1927,7 +1927,7 @@ void KCoreDirListerCache::deleteDir(const QUrl &_dirUrl)
// Idea: tell all the KCoreDirListers that they should forget the dir
// and then remove it from the cache.
- QUrl dirUrl(_dirUrl.adjusted(QUrl::StripTrailingSlash));
+ QUrl dirUrl(cleanUpTrailingSlash(_dirUrl));
// Separate itemsInUse iteration and calls to forgetDirs (which modify itemsInUse)
QList<QUrl> affectedItems;
@@ -2817,6 +2817,20 @@ KCoreDirListerCache::CacheHiddenFile *KCoreDirListerCache::cachedDotHiddenForDir
return {};
}
+QUrl KCoreDirListerCache::cleanUpTrailingSlash(const QUrl &url) const
+{
+ // Url is just a scheme or it's local, we can return it with regular clean
+ if (url.path().isEmpty() || url.isLocalFile()) {
+ return url.adjusted(QUrl::StripTrailingSlash);
+ }
+ QString cleanedPath = url.adjusted(QUrl::StripTrailingSlash).toString();
+ if (cleanedPath.endsWith(QLatin1Char('/'))) {
+ cleanedPath.chop(1);
+ }
+
+ return QUrl(cleanedPath);
+}
+
QList<KCoreDirLister *> KCoreDirListerCacheDirectoryData::listersByStatus(ListerStatus status) const
{
QList<KCoreDirLister *> listers;
diff --git a/src/core/kcoredirlister_p.h b/src/core/kcoredirlister_p.h
index e4b931fac0786dcb962fa73e80e8044dd3023adb..cc2d1826a6d79b524eca2f25067b7ae5f8d3bb5d 100644
--- a/src/core/kcoredirlister_p.h
+++ b/src/core/kcoredirlister_p.h
@@ -374,6 +374,16 @@ private:
*/
CacheHiddenFile *cachedDotHiddenForDir(const QString &dir);
+ /*! Due to QTBUG-35921 we sometimes get a trailing slash even
+ * if we expect it to be removed with remote files, since
+ * on remote file systems we do not know if foo/bar/ and foo/bar is the same
+ * item or not.
+ * This makes sure the trailing slash is completely removed from @p url
+ * and returns the cleaned url on remote files. On local files it
+ * runs the default Qt::StripTrailingSlash adjustment.
+ */
+ QUrl cleanUpTrailingSlash(const QUrl &url) const;
+
#ifndef NDEBUG
void printDebug();
#endif
あちらこちらで QUrl::adjusted(QUrl::StripTrailingSlash) で末尾のスラッシュの除去を試みているけど、それでもなお末尾にスラッシュが残る場合があるから KCoreDirListerCache::cleanUpTrailingSlash というラッパーを用意し完全にスラッシュを取り除くというコード。
ここから読み取れることを並べてみると
QUrl jobUrlは警告メッセージ中に出力されてる通り末尾にスラッシュがついてる- おそらく
QHash<QUrl, DirItem*> itemsInUseの中はスラッシュ無しのQUrl("smb://Administrator@hpmsrv01.local")が入ってる QUrlの==オペレータは末尾スラッシュありとなしを等価として扱わない- よって
QHash::valueはキーQUrl jobUrlを持つ値が存在しないとして nullptr を返す - 死~ん
ちゅーことで、等価になるように完全に末尾スラッシュを取り除くと動作するってことか。
ところが Bug 507278 の修正では KCoreDirListerCache::cleanUpTrailingSlash は QUrl::adjusted(QUrl::StripTrailingSlash) の結果を返すだけになってるので実質 revert なんよなこれ。
diff --git a/src/core/kcoredirlister.cpp b/src/core/kcoredirlister.cpp
index 0968ecec0343d5058bc7c6d482f7b20b7efa94ea..87c41c8e9d88f72bd383e2dc3572a4843e703ae5 100644
--- a/src/core/kcoredirlister.cpp
+++ b/src/core/kcoredirlister.cpp
@@ -1391,16 +1391,19 @@ void KCoreDirListerCache::slotRedirection(KIO::Job *j, const QUrl &url)
const QList<KCoreDirLister *> allListers = listers + holders;
DirItem *newDir = itemsInUse.value(newUrl);
- if (newDir) {
+
+ // get the job if one's running for newUrl already (can be a list-job or an update-job), but
+ // do not return this 'job', which would happen because of the use of redirectionURL()
+ // This is nullptr if the url has the same job for the given parameter to this slot.
+ KIO::ListJob *oldJob = jobForUrl(newUrl, job);
+
+ // Do not list files again if the jobForUrl is the current job.
+ if (newDir && oldJob) {
qCDebug(KIO_CORE_DIRLISTER) << newUrl << "already in use";
// only in this case there can newUrl already be in listersCurrentlyListing or listersCurrentlyHolding
delete dir;
- // get the job if one's running for newUrl already (can be a list-job or an update-job), but
- // do not return this 'job', which would happen because of the use of redirectionURL()
- KIO::ListJob *oldJob = jobForUrl(newUrl, job);
-
// listers of newUrl with oldJob: forget about the oldJob and use the already running one
// which will be converted to an updateJob
KCoreDirListerCacheDirectoryData &newDirData = directoryData[newUrl];
@@ -2819,16 +2822,7 @@ KCoreDirListerCache::CacheHiddenFile *KCoreDirListerCache::cachedDotHiddenForDir
QUrl KCoreDirListerCache::cleanUpTrailingSlash(const QUrl &url) const
{
- // Url is just a scheme or it's local, we can return it with regular clean
- if (url.path().isEmpty() || url.isLocalFile()) {
- return url.adjusted(QUrl::StripTrailingSlash);
- }
- QString cleanedPath = url.adjusted(QUrl::StripTrailingSlash).toString();
- if (cleanedPath.endsWith(QLatin1Char('/'))) {
- cleanedPath.chop(1);
- }
-
- return QUrl(cleanedPath);
+ return url.adjusted(QUrl::StripTrailingSlash);
}
QList<KCoreDirLister *> KCoreDirListerCacheDirectoryData::listersByStatus(ListerStatus status) const
そもそも末尾スラッシュが残るって Qt 側の実装どうなっとんねんこれ。
939 inline void QUrlPrivate::appendPath(QString &appendTo, QUrl::FormattingOptions options, Section appendingTo) const
940 {
941 QString thePath = path;
942 if (options & QUrl::NormalizePathSegments)
943 normalizePathSegments(&thePath);
944
945 QStringView thePathView(thePath);
...
952 // check if we need to remove trailing slashes
953 if (options & QUrl::StripTrailingSlash) {
954 while (thePathView.size() > 1 && thePathView.endsWith(u'/'))
955 thePathView.chop(1);
956 }
...
960 }
...
2893 QUrl QUrl::adjusted(QUrl::FormattingOptions options) const
2894 {
...
2918 } else if (auto pathOpts = options & (StripTrailingSlash | RemoveFilename | NormalizePathSegments)) {
2919 that.detach();
2920 that.d->path.resize(0);
2921 d->appendPath(that.d->path, pathOpts, QUrlPrivate::Path);
2922 }
...
2929 }
ポイントは 954 行目、末尾スラッシュを削るループ条件に thePathView.size() > 1 とある。
QString::size は UTF-16 の文字数を返すのでルートの / は削られず残ることになりますな。
そんでこの残った / をさらに削ろうとしたのが KCoreDirListerCache::cleanUpTrailingSlash ちゅうこと。
そんじゃなんで QUrl はルートだけ特別扱いしてるのか考察してみようか。
プロトコルが HTTP の場合、末尾スラッシュは空のパスを表すので http://example.com/foo と http://example.com/foo/ は別のリソースを示す。
だけど
Apache HTTP Server
や
Nginx
はデフォルトで http://example.com/foo へのアクセスは http://example.com/foo/ にリダイレクトするから、両者は等価だと思ってる人が多そうなんだな。
まあ
RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax
にすら実質等価のようなものって書いてあるしな。
6.2.4. Protocol-Based Normalization
Substantial effort to reduce the incidence of false negatives is
often cost-effective for web spiders. Therefore, they implement even
more aggressive techniques in URI comparison. For example, if they
observe that a URI such as
http://example.com/data
redirects to a URI differing only in the trailing slash
http://example.com/data/
they will likely regard the two as equivalent in the future. This
kind of technique is only appropriate when equivalence is clearly
indicated by both the result of accessing the resources and the
common conventions of their scheme's dereference algorithm (in this
case, use of redirection by HTTP origin servers to avoid problems
with relative references).
しかし等価として扱うのは
GET /foo HTTP/1.1
Host: example.com
...
という要求投げて
HTTP/1.1 301 Moved Permanently
...
Location: http://example.com/foo/
という応答で返された Location であってだな。
にもかかわらずリクエスト投げずに末尾スラッシュは無条件で削って URL Canonicalize したぜ!って悪癖でも蔓延ってるんですかね。
QUrl::adjusted(QUrl::StripTrailingSlash) の存在価値って手抜き以外に見出せないんですけど。
そうは問屋が卸さなくて Apache HTTP Server は設定で
DirectorySlash Off
を指定すればこのリダイレクトを無効にできるし、ついでに
DirectoryIndex index.html
Options Indexes
を指定すれば
- スラッシュあり … index.html
- スラッシュなし … ディレクトリの内容一覧
と別のコンテンツを表示することも可能なんだよね、まぁ マニュアル ではセキュリティ警告で意図しない情報漏洩が発生する例として挙げられてるから今時誰もやらんだろうけど。
そんでこのリダイレクト無効にはひとつ例外ケースがあって http://example.com と http://example.com/ つまりルートだけには効かないんですな。
$ telnet example.com 80
Trying ::1...
Connected to example.com.
Escape character is '^]'.
GET
HTTP/1.1 400 Bad Request
Date: Fri, 12 Sep 2025 02:10:46 GMT
Server: Apache
Content-Length: 226
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body></html>
Connection closed by foreign host.
このように http://example.com だと GET の引数が存在しないのでそのままだと 400 Bad Request になってしまう。
なのでアドレスバーに http://example.com が入力された場合に http://example.com/ に補完するのはブラウザの責任なわけ。
$ telnet example.com 80
Trying ::1...
Connected to example.com.
Escape character is '^]'.
GET /
<html><body><h1>It works!</h1></body></html>
Connection closed by foreign host.
空のクエリ ? だけ渡すと Apache HTTP Server は / を補完してくれるみたい。
ディレクトリの内容一覧でなく index.html が返ってくるけれども。
$ telnet example.com 80
Trying ::1...
Connected to example.com.
Escape character is '^]'.
GET ?
<html><body><h1>It works!</h1></body></html>
Connection closed by foreign host.
Nginx は普通に 400 Bad Request 返してくるけれど。
$ telnet example.com 80
Trying ::1...
Connected to example.com.
Escape character is '^]'.
GET ?
HTTP/1.1 400 Bad Request
Server: nginx/1.29.1
Date: Fri, 12 Sep 2025 02:12:06 GMT
Content-Type: text/html
Content-Length: 157
Connection: close
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.29.1</center>
</body>
</html>
Connection closed by foreign host.
ということでサーバーの設定無関係に http://example.com と http://example.com/ は必ず同じリソースを示すから等価と評価していいということ。
この例外があるから QUrl はルートの / を削らないのだと思う。
Perl の URI::URL なんかもパスが未定義あるいは空文字列なら $url->path は / 返してくるし。
ただ URL 仕様ではパスは空であっても有効だから削らなかったり / を返すのはバグなのでは?という気がしないでもない。
3.3. Path
If a URI contains an authority component, then the path component
must either be empty or begin with a slash ("/") character.
そもそも URL Canonicalize するならスラッシュありが正しいんだけどな、相対パスの起点変わっちゃうからね。
それに最初に但し書きした通りこれはプロトコルが HTTP の場合に限定した話なのである。
他のプロトコルではルートであっても '' と / が別のリソースを示したとしても不思議ではないのだ。
ということで SMB/CIFS の仕様にも触れていくわけだが、Windows においてリソースロケーションは
\\<NetBIOS名>\<共有名>\<ディレクトリのパス名>
つまり UNC(Universal Naming Convention) で記述するんだけど、Dolphin/KIO はこいつそのまま使うのではなく
smb://<NetBIOS名>/<共有名>/<ディレクトリのパス名>
つまり URL(Unified Resource Location) に翻訳したものを使う。 こいつは Java による SMB/CIFS 実装 JCIFS が発祥なんですかね、Internet Draft の SMB File Sharing URI Scheme を書いたのも JCIFS 作者のひとりみたいだし。
そんでやはり Java で書かれた Apache Commons Virtual File System という KIO によく似たユーザースペース VFS 実装もリソースロケーションは URL を採用している。
まあインターネットブラウザでローカルファイルを file:// で参照したあたりからの流れだろう。
ちなみに Internet Explorer では SMB/CIFS も
file:////<NetBIOS名>/<共有名>/<ディレクトリのパス名>
で開くことができた。
Dolphin も \\ や file://// を入力してやると smb:// に置き換えて頑張ろうとするのだが、ファイル共有の一覧がみれなかったり認証エラーになったりいろいろ挙動がおかしくてバグの宝庫の扉を開けてしまった感がある。
そんで jcifs.smb.SmbFile クラスの Javadoc を読むとわざわざ赤字で
Important: all SMB URLs that represent workgroups, servers, shares, or directories require a trailing slash '/'.
と末尾スラッシュは必須とあるんですわ。
Javadoc にも Internet Draft にも Why? が書かれていないのだが、まあこれは前述の通り末尾スラッシュの有無で相対パスの起点が変わるから厳格に必須としたんだろう。
こうやってプロトコル間の差異を考えてくと QUrl はインタフェースか抽象クラスとしてプロトコル毎に QHttpUrl とか QSmbUrl と実装書いてく方がよくねとなるのだが、話が大事になるから考えないこととする。
KIO は VFS だからプロトコル非依存なコードなのに、あちこちで HTTP のノリで末尾スラッシュを削る処理をやってる時点でとても筋の悪いコードに見えるのが、まあここはもっとちゃんとコード読まないと判らんな。
次回はそもそも QHash<QUrl, DirItem*> itemsInUse は何を管理してるのか確認するとこからか。
というか KCoreDirLister クラスが何者なのかすらまだ把握してねえ。
Dolphin と Konqueror のコードに辿り着くの当分先になりそうだし、できればその間に上流で修正されてほしいものである。