diff --git a/autotests/kerfuffle/archivetest.cpp b/autotests/kerfuffle/archivetest.cpp --- a/autotests/kerfuffle/archivetest.cpp +++ b/autotests/kerfuffle/archivetest.cpp @@ -149,6 +149,17 @@ qDebug() << "lrzip executable not found in path. Skipping lrzip test."; } + // Only run test for lz4-compressed tar if lz4 executable is found in path. + if (!QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { + QTest::newRow("lz4-compressed tarball") + << QFINDTESTDATA("data/simplearchive.tar.lz4") + << QStringLiteral("simplearchive") + << false << false << false << Archive::Unencrypted + << QStringLiteral("simplearchive"); + } else { + qDebug() << "lz4 executable not found in path. Skipping lz4 test."; + } + QTest::newRow("xar archive") << QFINDTESTDATA("data/simplearchive.xar") << QStringLiteral("simplearchive") @@ -461,6 +472,28 @@ qDebug() << "lrzip executable not found in path. Skipping lrzip test."; } + // Only run test for lz4-compressed tar if lz4 executable is found in path. + if (!QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { + archivePath = QFINDTESTDATA("data/simplearchive.tar.lz4"); + QTest::newRow("extract selected entries from a lz4-compressed tarball without path") + << archivePath + << QVariantList { + QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), + QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + } + << ExtractionOptions() + << 2; + + archivePath = QFINDTESTDATA("data/simplearchive.tar.lz4"); + QTest::newRow("extract all entries from a lz4-compressed tarball with path") + << archivePath + << QVariantList() + << optionsPreservePaths + << 7; + } else { + qDebug() << "lz4 executable not found in path. Skipping lz4 test."; + } + archivePath = QFINDTESTDATA("data/simplearchive.xar"); QTest::newRow("extract selected entries from a xar archive without path") << archivePath diff --git a/autotests/kerfuffle/mimetypetest.cpp b/autotests/kerfuffle/mimetypetest.cpp --- a/autotests/kerfuffle/mimetypetest.cpp +++ b/autotests/kerfuffle/mimetypetest.cpp @@ -56,6 +56,7 @@ const QString compressedLzipTarMime = QStringLiteral("application/x-lzip-compressed-tar"); const QString compressedLzopTarMime = QStringLiteral("application/x-tzo"); const QString compressedLrzipTarMime = QStringLiteral("application/x-lrzip-compressed-tar"); + const QString compressedLz4TarMime = QStringLiteral("application/x-lz4-compressed-tar"); const QString isoMimeType = QStringLiteral("application/x-cd-image"); const QString debMimeType = QMimeDatabase().mimeTypeForFile(QStringLiteral("dummy.deb"), QMimeDatabase::MatchExtension).name(); const QString xarMimeType = QStringLiteral("application/x-xar"); @@ -69,6 +70,7 @@ QTest::newRow("tar.lz") << QFINDTESTDATA("data/simplearchive.tar.lz") << compressedLzipTarMime; QTest::newRow("tar.lzo") << QFINDTESTDATA("data/simplearchive.tar.lzo") << compressedLzopTarMime; QTest::newRow("tar.lrz") << QFINDTESTDATA("data/simplearchive.tar.lrz") << compressedLrzipTarMime; + QTest::newRow("tar.lz4") << QFINDTESTDATA("data/simplearchive.tar.lz4") << compressedLz4TarMime; QTest::newRow("deb") << QFINDTESTDATA("data/smallarchive.deb") << debMimeType; QTest::newRow("xar") << QFINDTESTDATA("data/simplearchive.xar") << xarMimeType; diff --git a/kerfuffle/mime/kerfuffle.xml b/kerfuffle/mime/kerfuffle.xml --- a/kerfuffle/mime/kerfuffle.xml +++ b/kerfuffle/mime/kerfuffle.xml @@ -13,4 +13,9 @@ + + Tar archive (LZ4-compressed) + Tar archive (LZ4-compressed) + + diff --git a/kerfuffle/mimetypes.cpp b/kerfuffle/mimetypes.cpp --- a/kerfuffle/mimetypes.cpp +++ b/kerfuffle/mimetypes.cpp @@ -47,13 +47,36 @@ // we cannot rely on it when the archive extension is wrong; we need to validate by hand. if (fileinfo.completeSuffix().toLower().remove(QRegularExpression(QStringLiteral("[^a-z\\.]"))).contains(QStringLiteral("tar."))) { inputFile.chop(fileinfo.completeSuffix().length()); + QString cleanExtension(fileinfo.completeSuffix().toLower()); + + // tar.bz2 and tar.lz4 need special treatment since they contain numbers. + bool isBZ2 = false; + bool isLZ4 = false; + if (fileinfo.completeSuffix().toLower().contains(QStringLiteral("bz2"))) { + cleanExtension.remove(QStringLiteral("bz2")); + isBZ2 = true; + } + if (fileinfo.completeSuffix().toLower().contains(QStringLiteral("lz4"))) { + cleanExtension.remove(QStringLiteral("lz4")); + isLZ4 = true; + } + // We remove non-alpha chars from the filename extension, but not periods. // If the filename is e.g. "foo.tar.gz.1", we get the "foo.tar.gz." string, // so we need to manually drop the last period character from it. - QString cleanExtension = fileinfo.completeSuffix().toLower().remove(QRegularExpression(QStringLiteral("[^a-z\\.]"))); + cleanExtension.remove(QRegularExpression(QStringLiteral("[^a-z\\.]"))); if (cleanExtension.endsWith(QLatin1Char('.'))) { cleanExtension.chop(1); } + + // Re-add extension for tar.bz2 and tar.lz4. + if (isBZ2) { + cleanExtension.append(QStringLiteral(".bz2")); + } + if (isLZ4) { + cleanExtension.append(QStringLiteral(".lz4")); + } + inputFile += cleanExtension; qCDebug(ARK) << "Validated filename of compressed tar" << filename << "into filename" << inputFile; } @@ -83,7 +106,9 @@ (mimeFromExtension == db.mimeTypeForName(QStringLiteral("application/x-lzip-compressed-tar")) && mimeFromContent == db.mimeTypeForName(QStringLiteral("application/x-lzip"))) || (mimeFromExtension == db.mimeTypeForName(QStringLiteral("application/x-lrzip-compressed-tar")) && - mimeFromContent == db.mimeTypeForName(QStringLiteral("application/x-lrzip")))) { + mimeFromContent == db.mimeTypeForName(QStringLiteral("application/x-lrzip"))) || + (mimeFromExtension == db.mimeTypeForName(QStringLiteral("application/x-lz4-compressed-tar")) && + mimeFromContent == db.mimeTypeForName(QStringLiteral("application/x-lz4")))) { return mimeFromExtension; } diff --git a/kerfuffle/pluginmanager.cpp b/kerfuffle/pluginmanager.cpp --- a/kerfuffle/pluginmanager.cpp +++ b/kerfuffle/pluginmanager.cpp @@ -119,6 +119,11 @@ supported.remove(QStringLiteral("application/x-lrzip-compressed-tar")); } + // Remove entry for lz4-compressed tar if lz4 executable not found in path. + if (QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { + supported.remove(QStringLiteral("application/x-lz4-compressed-tar")); + } + return sortByComment(supported); } @@ -134,6 +139,11 @@ supported.remove(QStringLiteral("application/x-lrzip-compressed-tar")); } + // Remove entry for lz4-compressed tar if lz4 executable not found in path. + if (QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { + supported.remove(QStringLiteral("application/x-lz4-compressed-tar")); + } + return sortByComment(supported); } diff --git a/plugins/libarchive/CMakeLists.txt b/plugins/libarchive/CMakeLists.txt --- a/plugins/libarchive/CMakeLists.txt +++ b/plugins/libarchive/CMakeLists.txt @@ -6,6 +6,12 @@ set(SUPPORTED_LIBARCHIVE_READONLY_MIMETYPES "application/vnd.debian.binary-package;application/x-deb;application/x-cd-image;application/x-bcpio;application/x-cpio;application/x-cpio-compressed;application/x-sv4cpio;application/x-sv4crc;") set(SUPPORTED_LIBARCHIVE_READONLY_MIMETYPES "${SUPPORTED_LIBARCHIVE_READONLY_MIMETYPES}application/x-rpm;application/x-source-rpm;application/vnd.ms-cab-compressed;application/x-xar;") +if(LibArchive_FOUND) + if(LibArchive_VERSION VERSION_GREATER "3.1.2") + set(SUPPORTED_LIBARCHIVE_READWRITE_MIMETYPES "${SUPPORTED_LIBARCHIVE_READWRITE_MIMETYPES}application/x-lz4-compressed-tar;") + endif() +endif() + set(INSTALLED_LIBARCHIVE_PLUGINS "") set(kerfuffle_libarchive_readonly_SRCS libarchiveplugin.cpp readonlylibarchiveplugin.cpp ark_debug.cpp) @@ -46,6 +52,14 @@ \"application/x-tzo\", \"application/x-lrzip-compressed-tar") +if(LibArchive_FOUND) + if(LibArchive_VERSION VERSION_GREATER "3.1.2") + set(SUPPORTED_READWRITE_MIMETYPES + "${SUPPORTED_READWRITE_MIMETYPES}\", + \"application/x-lz4-compressed-tar") + endif() +endif() + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/kerfuffle_libarchive_readonly.json.cmake ${CMAKE_CURRENT_BINARY_DIR}/kerfuffle_libarchive_readonly.json) diff --git a/plugins/libarchive/kerfuffle_libarchive.json b/plugins/libarchive/kerfuffle_libarchive.json --- a/plugins/libarchive/kerfuffle_libarchive.json +++ b/plugins/libarchive/kerfuffle_libarchive.json @@ -44,7 +44,8 @@ "application/x-lzma-compressed-tar", "application/x-lzip-compressed-tar", "application/x-tzo", - "application/x-lrzip-compressed-tar" + "application/x-lrzip-compressed-tar", + "application/x-lz4-compressed-tar" ], "Name": "kerfuffle_libarchive", "Name[sv]": "Kerfuffle LibArchive", @@ -92,5 +93,10 @@ "CompressionLevelDefault": 6, "CompressionLevelMax": 9, "CompressionLevelMin": 0 + }, + "application/x-lz4-compressed-tar": { + "CompressionLevelDefault": 1, + "CompressionLevelMax": 9, + "CompressionLevelMin": 1 } -} \ No newline at end of file +} diff --git a/plugins/libarchive/readwritelibarchiveplugin.cpp b/plugins/libarchive/readwritelibarchiveplugin.cpp --- a/plugins/libarchive/readwritelibarchiveplugin.cpp +++ b/plugins/libarchive/readwritelibarchiveplugin.cpp @@ -98,7 +98,7 @@ archive_write_set_format_pax_restricted(arch_writer.data()); int ret; - bool isLrzip = false; + bool requiresExecutable = false; if (creatingNewFile) { if (filename().right(2).toUpper() == QLatin1String("GZ")) { qCDebug(ARK) << "Detected gzip compression for new file"; @@ -124,7 +124,11 @@ } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) { qCDebug(ARK) << "Detected lrzip compression for new file"; ret = archive_write_add_filter_lrzip(arch_writer.data()); - isLrzip = true; + requiresExecutable = true; + } else if (filename().right(3).toUpper() == QLatin1String("LZ4")) { + qCDebug(ARK) << "Detected lz4 compression for new file"; + ret = archive_write_add_filter_lz4(arch_writer.data()); + requiresExecutable = true; } else if (filename().right(3).toUpper() == QLatin1String("TAR")) { qCDebug(ARK) << "Detected no compression for new file (pure tar)"; ret = archive_write_add_filter_none(arch_writer.data()); @@ -134,8 +138,8 @@ } // Libarchive emits a warning for lrzip due to using external executable. - if ((isLrzip && ret != ARCHIVE_WARN) || - (!isLrzip && ret != ARCHIVE_OK)) { + if ((requiresExecutable && ret != ARCHIVE_WARN) || + (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; @@ -178,7 +182,11 @@ break; case ARCHIVE_FILTER_LRZIP: ret = archive_write_add_filter_lrzip(arch_writer.data()); - isLrzip = true; + requiresExecutable = true; + break; + case ARCHIVE_FILTER_LZ4: + ret = archive_write_add_filter_lz4(arch_writer.data()); + requiresExecutable = true; break; case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(arch_writer.data()); @@ -190,8 +198,8 @@ } // Libarchive emits a warning for lrzip due to using external executable. - if ((isLrzip && ret != ARCHIVE_WARN) || - (!isLrzip && ret != ARCHIVE_OK)) { + if ((requiresExecutable && ret != ARCHIVE_WARN) || + (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false;