QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCCompression.cc
Go to the documentation of this file.
1#include "QGCCompression.h"
2
3#include <QtCore/QCollator>
4#include <QtCore/QDir>
5#include <QtCore/QFile>
6#include <QtCore/QFileInfo>
7#include <QtCore/QLocale>
8#include <QtCore/QMimeDatabase>
9#include <QtCore/QMimeType>
10#include <QtCore/QtEndian>
11
12#include <algorithm>
13#include <cstring>
14
15#include "QGCDecompressDevice.h"
16#include "QGCFileHelper.h"
17#include "QGCLoggingCategory.h"
18#include "QGClibarchive.h"
19
20QGC_LOGGING_CATEGORY(QGCCompressionLog, "Utilities.QGCCompression")
21
22namespace QGCCompression {
23
24// ============================================================================
25// Format Detection Constants
26// ============================================================================
27
28namespace {
29
30// Minimum bytes needed for magic byte detection
31constexpr size_t kMinMagicBytes = 6;
32
33// TAR format detection offsets
34constexpr size_t kTarUstarOffset = 257;
35constexpr size_t kTarUstarMagicLen = 5;
36constexpr size_t kMinBytesForTar = 263; // kTarUstarOffset + kTarUstarMagicLen + 1
37
38// Magic byte sequences for format detection
39constexpr unsigned char kMagicZip[] = {0x50, 0x4B}; // "PK"
40constexpr unsigned char kZipLocalFile = 0x03; // PK\x03\x04 - local file header
41constexpr unsigned char kZipEmptyArchive = 0x05; // PK\x05\x06 - empty archive
42constexpr unsigned char kZipSpannedArchive = 0x07; // PK\x07\x08 - spanned archive
43constexpr unsigned char kMagic7z[] = {0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C}; // "7z\xBC\xAF'\x1C"
44constexpr unsigned char kMagicGzip[] = {0x1F, 0x8B};
45constexpr unsigned char kMagicXz[] = {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}; // "\xFD7zXZ\0"
46constexpr unsigned char kMagicZstd[] = {0x28, 0xB5, 0x2F, 0xFD};
47constexpr unsigned char kMagicBzip2[] = {0x42, 0x5A, 0x68}; // "BZh"
48constexpr unsigned char kMagicLz4[] = {0x04, 0x22, 0x4D, 0x18};
49
53Format formatFromMimeType(const QString& mimeType)
54{
55 // Archive formats
56 if (mimeType == QLatin1String("application/zip") || mimeType == QLatin1String("application/x-zip-compressed")) {
57 return Format::ZIP;
58 }
59 if (mimeType == QLatin1String("application/x-7z-compressed")) {
60 return Format::SEVENZ;
61 }
62 if (mimeType == QLatin1String("application/x-tar")) {
63 return Format::TAR;
64 }
65
66 // Compression formats
67 if (mimeType == QLatin1String("application/gzip") || mimeType == QLatin1String("application/x-gzip")) {
68 return Format::GZIP;
69 }
70 if (mimeType == QLatin1String("application/x-xz")) {
71 return Format::XZ;
72 }
73 if (mimeType == QLatin1String("application/zstd") || mimeType == QLatin1String("application/x-zstd")) {
74 return Format::ZSTD;
75 }
76 if (mimeType == QLatin1String("application/x-bzip2") || mimeType == QLatin1String("application/bzip2")) {
77 return Format::BZIP2;
78 }
79 if (mimeType == QLatin1String("application/x-lz4")) {
80 return Format::LZ4;
81 }
82
83 // Compound TAR formats (Qt may detect these as compressed TAR)
84 if (mimeType == QLatin1String("application/x-compressed-tar")) {
85 return Format::TAR_GZ; // Most common, fallback will refine
86 }
87 if (mimeType == QLatin1String("application/x-xz-compressed-tar")) {
88 return Format::TAR_XZ;
89 }
90 if (mimeType == QLatin1String("application/x-zstd-compressed-tar")) {
91 return Format::TAR_ZSTD;
92 }
93 if (mimeType == QLatin1String("application/x-bzip2-compressed-tar") ||
94 mimeType == QLatin1String("application/x-bzip-compressed-tar")) {
95 return Format::TAR_BZ2;
96 }
97 if (mimeType == QLatin1String("application/x-lz4-compressed-tar")) {
98 return Format::TAR_LZ4;
99 }
100
101 return Format::Auto;
102}
103
104} // namespace
105
106// ============================================================================
107// Thread-Local Error State
108// ============================================================================
109
110namespace {
111
112struct ThreadState
113{
114 Error error = Error::None;
115 QString errorString;
116 QString formatName;
117 QString filterName;
118};
119
120thread_local ThreadState t_state;
121
122void clearError()
123{
124 t_state.error = Error::None;
125 t_state.errorString.clear();
126}
127
128void setError(Error error, const QString& message = QString())
129{
130 t_state.error = error;
131 t_state.errorString = message;
132}
133
134void setFormatInfo(const QString& format, const QString& filter)
135{
136 t_state.formatName = format;
137 t_state.filterName = filter;
138}
139
140void clearFormatInfo()
141{
142 t_state.formatName.clear();
143 t_state.filterName.clear();
144}
145
146} // namespace
147
148// ============================================================================
149// Error Handling
150// ============================================================================
151
153{
154 return t_state.error;
155}
156
158{
159 if (!t_state.errorString.isEmpty()) {
160 return t_state.errorString;
161 }
162 return errorName(t_state.error);
163}
164
166{
167 switch (error) {
168 case Error::None:
169 return QStringLiteral("No error");
170 case Error::FileNotFound:
171 return QStringLiteral("File not found");
172 case Error::PermissionDenied:
173 return QStringLiteral("Permission denied");
174 case Error::InvalidArchive:
175 return QStringLiteral("Invalid or corrupt archive");
176 case Error::UnsupportedFormat:
177 return QStringLiteral("Unsupported format");
178 case Error::SizeLimitExceeded:
179 return QStringLiteral("Size limit exceeded");
180 case Error::Cancelled:
181 return QStringLiteral("Operation cancelled");
182 case Error::FileNotInArchive:
183 return QStringLiteral("File not found in archive");
184 case Error::IoError:
185 return QStringLiteral("I/O error");
186 case Error::InternalError:
187 return QStringLiteral("Internal error");
188 }
189 return QStringLiteral("Unknown error");
190}
191
193{
194 return t_state.formatName;
195}
196
198{
199 return t_state.filterName;
200}
201
202// ============================================================================
203// Internal Helpers
204// ============================================================================
205
210static bool validateFileInput(const QString& filePath, Format& format)
211{
212 clearError();
213 clearFormatInfo();
214
215 if (!QGCFileHelper::exists(filePath)) {
216 qCWarning(QGCCompressionLog) << "File does not exist:" << filePath;
217 setError(Error::FileNotFound, QStringLiteral("File does not exist: ") + filePath);
218 return false;
219 }
220 if (format == Format::Auto) {
221 format = detectFormat(filePath);
222 if (format == Format::Auto) {
223 qCWarning(QGCCompressionLog) << "Could not detect format for:" << filePath;
224 setError(Error::UnsupportedFormat, QStringLiteral("Could not detect format: ") + filePath);
225 return false;
226 }
227 }
228 return true;
229}
230
232static bool validateArchiveInput(const QString& archivePath, Format& format)
233{
234 if (!validateFileInput(archivePath, format)) {
235 return false;
236 }
237 if (!isArchiveFormat(format)) {
238 qCWarning(QGCCompressionLog) << "Not an archive format:" << formatName(format);
239 setError(Error::UnsupportedFormat, formatName(format) + QStringLiteral(" is not an archive format"));
240 return false;
241 }
242 return true;
243}
244
246static bool validateDeviceInput(QIODevice* device)
247{
248 clearError();
249 clearFormatInfo();
250
251 if (!device || !device->isOpen() || !device->isReadable()) {
252 qCWarning(QGCCompressionLog) << "Device is null, not open, or not readable";
253 setError(Error::IoError, QStringLiteral("Device is null, not open, or not readable"));
254 return false;
255 }
256 return true;
257}
258
264
265// ============================================================================
266// Format Detection
267// ============================================================================
268
270static Format detectFormatFromExtension(const QString& filePath)
271{
272 const QString lower = filePath.toLower();
273
274 // Check compound extensions first
275 if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) {
276 return Format::TAR_GZ;
277 }
278 if (lower.endsWith(".tar.xz") || lower.endsWith(".txz")) {
279 return Format::TAR_XZ;
280 }
281 if (lower.endsWith(".tar.zst") || lower.endsWith(".tar.zstd")) {
282 return Format::TAR_ZSTD;
283 }
284 if (lower.endsWith(".tar.bz2") || lower.endsWith(".tbz2") || lower.endsWith(".tbz")) {
285 return Format::TAR_BZ2;
286 }
287 if (lower.endsWith(".tar.lz4")) {
288 return Format::TAR_LZ4;
289 }
290
291 // Check single extensions
292 if (lower.endsWith(".zip")) {
293 return Format::ZIP;
294 }
295 if (lower.endsWith(".7z")) {
296 return Format::SEVENZ;
297 }
298 if (lower.endsWith(".gz") || lower.endsWith(".gzip")) {
299 return Format::GZIP;
300 }
301 if (lower.endsWith(".xz") || lower.endsWith(".lzma")) {
302 return Format::XZ;
303 }
304 if (lower.endsWith(".zst") || lower.endsWith(".zstd")) {
305 return Format::ZSTD;
306 }
307 if (lower.endsWith(".bz2") || lower.endsWith(".bzip2")) {
308 return Format::BZIP2;
309 }
310 if (lower.endsWith(".lz4")) {
311 return Format::LZ4;
312 }
313 if (lower.endsWith(".tar")) {
314 return Format::TAR;
315 }
316
317 return Format::Auto;
318}
319
320Format detectFormat(const QString& filePath, bool useContentFallback)
321{
322 // Try extension-based detection first
323 Format format = detectFormatFromExtension(filePath);
324 if (format != Format::Auto) {
325 return format;
326 }
327
328 // If extension detection failed and content fallback is enabled, try content-based
329 if (useContentFallback && QGCFileHelper::exists(filePath)) {
330 format = detectFormatFromFile(filePath);
331 if (format != Format::Auto) {
332 qCDebug(QGCCompressionLog) << "Format detected from content:" << formatName(format) << "for" << filePath;
333 }
334 }
335
336 return format;
337}
338
339Format detectFormatFromFile(const QString& filePath)
340{
341 // Handle Qt resources
342 if (QGCFileHelper::isQtResource(filePath)) {
343 QFile file(filePath);
344 if (!file.open(QIODevice::ReadOnly)) {
345 return Format::Auto;
346 }
347 return detectFormatFromData(file.read(512));
348 }
349
350 // Try QMimeDatabase first (uses both filename and content)
351 QMimeDatabase mimeDb;
352 QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
353
354 if (mimeType.isValid() && mimeType.name() != QLatin1String("application/octet-stream")) {
355 Format format = formatFromMimeType(mimeType.name());
356 if (format != Format::Auto) {
357 qCDebug(QGCCompressionLog) << "MIME detection:" << mimeType.name() << "->" << formatName(format);
358 return format;
359 }
360 }
361
362 // Fall back to reading raw bytes and using magic byte detection
363 QFile file(filePath);
364 if (!file.open(QIODevice::ReadOnly)) {
365 return Format::Auto;
366 }
367 return detectFormatFromData(file.read(512));
368}
369
370Format detectFormatFromData(const QByteArray& data)
371{
372 if (static_cast<size_t>(data.size()) < kMinMagicBytes) {
373 return Format::Auto;
374 }
375
376 const auto* bytes = reinterpret_cast<const unsigned char*>(data.constData());
377
378 // ZIP: PK\x03\x04 (local file) or PK\x05\x06 (empty) or PK\x07\x08 (spanned)
379 if (bytes[0] == kMagicZip[0] && bytes[1] == kMagicZip[1] &&
380 (bytes[2] == kZipLocalFile || bytes[2] == kZipEmptyArchive || bytes[2] == kZipSpannedArchive)) {
381 return Format::ZIP;
382 }
383
384 // 7-Zip
385 if (memcmp(bytes, kMagic7z, sizeof(kMagic7z)) == 0) {
386 return Format::SEVENZ;
387 }
388
389 // GZIP
390 if (memcmp(bytes, kMagicGzip, sizeof(kMagicGzip)) == 0) {
391 return Format::GZIP;
392 }
393
394 // XZ
395 if (memcmp(bytes, kMagicXz, sizeof(kMagicXz)) == 0) {
396 return Format::XZ;
397 }
398
399 // ZSTD
400 if (memcmp(bytes, kMagicZstd, sizeof(kMagicZstd)) == 0) {
401 return Format::ZSTD;
402 }
403
404 // BZip2
405 if (memcmp(bytes, kMagicBzip2, sizeof(kMagicBzip2)) == 0) {
406 return Format::BZIP2;
407 }
408
409 // LZ4
410 if (memcmp(bytes, kMagicLz4, sizeof(kMagicLz4)) == 0) {
411 return Format::LZ4;
412 }
413
414 // TAR: Check for ustar magic at offset 257
415 if (static_cast<size_t>(data.size()) >= kMinBytesForTar) {
416 if (data.mid(kTarUstarOffset, kTarUstarMagicLen) == "ustar") {
417 return Format::TAR;
418 }
419 }
420
421 // Fallback: Use QMimeDatabase for content-based detection
422 QMimeDatabase mimeDb;
423 QMimeType mimeType = mimeDb.mimeTypeForData(data);
424 if (mimeType.isValid() && mimeType.name() != QLatin1String("application/octet-stream")) {
425 Format format = formatFromMimeType(mimeType.name());
426 if (format != Format::Auto) {
427 qCDebug(QGCCompressionLog) << "MIME fallback detection:" << mimeType.name() << "->" << formatName(format);
428 return format;
429 }
430 }
431
432 return Format::Auto;
433}
434
435QString formatExtension(Format format)
436{
437 switch (format) {
438 case Format::ZIP:
439 return QStringLiteral(".zip");
440 case Format::SEVENZ:
441 return QStringLiteral(".7z");
442 case Format::GZIP:
443 return QStringLiteral(".gz");
444 case Format::XZ:
445 return QStringLiteral(".xz");
446 case Format::ZSTD:
447 return QStringLiteral(".zst");
448 case Format::BZIP2:
449 return QStringLiteral(".bz2");
450 case Format::LZ4:
451 return QStringLiteral(".lz4");
452 case Format::TAR:
453 return QStringLiteral(".tar");
454 case Format::TAR_GZ:
455 return QStringLiteral(".tar.gz");
456 case Format::TAR_XZ:
457 return QStringLiteral(".tar.xz");
458 case Format::TAR_ZSTD:
459 return QStringLiteral(".tar.zst");
460 case Format::TAR_BZ2:
461 return QStringLiteral(".tar.bz2");
462 case Format::TAR_LZ4:
463 return QStringLiteral(".tar.lz4");
464 case Format::Auto:
465 return QString();
466 }
467 Q_UNREACHABLE();
468}
469
470QString formatName(Format format)
471{
472 switch (format) {
473 case Format::Auto:
474 return QStringLiteral("Auto");
475 case Format::ZIP:
476 return QStringLiteral("ZIP");
477 case Format::SEVENZ:
478 return QStringLiteral("7-Zip");
479 case Format::GZIP:
480 return QStringLiteral("GZIP");
481 case Format::XZ:
482 return QStringLiteral("XZ/LZMA");
483 case Format::ZSTD:
484 return QStringLiteral("Zstandard");
485 case Format::BZIP2:
486 return QStringLiteral("BZip2");
487 case Format::LZ4:
488 return QStringLiteral("LZ4");
489 case Format::TAR:
490 return QStringLiteral("TAR");
491 case Format::TAR_GZ:
492 return QStringLiteral("TAR.GZ");
493 case Format::TAR_XZ:
494 return QStringLiteral("TAR.XZ");
495 case Format::TAR_ZSTD:
496 return QStringLiteral("TAR.ZSTD");
497 case Format::TAR_BZ2:
498 return QStringLiteral("TAR.BZ2");
499 case Format::TAR_LZ4:
500 return QStringLiteral("TAR.LZ4");
501 }
502 Q_UNREACHABLE();
503}
504
506{
507 switch (format) {
508 case Format::ZIP:
509 case Format::SEVENZ:
510 case Format::TAR:
511 case Format::TAR_GZ:
512 case Format::TAR_XZ:
513 case Format::TAR_ZSTD:
514 case Format::TAR_BZ2:
515 case Format::TAR_LZ4:
516 return true;
517 default:
518 return false;
519 }
520}
521
523{
524 switch (format) {
525 case Format::GZIP:
526 case Format::XZ:
527 case Format::ZSTD:
528 case Format::BZIP2:
529 case Format::LZ4:
530 return true;
531 default:
532 return false;
533 }
534}
535
536QString strippedPath(const QString& filePath)
537{
538 const Format format = detectFormat(filePath);
539 if (!isCompressionFormat(format)) {
540 return filePath;
541 }
542
543 const QString ext = formatExtension(format);
544 if (filePath.endsWith(ext, Qt::CaseInsensitive)) {
545 return filePath.left(filePath.size() - ext.size());
546 }
547
548 return filePath;
549}
550
551// ============================================================================
552// Single-File Decompression
553// ============================================================================
554
555bool decompressFile(const QString& inputPath, const QString& outputPath, Format format, ProgressCallback progress,
556 qint64 maxDecompressedBytes)
557{
558 if (!validateFileInput(inputPath, format)) {
559 return false;
560 }
561
562 // Determine output path
563 QString actualOutput = outputPath;
564 if (actualOutput.isEmpty()) {
565 QString ext = formatExtension(format);
566 if (inputPath.endsWith(ext, Qt::CaseInsensitive)) {
567 actualOutput = inputPath.left(inputPath.size() - ext.size());
568 } else {
569 actualOutput = inputPath + ".decompressed";
570 }
571 }
572
573 qCDebug(QGCCompressionLog) << "Decompressing" << inputPath << "to" << actualOutput << "using" << formatName(format);
574
575 // Single-file compression formats
576 if (isCompressionFormat(format)) {
577 const bool success =
578 QGClibarchive::decompressSingleFile(inputPath, actualOutput, progress, maxDecompressedBytes);
580 if (!success) {
581 setError(Error::IoError, QStringLiteral("Decompression failed: ") + inputPath);
582 }
583 return success;
584 }
585
586 // Archive formats - delegate to extractArchive with a warning
587 if (isArchiveFormat(format)) {
588 qCWarning(QGCCompressionLog) << formatName(format) << "is an archive format; use extractArchive() instead";
589 return extractArchive(inputPath, actualOutput, format, progress, maxDecompressedBytes);
590 }
591
592 qCWarning(QGCCompressionLog) << "Unsupported decompression format:" << formatName(format);
593 setError(Error::UnsupportedFormat, QStringLiteral("Unsupported decompression format: ") + formatName(format));
594 return false;
595}
596
597QString decompressIfNeeded(const QString& filePath, const QString& outputPath, bool removeOriginal)
598{
599 // If not a compressed file, return original path unchanged
600 if (!isCompressedFile(filePath)) {
601 return filePath;
602 }
603
604 // Determine output path
605 const QString actualOutput = outputPath.isEmpty() ? strippedPath(filePath) : outputPath;
606
607 // Attempt decompression
608 if (!decompressFile(filePath, actualOutput)) {
609 qCWarning(QGCCompressionLog) << "Decompression failed:" << filePath;
610 return QString();
611 }
612
613 if (removeOriginal && !QFile::remove(filePath)) {
614 qCWarning(QGCCompressionLog) << "Failed to remove original file:" << filePath;
615 }
616
617 return actualOutput;
618}
619
620QByteArray decompressData(const QByteArray& data, Format format, qint64 maxDecompressedBytes)
621{
622 if (data.isEmpty()) {
623 qCWarning(QGCCompressionLog) << "Cannot decompress empty data";
624 return {};
625 }
626
627 if (format == Format::Auto) {
628 format = detectFormatFromData(data);
629 if (format == Format::Auto) {
630 qCWarning(QGCCompressionLog) << "Could not detect format from data";
631 return {};
632 }
633 }
634
635 if (!isCompressionFormat(format)) {
636 qCWarning(QGCCompressionLog) << "Invalid decompression format:" << formatName(format);
637 setError(Error::UnsupportedFormat, formatName(format) + QStringLiteral(" is not a compression format"));
638 return {};
639 }
640
641 QByteArray result = QGClibarchive::decompressDataFromMemory(data, maxDecompressedBytes);
643 if (result.isEmpty()) {
644 setError(Error::IoError, QStringLiteral("Failed to decompress data"));
645 }
646 return result;
647}
648
649// ============================================================================
650// Archive Extraction
651// ============================================================================
652
653bool extractArchive(const QString& archivePath, const QString& outputDirectoryPath, Format format,
654 ProgressCallback progress, qint64 maxDecompressedBytes)
655{
656 if (!validateArchiveInput(archivePath, format)) {
657 return false;
658 }
659
660 qCDebug(QGCCompressionLog) << "Extracting" << formatName(format) << "archive" << archivePath << "to"
661 << outputDirectoryPath;
662
663 const bool success =
664 QGClibarchive::extractAnyArchive(archivePath, outputDirectoryPath, progress, maxDecompressedBytes);
666 if (!success) {
667 setError(Error::IoError, QStringLiteral("Failed to extract archive: ") + archivePath);
668 }
669 return success;
670}
671
672bool extractArchiveAtomic(const QString& archivePath, const QString& outputDirectoryPath, Format format,
673 ProgressCallback progress, qint64 maxDecompressedBytes)
674{
675 if (!validateArchiveInput(archivePath, format)) {
676 return false;
677 }
678
679 qCDebug(QGCCompressionLog) << "Atomically extracting" << formatName(format) << "archive" << archivePath
680 << "to" << outputDirectoryPath;
681
682 const bool success =
683 QGClibarchive::extractArchiveAtomic(archivePath, outputDirectoryPath, progress, maxDecompressedBytes);
685 if (!success) {
686 setError(Error::IoError, QStringLiteral("Failed to atomically extract archive: ") + archivePath);
687 }
688 return success;
689}
690
691bool extractArchiveFiltered(const QString& archivePath, const QString& outputDirectoryPath, EntryFilter filter,
692 ProgressCallback progress, qint64 maxDecompressedBytes)
693{
694 if (!filter) {
695 setError(Error::InternalError, QStringLiteral("No filter callback provided"));
696 return false;
697 }
698 Format format = Format::Auto;
699 if (!validateArchiveInput(archivePath, format)) {
700 return false;
701 }
702
703 qCDebug(QGCCompressionLog) << "Extracting archive with filter:" << archivePath;
704
705 const bool success =
706 QGClibarchive::extractWithFilter(archivePath, outputDirectoryPath, filter, progress, maxDecompressedBytes);
708 if (!success) {
709 setError(Error::IoError, QStringLiteral("Failed to extract archive: ") + archivePath);
710 }
711 return success;
712}
713
714QStringList listArchive(const QString& archivePath, Format format)
715{
716 if (!validateArchiveInput(archivePath, format)) {
717 return {};
718 }
719 QStringList entries = QGClibarchive::listArchiveEntries(archivePath);
720
721 // Natural sort: "file2.txt" before "file10.txt"
722 // Use English locale for consistent cross-platform numeric collation
723 QCollator collator{QLocale{QLocale::English}};
724 collator.setNumericMode(true);
725 collator.setCaseSensitivity(Qt::CaseInsensitive);
726 std::sort(entries.begin(), entries.end(), collator);
727
728 return entries;
729}
730
731QList<ArchiveEntry> listArchiveDetailed(const QString& archivePath, Format format)
732{
733 if (!validateArchiveInput(archivePath, format)) {
734 return {};
735 }
736 QList<ArchiveEntry> entries = QGClibarchive::listArchiveEntriesDetailed(archivePath);
737
738 // Natural sort by name: "file2.txt" before "file10.txt"
739 // Use English locale for consistent cross-platform numeric collation
740 QCollator collator{QLocale{QLocale::English}};
741 collator.setNumericMode(true);
742 collator.setCaseSensitivity(Qt::CaseInsensitive);
743 std::sort(entries.begin(), entries.end(), [&collator](const ArchiveEntry& a, const ArchiveEntry& b) {
744 return collator.compare(a.name, b.name) < 0;
745 });
746
747 return entries;
748}
749
750ArchiveStats getArchiveStats(const QString& archivePath, Format format)
751{
752 if (!validateArchiveInput(archivePath, format)) {
753 return {};
754 }
755 return QGClibarchive::getArchiveStats(archivePath);
756}
757
758bool validateArchive(const QString& archivePath, Format format)
759{
760 if (!validateArchiveInput(archivePath, format)) {
761 return false;
762 }
763 qCDebug(QGCCompressionLog) << "Validating archive:" << archivePath;
764 return QGClibarchive::validateArchive(archivePath);
765}
766
767bool fileExists(const QString& archivePath, const QString& fileName, Format format)
768{
769 if (fileName.isEmpty()) {
770 qCWarning(QGCCompressionLog) << "File name cannot be empty";
771 return false;
772 }
773 if (!validateArchiveInput(archivePath, format)) {
774 return false;
775 }
776 return QGClibarchive::fileExistsInArchive(archivePath, fileName);
777}
778
779bool extractFile(const QString& archivePath, const QString& fileName, const QString& outputPath, Format format)
780{
781 if (fileName.isEmpty()) {
782 qCWarning(QGCCompressionLog) << "File name cannot be empty";
783 return false;
784 }
785 if (!validateArchiveInput(archivePath, format)) {
786 return false;
787 }
788
789 const QString actualOutput = outputPath.isEmpty() ? QFileInfo(fileName).fileName() : outputPath;
790 qCDebug(QGCCompressionLog) << "Extracting" << fileName << "from" << archivePath << "to" << actualOutput;
791 return QGClibarchive::extractSingleFile(archivePath, fileName, actualOutput);
792}
793
794QByteArray extractFileData(const QString& archivePath, const QString& fileName, Format format)
795{
796 if (fileName.isEmpty()) {
797 qCWarning(QGCCompressionLog) << "File name cannot be empty";
798 return {};
799 }
800 if (!validateArchiveInput(archivePath, format)) {
801 return {};
802 }
803 qCDebug(QGCCompressionLog) << "Extracting" << fileName << "from" << archivePath << "to memory";
804 return QGClibarchive::extractFileToMemory(archivePath, fileName);
805}
806
807bool extractFiles(const QString& archivePath, const QStringList& fileNames, const QString& outputDirectoryPath,
808 Format format)
809{
810 if (fileNames.isEmpty()) {
811 return true;
812 }
813 if (!validateArchiveInput(archivePath, format)) {
814 return false;
815 }
816 qCDebug(QGCCompressionLog) << "Extracting" << fileNames.size() << "files from" << archivePath;
817 return QGClibarchive::extractMultipleFiles(archivePath, fileNames, outputDirectoryPath);
818}
819
820bool extractByPattern(const QString& archivePath, const QStringList& patterns, const QString& outputDirectoryPath,
821 QStringList* extractedFiles, Format format)
822{
823 if (patterns.isEmpty()) {
824 setError(Error::FileNotInArchive, QStringLiteral("No patterns provided"));
825 return false;
826 }
827 if (!validateArchiveInput(archivePath, format)) {
828 return false;
829 }
830 qCDebug(QGCCompressionLog) << "Extracting files matching patterns" << patterns << "from" << archivePath;
831 const bool success = QGClibarchive::extractByPattern(archivePath, patterns, outputDirectoryPath, extractedFiles);
832 if (!success) {
833 setError(Error::FileNotInArchive, QStringLiteral("No files matched patterns"));
834 }
835 return success;
836}
837
838// ============================================================================
839// QIODevice-based Operations
840// ============================================================================
841
842bool decompressFromDevice(QIODevice* device, const QString& outputPath, ProgressCallback progress,
843 qint64 maxDecompressedBytes)
844{
845 if (!validateDeviceInput(device)) {
846 return false;
847 }
848 qCDebug(QGCCompressionLog) << "Decompressing from device to" << outputPath;
849 const bool success = QGClibarchive::decompressFromDevice(device, outputPath, progress, maxDecompressedBytes);
851 if (!success) {
852 setError(Error::IoError, QStringLiteral("Failed to decompress from device"));
853 }
854 return success;
855}
856
857QByteArray decompressFromDevice(QIODevice* device, qint64 maxDecompressedBytes)
858{
859 if (!validateDeviceInput(device)) {
860 return {};
861 }
862 qCDebug(QGCCompressionLog) << "Decompressing from device to memory";
863 QByteArray result = QGClibarchive::decompressDataFromDevice(device, maxDecompressedBytes);
865 if (result.isEmpty()) {
866 setError(Error::IoError, QStringLiteral("Failed to decompress from device"));
867 }
868 return result;
869}
870
871bool extractFromDevice(QIODevice* device, const QString& outputDirectoryPath, ProgressCallback progress,
872 qint64 maxDecompressedBytes)
873{
874 if (!validateDeviceInput(device)) {
875 return false;
876 }
877 qCDebug(QGCCompressionLog) << "Extracting archive from device to" << outputDirectoryPath;
878 const bool success = QGClibarchive::extractFromDevice(device, outputDirectoryPath, progress, maxDecompressedBytes);
880 if (!success) {
881 setError(Error::IoError, QStringLiteral("Failed to extract archive from device"));
882 }
883 return success;
884}
885
886QByteArray extractFileDataFromDevice(QIODevice* device, const QString& fileName)
887{
888 if (!validateDeviceInput(device)) {
889 return {};
890 }
891 if (fileName.isEmpty()) {
892 qCWarning(QGCCompressionLog) << "File name cannot be empty";
893 setError(Error::FileNotInArchive, QStringLiteral("File name cannot be empty"));
894 return {};
895 }
896 qCDebug(QGCCompressionLog) << "Extracting" << fileName << "from device to memory";
897 QByteArray result = QGClibarchive::extractFileDataFromDevice(device, fileName);
899 if (result.isEmpty()) {
900 setError(Error::FileNotInArchive, QStringLiteral("File not found: ") + fileName);
901 }
902 return result;
903}
904
905// ============================================================================
906// In-Memory Compression
907// ============================================================================
908
909QByteArray compress(const QByteArray &data, CompressionLevel level)
910{
911 if (data.isEmpty() || level == CompressionLevel::None) {
912 return data;
913 }
914 return qCompress(data, static_cast<int>(level));
915}
916
917QByteArray uncompress(const QByteArray &data)
918{
919 if (data.isEmpty()) {
920 return {};
921 }
922 return qUncompress(data);
923}
924
925static constexpr quint8 kHeaderUncompressed = 0x00;
926static constexpr quint8 kHeaderCompressed = 0x01;
927
928static thread_local int s_lastCompressionRatio = 100; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
929
930QByteArray compressData(const QByteArray &data, CompressionLevel level, int minSize)
931{
932 if (level == CompressionLevel::None || data.size() < minSize) {
934 QByteArray result;
935 result.reserve(1 + data.size());
936 result.append(static_cast<char>(kHeaderUncompressed));
937 result.append(data);
938 return result;
939 }
940
941 const QByteArray compressed = qCompress(data, static_cast<int>(level));
942
943 if (!compressed.isEmpty() && compressed.size() < data.size()) {
944 s_lastCompressionRatio = (compressed.size() * 100) / data.size();
945 QByteArray result;
946 result.reserve(1 + compressed.size());
947 result.append(static_cast<char>(kHeaderCompressed));
948 result.append(compressed);
949 return result;
950 }
951
953 QByteArray result;
954 result.reserve(1 + data.size());
955 result.append(static_cast<char>(kHeaderUncompressed));
956 result.append(data);
957 return result;
958}
959
960QByteArray uncompressData(const QByteArray &data, qint64 maxDecompressedSize)
961{
962 if (data.isEmpty()) {
963 return {};
964 }
965
966 const auto header = static_cast<quint8>(data[0]);
967 const QByteArray payload = data.mid(1);
968
969 if (header == kHeaderUncompressed) {
970 return payload;
971 }
972
973 if (header == kHeaderCompressed) {
974 if (maxDecompressedSize > 0 && payload.size() >= 4) {
975 const quint32 declaredSize = qFromBigEndian<quint32>(payload.constData());
976 if (declaredSize > static_cast<quint32>(maxDecompressedSize)) {
977 qCWarning(QGCCompressionLog) << "Rejected decompression: declared size"
978 << declaredSize << "exceeds limit" << maxDecompressedSize;
979 return {};
980 }
981 }
982
983 const QByteArray result = qUncompress(payload);
984 if (result.isEmpty() && !payload.isEmpty()) {
985 qCWarning(QGCCompressionLog) << "Decompression failed";
986 }
987 return result;
988 }
989
990 qCWarning(QGCCompressionLog) << "Unknown compression header byte:" << header;
991 return {};
992}
993
994bool isDataCompressed(const QByteArray &data)
995{
996 return !data.isEmpty() && static_cast<quint8>(data[0]) == kHeaderCompressed;
997}
998
1000{
1002}
1003
1004QByteArray readFile(const QString &filePath, QString *errorString, qint64 maxBytes)
1005{
1006 if (!isCompressedFile(filePath)) {
1007 return QGCFileHelper::readFile(filePath, errorString, maxBytes);
1008 }
1009
1010 QGCDecompressDevice decompressor(filePath);
1011 if (!decompressor.open(QIODevice::ReadOnly)) {
1012 if (errorString != nullptr) {
1013 *errorString = QObject::tr("Failed to open compressed file: %1").arg(filePath);
1014 }
1015 return {};
1016 }
1017
1018 const QByteArray data = (maxBytes > 0) ? decompressor.read(maxBytes) : decompressor.readAll();
1019 decompressor.close();
1020 return data;
1021}
1022
1023QString computeFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
1024{
1025 if (filePath.isEmpty()) {
1026 qCWarning(QGCCompressionLog) << "computeFileHash: empty file path";
1027 return {};
1028 }
1029
1030 if (!isCompressedFile(filePath)) {
1031 return QGCFileHelper::computeFileHash(filePath, algorithm);
1032 }
1033
1034 QGCDecompressDevice decompressor(filePath);
1035 if (!decompressor.open(QIODevice::ReadOnly)) {
1036 qCWarning(QGCCompressionLog) << "computeFileHash: failed to open:" << filePath;
1037 return {};
1038 }
1039
1040 QCryptographicHash hash(algorithm);
1041 constexpr qint64 chunkSize = 65536;
1042 while (true) {
1043 const QByteArray buffer = decompressor.read(chunkSize);
1044 if (buffer.isEmpty()) {
1045 if (decompressor.atEnd()) {
1046 break;
1047 }
1048 qCWarning(QGCCompressionLog) << "computeFileHash: read error";
1049 decompressor.close();
1050 return {};
1051 }
1052 hash.addData(buffer);
1053 }
1054
1055 decompressor.close();
1056 return QString::fromLatin1(hash.result().toHex());
1057}
1058
1059// ============================================================================
1060// Compressed JSON Helpers
1061// ============================================================================
1062
1063bool looksLikeCompressedData(const QByteArray &data)
1064{
1066}
1067
1068QJsonDocument parseCompressedJson(const QByteArray &data, QJsonParseError *error)
1069{
1070 QByteArray jsonData = data;
1071
1072 if (looksLikeCompressedData(data)) {
1073 jsonData = decompressData(data);
1074 if (jsonData.isEmpty()) {
1075 if (error != nullptr) {
1076 error->error = QJsonParseError::IllegalValue;
1077 error->offset = 0;
1078 }
1079 return {};
1080 }
1081 }
1082
1083 return QJsonDocument::fromJson(jsonData, error);
1084}
1085
1086} // namespace QGCCompression
QString formatName
QString errorString
Error error
QString filterName
QIODevice wrapper for streaming decompression.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Private implementation details for QGCCompression.
QIODevice wrapper for streaming decompression of single-file formats.
bool open(OpenMode mode) override
QByteArray format(const QList< LogEntry > &entries, int fmt)
bool isCompressionFormat(Format format)
Check if format is a compression format (single stream)
QString computeFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
Hash file contents post-decompression.
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)
static constexpr quint8 kHeaderCompressed
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
bool looksLikeCompressedData(const QByteArray &data)
QByteArray uncompressData(const QByteArray &data, qint64 maxDecompressedSize)
int lastCompressionRatio()
Compression ratio from the last compressData() call (thread-local, percentage of original size).
QByteArray compressData(const QByteArray &data, CompressionLevel level, int minSize)
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)
static thread_local int s_lastCompressionRatio
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)
QByteArray compress(const QByteArray &data, CompressionLevel level)
static void captureFormatInfo()
Capture format detection info from QGClibarchive after an operation.
QByteArray uncompress(const QByteArray &data)
ArchiveStats getArchiveStats(const QString &archivePath, Format format)
QByteArray readFile(const QString &filePath, QString *errorString, qint64 maxBytes)
Read file contents, transparently decompressing .gz/.xz/.zst/.bz2/.lz4 files.
QList< ArchiveEntry > listArchiveDetailed(const QString &archivePath, Format format)
static bool validateFileInput(const QString &filePath, Format &format)
Error
Error codes for decompression operations.
QJsonDocument parseCompressedJson(const QByteArray &data, QJsonParseError *error)
Parse JSON from data that may be compressed. Auto-detects gzip/xz/zstd/bzip2/lz4.
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 isDataCompressed(const QByteArray &data)
Check if data has the compressed framing header.
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)
static constexpr quint8 kHeaderUncompressed
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)
QByteArray readFile(const QString &filePath, QString *errorString, qint64 maxBytes)
Read raw file contents.
bool exists(const QString &path)
QString computeFileHash(const QString &filePath, QCryptographicHash::Algorithm algorithm)
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.