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
11#include <algorithm>
12#include <cstring>
13
14#include "QGCFileHelper.h"
15#include "QGCLoggingCategory.h"
16#include "QGClibarchive.h"
17
18QGC_LOGGING_CATEGORY(QGCCompressionLog, "Utilities.QGCCompression")
19
20namespace QGCCompression {
21
22// ============================================================================
23// Format Detection Constants
24// ============================================================================
25
26namespace {
27
28// Minimum bytes needed for magic byte detection
29constexpr size_t kMinMagicBytes = 6;
30
31// TAR format detection offsets
32constexpr size_t kTarUstarOffset = 257;
33constexpr size_t kTarUstarMagicLen = 5;
34constexpr size_t kMinBytesForTar = 263; // kTarUstarOffset + kTarUstarMagicLen + 1
35
36// Magic byte sequences for format detection
37constexpr unsigned char kMagicZip[] = {0x50, 0x4B}; // "PK"
38constexpr unsigned char kZipLocalFile = 0x03; // PK\x03\x04 - local file header
39constexpr unsigned char kZipEmptyArchive = 0x05; // PK\x05\x06 - empty archive
40constexpr unsigned char kZipSpannedArchive = 0x07; // PK\x07\x08 - spanned archive
41constexpr unsigned char kMagic7z[] = {0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C}; // "7z\xBC\xAF'\x1C"
42constexpr unsigned char kMagicGzip[] = {0x1F, 0x8B};
43constexpr unsigned char kMagicXz[] = {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}; // "\xFD7zXZ\0"
44constexpr unsigned char kMagicZstd[] = {0x28, 0xB5, 0x2F, 0xFD};
45constexpr unsigned char kMagicBzip2[] = {0x42, 0x5A, 0x68}; // "BZh"
46constexpr unsigned char kMagicLz4[] = {0x04, 0x22, 0x4D, 0x18};
47
51Format formatFromMimeType(const QString& mimeType)
52{
53 // Archive formats
54 if (mimeType == QLatin1String("application/zip") || mimeType == QLatin1String("application/x-zip-compressed")) {
55 return Format::ZIP;
56 }
57 if (mimeType == QLatin1String("application/x-7z-compressed")) {
58 return Format::SEVENZ;
59 }
60 if (mimeType == QLatin1String("application/x-tar")) {
61 return Format::TAR;
62 }
63
64 // Compression formats
65 if (mimeType == QLatin1String("application/gzip") || mimeType == QLatin1String("application/x-gzip")) {
66 return Format::GZIP;
67 }
68 if (mimeType == QLatin1String("application/x-xz")) {
69 return Format::XZ;
70 }
71 if (mimeType == QLatin1String("application/zstd") || mimeType == QLatin1String("application/x-zstd")) {
72 return Format::ZSTD;
73 }
74 if (mimeType == QLatin1String("application/x-bzip2") || mimeType == QLatin1String("application/bzip2")) {
75 return Format::BZIP2;
76 }
77 if (mimeType == QLatin1String("application/x-lz4")) {
78 return Format::LZ4;
79 }
80
81 // Compound TAR formats (Qt may detect these as compressed TAR)
82 if (mimeType == QLatin1String("application/x-compressed-tar")) {
83 return Format::TAR_GZ; // Most common, fallback will refine
84 }
85 if (mimeType == QLatin1String("application/x-xz-compressed-tar")) {
86 return Format::TAR_XZ;
87 }
88 if (mimeType == QLatin1String("application/x-zstd-compressed-tar")) {
89 return Format::TAR_ZSTD;
90 }
91 if (mimeType == QLatin1String("application/x-bzip2-compressed-tar") ||
92 mimeType == QLatin1String("application/x-bzip-compressed-tar")) {
93 return Format::TAR_BZ2;
94 }
95 if (mimeType == QLatin1String("application/x-lz4-compressed-tar")) {
96 return Format::TAR_LZ4;
97 }
98
99 return Format::Auto;
100}
101
102} // namespace
103
104// ============================================================================
105// Thread-Local Error State
106// ============================================================================
107
108namespace {
109
110struct ThreadState
111{
112 Error error = Error::None;
113 QString errorString;
114 QString formatName;
115 QString filterName;
116};
117
118thread_local ThreadState t_state;
119
120void clearError()
121{
122 t_state.error = Error::None;
123 t_state.errorString.clear();
124}
125
126void setError(Error error, const QString& message = QString())
127{
128 t_state.error = error;
129 t_state.errorString = message;
130}
131
132void setFormatInfo(const QString& format, const QString& filter)
133{
134 t_state.formatName = format;
135 t_state.filterName = filter;
136}
137
138void clearFormatInfo()
139{
140 t_state.formatName.clear();
141 t_state.filterName.clear();
142}
143
144} // namespace
145
146// ============================================================================
147// Error Handling
148// ============================================================================
149
151{
152 return t_state.error;
153}
154
156{
157 if (!t_state.errorString.isEmpty()) {
158 return t_state.errorString;
159 }
160 return errorName(t_state.error);
161}
162
164{
165 switch (error) {
166 case Error::None:
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");
182 case Error::IoError:
183 return QStringLiteral("I/O error");
184 case Error::InternalError:
185 return QStringLiteral("Internal error");
186 }
187 return QStringLiteral("Unknown error");
188}
189
191{
192 return t_state.formatName;
193}
194
196{
197 return t_state.filterName;
198}
199
200// ============================================================================
201// Internal Helpers
202// ============================================================================
203
208static bool validateFileInput(const QString& filePath, Format& format)
209{
210 clearError();
211 clearFormatInfo();
212
213 if (!QGCFileHelper::exists(filePath)) {
214 qCWarning(QGCCompressionLog) << "File does not exist:" << filePath;
215 setError(Error::FileNotFound, QStringLiteral("File does not exist: ") + filePath);
216 return false;
217 }
218 if (format == Format::Auto) {
219 format = detectFormat(filePath);
220 if (format == Format::Auto) {
221 qCWarning(QGCCompressionLog) << "Could not detect format for:" << filePath;
222 setError(Error::UnsupportedFormat, QStringLiteral("Could not detect format: ") + filePath);
223 return false;
224 }
225 }
226 return true;
227}
228
230static bool validateArchiveInput(const QString& archivePath, Format& format)
231{
232 if (!validateFileInput(archivePath, format)) {
233 return false;
234 }
235 if (!isArchiveFormat(format)) {
236 qCWarning(QGCCompressionLog) << "Not an archive format:" << formatName(format);
237 setError(Error::UnsupportedFormat, formatName(format) + QStringLiteral(" is not an archive format"));
238 return false;
239 }
240 return true;
241}
242
244static bool validateDeviceInput(QIODevice* device)
245{
246 clearError();
247 clearFormatInfo();
248
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"));
252 return false;
253 }
254 return true;
255}
256
262
263// ============================================================================
264// Format Detection
265// ============================================================================
266
268static Format detectFormatFromExtension(const QString& filePath)
269{
270 const QString lower = filePath.toLower();
271
272 // Check compound extensions first
273 if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) {
274 return Format::TAR_GZ;
275 }
276 if (lower.endsWith(".tar.xz") || lower.endsWith(".txz")) {
277 return Format::TAR_XZ;
278 }
279 if (lower.endsWith(".tar.zst") || lower.endsWith(".tar.zstd")) {
280 return Format::TAR_ZSTD;
281 }
282 if (lower.endsWith(".tar.bz2") || lower.endsWith(".tbz2") || lower.endsWith(".tbz")) {
283 return Format::TAR_BZ2;
284 }
285 if (lower.endsWith(".tar.lz4")) {
286 return Format::TAR_LZ4;
287 }
288
289 // Check single extensions
290 if (lower.endsWith(".zip")) {
291 return Format::ZIP;
292 }
293 if (lower.endsWith(".7z")) {
294 return Format::SEVENZ;
295 }
296 if (lower.endsWith(".gz") || lower.endsWith(".gzip")) {
297 return Format::GZIP;
298 }
299 if (lower.endsWith(".xz") || lower.endsWith(".lzma")) {
300 return Format::XZ;
301 }
302 if (lower.endsWith(".zst") || lower.endsWith(".zstd")) {
303 return Format::ZSTD;
304 }
305 if (lower.endsWith(".bz2") || lower.endsWith(".bzip2")) {
306 return Format::BZIP2;
307 }
308 if (lower.endsWith(".lz4")) {
309 return Format::LZ4;
310 }
311 if (lower.endsWith(".tar")) {
312 return Format::TAR;
313 }
314
315 return Format::Auto;
316}
317
318Format detectFormat(const QString& filePath, bool useContentFallback)
319{
320 // Try extension-based detection first
321 Format format = detectFormatFromExtension(filePath);
322 if (format != Format::Auto) {
323 return format;
324 }
325
326 // If extension detection failed and content fallback is enabled, try content-based
327 if (useContentFallback && QGCFileHelper::exists(filePath)) {
328 format = detectFormatFromFile(filePath);
329 if (format != Format::Auto) {
330 qCDebug(QGCCompressionLog) << "Format detected from content:" << formatName(format) << "for" << filePath;
331 }
332 }
333
334 return format;
335}
336
337Format detectFormatFromFile(const QString& filePath)
338{
339 // Handle Qt resources
340 if (QGCFileHelper::isQtResource(filePath)) {
341 QFile file(filePath);
342 if (!file.open(QIODevice::ReadOnly)) {
343 return Format::Auto;
344 }
345 return detectFormatFromData(file.read(512));
346 }
347
348 // Try QMimeDatabase first (uses both filename and content)
349 QMimeDatabase mimeDb;
350 QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
351
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);
356 return format;
357 }
358 }
359
360 // Fall back to reading raw bytes and using magic byte detection
361 QFile file(filePath);
362 if (!file.open(QIODevice::ReadOnly)) {
363 return Format::Auto;
364 }
365 return detectFormatFromData(file.read(512));
366}
367
368Format detectFormatFromData(const QByteArray& data)
369{
370 if (static_cast<size_t>(data.size()) < kMinMagicBytes) {
371 return Format::Auto;
372 }
373
374 const auto* bytes = reinterpret_cast<const unsigned char*>(data.constData());
375
376 // ZIP: PK\x03\x04 (local file) or PK\x05\x06 (empty) or PK\x07\x08 (spanned)
377 if (bytes[0] == kMagicZip[0] && bytes[1] == kMagicZip[1] &&
378 (bytes[2] == kZipLocalFile || bytes[2] == kZipEmptyArchive || bytes[2] == kZipSpannedArchive)) {
379 return Format::ZIP;
380 }
381
382 // 7-Zip
383 if (memcmp(bytes, kMagic7z, sizeof(kMagic7z)) == 0) {
384 return Format::SEVENZ;
385 }
386
387 // GZIP
388 if (memcmp(bytes, kMagicGzip, sizeof(kMagicGzip)) == 0) {
389 return Format::GZIP;
390 }
391
392 // XZ
393 if (memcmp(bytes, kMagicXz, sizeof(kMagicXz)) == 0) {
394 return Format::XZ;
395 }
396
397 // ZSTD
398 if (memcmp(bytes, kMagicZstd, sizeof(kMagicZstd)) == 0) {
399 return Format::ZSTD;
400 }
401
402 // BZip2
403 if (memcmp(bytes, kMagicBzip2, sizeof(kMagicBzip2)) == 0) {
404 return Format::BZIP2;
405 }
406
407 // LZ4
408 if (memcmp(bytes, kMagicLz4, sizeof(kMagicLz4)) == 0) {
409 return Format::LZ4;
410 }
411
412 // TAR: Check for ustar magic at offset 257
413 if (static_cast<size_t>(data.size()) >= kMinBytesForTar) {
414 if (data.mid(kTarUstarOffset, kTarUstarMagicLen) == "ustar") {
415 return Format::TAR;
416 }
417 }
418
419 // Fallback: Use QMimeDatabase for content-based detection
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);
426 return format;
427 }
428 }
429
430 return Format::Auto;
431}
432
433QString formatExtension(Format format)
434{
435 switch (format) {
436 case Format::ZIP:
437 return QStringLiteral(".zip");
438 case Format::SEVENZ:
439 return QStringLiteral(".7z");
440 case Format::GZIP:
441 return QStringLiteral(".gz");
442 case Format::XZ:
443 return QStringLiteral(".xz");
444 case Format::ZSTD:
445 return QStringLiteral(".zst");
446 case Format::BZIP2:
447 return QStringLiteral(".bz2");
448 case Format::LZ4:
449 return QStringLiteral(".lz4");
450 case Format::TAR:
451 return QStringLiteral(".tar");
452 case Format::TAR_GZ:
453 return QStringLiteral(".tar.gz");
454 case Format::TAR_XZ:
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");
462 case Format::Auto:
463 return QString();
464 }
465 Q_UNREACHABLE();
466}
467
468QString formatName(Format format)
469{
470 switch (format) {
471 case Format::Auto:
472 return QStringLiteral("Auto");
473 case Format::ZIP:
474 return QStringLiteral("ZIP");
475 case Format::SEVENZ:
476 return QStringLiteral("7-Zip");
477 case Format::GZIP:
478 return QStringLiteral("GZIP");
479 case Format::XZ:
480 return QStringLiteral("XZ/LZMA");
481 case Format::ZSTD:
482 return QStringLiteral("Zstandard");
483 case Format::BZIP2:
484 return QStringLiteral("BZip2");
485 case Format::LZ4:
486 return QStringLiteral("LZ4");
487 case Format::TAR:
488 return QStringLiteral("TAR");
489 case Format::TAR_GZ:
490 return QStringLiteral("TAR.GZ");
491 case Format::TAR_XZ:
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");
499 }
500 Q_UNREACHABLE();
501}
502
504{
505 switch (format) {
506 case Format::ZIP:
507 case Format::SEVENZ:
508 case Format::TAR:
509 case Format::TAR_GZ:
510 case Format::TAR_XZ:
511 case Format::TAR_ZSTD:
512 case Format::TAR_BZ2:
513 case Format::TAR_LZ4:
514 return true;
515 default:
516 return false;
517 }
518}
519
521{
522 switch (format) {
523 case Format::GZIP:
524 case Format::XZ:
525 case Format::ZSTD:
526 case Format::BZIP2:
527 case Format::LZ4:
528 return true;
529 default:
530 return false;
531 }
532}
533
534QString strippedPath(const QString& filePath)
535{
536 const Format format = detectFormat(filePath);
537 if (!isCompressionFormat(format)) {
538 return filePath;
539 }
540
541 const QString ext = formatExtension(format);
542 if (filePath.endsWith(ext, Qt::CaseInsensitive)) {
543 return filePath.left(filePath.size() - ext.size());
544 }
545
546 return filePath;
547}
548
549// ============================================================================
550// Single-File Decompression
551// ============================================================================
552
553bool decompressFile(const QString& inputPath, const QString& outputPath, Format format, ProgressCallback progress,
554 qint64 maxDecompressedBytes)
555{
556 if (!validateFileInput(inputPath, format)) {
557 return false;
558 }
559
560 // Determine output path
561 QString actualOutput = outputPath;
562 if (actualOutput.isEmpty()) {
563 QString ext = formatExtension(format);
564 if (inputPath.endsWith(ext, Qt::CaseInsensitive)) {
565 actualOutput = inputPath.left(inputPath.size() - ext.size());
566 } else {
567 actualOutput = inputPath + ".decompressed";
568 }
569 }
570
571 qCDebug(QGCCompressionLog) << "Decompressing" << inputPath << "to" << actualOutput << "using" << formatName(format);
572
573 // Single-file compression formats
574 if (isCompressionFormat(format)) {
575 const bool success =
576 QGClibarchive::decompressSingleFile(inputPath, actualOutput, progress, maxDecompressedBytes);
578 if (!success) {
579 setError(Error::IoError, QStringLiteral("Decompression failed: ") + inputPath);
580 }
581 return success;
582 }
583
584 // Archive formats - delegate to extractArchive with a warning
585 if (isArchiveFormat(format)) {
586 qCWarning(QGCCompressionLog) << formatName(format) << "is an archive format; use extractArchive() instead";
587 return extractArchive(inputPath, actualOutput, format, progress, maxDecompressedBytes);
588 }
589
590 qCWarning(QGCCompressionLog) << "Unsupported decompression format:" << formatName(format);
591 setError(Error::UnsupportedFormat, QStringLiteral("Unsupported decompression format: ") + formatName(format));
592 return false;
593}
594
595QString decompressIfNeeded(const QString& filePath, const QString& outputPath, bool removeOriginal)
596{
597 // If not a compressed file, return original path unchanged
598 if (!isCompressedFile(filePath)) {
599 return filePath;
600 }
601
602 // Determine output path
603 const QString actualOutput = outputPath.isEmpty() ? strippedPath(filePath) : outputPath;
604
605 // Attempt decompression
606 if (!decompressFile(filePath, actualOutput)) {
607 qCWarning(QGCCompressionLog) << "Decompression failed:" << filePath;
608 return QString();
609 }
610
611 if (removeOriginal && !QFile::remove(filePath)) {
612 qCWarning(QGCCompressionLog) << "Failed to remove original file:" << filePath;
613 }
614
615 return actualOutput;
616}
617
618QByteArray decompressData(const QByteArray& data, Format format, qint64 maxDecompressedBytes)
619{
620 if (data.isEmpty()) {
621 qCWarning(QGCCompressionLog) << "Cannot decompress empty data";
622 return {};
623 }
624
625 if (format == Format::Auto) {
626 format = detectFormatFromData(data);
627 if (format == Format::Auto) {
628 qCWarning(QGCCompressionLog) << "Could not detect format from data";
629 return {};
630 }
631 }
632
633 if (!isCompressionFormat(format)) {
634 qCWarning(QGCCompressionLog) << "Invalid decompression format:" << formatName(format);
635 setError(Error::UnsupportedFormat, formatName(format) + QStringLiteral(" is not a compression format"));
636 return {};
637 }
638
639 QByteArray result = QGClibarchive::decompressDataFromMemory(data, maxDecompressedBytes);
641 if (result.isEmpty()) {
642 setError(Error::IoError, QStringLiteral("Failed to decompress data"));
643 }
644 return result;
645}
646
647// ============================================================================
648// Archive Extraction
649// ============================================================================
650
651bool extractArchive(const QString& archivePath, const QString& outputDirectoryPath, Format format,
652 ProgressCallback progress, qint64 maxDecompressedBytes)
653{
654 if (!validateArchiveInput(archivePath, format)) {
655 return false;
656 }
657
658 qCDebug(QGCCompressionLog) << "Extracting" << formatName(format) << "archive" << archivePath << "to"
659 << outputDirectoryPath;
660
661 const bool success =
662 QGClibarchive::extractAnyArchive(archivePath, outputDirectoryPath, progress, maxDecompressedBytes);
664 if (!success) {
665 setError(Error::IoError, QStringLiteral("Failed to extract archive: ") + archivePath);
666 }
667 return success;
668}
669
670bool extractArchiveAtomic(const QString& archivePath, const QString& outputDirectoryPath, Format format,
671 ProgressCallback progress, qint64 maxDecompressedBytes)
672{
673 if (!validateArchiveInput(archivePath, format)) {
674 return false;
675 }
676
677 qCDebug(QGCCompressionLog) << "Atomically extracting" << formatName(format) << "archive" << archivePath
678 << "to" << outputDirectoryPath;
679
680 const bool success =
681 QGClibarchive::extractArchiveAtomic(archivePath, outputDirectoryPath, progress, maxDecompressedBytes);
683 if (!success) {
684 setError(Error::IoError, QStringLiteral("Failed to atomically extract archive: ") + archivePath);
685 }
686 return success;
687}
688
689bool extractArchiveFiltered(const QString& archivePath, const QString& outputDirectoryPath, EntryFilter filter,
690 ProgressCallback progress, qint64 maxDecompressedBytes)
691{
692 if (!filter) {
693 setError(Error::InternalError, QStringLiteral("No filter callback provided"));
694 return false;
695 }
696 Format format = Format::Auto;
697 if (!validateArchiveInput(archivePath, format)) {
698 return false;
699 }
700
701 qCDebug(QGCCompressionLog) << "Extracting archive with filter:" << archivePath;
702
703 const bool success =
704 QGClibarchive::extractWithFilter(archivePath, outputDirectoryPath, filter, progress, maxDecompressedBytes);
706 if (!success) {
707 setError(Error::IoError, QStringLiteral("Failed to extract archive: ") + archivePath);
708 }
709 return success;
710}
711
712QStringList listArchive(const QString& archivePath, Format format)
713{
714 if (!validateArchiveInput(archivePath, format)) {
715 return {};
716 }
717 QStringList entries = QGClibarchive::listArchiveEntries(archivePath);
718
719 // Natural sort: "file2.txt" before "file10.txt"
720 // Use English locale for consistent cross-platform numeric collation
721 QCollator collator{QLocale{QLocale::English}};
722 collator.setNumericMode(true);
723 collator.setCaseSensitivity(Qt::CaseInsensitive);
724 std::sort(entries.begin(), entries.end(), collator);
725
726 return entries;
727}
728
729QList<ArchiveEntry> listArchiveDetailed(const QString& archivePath, Format format)
730{
731 if (!validateArchiveInput(archivePath, format)) {
732 return {};
733 }
734 QList<ArchiveEntry> entries = QGClibarchive::listArchiveEntriesDetailed(archivePath);
735
736 // Natural sort by name: "file2.txt" before "file10.txt"
737 // Use English locale for consistent cross-platform numeric collation
738 QCollator collator{QLocale{QLocale::English}};
739 collator.setNumericMode(true);
740 collator.setCaseSensitivity(Qt::CaseInsensitive);
741 std::sort(entries.begin(), entries.end(), [&collator](const ArchiveEntry& a, const ArchiveEntry& b) {
742 return collator.compare(a.name, b.name) < 0;
743 });
744
745 return entries;
746}
747
748ArchiveStats getArchiveStats(const QString& archivePath, Format format)
749{
750 if (!validateArchiveInput(archivePath, format)) {
751 return {};
752 }
753 return QGClibarchive::getArchiveStats(archivePath);
754}
755
756bool validateArchive(const QString& archivePath, Format format)
757{
758 if (!validateArchiveInput(archivePath, format)) {
759 return false;
760 }
761 qCDebug(QGCCompressionLog) << "Validating archive:" << archivePath;
762 return QGClibarchive::validateArchive(archivePath);
763}
764
765bool fileExists(const QString& archivePath, const QString& fileName, Format format)
766{
767 if (fileName.isEmpty()) {
768 qCWarning(QGCCompressionLog) << "File name cannot be empty";
769 return false;
770 }
771 if (!validateArchiveInput(archivePath, format)) {
772 return false;
773 }
774 return QGClibarchive::fileExistsInArchive(archivePath, fileName);
775}
776
777bool extractFile(const QString& archivePath, const QString& fileName, const QString& outputPath, Format format)
778{
779 if (fileName.isEmpty()) {
780 qCWarning(QGCCompressionLog) << "File name cannot be empty";
781 return false;
782 }
783 if (!validateArchiveInput(archivePath, format)) {
784 return false;
785 }
786
787 const QString actualOutput = outputPath.isEmpty() ? QFileInfo(fileName).fileName() : outputPath;
788 qCDebug(QGCCompressionLog) << "Extracting" << fileName << "from" << archivePath << "to" << actualOutput;
789 return QGClibarchive::extractSingleFile(archivePath, fileName, actualOutput);
790}
791
792QByteArray extractFileData(const QString& archivePath, const QString& fileName, Format format)
793{
794 if (fileName.isEmpty()) {
795 qCWarning(QGCCompressionLog) << "File name cannot be empty";
796 return {};
797 }
798 if (!validateArchiveInput(archivePath, format)) {
799 return {};
800 }
801 qCDebug(QGCCompressionLog) << "Extracting" << fileName << "from" << archivePath << "to memory";
802 return QGClibarchive::extractFileToMemory(archivePath, fileName);
803}
804
805bool extractFiles(const QString& archivePath, const QStringList& fileNames, const QString& outputDirectoryPath,
806 Format format)
807{
808 if (fileNames.isEmpty()) {
809 return true;
810 }
811 if (!validateArchiveInput(archivePath, format)) {
812 return false;
813 }
814 qCDebug(QGCCompressionLog) << "Extracting" << fileNames.size() << "files from" << archivePath;
815 return QGClibarchive::extractMultipleFiles(archivePath, fileNames, outputDirectoryPath);
816}
817
818bool extractByPattern(const QString& archivePath, const QStringList& patterns, const QString& outputDirectoryPath,
819 QStringList* extractedFiles, Format format)
820{
821 if (patterns.isEmpty()) {
822 setError(Error::FileNotInArchive, QStringLiteral("No patterns provided"));
823 return false;
824 }
825 if (!validateArchiveInput(archivePath, format)) {
826 return false;
827 }
828 qCDebug(QGCCompressionLog) << "Extracting files matching patterns" << patterns << "from" << archivePath;
829 const bool success = QGClibarchive::extractByPattern(archivePath, patterns, outputDirectoryPath, extractedFiles);
830 if (!success) {
831 setError(Error::FileNotInArchive, QStringLiteral("No files matched patterns"));
832 }
833 return success;
834}
835
836// ============================================================================
837// QIODevice-based Operations
838// ============================================================================
839
840bool decompressFromDevice(QIODevice* device, const QString& outputPath, ProgressCallback progress,
841 qint64 maxDecompressedBytes)
842{
843 if (!validateDeviceInput(device)) {
844 return false;
845 }
846 qCDebug(QGCCompressionLog) << "Decompressing from device to" << outputPath;
847 const bool success = QGClibarchive::decompressFromDevice(device, outputPath, progress, maxDecompressedBytes);
849 if (!success) {
850 setError(Error::IoError, QStringLiteral("Failed to decompress from device"));
851 }
852 return success;
853}
854
855QByteArray decompressFromDevice(QIODevice* device, qint64 maxDecompressedBytes)
856{
857 if (!validateDeviceInput(device)) {
858 return {};
859 }
860 qCDebug(QGCCompressionLog) << "Decompressing from device to memory";
861 QByteArray result = QGClibarchive::decompressDataFromDevice(device, maxDecompressedBytes);
863 if (result.isEmpty()) {
864 setError(Error::IoError, QStringLiteral("Failed to decompress from device"));
865 }
866 return result;
867}
868
869bool extractFromDevice(QIODevice* device, const QString& outputDirectoryPath, ProgressCallback progress,
870 qint64 maxDecompressedBytes)
871{
872 if (!validateDeviceInput(device)) {
873 return false;
874 }
875 qCDebug(QGCCompressionLog) << "Extracting archive from device to" << outputDirectoryPath;
876 const bool success = QGClibarchive::extractFromDevice(device, outputDirectoryPath, progress, maxDecompressedBytes);
878 if (!success) {
879 setError(Error::IoError, QStringLiteral("Failed to extract archive from device"));
880 }
881 return success;
882}
883
884QByteArray extractFileDataFromDevice(QIODevice* device, const QString& fileName)
885{
886 if (!validateDeviceInput(device)) {
887 return {};
888 }
889 if (fileName.isEmpty()) {
890 qCWarning(QGCCompressionLog) << "File name cannot be empty";
891 setError(Error::FileNotInArchive, QStringLiteral("File name cannot be empty"));
892 return {};
893 }
894 qCDebug(QGCCompressionLog) << "Extracting" << fileName << "from device to memory";
895 QByteArray result = QGClibarchive::extractFileDataFromDevice(device, fileName);
897 if (result.isEmpty()) {
898 setError(Error::FileNotInArchive, QStringLiteral("File not found: ") + fileName);
899 }
900 return result;
901}
902
903} // namespace QGCCompression
QString formatName
QString errorString
Error error
QString filterName
#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.