Commit 69453c83 authored by Elvis Angelaccio's avatar Elvis Angelaccio
Browse files

Enable libarchive raw format to support "single-file" archives

libarchive provides a "raw format" that returns a single fake entry
called 'data' and the data for that entry is the whole input archive.

We can use this raw format to handle the "single-file" archives for
which we had the special KArchive-based singlefile plugins.
The advantage is that libarchive supports more formats than KArchive. In
particular with this commit we add support for the following new
formats that KArchive cannot handle:

- application/zlib
- application/x-lz4
- application/x-lzip
- application/x-lrzip
- application/x-lzop

Note 1: This fake 'data' entry requires a bunch of small changes in different
places, for which we use the recently 'displayName' property of
Archive::Entry.

Note 2: the documentation of archive_read_support_format_raw says that
it should not be always enabled, so we only enable it if we have a
"single-file" mimetype. For this purpose we generate in cmake a
LIBARCHIVE_RAW_MIMETYPES define that will be automatically updated when
adding new future mimetypes.

BUG: 458908
FIXED-IN: 22.12.0
parent a5d78f17
......@@ -223,7 +223,7 @@ QIcon Archive::Entry::icon() const
static QIcon directoryIcon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName());
m_icon = directoryIcon;
} else {
m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_name, QMimeDatabase::MatchMode::MatchExtension).iconName());
m_icon = QIcon::fromTheme(db.mimeTypeForFile(displayName(), QMimeDatabase::MatchMode::MatchExtension).iconName());
}
}
......
......@@ -36,7 +36,7 @@ class Archive::Entry : public QObject
Q_PROPERTY(QString fullPath MEMBER m_fullPath WRITE setFullPath)
/// The internal name of the entry in the archive.
Q_PROPERTY(QString name READ name)
/// The visible name of the entry in the UI. This is currently always equal to the name of the entry.
/// The visible name of the entry in the UI. This is usually (but not necessarily) equal to the name of the entry.
Q_PROPERTY(QString displayName READ displayName WRITE setDisplayName)
Q_PROPERTY(QString permissions MEMBER m_permissions)
Q_PROPERTY(QString owner MEMBER m_owner)
......
......@@ -598,7 +598,15 @@ Archive::Entry *TempExtractJob::entry() const
QString TempExtractJob::validatedFilePath() const
{
QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath();
QString path;
// For single-file archives the filepath of the extracted entry is the displayName and not the fullpath.
// TODO: find a better way to handle this.
// Should the ReadOnlyArchiveInterface tell us which is the actual filepath of the entry that it has extracted?
if (m_entry->displayName() != m_entry->name()) {
path = extractionDir() + QLatin1Char('/') + m_entry->displayName();
} else {
path = extractionDir() + QLatin1Char('/') + m_entry->fullPath();
}
// Make sure a maliciously crafted archive with parent folders named ".." do
// not cause the previewed file path to be located outside the temporary
......
......@@ -449,7 +449,7 @@ void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behav
const auto size = receivedEntry->property("size").toULongLong();
const auto compressedSize = receivedEntry->property("compressedSize").toULongLong();
for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) {
// Singlefile plugin doesn't report the uncompressed size.
// libarchive plugin doesn't report the uncompressed size for "single-file" archives.
if (i.key() == Size && size == 0 && compressedSize > 0) {
continue;
}
......
......@@ -139,10 +139,9 @@ bool ArkViewer::askViewAsPlainText(const QMimeType& mimeType)
return response != KMessageBox::Cancel;
}
void ArkViewer::view(const QString& fileName, const QString& entryPath)
void ArkViewer::view(const QString& fileName, const QString& entryPath, const QMimeType& mimeType)
{
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForFile(fileName);
qCDebug(ARK) << "viewing" << fileName << "from" << entryPath << "with mime type:" << mimeType.name();
const std::optional<KPluginMetaData> internalViewer = ArkViewer::getInternalViewer(mimeType.name());
......
......@@ -27,7 +27,7 @@ class ArkViewer : public KParts::MainWindow, public Ui::ArkViewer
public:
~ArkViewer() override;
static void view(const QString& fileName, const QString& entryPath = QString());
static void view(const QString& fileName, const QString& entryPath = QString(), const QMimeType& mimeType = QMimeType());
private:
explicit ArkViewer();
......
......@@ -90,7 +90,7 @@ void InfoPanel::setIndex(const QModelIndex& index)
if (entry->isDir()) {
mimeType = db.mimeTypeForName(QStringLiteral("inode/directory"));
} else {
mimeType = db.mimeTypeForFile(entry->fullPath(), QMimeDatabase::MatchExtension);
mimeType = db.mimeTypeForFile(entry->displayName(), QMimeDatabase::MatchExtension);
}
iconLabel->setPixmap(getPixmap(mimeType.iconName()));
......@@ -110,10 +110,7 @@ void InfoPanel::setIndex(const QModelIndex& index)
}
}
const QStringList nameParts = entry->fullPath().split(QLatin1Char( '/' ), Qt::SkipEmptyParts);
const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->fullPath();
fileName->setText(name);
fileName->setText(entry->displayName());
showMetaDataFor(index);
}
}
......@@ -161,7 +158,7 @@ void InfoPanel::showMetaDataFor(const QModelIndex &index)
if (entry->isDir()) {
mimeType = db.mimeTypeForName(QStringLiteral("inode/directory"));
} else {
mimeType = db.mimeTypeForFile(entry->fullPath(), QMimeDatabase::MatchExtension);
mimeType = db.mimeTypeForFile(entry->displayName(), QMimeDatabase::MatchExtension);
}
if (entry->isExecutable() && mimeType.isDefault()) {
......
......@@ -1077,7 +1077,13 @@ void Part::slotPreviewExtractedEntry(KJob *job)
Q_ASSERT(previewJob);
m_tmpExtractDirList << previewJob->tempDir();
ArkViewer::view(previewJob->validatedFilePath(), previewJob->entry()->fullPath(PathFormat::NoTrailingSlash));
// Use displayName to detect the mimetype, otherwise with single-file archives with fake 'data' entry the detected mime would be the default one.
QMimeType mimeType = QMimeDatabase().mimeTypeForFile(previewJob->entry()->displayName());
if (previewJob->entry()->displayName() != previewJob->entry()->name()) {
ArkViewer::view(previewJob->validatedFilePath(), previewJob->entry()->displayName(), mimeType);
} else {
ArkViewer::view(previewJob->validatedFilePath(), previewJob->entry()->fullPath(PathFormat::NoTrailingSlash), mimeType);
}
} else if (job->error() != KJob::KilledJobError) {
KMessageBox::error(widget(), job->errorString());
......
......@@ -6,6 +6,8 @@ set(SUPPORTED_LIBARCHIVE_READWRITE_MIMETYPES "${SUPPORTED_LIBARCHIVE_READWRITE_M
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;application/x-iso9660-appimage;application/x-archive;")
set(SUPPORTED_LIBARCHIVE_READWRITE_MIMETYPES "${SUPPORTED_LIBARCHIVE_READWRITE_MIMETYPES}application/x-zstd-compressed-tar;")
set(SUPPORTED_LIBARCHIVE_RAW_MIMETYPES "application/x-compress;application/gzip;application/x-bzip;application/zlib;application/zstd;application/x-lzma;application/x-xz;application/x-lz4;application/x-lzip;application/x-lrzip;application/x-lzop;")
set(SUPPORTED_LIBARCHIVE_READONLY_MIMETYPES "${SUPPORTED_LIBARCHIVE_READONLY_MIMETYPES}${SUPPORTED_LIBARCHIVE_RAW_MIMETYPES}")
set(INSTALLED_LIBARCHIVE_PLUGINS "")
......@@ -29,6 +31,17 @@ set(SUPPORTED_READONLY_MIMETYPES
\"application/x-sv4cpio\",
\"application/x-sv4crc\",
\"application/x-rpm\",
\"application/x-compress\",
\"application/gzip\",
\"application/x-bzip\",
\"application/x-lzma\",
\"application/x-xz\",
\"application/zlib\",
\"application/zstd\",
\"application/x-lz4\",
\"application/x-lzip\",
\"application/x-lrzip\",
\"application/x-lzop\",
\"application/x-source-rpm\",
\"application/vnd.debian.binary-package\",
\"application/vnd.ms-cab-compressed\",
......@@ -62,6 +75,11 @@ configure_file(
kerfuffle_add_plugin(kerfuffle_libarchive_readonly ${kerfuffle_libarchive_readonly_SRCS})
kerfuffle_add_plugin(kerfuffle_libarchive ${kerfuffle_libarchive_readwrite_SRCS})
# Concat the list of raw mimetypes in a single string, so that we can pass it as C++ define to the plugin.
# NOTE: cannot use ";" as separator since it breaks cmake badly.
list(JOIN SUPPORTED_LIBARCHIVE_RAW_MIMETYPES ":" RAW_MIMETYPES_CONCAT)
target_compile_definitions(kerfuffle_libarchive_readonly PRIVATE -DLIBARCHIVE_RAW_MIMETYPES="${RAW_MIMETYPES_CONCAT}")
target_link_libraries(kerfuffle_libarchive_readonly ${LibArchive_LIBRARIES})
target_link_libraries(kerfuffle_libarchive ${LibArchive_LIBRARIES})
......
......@@ -31,6 +31,11 @@ LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList &args)
connect(this, &ReadOnlyArchiveInterface::error, this, &LibarchivePlugin::slotRestoreWorkingDir);
connect(this, &ReadOnlyArchiveInterface::cancelled, this, &LibarchivePlugin::slotRestoreWorkingDir);
#ifdef LIBARCHIVE_RAW_MIMETYPES
m_rawMimetypes = QStringLiteral(LIBARCHIVE_RAW_MIMETYPES).split(QLatin1Char(':'), Qt::SkipEmptyParts);
qCDebug(ARK) << "# available raw mimetypes:" << m_rawMimetypes.count();
#endif
}
LibarchivePlugin::~LibarchivePlugin()
......@@ -72,7 +77,8 @@ bool LibarchivePlugin::list()
}
if (!m_emitNoEntries) {
emitEntryFromArchiveEntry(aentry);
const bool isRawFormat = (archive_format(m_archiveReader.data()) == ARCHIVE_FORMAT_RAW);
emitEntryFromArchiveEntry(aentry, isRawFormat);
}
m_extractedFilesSize += (qlonglong)archive_entry_size(aentry);
......@@ -125,6 +131,24 @@ bool LibarchivePlugin::emitCorruptArchive()
}
}
const QString LibarchivePlugin::uncompressedFileName() const
{
QFileInfo fileInfo(filename());
QString uncompressedName(fileInfo.fileName());
// Bug 252701: For .svgz just remove the terminal "z".
if (uncompressedName.endsWith(QLatin1String(".svgz"), Qt::CaseInsensitive)) {
uncompressedName.chop(1);
return uncompressedName;
}
if (!fileInfo.suffix().isEmpty()) {
return fileInfo.completeBaseName();
}
return uncompressedName + QLatin1String(".uncompressed");
}
bool LibarchivePlugin::addFiles(const QVector<Archive::Entry*> &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd)
{
Q_UNUSED(files)
......@@ -217,6 +241,7 @@ bool LibarchivePlugin::extractFiles(const QVector<Archive::Entry*> &files, const
bool overwriteAll = false; // Whether to overwrite all files
bool skipAll = false; // Whether to skip all files
bool dontPromptErrors = false; // Whether to prompt for errors
bool isSingleFile = false;
m_currentExtractedFilesSize = 0;
int extractedEntriesCount = 0;
int progressEntryCount = 0;
......@@ -249,7 +274,10 @@ bool LibarchivePlugin::extractFiles(const QVector<Archive::Entry*> &files, const
// entryName is the name inside the archive, full path
QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry)));
if (archive_format(m_archiveReader.data()) == ARCHIVE_FORMAT_RAW) {
isSingleFile = true;
qCDebug(ARK) << "Detected single file archive, entry path: " << entryName;
}
// Some archive types e.g. AppImage prepend all entries with "./" so remove this part.
if (entryName.startsWith(QLatin1String("./"))) {
entryName.remove(0, 2);
......@@ -286,6 +314,14 @@ bool LibarchivePlugin::extractFiles(const QVector<Archive::Entry*> &files, const
QFileInfo entryFI(entryName);
//qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry );
if (isSingleFile && fileBeingRenamed.isEmpty()) {
// Rename extracted file from libarchive-internal "data" name to the archive uncompressed name.
const QString uncompressedName = uncompressedFileName();
qCDebug(ARK) << "going to rename libarchive-internal 'data' filename to:" << uncompressedName;
archive_entry_copy_pathname(entry, QFile::encodeName(uncompressedName).constData());
entryFI = QFileInfo(uncompressedName);
}
const QString fileWithoutPath(entryFI.fileName());
// If we DON'T preserve paths, we cut the path and set the entryFI
// fileinfo to the one without the path.
......@@ -435,8 +471,16 @@ bool LibarchivePlugin::initializeReader()
return false;
}
if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) {
return false;
if (m_rawMimetypes.contains(mimetype().name())) {
qCDebug(ARK) << "Enabling RAW filter for mimetype: " << mimetype().name();
// Enable "raw" format only if we have a "raw mimetype", i.e. a single-file archive, as to not affect normal tar archives.
if (archive_read_support_format_raw(m_archiveReader.data()) != ARCHIVE_OK) {
return false;
}
} else {
if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) {
return false;
}
}
if (archive_read_open_filename(m_archiveReader.data(), QFile::encodeName(filename()).constData(), 10240) != ARCHIVE_OK) {
......@@ -448,7 +492,7 @@ bool LibarchivePlugin::initializeReader()
return true;
}
void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry)
void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry, bool isRawFormat)
{
auto e = new Archive::Entry();
......@@ -458,36 +502,42 @@ void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry)
e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry))));
#endif
const QString owner = QString::fromLatin1(archive_entry_uname(aentry));
if (!owner.isEmpty()) {
e->setProperty("owner", owner);
if (isRawFormat) {
e->setProperty("displayName", uncompressedFileName()); // libarchive reports a fake 'data' entry if raw format, ignore it and use the uncompressed filename.
e->setProperty("compressedSize", QFileInfo(filename()).size());
e->compressedSizeIsSet = true;
} else {
e->setProperty("owner", static_cast<qlonglong>(archive_entry_uid(aentry)));
}
const QString owner = QString::fromLatin1(archive_entry_uname(aentry));
if (!owner.isEmpty()) {
e->setProperty("owner", owner);
} else {
e->setProperty("owner", static_cast<qlonglong>(archive_entry_uid(aentry)));
}
const QString group = QString::fromLatin1(archive_entry_gname(aentry));
if (!group.isEmpty()) {
e->setProperty("group", group);
} else {
e->setProperty("group", static_cast<qlonglong>(archive_entry_gid(aentry)));
}
const QString group = QString::fromLatin1(archive_entry_gname(aentry));
if (!group.isEmpty()) {
e->setProperty("group", group);
} else {
e->setProperty("group", static_cast<qlonglong>(archive_entry_gid(aentry)));
}
const mode_t mode = archive_entry_mode(aentry);
if (mode != 0) {
e->setProperty("permissions", permissionsToString(mode));
}
e->setProperty("isExecutable", mode & (S_IXUSR | S_IXGRP | S_IXOTH));
const mode_t mode = archive_entry_mode(aentry);
if (mode != 0) {
e->setProperty("permissions", permissionsToString(mode));
}
e->setProperty("isExecutable", mode & (S_IXUSR | S_IXGRP | S_IXOTH));
e->compressedSizeIsSet = false;
e->setProperty("size", (qlonglong)archive_entry_size(aentry));
e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry)));
e->compressedSizeIsSet = false;
e->setProperty("size", (qlonglong)archive_entry_size(aentry));
e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry)));
if (archive_entry_symlink(aentry)) {
e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) ));
}
if (archive_entry_symlink(aentry)) {
e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) ));
}
auto time = static_cast<uint>(archive_entry_mtime(aentry));
e->setProperty("timestamp", QDateTime::fromSecsSinceEpoch(time));
auto time = static_cast<uint>(archive_entry_mtime(aentry));
e->setProperty("timestamp", QDateTime::fromSecsSinceEpoch(time));
}
if (archive_entry_sparse_reset(aentry)) {
qulonglong sparseSize = 0;
......
......@@ -62,7 +62,7 @@ protected:
typedef QScopedPointer<struct archive, ArchiveWriteCustomDeleter> ArchiveWrite;
bool initializeReader();
void emitEntryFromArchiveEntry(struct archive_entry *entry);
void emitEntryFromArchiveEntry(struct archive_entry *entry, bool isRawFormat = false);
void copyData(const QString& filename, struct archive *dest, bool partialprogress = true);
void copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress = true);
......@@ -76,6 +76,7 @@ private:
int extractionFlags() const;
QString convertCompressionName(const QString &method);
bool emitCorruptArchive();
const QString uncompressedFileName() const;
int m_cachedArchiveEntryCount;
qlonglong m_currentExtractedFilesSize;
......@@ -83,6 +84,7 @@ private:
qlonglong m_extractedFilesSize;
QVector<Archive::Entry*> m_emittedEntries;
QString m_oldWorkingDir;
QStringList m_rawMimetypes;
};
#endif // LIBARCHIVEPLUGIN_H
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment