Branch data Line data Source code
1 : : // Copyright (C) 2021 The Qt Company Ltd.
2 : : // Copyright (C) 2019 Luxoft Sweden AB
3 : : // Copyright (C) 2018 Pelagicore AG
4 : : // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
5 : :
6 : : #include <QStringList>
7 : : #include <QThread>
8 : : #include <QAtomicInt>
9 : : #include <QFile>
10 : : #include <QDir>
11 : : #include <QDataStream>
12 : : #include <QUrl>
13 : : #include <QDebug>
14 : : #include <QCryptographicHash>
15 : :
16 : : #include <archive.h>
17 : : #include <archive_entry.h>
18 : :
19 : : #include "packageutilities_p.h"
20 : : #include "packageextractor.h"
21 : : #include "packageextractor_p.h"
22 : : #include "exception.h"
23 : : #include "error.h"
24 : : #include "installationreport.h"
25 : : #include "utilities.h"
26 : : #include "packageinfo.h"
27 : : #include "qtyaml.h"
28 : :
29 : : // archive.h might #define this for Android
30 : : #ifdef open
31 : : # undef open
32 : : #endif
33 : : // these are not defined on all platforms
34 : : #ifndef S_IREAD
35 : : # define S_IREAD S_IRUSR
36 : : #endif
37 : : #ifndef S_IEXEC
38 : : # define S_IEXEC S_IXUSR
39 : : #endif
40 : :
41 : : using namespace Qt::StringLiterals;
42 : :
43 : : QT_BEGIN_NAMESPACE_AM
44 : :
45 : 66 : PackageExtractor::PackageExtractor(const QUrl &downloadUrl, const QDir &destinationDir, QObject *parent)
46 : : : QObject(parent)
47 [ + - + - ]: 66 : , d(new PackageExtractorPrivate(this, downloadUrl))
48 : : {
49 [ + - ]: 66 : setDestinationDirectory(destinationDir);
50 : 66 : }
51 : :
52 : 135 : QDir PackageExtractor::destinationDirectory() const
53 : : {
54 : 135 : return QDir(d->m_destinationPath);
55 : : }
56 : :
57 : 104 : void PackageExtractor::setDestinationDirectory(const QDir &destinationDir)
58 : : {
59 [ + - ]: 104 : d->m_destinationPath = destinationDir.absolutePath() + u'/';
60 : 104 : }
61 : :
62 : 88 : void PackageExtractor::setFileExtractedCallback(const std::function<void(const QString &)> &callback)
63 : : {
64 : 88 : d->m_fileExtractedCallback = callback;
65 : 88 : }
66 : :
67 : 209 : const InstallationReport &PackageExtractor::installationReport() const
68 : : {
69 : 209 : return d->m_report;
70 : : }
71 : :
72 : 66 : bool PackageExtractor::extract()
73 : : {
74 : 66 : if (!wasCanceled()) {
75 : 65 : d->m_failed = false;
76 : :
77 : 65 : d->download(d->m_url);
78 : :
79 : 65 : QMetaObject::invokeMethod(d, "extract", Qt::QueuedConnection);
80 : 65 : d->m_loop.exec();
81 : :
82 [ + - ]: 65 : delete d->m_reply;
83 : 65 : d->m_reply = nullptr;
84 : : }
85 [ + + ]: 64 : return !wasCanceled() && !hasFailed();
86 : : }
87 : :
88 : 66 : bool PackageExtractor::hasFailed() const
89 : : {
90 [ + + + + ]: 66 : return d->m_failed || wasCanceled();
91 : : }
92 : :
93 : 311 : bool PackageExtractor::wasCanceled() const
94 : : {
95 [ + + + + : 183 : return d->m_canceled != 0;
+ + ]
96 : : }
97 : :
98 : 20 : Error PackageExtractor::errorCode() const
99 : : {
100 [ + + + - ]: 20 : return wasCanceled() ? Error::Canceled : (d->m_failed ? d->m_errorCode : Error::None);
101 : : }
102 : :
103 : 21 : QString PackageExtractor::errorString() const
104 : : {
105 [ - + + + ]: 21 : return wasCanceled() ? u"canceled"_s : (d->m_failed ? d->m_errorString : QString());
106 : : }
107 : :
108 : : /*! \internal
109 : : This function can be called from another thread while extract() is running
110 : : */
111 : 10 : void PackageExtractor::cancel()
112 : : {
113 [ + + ]: 10 : if (!d->m_canceled.fetchAndStoreOrdered(1)) {
114 [ + + ]: 9 : if (d->m_loop.isRunning())
115 : 1 : d->m_loop.wakeUp();
116 : : }
117 : 10 : }
118 : :
119 : :
120 : : /* * * * * * * * * * * * * * * * * * *
121 : : * vvv PackageExtractorPrivate vvv *
122 : : * * * * * * * * * * * * * * * * * * */
123 : :
124 : 66 : PackageExtractorPrivate::PackageExtractorPrivate(PackageExtractor *extractor, const QUrl &downloadUrl)
125 : : : QObject(extractor)
126 : 66 : , q(extractor)
127 : 66 : , m_url(downloadUrl)
128 [ + - + - ]: 66 : , m_nam(new QNetworkAccessManager(this))
129 [ + - + - ]: 132 : , m_report(QString())
130 : : {
131 [ + - ]: 66 : m_buffer.resize(64 * 1024);
132 : 66 : }
133 : :
134 : 66 : qint64 PackageExtractorPrivate::readTar(struct archive *ar, const void **archiveBuffer)
135 : : {
136 : 66 : forever {
137 : : // the event loop is gone
138 [ - + ]: 66 : if (!m_loop.isRunning()) {
139 : 0 : archive_set_error(ar, -1, "no eventlopp");
140 : 0 : return -1;
141 : : }
142 : : // we have been canceled
143 [ - + ]: 66 : if (q->wasCanceled()) {
144 : 0 : archive_set_error(ar, -1, "canceled");
145 : 0 : return -1;
146 : : }
147 : 66 : qint64 bytesAvailable = m_reply->bytesAvailable();
148 : :
149 : : // there is something to read
150 : : // (or this is a FIFO and we need this ugly hack - for testing only though!)
151 [ + + + + ]: 66 : if ((bytesAvailable > 0) || m_downloadingFromFIFO) {
152 : 64 : qint64 bytesRead = m_reply->read(m_buffer.data(), m_buffer.size());
153 : :
154 [ - + ]: 64 : if (bytesRead < 0) {
155 : : // another FIFO hack: if the writer dies, we will get an -1 return from read()
156 [ # # # # ]: 0 : if (m_downloadingFromFIFO && m_reply->atEnd()) {
157 : : return 0;
158 : : } else {
159 : 0 : archive_set_error(ar, -1, "could not read from tar archive");
160 : 0 : return -1;
161 : : }
162 : : }
163 : :
164 : 64 : m_bytesReadTotal += bytesRead;
165 [ + - ]: 64 : *archiveBuffer = m_buffer.constData();
166 : :
167 [ + + ]: 64 : qint64 progress = m_downloadTotal ? (100 * m_bytesReadTotal / m_downloadTotal) : 0;
168 [ + + ]: 64 : if (progress != m_lastProgress) {
169 : 63 : emit q->progress(qreal(progress) / 100);
170 : 63 : m_lastProgress = progress;
171 : : }
172 : :
173 : 64 : return bytesRead;
174 : : }
175 : :
176 : : // got an error while reading
177 [ + + ]: 2 : if (m_reply->error() != QNetworkReply::NoError) {
178 [ + - + - ]: 3 : archive_set_error(ar, -1, "%s", m_reply->errorString().toLocal8Bit().constData());
179 : 1 : return -1;
180 : : }
181 : :
182 : : // we're done
183 [ - + ]: 1 : if (m_reply->isFinished())
184 : : return 0;
185 : :
186 : 0 : m_loop.processEvents(QEventLoop::WaitForMoreEvents);
187 : 0 : }
188 : : }
189 : :
190 : 65 : void PackageExtractorPrivate::extract()
191 : : {
192 : 65 : struct archive *ar = nullptr;
193 : :
194 : 65 : try {
195 [ + - ]: 65 : ar = archive_read_new();
196 [ - + ]: 65 : if (!ar)
197 : 0 : throw Exception("[libarchive] could not create a new archive object");
198 [ + - - + ]: 65 : if (archive_read_support_format_tar(ar) != ARCHIVE_OK)
199 [ # # ]: 0 : throw ArchiveException(ar, "could not enable TAR support");
200 : : // disabled for now -- see libarchive.pro
201 : : // if (archive_read_support_filter_xz(ar) != ARCHIVE_OK)
202 : : // throw ArchiveException(ar, "could not enable XZ support");
203 [ + - - + ]: 65 : if (archive_read_support_filter_gzip(ar) != ARCHIVE_OK)
204 [ # # ]: 0 : throw ArchiveException(ar, "could not enable GZIP support");
205 : : #if !defined(Q_OS_ANDROID)
206 [ + - - + ]: 65 : if (archive_read_set_options(ar, "hdrcharset=UTF-8") != ARCHIVE_OK)
207 [ # # ]: 0 : throw ArchiveException(ar, "could not set the HDRCHARSET option");
208 : : #endif
209 : :
210 : 65 : auto dummyCallback = [](archive *, void *) { return ARCHIVE_OK; };
211 : 65 : auto readCallback = [](archive *arRead, void *user, const void **buffer)
212 : : { return static_cast<__LA_SSIZE_T>(static_cast<PackageExtractorPrivate *>(user)->readTar(arRead, buffer)); };
213 : :
214 [ + - + + ]: 65 : if (archive_read_open(ar, this, dummyCallback, readCallback, dummyCallback) != ARCHIVE_OK)
215 [ + - ]: 2 : throw ArchiveException(ar, "could not open archive");
216 : :
217 : 63 : bool seenHeader = false;
218 : 63 : bool seenFooter = false;
219 : 63 : QByteArray header;
220 : 63 : QByteArray footer;
221 : :
222 [ + - ]: 63 : QCryptographicHash digest(QCryptographicHash::Sha256);
223 : :
224 : : // Iterate over all entries in the archive
225 [ + + ]: 450 : for (bool finished = false; !finished; ) {
226 : 397 : archive_entry *entry = nullptr;
227 [ + - ]: 397 : QFile f;
228 : :
229 : : // Try to read the next entry from the archive
230 : :
231 [ + - + - : 397 : switch (archive_read_next_header(ar, &entry)) {
+ ]
232 : 53 : case ARCHIVE_EOF:
233 : 53 : finished = true;
234 : 53 : continue;
235 : 344 : case ARCHIVE_OK:
236 : 344 : break;
237 : 0 : default:
238 [ # # ]: 0 : throw ArchiveException(ar, "could not read header");
239 : : }
240 : :
241 : : // Make sure to quit if we get something funky, i.e. something other than files or dirs
242 : :
243 [ + - ]: 344 : __LA_MODE_T entryMode = archive_entry_mode(entry);
244 : 344 : PackageEntryType packageEntryType;
245 [ + - + - ]: 344 : QString entryPath = QString::fromUtf8(archive_entry_pathname_utf8(entry))
246 [ + - ]: 354 : .normalized(QString::NormalizationForm_C);
247 : :
248 [ - - + ]: 344 : switch (entryMode & S_IFMT) {
249 : : case S_IFREG:
250 : : packageEntryType = PackageEntry_File;
251 : : break;
252 : 0 : case S_IFDIR:
253 : 0 : packageEntryType = PackageEntry_Dir;
254 : 0 : break;
255 : 0 : default:
256 : 0 : throw Exception(Error::Package, "file %1 in the archive has the unsupported type (mode) 0%2").arg(entryPath).arg(int(entryMode & S_IFMT), 0, 8);
257 : : }
258 : :
259 : : // Check if this entry is special (metadata vs. data)
260 : :
261 [ + + ]: 344 : if (entryPath == u"--PACKAGE-HEADER--")
262 : : packageEntryType = PackageEntry_Header;
263 [ + + ]: 281 : else if (entryPath.startsWith(u"--PACKAGE-FOOTER--"))
264 : : packageEntryType = PackageEntry_Footer;
265 [ - + ]: 205 : else if (entryPath.startsWith(u"--"))
266 : 0 : throw Exception(Error::Package, "filename %1 in the archive starts with the reserved characters '--'").arg(entryPath);
267 : :
268 : : // The first (and only the first) file in every package needs to be --PACKAGE-HEADER--
269 : :
270 [ - + ]: 344 : if (seenHeader == (packageEntryType == PackageEntry_Header))
271 : 0 : throw Exception(Error::Package, "the first (and only the first) file of the package must be --PACKAGE-HEADER--");
272 : :
273 : : // There cannot be and normal files after the first --PACKAGE-FOOTER--
274 [ - + ]: 344 : if (seenFooter && (packageEntryType != PackageEntry_Footer))
275 : 0 : throw Exception(Error::Package, "only --PACKAGE-FOOTER--* files are allowed at the end of the package");
276 : :
277 [ - + + + ]: 344 : switch (packageEntryType) {
278 : 0 : case PackageEntry_Dir:
279 [ # # # # ]: 0 : if (!entryPath.endsWith(u'/'))
280 : 0 : throw Exception(Error::Package, "invalid archive entry '%1': directory name is missing '/' at the end").arg(entryPath);
281 [ # # ]: 0 : entryPath.chop(1);
282 : 205 : Q_FALLTHROUGH();
283 : :
284 : 205 : case PackageEntry_File: {
285 : : // get the directory, where the new entry will be created
286 [ + - + - : 410 : QDir entryDir(QString(m_destinationPath + entryPath).section(u'/', 0, -2));
+ - ]
287 [ + - - + ]: 205 : if (!entryDir.exists())
288 : 0 : throw Exception(Error::Package, "invalid archive entry '%1': parent directory is missing").arg(entryPath);
289 : :
290 [ + - + - ]: 205 : QString entryCanonicalPath = entryDir.canonicalPath() + u'/';
291 [ + - + - : 206 : QString baseCanonicalPath = QDir(m_destinationPath).canonicalPath() + u'/';
+ - ]
292 : :
293 : : // security check: make sure that entryCanonicalPath is NOT outside of baseCanonicalPath
294 [ + - + + ]: 205 : if (!entryCanonicalPath.startsWith(baseCanonicalPath))
295 : 1 : throw Exception(Error::Package, "invalid archive entry '%1': pointing outside of extraction directory").arg(entryPath);
296 : :
297 [ - + ]: 204 : if (packageEntryType == PackageEntry_Dir) {
298 [ # # ]: 0 : QString entryName = entryPath.section(u'/', -1, -1);
299 : :
300 [ # # # # : 0 : if ((entryName != u".") && !entryDir.mkdir(entryName))
# # ]
301 [ # # ]: 0 : throw Exception(Error::IO, "could not create directory '%1'").arg(entryDir.filePath(entryName));
302 : :
303 [ # # ]: 0 : archive_read_data_skip(ar);
304 : :
305 : 0 : } else { // PackageEntry_File
306 [ + - + - ]: 204 : f.setFileName(m_destinationPath + entryPath);
307 [ + - - + ]: 204 : if (!f.open(QFile::WriteOnly | QFile::Truncate))
308 : 0 : throw Exception(f, "could not create file");
309 : :
310 [ - + ]: 204 : if (entryMode & S_IEXEC)
311 [ - - - - ]: 1 : f.setPermissions(f.permissions() | QFile::ExeUser);
312 : : }
313 : :
314 [ + - ]: 204 : m_report.addFile(entryPath);
315 : 204 : break;
316 : 206 : }
317 : : case PackageEntry_Footer:
318 : : seenFooter = true;
319 : : break;
320 : 63 : case PackageEntry_Header:
321 : 63 : seenHeader = true;
322 : 63 : break;
323 : : default:
324 : : archive_read_data_skip(ar);
325 : : continue;
326 : : }
327 : :
328 : : // Read in the entry's data (which can be a normal file or header/footer metadata)
329 : :
330 [ + - + - ]: 343 : if (archive_entry_size(entry)) {
331 : 343 : __LA_INT64_T readPosition = 0;
332 : :
333 : 343 : for (bool fileFinished = false; !fileFinished; ) {
334 : 846 : const char *buffer;
335 : 846 : size_t bytesRead;
336 : 846 : __LA_INT64_T offset;
337 : :
338 [ + - + + : 846 : switch (archive_read_data_block(ar, reinterpret_cast<const void **>(&buffer), &bytesRead, &offset)) {
- ]
339 : 343 : case ARCHIVE_EOF:
340 : 343 : fileFinished = true;
341 : 343 : continue;
342 : 503 : case ARCHIVE_OK:
343 : 503 : break;
344 : 0 : default:
345 [ # # ]: 0 : throw ArchiveException(ar, "could not read from archive");
346 : : }
347 : :
348 [ - + ]: 503 : if (offset != readPosition)
349 : 0 : throw Exception(Error::Package, "[libarchive] current read position (%1) does not match requested offset (%2)").arg(readPosition).arg(offset);
350 : :
351 : 503 : readPosition += bytesRead;
352 : :
353 [ + + + - ]: 503 : switch (packageEntryType) {
354 : 364 : case PackageEntry_File:
355 : 364 : digest.addData({ buffer, qsizetype(bytesRead) });
356 : :
357 [ + - - + ]: 364 : if (!f.write(buffer, qint64(bytesRead)))
358 : 0 : throw Exception(f, "could not write to file");
359 : : break;
360 : 63 : case PackageEntry_Header:
361 [ + - ]: 63 : header.append(buffer, qsizetype(bytesRead));
362 : : break;
363 : 76 : case PackageEntry_Footer:
364 [ + - ]: 76 : footer.append(buffer, qsizetype(bytesRead));
365 : : break;
366 : : default:
367 : : break;
368 : : }
369 : : }
370 : : }
371 : :
372 : : // We finished reading an entry from the archive. Now we need to
373 : : // post-process it, depending on its type
374 : :
375 [ + + - + ]: 343 : switch (packageEntryType) {
376 : 63 : case PackageEntry_Header:
377 [ + + ]: 63 : processMetaData(header, digest, true /*header*/);
378 : : break;
379 : :
380 : 204 : case PackageEntry_File:
381 [ + - ]: 204 : f.close();
382 : 204 : Q_FALLTHROUGH();
383 : :
384 : 204 : case PackageEntry_Dir: {
385 : : // Just to be on the safe side, we also add the file's meta-data to the digest
386 [ + - + - : 214 : PackageUtilities::addFileMetadataToDigest(entryPath, QFileInfo(m_destinationPath + entryPath), digest);
+ - ]
387 : :
388 : : // Finally call the user's code to post-process whatever was extracted right now
389 [ + + ]: 204 : if (m_fileExtractedCallback)
390 [ + + ]: 97 : m_fileExtractedCallback(entryPath);
391 : : break;
392 : : }
393 : : default:
394 : : break;
395 : : }
396 : 397 : }
397 : :
398 : : // Finished extracting
399 : :
400 : : // We are only post-processing the footer now, because we allow for multiple --PACKAGE-FOOTER--
401 : : // files in the archive, so we can only start processing them, when we are sure that there
402 : : // are no more. This makes it easier for 3rd party tools like e.g. app-stores to add the required
403 : : // signature metadata
404 [ + + ]: 53 : processMetaData(footer, digest, false /*footer*/);
405 : :
406 [ + - ]: 51 : emit q->progress(1);
407 : :
408 [ - + ]: 89 : } catch (const Exception &e) {
409 [ + - ]: 14 : if (!q->wasCanceled())
410 : 28 : setError(e.errorCode(), e.errorString());
411 : 14 : }
412 : :
413 [ + - ]: 65 : if (ar)
414 : 65 : archive_read_free(ar);
415 : :
416 : 65 : m_loop.quit();
417 : 65 : }
418 : :
419 : 116 : void PackageExtractorPrivate::processMetaData(const QByteArray &metadata, QCryptographicHash &digest,
420 : : bool isHeader) noexcept(false)
421 : : {
422 : 116 : QVector<QVariant> docs;
423 : 116 : try {
424 [ + - ]: 232 : docs = YamlParser::parseAllDocuments(metadata);
425 [ - - ]: 0 : } catch (const Exception &e) {
426 : 0 : throw Exception(Error::Package, "metadata is not a valid YAML document: %1")
427 : 0 : .arg(e.errorString());
428 : 0 : }
429 : :
430 [ + + ]: 116 : const QString formatType = isHeader ? u"am-package-header"_s : u"am-package-footer"_s;
431 : 116 : int formatVersion = 0;
432 : 116 : try {
433 [ + + + + : 466 : formatVersion = checkYamlFormat(docs, -2 /*at least 2 docs*/, { { formatType, 2 },
+ + ]
434 : 116 : { formatType, 1 } }).second;
435 [ - + ]: 1 : } catch (const Exception &e) {
436 : 1 : throw Exception(Error::Package, "metadata has an invalid format specification: %1").arg(e.errorString());
437 : 1 : }
438 : :
439 [ + - ]: 115 : QVariantMap map = docs.at(1).toMap();
440 : :
441 [ + + ]: 115 : if (isHeader) {
442 [ - + ]: 62 : const QString idField = (formatVersion == 1) ? u"applicationId"_s : u"packageId"_s;
443 : :
444 [ + - + - ]: 126 : QString packageId = map.value(idField).toString();
445 [ + - + - ]: 62 : quint64 diskSpaceUsed = map.value(u"diskSpaceUsed"_s).toULongLong();
446 : :
447 [ + - + - : 62 : if (packageId.isNull() || !PackageInfo::isValidApplicationId(packageId))
+ + ]
448 : 1 : throw Exception(Error::Package, "metadata has an invalid %2 field (%1)").arg(packageId).arg(idField);
449 [ + - ]: 61 : m_report.setPackageId(packageId);
450 : :
451 [ + + ]: 61 : if (!diskSpaceUsed)
452 : 1 : throw Exception(Error::Package, "metadata has an invalid diskSpaceUsed field (%1)").arg(diskSpaceUsed);
453 [ + - ]: 60 : m_report.setDiskSpaceUsed(diskSpaceUsed);
454 : :
455 [ + - + - : 120 : m_report.setExtraMetaData(map.value(u"extra"_s).toMap());
+ - ]
456 [ + - + - : 122 : m_report.setExtraSignedMetaData(map.value(u"extraSigned"_s).toMap());
+ - ]
457 : :
458 [ + - ]: 60 : PackageUtilities::addHeaderDataToDigest(map, digest);
459 : :
460 : 64 : } else { // footer(s)
461 [ + + ]: 76 : for (int i = 2; i < docs.size(); ++i)
462 [ + - + - ]: 46 : map.insert(docs.at(i).toMap());
463 : :
464 [ + - + - : 163 : QByteArray packageDigest = QByteArray::fromHex(map.value(u"digest"_s).toString().toLatin1());
+ - ]
465 : :
466 [ - + ]: 53 : if (packageDigest.isEmpty())
467 : 0 : throw Exception(Error::Package, "metadata is missing the digest field");
468 [ + - ]: 53 : m_report.setDigest(packageDigest);
469 : :
470 [ + - ]: 53 : QByteArray calculatedDigest = digest.result();
471 [ + + ]: 53 : if (calculatedDigest != packageDigest)
472 [ + - + - ]: 2 : throw Exception(Error::Package, "package digest mismatch (is %1, but should be %2").arg(calculatedDigest.toHex()).arg(packageDigest.toHex());
473 : :
474 [ + - + - : 153 : m_report.setStoreSignature(QByteArray::fromBase64(map.value(u"storeSignature"_s).toString().toLatin1()));
+ - + - ]
475 [ + - + - : 155 : m_report.setDeveloperSignature(QByteArray::fromBase64(map.value(u"developerSignature"_s).toString().toLatin1()));
+ - + - ]
476 : 55 : }
477 : 120 : }
478 : :
479 : 15 : void PackageExtractorPrivate::setError(Error errorCode, const QString &errorString)
480 : : {
481 : 15 : m_failed = true;
482 : :
483 : : // only the first error is the one that counts!
484 [ - - + + ]: 14 : if (m_errorCode == Error::None) {
485 : 14 : m_errorCode = errorCode;
486 : 14 : m_errorString = errorString;
487 : : }
488 : 0 : }
489 : :
490 : :
491 : 65 : void PackageExtractorPrivate::download(const QUrl &url)
492 : : {
493 : 65 : QNetworkRequest request(url);
494 [ + - ]: 65 : m_reply = m_nam->get(request);
495 : :
496 : : #if defined(Q_OS_UNIX)
497 : : // This is an ugly hack, but it allows us to use FIFOs in the unit tests.
498 : : // (the problem being, that bytesAvailable() on a QFile wrapping a FIFO will always return 0)
499 : :
500 [ + - + - ]: 65 : if (url.isLocalFile()) {
501 : 65 : struct stat statBuffer;
502 [ + - + - : 130 : if (stat(url.toLocalFile().toLocal8Bit(), &statBuffer) == 0) {
+ + ]
503 [ + + ]: 64 : if (S_ISFIFO(statBuffer.st_mode)) {
504 : 1 : m_downloadingFromFIFO = true;
505 : : }
506 : : }
507 : : }
508 : : #endif
509 : :
510 : 65 : connect(m_reply, &QNetworkReply::errorOccurred,
511 [ + - ]: 65 : this, &PackageExtractorPrivate::networkError);
512 : 65 : connect(m_reply, &QNetworkReply::metaDataChanged,
513 [ + - ]: 65 : this, &PackageExtractorPrivate::handleRedirect);
514 : 65 : connect(m_reply, &QNetworkReply::downloadProgress,
515 [ + - ]: 65 : this, &PackageExtractorPrivate::downloadProgressChanged);
516 : 65 : }
517 : :
518 : 1 : void PackageExtractorPrivate::networkError(QNetworkReply::NetworkError)
519 : : {
520 [ + - ]: 2 : setError(Error::Network, qobject_cast<QNetworkReply *>(sender())->errorString());
521 : 1 : QMetaObject::invokeMethod(&m_loop, "quit", Qt::QueuedConnection);
522 : 1 : }
523 : :
524 : 64 : void PackageExtractorPrivate::handleRedirect()
525 : : {
526 [ + - ]: 64 : int status = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
527 [ - + ]: 64 : if ((status >= 300) && (status < 400)) {
528 [ # # ]: 0 : QUrl url = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
529 [ # # ]: 0 : m_reply->disconnect();
530 [ # # ]: 0 : m_reply->deleteLater();
531 [ # # ]: 0 : QNetworkRequest request(url);
532 [ # # ]: 0 : m_reply = m_nam->get(request);
533 : 0 : }
534 : 64 : }
535 : :
536 : 64 : void PackageExtractorPrivate::downloadProgressChanged(qint64 downloaded, qint64 total)
537 : : {
538 : 64 : Q_UNUSED(downloaded)
539 : 64 : m_downloadTotal = total;
540 : 0 : }
541 : :
542 : : QT_END_NAMESPACE_AM
543 : :
544 : : #include "moc_packageextractor.cpp"
545 : :
546 : : #include "moc_packageextractor_p.cpp"
|