retro-imager/downloadextractthread.cpp
Floris Bos fa7637e7dc Do not use libcurl for reading local files
We originally used libcurl for both downloading images from
Internet and reading local files to have the same code path for both.

It doesn't work that well in practice, as Qt and libcurl are not
on the same page how special characters such as Chinese characters
are represented in a local file URL.

So create a new class to handle local files.

Closes #76
2020-06-01 19:43:51 +02:00

439 lines
12 KiB
C++

/*
* SPDX-License-Identifier: Apache-2.0
* Copyright (C) 2020 Raspberry Pi (Trading) Limited
*/
#include "downloadextractthread.h"
#include "config.h"
#include "dependencies/drivelist/src/drivelist.hpp"
#include "dependencies/mountutils/src/mountutils.hpp"
#include <iostream>
#include <archive.h>
#include <archive_entry.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <QDir>
#include <QProcess>
#include <QTemporaryDir>
#include <QDebug>
using namespace std;
const int DownloadExtractThread::MAX_QUEUE_SIZE = 64;
class _extractThreadClass : public QThread {
public:
_extractThreadClass(DownloadExtractThread *parent)
: QThread(parent), _de(parent)
{
}
virtual void run()
{
if (_de->isImage())
_de->extractImageRun();
else
_de->extractMultiFileRun();
}
protected:
DownloadExtractThread *_de;
};
DownloadExtractThread::DownloadExtractThread(const QByteArray &url, const QByteArray &localfilename, const QByteArray &expectedHash, QObject *parent)
: DownloadThread(url, localfilename, expectedHash, parent), _abufsize(IMAGEWRITER_BLOCKSIZE), _ethreadStarted(false),
_isImage(true), _inputHash(OSLIST_HASH_ALGORITHM), _activeBuf(0), _writeThreadStarted(false)
{
_extractThread = new _extractThreadClass(this);
_abuf[0] = (char *) qMallocAligned(_abufsize, 4096);
_abuf[1] = (char *) qMallocAligned(_abufsize, 4096);
}
DownloadExtractThread::~DownloadExtractThread()
{
if (!_extractThread->wait(2000))
{
_extractThread->terminate();
}
_queue.clear();
_cv.notify_one();
qFreeAligned(_abuf[0]);
qFreeAligned(_abuf[1]);
}
size_t DownloadExtractThread::_writeData(const char *buf, size_t len)
{
if (_cancelled)
return 0;
_writeCache(buf, len);
if (!_ethreadStarted)
{
// Extract thread is started when first data comes in
_ethreadStarted = true;
_extractThread->start();
msleep(100);
}
if (!_isImage)
{
_inputHash.addData(buf, len);
}
_pushQueue(buf, len);
return len;
}
void DownloadExtractThread::_onDownloadSuccess()
{
_pushQueue("", 0);
}
void DownloadExtractThread::_onDownloadError(const QString &msg)
{
DownloadThread::_onDownloadError(msg);
_cancelExtract();
}
void DownloadExtractThread::_cancelExtract()
{
std::unique_lock<std::mutex> lock(_queueMutex);
_queue.clear();
_queue.push_back(QByteArray());
if (_queue.size() == 1)
{
lock.unlock();
_cv.notify_one();
}
}
void DownloadExtractThread::cancelDownload()
{
DownloadThread::cancelDownload();
_cancelExtract();
}
// Raise exception on libarchive errors
static inline void _checkResult(int r, struct archive *a)
{
if (r < ARCHIVE_OK)
// Warning
cerr << archive_error_string(a) << endl;
if (r < ARCHIVE_WARN)
// Fatal
throw runtime_error(archive_error_string(a));
}
// libarchive thread
void DownloadExtractThread::extractImageRun()
{
struct archive *a = archive_read_new();
struct archive_entry *entry;
int r;
archive_read_support_filter_all(a);
archive_read_support_format_all(a);
archive_read_support_format_raw(a); // for .gz and such
archive_read_open(a, this, NULL, &DownloadExtractThread::_archive_read, &DownloadExtractThread::_archive_close);
try
{
r = archive_read_next_header(a, &entry);
_checkResult(r, a);
while (true)
{
ssize_t size = archive_read_data(a, _abuf[_activeBuf], _abufsize);
if (size < 0)
throw runtime_error(archive_error_string(a));
if (size == 0)
break;
if (_writeThreadStarted)
{
//if (_writeFile(_abuf, size) != (size_t) size)
if (!_writeFuture.result())
{
if (!_cancelled)
{
DownloadThread::cancelDownload();
emit error(tr("Error writing to storage"));
}
archive_read_free(a);
return;
}
}
_writeFuture = QtConcurrent::run(static_cast<DownloadThread *>(this), &DownloadThread::_writeFile, _abuf[_activeBuf], size);
_activeBuf = _activeBuf ? 0 : 1;
_writeThreadStarted = true;
}
if (_writeThreadStarted)
_writeFuture.waitForFinished();
_writeComplete();
}
catch (exception &e)
{
if (!_cancelled)
{
// Fatal error
DownloadThread::cancelDownload();
emit error(tr("Error extracting archive: %1").arg(e.what()));
}
}
archive_read_free(a);
}
void DownloadExtractThread::extractMultiFileRun()
{
QString folder;
QStringList filesExtracted, dirExtracted;
QByteArray devlower = _filename.toLower();
/* See if OS auto-mounted the device */
for (int tries = 0; tries < 3; tries++)
{
QThread::sleep(1);
auto l = Drivelist::ListStorageDevices();
for (auto i : l)
{
if (QByteArray::fromStdString(i.device).toLower() == devlower && i.mountpoints.size() == 1)
{
folder = QByteArray::fromStdString(i.mountpoints.front());
break;
}
}
}
#ifdef Q_OS_LINUX
bool manualmount = false;
if (folder.isEmpty())
{
/* Manually mount folder */
QTemporaryDir td;
QStringList args;
folder = td.path();
QByteArray fatpartition = _filename;
if (isdigit(fatpartition.at(fatpartition.length()-1)))
fatpartition += "p1";
else
fatpartition += "1";
args << fatpartition << folder;
if (QProcess::execute("mount", args) != 0)
{
emit error(tr("Error mounting FAT32 partition"));
return;
}
td.setAutoRemove(false);
manualmount = true;
}
#endif
if (folder.isEmpty())
{
emit error(tr("Operating system did not mount FAT32 partition"));
return;
}
QString currentDir;
struct archive *a = archive_read_new();
struct archive *ext = archive_write_disk_new();
struct archive_entry *entry;
/* Extra safety checks: do not allow existing files to be overwritten (SD card should be formatted by previous step),
* do not allow absolute paths, do not allow insecure symlinks, no special permissions */
int r, flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS
| ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS | ARCHIVE_EXTRACT_NO_OVERWRITE
/*ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | ARCHIVE_EXTRACT_XATTR*/;
#ifndef Q_OS_WIN
if (::getuid() == 0)
flags |= ARCHIVE_EXTRACT_OWNER;
#endif
currentDir = QDir::currentPath();
if (!QDir::setCurrent(folder))
{
DownloadThread::cancelDownload();
emit error(tr("Error changing to directory '%1'").arg(folder));
return;
}
archive_read_support_filter_all(a);
archive_read_support_format_all(a);
archive_write_disk_set_options(ext, flags);
archive_read_open(a, this, NULL, &DownloadExtractThread::_archive_read, &DownloadExtractThread::_archive_close);
try
{
while ( (r = archive_read_next_header(a, &entry)) != ARCHIVE_EOF)
{
_checkResult(r, a);
r = archive_write_header(ext, entry);
if (r < ARCHIVE_OK)
qDebug() << archive_error_string(ext);
else if (archive_entry_size(entry) > 0)
{
//checkResult(copyData(a, ext), a);
const void *buff;
size_t size;
int64_t offset;
QString filename = QString::fromWCharArray(archive_entry_pathname_w(entry));
if (archive_entry_filetype(entry) == AE_IFDIR) // Empty directory
dirExtracted.append(filename);
else
filesExtracted.append(filename);
while ( (r = archive_read_data_block(a, &buff, &size, &offset)) != ARCHIVE_EOF)
{
_checkResult(r, a);
_checkResult(archive_write_data_block(ext, buff, size, offset), ext);
_bytesWritten += size;
}
}
_checkResult(archive_write_finish_entry(ext), ext);
}
QByteArray computedHash = _inputHash.result().toHex();
qDebug() << "Hash of compressed multi-file zip:" << computedHash;
if (!_expectedHash.isEmpty() && _expectedHash != computedHash)
{
qDebug() << "Mismatch with expected hash:" << _expectedHash;
throw runtime_error("Download corrupt. SHA256 does not match");
}
if (_cacheEnabled && _expectedHash == computedHash)
{
_cachefile.close();
emit cacheFileUpdated(computedHash);
}
emit success();
}
catch (exception &e)
{
if (_cachefile.isOpen())
_cachefile.remove();
qDebug() << "Deleting extracted files";
for (auto filename : filesExtracted)
{
QFileInfo fi(filename);
QString path = fi.path();
if (!path.isEmpty() && path != "." && !dirExtracted.contains(path))
dirExtracted.append(path);
QFile::remove(filename);
}
for (int idx = dirExtracted.count()-1; idx >= 0; idx--)
{
QDir d;
d.rmdir(dirExtracted[idx]);
}
qDebug() << filesExtracted << dirExtracted;
if (!_cancelled)
{
/* Fatal error */
DownloadThread::cancelDownload();
emit error(tr("Error extracting archive: %1").arg(e.what()));
}
}
archive_read_free(a);
archive_write_free(ext);
QDir::setCurrent(currentDir);
#ifdef Q_OS_LINUX
if (manualmount)
{
QStringList args;
args << folder;
QProcess::execute("umount", args);
QDir d;
d.rmdir(folder);
}
#endif
eject_disk(_filename.constData());
}
ssize_t DownloadExtractThread::_on_read(struct archive *, const void **buff)
{
_buf = _popQueue();
*buff = _buf.data();
return _buf.size();
}
int DownloadExtractThread::_on_close(struct archive *)
{
return 0;
}
// static callback functions that call object oriented equivalents
ssize_t DownloadExtractThread::_archive_read(struct archive *a, void *client_data, const void **buff)
{
return qobject_cast<DownloadExtractThread *>((QObject *) client_data)->_on_read(a, buff);
}
int DownloadExtractThread::_archive_close(struct archive *a, void *client_data)
{
return qobject_cast<DownloadExtractThread *>((QObject *) client_data)->_on_close(a);
}
bool DownloadExtractThread::isImage()
{
return _isImage;
}
void DownloadExtractThread::enableMultipleFileExtraction()
{
_isImage = false;
}
// Synchronized queue using monitor consumer/producer pattern
QByteArray DownloadExtractThread::_popQueue()
{
std::unique_lock<std::mutex> lock(_queueMutex);
_cv.wait(lock, [this]{
return _queue.size() != 0;
});
QByteArray result = _queue.front();
_queue.pop_front();
if (_queue.size() == (MAX_QUEUE_SIZE-1))
{
lock.unlock();
_cv.notify_one();
}
return result;
}
void DownloadExtractThread::_pushQueue(const char *data, size_t len)
{
std::unique_lock<std::mutex> lock(_queueMutex);
_cv.wait(lock, [this]{
return _queue.size() != MAX_QUEUE_SIZE;
});
_queue.emplace_back(data, len);
if (_queue.size() == 1)
{
lock.unlock();
_cv.notify_one();
}
}