3#include <QtCore/QCollator>
6#include <QtCore/QFileInfo>
7#include <QtCore/QLocale>
8#include <QtCore/QMimeDatabase>
9#include <QtCore/QMimeType>
29constexpr size_t kMinMagicBytes = 6;
32constexpr size_t kTarUstarOffset = 257;
33constexpr size_t kTarUstarMagicLen = 5;
34constexpr size_t kMinBytesForTar = 263;
37constexpr unsigned char kMagicZip[] = {0x50, 0x4B};
38constexpr unsigned char kZipLocalFile = 0x03;
39constexpr unsigned char kZipEmptyArchive = 0x05;
40constexpr unsigned char kZipSpannedArchive = 0x07;
41constexpr unsigned char kMagic7z[] = {0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C};
42constexpr unsigned char kMagicGzip[] = {0x1F, 0x8B};
43constexpr unsigned char kMagicXz[] = {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00};
44constexpr unsigned char kMagicZstd[] = {0x28, 0xB5, 0x2F, 0xFD};
45constexpr unsigned char kMagicBzip2[] = {0x42, 0x5A, 0x68};
46constexpr unsigned char kMagicLz4[] = {0x04, 0x22, 0x4D, 0x18};
51Format formatFromMimeType(
const QString& mimeType)
54 if (mimeType == QLatin1String(
"application/zip") || mimeType == QLatin1String(
"application/x-zip-compressed")) {
57 if (mimeType == QLatin1String(
"application/x-7z-compressed")) {
58 return Format::SEVENZ;
60 if (mimeType == QLatin1String(
"application/x-tar")) {
65 if (mimeType == QLatin1String(
"application/gzip") || mimeType == QLatin1String(
"application/x-gzip")) {
68 if (mimeType == QLatin1String(
"application/x-xz")) {
71 if (mimeType == QLatin1String(
"application/zstd") || mimeType == QLatin1String(
"application/x-zstd")) {
74 if (mimeType == QLatin1String(
"application/x-bzip2") || mimeType == QLatin1String(
"application/bzip2")) {
77 if (mimeType == QLatin1String(
"application/x-lz4")) {
82 if (mimeType == QLatin1String(
"application/x-compressed-tar")) {
83 return Format::TAR_GZ;
85 if (mimeType == QLatin1String(
"application/x-xz-compressed-tar")) {
86 return Format::TAR_XZ;
88 if (mimeType == QLatin1String(
"application/x-zstd-compressed-tar")) {
89 return Format::TAR_ZSTD;
91 if (mimeType == QLatin1String(
"application/x-bzip2-compressed-tar") ||
92 mimeType == QLatin1String(
"application/x-bzip-compressed-tar")) {
93 return Format::TAR_BZ2;
95 if (mimeType == QLatin1String(
"application/x-lz4-compressed-tar")) {
96 return Format::TAR_LZ4;
118thread_local ThreadState t_state;
122 t_state.error = Error::None;
123 t_state.errorString.clear();
126void setError(Error
error,
const QString& message = QString())
128 t_state.error =
error;
129 t_state.errorString = message;
132void setFormatInfo(
const QString& format,
const QString& filter)
134 t_state.formatName = format;
135 t_state.filterName = filter;
138void clearFormatInfo()
140 t_state.formatName.clear();
141 t_state.filterName.clear();
152 return t_state.error;
157 if (!t_state.errorString.isEmpty()) {
158 return t_state.errorString;
167 return QStringLiteral(
"No error");
168 case Error::FileNotFound:
169 return QStringLiteral(
"File not found");
170 case Error::PermissionDenied:
171 return QStringLiteral(
"Permission denied");
172 case Error::InvalidArchive:
173 return QStringLiteral(
"Invalid or corrupt archive");
174 case Error::UnsupportedFormat:
175 return QStringLiteral(
"Unsupported format");
176 case Error::SizeLimitExceeded:
177 return QStringLiteral(
"Size limit exceeded");
178 case Error::Cancelled:
179 return QStringLiteral(
"Operation cancelled");
180 case Error::FileNotInArchive:
181 return QStringLiteral(
"File not found in archive");
183 return QStringLiteral(
"I/O error");
184 case Error::InternalError:
185 return QStringLiteral(
"Internal error");
187 return QStringLiteral(
"Unknown error");
192 return t_state.formatName;
197 return t_state.filterName;
214 qCWarning(QGCCompressionLog) <<
"File does not exist:" << filePath;
215 setError(Error::FileNotFound, QStringLiteral(
"File does not exist: ") + filePath);
218 if (format == Format::Auto) {
220 if (format == Format::Auto) {
221 qCWarning(QGCCompressionLog) <<
"Could not detect format for:" << filePath;
222 setError(Error::UnsupportedFormat, QStringLiteral(
"Could not detect format: ") + filePath);
236 qCWarning(QGCCompressionLog) <<
"Not an archive format:" <<
formatName(format);
237 setError(Error::UnsupportedFormat,
formatName(format) + QStringLiteral(
" is not an archive format"));
249 if (!device || !device->isOpen() || !device->isReadable()) {
250 qCWarning(QGCCompressionLog) <<
"Device is null, not open, or not readable";
251 setError(Error::IoError, QStringLiteral(
"Device is null, not open, or not readable"));
270 const QString lower = filePath.toLower();
273 if (lower.endsWith(
".tar.gz") || lower.endsWith(
".tgz")) {
274 return Format::TAR_GZ;
276 if (lower.endsWith(
".tar.xz") || lower.endsWith(
".txz")) {
277 return Format::TAR_XZ;
279 if (lower.endsWith(
".tar.zst") || lower.endsWith(
".tar.zstd")) {
280 return Format::TAR_ZSTD;
282 if (lower.endsWith(
".tar.bz2") || lower.endsWith(
".tbz2") || lower.endsWith(
".tbz")) {
283 return Format::TAR_BZ2;
285 if (lower.endsWith(
".tar.lz4")) {
286 return Format::TAR_LZ4;
290 if (lower.endsWith(
".zip")) {
293 if (lower.endsWith(
".7z")) {
294 return Format::SEVENZ;
296 if (lower.endsWith(
".gz") || lower.endsWith(
".gzip")) {
299 if (lower.endsWith(
".xz") || lower.endsWith(
".lzma")) {
302 if (lower.endsWith(
".zst") || lower.endsWith(
".zstd")) {
305 if (lower.endsWith(
".bz2") || lower.endsWith(
".bzip2")) {
306 return Format::BZIP2;
308 if (lower.endsWith(
".lz4")) {
311 if (lower.endsWith(
".tar")) {
322 if (format != Format::Auto) {
329 if (format != Format::Auto) {
330 qCDebug(QGCCompressionLog) <<
"Format detected from content:" <<
formatName(format) <<
"for" << filePath;
341 QFile file(filePath);
342 if (!file.open(QIODevice::ReadOnly)) {
349 QMimeDatabase mimeDb;
350 QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
352 if (mimeType.isValid() && mimeType.name() != QLatin1String(
"application/octet-stream")) {
353 Format format = formatFromMimeType(mimeType.name());
354 if (format != Format::Auto) {
355 qCDebug(QGCCompressionLog) <<
"MIME detection:" << mimeType.name() <<
"->" <<
formatName(format);
361 QFile file(filePath);
362 if (!file.open(QIODevice::ReadOnly)) {
370 if (
static_cast<size_t>(data.size()) < kMinMagicBytes) {
374 const auto* bytes =
reinterpret_cast<const unsigned char*
>(data.constData());
377 if (bytes[0] == kMagicZip[0] && bytes[1] == kMagicZip[1] &&
378 (bytes[2] == kZipLocalFile || bytes[2] == kZipEmptyArchive || bytes[2] == kZipSpannedArchive)) {
383 if (memcmp(bytes, kMagic7z,
sizeof(kMagic7z)) == 0) {
384 return Format::SEVENZ;
388 if (memcmp(bytes, kMagicGzip,
sizeof(kMagicGzip)) == 0) {
393 if (memcmp(bytes, kMagicXz,
sizeof(kMagicXz)) == 0) {
398 if (memcmp(bytes, kMagicZstd,
sizeof(kMagicZstd)) == 0) {
403 if (memcmp(bytes, kMagicBzip2,
sizeof(kMagicBzip2)) == 0) {
404 return Format::BZIP2;
408 if (memcmp(bytes, kMagicLz4,
sizeof(kMagicLz4)) == 0) {
413 if (
static_cast<size_t>(data.size()) >= kMinBytesForTar) {
414 if (data.mid(kTarUstarOffset, kTarUstarMagicLen) ==
"ustar") {
420 QMimeDatabase mimeDb;
421 QMimeType mimeType = mimeDb.mimeTypeForData(data);
422 if (mimeType.isValid() && mimeType.name() != QLatin1String(
"application/octet-stream")) {
423 Format format = formatFromMimeType(mimeType.name());
424 if (format != Format::Auto) {
425 qCDebug(QGCCompressionLog) <<
"MIME fallback detection:" << mimeType.name() <<
"->" <<
formatName(format);
437 return QStringLiteral(
".zip");
439 return QStringLiteral(
".7z");
441 return QStringLiteral(
".gz");
443 return QStringLiteral(
".xz");
445 return QStringLiteral(
".zst");
447 return QStringLiteral(
".bz2");
449 return QStringLiteral(
".lz4");
451 return QStringLiteral(
".tar");
453 return QStringLiteral(
".tar.gz");
455 return QStringLiteral(
".tar.xz");
456 case Format::TAR_ZSTD:
457 return QStringLiteral(
".tar.zst");
458 case Format::TAR_BZ2:
459 return QStringLiteral(
".tar.bz2");
460 case Format::TAR_LZ4:
461 return QStringLiteral(
".tar.lz4");
472 return QStringLiteral(
"Auto");
474 return QStringLiteral(
"ZIP");
476 return QStringLiteral(
"7-Zip");
478 return QStringLiteral(
"GZIP");
480 return QStringLiteral(
"XZ/LZMA");
482 return QStringLiteral(
"Zstandard");
484 return QStringLiteral(
"BZip2");
486 return QStringLiteral(
"LZ4");
488 return QStringLiteral(
"TAR");
490 return QStringLiteral(
"TAR.GZ");
492 return QStringLiteral(
"TAR.XZ");
493 case Format::TAR_ZSTD:
494 return QStringLiteral(
"TAR.ZSTD");
495 case Format::TAR_BZ2:
496 return QStringLiteral(
"TAR.BZ2");
497 case Format::TAR_LZ4:
498 return QStringLiteral(
"TAR.LZ4");
511 case Format::TAR_ZSTD:
512 case Format::TAR_BZ2:
513 case Format::TAR_LZ4:
542 if (filePath.endsWith(ext, Qt::CaseInsensitive)) {
543 return filePath.left(filePath.size() - ext.size());
554 qint64 maxDecompressedBytes)
561 QString actualOutput = outputPath;
562 if (actualOutput.isEmpty()) {
564 if (inputPath.endsWith(ext, Qt::CaseInsensitive)) {
565 actualOutput = inputPath.left(inputPath.size() - ext.size());
567 actualOutput = inputPath +
".decompressed";
571 qCDebug(QGCCompressionLog) <<
"Decompressing" << inputPath <<
"to" << actualOutput <<
"using" <<
formatName(format);
579 setError(Error::IoError, QStringLiteral(
"Decompression failed: ") + inputPath);
586 qCWarning(QGCCompressionLog) <<
formatName(format) <<
"is an archive format; use extractArchive() instead";
587 return extractArchive(inputPath, actualOutput, format, progress, maxDecompressedBytes);
590 qCWarning(QGCCompressionLog) <<
"Unsupported decompression format:" <<
formatName(format);
591 setError(Error::UnsupportedFormat, QStringLiteral(
"Unsupported decompression format: ") +
formatName(format));
603 const QString actualOutput = outputPath.isEmpty() ?
strippedPath(filePath) : outputPath;
607 qCWarning(QGCCompressionLog) <<
"Decompression failed:" << filePath;
611 if (removeOriginal && !QFile::remove(filePath)) {
612 qCWarning(QGCCompressionLog) <<
"Failed to remove original file:" << filePath;
620 if (data.isEmpty()) {
621 qCWarning(QGCCompressionLog) <<
"Cannot decompress empty data";
625 if (format == Format::Auto) {
627 if (format == Format::Auto) {
628 qCWarning(QGCCompressionLog) <<
"Could not detect format from data";
634 qCWarning(QGCCompressionLog) <<
"Invalid decompression format:" <<
formatName(format);
635 setError(Error::UnsupportedFormat,
formatName(format) + QStringLiteral(
" is not a compression format"));
641 if (result.isEmpty()) {
642 setError(Error::IoError, QStringLiteral(
"Failed to decompress data"));
658 qCDebug(QGCCompressionLog) <<
"Extracting" <<
formatName(format) <<
"archive" << archivePath <<
"to"
659 << outputDirectoryPath;
665 setError(Error::IoError, QStringLiteral(
"Failed to extract archive: ") + archivePath);
677 qCDebug(QGCCompressionLog) <<
"Atomically extracting" <<
formatName(format) <<
"archive" << archivePath
678 <<
"to" << outputDirectoryPath;
684 setError(Error::IoError, QStringLiteral(
"Failed to atomically extract archive: ") + archivePath);
693 setError(Error::InternalError, QStringLiteral(
"No filter callback provided"));
696 Format format = Format::Auto;
701 qCDebug(QGCCompressionLog) <<
"Extracting archive with filter:" << archivePath;
707 setError(Error::IoError, QStringLiteral(
"Failed to extract archive: ") + archivePath);
721 QCollator collator{QLocale{QLocale::English}};
722 collator.setNumericMode(
true);
723 collator.setCaseSensitivity(Qt::CaseInsensitive);
724 std::sort(entries.begin(), entries.end(), collator);
738 QCollator collator{QLocale{QLocale::English}};
739 collator.setNumericMode(
true);
740 collator.setCaseSensitivity(Qt::CaseInsensitive);
742 return collator.compare(a.name, b.name) < 0;
761 qCDebug(QGCCompressionLog) <<
"Validating archive:" << archivePath;
767 if (fileName.isEmpty()) {
768 qCWarning(QGCCompressionLog) <<
"File name cannot be empty";
777bool extractFile(
const QString& archivePath,
const QString& fileName,
const QString& outputPath,
Format format)
779 if (fileName.isEmpty()) {
780 qCWarning(QGCCompressionLog) <<
"File name cannot be empty";
787 const QString actualOutput = outputPath.isEmpty() ? QFileInfo(fileName).fileName() : outputPath;
788 qCDebug(QGCCompressionLog) <<
"Extracting" << fileName <<
"from" << archivePath <<
"to" << actualOutput;
794 if (fileName.isEmpty()) {
795 qCWarning(QGCCompressionLog) <<
"File name cannot be empty";
801 qCDebug(QGCCompressionLog) <<
"Extracting" << fileName <<
"from" << archivePath <<
"to memory";
805bool extractFiles(
const QString& archivePath,
const QStringList& fileNames,
const QString& outputDirectoryPath,
808 if (fileNames.isEmpty()) {
814 qCDebug(QGCCompressionLog) <<
"Extracting" << fileNames.size() <<
"files from" << archivePath;
818bool extractByPattern(
const QString& archivePath,
const QStringList& patterns,
const QString& outputDirectoryPath,
819 QStringList* extractedFiles,
Format format)
821 if (patterns.isEmpty()) {
822 setError(Error::FileNotInArchive, QStringLiteral(
"No patterns provided"));
828 qCDebug(QGCCompressionLog) <<
"Extracting files matching patterns" << patterns <<
"from" << archivePath;
831 setError(Error::FileNotInArchive, QStringLiteral(
"No files matched patterns"));
841 qint64 maxDecompressedBytes)
846 qCDebug(QGCCompressionLog) <<
"Decompressing from device to" << outputPath;
850 setError(Error::IoError, QStringLiteral(
"Failed to decompress from device"));
860 qCDebug(QGCCompressionLog) <<
"Decompressing from device to memory";
863 if (result.isEmpty()) {
864 setError(Error::IoError, QStringLiteral(
"Failed to decompress from device"));
870 qint64 maxDecompressedBytes)
875 qCDebug(QGCCompressionLog) <<
"Extracting archive from device to" << outputDirectoryPath;
879 setError(Error::IoError, QStringLiteral(
"Failed to extract archive from device"));
889 if (fileName.isEmpty()) {
890 qCWarning(QGCCompressionLog) <<
"File name cannot be empty";
891 setError(Error::FileNotInArchive, QStringLiteral(
"File name cannot be empty"));
894 qCDebug(QGCCompressionLog) <<
"Extracting" << fileName <<
"from device to memory";
897 if (result.isEmpty()) {
898 setError(Error::FileNotInArchive, QStringLiteral(
"File not found: ") + fileName);
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Private implementation details for QGCCompression.
bool isCompressionFormat(Format format)
Check if format is a compression format (single stream)
bool fileExists(const QString &archivePath, const QString &fileName, Format format)
static bool validateDeviceInput(QIODevice *device)
Validate device input for streaming operations.
QByteArray decompressData(const QByteArray &data, Format format, qint64 maxDecompressedBytes)
bool extractArchiveAtomic(const QString &archivePath, const QString &outputDirectoryPath, Format format, ProgressCallback progress, qint64 maxDecompressedBytes)
bool extractArchive(const QString &archivePath, const QString &outputDirectoryPath, Format format, ProgressCallback progress, qint64 maxDecompressedBytes)
bool decompressFile(const QString &inputPath, const QString &outputPath, Format format, ProgressCallback progress, qint64 maxDecompressedBytes)
std::function< bool(const ArchiveEntry &entry)> EntryFilter
QStringList listArchive(const QString &archivePath, Format format)
bool extractArchiveFiltered(const QString &archivePath, const QString &outputDirectoryPath, EntryFilter filter, ProgressCallback progress, qint64 maxDecompressedBytes)
bool isArchiveFormat(Format format)
Check if format is an archive (contains multiple files)
bool extractByPattern(const QString &archivePath, const QStringList &patterns, const QString &outputDirectoryPath, QStringList *extractedFiles, Format format)
Format detectFormatFromData(const QByteArray &data)
QString lastErrorString()
Get a human-readable error message from the last operation (thread-local)
QString strippedPath(const QString &filePath)
QString detectedFilterName()
bool isCompressedFile(const QString &filePath)
Check if file path indicates a compressed file (.gz, .xz, .zst)
static bool validateArchiveInput(const QString &archivePath, Format &format)
Validate archive input: file exists, format detected, and is archive format.
QString errorName(Error error)
Get a human-readable name for an error code.
bool decompressFromDevice(QIODevice *device, const QString &outputPath, ProgressCallback progress, qint64 maxDecompressedBytes)
Format
Archive and compression format types (for decompression)
QByteArray extractFileData(const QString &archivePath, const QString &fileName, Format format)
static void captureFormatInfo()
Capture format detection info from QGClibarchive after an operation.
ArchiveStats getArchiveStats(const QString &archivePath, Format format)
QList< ArchiveEntry > listArchiveDetailed(const QString &archivePath, Format format)
static bool validateFileInput(const QString &filePath, Format &format)
Error
Error codes for decompression operations.
bool validateArchive(const QString &archivePath, Format format)
bool extractFile(const QString &archivePath, const QString &fileName, const QString &outputPath, Format format)
std::function< bool(qint64 bytesProcessed, qint64 totalBytes)> ProgressCallback
Error lastError()
Get the error code from the last operation (thread-local)
bool extractFromDevice(QIODevice *device, const QString &outputDirectoryPath, ProgressCallback progress, qint64 maxDecompressedBytes)
QByteArray extractFileDataFromDevice(QIODevice *device, const QString &fileName)
bool extractFiles(const QString &archivePath, const QStringList &fileNames, const QString &outputDirectoryPath, Format format)
QString detectedFormatName()
static Format detectFormatFromExtension(const QString &filePath)
Extension-based format detection (internal helper)
QString decompressIfNeeded(const QString &filePath, const QString &outputPath, bool removeOriginal)
QString formatExtension(Format format)
Get file extension for a format.
Format detectFormatFromFile(const QString &filePath)
Format detectFormat(const QString &filePath, bool useContentFallback)
bool isQtResource(const QString &path)
bool exists(const QString &path)
ArchiveStats getArchiveStats(const QString &archivePath)
bool extractMultipleFiles(const QString &archivePath, const QStringList &fileNames, const QString &outputDirectoryPath)
QString lastDetectedFormatName()
bool extractWithFilter(const QString &archivePath, const QString &outputDirectoryPath, EntryFilter filter, ProgressCallback progress, qint64 maxBytes)
QString lastDetectedFilterName()
bool decompressSingleFile(const QString &inputPath, const QString &outputPath, ProgressCallback progress, qint64 maxBytes)
bool extractFromDevice(QIODevice *device, const QString &outputDirectoryPath, ProgressCallback progress, qint64 maxBytes)
QStringList listArchiveEntries(const QString &archivePath)
bool extractSingleFile(const QString &archivePath, const QString &fileName, const QString &outputPath)
QByteArray decompressDataFromDevice(QIODevice *device, qint64 maxBytes)
QList< ArchiveEntry > listArchiveEntriesDetailed(const QString &archivePath)
bool extractArchiveAtomic(const QString &archivePath, const QString &outputDirectoryPath, ProgressCallback progress, qint64 maxBytes)
QByteArray extractFileToMemory(const QString &archivePath, const QString &fileName)
QByteArray decompressDataFromMemory(const QByteArray &data, qint64 maxBytes)
bool validateArchive(const QString &archivePath)
bool fileExistsInArchive(const QString &archivePath, const QString &fileName)
bool decompressFromDevice(QIODevice *device, const QString &outputPath, ProgressCallback progress, qint64 maxBytes)
bool extractAnyArchive(const QString &archivePath, const QString &outputDirectoryPath, ProgressCallback progress, qint64 maxBytes)
QByteArray extractFileDataFromDevice(QIODevice *device, const QString &fileName)
bool extractByPattern(const QString &archivePath, const QStringList &patterns, const QString &outputDirectoryPath, QStringList *extractedFiles)
Metadata for a single entry in an archive.
Summary statistics for an archive.