QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGClibarchive.cc
Go to the documentation of this file.
1#include "QGClibarchive.h"
2
3#include <QtCore/QDir>
4#include <QtCore/QFileInfo>
5#include <QtCore/QSaveFile>
6#include <QtCore/QSet>
7#include <QtCore/QTemporaryDir>
8
9#include <archive.h>
10#include <archive_entry.h>
11
12#include "QGCFileHelper.h"
13#include "QGCLoggingCategory.h"
14
15QGC_LOGGING_CATEGORY(QGClibarchiveLog, "Utilities.QGClibarchive")
16
17// ============================================================================
18// Thread-Local Format Detection State
19// ============================================================================
20
21namespace {
22
23struct FormatState
24{
25 QString formatName;
26 QString filterName;
27};
28
29thread_local FormatState t_formatState;
30
31void updateFormatState(struct archive* a)
32{
33 if (!a) {
34 t_formatState.formatName.clear();
35 t_formatState.filterName.clear();
36 return;
37 }
38
39 // Get format name
40 const char* fmt = archive_format_name(a);
41 t_formatState.formatName = fmt ? QString::fromUtf8(fmt) : QString();
42
43 // Get compression filter name (index 0 is the outermost filter)
44 const char* flt = archive_filter_name(a, 0);
45 t_formatState.filterName = flt ? QString::fromUtf8(flt) : QStringLiteral("none");
46
47 if (!t_formatState.formatName.isEmpty()) {
48 qCDebug(QGClibarchiveLog) << "Detected format:" << t_formatState.formatName
49 << "filter:" << t_formatState.filterName;
50 }
51}
52
53} // namespace
54
55namespace QGClibarchive {
56
58{
59 return t_formatState.formatName;
60}
61
63{
64 return t_formatState.filterName;
65}
66
67} // namespace QGClibarchive
68
69// ============================================================================
70// libarchive callbacks for QIODevice streaming
71// ============================================================================
72
73namespace QGClibarchive {
74
75la_ssize_t deviceReadCallback(struct archive*, void* clientData, const void** buffer)
76{
77 static thread_local char readBuffer[QGCFileHelper::kBufferSizeMax];
78 auto* device = static_cast<QIODevice*>(clientData);
79
80 const qint64 bytesRead = device->read(readBuffer, sizeof(readBuffer));
81 if (bytesRead < 0) {
82 return ARCHIVE_FATAL;
83 }
84
85 *buffer = readBuffer;
86 return static_cast<la_ssize_t>(bytesRead);
87}
88
89la_int64_t deviceSkipCallback(struct archive*, void* clientData, la_int64_t request)
90{
91 auto* device = static_cast<QIODevice*>(clientData);
92
93 if (!device->isSequential()) {
94 const qint64 currentPos = device->pos();
95 const qint64 newPos = currentPos + request;
96 if (device->seek(newPos)) {
97 return request;
98 }
99 }
100
101 // For sequential devices or failed seek, read and discard
102 char discardBuffer[QGCFileHelper::kBufferSizeMax];
103 la_int64_t skipped = 0;
104 while (skipped < request) {
105 const qint64 toRead = qMin(static_cast<qint64>(sizeof(discardBuffer)), request - skipped);
106 const qint64 bytesRead = device->read(discardBuffer, toRead);
107 if (bytesRead <= 0)
108 break;
109 skipped += bytesRead;
110 }
111 return skipped;
112}
113
114int deviceCloseCallback(struct archive*, void*)
115{
116 // Don't close the device - caller owns it
117 return ARCHIVE_OK;
118}
119
120la_int64_t deviceSeekCallback(struct archive*, void* clientData, la_int64_t offset, int whence)
121{
122 auto* device = static_cast<QIODevice*>(clientData);
123
124 if (device->isSequential()) {
125 return ARCHIVE_FATAL;
126 }
127
128 qint64 newPos;
129 switch (whence) {
130 case SEEK_SET:
131 newPos = offset;
132 break;
133 case SEEK_CUR:
134 newPos = device->pos() + offset;
135 break;
136 case SEEK_END:
137 newPos = device->size() + offset;
138 break;
139 default:
140 return ARCHIVE_FATAL;
141 }
142
143 if (!device->seek(newPos)) {
144 return ARCHIVE_FATAL;
145 }
146
147 return newPos;
148}
149
150// ============================================================================
151// Utility Functions
152// ============================================================================
153
154ArchiveEntry toArchiveEntry(struct archive_entry* entry)
155{
156 ArchiveEntry info;
157 info.name = QString::fromUtf8(archive_entry_pathname(entry));
158 info.size = archive_entry_size(entry);
159 info.isDirectory = (archive_entry_filetype(entry) == AE_IFDIR);
160 info.permissions = static_cast<quint32>(archive_entry_perm(entry));
161
162 const time_t mtime = archive_entry_mtime(entry);
163 if (mtime > 0) {
164 info.modified = QDateTime::fromSecsSinceEpoch(mtime);
165 }
166
167 return info;
168}
169
170} // namespace QGClibarchive
171
172// ============================================================================
173// Internal helpers (anonymous namespace)
174// ============================================================================
175
176namespace {
177
183QByteArray readArchiveToMemory(struct archive* a, qint64 expectedSize = 0, qint64 maxBytes = 0)
184{
185 QByteArray result;
186 if (expectedSize > 0) {
187 result.reserve(static_cast<qsizetype>(expectedSize));
188 }
189
191 la_ssize_t size;
192 while ((size = archive_read_data(a, buffer, sizeof(buffer))) > 0) {
193 if (maxBytes > 0 && (result.size() + size) > maxBytes) {
194 qCWarning(QGClibarchiveLog) << "Size limit exceeded:" << maxBytes << "bytes";
195 return {};
196 }
197 result.append(buffer, static_cast<qsizetype>(size));
198 }
199
200 if (size < 0) {
201 qCWarning(QGClibarchiveLog) << "Read error:" << archive_error_string(a);
202 return {};
203 }
204
205 return result;
206}
207
214bool writeArchiveEntryToFile(struct archive* a, const QString& outputPath)
215{
216 QSaveFile outFile(outputPath);
217 if (!outFile.open(QIODevice::WriteOnly)) {
218 qCWarning(QGClibarchiveLog) << "Failed to open output file:" << outputPath << outFile.errorString();
219 return false;
220 }
221
222 const void* buff;
223 size_t size;
224 la_int64_t offset;
225 int r;
226
227 while ((r = archive_read_data_block(a, &buff, &size, &offset)) == ARCHIVE_OK) {
228 // Seek to offset - creates sparse hole if gap from previous position
229 if (!outFile.seek(offset)) {
230 qCWarning(QGClibarchiveLog) << "Failed to seek:" << outFile.errorString();
231 outFile.cancelWriting();
232 return false;
233 }
234 if (outFile.write(static_cast<const char*>(buff), static_cast<qint64>(size)) != static_cast<qint64>(size)) {
235 qCWarning(QGClibarchiveLog) << "Failed to write output:" << outFile.errorString();
236 outFile.cancelWriting();
237 return false;
238 }
239 }
240
241 if (r != ARCHIVE_EOF) {
242 qCWarning(QGClibarchiveLog) << "Read error:" << archive_error_string(a);
243 outFile.cancelWriting();
244 return false;
245 }
246
247 // Atomically commit - renames temp file to final path
248 if (!outFile.commit()) {
249 qCWarning(QGClibarchiveLog) << "Failed to commit output file:" << outFile.errorString();
250 return false;
251 }
252
253 return true;
254}
255
263bool decompressStreamToFile(struct archive* a, const QString& outputPath,
264 const QGClibarchive::ProgressCallback& progress, qint64 totalSize, qint64 maxBytes)
265{
266 QSaveFile outFile(outputPath);
267 if (!outFile.open(QIODevice::WriteOnly)) {
268 qCWarning(QGClibarchiveLog) << "Failed to open output file:" << outputPath << outFile.errorString();
269 return false;
270 }
271
272 qint64 totalBytesWritten = 0;
274 la_ssize_t size;
275
276 while ((size = archive_read_data(a, buffer, sizeof(buffer))) > 0) {
277 if (maxBytes > 0 && (totalBytesWritten + size) > maxBytes) {
278 qCWarning(QGClibarchiveLog) << "Size limit exceeded:" << maxBytes << "bytes";
279 outFile.cancelWriting();
280 return false;
281 }
282
283 if (outFile.write(buffer, size) != size) {
284 qCWarning(QGClibarchiveLog) << "Failed to write output:" << outFile.errorString();
285 outFile.cancelWriting();
286 return false;
287 }
288 totalBytesWritten += size;
289
290 if (progress) {
291 const qint64 bytesRead = archive_filter_bytes(a, -1);
292 if (!progress(bytesRead, totalSize)) {
293 qCDebug(QGClibarchiveLog) << "Decompression cancelled by user";
294 outFile.cancelWriting();
295 return false;
296 }
297 }
298 }
299
300 if (size < 0) {
301 qCWarning(QGClibarchiveLog) << "Decompression error:" << archive_error_string(a);
302 outFile.cancelWriting();
303 return false;
304 }
305
306 if (!outFile.commit()) {
307 qCWarning(QGClibarchiveLog) << "Failed to commit output file:" << outFile.errorString();
308 return false;
309 }
310
311 qCDebug(QGClibarchiveLog) << "Decompressed" << totalBytesWritten << "bytes to" << outputPath;
312 return true;
313}
314
318void cleanupCreatedEntries(const QStringList& createdFiles, const QStringList& createdDirs)
319{
320 // Remove files first
321 for (const QString& file : createdFiles) {
322 QFile::remove(file);
323 }
324
325 // Remove directories in reverse order (deepest first), only if empty
326 for (auto it = createdDirs.crbegin(); it != createdDirs.crend(); ++it) {
327 QDir dir(*it);
328 if (dir.isEmpty()) {
329 dir.rmdir(*it);
330 }
331 }
332}
333
339void trackAndCreateParentDirs(const QString& path, const QString& outputDir, QSet<QString>& existingDirs,
340 QStringList& createdDirs)
341{
342 QStringList dirsToCreate;
343 QString current = QFileInfo(path).absolutePath();
344
345 // Walk up the tree finding directories that don't exist
346 while (current != outputDir && !current.isEmpty() && !existingDirs.contains(current)) {
347 if (!QDir(current).exists()) {
348 dirsToCreate.prepend(current);
349 } else {
350 existingDirs.insert(current);
351 break;
352 }
353 current = QFileInfo(current).absolutePath();
354 }
355
356 // Create directories and track them
357 for (const QString& dir : dirsToCreate) {
358 if (QDir().mkdir(dir)) {
359 createdDirs.append(dir);
360 existingDirs.insert(dir);
361 }
362 }
363}
364
372bool extractArchiveEntries(struct archive* a, const QString& outputDirectoryPath,
373 const QGClibarchive::ProgressCallback& progress, qint64 totalSize, qint64 maxBytes)
374{
375 struct archive* ext = archive_write_disk_new();
376 // Security flags:
377 // - ARCHIVE_EXTRACT_TIME: Restore file modification times
378 // - ARCHIVE_EXTRACT_SECURE_NODOTDOT: Reject paths containing ".."
379 // - ARCHIVE_EXTRACT_SECURE_SYMLINKS: Don't follow symlinks when setting perms
380 // Note: We don't use ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS because we set
381 // absolute output paths manually (after validating the original entry path)
382 archive_write_disk_set_options(
383 ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS);
384 archive_write_disk_set_standard_lookup(ext);
385
386 bool success = true;
387 bool cancelled = false;
388 bool sizeLimitExceeded = false;
389 qint64 totalBytesWritten = 0;
390 struct archive_entry* entry;
391
392 // Track created entries for safe cleanup on failure
393 QStringList createdFiles;
394 QStringList createdDirs;
395 QSet<QString> existingDirs;
396
397 // Resolve the extraction root to a canonical path when possible so platforms
398 // with symlinked temp roots (for example, /var -> /private/var on macOS)
399 // do not trip libarchive's secure symlink checks for parent directories.
400 QString canonicalOutputDir = QFileInfo(outputDirectoryPath).canonicalFilePath();
401 if (canonicalOutputDir.isEmpty()) {
402 canonicalOutputDir = QFileInfo(outputDirectoryPath).absoluteFilePath();
403 }
404 existingDirs.insert(canonicalOutputDir);
405
406 bool formatLogged = false;
407 while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
408 // Log detected format after first header read
409 if (!formatLogged) {
410 updateFormatState(a);
411 formatLogged = true;
412 }
413
414 const char* currentFile = archive_entry_pathname(entry);
415 QString entryName = QString::fromUtf8(currentFile);
416 QString outputPath = QGCFileHelper::joinPath(canonicalOutputDir, entryName);
417
418 // Prevent path traversal attacks
419 QFileInfo outputInfo(outputPath);
420 QString canonicalOutput = outputInfo.absoluteFilePath();
421
422 if (!canonicalOutput.startsWith(canonicalOutputDir + "/") && canonicalOutput != canonicalOutputDir) {
423 qCWarning(QGClibarchiveLog) << "Skipping path traversal attempt:" << currentFile;
424 continue;
425 }
426
427 // Security: reject filenames with embedded null bytes (libarchive issue #2774)
428 if (entryName.contains(QChar('\0'))) {
429 qCWarning(QGClibarchiveLog) << "Skipping path with embedded null byte";
430 continue;
431 }
432
433 // Security: validate symlink targets don't escape output directory
434 const auto fileType = archive_entry_filetype(entry);
435 if (fileType == AE_IFLNK) {
436 const char* symlinkTarget = archive_entry_symlink(entry);
437 if (symlinkTarget) {
438 QString target = QString::fromUtf8(symlinkTarget);
439 QString resolvedTarget;
440
441 if (QFileInfo(target).isAbsolute()) {
442 // Absolute symlink - reject outright
443 qCWarning(QGClibarchiveLog)
444 << "Skipping symlink with absolute target:" << currentFile << "->" << target;
445 continue;
446 } else {
447 // Relative symlink - resolve against entry's parent directory
448 QString entryDir = QFileInfo(outputPath).absolutePath();
449 resolvedTarget = QFileInfo(entryDir + "/" + target).absoluteFilePath();
450 }
451
452 // Verify resolved target stays within output directory
453 if (!resolvedTarget.startsWith(canonicalOutputDir + "/") && resolvedTarget != canonicalOutputDir) {
454 qCWarning(QGClibarchiveLog)
455 << "Skipping symlink escaping output directory:" << currentFile << "->" << symlinkTarget;
456 continue;
457 }
458 }
459 }
460
461 // Security: mask permissions for cross-platform safety
462 // Files: 0644 (rw-r--r--), Directories: 0755 (rwxr-xr-x)
463 if (fileType == AE_IFDIR) {
464 archive_entry_set_perm(entry, 0755);
465 } else if (fileType == AE_IFREG) {
466 archive_entry_set_perm(entry, 0644);
467 }
468 // Note: symlinks don't have meaningful permissions on most systems
469
470 archive_entry_set_pathname(entry, outputPath.toUtf8().constData());
471
472 // Create parent directories and track newly created ones
473 trackAndCreateParentDirs(outputPath, canonicalOutputDir, existingDirs, createdDirs);
474
475 int r = archive_write_header(ext, entry);
476 if (r != ARCHIVE_OK) {
477 qCWarning(QGClibarchiveLog) << "Failed to write header for" << outputPath << ":"
478 << archive_error_string(ext);
479 success = false;
480 break;
481 }
482
483 // Track created file/directory for cleanup
484 if (fileType == AE_IFDIR) {
485 if (!existingDirs.contains(canonicalOutput)) {
486 createdDirs.append(canonicalOutput);
487 existingDirs.insert(canonicalOutput);
488 }
489 } else {
490 createdFiles.append(canonicalOutput);
491 }
492
493 if (archive_entry_size(entry) > 0) {
494 const void* buff;
495 size_t size;
496 la_int64_t offset;
497
498 while ((r = archive_read_data_block(a, &buff, &size, &offset)) == ARCHIVE_OK) {
499 // Check size limit before writing
500 if (maxBytes > 0 && (totalBytesWritten + static_cast<qint64>(size)) > maxBytes) {
501 qCWarning(QGClibarchiveLog) << "Size limit exceeded:" << maxBytes << "bytes";
502 sizeLimitExceeded = true;
503 success = false;
504 break;
505 }
506
507 if (archive_write_data_block(ext, buff, size, offset) != ARCHIVE_OK) {
508 qCWarning(QGClibarchiveLog) << "Failed to write data:" << archive_error_string(ext);
509 success = false;
510 break;
511 }
512
513 totalBytesWritten += static_cast<qint64>(size);
514
515 // Report progress based on compressed bytes read
516 if (progress) {
517 const qint64 compressedBytesRead = archive_filter_bytes(a, -1);
518 if (!progress(compressedBytesRead, totalSize)) {
519 qCDebug(QGClibarchiveLog) << "Extraction cancelled by user";
520 cancelled = true;
521 success = false;
522 break;
523 }
524 }
525 }
526
527 if (!success)
528 break;
529
530 if (r != ARCHIVE_EOF && r != ARCHIVE_OK) {
531 qCWarning(QGClibarchiveLog) << "Failed to read data:" << archive_error_string(a);
532 success = false;
533 break;
534 }
535 }
536
537 if (archive_write_finish_entry(ext) != ARCHIVE_OK) {
538 qCWarning(QGClibarchiveLog) << "Failed to finish entry:" << archive_error_string(ext);
539 success = false;
540 break;
541 }
542 }
543
544 archive_read_close(a);
545 archive_read_free(a);
546 archive_write_close(ext);
547 archive_write_free(ext);
548
549 // Clean up only created entries on cancellation or size limit exceeded
550 if (cancelled || sizeLimitExceeded) {
551 cleanupCreatedEntries(createdFiles, createdDirs);
552 }
553
554 return success;
555}
556
557} // namespace
558
559// ============================================================================
560// QGClibarchive Implementation
561// ============================================================================
562
563namespace QGClibarchive {
564
565// ----------------------------------------------------------------------------
566// ArchiveReader
567// ----------------------------------------------------------------------------
568
570{
571 if (_archive) {
572 archive_read_free(_archive);
573 }
574}
575
577{
578 if (!_resourceData.isEmpty()) {
579 return _resourceData.size();
580 }
581 if (_device) {
582 return _device->size() > 0 ? _device->size() : 0;
583 }
584 if (!_filePath.isEmpty()) {
585 return QFileInfo(_filePath).size();
586 }
587 return 0;
588}
589
591{
592 if (!_archive) {
593 return {};
594 }
595 const char* name = archive_format_name(_archive);
596 return name ? QString::fromUtf8(name) : QString();
597}
598
600{
601 if (!_archive) {
602 return {};
603 }
604 // Get the outermost compression filter (index 0 is the format, 1+ are filters)
605 // archive_filter_count returns total filters including format at index 0
606 const int filterCount = archive_filter_count(_archive);
607 if (filterCount <= 1) {
608 return QStringLiteral("none");
609 }
610 // Index 0 is innermost (format), higher indices are outer filters
611 // We want the first compression filter (index 1)
612 const char* name = archive_filter_name(_archive, 0);
613 return name ? QString::fromUtf8(name) : QStringLiteral("none");
614}
615
616bool ArchiveReader::open(const QString& path, ReaderMode mode)
617{
618 if (_archive) {
619 archive_read_free(_archive);
620 _archive = nullptr;
621 }
622 _device = nullptr;
623 _filePath = path;
624
625 _archive = archive_read_new();
626 if (!_archive) {
627 qCWarning(QGClibarchiveLog) << "Failed to create archive reader";
628 return false;
629 }
630
631 archive_read_support_filter_all(_archive);
632
633 switch (mode) {
635 archive_read_support_format_all(_archive);
636 break;
638 archive_read_support_format_raw(_archive);
639 break;
640 }
641
642 if (!openArchiveForReading(_archive, path, _resourceData)) {
643 archive_read_free(_archive);
644 _archive = nullptr;
645 return false;
646 }
647
648 return true;
649}
650
651bool ArchiveReader::open(QIODevice* device, ReaderMode mode)
652{
653 if (!device || !device->isOpen() || !device->isReadable()) {
654 qCWarning(QGClibarchiveLog) << "Device is null, not open, or not readable";
655 return false;
656 }
657
658 if (_archive) {
659 archive_read_free(_archive);
660 _archive = nullptr;
661 }
662 _device = device;
663 _resourceData.clear();
664
665 _archive = archive_read_new();
666 if (!_archive) {
667 qCWarning(QGClibarchiveLog) << "Failed to create archive reader";
668 return false;
669 }
670
671 archive_read_support_filter_all(_archive);
672
673 switch (mode) {
675 archive_read_support_format_all(_archive);
676 break;
678 archive_read_support_format_raw(_archive);
679 break;
680 }
681
682 // Enable seek callback for random-access devices (improves ZIP performance)
683 // Must be set before opening the archive
684 if (!device->isSequential()) {
685 archive_read_set_seek_callback(_archive, deviceSeekCallback);
686 }
687
688 const int result = archive_read_open2(_archive, device,
689 nullptr, // open callback (not needed)
691
692 if (result != ARCHIVE_OK) {
693 qCWarning(QGClibarchiveLog) << "Failed to open device:" << archive_error_string(_archive);
694 archive_read_free(_archive);
695 _archive = nullptr;
696 _device = nullptr;
697 return false;
698 }
699
700 return true;
701}
702
703// ----------------------------------------------------------------------------
704// Helper Functions
705// ----------------------------------------------------------------------------
706
707bool openArchiveForReading(struct archive* a, const QString& filePath, QByteArray& resourceData)
708{
709 const bool isResource = QGCFileHelper::isQtResource(filePath);
710
711 if (isResource) {
712 // Qt resources must be loaded into memory
713 QFile inputFile(filePath);
714 if (!inputFile.open(QIODevice::ReadOnly)) {
715 qCWarning(QGClibarchiveLog) << "Failed to open file:" << filePath << inputFile.errorString();
716 return false;
717 }
718 resourceData = inputFile.readAll();
719 inputFile.close();
720
721 if (resourceData.isEmpty()) {
722 qCWarning(QGClibarchiveLog) << "File is empty:" << filePath;
723 return false;
724 }
725
726 if (archive_read_open_memory(a, resourceData.constData(), static_cast<size_t>(resourceData.size())) !=
727 ARCHIVE_OK) {
728 qCWarning(QGClibarchiveLog) << "Failed to open data:" << archive_error_string(a);
729 return false;
730 }
731 } else {
732 // Stream directly from file (memory efficient)
733 if (archive_read_open_filename(a, filePath.toLocal8Bit().constData(),
734 QGCFileHelper::optimalBufferSize(filePath)) != ARCHIVE_OK) {
735 qCWarning(QGClibarchiveLog) << "Failed to open file:" << filePath << archive_error_string(a);
736 return false;
737 }
738 }
739
740 return true;
741}
742
743// ----------------------------------------------------------------------------
744// Archive Extraction
745// ----------------------------------------------------------------------------
746
747bool extractAnyArchive(const QString& archivePath, const QString& outputDirectoryPath, ProgressCallback progress,
748 qint64 maxBytes)
749{
750 if (!QGCFileHelper::ensureDirectoryExists(outputDirectoryPath)) {
751 return false;
752 }
753
754 // Pre-check: verify sufficient disk space before extraction
755 const ArchiveStats stats = getArchiveStats(archivePath);
756 if (stats.totalUncompressedSize > 0) {
757 if (!QGCFileHelper::hasSufficientDiskSpace(outputDirectoryPath, stats.totalUncompressedSize)) {
758 return false;
759 }
760 }
761
762 ArchiveReader reader;
763 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
764 return false;
765 }
766
767 const qint64 totalSize = reader.dataSize();
768 return extractArchiveEntries(reader.release(), outputDirectoryPath, progress, totalSize, maxBytes);
769}
770
771bool extractArchiveAtomic(const QString& archivePath, const QString& outputDirectoryPath, ProgressCallback progress,
772 qint64 maxBytes)
773{
774 const QFileInfo outputInfo(outputDirectoryPath);
775 if (outputInfo.fileName().isEmpty()) {
776 qCWarning(QGClibarchiveLog) << "Invalid output directory path:" << outputDirectoryPath;
777 return false;
778 }
779
780 const QString parentDirectoryPath = outputInfo.absoluteDir().absolutePath();
781 if (!QGCFileHelper::ensureDirectoryExists(parentDirectoryPath)) {
782 qCWarning(QGClibarchiveLog) << "Failed to create output parent directory:" << parentDirectoryPath;
783 return false;
784 }
785
786 // Pre-check: verify archive can be opened and get stats for disk space check
787 const ArchiveStats stats = getArchiveStats(archivePath);
788 if (stats.totalEntries == 0) {
789 qCWarning(QGClibarchiveLog) << "Archive is empty or invalid:" << archivePath;
790 return false;
791 }
792
793 // Need ~2x space: one for temp extraction, one for final location
794 // (though move on same filesystem doesn't need extra space)
795 if (stats.totalUncompressedSize > 0) {
796 if (!QGCFileHelper::hasSufficientDiskSpace(outputDirectoryPath, stats.totalUncompressedSize * 2)) {
797 return false;
798 }
799 }
800
801 const QString outputDirectoryName = outputInfo.fileName();
802
803 // Stage extraction in the same parent directory to keep commit/rollback renames on one filesystem.
804 QTemporaryDir stagingDir(QGCFileHelper::joinPath(parentDirectoryPath,
805 QStringLiteral("%1.qgc_stage_XXXXXX").arg(outputDirectoryName)));
806 if (!stagingDir.isValid()) {
807 qCWarning(QGClibarchiveLog) << "Failed to create staging directory:" << stagingDir.errorString();
808 return false;
809 }
810 stagingDir.setAutoRemove(false);
811
812 const QString stagingPath = stagingDir.path();
813 const QFileInfo stagingInfo(stagingPath);
814 const QString stagingName = stagingInfo.fileName();
815
816 qCDebug(QGClibarchiveLog) << "Atomic extraction: staging to" << stagingPath;
817
818 // Extract to temporary directory
819 ArchiveReader reader;
820 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
821 (void) QDir(stagingPath).removeRecursively();
822 return false;
823 }
824
825 const qint64 totalSize = reader.dataSize();
826 if (!extractArchiveEntries(reader.release(), stagingPath, progress, totalSize, maxBytes)) {
827 qCDebug(QGClibarchiveLog) << "Staged extraction failed, cleaning up";
828 (void) QDir(stagingPath).removeRecursively();
829 return false;
830 }
831
832 QDir parentDir(parentDirectoryPath);
833 QString backupPath;
834 QString backupName;
835
836 const QFileInfo existingOutputInfo(outputDirectoryPath);
837 if (existingOutputInfo.exists()) {
838 if (!existingOutputInfo.isDir()) {
839 qCWarning(QGClibarchiveLog) << "Output path exists and is not a directory:" << outputDirectoryPath;
840 (void) QDir(stagingPath).removeRecursively();
841 return false;
842 }
843
844 QTemporaryDir backupDir(QGCFileHelper::joinPath(parentDirectoryPath,
845 QStringLiteral("%1.qgc_backup_XXXXXX").arg(outputDirectoryName)));
846 if (!backupDir.isValid()) {
847 qCWarning(QGClibarchiveLog) << "Failed to create backup directory placeholder:"
848 << backupDir.errorString();
849 (void) QDir(stagingPath).removeRecursively();
850 return false;
851 }
852 backupDir.setAutoRemove(false);
853 backupPath = backupDir.path();
854 backupName = QFileInfo(backupPath).fileName();
855 (void) backupDir.remove();
856
857 if (!parentDir.rename(outputDirectoryName, backupName)) {
858 qCWarning(QGClibarchiveLog) << "Failed to move existing output to backup:" << outputDirectoryPath;
859 (void) QDir(stagingPath).removeRecursively();
860 return false;
861 }
862 }
863
864 if (!parentDir.rename(stagingName, outputDirectoryName)) {
865 qCWarning(QGClibarchiveLog) << "Failed to commit staged extraction for" << outputDirectoryPath;
866 (void) QDir(stagingPath).removeRecursively();
867
868 if (!backupName.isEmpty()) {
869 if (!parentDir.rename(backupName, outputDirectoryName)) {
870 qCWarning(QGClibarchiveLog) << "Failed to rollback backup for" << outputDirectoryPath;
871 }
872 }
873
874 return false;
875 }
876
877 if (!backupPath.isEmpty()) {
878 (void) QDir(backupPath).removeRecursively();
879 }
880
881 qCDebug(QGClibarchiveLog) << "Atomic extraction committed:" << outputDirectoryPath;
882 return true;
883}
884
885bool extractSingleFile(const QString& archivePath, const QString& fileName, const QString& outputPath)
886{
887 ArchiveReader reader;
888 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
889 return false;
890 }
891
892 struct archive_entry* entry;
893
894 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
895 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
896
897 if (entryName == fileName) {
899 return writeArchiveEntryToFile(reader.handle(), outputPath);
900 }
901
902 archive_read_data_skip(reader.handle());
903 }
904
905 qCWarning(QGClibarchiveLog) << "File not found in archive:" << fileName;
906 return false;
907}
908
909QByteArray extractFileToMemory(const QString& archivePath, const QString& fileName)
910{
911 if (fileName.isEmpty()) {
912 qCWarning(QGClibarchiveLog) << "Empty file name";
913 return QByteArray();
914 }
915
916 ArchiveReader reader;
917 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
918 return QByteArray();
919 }
920
921 struct archive_entry* entry;
922
923 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
924 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
925
926 if (entryName == fileName) {
927 QByteArray result = readArchiveToMemory(reader.handle(), archive_entry_size(entry));
928 if (!result.isEmpty()) {
929 qCDebug(QGClibarchiveLog) << "Extracted" << fileName << "to memory:" << result.size() << "bytes";
930 }
931 return result;
932 }
933
934 archive_read_data_skip(reader.handle());
935 }
936
937 qCWarning(QGClibarchiveLog) << "File not found in archive:" << fileName;
938 return QByteArray();
939}
940
941bool extractMultipleFiles(const QString& archivePath, const QStringList& fileNames, const QString& outputDirectoryPath)
942{
943 if (fileNames.isEmpty()) {
944 return true;
945 }
946
947 if (!QGCFileHelper::ensureDirectoryExists(outputDirectoryPath)) {
948 return false;
949 }
950
951 ArchiveReader reader;
952 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
953 return false;
954 }
955
956 QSet<QString> targetFiles(fileNames.begin(), fileNames.end());
957 QSet<QString> extractedFiles;
958 struct archive_entry* entry;
959
960 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
961 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
962
963 if (!targetFiles.contains(entryName)) {
964 archive_read_data_skip(reader.handle());
965 continue;
966 }
967
968 const QString outputPath = QGCFileHelper::joinPath(outputDirectoryPath, entryName);
970
971 if (!writeArchiveEntryToFile(reader.handle(), outputPath)) {
972 return false;
973 }
974
975 extractedFiles.insert(entryName);
976
977 if (extractedFiles.size() == targetFiles.size()) {
978 break;
979 }
980 }
981
982 if (extractedFiles.size() != targetFiles.size()) {
983 for (const QString& name : fileNames) {
984 if (!extractedFiles.contains(name)) {
985 qCWarning(QGClibarchiveLog) << "File not found in archive:" << name;
986 }
987 }
988 return false;
989 }
990
991 return true;
992}
993
994bool extractByPattern(const QString& archivePath, const QStringList& patterns, const QString& outputDirectoryPath,
995 QStringList* extractedFiles)
996{
997 if (patterns.isEmpty()) {
998 return false;
999 }
1000
1001 if (!QGCFileHelper::ensureDirectoryExists(outputDirectoryPath)) {
1002 return false;
1003 }
1004
1005 struct archive* match = archive_match_new();
1006 if (!match) {
1007 qCWarning(QGClibarchiveLog) << "Failed to create archive_match";
1008 return false;
1009 }
1010
1011 for (const QString& pattern : patterns) {
1012 if (archive_match_include_pattern(match, pattern.toUtf8().constData()) != ARCHIVE_OK) {
1013 qCWarning(QGClibarchiveLog) << "Invalid pattern:" << pattern;
1014 archive_match_free(match);
1015 return false;
1016 }
1017 }
1018
1019 ArchiveReader reader;
1020 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1021 archive_match_free(match);
1022 return false;
1023 }
1024
1025 struct archive_entry* entry;
1026 int matchCount = 0;
1027
1028 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1029 if (archive_match_excluded(match, entry)) {
1030 archive_read_data_skip(reader.handle());
1031 continue;
1032 }
1033
1034 if (archive_entry_filetype(entry) == AE_IFDIR) {
1035 archive_read_data_skip(reader.handle());
1036 continue;
1037 }
1038
1039 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
1040 const QString outputPath = QGCFileHelper::joinPath(outputDirectoryPath, entryName);
1041
1043
1044 if (!writeArchiveEntryToFile(reader.handle(), outputPath)) {
1045 archive_match_free(match);
1046 return false;
1047 }
1048
1049 matchCount++;
1050 if (extractedFiles) {
1051 extractedFiles->append(entryName);
1052 }
1053 }
1054
1055 if (archive_match_path_unmatched_inclusions(match) > 0) {
1056 const char* unmatched;
1057 while (archive_match_path_unmatched_inclusions_next(match, &unmatched) == ARCHIVE_OK) {
1058 qCDebug(QGClibarchiveLog) << "Pattern not matched:" << unmatched;
1059 }
1060 }
1061
1062 archive_match_free(match);
1063
1064 qCDebug(QGClibarchiveLog) << "Extracted" << matchCount << "files matching patterns from" << archivePath;
1065 return matchCount > 0;
1066}
1067
1068bool validateArchive(const QString& archivePath)
1069{
1070 ArchiveReader reader;
1071 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1072 return false;
1073 }
1074
1075 struct archive_entry* entry;
1076
1077 // Read through all entries and their data to validate
1078 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1079 // Read and discard data to verify it can be decompressed
1080 char buffer[QGCFileHelper::kBufferSizeMax];
1081 la_ssize_t size;
1082 while ((size = archive_read_data(reader.handle(), buffer, sizeof(buffer))) > 0) {
1083 // Just reading to validate
1084 }
1085
1086 if (size < 0) {
1087 qCWarning(QGClibarchiveLog) << "Validation failed for entry:" << archive_entry_pathname(entry)
1088 << archive_error_string(reader.handle());
1089 return false;
1090 }
1091 }
1092
1093 return true;
1094}
1095
1096bool fileExistsInArchive(const QString& archivePath, const QString& fileName)
1097{
1098 if (fileName.isEmpty()) {
1099 return false;
1100 }
1101
1102 ArchiveReader reader;
1103 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1104 return false;
1105 }
1106
1107 struct archive_entry* entry;
1108 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1109 if (QString::fromUtf8(archive_entry_pathname(entry)) == fileName) {
1110 return true;
1111 }
1112 archive_read_data_skip(reader.handle());
1113 }
1114
1115 return false;
1116}
1117
1118QStringList listArchiveEntries(const QString& archivePath)
1119{
1120 ArchiveReader reader;
1121 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1122 return {};
1123 }
1124
1125 QStringList entries;
1126 struct archive_entry* entry;
1127 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1128 entries.append(QString::fromUtf8(archive_entry_pathname(entry)));
1129 archive_read_data_skip(reader.handle());
1130 }
1131
1132 qCDebug(QGClibarchiveLog) << "Listed" << entries.size() << "entries in" << archivePath;
1133 return entries;
1134}
1135
1136QList<ArchiveEntry> listArchiveEntriesDetailed(const QString& archivePath)
1137{
1138 ArchiveReader reader;
1139 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1140 return {};
1141 }
1142
1143 QList<ArchiveEntry> entries;
1144 struct archive_entry* entry;
1145 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1146 entries.append(toArchiveEntry(entry));
1147 archive_read_data_skip(reader.handle());
1148 }
1149
1150 qCDebug(QGClibarchiveLog) << "Listed" << entries.size() << "entries with metadata in" << archivePath;
1151 return entries;
1152}
1153
1154ArchiveStats getArchiveStats(const QString& archivePath)
1155{
1156 ArchiveReader reader;
1157 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1158 return {};
1159 }
1160
1161 ArchiveStats stats;
1162 struct archive_entry* entry;
1163
1164 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1165 stats.totalEntries++;
1166
1167 if (archive_entry_filetype(entry) == AE_IFDIR) {
1168 stats.directoryCount++;
1169 } else {
1170 stats.fileCount++;
1171 const qint64 size = archive_entry_size(entry);
1172 stats.totalUncompressedSize += size;
1173
1174 if (size > stats.largestFileSize) {
1175 stats.largestFileSize = size;
1176 stats.largestFileName = QString::fromUtf8(archive_entry_pathname(entry));
1177 }
1178 }
1179
1180 archive_read_data_skip(reader.handle());
1181 }
1182
1183 qCDebug(QGClibarchiveLog) << "Archive stats:" << stats.fileCount << "files," << stats.directoryCount << "dirs,"
1184 << stats.totalUncompressedSize << "bytes total";
1185 return stats;
1186}
1187
1188bool extractWithFilter(const QString& archivePath, const QString& outputDirectoryPath, EntryFilter filter,
1189 ProgressCallback progress, qint64 maxBytes)
1190{
1191 if (!filter) {
1192 qCWarning(QGClibarchiveLog) << "No filter provided";
1193 return false;
1194 }
1195
1196 if (!QGCFileHelper::ensureDirectoryExists(outputDirectoryPath)) {
1197 return false;
1198 }
1199
1200 // Pre-check: verify sufficient disk space (uses total archive size as upper bound)
1201 const ArchiveStats stats = getArchiveStats(archivePath);
1202 if (stats.totalUncompressedSize > 0) {
1203 if (!QGCFileHelper::hasSufficientDiskSpace(outputDirectoryPath, stats.totalUncompressedSize)) {
1204 return false;
1205 }
1206 }
1207
1208 ArchiveReader reader;
1209 if (!reader.open(archivePath, ReaderMode::AllFormats)) {
1210 return false;
1211 }
1212
1213 const qint64 totalSize = reader.dataSize();
1214 qint64 bytesProcessed = 0;
1215 qint64 bytesExtracted = 0;
1216 int extractedCount = 0;
1217 int skippedCount = 0;
1218 struct archive_entry* entry;
1219
1220 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1221 // Build ArchiveEntry for filter callback
1222 const ArchiveEntry info = toArchiveEntry(entry);
1223
1224 // Check filter
1225 if (!filter(info)) {
1226 skippedCount++;
1227 archive_read_data_skip(reader.handle());
1228 continue;
1229 }
1230
1231 // Skip directories for extraction (they're created by ensureParentExists)
1232 if (info.isDirectory) {
1233 archive_read_data_skip(reader.handle());
1234 continue;
1235 }
1236
1237 // Check size limit before extraction
1238 if (maxBytes > 0 && (bytesExtracted + info.size) > maxBytes) {
1239 qCWarning(QGClibarchiveLog) << "Size limit exceeded:" << maxBytes << "bytes";
1240 return false;
1241 }
1242
1243 const QString outputPath = QGCFileHelper::joinPath(outputDirectoryPath, info.name);
1245
1246 if (!writeArchiveEntryToFile(reader.handle(), outputPath)) {
1247 return false;
1248 }
1249
1250 bytesExtracted += info.size;
1251 extractedCount++;
1252
1253 // Progress callback
1254 if (progress) {
1255 bytesProcessed += info.size;
1256 if (!progress(bytesProcessed, totalSize)) {
1257 qCDebug(QGClibarchiveLog) << "Extraction cancelled by user";
1258 return false;
1259 }
1260 }
1261 }
1262
1263 qCDebug(QGClibarchiveLog) << "Extracted" << extractedCount << "files, skipped" << skippedCount;
1264 return true;
1265}
1266
1267// ----------------------------------------------------------------------------
1268// Single-File Decompression
1269// ----------------------------------------------------------------------------
1270
1271bool decompressSingleFile(const QString& inputPath, const QString& outputPath, ProgressCallback progress,
1272 qint64 maxBytes)
1273{
1274 ArchiveReader reader;
1275 if (!reader.open(inputPath, ReaderMode::RawFormat)) {
1276 return false;
1277 }
1278
1279 struct archive_entry* entry = nullptr;
1280 if (archive_read_next_header(reader.handle(), &entry) != ARCHIVE_OK) {
1281 qCWarning(QGClibarchiveLog) << "Failed to read header:" << archive_error_string(reader.handle());
1282 return false;
1283 }
1284
1285 updateFormatState(reader.handle());
1287
1288 return decompressStreamToFile(reader.handle(), outputPath, progress, reader.dataSize(), maxBytes);
1289}
1290
1291QByteArray decompressDataFromMemory(const QByteArray& data, qint64 maxBytes)
1292{
1293 if (data.isEmpty()) {
1294 qCWarning(QGClibarchiveLog) << "Cannot decompress empty data";
1295 return {};
1296 }
1297
1298 struct archive* a = archive_read_new();
1299 if (!a) {
1300 qCWarning(QGClibarchiveLog) << "Failed to create archive reader";
1301 return {};
1302 }
1303
1304 archive_read_support_filter_all(a);
1305 archive_read_support_format_raw(a);
1306
1307 if (archive_read_open_memory(a, data.constData(), static_cast<size_t>(data.size())) != ARCHIVE_OK) {
1308 qCWarning(QGClibarchiveLog) << "Failed to open compressed data:" << archive_error_string(a);
1309 archive_read_free(a);
1310 return {};
1311 }
1312
1313 struct archive_entry* entry;
1314 if (archive_read_next_header(a, &entry) != ARCHIVE_OK) {
1315 qCWarning(QGClibarchiveLog) << "Failed to read header:" << archive_error_string(a);
1316 archive_read_free(a);
1317 return {};
1318 }
1319
1320 updateFormatState(a);
1321
1322 QByteArray result = readArchiveToMemory(a, 0, maxBytes);
1323 archive_read_free(a);
1324 qCDebug(QGClibarchiveLog) << "Decompressed" << data.size() << "bytes to" << result.size() << "bytes";
1325 return result;
1326}
1327
1328// ----------------------------------------------------------------------------
1329// QIODevice-based Operations
1330// ----------------------------------------------------------------------------
1331
1332bool extractFromDevice(QIODevice* device, const QString& outputDirectoryPath, ProgressCallback progress,
1333 qint64 maxBytes)
1334{
1335 if (!QGCFileHelper::ensureDirectoryExists(outputDirectoryPath)) {
1336 return false;
1337 }
1338
1339 ArchiveReader reader;
1340 if (!reader.open(device, ReaderMode::AllFormats)) {
1341 return false;
1342 }
1343
1344 // For device-based reads, we don't know the total size upfront
1345 const qint64 totalSize = device->size() > 0 ? device->size() : 0;
1346 return extractArchiveEntries(reader.release(), outputDirectoryPath, progress, totalSize, maxBytes);
1347}
1348
1349QByteArray extractFileDataFromDevice(QIODevice* device, const QString& fileName)
1350{
1351 if (fileName.isEmpty()) {
1352 qCWarning(QGClibarchiveLog) << "Empty file name";
1353 return {};
1354 }
1355
1356 ArchiveReader reader;
1357 if (!reader.open(device, ReaderMode::AllFormats)) {
1358 return {};
1359 }
1360
1361 struct archive_entry* entry;
1362 while (archive_read_next_header(reader.handle(), &entry) == ARCHIVE_OK) {
1363 if (QString::fromUtf8(archive_entry_pathname(entry)) == fileName) {
1364 QByteArray result = readArchiveToMemory(reader.handle(), archive_entry_size(entry));
1365 if (!result.isEmpty()) {
1366 qCDebug(QGClibarchiveLog) << "Extracted" << fileName << "from device:" << result.size() << "bytes";
1367 }
1368 return result;
1369 }
1370 archive_read_data_skip(reader.handle());
1371 }
1372
1373 qCWarning(QGClibarchiveLog) << "File not found in archive:" << fileName;
1374 return QByteArray();
1375}
1376
1377bool decompressFromDevice(QIODevice* device, const QString& outputPath, ProgressCallback progress, qint64 maxBytes)
1378{
1379 ArchiveReader reader;
1380 if (!reader.open(device, ReaderMode::RawFormat)) {
1381 return false;
1382 }
1383
1385
1386 struct archive_entry* entry = nullptr;
1387 if (archive_read_next_header(reader.handle(), &entry) != ARCHIVE_OK) {
1388 qCWarning(QGClibarchiveLog) << "Failed to read header:" << archive_error_string(reader.handle());
1389 return false;
1390 }
1391
1392 updateFormatState(reader.handle());
1393
1394 const qint64 totalSize = device->size() > 0 ? device->size() : 0;
1395 return decompressStreamToFile(reader.handle(), outputPath, progress, totalSize, maxBytes);
1396}
1397
1398QByteArray decompressDataFromDevice(QIODevice* device, qint64 maxBytes)
1399{
1400 ArchiveReader reader;
1401 if (!reader.open(device, ReaderMode::RawFormat)) {
1402 return {};
1403 }
1404
1405 struct archive_entry* entry;
1406 if (archive_read_next_header(reader.handle(), &entry) != ARCHIVE_OK) {
1407 qCWarning(QGClibarchiveLog) << "Failed to read header:" << archive_error_string(reader.handle());
1408 return {};
1409 }
1410
1411 updateFormatState(reader.handle());
1412
1413 QByteArray result = readArchiveToMemory(reader.handle(), archive_entry_size(entry), maxBytes);
1414 if (!result.isEmpty()) {
1415 qCDebug(QGClibarchiveLog) << "Decompressed from device:" << result.size() << "bytes";
1416 }
1417 return result;
1418}
1419
1420} // namespace QGClibarchive
QString formatName
QString filterName
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Private implementation details for QGCCompression.
RAII wrapper for libarchive reader with automatic cleanup.
bool open(const QString &path, ReaderMode mode=ReaderMode::AllFormats)
struct archive * handle() const
Get the underlying archive handle.
struct archive * release()
Release ownership of archive handle (caller must free)
bool isQtResource(const QString &path)
constexpr size_t kBufferSizeMax
Maximum buffer size for I/O operations.
bool ensureParentExists(const QString &filePath)
bool hasSufficientDiskSpace(const QString &path, qint64 requiredBytes, double margin)
bool ensureDirectoryExists(const QString &path)
QString joinPath(const QString &dir, const QString &name)
bool exists(const QString &path)
size_t optimalBufferSize(const QString &path)
QGCCompression::EntryFilter EntryFilter
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)
la_int64_t deviceSkipCallback(struct archive *, void *clientData, la_int64_t request)
QGCCompression::ProgressCallback ProgressCallback
QString lastDetectedFilterName()
ReaderMode
Mode for ArchiveReader format support.
@ AllFormats
Support all archive formats (ZIP, TAR, 7z, etc.)
@ RawFormat
Raw format for single-file decompression (.gz, .xz, etc.)
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)
la_ssize_t deviceReadCallback(struct archive *, void *clientData, const void **buffer)
bool extractSingleFile(const QString &archivePath, const QString &fileName, const QString &outputPath)
bool openArchiveForReading(struct archive *a, const QString &filePath, QByteArray &resourceData)
QByteArray decompressDataFromDevice(QIODevice *device, qint64 maxBytes)
QList< ArchiveEntry > listArchiveEntriesDetailed(const QString &archivePath)
la_int64_t deviceSeekCallback(struct archive *, void *clientData, la_int64_t offset, int whence)
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)
ArchiveEntry toArchiveEntry(struct archive_entry *entry)
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)
int deviceCloseCallback(struct archive *, void *)
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.
bool isDirectory
True if entry is a directory.
qint64 size
Uncompressed size in bytes.
QString name
Path/name within archive.
quint32 permissions
Unix-style permissions.
QDateTime modified
Last modification time.
Summary statistics for an archive.
qint64 totalUncompressedSize
Sum of all file sizes (uncompressed)
qint64 largestFileSize
Size of largest file.
QString largestFileName
Name of largest file.
int totalEntries
Total number of entries (files + directories)
int directoryCount
Number of directories.