From 84967885889bfb2f98bb2001f9eaf9bda4588b6a Mon Sep 17 00:00:00 2001 From: zhanghongyuan Date: Mon, 21 Sep 2026 10:29:19 +0800 Subject: [PATCH] feat(plugin): add linglong layer and uab package support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a read-only kerfuffle plugin for linglong layer/uab packages, parsing the official container formats directly and extracting the embedded EROFS image with fsck.erofs, so no linglong package is required. layer: 40-byte magic + meta JSON + EROFS payload; uab: ELF64 container, locate the bundle section via linglong.meta JSON. Register the vnd.linglong.layer mimetype and add association checkboxes; uab files keep the official vnd.linyaps.uab type registered by linglong-bin, so no self-made uab type is needed and no icon override is applied (generic file icons are kept). Also fix KPluginLoader for Qt6 where QLibrary state is not shared with QPluginLoader. uab is detected inside the app via the linglong.meta ELF section since plain mime magic would hijack all ELF executables. 新增玲珑 layer/uab 只读解压插件:直接解析官方容器格式并用 fsck.erofs 解压内嵌 EROFS 镜像,不依赖玲珑软件包。layer 为 40 字节魔数 + 元信息 JSON + EROFS 载荷;uab 为 ELF64 容器, 经 linglong.meta JSON 定位 bundle section。注册 vnd.linglong.layer mimetype 并新增文件关联勾选项;uab 保持 官方 linglong-bin 注册的 vnd.linyaps.uab 类型,不自造 uab 类型也不覆盖图标(保留通用文件图标)。修复 Qt6 下 KPluginLoader 与 QPluginLoader 不共享加载状态导致的断言问题。 应用内通过 linglong.meta ELF section 精确识别 uab,避免 magic 误伤所有可执行文件。 Log: 新增玲珑 layer/uab 包的预览与解压支持 Influence: 无 ll-builder/linglong 环境也能预览解压 layer/uab; Qt6 调试构建下插件加载断言修复。 --- 3rdparty/CMakeLists.txt | 3 + 3rdparty/clilllayerplugin/CMakeLists.txt | 30 + .../clilllayerplugin/clilllayerplugin.cpp | 630 ++++++++++++++++++ 3rdparty/clilllayerplugin/clilllayerplugin.h | 124 ++++ .../kerfuffle_clilllayer.json | 22 + 3rdparty/interface/plugin/kpluginloader.cpp | 6 +- CMakeLists.txt | 1 + src/assets/data/deepin-compressor.json | 14 + src/assets/mimetype/deepin-compressor.xml | 10 +- src/source/common/mimetypes.cpp | 77 ++- src/source/common/uitools.cpp | 6 +- 11 files changed, 918 insertions(+), 5 deletions(-) create mode 100644 3rdparty/clilllayerplugin/CMakeLists.txt create mode 100644 3rdparty/clilllayerplugin/clilllayerplugin.cpp create mode 100644 3rdparty/clilllayerplugin/clilllayerplugin.h create mode 100644 3rdparty/clilllayerplugin/kerfuffle_clilllayer.json diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 950b0fca2..f3e94f24a 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -16,6 +16,9 @@ add_subdirectory(libzipplugin) add_subdirectory(libminizipplugin) +# 玲珑 layer/uab 包只读插件(依赖系统 fsck.erofs 命令) +add_subdirectory(clilllayerplugin) + # clipzipplugin(仅 x86/ARM + Qt6 环境启用,依赖 src/pzip) if((CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm|x86_64|amd64") AND (QT_VERSION_MAJOR EQUAL 6)) message(STATUS "x86/ARM + Qt6 环境,启用 pzip 高性能压缩插件") diff --git a/3rdparty/clilllayerplugin/CMakeLists.txt b/3rdparty/clilllayerplugin/CMakeLists.txt new file mode 100644 index 000000000..7a54a3337 --- /dev/null +++ b/3rdparty/clilllayerplugin/CMakeLists.txt @@ -0,0 +1,30 @@ +#定义cmake版本 +cmake_minimum_required(VERSION 3.9.5) + +#设置项目名称 +set(LIB_NAME clilllayerplugin) +project(${LIB_NAME}) + +#查找依赖关系的包信息 +find_package(Qt${QT_DESIRED_VERSION} REQUIRED COMPONENTS Widgets) + +#指定头文件路径 +include_directories(${PROJECT_SOURCE_DIR}) +include_directories(${HFILES_OUTPUT_PATH}) + +#链接库路径 +link_directories(${LIBRARY_OUTPUT_PATH}) + +#源文件、头文件和资源文件 +file(GLOB_RECURSE c_files RELATIVE ${PROJECT_SOURCE_DIR} *.cpp) +file(GLOB_RECURSE h_files RELATIVE ${PROJECT_SOURCE_DIR} *.h) +file(GLOB_RECURSE json_files RELATIVE ${PROJECT_SOURCE_DIR} *.json) + +#生成动静态库 +add_library(${LIB_NAME} SHARED ${c_files} ${h_files} ${json_files}) + +#链接动静态库 +target_link_libraries(${LIB_NAME} + Qt${QT_DESIRED_VERSION}::Widgets + compressor-interface +) diff --git a/3rdparty/clilllayerplugin/clilllayerplugin.cpp b/3rdparty/clilllayerplugin/clilllayerplugin.cpp new file mode 100644 index 000000000..6d6db3291 --- /dev/null +++ b/3rdparty/clilllayerplugin/clilllayerplugin.cpp @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2026 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "clilllayerplugin.h" +#include "datamanager.h" +#include "queries.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +/** layer 包 40 字节魔数,对齐官方 linglong::package::magicNumber() */ +const qint64 kLayerMagicSize = 40; +const char kLayerMagic[kLayerMagicSize] = "<<< deepin linglong layer archive >>>"; +// 注:字符串字面量 37 字符 + 编译器补的 '\0',官方为 leftJustified(40, 0) 补零 +static_assert(sizeof(kLayerMagic) == kLayerMagicSize, "layer magic must be 40 bytes"); + +/** uab 包 meta 信息所在的 ELF section 名,对齐官方 common::uab::metaSection */ +const char kUabMetaSection[] = "linglong.meta"; + +/** meta JSON 尺寸合理上限,防止异常文件导致超大读取 */ +const qint64 kMaxMetaSize = 16 * 1024 * 1024; + +/** fsck.erofs 解压超时(毫秒),兜底防止异常挂死 */ +constexpr int kFsckTimeoutMs = 10 * 60 * 1000; + +} // namespace + +LayerPluginFactory::LayerPluginFactory() +{ + registerPlugin(); +} + +LayerPluginFactory::~LayerPluginFactory() +{ +} + +LayerPlugin::LayerPlugin(QObject *parent, const QVariantList &args) + : ReadOnlyArchiveInterface(parent, args) +{ + qDebug() << "LayerPlugin created for:" << m_strArchiveName; +} + +LayerPlugin::~LayerPlugin() +{ +} + +bool LayerPlugin::parseLayerHeader(QFile &file, qint64 *erofsOffset, qint64 *erofsSize, QString *errOut) +{ + // 布局对齐官方 LayerFile: [40B 魔数][4B LE metaInfoLength][LayerInfo JSON][EROFS 至文件尾] + file.seek(0); + QByteArray magic = file.read(kLayerMagicSize); + if (magic.size() != kLayerMagicSize || memcmp(magic.constData(), kLayerMagic, kLayerMagicSize) != 0) { + *errOut = QStringLiteral("invalid magic number, this is not a linglong layer"); + return false; + } + + quint32 metaLen = 0; + QByteArray lenBuf = file.read(4); + if (lenBuf.size() != 4) { + *errOut = QStringLiteral("layer file truncated"); + return false; + } + metaLen = qFromLittleEndian(lenBuf.constData()); + if (metaLen == 0 || metaLen > kMaxMetaSize) { + *errOut = QStringLiteral("layer meta info length invalid: %1").arg(metaLen); + return false; + } + + const qint64 fileSize = file.size(); + const qint64 offset = kLayerMagicSize + 4 + static_cast(metaLen); + if (offset >= fileSize) { + *errOut = QStringLiteral("layer file truncated (no erofs payload)"); + return false; + } + *erofsOffset = offset; + *erofsSize = fileSize - offset; + return true; +} + +bool LayerPlugin::parseUabHeader(QFile &file, qint64 *erofsOffset, qint64 *erofsSize, QString *errOut) +{ + // 布局对齐官方 UABFile: ELF64 容器 + section "linglong.meta"(JSON, 内含 + // sections.bundle 指向 EROFS 镜像 section) + 可选 .note.uab.sig 签名 + file.seek(0); + const QByteArray ehdr = file.read(64); + if (ehdr.size() != 64) { + *errOut = QStringLiteral("uab file too small"); + return false; + } + const uchar *e = reinterpret_cast(ehdr.constData()); + if (e[0] != 0x7f || e[1] != 'E' || e[2] != 'L' || e[3] != 'F') { + *errOut = QStringLiteral("invalid magic number, this is not a linglong uab"); + return false; + } + // 仅支持 64 位小端(官方 UAB 仅在 64 位小端平台产生) + if (e[4] != 2 || e[5] != 1) { + *errOut = QStringLiteral("unsupported uab elf class/encoding"); + return false; + } + const quint64 shoff = qFromLittleEndian(e + 40); // e_shoff + const quint16 shentsize = qFromLittleEndian(e + 58); // e_shentsize + const quint16 shnum = qFromLittleEndian(e + 60); // e_shnum + const quint16 shstrndx = qFromLittleEndian(e + 62); // e_shstrndx + if (shoff == 0 || shentsize != 64 || shnum == 0 || shstrndx >= shnum) { + *errOut = QStringLiteral("invalid uab elf section header table"); + return false; + } + + // 读全部 section 头(避免加法回绕:先校验各分量再作差) + if (shoff > static_cast(file.size()) + || static_cast(shnum) * shentsize > file.size() - static_cast(shoff)) { + *errOut = QStringLiteral("uab section header table out of range"); + return false; + } + file.seek(static_cast(shoff)); + QByteArray shdrs = file.read(static_cast(shnum) * shentsize); + if (shdrs.size() != static_cast(shnum) * shentsize) { + *errOut = QStringLiteral("uab section header table truncated"); + return false; + } + auto shdrAt = [&shdrs](int idx) -> const uchar * { + return reinterpret_cast(shdrs.constData()) + qint64(idx) * 64; + }; + + // 读 section 名字表 + const uchar *strSh = shdrAt(shstrndx); + const quint64 strOff = qFromLittleEndian(strSh + 24); // sh_offset + const quint64 strSize = qFromLittleEndian(strSh + 32); // sh_size + if (strSize == 0 || strSize > quint64(kMaxMetaSize) + || strSize > static_cast(file.size()) + || strOff > static_cast(file.size()) - strSize) { + *errOut = QStringLiteral("invalid uab elf string table"); + return false; + } + file.seek(static_cast(strOff)); + const QByteArray shstrtab = file.read(static_cast(strSize)); + + // 按名字取 section 头信息;名字为 shstrtab 中 sh_name 偏移处的 NUL 结尾串, + // 手动定位 NUL 边界以防恶意 ELF 字符串表无终止符导致越界读 + auto findSection = [&shdrAt, &shstrtab, shnum](const QByteArray &name, + quint64 *off, + quint64 *size) -> bool { + for (int i = 0; i < shnum; ++i) { + const uchar *sh = shdrAt(i); + const quint32 nameOff = qFromLittleEndian(sh); // sh_name + if (nameOff >= static_cast(shstrtab.size())) { + continue; + } + const int nameEnd = shstrtab.indexOf('\0', static_cast(nameOff)); + if (nameEnd < 0) { + continue; + } + const QByteArray secName = shstrtab.mid(static_cast(nameOff), + nameEnd - static_cast(nameOff)); + if (secName != name) { + continue; + } + *off = qFromLittleEndian(sh + 24); + *size = qFromLittleEndian(sh + 32); + return true; + } + return false; + }; + + // linglong.meta section → UabMetaInfo JSON + quint64 metaOff = 0, metaSize = 0; + if (!findSection(kUabMetaSection, &metaOff, &metaSize) || metaSize == 0 + || metaSize > quint64(kMaxMetaSize) || metaSize > static_cast(file.size()) + || metaOff > static_cast(file.size()) - metaSize) { + *errOut = QStringLiteral("uab meta section missing or invalid"); + return false; + } + file.seek(static_cast(metaOff)); + const QByteArray metaJson = file.read(static_cast(metaSize)); + QJsonParseError parseErr; + const QJsonDocument doc = QJsonDocument::fromJson(metaJson, &parseErr); + if (!doc.isObject()) { + *errOut = QStringLiteral("uab meta json invalid: %1").arg(parseErr.errorString()); + return false; + } + const QString bundleName = doc.object() + .value(QStringLiteral("sections")) + .toObject() + .value(QStringLiteral("bundle")) + .toString(); + if (bundleName.isEmpty()) { + *errOut = QStringLiteral("uab meta json has no sections.bundle"); + return false; + } + + // bundle section 即 EROFS 镜像 + quint64 bundleOff = 0, bundleSize = 0; + if (!findSection(bundleName.toLatin1(), &bundleOff, &bundleSize) || bundleSize == 0 + || bundleSize > static_cast(file.size()) + || bundleOff > static_cast(file.size()) - bundleSize) { + *errOut = QStringLiteral("uab bundle section missing or invalid"); + return false; + } + *erofsOffset = static_cast(bundleOff); + *erofsSize = static_cast(bundleSize); + return true; +} + +bool LayerPlugin::parseHeader(qint64 *erofsOffset, qint64 *erofsSize, QString *errOut) +{ + QFile file(m_strArchiveName); + if (!file.open(QIODevice::ReadOnly)) { + *errOut = QStringLiteral("failed to open %1").arg(m_strArchiveName); + return false; + } + file.seek(0); + const QByteArray head = file.peek(4); + if (head.startsWith(QByteArrayLiteral("\x7f""ELF"))) { + return parseUabHeader(file, erofsOffset, erofsSize, errOut); + } + return parseLayerHeader(file, erofsOffset, erofsSize, errOut); +} + +bool LayerPlugin::copyPayload(QFile &file, qint64 offset, qint64 size, const QString &dstPath, + QString *errOut) +{ + QFile dst(dstPath); + if (!dst.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + *errOut = QStringLiteral("failed to create %1").arg(dstPath); + return false; + } + if (!file.seek(offset)) { + *errOut = QStringLiteral("failed to seek to payload offset %1").arg(offset); + return false; + } + constexpr qint64 kChunk = 4 * 1024 * 1024; + QByteArray buf; + buf.resize(int(kChunk)); + qint64 remaining = size; + while (remaining > 0) { + const qint64 toRead = qMin(kChunk, remaining); + const qint64 got = file.read(buf.data(), toRead); + if (got <= 0) { + *errOut = QStringLiteral("failed to read erofs payload at %1").arg(size - remaining); + return false; + } + if (dst.write(buf.constData(), got) != got) { + *errOut = QStringLiteral("failed to write %1").arg(dstPath); + return false; + } + remaining -= got; + } + if (!dst.flush()) { + *errOut = QStringLiteral("failed to write %1").arg(dstPath); + return false; + } + return true; +} + +QString LayerPlugin::runFsckExtract(const QString &erofsFile, const QString &destDir) +{ + const QString fsck = QStandardPaths::findExecutable(QStringLiteral("fsck.erofs")); + if (fsck.isEmpty()) { + return QStringLiteral("fsck.erofs not found, please install erofs-utils"); + } + + QProcess process; + process.setProgram(fsck); + // 对齐官方 LayerPackager/UABFile 的 fsck.erofs 兜底分支 + process.setArguments({ QStringLiteral("--extract=%1").arg(destDir), erofsFile }); + process.start(); + if (!process.waitForStarted()) { + return QStringLiteral("failed to start fsck.erofs"); + } + if (!process.waitForFinished(kFsckTimeoutMs)) { + process.kill(); + process.waitForFinished(5000); + return QStringLiteral("fsck.erofs extract timed out"); + } + if (process.exitCode() != 0 || process.exitStatus() != QProcess::NormalExit) { + // stderr 可能为空,必须保证失败时返回非空串,否则上层会误判为成功 + QString strError = QString::fromLocal8Bit(process.readAllStandardError().simplified()); + if (strError.isEmpty()) { + strError = QStringLiteral("fsck.erofs extract failed (exit code %1)").arg(process.exitCode()); + } + return strError; + } + return QString(); +} + +QString LayerPlugin::extractPackage(const QString &destDir) +{ + qint64 erofsOffset = 0; + qint64 erofsSize = 0; + QString strError; + if (!parseHeader(&erofsOffset, &erofsSize, &strError)) { + return strError; + } + + // fsck.erofs 老版本不支持 offset 参数,与官方兜底策略一致先落盘为独立镜像 + QFile srcFile(m_strArchiveName); + if (!srcFile.open(QIODevice::ReadOnly)) { + return QStringLiteral("failed to open %1").arg(m_strArchiveName); + } + const QString erofsFile = QFileInfo(destDir).absolutePath() + QStringLiteral("/bundle.erofs"); + if (!copyPayload(srcFile, erofsOffset, erofsSize, erofsFile, &strError)) { + return strError; + } + srcFile.close(); + + return runFsckExtract(erofsFile, destDir); +} + +void LayerPlugin::collectEntries(const QString &dir) +{ + ArchiveData &stArchiveData = DataManager::get_instance().archiveData(); + + // 先收集再排序,保证同目录下文件顺序稳定 + QFileInfoList list; + QDirIterator it(dir, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System, QDirIterator::Subdirectories); + while (it.hasNext()) { + list << it.fileInfo(); + it.next(); + } + + const QDir layerDir(dir); + for (const QFileInfo &fi : list) { + const QString rel = layerDir.relativeFilePath(fi.absoluteFilePath()); + if (rel.isEmpty() || rel == QLatin1String(".")) { + continue; + } + + FileEntry stEntry; + // 符号链接按文件处理,避免跟随链接导致目录误判 + stEntry.isDirectory = fi.isDir() && !fi.isSymLink(); + stEntry.strFullPath = stEntry.isDirectory ? rel + QLatin1Char('/') : rel; + stEntry.strFullPath.replace(QDir::separator(), QLatin1Char('/')); // 统一为 '/' 分隔 + // 与其他插件一致:split 去空后取最后一段,避免目录尾部 '/' 导致名字为空 + const QStringList pieces = stEntry.strFullPath.split(QLatin1Char('/'), Qt::SkipEmptyParts); + stEntry.strFileName = pieces.isEmpty() ? QString() : pieces.last(); + stEntry.qSize = stEntry.isDirectory ? 0 : fi.size(); + stEntry.uLastModifiedTime = static_cast(fi.lastModified().toSecsSinceEpoch()); + + handleEntry(stEntry); // 填充第一层列表并补齐缺失的父目录 + stArchiveData.mapFileEntry.insert(stEntry.strFullPath, stEntry); + stArchiveData.qSize += stEntry.qSize; + } +} + +PluginFinishType LayerPlugin::list() +{ + qInfo() << "LayerPlugin加载压缩包数据"; + m_eErrorType = ET_NoError; + ArchiveData &stArchiveData = DataManager::get_instance().archiveData(); + stArchiveData.reset(); + m_setHasHandlesDirs.clear(); + m_setHasRootDirs.clear(); + + QFileInfo layerInfo(m_strArchiveName); + if (!layerInfo.exists() || !layerInfo.isReadable()) { + m_eErrorType = ET_FileOpenError; + return PFT_Error; + } + stArchiveData.qComressSize = layerInfo.size(); + + QTemporaryDir tmpRoot(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + + QStringLiteral("/deepin-compressor-linglong-XXXXXX")); + if (!tmpRoot.isValid()) { + m_eErrorType = ET_FileWriteError; + return PFT_Error; + } + const QString stageDir = tmpRoot.path() + QStringLiteral("/layer"); + if (!QDir().mkpath(stageDir)) { + m_eErrorType = ET_FileWriteError; + return PFT_Error; + } + + const QString strError = extractPackage(stageDir); + if (!strError.isEmpty()) { + qWarning() << "LayerPlugin extract failed:" << strError; + m_eErrorType = ET_ArchiveDamaged; + return PFT_Error; + } + + collectEntries(stageDir); + return PFT_Nomral; +} + +PluginFinishType LayerPlugin::testArchive() +{ + // 包头解析成功即认为格式合法(EROFS 完整性由解压过程保证) + qint64 erofsOffset = 0; + qint64 erofsSize = 0; + QString strError; + if (!parseHeader(&erofsOffset, &erofsSize, &strError)) { + qWarning() << "LayerPlugin testArchive failed:" << strError; + m_eErrorType = ET_ArchiveDamaged; + return PFT_Error; + } + return PFT_Nomral; +} + +bool LayerPlugin::moveToDest(const QString &src, const QString &dst) +{ + QFileInfo dstInfo(dst); + if (dstInfo.exists()) { + if (m_bSkipAll) { + return true; + } + if (!m_bOverwriteAll) { + OverwriteQuery query(dst); + emit signalQuery(&query); + query.waitForResponse(); + if (query.responseCancelled()) { + emit signalCancel(); + m_eErrorType = ET_UserCancelOpertion; + return false; + } else if (query.responseSkipAll()) { + m_bSkipAll = true; + return true; + } else if (query.responseSkip()) { + return true; + } else if (query.responseOverwriteAll()) { + m_bOverwriteAll = true; + } + } + // 覆盖:先删除已有项 + if (dstInfo.isDir() && !dstInfo.isSymLink()) { + QDir(dst).removeRecursively(); + } else { + QFile::remove(dst); + } + } + + QFileInfo srcInfo(src); + if (srcInfo.isDir() && !srcInfo.isSymLink()) { + if (!QDir().mkpath(dst)) { + m_eErrorType = ET_FileWriteError; + emit signalFileWriteErrorName(dst); + return false; + } + QDir srcDir(src); + const QFileInfoList children = + srcDir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden); + for (const QFileInfo &child : children) { + if (!moveToDest(child.absoluteFilePath(), dst + QLatin1Char('/') + child.fileName())) { + return false; + } + } + return true; + } + + // 文件(含符号链接):优先 rename(符号链接原样保留);rename 失败时, + // 符号链接用 QFile::link 重建(QFile::copy 会跟随链接复制内容,语义不对) + QFile srcFile(src); + if (srcFile.rename(dst)) { + return true; + } + srcFile.unsetError(); + if (srcInfo.isSymLink()) { + // readlink 取原始链接文本重建:symLinkTarget() 会解析为绝对路径, + // 相对链接将指向已删除的 staging 目录;读取失败再退回解析后的目标 + const QByteArray encoded = QFile::encodeName(src); + char buf[4096]; + const ssize_t n = ::readlink(encoded.constData(), buf, sizeof(buf) - 1); + const QString linkTarget = (n > 0) ? QString::fromLocal8Bit(buf, static_cast(n)) + : srcInfo.symLinkTarget(); + if (QFile::link(linkTarget, dst)) { + return true; + } + } else if (srcFile.copy(dst)) { + return true; + } + m_eErrorType = ET_FileWriteError; + emit signalFileWriteErrorName(dst); + return false; +} + +bool LayerPlugin::moveStagedToTarget(const QString &stageDir, const QString &targetPath, bool extractAll, + const QList &files, const QString &strDestination) +{ + // 需要搬入 target 的源目录与顶层子项列表: + // 1) 全部解压:搬 stageDir 下所有顶层项 + // 2) 提取包根目录下的项(strDestination 为空):仅搬选中的项,保留包内名称 + // 3) 提取深层项(strDestination 非空):搬该目录下所有子项,不保留前缀 + QString srcRoot = stageDir; + bool keepName = true; + + const QStringList stageTopItems = QDir(stageDir) + .entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System + | QDir::Hidden); + + if (!extractAll && !strDestination.isEmpty() && strDestination != QLatin1String("/")) { + // 深层提取:进入选中的上级目录,去掉包内前缀 + QString rootNode = strDestination; + while (rootNode.startsWith(QLatin1Char('/'))) { + rootNode.remove(0, 1); + } + while (rootNode.endsWith(QLatin1Char('/'))) { + rootNode.chop(1); + } + srcRoot = stageDir + QLatin1Char('/') + rootNode; + keepName = false; + if (!QFileInfo(srcRoot).isDir()) { + qWarning() << "LayerPlugin selected root node missing in staged dir:" << srcRoot; + m_eErrorType = ET_FileReadError; + return false; + } + } + + QStringList itemsToMove; + if (extractAll) { + itemsToMove = stageTopItems; + } else if (keepName) { + for (const FileEntry &entry : files) { + QString name = entry.strFullPath; + while (name.startsWith(QLatin1Char('/'))) { + name.remove(0, 1); + } + while (name.endsWith(QLatin1Char('/'))) { + name.chop(1); + } + if (!name.isEmpty() && !itemsToMove.contains(name)) { + itemsToMove << name; + } + } + } else { + itemsToMove = QDir(srcRoot) + .entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden); + } + + // 统计总大小用于进度显示 + m_qTotalSize = 0; + m_qDoneSize = 0; + QList> moveList; // <源绝对路径, 目标绝对路径> + for (const QString &name : itemsToMove) { + const QString src = srcRoot + QLatin1Char('/') + name; + const QString dst = targetPath + QLatin1Char('/') + name; + m_qTotalSize += QFileInfo(src).isDir() ? 0 : QFileInfo(src).size(); + moveList << qMakePair(src, dst); + } + // 总字节数为 0(纯目录)时按条目计数推进进度 + const bool countByItems = (m_qTotalSize <= 0); + if (countByItems) { + m_qTotalSize = moveList.size(); + } + + for (const auto &pair : moveList) { + emit signalCurFileName(QFileInfo(pair.first).fileName()); + // rename 成功后源路径已不存在,须在 move 前记录条目大小 + const qint64 entrySize = QFileInfo(pair.first).isDir() ? 0 : QFileInfo(pair.first).size(); + if (!moveToDest(pair.first, pair.second)) { + return false; + } + if (countByItems) { + m_qDoneSize += 1; + } else { + m_qDoneSize += entrySize; + } + emit signalprogress(100.0 * m_qDoneSize / m_qTotalSize); + } + return true; +} + +PluginFinishType LayerPlugin::extractFiles(const QList &files, const ExtractionOptions &options) +{ + qInfo() << "LayerPlugin解压"; + m_eErrorType = ET_NoError; + m_bOverwriteAll = false; + m_bSkipAll = false; + + const bool extractAll = files.isEmpty(); + QString targetPath = options.strTargetPath; + if (targetPath.isEmpty()) { + m_eErrorType = ET_FileWriteError; + return PFT_Error; + } + + // 目标目录存在性检查与创建(对齐 libarchive 插件行为) + QFileInfo targetInfo(targetPath); + if (targetInfo.exists() && !targetInfo.isDir()) { + m_eErrorType = ET_FileWriteError; + qWarning() << "LayerPlugin target is not a directory:" << targetPath; + return PFT_Error; + } + if (!targetInfo.exists() && !QDir().mkpath(targetPath)) { + if (isInsufficientDiskSpace(targetPath)) { + m_eErrorType = ET_InsufficientDiskSpace; + } else { + m_eErrorType = ET_FileWriteError; + } + return PFT_Error; + } + + // 暂存目录放在目标目录内(保证同文件系统,rename 高效) + QTemporaryDir staging(targetPath + QStringLiteral("/.deepin-compressor-linglong-XXXXXX")); + if (!staging.isValid()) { + m_eErrorType = ET_FileWriteError; + return PFT_Error; + } + const QString stageDir = staging.path() + QStringLiteral("/layer"); + if (!QDir().mkpath(stageDir)) { + m_eErrorType = ET_FileWriteError; + return PFT_Error; + } + + const QString strError = extractPackage(stageDir); + if (!strError.isEmpty()) { + qWarning() << "LayerPlugin extract failed:" << strError; + m_eErrorType = ET_ArchiveDamaged; + return PFT_Error; + } + + if (!moveStagedToTarget(stageDir, targetPath, extractAll, files, options.strDestination)) { + return (m_eErrorType == ET_UserCancelOpertion) ? PFT_Cancel : PFT_Error; + } + + return PFT_Nomral; +} diff --git a/3rdparty/clilllayerplugin/clilllayerplugin.h b/3rdparty/clilllayerplugin/clilllayerplugin.h new file mode 100644 index 000000000..4a9adc72c --- /dev/null +++ b/3rdparty/clilllayerplugin/clilllayerplugin.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2026 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef CLLLAYERPLUGIN_H +#define CLLLAYERPLUGIN_H + +#include "archiveinterface.h" +#include "kpluginfactory.h" + +class QFile; +class QTemporaryDir; + +class LayerPluginFactory : public KPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.KPluginFactory" FILE "kerfuffle_clilllayer.json") + Q_INTERFACES(KPluginFactory) + +public: + explicit LayerPluginFactory(); + ~LayerPluginFactory() override; +}; + +/** + * @brief 玲珑(linglong)layer/uab 包只读插件 + * + * 复刻官方 linglong 库(libs/linglong)的解包逻辑: + * - layer: [40B 魔数][4B LE 长度][LayerInfo JSON][EROFS 数据至文件尾] + * 对应官方 LayerFile::binaryDataOffset() = 40 + metaLen + 4 + * - uab: ELF64 容器,section "linglong.meta"(JSON,内含 sections.bundle + * 指向存放 EROFS 镜像的 section 名)对应官方 UABFile::unpack() + * 解压使用官方同款兜底工具 `fsck.erofs --extract= `(无 root、 + * 无挂载要求;官方优先 erofsfuse 挂载,无该命令时同样回退 fsck.erofs)。 + */ +class LayerPlugin : public ReadOnlyArchiveInterface +{ + Q_OBJECT + +public: + explicit LayerPlugin(QObject *parent, const QVariantList &args); + ~LayerPlugin() override; + + PluginFinishType list() override; + PluginFinishType testArchive() override; + PluginFinishType extractFiles(const QList &files, const ExtractionOptions &options) override; + + void pauseOperation() override {} + void continueOperation() override {} + +private: + /** + * @brief extractPackage 将当前 layer/uab 包解压到 destDir + * (解析格式头 → 复制 EROFS 载荷到临时文件 → fsck.erofs 解压) + * @param destDir 目标目录(可为已存在的空目录,fsck.erofs 解压到该目录) + * @return 空串表示成功,否则为错误信息 + */ + QString extractPackage(const QString &destDir); + + /** + * @brief parseHeader 解析包头,定位 EROFS 载荷 + * @param erofsOffset 载荷在包文件中的起始偏移 + * @param erofsSize 载荷字节数 + * @param errOut 失败时的错误信息 + * @return true: 成功;false: 格式不识别或文件损坏 + */ + bool parseHeader(qint64 *erofsOffset, qint64 *erofsSize, QString *errOut); + + /** + * @brief parseLayerHeader 按 layer 布局解析(对齐官方 LayerFile) + */ + bool parseLayerHeader(QFile &file, qint64 *erofsOffset, qint64 *erofsSize, QString *errOut); + + /** + * @brief parseUabHeader 按 uab 布局解析 ELF64 section(对齐官方 UABFile) + */ + bool parseUabHeader(QFile &file, qint64 *erofsOffset, qint64 *erofsSize, QString *errOut); + + /** + * @brief copyPayload 从包文件 offset 复制 size 字节到 dstPath + * (fsck.erofs 老版本不支持 offset 参数,与官方兜底策略一致先落盘) + */ + bool copyPayload(QFile &file, qint64 offset, qint64 size, const QString &dstPath, QString *errOut); + + /** + * @brief runFsckExtract 调用 fsck.erofs 将 EROFS 镜像解压到 destDir + * @param erofsFile EROFS 镜像文件 + * @param destDir 解压目标目录 + * @return 空串表示成功,否则为错误信息 + */ + QString runFsckExtract(const QString &erofsFile, const QString &destDir); + + /** + * @brief collectEntries 递归收集 dir 下所有文件/目录(含 dir 本身的直接子项), + * 填充 DataManager 中的归档数据,用于预览列表 + * @param dir 已解压出的包根目录 + */ + void collectEntries(const QString &dir); + + /** + * @brief moveToDest 将 src 移动(或拷贝)到 dst,处理同名冲突(弹覆盖询问) + * @return true: 成功(含跳过);false: 用户取消 + */ + bool moveToDest(const QString &src, const QString &dst); + + /** + * @brief moveStagedToTarget 把暂存目录中的内容搬到最终解压目录 + * @param stageDir 解压出的包根目录 + * @param targetPath 最终解压目标目录 + * @param extractAll 是否全部解压 + * @param files 选中的文件列表(extractAll 为 false 时使用) + * @param strDestination 选中项在包内的上级目录(空表示选中项位于包根目录) + * @return true: 成功;false: 用户取消或失败 + */ + bool moveStagedToTarget(const QString &stageDir, const QString &targetPath, bool extractAll, + const QList &files, const QString &strDestination); + + qlonglong m_qTotalSize = 0; // 本次解压/提取的总字节数,用于进度计算 + qlonglong m_qDoneSize = 0; // 已完成的字节数 +}; + +#endif // CLLLAYERPLUGIN_H diff --git a/3rdparty/clilllayerplugin/kerfuffle_clilllayer.json b/3rdparty/clilllayerplugin/kerfuffle_clilllayer.json new file mode 100644 index 000000000..6ee9aabc6 --- /dev/null +++ b/3rdparty/clilllayerplugin/kerfuffle_clilllayer.json @@ -0,0 +1,22 @@ +{ + "KPlugin": { + "Description": "Open and extract linglong layer and uab packages", + "Description[zh_CN]": "打开和解压如意玲珑 layer/uab 软件包", + "Id": "kerfuffle_clilllayer", + "MimeTypes": [ + "application/vnd.linglong.layer", + "application/vnd.linyaps.uab" + ], + "Name": "Linglong package plugin", + "Name[zh_CN]": "玲珑软件包插件", + "ServiceTypes": [ + "Kerfuffle/Plugin" + ], + "Version": "1.1" + }, + "X-KDE-Kerfuffle-ReadOnlyExecutables": [ + "fsck.erofs" + ], + "X-KDE-Kerfuffle-ReadWrite": false, + "X-KDE-Priority": 100 +} diff --git a/3rdparty/interface/plugin/kpluginloader.cpp b/3rdparty/interface/plugin/kpluginloader.cpp index c1b730d6f..60fb61108 100644 --- a/3rdparty/interface/plugin/kpluginloader.cpp +++ b/3rdparty/interface/plugin/kpluginloader.cpp @@ -191,7 +191,11 @@ bool KPluginLoader::load() Q_ASSERT(!fileName().isEmpty()); QLibrary lib(fileName()); - Q_ASSERT(lib.isLoaded()); // already loaded by QPluginLoader::load() + // Qt6 中 QLibrary 与 QPluginLoader 不再共享加载状态,Q_ASSERT(lib.isLoaded()) 会在调试构建中误触发。 + // 显式加载(对已加载库只是引用计数 +1,语义不变)。 + if (!lib.isLoaded()) { + lib.load(); + } // TODO: this messes up KPluginLoader::errorString(): it will change from unknown error to could not resolve kde_plugin_version quint32 *version = reinterpret_cast(lib.resolve("kde_plugin_version")); diff --git a/CMakeLists.txt b/CMakeLists.txt index 661ad5918..d1883478f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,7 @@ install(TARGETS compressor-interface DESTINATION ${COMPRESSOR_PLUGIN_PATH}) install(TARGETS bz2plugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) install(TARGETS gzplugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) install(TARGETS xzplugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) +install(TARGETS clilllayerplugin DESTINATION ${COMPRESSOR_PLUGIN_PATH}) # pzip 高性能压缩插件(仅 x86/ARM + Qt6 环境) if((CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm|x86_64|amd64") AND (QT_VERSION_MAJOR EQUAL 6)) diff --git a/src/assets/data/deepin-compressor.json b/src/assets/data/deepin-compressor.json index 710fa007b..93bd639c5 100644 --- a/src/assets/data/deepin-compressor.json +++ b/src/assets/data/deepin-compressor.json @@ -268,6 +268,20 @@ "text": "x-chrome-extension(crx)", "default": true }, + { + "key": "vnd.linglong.layer", + "name": " ", + "type": "checkbox", + "text": "vnd.linglong.layer(layer)", + "default": true + }, + { + "key": "vnd.linyaps.uab", + "name": " ", + "type": "checkbox", + "text": "vnd.linyaps.uab(uab)", + "default": true + }, { "key": "style", "name": "Font Style", diff --git a/src/assets/mimetype/deepin-compressor.xml b/src/assets/mimetype/deepin-compressor.xml index f1b427f59..9aaddf4ef 100644 --- a/src/assets/mimetype/deepin-compressor.xml +++ b/src/assets/mimetype/deepin-compressor.xml @@ -1,7 +1,7 @@ @@ -12,5 +12,13 @@ SPDX-License-Identifier: GPL-3.0-or-later + + Linglong layer package + 如意玲珑 layer 软件包 + + + + + diff --git a/src/source/common/mimetypes.cpp b/src/source/common/mimetypes.cpp index 896215550..864b77116 100644 --- a/src/source/common/mimetypes.cpp +++ b/src/source/common/mimetypes.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2019 ~ 2019 Deepin Technology Co., Ltd. -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -9,7 +9,72 @@ #include #include #include +#include #include +#include + +namespace { +/** + * @brief 检测 ELF64 小端文件是否包含指定 section 名 + * + * 玲珑 uab 包本质是 ELF 容器,内容识别命中 x-executable 及其子类型(如新版 + * mime db 中的 x-pie-executable),与普通 ELF 可执行文件仅能靠 section 特征 + * 区分,mime magic 无法安全表达(ELF magic 会误伤所有可执行文件),因此在此 + * 精确检测。 + */ +bool elfContainsSection(const QString &path, const QByteArray §ionName) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + const QByteArray ehdr = file.read(64); + if (ehdr.size() != 64) { + return false; + } + const uchar *e = reinterpret_cast(ehdr.constData()); + if (memcmp(e, "\x7f""ELF", 4) != 0 || e[4] != 2 || e[5] != 1) { // \x7fELF, 64-bit LE + return false; + } + const quint64 shoff = qFromLittleEndian(e + 40); // e_shoff + const quint16 shentsize = qFromLittleEndian(e + 58); // e_shentsize + const quint16 shnum = qFromLittleEndian(e + 60); // e_shnum + const quint16 shstrndx = qFromLittleEndian(e + 62); // e_shstrndx + if (shoff == 0 || shentsize != 64 || shnum == 0 || shstrndx >= shnum) { + return false; + } + if (shoff > static_cast(file.size()) + || static_cast(shnum) * shentsize > file.size() - static_cast(shoff)) { + return false; + } + file.seek(static_cast(shoff)); + const QByteArray shdrs = file.read(static_cast(shnum) * shentsize); + if (shdrs.size() != static_cast(shnum) * shentsize) { + return false; + } + const uchar *strSh = reinterpret_cast(shdrs.constData()) + qint64(shstrndx) * 64; + const quint64 strOff = qFromLittleEndian(strSh + 24); // sh_offset + const quint64 strSize = qFromLittleEndian(strSh + 32); // sh_size + if (strSize == 0 || strSize > 16 * 1024 * 1024 + || strSize > static_cast(file.size()) + || strOff > static_cast(file.size()) - strSize) { + return false; + } + file.seek(static_cast(strOff)); + const QByteArray shstrtab = file.read(static_cast(strSize)); + for (int i = 0; i < shnum; ++i) { + const uchar *sh = reinterpret_cast(shdrs.constData()) + qint64(i) * 64; + const quint32 nameOff = qFromLittleEndian(sh); // sh_name + if (nameOff >= static_cast(shstrtab.size())) { + continue; + } + if (shstrtab.mid(int(nameOff), sectionName.size() + 1) == sectionName + '\0') { + return true; + } + } + return false; +} +} // namespace CustomMimeType determineMimeType(const QString &filename) { @@ -162,6 +227,16 @@ CustomMimeType determineMimeType(const QString &filename) stMimeType.m_bUnKnown = false; // 对于内容和后缀不一致的情况进行的处理 if (mimeFromExtension != mimeFromContent) { + // 玲珑 uab 包:系统 mimetype 归属为官方 linglong-bin 注册的 + // application/vnd.linyaps.uab(ELF 容器,sub-class-of x-executable),此处 + // 用 ELF section 特征精确确认;inherits 同时匹配自身与子类型 + // (x-pie-executable 等),检测失败则保持内容识别结果(非 uab 的 ELF) + if (extName == QStringLiteral("application/vnd.linyaps.uab") + && mimeFromContent.inherits(QStringLiteral("application/x-executable")) + && elfContainsSection(filename, QByteArrayLiteral("linglong.meta"))) { + stMimeType.m_mimeType = mimeFromExtension; + return stMimeType; + } if ((mimeFromContent.inherits(QStringLiteral("text/x-qml")) && fileinfo.completeSuffix().toLower().contains(QStringLiteral("rar"))) || (mimeFromContent.name() == QStringLiteral("image/svg+xml") && mimeFromExtension.name() == QStringLiteral("application/zip"))) { stMimeType.m_mimeType = mimeFromExtension; diff --git a/src/source/common/uitools.cpp b/src/source/common/uitools.cpp index 84f81a28e..5d61eeb52 100644 --- a/src/source/common/uitools.cpp +++ b/src/source/common/uitools.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2019 ~ 2020 Uniontech Software Technology Co.,Ltd. -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -64,7 +64,9 @@ QStringList UiTools::m_associtionlist = QStringList() << "file_association.file_ << "file_association.file_association_type.x-iso9660-image" << "file_association.file_association_type.x-iso9660-appimage" << "file_association.file_association_type.x-source-rpm" - << "file_association.file_association_type.x-chrome-extension"; + << "file_association.file_association_type.x-chrome-extension" + << "file_association.file_association_type.vnd.linglong.layer" + << "file_association.file_association_type.vnd.linyaps.uab"; UiTools::UiTools(QObject *parent) : QObject(parent)