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 *dirnullptr-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::cleanUpTrailingSlashQUrl::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/foohttp://example.com/foo/ は別のリソースを示す。

だけど Apache HTTP ServerNginx はデフォルトで 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.comhttp://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.comhttp://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 のコードに辿り着くの当分先になりそうだし、できればその間に上流で修正されてほしいものである。