/* * SPDX-License-Identifier: Apache-2.0 * Copyright (C) 2020 Raspberry Pi Ltd */ #include "downloadextractthread.h" #include "imagewriter.h" #include "drivelistitem.h" #include "dependencies/drivelist/src/drivelist.hpp" #include "dependencies/sha256crypt/sha256crypt.h" #include "driveformatthread.h" #include "localfileextractthread.h" #include "downloadstatstelemetry.h" #include "wlancredentials.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef QT_NO_WIDGETS #include #include #endif #ifdef Q_OS_DARWIN #include #endif #ifdef Q_OS_WIN #include #include #include #include #endif #ifdef QT_NO_WIDGETS #include #endif namespace { constexpr uint MAX_SUBITEMS_DEPTH = 16; } // namespace anonymous ImageWriter::ImageWriter(QObject *parent) : QObject(parent), _repo(QUrl(QString(OSLIST_URL))), _dlnow(0), _verifynow(0), _engine(nullptr), _thread(nullptr), _verifyEnabled(false), _cachingEnabled(false), _embeddedMode(false), _online(false), _customCacheFile(false), _trans(nullptr), _networkManager(this) { connect(&_polltimer, SIGNAL(timeout()), SLOT(pollProgress())); QString platform; if (qobject_cast(QCoreApplication::instance()) ) { platform = QGuiApplication::platformName(); } else { platform = "cli"; } if (platform == "eglfs" || platform == "linuxfb") { _embeddedMode = true; connect(&_networkchecktimer, SIGNAL(timeout()), SLOT(pollNetwork())); _networkchecktimer.start(100); changeKeyboard(detectPiKeyboard()); if (_currentKeyboard.isEmpty()) _currentKeyboard = "us"; QFile f("/sys/bus/nvmem/devices/rmem0/nvmem"); if (f.exists() && f.open(f.ReadOnly)) { const QByteArrayList eepromSettings = f.readAll().split('\n'); f.close(); for (const QByteArray &setting : eepromSettings) { if (setting.startsWith("IMAGER_REPO_URL=")) { _repo = setting.mid(16).trimmed(); qDebug() << "Repository from EEPROM:" << _repo; } } } } #ifdef Q_OS_WIN _taskbarButton = nullptr; #endif if (!_settings.isWritable() && !_settings.fileName().isEmpty()) { /* Settings file is not writable, probably run by root previously */ QString settingsFile = _settings.fileName(); qDebug() << "Settings file" << settingsFile << "not writable. Recreating it"; QFile f(_settings.fileName()); QByteArray oldsettings; if (f.open(f.ReadOnly)) { oldsettings = f.readAll(); f.close(); } f.remove(); if (f.open(f.WriteOnly)) { f.write(oldsettings); f.close(); _settings.sync(); } else { qDebug() << "Error deleting and recreating settings file. Please remove manually."; } } _settings.beginGroup("caching"); _cachingEnabled = !_embeddedMode && _settings.value("enabled", IMAGEWRITER_ENABLE_CACHE_DEFAULT).toBool(); _cachedFileHash = _settings.value("lastDownloadSHA256").toByteArray(); _cacheFileName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)+QDir::separator()+"lastdownload.cache"; if (!_cachedFileHash.isEmpty()) { QFileInfo f(_cacheFileName); if (!f.exists() || !f.isReadable() || !f.size()) { _cachedFileHash.clear(); _settings.remove("lastDownloadSHA256"); _settings.sync(); } } _settings.endGroup(); QDir dir(":/i18n", "rpi-imager_*.qm"); const QStringList transFiles = dir.entryList(); QLocale currentLocale; QStringList localeComponents = currentLocale.name().split('_'); QString currentlangcode; if (!localeComponents.isEmpty()) currentlangcode = localeComponents.first(); for (const QString &tf : transFiles) { QString langcode = tf.mid(11, tf.length()-14); /* FIXME: we currently lack a font with support for Chinese characters in embedded mode */ //if (isEmbeddedMode() && langcode == "zh") // continue; QLocale loc(langcode); /* Use "English" for "en" and not "American English" */ QString langname = (langcode == "en" ? "English" : loc.nativeLanguageName() ); _translations.insert(langname, langcode); if (langcode == currentlangcode) { _currentLang = langname; _currentLangcode = currentlangcode; } } //_currentKeyboard = "us"; // Centralised network manager, for fetching OS lists connect(&_networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(handleNetworkRequestFinished(QNetworkReply *))); } ImageWriter::~ImageWriter() { if (_trans) { QCoreApplication::removeTranslator(_trans); delete _trans; } } void ImageWriter::setEngine(QQmlApplicationEngine *engine) { _engine = engine; } /* Set URL to download from */ void ImageWriter::setSrc(const QUrl &url, quint64 downloadLen, quint64 extrLen, QByteArray expectedHash, bool multifilesinzip, QString parentcategory, QString osname, QByteArray initFormat) { _src = url; _downloadLen = downloadLen; _extrLen = extrLen; _expectedHash = expectedHash; _multipleFilesInZip = multifilesinzip; _parentCategory = parentcategory; _osName = osname; _initFormat = (initFormat == "none") ? "" : initFormat; if (!_downloadLen && url.isLocalFile()) { QFileInfo fi(url.toLocalFile()); _downloadLen = fi.size(); } if (url.isLocalFile()) { _initFormat = "auto"; } } /* Set device to write to */ void ImageWriter::setDst(const QString &device, quint64 deviceSize) { _dst = device; _devLen = deviceSize; } /* Returns true if src and dst are set */ bool ImageWriter::readyToWrite() { return !_src.isEmpty() && !_dst.isEmpty(); } /* Start writing */ void ImageWriter::startWrite() { if (!readyToWrite()) return; if (_src.toString() == "internal://format") { DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), this); connect(dft, SIGNAL(success()), SLOT(onSuccess())); connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); dft->start(); return; } QByteArray urlstr = _src.toString(_src.FullyEncoded).toLatin1(); QString lowercaseurl = urlstr.toLower(); bool compressed = lowercaseurl.endsWith(".zip") || lowercaseurl.endsWith(".xz") || lowercaseurl.endsWith(".bz2") || lowercaseurl.endsWith(".gz") || lowercaseurl.endsWith(".7z") || lowercaseurl.endsWith(".zst") || lowercaseurl.endsWith(".cache"); if (!_extrLen && _src.isLocalFile()) { if (!compressed) _extrLen = _downloadLen; else if (lowercaseurl.endsWith(".zip")) _parseCompressedFile(); else if (lowercaseurl.endsWith(".xz")) _parseXZFile(); } if (_devLen && _extrLen > _devLen) { emit error(tr("Storage capacity is not large enough.
Needs to be at least %1 GB.").arg(QString::number(_extrLen/1000000000.0, 'f', 1))); return; } if (_extrLen && !_multipleFilesInZip && _extrLen % 512 != 0) { emit error(tr("Input file is not a valid disk image.
File size %1 bytes is not a multiple of 512 bytes.").arg(_extrLen)); return; } if (!_expectedHash.isEmpty() && _cachedFileHash == _expectedHash) { // Use cached file urlstr = QUrl::fromLocalFile(_cacheFileName).toString(_src.FullyEncoded).toLatin1(); } if (QUrl(urlstr).isLocalFile()) { _thread = new LocalFileExtractThread(urlstr, _dst.toLatin1(), _expectedHash, this); } else { _thread = new DownloadExtractThread(urlstr, _dst.toLatin1(), _expectedHash, this); if (_repo.toString() == OSLIST_URL) { DownloadStatsTelemetry *tele = new DownloadStatsTelemetry(urlstr, _parentCategory.toLatin1(), _osName.toLatin1(), _embeddedMode, _currentLangcode, this); connect(tele, SIGNAL(finished()), tele, SLOT(deleteLater())); tele->start(); } } connect(_thread, SIGNAL(success()), SLOT(onSuccess())); connect(_thread, SIGNAL(error(QString)), SLOT(onError(QString))); connect(_thread, SIGNAL(finalizing()), SLOT(onFinalizing())); connect(_thread, SIGNAL(preparationStatusUpdate(QString)), SLOT(onPreparationStatusUpdate(QString))); _thread->setVerifyEnabled(_verifyEnabled); _thread->setUserAgent(QString("Mozilla/5.0 rpi-imager/%1").arg(constantVersion()).toUtf8()); _thread->setImageCustomization(_config, _cmdline, _firstrun, _cloudinit, _cloudinitNetwork, _initFormat); if (!_expectedHash.isEmpty() && _cachedFileHash != _expectedHash && _cachingEnabled) { if (!_cachedFileHash.isEmpty()) { if (_settings.isWritable() && QFile::remove(_cacheFileName)) { _settings.remove("caching/lastDownloadSHA256"); _settings.sync(); _cachedFileHash.clear(); } else { qDebug() << "Error removing old cache file. Disabling caching"; _cachingEnabled = false; } } if (_cachingEnabled) { QStorageInfo si(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); qint64 avail = si.bytesAvailable(); qDebug() << "Available disk space for caching:" << avail/1024/1024/1024 << "GB"; if (avail-_downloadLen < IMAGEWRITER_MINIMAL_SPACE_FOR_CACHING) { qDebug() << "Low disk space. Not caching files to disk."; } else { _thread->setCacheFile(_cacheFileName, _downloadLen); connect(_thread, SIGNAL(cacheFileUpdated(QByteArray)), SLOT(onCacheFileUpdated(QByteArray))); } } } if (_multipleFilesInZip) { static_cast(_thread)->enableMultipleFileExtraction(); DriveFormatThread *dft = new DriveFormatThread(_dst.toLatin1(), this); connect(dft, SIGNAL(success()), _thread, SLOT(start())); connect(dft, SIGNAL(error(QString)), SLOT(onError(QString))); dft->start(); } else { _thread->start(); } startProgressPolling(); } void ImageWriter::onCacheFileUpdated(QByteArray sha256) { if (!_customCacheFile) { _settings.setValue("caching/lastDownloadSHA256", sha256); _settings.sync(); } _cachedFileHash = sha256; qDebug() << "Done writing cache file"; } /* Cancel write */ void ImageWriter::cancelWrite() { if (_thread) { connect(_thread, SIGNAL(finished()), SLOT(onCancelled())); _thread->cancelDownload(); } if (!_thread || !_thread->isRunning()) { emit cancelled(); } } void ImageWriter::onCancelled() { sender()->deleteLater(); if (sender() == _thread) { _thread = nullptr; } emit cancelled(); } /* Return true if url is in our local disk cache */ bool ImageWriter::isCached(const QUrl &, const QByteArray &sha256) { return !sha256.isEmpty() && _cachedFileHash == sha256; } /* Utility function to return filename part from URL */ QString ImageWriter::fileNameFromUrl(const QUrl &url) { //return QFileInfo(url.toLocalFile()).fileName(); return url.fileName(); } QString ImageWriter::srcFileName() { return _src.isEmpty() ? "" : _src.fileName(); } /* Function to return OS list URL */ QUrl ImageWriter::constantOsListUrl() const { return _repo; } /* Function to return version */ QString ImageWriter::constantVersion() const { return IMAGER_VERSION_STR; } /* Returns true if version argument is newer than current program */ bool ImageWriter::isVersionNewer(const QString &version) { return QVersionNumber::fromString(version) > QVersionNumber::fromString(IMAGER_VERSION_STR); } void ImageWriter::setCustomOsListUrl(const QUrl &url) { _repo = url; } namespace { QJsonArray findAndInsertJsonResult(QJsonArray parent_list, QJsonArray incomingBody, QUrl referenceUrl, uint8_t count = 0) { if (count > MAX_SUBITEMS_DEPTH) { qDebug() << "Aborting insertion of subitems, exceeded maximum configured limit of " << MAX_SUBITEMS_DEPTH << " levels."; return {}; } QJsonArray returnArray = {}; for (auto ositem : parent_list) { auto ositemObject = ositem.toObject(); if (ositemObject.contains("subitems")) { // Recurse! ositemObject["subitems"] = findAndInsertJsonResult(ositemObject["subitems"].toArray(), incomingBody, referenceUrl, count++); } else if (ositemObject.contains("subitems_url")) { if ( !ositemObject["subitems_url"].toString().compare(referenceUrl.toString())) { ositemObject.insert("subitems", incomingBody); ositemObject.remove("subitems_url"); } } returnArray += ositemObject; } return returnArray; } void findAndQueueUnresolvedSubitemsJson(QJsonArray incoming, QNetworkAccessManager &manager, uint8_t count = 0) { if (count > MAX_SUBITEMS_DEPTH) { qDebug() << "Aborting fetch of subitems JSON, exceeded maximum configured limit of " << MAX_SUBITEMS_DEPTH << " levels."; return; } for (auto entry : incoming) { auto entryObject = entry.toObject(); if (entryObject.contains("subitems")) { // No need to handle a return - this isn't processing a list, it's searching and queuing downloads. findAndQueueUnresolvedSubitemsJson(entryObject["subitems"].toArray(), manager, count++); } else if (entryObject.contains("subitems_url")) { auto url = entryObject["subitems_url"].toString(); auto request = QNetworkRequest(url); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); manager.get(request); } } } } // namespace anonymous void ImageWriter::setHWFilterList(const QByteArray &json, const bool &inclusive) { QJsonDocument json_document = QJsonDocument::fromJson(json); _deviceFilter = json_document.array(); _deviceFilterIsInclusive = inclusive; } void ImageWriter::handleNetworkRequestFinished(QNetworkReply *data) { // Defer deletion data->deleteLater(); if (data->error() == QNetworkReply::NoError) { auto httpStatusCode = data->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (httpStatusCode >= 200 && httpStatusCode < 300) { auto response_object = QJsonDocument::fromJson(data->readAll()).object(); if (response_object.contains("os_list")) { // Step 1: Insert the items into the canonical JSON document. // It doesn't matter that these may still contain subitems_url items // As these will be fixed up as the subitems_url instances are blinked in if (_completeOsList.isEmpty()) { _completeOsList = QJsonDocument(response_object); } else { auto new_list = findAndInsertJsonResult(_completeOsList["os_list"].toArray(), response_object["os_list"].toArray(), data->request().url()); auto imager_meta = _completeOsList["imager"].toObject(); _completeOsList = QJsonDocument(QJsonObject({ {"imager", imager_meta}, {"os_list", new_list} })); } findAndQueueUnresolvedSubitemsJson(response_object["os_list"].toArray(), _networkManager); emit osListPrepared(); } else { qDebug() << "Incorrectly formatted OS list at: " << data->url(); } } else if (httpStatusCode >= 300 && httpStatusCode < 400) { // We should _never_ enter this branch. All requests are set to follow redirections // at their call sites - so the only way you got here was a logic defect. auto request = QNetworkRequest(data->url()); request.setAttribute(QNetworkRequest::RedirectionTargetAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); data->manager()->get(request); // maintain manager return; } else if (httpStatusCode >= 400 && httpStatusCode < 600) { // HTTP Error qDebug() << "Failed to fetch URL [" << data->url() << "], got: " << httpStatusCode; } else { // Completely unknown error, worth logging separately qDebug() << "Failed to fetch URL [" << data->url() << "], got unknown response code: " << httpStatusCode; } } else { // QT Error. qDebug() << "Unrecognised QT error: " << data->error() << ", explainer: " << data->errorString(); } } namespace { QJsonArray filterOsListWithHWTags(QJsonArray incoming_os_list, QJsonArray hw_filter, const bool inclusive, uint8_t count = 0) { if (count > MAX_SUBITEMS_DEPTH) { qDebug() << "Aborting insertion of subitems, exceeded maximum configured limit of " << MAX_SUBITEMS_DEPTH << " levels."; return {}; } QJsonArray returnArray = {}; for (auto ositem : incoming_os_list) { auto ositemObject = ositem.toObject(); if (ositemObject.contains("subitems")) { // Recurse! ositemObject["subitems"] = filterOsListWithHWTags(ositemObject["subitems"].toArray(), hw_filter, inclusive, count++); if (ositemObject["subitems"].toArray().count() > 0) { returnArray += ositemObject; } } else { // Filter this one! if (ositemObject.contains("devices")) { auto keep = false; auto ositem_devices = ositemObject["devices"].toArray(); for (auto compat_device : ositem_devices) { if (hw_filter.contains(compat_device.toString())) { keep = true; break; } } if (keep) { returnArray.append(ositem); } } else { // No devices tags, so work out if we're exclusive or inclusive filtering! if (inclusive) { returnArray.append(ositem); } } } } return returnArray; } } // namespace anonymous QByteArray ImageWriter::getFilteredOSlist() { QJsonArray reference_os_list_array = {}; QJsonObject reference_imager_metadata = {}; { if (!_completeOsList.isEmpty()) { reference_os_list_array = filterOsListWithHWTags(_completeOsList.object()["os_list"].toArray(), _deviceFilter, _deviceFilterIsInclusive); reference_imager_metadata = _completeOsList.object()["imager"].toObject(); } } reference_os_list_array.append(QJsonObject({ {"name", tr("Erase")}, {"description", tr("Format card as FAT32")}, {"icon", "icons/erase.png"}, {"url", "internal://format"}, })); reference_os_list_array.append(QJsonObject({ {"name", tr("Use custom")}, {"description", tr("Select a custom .img from your computer")}, {"icon", "icons/use_custom.png"}, {"url", ""}, })); return QJsonDocument( QJsonObject({ {"imager", reference_imager_metadata}, {"os_list", reference_os_list_array}, } )).toJson(); } void ImageWriter::beginOSListFetch() { QNetworkRequest request = QNetworkRequest(constantOsListUrl()); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); // This will set up a chain of requests that culiminate in the eventual fetch and assembly of // a complete cached OS list. _networkManager.get(request); } void ImageWriter::setCustomCacheFile(const QString &cacheFile, const QByteArray &sha256) { _cacheFileName = cacheFile; _cachedFileHash = QFile::exists(cacheFile) ? sha256 : ""; _customCacheFile = true; _cachingEnabled = true; } /* Start polling the list of available drives */ void ImageWriter::startDriveListPolling() { _drivelist.startPolling(); } /* Stop polling the list of available drives */ void ImageWriter::stopDriveListPolling() { _drivelist.stopPolling(); } DriveListModel *ImageWriter::getDriveList() { return &_drivelist; } void ImageWriter::startProgressPolling() { _powersave.applyBlock(tr("Downloading and writing image")); #ifdef Q_OS_WIN if (!_taskbarButton && _engine) { QWindow* window = qobject_cast( _engine->rootObjects().at(0) ); if (window) { _taskbarButton = new QWinTaskbarButton(this); _taskbarButton->setWindow(window); _taskbarButton->progress()->setMaximum(0); _taskbarButton->progress()->setVisible(true); } } #endif _dlnow = 0; _verifynow = 0; _polltimer.start(PROGRESS_UPDATE_INTERVAL); } void ImageWriter::stopProgressPolling() { _polltimer.stop(); pollProgress(); #ifdef Q_OS_WIN if (_taskbarButton) { _taskbarButton->progress()->setVisible(false); _taskbarButton->deleteLater(); _taskbarButton = nullptr; } #endif _powersave.removeBlock(); } void ImageWriter::pollProgress() { if (!_thread) return; quint64 newDlNow, dlTotal; if (_extrLen) { newDlNow = _thread->bytesWritten(); dlTotal = _extrLen; } else { newDlNow = _thread->dlNow(); dlTotal = _thread->dlTotal(); } if (newDlNow != _dlnow) { _dlnow = newDlNow; #ifdef Q_OS_WIN if (_taskbarButton) { _taskbarButton->progress()->setMaximum(dlTotal/1048576); _taskbarButton->progress()->setValue(newDlNow/1048576); } #endif emit downloadProgress(newDlNow, dlTotal); } quint64 newVerifyNow = _thread->verifyNow(); if (newVerifyNow != _verifynow) { _verifynow = newVerifyNow; quint64 verifyTotal = _thread->verifyTotal(); #ifdef Q_OS_WIN if (_taskbarButton) { _taskbarButton->progress()->setMaximum(verifyTotal/1048576); _taskbarButton->progress()->setValue(newVerifyNow/1048576); } #endif emit verifyProgress(newVerifyNow, verifyTotal); } } void ImageWriter::setVerifyEnabled(bool verify) { _verifyEnabled = verify; if (_thread) _thread->setVerifyEnabled(verify); } /* Relay events from download thread to QML */ void ImageWriter::onSuccess() { stopProgressPolling(); emit success(); #ifndef QT_NO_WIDGETS if (_settings.value("beep").toBool() && qobject_cast(QCoreApplication::instance()) ) { QApplication::beep(); } #endif } void ImageWriter::onError(QString msg) { stopProgressPolling(); emit error(msg); #ifndef QT_NO_WIDGETS if (_settings.value("beep").toBool() && qobject_cast(QCoreApplication::instance()) ) QApplication::beep(); #endif } void ImageWriter::onFinalizing() { _polltimer.stop(); emit finalizing(); } void ImageWriter::onPreparationStatusUpdate(QString msg) { emit preparationStatusUpdate(msg); } void ImageWriter::openFileDialog() { #ifndef QT_NO_WIDGETS QSettings settings; QString path = settings.value("lastpath").toString(); QFileInfo fi(path); if (path.isEmpty() || !fi.exists() || !fi.isReadable() ) path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); QFileDialog *fd = new QFileDialog(nullptr, tr("Select image"), path, "Image files (*.img *.zip *.iso *.gz *.xz *.zst);;All files (*)"); connect(fd, SIGNAL(fileSelected(QString)), SLOT(onFileSelected(QString))); if (_engine) { fd->createWinId(); QWindow *handle = fd->windowHandle(); QWindow *qmlwindow = qobject_cast(_engine->rootObjects().value(0)); if (qmlwindow) { handle->setTransientParent(qmlwindow); } } fd->show(); #endif } void ImageWriter::onFileSelected(QString filename) { #ifndef QT_NO_WIDGETS QFileInfo fi(filename); QSettings settings; if (fi.isFile()) { QString path = fi.path(); if (path != settings.value("lastpath")) { settings.setValue("lastpath", path); settings.sync(); } emit fileSelected(QUrl::fromLocalFile(filename)); } else { qDebug() << "Item selected is not a regular file"; } sender()->deleteLater(); #endif } void ImageWriter::_parseCompressedFile() { struct archive *a = archive_read_new(); struct archive_entry *entry; QByteArray fn = _src.toLocalFile().toLatin1(); int numFiles = 0; _extrLen = 0; archive_read_support_filter_all(a); archive_read_support_format_all(a); if (archive_read_open_filename(a, fn.data(), 10240) == ARCHIVE_OK) { while ( (archive_read_next_header(a, &entry)) == ARCHIVE_OK) { if (archive_entry_size(entry) > 0) { _extrLen += archive_entry_size(entry); numFiles++; } } } if (numFiles > 1) _multipleFilesInZip = true; qDebug() << "Parsed .zip file containing" << numFiles << "files, uncompressed size:" << _extrLen; } void ImageWriter::_parseXZFile() { QFile f(_src.toLocalFile()); lzma_stream_flags opts = { 0 }; _extrLen = 0; if (f.size() > LZMA_STREAM_HEADER_SIZE && f.open(f.ReadOnly)) { f.seek(f.size()-LZMA_STREAM_HEADER_SIZE); QByteArray footer = f.read(LZMA_STREAM_HEADER_SIZE); lzma_ret ret = lzma_stream_footer_decode(&opts, (const uint8_t *) footer.constData()); if (ret == LZMA_OK && opts.backward_size < 1000000 && opts.backward_size < f.size()-LZMA_STREAM_HEADER_SIZE) { f.seek(f.size()-LZMA_STREAM_HEADER_SIZE-opts.backward_size); QByteArray buf = f.read(opts.backward_size+LZMA_STREAM_HEADER_SIZE); lzma_index *idx; uint64_t memlimit = UINT64_MAX; size_t pos = 0; ret = lzma_index_buffer_decode(&idx, &memlimit, NULL, (const uint8_t *) buf.constData(), &pos, buf.size()); if (ret == LZMA_OK) { _extrLen = lzma_index_uncompressed_size(idx); qDebug() << "Parsed .xz file. Uncompressed size:" << _extrLen; } else { qDebug() << "Unable to parse index of .xz file"; } lzma_index_end(idx, NULL); } else { qDebug() << "Unable to parse footer of .xz file"; } f.close(); } } bool ImageWriter::isOnline() { return _online || !_embeddedMode; } void ImageWriter::pollNetwork() { #ifdef Q_OS_LINUX /* Check if we have an IP-address other than localhost */ QList addresses = QNetworkInterface::allAddresses(); foreach (QHostAddress a, addresses) { if (!a.isLoopback() && a.scopeId().isEmpty()) { /* Not a loopback or IPv6 link-local address, so online */ qDebug() << "IP:" << a; _online = true; break; } } if (_online) { _networkchecktimer.stop(); // Wait another 0.1 sec, as dhcpcd may not have set up nameservers yet QTimer::singleShot(100, this, SLOT(syncTime())); } #endif } void ImageWriter::syncTime() { #ifdef Q_OS_LINUX qDebug() << "Network online. Synchronizing time."; QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, SIGNAL(finished(QNetworkReply*)), SLOT(onTimeSyncReply(QNetworkReply*))); manager->head(QNetworkRequest(QUrl(TIME_URL))); #endif } void ImageWriter::onTimeSyncReply(QNetworkReply *reply) { #ifdef Q_OS_LINUX if (reply->hasRawHeader("Date")) { QDateTime dt = QDateTime::fromString(reply->rawHeader("Date"), Qt::RFC2822Date); qDebug() << "Received current time from server:" << dt; struct timeval tv = { (time_t) dt.toSecsSinceEpoch(), 0 }; ::settimeofday(&tv, NULL); emit networkOnline(); } else { qDebug() << "Error synchronizing time. Trying again in 3 seconds"; QTimer::singleShot(3000, this, SLOT(syncTime())); } reply->deleteLater(); #else Q_UNUSED(reply) #endif } bool ImageWriter::isEmbeddedMode() { return _embeddedMode; } /* Mount any USB sticks that can contain source images under /media */ bool ImageWriter::mountUsbSourceMedia() { int devices = 0; #ifdef Q_OS_LINUX QDir dir("/sys/class/block"); const QStringList list = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); if (!dir.exists("/media")) dir.mkdir("/media"); for (const QString &devname : list) { if (!devname.startsWith("mmcblk0") && !QFile::symLinkTarget("/sys/class/block/"+devname).contains("/devices/virtual/")) { QString mntdir = "/media/"+devname; if (dir.exists(mntdir)) { devices++; continue; } dir.mkdir(mntdir); QStringList args = { "-o", "ro", QString("/dev/")+devname, mntdir }; if ( QProcess::execute("mount", args) == 0 ) devices++; else dir.rmdir(mntdir); } } #endif return devices > 0; } QByteArray ImageWriter::getUsbSourceOSlist() { #ifdef Q_OS_LINUX QJsonArray oslist; QDir dir("/media"); const QStringList medialist = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QStringList namefilters = {"*.img", "*.zip", "*.gz", "*.xz", "*.zst"}; for (const QString &devname : medialist) { QDir subdir("/media/"+devname); const QStringList files = subdir.entryList(namefilters, QDir::Files, QDir::Name); for (const QString &file : files) { QString path = "/media/"+devname+"/"+file; QFileInfo fi(path); QJsonObject f = { {"name", file}, {"description", devname+"/"+file}, {"url", QUrl::fromLocalFile(path).toString() }, {"release_date", ""}, {"image_download_size", fi.size()} }; oslist.append(f); } } return QJsonDocument(oslist).toJson(); #else return QByteArray(); #endif } QString ImageWriter::_sshKeyDir() { return QDir::homePath()+"/.ssh"; } QString ImageWriter::_pubKeyFileName() { return _sshKeyDir()+"/id_rsa.pub"; } QString ImageWriter::_privKeyFileName() { return _sshKeyDir()+"/id_rsa"; } QString ImageWriter::getDefaultPubKey() { QByteArray pubkey; QFile pubfile(_pubKeyFileName()); if (pubfile.exists() && pubfile.open(QFile::ReadOnly)) { pubkey = pubfile.readAll().trimmed(); pubfile.close(); } return pubkey; } bool ImageWriter::hasPubKey() { return QFile::exists(_pubKeyFileName()); } QString ImageWriter::_sshKeyGen() { #ifdef Q_OS_WIN QString windir = QProcessEnvironment::systemEnvironment().value("windir"); return QDir::fromNativeSeparators(windir+"\\SysNative\\OpenSSH\\ssh-keygen.exe"); #else return "ssh-keygen"; #endif } bool ImageWriter::hasSshKeyGen() { #ifdef Q_OS_WIN return QFile::exists(_sshKeyGen()); #else return true; #endif } void ImageWriter::generatePubKey() { if (!hasPubKey() && !QFile::exists(_privKeyFileName())) { QDir dir; QProcess proc; QString progName = _sshKeyGen(); QStringList args; args << "-t" << "rsa" << "-f" << _privKeyFileName() << "-N" << ""; if (!dir.exists(_sshKeyDir())) { qDebug() << "Creating" << _sshKeyDir(); dir.mkdir(_sshKeyDir()); } qDebug() << "Executing:" << progName << args; proc.start(progName, args); proc.waitForFinished(); qDebug() << proc.readAll(); } } QString ImageWriter::getTimezone() { return QTimeZone::systemTimeZoneId(); } QStringList ImageWriter::getTimezoneList() { QStringList timezones; QFile f(":/timezones.txt"); if ( f.open(f.ReadOnly) ) { timezones = QString(f.readAll()).split('\n'); f.close(); } return timezones; } QStringList ImageWriter::getCountryList() { QStringList countries; QFile f(":/countries.txt"); if ( f.open(f.ReadOnly) ) { countries = QString(f.readAll()).trimmed().split('\n'); f.close(); } return countries; } QStringList ImageWriter::getKeymapLayoutList() { QStringList keymaps; QFile f(":/keymap-layouts.txt"); if ( f.open(f.ReadOnly) ) { keymaps = QString(f.readAll()).trimmed().split('\n'); f.close(); } return keymaps; } QString ImageWriter::getSSID() { return WlanCredentials::instance()->getSSID(); } QString ImageWriter::getPSK() { #ifdef Q_OS_DARWIN /* On OSX the user is presented with a prompt for the admin password when opening the system key chain. * Ask if user wants to obtain the wlan password first to make sure this is desired and * to provide the user with context. */ if (QMessageBox::question(nullptr, "", tr("Would you like to prefill the wifi password from the system keychain?")) != QMessageBox::Yes) { return QString(); } #endif return WlanCredentials::instance()->getPSK(); } bool ImageWriter::getBoolSetting(const QString &key) { /* Some keys have defaults */ if (key == "telemetry") return _settings.value(key, TELEMETRY_ENABLED_DEFAULT).toBool(); else if (key == "eject") return _settings.value(key, true).toBool(); else if (key == "check_version") return _settings.value(key, CHECK_VERSION_DEFAULT).toBool(); else return _settings.value(key).toBool(); } void ImageWriter::setSetting(const QString &key, const QVariant &value) { _settings.setValue(key, value); _settings.sync(); } void ImageWriter::setImageCustomization(const QByteArray &config, const QByteArray &cmdline, const QByteArray &firstrun, const QByteArray &cloudinit, const QByteArray &cloudinitNetwork) { _config = config; _cmdline = cmdline; _firstrun = firstrun; _cloudinit = cloudinit; _cloudinitNetwork = cloudinitNetwork; qDebug() << "Custom config.txt entries:" << config; qDebug() << "Custom cmdline.txt entries:" << cmdline; qDebug() << "Custom firstuse.sh:" << firstrun; qDebug() << "Cloudinit:" << cloudinit; } QString ImageWriter::crypt(const QByteArray &password) { QByteArray salt = "$5$"; QByteArray saltchars = "./0123456789ABCDEFGHIJKLMNOPQRST" "UVWXYZabcdefghijklmnopqrstuvwxyz"; std::mt19937 gen(static_cast(QDateTime::currentMSecsSinceEpoch())); std::uniform_int_distribution<> uid(0, saltchars.length()-1); for (int i=0; i<10; i++) salt += saltchars[uid(gen)]; return sha256_crypt(password.constData(), salt.constData()); } QString ImageWriter::pbkdf2(const QByteArray &psk, const QByteArray &ssid) { return QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha1, psk, ssid, 4096, 32).toHex(); } void ImageWriter::setSavedCustomizationSettings(const QVariantMap &map) { _settings.beginGroup("imagecustomization"); _settings.remove(""); const QStringList keys = map.keys(); for (const QString &key : keys) { _settings.setValue(key, map.value(key)); } _settings.endGroup(); _settings.sync(); } QVariantMap ImageWriter::getSavedCustomizationSettings() { QVariantMap result; _settings.beginGroup("imagecustomization"); const QStringList keys = _settings.childKeys(); for (const QString &key : keys) { result.insert(key, _settings.value(key)); } _settings.endGroup(); return result; } void ImageWriter::clearSavedCustomizationSettings() { _settings.beginGroup("imagecustomization"); _settings.remove(""); _settings.endGroup(); _settings.sync(); } bool ImageWriter::hasSavedCustomizationSettings() { _settings.sync(); _settings.beginGroup("imagecustomization"); bool result = !_settings.childKeys().isEmpty(); _settings.endGroup(); return result; } bool ImageWriter::imageSupportsCustomization() { return !_initFormat.isEmpty(); } QStringList ImageWriter::getTranslations() { QStringList t = _translations.keys(); t.sort(Qt::CaseInsensitive); return t; } QString ImageWriter::getCurrentLanguage() { return _currentLang; } QString ImageWriter::getCurrentKeyboard() { return _currentKeyboard; } void ImageWriter::changeLanguage(const QString &newLanguageName) { if (newLanguageName.isEmpty() || newLanguageName == _currentLang || !_translations.contains(newLanguageName)) return; QString langcode = _translations[newLanguageName]; qDebug() << "Changing language to" << langcode; QTranslator *trans = new QTranslator(); if (trans->load(":/i18n/rpi-imager_"+langcode+".qm")) { replaceTranslator(trans); _currentLang = newLanguageName; _currentLangcode = langcode; } else { qDebug() << "Failed to load translation file"; delete trans; } } void ImageWriter::changeKeyboard(const QString &newKeymapLayout) { if (newKeymapLayout.isEmpty() || newKeymapLayout == _currentKeyboard) return; #ifdef QT_NO_WIDGETS QString kmapfile = "/usr/share/qmaps/"+newKeymapLayout+".qmap"; if (QFile::exists(kmapfile)) QEglFSFunctions::loadKeymap(kmapfile); #endif _currentKeyboard = newKeymapLayout; } void ImageWriter::replaceTranslator(QTranslator *trans) { if (_trans) { QCoreApplication::removeTranslator(_trans); delete _trans; } _trans = trans; QCoreApplication::installTranslator(_trans); if (_engine) { _engine->retranslate(); } } QString ImageWriter::detectPiKeyboard() { unsigned int typenr = 0; QFile f("/proc/device-tree/chosen/rpi-country-code"); if (f.exists() && f.open(f.ReadOnly)) { QByteArray d = f.readAll(); f.close(); if (d.length() == 4) { typenr = d.at(2); } } if (!typenr) { QDir dir("/dev/input/by-id"); QRegularExpression rx("RPI_Wired_Keyboard_([0-9]+)"); const QStringList entries = dir.entryList(QDir::Files); for (const QString &fn : entries) { QRegularExpressionMatch match = rx.match(fn); if (match.hasMatch()) { typenr = match.captured(1).toUInt(); } } } if (typenr) { QStringList kbcountries = { "", "gb", "fr", "es", "us", "de", "it", "jp", "pt", "no", "se", "dk", "ru", "tr", "il" }; if (typenr < kbcountries.count()) { return kbcountries.at(typenr); } } return QString(); } QString ImageWriter::getCurrentUser() { QString user = qgetenv("USER"); if (user.isEmpty()) user = qgetenv("USERNAME"); user = user.toLower(); if (user.contains(" ")) { auto names = user.split(" "); user = names.first(); } if (user.isEmpty() || user == "root") user = "pi"; return user; } bool ImageWriter::hasMouse() { return !_embeddedMode || QFile::exists("/dev/input/mouse0"); } bool ImageWriter::customRepo() { return _repo.toString() != OSLIST_URL; } void MountUtilsLog(std::string msg) { Q_UNUSED(msg) //qDebug() << "mountutils:" << msg.c_str(); }