Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions src/imagewriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ ImageWriter::ImageWriter(QObject *parent)
_osListRefreshTimer(),
_suspendInhibitor(nullptr),
_thread(nullptr),
_verifyEnabled(true), _multipleFilesInZip(false), _online(false), _extractSizeKnown(true),
_verifyEnabled(true), _multipleFilesInZip(false), _online(false), _extractSizeKnown(true), _needsDecompressScan(false),
_settings(),
_translations(),
_trans(nullptr),
Expand Down Expand Up @@ -491,6 +491,7 @@ void ImageWriter::setSrc(const QUrl &url, quint64 downloadLen, quint64 extrLen,
_osReleaseDate = releaseDate;
// If extract size is provided from manifest, we can trust it; otherwise assume known until proven otherwise
_extractSizeKnown = true;
_needsDecompressScan = false;
qDebug() << "setSrc: initFormat parameter:" << initFormat << "-> _initFormat set to:" << _initFormat;

if (!_downloadLen && url.isLocalFile())
Expand Down Expand Up @@ -824,7 +825,9 @@ void ImageWriter::startWrite()
try {
if (QUrl(urlstr).isLocalFile())
{
_thread = new LocalFileExtractThread(urlstr, writeDevicePath.toLatin1(), _expectedHash, this);
auto *localThread = new LocalFileExtractThread(urlstr, writeDevicePath.toLatin1(), _expectedHash, this);
localThread->setNeedsDecompressScan(_needsDecompressScan);
_thread = localThread;
}
else
{
Expand Down Expand Up @@ -2298,10 +2301,10 @@ void ImageWriter::_parseGzFile()
// but mark the size as unreliable for progress display purposes.
const qint64 GZIP_TRAILER_SIZE = 8;

// Mark gzip extract size as unreliable - the format cannot accurately represent
// sizes >4GB, so progress percentage cannot be reliably calculated.
// This causes the UI to show an indeterminate progress bar instead of misleading percentages.
_extractSizeKnown = false;
// By default, assume ISIZE is accurate (files <4GB).
// Will be overridden below if ISIZE wraps.
_extractSizeKnown = true;
_needsDecompressScan = false;

if (f.size() > GZIP_TRAILER_SIZE && f.open(QIODevice::ReadOnly))
{
Expand All @@ -2320,16 +2323,30 @@ void ImageWriter::_parseGzFile()

// Handle files larger than 4GB where ISIZE wraps around
// If the uncompressed size appears smaller than the compressed size,
// the original file was likely > 4GB. This is a heuristic for storage
// space checks but NOT reliable for progress calculation.
// the original file was likely > 4GB.
qint64 compressedSize = f.size();
bool isizeWrapped = false;
while (_extrLen < static_cast<quint64>(compressedSize))
{
_extrLen += Q_UINT64_C(0x100000000); // Add 4GB
isizeWrapped = true;
}

qDebug() << "Parsed .gz file. Estimated uncompressed size:" << _extrLen
<< "(ISIZE field:" << isize << ") - size unreliable for progress";
if (isizeWrapped)
{
// ISIZE wrapped: 32-bit field cannot represent the true size.
// Use sample-based estimation on the worker thread for progress.
_extractSizeKnown = false;
_needsDecompressScan = true;
qDebug() << "Parsed .gz file. ISIZE wrapped (>4GB). Heuristic estimate:"
<< _extrLen << "- will estimate via sampling";
}
else
{
// ISIZE did not wrap: the 32-bit value is the exact uncompressed size.
qDebug() << "Parsed .gz file. Exact uncompressed size:" << _extrLen
<< "(ISIZE field:" << isize << ")";
}
}
else
{
Expand Down Expand Up @@ -3555,7 +3572,9 @@ void ImageWriter::_continueStartWriteAfterCacheVerification(bool cacheIsValid)
// Use platform-specific write device path (e.g., rdisk on macOS for direct I/O)
QString writeDevicePath = PlatformQuirks::getWriteDevicePath(_dst);
try {
_thread = new LocalFileExtractThread(urlstr.toLatin1(), writeDevicePath.toLatin1(), _expectedHash, this);
auto *localThread = new LocalFileExtractThread(urlstr.toLatin1(), writeDevicePath.toLatin1(), _expectedHash, this);
localThread->setNeedsDecompressScan(_needsDecompressScan);
_thread = localThread;
} catch (const std::bad_alloc& e) {
_handleMemoryAllocationFailure(e.what());
return;
Expand Down
2 changes: 1 addition & 1 deletion src/imagewriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ protected slots:
QTimer _osListRefreshTimer;
SuspendInhibitor *_suspendInhibitor;
DownloadThread *_thread;
bool _verifyEnabled, _multipleFilesInZip, _online, _extractSizeKnown;
bool _verifyEnabled, _multipleFilesInZip, _online, _extractSizeKnown, _needsDecompressScan;
QSettings _settings;
QMap<QString,QString> _translations;
QTranslator *_trans;
Expand Down
76 changes: 76 additions & 0 deletions src/localfileextractthread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <archive_entry.h>

#include <QUrl>
#include <QFileInfo>
#include <QDebug>

LocalFileExtractThread::LocalFileExtractThread(const QByteArray &url, const QByteArray &dst, const QByteArray &expectedHash, QObject *parent)
Expand Down Expand Up @@ -81,6 +82,16 @@ void LocalFileExtractThread::run()
canUseArchive = _testArchiveFormat();
}

if (isImage() && canUseArchive && _needsDecompressScan)
{
quint64 estimatedSize = _estimateDecompressedSize();
if (estimatedSize > 0 && !_cancelled)
{
setExtractTotal(estimatedSize);
qDebug() << "Estimation complete. Estimated decompressed size:" << estimatedSize;
}
}

if (isImage() && canUseArchive)
extractImageRun(); // Use libarchive for compressed/archive files
else if (isImage() && !canUseArchive)
Expand Down Expand Up @@ -166,6 +177,71 @@ void LocalFileExtractThread::extractRawImageRun()
}
}

quint64 LocalFileExtractThread::_estimateDecompressedSize()
{
emit preparationStatusUpdate(tr("Estimating compressed image size..."));

QString filePath = QUrl(_url).toLocalFile();
struct archive *a = archive_read_new();
struct archive_entry *entry;
quint64 decompressedSample = 0;

archive_read_support_filter_all(a);
archive_read_support_format_all(a);
archive_read_support_format_raw(a);

const size_t blockSize = 1024 * 1024;
// Sample ~50MB of compressed data to get a representative compression ratio
const qint64 compressedSampleTarget = 50 * 1024 * 1024;

if (archive_read_open_filename(a, filePath.toLocal8Bit().constData(), blockSize) == ARCHIVE_OK)
{
if (archive_read_next_header(a, &entry) == ARCHIVE_OK)
{
// Reuse the existing heap-allocated input buffer for reading
ssize_t size;
while ((size = archive_read_data(a, _inputBuf, _inputBufSize)) > 0)
{
decompressedSample += size;
if (_cancelled)
break;

qint64 compressedConsumed = archive_filter_bytes(a, -1);
if (compressedConsumed >= compressedSampleTarget)
break;
}
}
}

qint64 compressedConsumed = archive_filter_bytes(a, -1);
archive_read_free(a);

// Reset input file position and download counter for the actual extraction pass
_inputfile.seek(0);
_lastDlNow = 0;

if (_cancelled || compressedConsumed <= 0 || decompressedSample == 0)
{
qDebug() << "Decompressed size estimation failed or cancelled";
return 0;
}

// Calculate compression ratio and extrapolate
QFileInfo fi(filePath);
qint64 totalCompressedSize = fi.size();
double ratio = static_cast<double>(decompressedSample) / static_cast<double>(compressedConsumed);
quint64 estimated = static_cast<quint64>(totalCompressedSize * ratio);

qDebug() << "Decompressed size estimation:"
<< "sample_compressed=" << compressedConsumed
<< "sample_decompressed=" << decompressedSample
<< "ratio=" << ratio
<< "estimated_total=" << estimated
<< QString("(%1 GB)").arg(estimated / 1073741824.0, 0, 'f', 2);

return estimated;
}

bool LocalFileExtractThread::_testArchiveFormat()
{
// Test if libarchive can handle this file format AND actually extract data from it
Expand Down
3 changes: 3 additions & 0 deletions src/localfileextractthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ class LocalFileExtractThread : public DownloadExtractThread
public:
explicit LocalFileExtractThread(const QByteArray &url, const QByteArray &dst = "", const QByteArray &expectedHash = "", QObject *parent = nullptr);
virtual ~LocalFileExtractThread();
void setNeedsDecompressScan(bool needs) { _needsDecompressScan = needs; }

protected:
virtual void _cancelExtract();
virtual void run();
virtual ssize_t _on_read(struct archive *a, const void **buff);
virtual int _on_close(struct archive *a);
void extractRawImageRun();
quint64 _estimateDecompressedSize();
bool _testArchiveFormat();
static ssize_t _archive_read_test(struct archive *, void *client_data, const void **buff);
static int _archive_close_test(struct archive *, void *client_data);
Expand All @@ -36,6 +38,7 @@ class LocalFileExtractThread : public DownloadExtractThread

private:
SuspendInhibitor *_suspendInhibitor;
bool _needsDecompressScan = false;
};

#endif // LOCALFILEEXTRACTTHREAD_H