From be8f2a274bdef0a24306774fc349256ad097a9f5 Mon Sep 17 00:00:00 2001 From: liyigang Date: Wed, 24 Jun 2026 17:25:28 +0800 Subject: [PATCH 01/10] perf(dfm-io): cache statx results in DFileInfoPrivate::attributesBySelf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce ensureStatxCached() to consolidate six independent statx syscalls into one. Cache the result in mutable member fields so that subsequent time-attribute queries reuse the same buffer. 在 attributesBySelf 中引入 ensureStatxCached() 方法,将 6 个时间 属性的重复 statx 系统调用合并为一次,结果缓存后各 case 直接读取。 Log: 优化 attributesBySelf 中 statx 调用,消除重复系统调用 Task: https://pms.uniontech.com/task-view-391003.html Influence: 查询 kTimeCreated/Modified/Access 等属性时,statx 从最多 6 次降为 1 次,减少不必要的系统调用开销。 --- src/dfm-io/dfm-io/dfileinfo.cpp | 79 ++++++++++--------------- src/dfm-io/dfm-io/private/dfileinfo_p.h | 7 +++ 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/src/dfm-io/dfm-io/dfileinfo.cpp b/src/dfm-io/dfm-io/dfileinfo.cpp index 25533406..ceb1e97c 100644 --- a/src/dfm-io/dfm-io/dfileinfo.cpp +++ b/src/dfm-io/dfm-io/dfileinfo.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include @@ -281,6 +280,24 @@ void DFileInfoPrivate::queryInfoAsync(int ioPriority, DFileInfo::InitQuerierAsyn g_file_query_info_async(this->gfile, attributes, GFileQueryInfoFlags(flag), ioPriority, gcancellable, queryInfoAsyncCallback, dataOp); } +bool DFileInfoPrivate::ensureStatxCached() const +{ + if (statxCached) + return statxValid; + + statxCached = true; + + const QUrl &url = q->uri(); + if (!url.isLocalFile()) + return false; + + unsigned mask = STATX_BASIC_STATS | STATX_BTIME; + int ret = statx(AT_FDCWD, url.path().toStdString().data(), + AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuf); + statxValid = (ret == 0); + return statxValid; +} + QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) { QVariant retValue; @@ -295,16 +312,10 @@ QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) return QVariant(); uint64_t ret = g_file_info_get_attribute_uint64(gfileinfo, key.c_str()); if (ret == 0) { - struct statx statxBuffer; - unsigned mask = STATX_BASIC_STATS | STATX_BTIME; - const QUrl &url = q->uri(); - if (!url.isLocalFile()) - return QVariant(); - int ret = statx(AT_FDCWD, url.path().toStdString().data(), AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuffer); - if (ret != 0) + if (!ensureStatxCached()) return QVariant(); - return quint64(statxBuffer.stx_btime.tv_sec); + return quint64(statxBuf.stx_btime.tv_sec); } return qulonglong(ret); } @@ -314,16 +325,10 @@ QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) return QVariant(); uint32_t ret = g_file_info_get_attribute_uint32(gfileinfo, key.c_str()); if (ret == 0) { - struct statx statxBuffer; - unsigned mask = STATX_BASIC_STATS | STATX_BTIME; - const QUrl &url = q->uri(); - if (!url.isLocalFile()) - return QVariant(); - int ret = statx(AT_FDCWD, url.path().toStdString().data(), AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuffer); - if (ret != 0) + if (!ensureStatxCached()) return QVariant(); - return statxBuffer.stx_btime.tv_nsec / 1000000; + return statxBuf.stx_btime.tv_nsec / 1000000; } return QVariant(ret); } @@ -333,16 +338,10 @@ QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) return QVariant(); uint64_t ret = g_file_info_get_attribute_uint64(gfileinfo, key.c_str()); if (ret == 0) { - struct statx statxBuffer; - unsigned mask = STATX_BASIC_STATS | STATX_BTIME; - const QUrl &url = q->uri(); - if (!url.isLocalFile()) - return QVariant(); - int ret = statx(AT_FDCWD, url.path().toStdString().data(), AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuffer); - if (ret != 0) + if (!ensureStatxCached()) return QVariant(); - return quint64(statxBuffer.stx_mtime.tv_sec); + return quint64(statxBuf.stx_mtime.tv_sec); } return qulonglong(ret); } @@ -352,16 +351,10 @@ QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) return QVariant(); uint32_t ret = g_file_info_get_attribute_uint32(gfileinfo, key.c_str()); if (ret == 0) { - struct statx statxBuffer; - unsigned mask = STATX_BASIC_STATS | STATX_BTIME; - const QUrl &url = q->uri(); - if (!url.isLocalFile()) - return QVariant(); - int ret = statx(AT_FDCWD, url.path().toStdString().data(), AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuffer); - if (ret != 0) + if (!ensureStatxCached()) return QVariant(); - return statxBuffer.stx_mtime.tv_nsec / 1000000; + return statxBuf.stx_mtime.tv_nsec / 1000000; } return QVariant(ret); } @@ -371,16 +364,10 @@ QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) return QVariant(); uint64_t ret = g_file_info_get_attribute_uint64(gfileinfo, key.c_str()); if (ret == 0) { - struct statx statxBuffer; - unsigned mask = STATX_BASIC_STATS | STATX_BTIME; - const QUrl &url = q->uri(); - if (!url.isLocalFile()) - return QVariant(); - int ret = statx(AT_FDCWD, url.path().toStdString().data(), AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuffer); - if (ret != 0) + if (!ensureStatxCached()) return QVariant(); - return quint64(statxBuffer.stx_atime.tv_sec); + return quint64(statxBuf.stx_atime.tv_sec); } return qulonglong(ret); } @@ -390,16 +377,10 @@ QVariant DFileInfoPrivate::attributesBySelf(DFileInfo::AttributeID id) return QVariant(); uint32_t ret = g_file_info_get_attribute_uint32(gfileinfo, key.c_str()); if (ret == 0) { - struct statx statxBuffer; - unsigned mask = STATX_BASIC_STATS | STATX_BTIME; - const QUrl &url = q->uri(); - if (!url.isLocalFile()) - return QVariant(); - int ret = statx(AT_FDCWD, url.path().toStdString().data(), AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT, mask, &statxBuffer); - if (ret != 0) + if (!ensureStatxCached()) return QVariant(); - return statxBuffer.stx_atime.tv_nsec / 1000000; + return statxBuf.stx_atime.tv_nsec / 1000000; } return QVariant(ret); } diff --git a/src/dfm-io/dfm-io/private/dfileinfo_p.h b/src/dfm-io/dfm-io/private/dfileinfo_p.h index 348ed6df..1547a6b9 100644 --- a/src/dfm-io/dfm-io/private/dfileinfo_p.h +++ b/src/dfm-io/dfm-io/private/dfileinfo_p.h @@ -19,6 +19,7 @@ #include #include +#include BEGIN_IO_NAMESPACE @@ -55,6 +56,7 @@ class DFileInfoPrivate : public QObject, public QSharedData QVariant attributesBySelf(DFileInfo::AttributeID id); QVariant attributesFromUrl(DFileInfo::AttributeID id); void checkAndResetCancel(); + [[nodiscard]] bool ensureStatxCached() const; [[nodiscard]] DFileFuture *initQuerierAsync(int ioPriority, QObject *parent = nullptr) const; [[nodiscard]] QFuture refreshAsync(); @@ -94,6 +96,11 @@ class DFileInfoPrivate : public QObject, public QSharedData std::atomic_bool stoped { false }; std::atomic_bool fileExists { false }; QMap caches; + + // statx 缓存:6 个时间属性 case 共享一次 statx 调用,避免重复系统调用 + mutable struct statx statxBuf {}; + mutable bool statxCached { false }; + mutable bool statxValid { false }; std::atomic_bool cacheing { false }; std::atomic_bool refreshing { false }; QMutex mutex; From fd494fb66b7a2ae7bf45209669a0c2cbcfc330fd Mon Sep 17 00:00:00 2001 From: liyigang Date: Tue, 23 Jun 2026 20:00:03 +0800 Subject: [PATCH 02/10] fix: prevent BOM character loss in path concatenation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use std::string for directory path concatenation to avoid QString's normalization of UTF-8 BOM (U+FEFF / zero-width no-break space). 使用 std::string 进行路径拼接,避免 QString 对 UTF-8 BOM (零宽不换行空格) 的规范化导致字节丢失。 Log: 修复路径拼接时 BOM 字符丢失的问题 Bug: https://pms.uniontech.com//bug-view-367075.html Influence: 修复后包含 BOM/零宽不换行空格的路径能正确拼接,避免文件操作失败。 --- src/dfm-io/dfm-io/denumerator.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/dfm-io/dfm-io/denumerator.cpp b/src/dfm-io/dfm-io/denumerator.cpp index 6c129566..36531169 100644 --- a/src/dfm-io/dfm-io/denumerator.cpp +++ b/src/dfm-io/dfm-io/denumerator.cpp @@ -446,12 +446,29 @@ char *DEnumeratorPrivate::filePath(const QUrl &url) QUrl DEnumeratorPrivate::buildUrl(const QUrl &url, const char *fileName) { - QString path; - if (url.path() == "/") { - path = "/" + QString(fileName); + // 防御空指针,避免std::string或QByteArray构造时崩溃 + if (!fileName) { + return QUrl(); + } + + // 拦截路径遍历攻击,防止恶意文件名越权 + QByteArray fileNameBa(fileName); + if (fileNameBa.contains('/') || fileNameBa.contains('\\') || fileNameBa == "." || fileNameBa == "..") { + return QUrl(); + } + + QByteArray path; + QString urlPath = url.path(); + + if (urlPath == "/" || urlPath.isEmpty()) { + path = QByteArray("/") + fileNameBa; } else { - QString dirPath = url.path(); - path = dirPath.endsWith('/') ? dirPath + QString(fileName) : dirPath + "/" + QString(fileName); + QByteArray dirPath = urlPath.toUtf8(); + if (!dirPath.endsWith('/')) { + dirPath.append('/'); + } + // 使用QByteArray进行底层字节数组拼接,避免QString剥离BOM (efbbbf) + path = dirPath + fileNameBa; } // 保留原始 URL 的 scheme 和 host,而不是假定为本地文件 From a18cf1ab6ffdd6193ecad10b1aad59fcc17b3f8d Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Mon, 13 Jul 2026 14:59:17 +0800 Subject: [PATCH 03/10] feat: add dvd+rw-format support for DVD RW erase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Added new DVDRwFormatEngine class to handle DVD RW erasure using dvd+rw-tools 2. Modified control files to add dvd+rw-tools as a dependency 3. Updated DOpticalDiscManager to detect DVD±RW media and use new engine when available 4. Implemented progress parsing for dvd+rw-format's special output format 5. Added fallback to xorriso when dvd+rw-format is not available Log: Added support for DVD±RW erasure using dvd+rw-format tool Influence: 1. Test DVD+RW and DVD-RW erasure with dvd+rw-tools installed 2. Verify fallback to xorriso when dvd+rw-tools is missing 3. Check progress reporting during erase operations 4. Test with different DVD RW media types 5. Verify no regression in CD/DVD non-RW erase functionality feat: 添加对DVD RW擦除的dvd+rw-format支持 1. 新增DVDRwFormatEngine类用于通过dvd+rw-tools处理DVD RW擦除 2. 修改控制文件添加dvd+rw-tools作为依赖项 3. 更新DOpticalDiscManager以检测DVD±RW介质并在可用时使用新引擎 4. 实现针对dvd+rw-format特殊输出格式的进度解析 5. 添加当dvd+rw-format不可用时回退到xorriso的处理 Log: 新增使用dvd+rw-format工具进行DVD±RW擦除的支持 Influence: 1. 测试dvd+rw-tools安装时的DVD+RW和DVD-RW擦除功能 2. 验证dvd+rw-tools缺失时回退到xorriso的功能 3. 检查擦除过程中的进度报告 4. 使用不同类型的DVD RW介质进行测试 5. 验证普通CD/DVD非RW擦除功能无回归 --- debian/control.in | 3 +- .../dfm-burn-lib/dopticaldiscmanager.cpp | 31 ++++ .../private/dvdrwformatengine.cpp | 165 ++++++++++++++++++ .../dfm-burn-lib/private/dvdrwformatengine.h | 37 ++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.cpp create mode 100644 src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.h diff --git a/debian/control.in b/debian/control.in index 6335b292..21dcfec9 100644 --- a/debian/control.in +++ b/debian/control.in @@ -76,7 +76,8 @@ Section: libdevel Multi-Arch: same Depends: ${shlibs:Depends}, - ${misc:Depends} + ${misc:Depends}, + dvd+rw-tools Recommends: libudfburn Description: A library about burning CD. diff --git a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp index 270f06de..6860f76f 100644 --- a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp +++ b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp @@ -8,6 +8,7 @@ #include "private/dopticaldiscmanager_p.h" #include "private/dxorrisoengine.h" #include "private/dudfburnengine.h" +#include "private/dvdrwformatengine.h" #include "private/dsm3hash.h" #include @@ -144,6 +145,36 @@ bool DOpticalDiscManager::commit(const BurnOptions &opts, int speed, const QStri bool DOpticalDiscManager::erase() { bool ret { false }; + + // 查询介质类型 + MediaType mediaType { MediaType::kNoMedia }; + { + QScopedPointer info { DOpticalDiscManager::createOpticalInfo(dptr->curDev) }; + if (info) + mediaType = info->mediaType(); + } + + // DVD±RW 且 dvd+rw-format 可用时走新引擎 + bool isDvdRw = (mediaType == MediaType::kDVD_PLUS_RW || mediaType == MediaType::kDVD_RW); + QString dvdRwFormat = QStandardPaths::findExecutable("dvd+rw-format"); + + if (isDvdRw && !dvdRwFormat.isEmpty()) { + QScopedPointer engine { new DVDRwFormatEngine }; + connect( + engine.data(), &DVDRwFormatEngine::jobStatusChanged, this, + [this](JobStatus status, int progress) { + Q_EMIT jobStatusChanged(status, progress, {}, {}); + }, + Qt::DirectConnection); + + ret = engine->doErase(dptr->curDev, mediaType); + return ret; + } + + // 降级:走 xorriso 原路径 + if (isDvdRw) + qWarning() << "[dfm-burn] dvd+rw-format not found, falling back to xorriso for DVD±RW erase"; + QScopedPointer engine { new DXorrisoEngine }; connect( engine.data(), &DXorrisoEngine::jobStatusChanged, this, diff --git a/src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.cpp b/src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.cpp new file mode 100644 index 00000000..004fa275 --- /dev/null +++ b/src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.cpp @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dvdrwformatengine.h" + +#include +#include +#include +#include +#include + +DFM_BURN_USE_NS + +DVDRwFormatEngine::DVDRwFormatEngine(QObject *parent) + : QObject(parent) +{ +} + +DVDRwFormatEngine::~DVDRwFormatEngine() +{ +} + +bool DVDRwFormatEngine::doErase(const QString &dev, MediaType type) +{ + phase = 0; + lastRawProgress = 0; + + // 1. 校验设备路径 + QString cleanDev = QDir::cleanPath(dev); + if (!cleanDev.startsWith("/dev/sr")) { + qWarning() << "[dfm-burn] Invalid device path:" << dev; + Q_EMIT jobStatusChanged(JobStatus::kFailed, -1); + return false; + } + + // 2. 查找 dvd+rw-format 二进制 + QString binary = QStandardPaths::findExecutable("dvd+rw-format"); + if (binary.isEmpty()) { + qWarning() << "[dfm-burn] dvd+rw-format not found"; + Q_EMIT jobStatusChanged(JobStatus::kFailed, -1); + return false; + } + + // 3. 根据介质类型选择命令参数 + QStringList args; + if (type == MediaType::kDVD_PLUS_RW) { + args << "-force" + << "-gui" + << cleanDev; + } else if (type == MediaType::kDVD_RW) { + args << "-blank=full" + << "-gui" + << cleanDev; + } else { + qWarning() << "[dfm-burn] Unsupported media type for dvd+rw-format:" << static_cast(type); + Q_EMIT jobStatusChanged(JobStatus::kFailed, -1); + return false; + } + + Q_EMIT jobStatusChanged(JobStatus::kRunning, 0); + + // dvd+rw-format 进度输出机制分析(源码 dvd+rw-format.cpp): + // + // 1. fork() 后子进程 setsid() 执行 SCSI 操作,父进程安装 SIGALRM + // 定时轮询共享内存中的 progress 值(0~65536),用退格符覆盖式 + // 输出百分比到 stderr。 + // + // 2. -gui 标志的 gui_action 赋值在 fork 之后的子进程中(727 行), + // 父进程的 gui_action 始终为 NULL(129 行),所以 -gui 对父进程 + // 的进度输出格式无影响——父进程始终用 \b 退格覆盖方式。 + // + // 3. DVD+RW -force 路径有 3 个 wait_for_unit(progress) 调用 + // (FORMAT UNIT → STOP DE-ICING → CLOSE SESSION), + // 每个 SCSI 操作的进度指示器独立从 0 开始, + // 导致原始输出出现 3 个 0→100% 周期。 + // DVD-RW -blank 路径只有 1 个 wait_for_unit,单周期。 + // + // 4. 进度数据通过 stderr 管道传输。QProcess::readyReadStandardOutput + // 信号在 QEventLoop + MergedChannels 下可能不能及时触发(尤其在 + // DVD-RW 单阶段输出稀疏时)。改用 waitForReadyRead 主动轮询 + // 管道——其内部调用 processEvents 处理 Qt 信号,进度可实时传递。 + + int totalPhases = (type == MediaType::kDVD_PLUS_RW) ? 3 : 1; + + QProcess proc; + proc.setProcessChannelMode(QProcess::MergedChannels); + + QByteArray buffer; + + proc.start(binary, args); + if (!proc.waitForStarted()) { + qWarning() << "[dfm-burn] Failed to start dvd+rw-format"; + Q_EMIT jobStatusChanged(JobStatus::kFailed, -1); + return false; + } + + // 主动轮询管道,不依赖 readyReadStandardOutput 信号。 + // waitForReadyRead 内部调用 processEvents,emit 的信号能被上层接收。 + const int pollInterval { 200 }; // ms + + while (proc.state() == QProcess::Running) { + proc.waitForReadyRead(pollInterval); + + if (proc.bytesAvailable() > 0) { + buffer += proc.readAllStandardOutput(); + parseProgress(buffer, totalPhases); + if (buffer.size() > 1024) + buffer = buffer.right(512); + } + } + + // 读取进程退出后管道中的剩余数据 + if (proc.bytesAvailable() > 0) { + buffer += proc.readAllStandardOutput(); + parseProgress(buffer, totalPhases); + } + + // 检查退出结果 + if (proc.exitStatus() == QProcess::CrashExit) { + qWarning() << "[dfm-burn] dvd+rw-format crashed"; + Q_EMIT jobStatusChanged(JobStatus::kFailed, -1); + return false; + } + + int exitCode = proc.exitCode(); + if (exitCode == 0) { + Q_EMIT jobStatusChanged(JobStatus::kFinished, 100); + return true; + } else { + qWarning() << "[dfm-burn] dvd+rw-format exited with code" << exitCode; + Q_EMIT jobStatusChanged(JobStatus::kFailed, -1); + return false; + } +} + +void DVDRwFormatEngine::parseProgress(const QByteArray &data, int totalPhases) +{ + // dvd+rw-format 父进程用退格符覆盖式输出进度,格式如: + // 0.0%\b\b\b\b0.1%\b|\b/\b\b0.2% ... + // 也有部分行带换行符(子进程的诊断输出、父进程结束时的 \n)。 + // 在整个缓冲区中搜索最后一个 XX.X% 模式即可获取当前进度。 + static const QRegularExpression re("([0-9]+(?:\\.[0-9]+)?)%"); + auto matches = re.globalMatch(QString::fromLocal8Bit(data)); + QString lastPct; + while (matches.hasNext()) + lastPct = matches.next().captured(1); + if (lastPct.isEmpty()) + return; + + int rawProgress = static_cast(lastPct.toDouble()); + + // 检测相位切换:进度显著回退说明进入了下一个 SCSI 操作 + if (rawProgress < lastRawProgress - 5) { + phase++; + if (phase >= totalPhases) + phase = totalPhases - 1; + } + lastRawProgress = rawProgress; + + // 加权计算总体进度:(phase * 100 + rawProgress) / totalPhases + int overall = (phase * 100 + rawProgress) / totalPhases; + if (overall < 100) + Q_EMIT jobStatusChanged(JobStatus::kRunning, overall); +} diff --git a/src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.h b/src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.h new file mode 100644 index 00000000..40c76c47 --- /dev/null +++ b/src/dfm-burn/dfm-burn-lib/private/dvdrwformatengine.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DVDRWFORMATENGINE_H +#define DVDRWFORMATENGINE_H + +#include + +#include + +DFM_BURN_BEGIN_NS + +class DVDRwFormatEngine : public QObject +{ + Q_OBJECT + +public: + explicit DVDRwFormatEngine(QObject *parent = nullptr); + ~DVDRwFormatEngine() override; + + bool doErase(const QString &dev, MediaType type); + +Q_SIGNALS: + void jobStatusChanged(JobStatus status, int progress); + +private: + void parseProgress(const QByteArray &data, int totalPhases); + +private: + int phase { 0 }; + int lastRawProgress { 0 }; +}; + +DFM_BURN_END_NS + +#endif // DVDRWFORMATENGINE_H From d031f8ec0a51e990166877b09f077aff94b6b6f3 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Fri, 24 Jul 2026 09:52:10 +0800 Subject: [PATCH 04/10] chore: bump version to 1.3.70 1.3.70 Log: --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 067b8746..dff1c0dd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +util-dfm (1.3.70) unstable; urgency=medium + + * perf(dfm-io): cache statx results in DFileInfoPrivate::attributesBySelf + * fix: prevent BOM character loss in path concatenation + * feat: add dvd+rw-format support for DVD RW erase + + -- Zhang Sheng Fri, 24 Jul 2026 09:51:44 +0800 + util-dfm (1.3.59) unstable; urgency=medium * fix: improve symlink handling in filename search From 496e59b4f74e14746433aa8e3cf2881a47a08c06 Mon Sep 17 00:00:00 2001 From: zhangsheng Date: Wed, 5 Aug 2026 15:03:27 +0800 Subject: [PATCH 05/10] fix: restrict path traversal check to file scheme in DEnumerator buildUrl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. DEnumeratorPrivate::buildUrl() 对所有 scheme 的文件名做路径遍历检查,导致 gio trash:/// 后端使用反斜杠分隔的扁平文件名被误判为恶意路径并返回空 URL; 2. 将路径遍历检查限制为仅对 file:/// 或无 scheme 的本地文件系统生效,gio trash:/// 等虚拟文件系统的合法文件名不受影响; 3. 修复手动分区场景下 /media 挂载点的回收站文件无法显示和清空的问题; Log: 修复 buildUrl 路径遍历安全检查误伤 gio trash 反斜杠文件名导致回收站无法显示和清空的问题 PMS: BUG-372733 Bug: https://pms.uniontech.com/bug-view-372733.html --- src/dfm-io/dfm-io/denumerator.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/dfm-io/dfm-io/denumerator.cpp b/src/dfm-io/dfm-io/denumerator.cpp index 36531169..ae8cf75e 100644 --- a/src/dfm-io/dfm-io/denumerator.cpp +++ b/src/dfm-io/dfm-io/denumerator.cpp @@ -451,10 +451,17 @@ QUrl DEnumeratorPrivate::buildUrl(const QUrl &url, const char *fileName) return QUrl(); } - // 拦截路径遍历攻击,防止恶意文件名越权 QByteArray fileNameBa(fileName); - if (fileNameBa.contains('/') || fileNameBa.contains('\\') || fileNameBa == "." || fileNameBa == "..") { - return QUrl(); + + // 路径遍历检查仅对本地文件系统 (file:/// 或无 scheme) 生效 + // gio 的 trash:/// 后端对非用户主目录挂载点的回收站文件,使用反斜杠分隔的扁平路径 + // 作为 GFileInfo 的 standard::name(例如 "\media\user\dev\.Trash-1000\files\x"), + // 这是合法的 trash 文件名而非恶意路径,故不应对其做路径遍历拦截。 + const QString scheme = url.scheme(); + if (scheme.isEmpty() || scheme == QLatin1String("file")) { + if (fileNameBa.contains("../") || fileNameBa.contains("..\\") || fileNameBa.startsWith("..")) { + return QUrl(); + } } QByteArray path; From 4dd898bb2d033083feb3e2d4beb2aebc32ac6dc9 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Thu, 6 Aug 2026 10:24:28 +0800 Subject: [PATCH 06/10] chore: bump version to 1.3.71 1.3.71 Log: --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index dff1c0dd..877706d5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +util-dfm (1.3.71) unstable; urgency=medium + + * fix: restrict path traversal check to file scheme in DEnumerator buildUrl + + -- zhangsheng Thu, 06 Aug 2026 10:24:21 +0800 + util-dfm (1.3.70) unstable; urgency=medium * perf(dfm-io): cache statx results in DFileInfoPrivate::attributesBySelf From 9f4077c90fcc3618483e618a93ecc9d5e9018054 Mon Sep 17 00:00:00 2001 From: deepin-admin-bot Date: Wed, 19 Aug 2026 08:18:28 +0000 Subject: [PATCH 07/10] chore: Sync by https://github.com/linuxdeepin/.github/commit/a300f8523d4876677112edab6a0772f7ad112976 sync by github workflow Log: none Influence: none --- .github/workflows/call-update-changelog.yml | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/call-update-changelog.yml diff --git a/.github/workflows/call-update-changelog.yml b/.github/workflows/call-update-changelog.yml new file mode 100644 index 00000000..b76b9b3f --- /dev/null +++ b/.github/workflows/call-update-changelog.yml @@ -0,0 +1,53 @@ +name: Call update changelog + +on: + workflow_dispatch: + inputs: + version: + description: "Target version; leave empty to auto bump" + required: false + type: string + name: + description: "Maintainer name used in debian/changelog" + required: true + type: string + email: + description: "Maintainer email used in debian/changelog" + required: true + type: string + base_branch: + description: "Base branch to read and target" + required: false + default: master + type: string + distribution: + description: "Changelog distribution" + required: false + default: unstable + type: string + create_pr: + description: "Create a pull request after generating changelog" + required: false + default: true + type: boolean + +jobs: + update_changelog: + uses: linuxdeepin/.github/.github/workflows/update-changelog.yml@master + with: + version: ${{ inputs.version }} + name: ${{ inputs.name }} + email: ${{ inputs.email }} + base_branch: ${{ inputs.base_branch }} + distribution: ${{ inputs.distribution }} + create_pr: ${{ inputs.create_pr }} + secrets: inherit + + show_result: + runs-on: ubuntu-latest + needs: update_changelog + steps: + - name: Show update changelog result + run: | + echo "Version: ${{ needs.update_changelog.outputs.version }}" + echo "Pull Request URL: ${{ needs.update_changelog.outputs.pr_url }}" From 09afe9177aacebf779b85e48e03d6958b7ecdf96 Mon Sep 17 00:00:00 2001 From: zhangsheng Date: Fri, 28 Aug 2026 13:25:08 +0800 Subject: [PATCH 08/10] fix: include share name in SMB keyring credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Root cause: DNetworkMounter's keyring credential management (savePasswd/loginPasswd/smbSchema) did not include share name, so loginPasswd searched by {server, protocol} only and returned all saved credentials for the same server regardless of share 2. Fix: add "object" attribute to smbSchema for share name, extract share from URL path in savePasswd and loginPasswd, include it in both store and search operations to isolate credentials per share 3. Impact: credentials are now stored and searched per-share; old credentials without "object" attribute won't be matched, users need to re-enter password once per share after upgrade 4. 方案层级: 根因层修复(5-Why 在 Why 4 收敛,根因层与症状层一致) Log: fix SMB share only authenticating once across different users Influence: 1. Test accessing non-anonymous shares of different users on the same server, second share should prompt for authentication 2. Test re-accessing a saved share, credential should auto-fill 3. Test anonymous share access is unaffected 4. Test FTP/WebDAV mounting is unaffected fix: SMB共享凭证加入share name属性隔离 1. 根因:DNetworkMounter 的 keyring 凭证管理(savePasswd/loginPasswd/ smbSchema)未包含 share name 属性,loginPasswd 仅按 {server, protocol} 搜索,返回同一服务器所有共享的已保存凭证 2. 方案:在 smbSchema 中新增 "object" 属性用于 share name, savePasswd 和 loginPasswd 从 URL 提取 share name 并在存储和 查询时附带,实现凭证按共享级别隔离 3. 影响:凭证按共享级别存储和搜索,旧凭证不含 "object" 属性 无法命中,升级后用户需对每个已保存共享重新输入一次密码 4. 方案层级:根因层修复(5-Why 在 Why 4 收敛,根因层与症状层一致) Log: 修复访问不同用户不同密码的SMB共享时仅鉴权一次的问题 Influence: 1. 测试同一服务器不同用户的非匿名共享,访问第二个共享应弹出鉴权窗口 2. 测试重复访问已保存的共享,凭证应自动填充无需再次输入 3. 测试匿名共享访问不受影响 4. 测试 FTP/WebDAV 挂载不受影响 PMS: BUG-375291 --- src/dfm-mount/private/dnetworkmounter.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dfm-mount/private/dnetworkmounter.cpp b/src/dfm-mount/private/dnetworkmounter.cpp index 6ad2fca7..610a9df1 100644 --- a/src/dfm-mount/private/dnetworkmounter.cpp +++ b/src/dfm-mount/private/dnetworkmounter.cpp @@ -43,6 +43,7 @@ static constexpr char kSchemaUser[] { "user" }; static constexpr char kSchemaProtocol[] { "protocol" }; static constexpr char kSchemaDomain[] { "domain" }; static constexpr char kSchemaServer[] { "server" }; +static constexpr char kSchemaObject[] { "object" }; static constexpr char kLoginUser[] { "user" }; static constexpr char kLoginDomain[] { "domain" }; @@ -114,10 +115,12 @@ QList DNetworkMounter::loginPasswd(const QString &address) QUrl u(address); QString protocol = u.scheme(); QString host = u.host(); + QString share = u.path().remove("/"); GHashTable_autoptr query = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); g_hash_table_insert(query, strdup(kSchemaServer), strdup(host.toStdString().c_str())); g_hash_table_insert(query, strdup(kSchemaProtocol), strdup(protocol.toStdString().c_str())); + g_hash_table_insert(query, strdup(kSchemaObject), strdup(share.toStdString().c_str())); QList passwds; GError_autoptr err { nullptr }; @@ -164,10 +167,11 @@ QList DNetworkMounter::loginPasswd(const QString &address) std::string domain = passwd.value(kSchemaDomain).toString().toStdString(); GError_autoptr err { nullptr }; + std::string object = passwd.value(kSchemaObject).toString().toStdString(); g_autofree char *pwd = secret_password_lookup_sync( smbSchema(), nullptr, &err, kSchemaServer, server.c_str(), kSchemaProtocol, protocol.c_str(), kSchemaUser, user.c_str(), kSchemaDomain, domain.c_str(), - nullptr); + kSchemaObject, object.c_str(), nullptr); if (err) qDebug() << "query password failed: " << passwd << err->message; else { @@ -182,6 +186,7 @@ void DNetworkMounter::savePasswd(const QString &address, const MountPassInfo &in QUrl u(address); QString protocol = u.scheme(); QString server = u.host(); + QString share = u.path().remove("/"); const char *collection = info.savePasswd == NetworkMountPasswdSaveMode::kSaveBeforeLogout ? SECRET_COLLECTION_SESSION : SECRET_COLLECTION_DEFAULT; @@ -196,7 +201,8 @@ void DNetworkMounter::savePasswd(const QString &address, const MountPassInfo &in info.domain.toStdString().c_str(), kSchemaProtocol, protocol.toStdString().c_str(), kSchemaServer, server.toStdString().c_str(), kSchemaUser, - info.userName.toStdString().c_str(), nullptr); + info.userName.toStdString().c_str(), kSchemaObject, + share.toStdString().c_str(), nullptr); if (err) qWarning() << "save passwd failed: " << err->message; } @@ -211,6 +217,7 @@ SecretSchema *DNetworkMounter::smbSchema() sche.attributes[1] = { kSchemaDomain, SECRET_SCHEMA_ATTRIBUTE_STRING }; sche.attributes[2] = { kSchemaServer, SECRET_SCHEMA_ATTRIBUTE_STRING }; sche.attributes[3] = { kSchemaProtocol, SECRET_SCHEMA_ATTRIBUTE_STRING }; + sche.attributes[4] = { kSchemaObject, SECRET_SCHEMA_ATTRIBUTE_STRING }; return &sche; } From c73c189bed2f714554fa5ab3f07a09907a996df0 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Fri, 28 Aug 2026 14:21:48 +0800 Subject: [PATCH 09/10] chore: bump version to 1.3.72 1.3.72 Log: --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 877706d5..3c5dce85 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +util-dfm (1.3.72) unstable; urgency=medium + + * fix: include share name in SMB keyring credentials + + -- Zhang Sheng Fri, 28 Aug 2026 14:21:23 +0800 + util-dfm (1.3.71) unstable; urgency=medium * fix: restrict path traversal check to file scheme in DEnumerator buildUrl From 7b79889d79a44183e3411be4e5dca20295033369 Mon Sep 17 00:00:00 2001 From: "liyigang1@uniontech.com" Date: Mon, 21 Sep 2026 12:05:13 +0800 Subject: [PATCH 10/10] fix: add timeout protection and force-unmount retry for async unmount When unmounting SFTP/protocol devices via the async path (unmountAsync), no GCancellable was self-created and no timeout protection existed. When the caller passes empty opts (e.g. detachProtoDev), cancellable was always nullptr, causing the GIO operation to hang indefinitely after network disconnection. gvfsd-sftp would be blocked, the mount-removed signal never fired, and subsequent unmount attempts received BUSY errors. Fix: self-create a GCancellable (mirroring the sync unmount path), add a 5-second timeout. On timeout, cancel the graceful unmount and retry with G_MOUNT_UNMOUNT_FORCE so gvfsd closes the connection and the mount-removed signal chain fires, removing the device from the UI. Normal network conditions keep the graceful unmount behavior with no regression. Log: fix SFTP unmount hang on network disconnection Bug: https://pms.uniontech.com/bug-view-278523.html Change-Id: I4d8232c4a1064e7c84ce387431568032 --- src/dfm-mount/lib/dprotocoldevice.cpp | 48 ++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/dfm-mount/lib/dprotocoldevice.cpp b/src/dfm-mount/lib/dprotocoldevice.cpp index b15c9c64..56d570ab 100644 --- a/src/dfm-mount/lib/dprotocoldevice.cpp +++ b/src/dfm-mount/lib/dprotocoldevice.cpp @@ -21,6 +21,8 @@ DFM_MOUNT_USE_NS namespace { +static constexpr int kUnmountAsyncTimeoutMs { 5000 }; + struct CallbackProxyWithData { CallbackProxyWithData() = delete; @@ -31,6 +33,12 @@ struct CallbackProxyWithData CallbackProxy caller; QPointer data; DProtocolDevicePrivate *d { nullptr }; + + QScopedPointer unmountTimer; + GCancellable *unmountCancellable { nullptr }; + bool ownsCancellable { false }; + bool unmountTimedOut { false }; + bool forceUnmountIssued { false }; }; } @@ -347,19 +355,36 @@ void DProtocolDevicePrivate::unmountAsync(const QVariantMap &opts, DeviceOperate if (opts.contains(ParamMountOperation)) operation = reinterpret_cast((opts.value(ParamMountOperation).value())); + bool ownsCancellable = false; + if (!cancellable) { + cancellable = g_cancellable_new(); + ownsCancellable = true; + } + GMountUnmountFlags flag; if (opts.contains(ParamForce) && opts.value(ParamForce).toBool()) flag = GMountUnmountFlags::G_MOUNT_UNMOUNT_FORCE; else flag = GMountUnmountFlags::G_MOUNT_UNMOUNT_NONE; - // qInfo() << "mutexForMount prelock" << __FUNCTION__; - // QMutexLocker locker(&mutexForMount); - // qInfo() << "mutexForMount locked" << __FUNCTION__; - CallbackProxyWithData *proxy = new CallbackProxyWithData(cb); proxy->data = qobject_cast(q); proxy->d = this; + proxy->unmountCancellable = cancellable; + proxy->ownsCancellable = ownsCancellable; + + if (flag == G_MOUNT_UNMOUNT_NONE) { + proxy->unmountTimer.reset(new QTimer()); + proxy->unmountTimer->setInterval(kUnmountAsyncTimeoutMs); + proxy->unmountTimer->setSingleShot(true); + QObject::connect(proxy->unmountTimer.data(), &QTimer::timeout, [proxy]() { + proxy->unmountTimedOut = true; + if (proxy->unmountCancellable) + g_cancellable_cancel(proxy->unmountCancellable); + }); + proxy->unmountTimer->start(); + } + g_mount_unmount_with_operation(mountHandler, flag, operation, cancellable, unmountWithCallback, proxy); } } @@ -613,12 +638,27 @@ void DProtocolDevicePrivate::unmountWithCallback(GObject *sourceObj, GAsyncResul auto proxy = static_cast(cbProxy); if (proxy) { + if (proxy->unmountTimedOut && !proxy->forceUnmountIssued && !ret && proxy->data) { + proxy->forceUnmountIssued = true; + g_mount_unmount_with_operation(mnt, G_MOUNT_UNMOUNT_FORCE, + nullptr, nullptr, + unmountWithCallback, proxy); + return; + } + + if (proxy->unmountTimer) + proxy->unmountTimer->stop(); + if (proxy->data) { proxy->d->mountHandler = nullptr; } if (proxy->caller.cb) { proxy->caller.cb(ret, derr); } + + if (proxy->ownsCancellable && proxy->unmountCancellable) + g_object_unref(proxy->unmountCancellable); + delete proxy; } }