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)