4#include <QtCore/QFileInfo>
5#include <QtCore/QSaveFile>
7#include <QtCore/QTemporaryDir>
10#include <archive_entry.h>
29thread_local FormatState t_formatState;
31void updateFormatState(
struct archive* a)
34 t_formatState.formatName.clear();
35 t_formatState.filterName.clear();
40 const char* fmt = archive_format_name(a);
41 t_formatState.formatName = fmt ? QString::fromUtf8(fmt) : QString();
44 const char* flt = archive_filter_name(a, 0);
45 t_formatState.filterName = flt ? QString::fromUtf8(flt) : QStringLiteral(
"none");
47 if (!t_formatState.formatName.isEmpty()) {
48 qCDebug(QGClibarchiveLog) <<
"Detected format:" << t_formatState.formatName
49 <<
"filter:" << t_formatState.filterName;
59 return t_formatState.formatName;
64 return t_formatState.filterName;
78 auto* device =
static_cast<QIODevice*
>(clientData);
80 const qint64 bytesRead = device->read(readBuffer,
sizeof(readBuffer));
86 return static_cast<la_ssize_t
>(bytesRead);
91 auto* device =
static_cast<QIODevice*
>(clientData);
93 if (!device->isSequential()) {
94 const qint64 currentPos = device->pos();
95 const qint64 newPos = currentPos + request;
96 if (device->seek(newPos)) {
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);
109 skipped += bytesRead;
122 auto* device =
static_cast<QIODevice*
>(clientData);
124 if (device->isSequential()) {
125 return ARCHIVE_FATAL;
134 newPos = device->pos() + offset;
137 newPos = device->size() + offset;
140 return ARCHIVE_FATAL;
143 if (!device->seek(newPos)) {
144 return ARCHIVE_FATAL;
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));
162 const time_t mtime = archive_entry_mtime(entry);
164 info.
modified = QDateTime::fromSecsSinceEpoch(mtime);
183QByteArray readArchiveToMemory(
struct archive* a, qint64 expectedSize = 0, qint64 maxBytes = 0)
186 if (expectedSize > 0) {
187 result.reserve(
static_cast<qsizetype
>(expectedSize));
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";
197 result.append(buffer,
static_cast<qsizetype
>(size));
201 qCWarning(QGClibarchiveLog) <<
"Read error:" << archive_error_string(a);
214bool writeArchiveEntryToFile(
struct archive* a,
const QString& outputPath)
216 QSaveFile outFile(outputPath);
217 if (!outFile.open(QIODevice::WriteOnly)) {
218 qCWarning(QGClibarchiveLog) <<
"Failed to open output file:" << outputPath << outFile.errorString();
227 while ((r = archive_read_data_block(a, &buff, &size, &offset)) == ARCHIVE_OK) {
229 if (!outFile.seek(offset)) {
230 qCWarning(QGClibarchiveLog) <<
"Failed to seek:" << outFile.errorString();
231 outFile.cancelWriting();
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();
241 if (r != ARCHIVE_EOF) {
242 qCWarning(QGClibarchiveLog) <<
"Read error:" << archive_error_string(a);
243 outFile.cancelWriting();
248 if (!outFile.commit()) {
249 qCWarning(QGClibarchiveLog) <<
"Failed to commit output file:" << outFile.errorString();
263bool decompressStreamToFile(
struct archive* a,
const QString& outputPath,
266 QSaveFile outFile(outputPath);
267 if (!outFile.open(QIODevice::WriteOnly)) {
268 qCWarning(QGClibarchiveLog) <<
"Failed to open output file:" << outputPath << outFile.errorString();
272 qint64 totalBytesWritten = 0;
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();
283 if (outFile.write(buffer, size) != size) {
284 qCWarning(QGClibarchiveLog) <<
"Failed to write output:" << outFile.errorString();
285 outFile.cancelWriting();
288 totalBytesWritten += size;
291 const qint64 bytesRead = archive_filter_bytes(a, -1);
292 if (!progress(bytesRead, totalSize)) {
293 qCDebug(QGClibarchiveLog) <<
"Decompression cancelled by user";
294 outFile.cancelWriting();
301 qCWarning(QGClibarchiveLog) <<
"Decompression error:" << archive_error_string(a);
302 outFile.cancelWriting();
306 if (!outFile.commit()) {
307 qCWarning(QGClibarchiveLog) <<
"Failed to commit output file:" << outFile.errorString();
311 qCDebug(QGClibarchiveLog) <<
"Decompressed" << totalBytesWritten <<
"bytes to" << outputPath;
318void cleanupCreatedEntries(
const QStringList& createdFiles,
const QStringList& createdDirs)
321 for (
const QString& file : createdFiles) {
326 for (
auto it = createdDirs.crbegin(); it != createdDirs.crend(); ++it) {
339void trackAndCreateParentDirs(
const QString& path,
const QString& outputDir, QSet<QString>& existingDirs,
340 QStringList& createdDirs)
342 QStringList dirsToCreate;
343 QString current = QFileInfo(path).absolutePath();
346 while (current != outputDir && !current.isEmpty() && !existingDirs.contains(current)) {
347 if (!QDir(current).
exists()) {
348 dirsToCreate.prepend(current);
350 existingDirs.insert(current);
353 current = QFileInfo(current).absolutePath();
357 for (
const QString& dir : dirsToCreate) {
358 if (QDir().mkdir(dir)) {
359 createdDirs.append(dir);
360 existingDirs.insert(dir);
372bool extractArchiveEntries(
struct archive* a,
const QString& outputDirectoryPath,
375 struct archive* ext = archive_write_disk_new();
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);
387 bool cancelled =
false;
388 bool sizeLimitExceeded =
false;
389 qint64 totalBytesWritten = 0;
390 struct archive_entry* entry;
393 QStringList createdFiles;
394 QStringList createdDirs;
395 QSet<QString> existingDirs;
400 QString canonicalOutputDir = QFileInfo(outputDirectoryPath).canonicalFilePath();
401 if (canonicalOutputDir.isEmpty()) {
402 canonicalOutputDir = QFileInfo(outputDirectoryPath).absoluteFilePath();
404 existingDirs.insert(canonicalOutputDir);
406 bool formatLogged =
false;
407 while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
410 updateFormatState(a);
414 const char* currentFile = archive_entry_pathname(entry);
415 QString entryName = QString::fromUtf8(currentFile);
419 QFileInfo outputInfo(outputPath);
420 QString canonicalOutput = outputInfo.absoluteFilePath();
422 if (!canonicalOutput.startsWith(canonicalOutputDir +
"/") && canonicalOutput != canonicalOutputDir) {
423 qCWarning(QGClibarchiveLog) <<
"Skipping path traversal attempt:" << currentFile;
428 if (entryName.contains(QChar(
'\0'))) {
429 qCWarning(QGClibarchiveLog) <<
"Skipping path with embedded null byte";
434 const auto fileType = archive_entry_filetype(entry);
435 if (fileType == AE_IFLNK) {
436 const char* symlinkTarget = archive_entry_symlink(entry);
438 QString target = QString::fromUtf8(symlinkTarget);
439 QString resolvedTarget;
441 if (QFileInfo(target).isAbsolute()) {
443 qCWarning(QGClibarchiveLog)
444 <<
"Skipping symlink with absolute target:" << currentFile <<
"->" << target;
448 QString entryDir = QFileInfo(outputPath).absolutePath();
449 resolvedTarget = QFileInfo(entryDir +
"/" + target).absoluteFilePath();
453 if (!resolvedTarget.startsWith(canonicalOutputDir +
"/") && resolvedTarget != canonicalOutputDir) {
454 qCWarning(QGClibarchiveLog)
455 <<
"Skipping symlink escaping output directory:" << currentFile <<
"->" << symlinkTarget;
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);
470 archive_entry_set_pathname(entry, outputPath.toUtf8().constData());
473 trackAndCreateParentDirs(outputPath, canonicalOutputDir, existingDirs, createdDirs);
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);
484 if (fileType == AE_IFDIR) {
485 if (!existingDirs.contains(canonicalOutput)) {
486 createdDirs.append(canonicalOutput);
487 existingDirs.insert(canonicalOutput);
490 createdFiles.append(canonicalOutput);
493 if (archive_entry_size(entry) > 0) {
498 while ((r = archive_read_data_block(a, &buff, &size, &offset)) == ARCHIVE_OK) {
500 if (maxBytes > 0 && (totalBytesWritten +
static_cast<qint64
>(size)) > maxBytes) {
501 qCWarning(QGClibarchiveLog) <<
"Size limit exceeded:" << maxBytes <<
"bytes";
502 sizeLimitExceeded =
true;
507 if (archive_write_data_block(ext, buff, size, offset) != ARCHIVE_OK) {
508 qCWarning(QGClibarchiveLog) <<
"Failed to write data:" << archive_error_string(ext);
513 totalBytesWritten +=
static_cast<qint64
>(size);
517 const qint64 compressedBytesRead = archive_filter_bytes(a, -1);
518 if (!progress(compressedBytesRead, totalSize)) {
519 qCDebug(QGClibarchiveLog) <<
"Extraction cancelled by user";
530 if (r != ARCHIVE_EOF && r != ARCHIVE_OK) {
531 qCWarning(QGClibarchiveLog) <<
"Failed to read data:" << archive_error_string(a);
537 if (archive_write_finish_entry(ext) != ARCHIVE_OK) {
538 qCWarning(QGClibarchiveLog) <<
"Failed to finish entry:" << archive_error_string(ext);
544 archive_read_close(a);
545 archive_read_free(a);
546 archive_write_close(ext);
547 archive_write_free(ext);
550 if (cancelled || sizeLimitExceeded) {
551 cleanupCreatedEntries(createdFiles, createdDirs);
572 archive_read_free(_archive);
578 if (!_resourceData.isEmpty()) {
579 return _resourceData.size();
582 return _device->size() > 0 ? _device->size() : 0;
584 if (!_filePath.isEmpty()) {
585 return QFileInfo(_filePath).size();
595 const char* name = archive_format_name(_archive);
596 return name ? QString::fromUtf8(name) : QString();
606 const int filterCount = archive_filter_count(_archive);
607 if (filterCount <= 1) {
608 return QStringLiteral(
"none");
612 const char* name = archive_filter_name(_archive, 0);
613 return name ? QString::fromUtf8(name) : QStringLiteral(
"none");
619 archive_read_free(_archive);
625 _archive = archive_read_new();
627 qCWarning(QGClibarchiveLog) <<
"Failed to create archive reader";
631 archive_read_support_filter_all(_archive);
635 archive_read_support_format_all(_archive);
638 archive_read_support_format_raw(_archive);
643 archive_read_free(_archive);
653 if (!device || !device->isOpen() || !device->isReadable()) {
654 qCWarning(QGClibarchiveLog) <<
"Device is null, not open, or not readable";
659 archive_read_free(_archive);
663 _resourceData.clear();
665 _archive = archive_read_new();
667 qCWarning(QGClibarchiveLog) <<
"Failed to create archive reader";
671 archive_read_support_filter_all(_archive);
675 archive_read_support_format_all(_archive);
678 archive_read_support_format_raw(_archive);
684 if (!device->isSequential()) {
688 const int result = archive_read_open2(_archive, device,
692 if (result != ARCHIVE_OK) {
693 qCWarning(QGClibarchiveLog) <<
"Failed to open device:" << archive_error_string(_archive);
694 archive_read_free(_archive);
713 QFile inputFile(filePath);
714 if (!inputFile.open(QIODevice::ReadOnly)) {
715 qCWarning(QGClibarchiveLog) <<
"Failed to open file:" << filePath << inputFile.errorString();
718 resourceData = inputFile.readAll();
721 if (resourceData.isEmpty()) {
722 qCWarning(QGClibarchiveLog) <<
"File is empty:" << filePath;
726 if (archive_read_open_memory(a, resourceData.constData(),
static_cast<size_t>(resourceData.size())) !=
728 qCWarning(QGClibarchiveLog) <<
"Failed to open data:" << archive_error_string(a);
733 if (archive_read_open_filename(a, filePath.toLocal8Bit().constData(),
735 qCWarning(QGClibarchiveLog) <<
"Failed to open file:" << filePath << archive_error_string(a);
767 const qint64 totalSize = reader.
dataSize();
768 return extractArchiveEntries(reader.
release(), outputDirectoryPath, progress, totalSize, maxBytes);
774 const QFileInfo outputInfo(outputDirectoryPath);
775 if (outputInfo.fileName().isEmpty()) {
776 qCWarning(QGClibarchiveLog) <<
"Invalid output directory path:" << outputDirectoryPath;
780 const QString parentDirectoryPath = outputInfo.absoluteDir().absolutePath();
782 qCWarning(QGClibarchiveLog) <<
"Failed to create output parent directory:" << parentDirectoryPath;
789 qCWarning(QGClibarchiveLog) <<
"Archive is empty or invalid:" << archivePath;
801 const QString outputDirectoryName = outputInfo.fileName();
805 QStringLiteral(
"%1.qgc_stage_XXXXXX").arg(outputDirectoryName)));
806 if (!stagingDir.isValid()) {
807 qCWarning(QGClibarchiveLog) <<
"Failed to create staging directory:" << stagingDir.errorString();
810 stagingDir.setAutoRemove(
false);
812 const QString stagingPath = stagingDir.path();
813 const QFileInfo stagingInfo(stagingPath);
814 const QString stagingName = stagingInfo.fileName();
816 qCDebug(QGClibarchiveLog) <<
"Atomic extraction: staging to" << stagingPath;
821 (void) QDir(stagingPath).removeRecursively();
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();
832 QDir parentDir(parentDirectoryPath);
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();
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();
852 backupDir.setAutoRemove(
false);
853 backupPath = backupDir.path();
854 backupName = QFileInfo(backupPath).fileName();
855 (void) backupDir.remove();
857 if (!parentDir.rename(outputDirectoryName, backupName)) {
858 qCWarning(QGClibarchiveLog) <<
"Failed to move existing output to backup:" << outputDirectoryPath;
859 (void) QDir(stagingPath).removeRecursively();
864 if (!parentDir.rename(stagingName, outputDirectoryName)) {
865 qCWarning(QGClibarchiveLog) <<
"Failed to commit staged extraction for" << outputDirectoryPath;
866 (void) QDir(stagingPath).removeRecursively();
868 if (!backupName.isEmpty()) {
869 if (!parentDir.rename(backupName, outputDirectoryName)) {
870 qCWarning(QGClibarchiveLog) <<
"Failed to rollback backup for" << outputDirectoryPath;
877 if (!backupPath.isEmpty()) {
878 (void) QDir(backupPath).removeRecursively();
881 qCDebug(QGClibarchiveLog) <<
"Atomic extraction committed:" << outputDirectoryPath;
885bool extractSingleFile(
const QString& archivePath,
const QString& fileName,
const QString& outputPath)
892 struct archive_entry* entry;
894 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
895 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
897 if (entryName == fileName) {
899 return writeArchiveEntryToFile(reader.
handle(), outputPath);
902 archive_read_data_skip(reader.
handle());
905 qCWarning(QGClibarchiveLog) <<
"File not found in archive:" << fileName;
911 if (fileName.isEmpty()) {
912 qCWarning(QGClibarchiveLog) <<
"Empty file name";
921 struct archive_entry* entry;
923 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
924 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
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";
934 archive_read_data_skip(reader.
handle());
937 qCWarning(QGClibarchiveLog) <<
"File not found in archive:" << fileName;
941bool extractMultipleFiles(
const QString& archivePath,
const QStringList& fileNames,
const QString& outputDirectoryPath)
943 if (fileNames.isEmpty()) {
956 QSet<QString> targetFiles(fileNames.begin(), fileNames.end());
957 QSet<QString> extractedFiles;
958 struct archive_entry* entry;
960 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
961 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
963 if (!targetFiles.contains(entryName)) {
964 archive_read_data_skip(reader.
handle());
971 if (!writeArchiveEntryToFile(reader.
handle(), outputPath)) {
975 extractedFiles.insert(entryName);
977 if (extractedFiles.size() == targetFiles.size()) {
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;
994bool extractByPattern(
const QString& archivePath,
const QStringList& patterns,
const QString& outputDirectoryPath,
995 QStringList* extractedFiles)
997 if (patterns.isEmpty()) {
1005 struct archive* match = archive_match_new();
1007 qCWarning(QGClibarchiveLog) <<
"Failed to create archive_match";
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);
1021 archive_match_free(match);
1025 struct archive_entry* entry;
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());
1034 if (archive_entry_filetype(entry) == AE_IFDIR) {
1035 archive_read_data_skip(reader.
handle());
1039 const QString entryName = QString::fromUtf8(archive_entry_pathname(entry));
1044 if (!writeArchiveEntryToFile(reader.
handle(), outputPath)) {
1045 archive_match_free(match);
1050 if (extractedFiles) {
1051 extractedFiles->append(entryName);
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;
1062 archive_match_free(match);
1064 qCDebug(QGClibarchiveLog) <<
"Extracted" << matchCount <<
"files matching patterns from" << archivePath;
1065 return matchCount > 0;
1075 struct archive_entry* entry;
1078 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
1082 while ((size = archive_read_data(reader.
handle(), buffer,
sizeof(buffer))) > 0) {
1087 qCWarning(QGClibarchiveLog) <<
"Validation failed for entry:" << archive_entry_pathname(entry)
1088 << archive_error_string(reader.
handle());
1098 if (fileName.isEmpty()) {
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) {
1112 archive_read_data_skip(reader.
handle());
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());
1132 qCDebug(QGClibarchiveLog) <<
"Listed" << entries.size() <<
"entries in" << archivePath;
1143 QList<ArchiveEntry> entries;
1144 struct archive_entry* entry;
1145 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
1147 archive_read_data_skip(reader.
handle());
1150 qCDebug(QGClibarchiveLog) <<
"Listed" << entries.size() <<
"entries with metadata in" << archivePath;
1162 struct archive_entry* entry;
1164 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
1167 if (archive_entry_filetype(entry) == AE_IFDIR) {
1171 const qint64 size = archive_entry_size(entry);
1176 stats.
largestFileName = QString::fromUtf8(archive_entry_pathname(entry));
1180 archive_read_data_skip(reader.
handle());
1183 qCDebug(QGClibarchiveLog) <<
"Archive stats:" << stats.
fileCount <<
"files," << stats.
directoryCount <<
"dirs,"
1192 qCWarning(QGClibarchiveLog) <<
"No filter provided";
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;
1220 while (archive_read_next_header(reader.
handle(), &entry) == ARCHIVE_OK) {
1225 if (!filter(info)) {
1227 archive_read_data_skip(reader.
handle());
1233 archive_read_data_skip(reader.
handle());
1238 if (maxBytes > 0 && (bytesExtracted + info.
size) > maxBytes) {
1239 qCWarning(QGClibarchiveLog) <<
"Size limit exceeded:" << maxBytes <<
"bytes";
1246 if (!writeArchiveEntryToFile(reader.
handle(), outputPath)) {
1250 bytesExtracted += info.
size;
1255 bytesProcessed += info.
size;
1256 if (!progress(bytesProcessed, totalSize)) {
1257 qCDebug(QGClibarchiveLog) <<
"Extraction cancelled by user";
1263 qCDebug(QGClibarchiveLog) <<
"Extracted" << extractedCount <<
"files, skipped" << skippedCount;
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());
1285 updateFormatState(reader.
handle());
1288 return decompressStreamToFile(reader.
handle(), outputPath, progress, reader.
dataSize(), maxBytes);
1293 if (data.isEmpty()) {
1294 qCWarning(QGClibarchiveLog) <<
"Cannot decompress empty data";
1298 struct archive* a = archive_read_new();
1300 qCWarning(QGClibarchiveLog) <<
"Failed to create archive reader";
1304 archive_read_support_filter_all(a);
1305 archive_read_support_format_raw(a);
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);
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);
1320 updateFormatState(a);
1322 QByteArray result = readArchiveToMemory(a, 0, maxBytes);
1323 archive_read_free(a);
1324 qCDebug(QGClibarchiveLog) <<
"Decompressed" << data.size() <<
"bytes to" << result.size() <<
"bytes";
1345 const qint64 totalSize = device->size() > 0 ? device->size() : 0;
1346 return extractArchiveEntries(reader.
release(), outputDirectoryPath, progress, totalSize, maxBytes);
1351 if (fileName.isEmpty()) {
1352 qCWarning(QGClibarchiveLog) <<
"Empty file name";
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";
1370 archive_read_data_skip(reader.
handle());
1373 qCWarning(QGClibarchiveLog) <<
"File not found in archive:" << fileName;
1374 return QByteArray();
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());
1392 updateFormatState(reader.
handle());
1394 const qint64 totalSize = device->size() > 0 ? device->size() : 0;
1395 return decompressStreamToFile(reader.
handle(), outputPath, progress, totalSize, maxBytes);
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());
1411 updateFormatState(reader.
handle());
1413 QByteArray result = readArchiveToMemory(reader.
handle(), archive_entry_size(entry), maxBytes);
1414 if (!result.isEmpty()) {
1415 qCDebug(QGClibarchiveLog) <<
"Decompressed from device:" << result.size() <<
"bytes";
#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.
QString formatName() const
QString filterName() const
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.
int fileCount
Number of files.