1/****************************************************************************
2**
3** Copyright (C) 2020 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtNetwork module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qdecompresshelper_p.h"
41
42#include <QtCore/private/qbytearray_p.h>
43#include <QtCore/qiodevice.h>
44
45#include <zlib.h>
46
47#if QT_CONFIG(brotli)
48# include <brotli/decode.h>
49#endif
50
51#if QT_CONFIG(zstd)
52# include <zstd.h>
53#endif
54
55#include <array>
56
57QT_BEGIN_NAMESPACE
58namespace {
59struct ContentEncodingMapping
60{
61 char name[8];
62 QDecompressHelper::ContentEncoding encoding;
63};
64
65constexpr ContentEncodingMapping contentEncodingMapping[] {
66#if QT_CONFIG(zstd)
67 { "zstd", QDecompressHelper::Zstandard },
68#endif
69#if QT_CONFIG(brotli)
70 { "br", QDecompressHelper::Brotli },
71#endif
72 { "gzip", QDecompressHelper::GZip },
73 { "deflate", QDecompressHelper::Deflate },
74};
75
76QDecompressHelper::ContentEncoding encodingFromByteArray(const QByteArray &ce) noexcept
77{
78 for (const auto &mapping : contentEncodingMapping) {
79 if (ce.compare(QByteArrayView(mapping.name, strlen(mapping.name)), Qt::CaseInsensitive) == 0)
80 return mapping.encoding;
81 }
82 return QDecompressHelper::None;
83}
84
85z_stream *toZlibPointer(void *ptr)
86{
87 return static_cast<z_stream_s *>(ptr);
88}
89
90#if QT_CONFIG(brotli)
91BrotliDecoderState *toBrotliPointer(void *ptr)
92{
93 return static_cast<BrotliDecoderState *>(ptr);
94}
95#endif
96
97#if QT_CONFIG(zstd)
98ZSTD_DStream *toZstandardPointer(void *ptr)
99{
100 return static_cast<ZSTD_DStream *>(ptr);
101}
102#endif
103}
104
105bool QDecompressHelper::isSupportedEncoding(const QByteArray &encoding)
106{
107 return encodingFromByteArray(encoding) != QDecompressHelper::None;
108}
109
110QByteArrayList QDecompressHelper::acceptedEncoding()
111{
112 static QByteArrayList accepted = []() {
113 QByteArrayList list;
114 list.reserve(sizeof(contentEncodingMapping) / sizeof(contentEncodingMapping[0]));
115 for (const auto &mapping : contentEncodingMapping) {
116 list << QByteArray(mapping.name);
117 }
118 return list;
119 }();
120 return accepted;
121}
122
123QDecompressHelper::~QDecompressHelper()
124{
125 clear();
126}
127
128bool QDecompressHelper::setEncoding(const QByteArray &encoding)
129{
130 Q_ASSERT(contentEncoding == QDecompressHelper::None);
131 if (contentEncoding != QDecompressHelper::None) {
132 qWarning("Encoding is already set.");
133 return false;
134 }
135 ContentEncoding ce = encodingFromByteArray(encoding);
136 if (ce == None) {
137 qWarning("An unsupported content encoding was selected: %s", encoding.data());
138 return false;
139 }
140 return setEncoding(ce);
141}
142
143bool QDecompressHelper::setEncoding(ContentEncoding ce)
144{
145 Q_ASSERT(contentEncoding == None);
146 contentEncoding = ce;
147 switch (contentEncoding) {
148 case None:
149 Q_UNREACHABLE();
150 break;
151 case Deflate:
152 case GZip: {
153 z_stream *inflateStream = new z_stream;
154 memset(inflateStream, 0, sizeof(z_stream));
155 // "windowBits can also be greater than 15 for optional gzip decoding.
156 // Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection"
157 // http://www.zlib.net/manual.html
158 if (inflateInit2(inflateStream, MAX_WBITS + 32) != Z_OK) {
159 delete inflateStream;
160 inflateStream = nullptr;
161 }
162 decoderPointer = inflateStream;
163 break;
164 }
165 case Brotli:
166#if QT_CONFIG(brotli)
167 decoderPointer = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
168#else
169 Q_UNREACHABLE();
170#endif
171 break;
172 case Zstandard:
173#if QT_CONFIG(zstd)
174 decoderPointer = ZSTD_createDStream();
175#else
176 Q_UNREACHABLE();
177#endif
178 break;
179 }
180 if (!decoderPointer) {
181 qWarning("Failed to initialize the decoder.");
182 contentEncoding = QDecompressHelper::None;
183 return false;
184 }
185 return true;
186}
187
188/*!
189 \internal
190
191 Returns true if the QDecompressHelper is measuring the
192 size of the decompressed data.
193
194 \sa setCountingBytesEnabled, uncompressedSize
195*/
196bool QDecompressHelper::isCountingBytes() const
197{
198 return countDecompressed;
199}
200
201/*!
202 \internal
203
204 Enable or disable counting the decompressed size of the data
205 based on \a shouldCount. Enabling this means the data will be
206 decompressed twice (once for counting and once when data is
207 being read).
208
209 \note Can only be called before contentEncoding is set and data
210 is fed to the object.
211
212 \sa isCountingBytes, uncompressedSize
213*/
214void QDecompressHelper::setCountingBytesEnabled(bool shouldCount)
215{
216 // These are a best-effort check to ensure that no data has already been processed before this
217 // gets enabled
218 Q_ASSERT(compressedDataBuffer.byteAmount() == 0);
219 Q_ASSERT(contentEncoding == None);
220 countDecompressed = shouldCount;
221}
222
223/*!
224 \internal
225
226 Returns the amount of uncompressed bytes left.
227
228 \note Since this is only based on the data received
229 so far the final size could be larger.
230
231 \note It is only valid to call this if isCountingBytes()
232 returns true
233
234 \sa isCountingBytes, setCountBytes
235*/
236qint64 QDecompressHelper::uncompressedSize() const
237{
238 Q_ASSERT(countDecompressed);
239 return uncompressedBytes;
240}
241
242/*!
243 \internal
244 \overload
245*/
246void QDecompressHelper::feed(const QByteArray &data)
247{
248 return feed(QByteArray(data));
249}
250
251/*!
252 \internal
253 Give \a data to the QDecompressHelper which will be stored until
254 a read is attempted.
255
256 If \c isCountingBytes() is true then it will decompress immediately
257 before discarding the data, but will count the uncompressed byte
258 size.
259*/
260void QDecompressHelper::feed(QByteArray &&data)
261{
262 Q_ASSERT(contentEncoding != None);
263 totalCompressedBytes += data.size();
264 if (!countInternal(data))
265 clear(); // If our counting brother failed then so will we :|
266 else
267 compressedDataBuffer.append(std::move(data));
268}
269
270/*!
271 \internal
272 \overload
273*/
274void QDecompressHelper::feed(const QByteDataBuffer &buffer)
275{
276 Q_ASSERT(contentEncoding != None);
277 totalCompressedBytes += buffer.byteAmount();
278 if (!countInternal(buffer))
279 clear(); // If our counting brother failed then so will we :|
280 else
281 compressedDataBuffer.append(buffer);
282}
283
284/*!
285 \internal
286 \overload
287*/
288void QDecompressHelper::feed(QByteDataBuffer &&buffer)
289{
290 Q_ASSERT(contentEncoding != None);
291 totalCompressedBytes += buffer.byteAmount();
292 if (!countInternal(buffer))
293 clear(); // If our counting brother failed then so will we :|
294 else
295 compressedDataBuffer.append(std::move(buffer));
296}
297
298/*!
299 \internal
300 Decompress the data internally and immediately discard the
301 uncompressed data, but count how many bytes were decoded.
302 This lets us know the final size, unfortunately at the cost of
303 increased computation.
304
305 Potential @future improvement:
306 Decompress XX MiB/KiB before starting the count.
307 For smaller files the extra decompression can then be avoided.
308*/
309bool QDecompressHelper::countInternal()
310{
311 Q_ASSERT(countDecompressed);
312 while (countHelper->hasData()) {
313 std::array<char, 1024> temp;
314 qsizetype bytesRead = countHelper->read(temp.data(), temp.size());
315 if (bytesRead == -1)
316 return false;
317 uncompressedBytes += bytesRead;
318 }
319 return true;
320}
321
322/*!
323 \internal
324 \overload
325*/
326bool QDecompressHelper::countInternal(const QByteArray &data)
327{
328 if (countDecompressed) {
329 if (!countHelper) {
330 countHelper = std::make_unique<QDecompressHelper>();
331 countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled);
332 countHelper->setEncoding(contentEncoding);
333 }
334 countHelper->feed(data);
335 return countInternal();
336 }
337 return true;
338}
339
340/*!
341 \internal
342 \overload
343*/
344bool QDecompressHelper::countInternal(const QByteDataBuffer &buffer)
345{
346 if (countDecompressed) {
347 if (!countHelper) {
348 countHelper = std::make_unique<QDecompressHelper>();
349 countHelper->setArchiveBombDetectionEnabled(archiveBombDetectionEnabled);
350 countHelper->setEncoding(contentEncoding);
351 }
352 countHelper->feed(buffer);
353 return countInternal();
354 }
355 return true;
356}
357
358qsizetype QDecompressHelper::read(char *data, qsizetype maxSize)
359{
360 if (!isValid())
361 return -1;
362
363 qsizetype bytesRead = -1;
364 if (!hasData())
365 return 0;
366
367 switch (contentEncoding) {
368 case None:
369 Q_UNREACHABLE();
370 break;
371 case Deflate:
372 case GZip:
373 bytesRead = readZLib(data, maxSize);
374 break;
375 case Brotli:
376 bytesRead = readBrotli(data, maxSize);
377 break;
378 case Zstandard:
379 bytesRead = readZstandard(data, maxSize);
380 break;
381 }
382 if (bytesRead == -1)
383 clear();
384 else if (countDecompressed)
385 uncompressedBytes -= bytesRead;
386
387 totalUncompressedBytes += bytesRead;
388 if (isPotentialArchiveBomb())
389 return -1;
390
391 return bytesRead;
392}
393
394/*!
395 \internal
396 Disables or enables checking the decompression ratio of archives
397 according to the value of \a enable.
398 Only for enabling us to test handling of large decompressed files
399 without needing to bundle large compressed files.
400*/
401void QDecompressHelper::setArchiveBombDetectionEnabled(bool enable)
402{
403 archiveBombDetectionEnabled = enable;
404 if (countHelper)
405 countHelper->setArchiveBombDetectionEnabled(enable);
406}
407
408bool QDecompressHelper::isPotentialArchiveBomb() const
409{
410 if (!archiveBombDetectionEnabled)
411 return false;
412
413 if (totalCompressedBytes == 0)
414 return false;
415
416 // Some protection against malicious or corrupted compressed files that expand far more than
417 // is reasonable.
418 double ratio = double(totalUncompressedBytes) / double(totalCompressedBytes);
419 switch (contentEncoding) {
420 case None:
421 Q_UNREACHABLE();
422 break;
423 case Deflate:
424 case GZip:
425 if (ratio > 40) {
426 return true;
427 }
428 break;
429 case Brotli:
430 case Zstandard:
431 if (ratio > 100) {
432 return true;
433 }
434 break;
435 }
436 return false;
437}
438
439/*!
440 \internal
441 Returns true if there are encoded bytes left or there is some
442 indication that the decoder still has data left internally.
443
444 \note Even if this returns true the next call to read() might
445 read 0 bytes. This most likely means the decompression is done.
446*/
447bool QDecompressHelper::hasData() const
448{
449 return encodedBytesAvailable() || decoderHasData;
450}
451
452qint64 QDecompressHelper::encodedBytesAvailable() const
453{
454 return compressedDataBuffer.byteAmount();
455}
456
457bool QDecompressHelper::isValid() const
458{
459 return contentEncoding != None;
460}
461
462void QDecompressHelper::clear()
463{
464 switch (contentEncoding) {
465 case None:
466 break;
467 case Deflate:
468 case GZip: {
469 z_stream *inflateStream = toZlibPointer(decoderPointer);
470 if (inflateStream)
471 inflateEnd(inflateStream);
472 delete inflateStream;
473 break;
474 }
475 case Brotli: {
476#if QT_CONFIG(brotli)
477 BrotliDecoderState *brotliDecoderState = toBrotliPointer(decoderPointer);
478 if (brotliDecoderState)
479 BrotliDecoderDestroyInstance(brotliDecoderState);
480#endif
481 break;
482 }
483 case Zstandard: {
484#if QT_CONFIG(zstd)
485 ZSTD_DStream *zstdStream = toZstandardPointer(decoderPointer);
486 if (zstdStream)
487 ZSTD_freeDStream(zstdStream);
488#endif
489 break;
490 }
491 }
492 decoderPointer = nullptr;
493 contentEncoding = None;
494
495 compressedDataBuffer.clear();
496 decoderHasData = false;
497
498 countDecompressed = false;
499 countHelper.reset();
500 uncompressedBytes = 0;
501 totalUncompressedBytes = 0;
502 totalCompressedBytes = 0;
503}
504
505qsizetype QDecompressHelper::readZLib(char *data, const qsizetype maxSize)
506{
507 bool triedRawDeflate = false;
508
509 z_stream *inflateStream = toZlibPointer(decoderPointer);
510 static const size_t zlibMaxSize =
511 size_t(std::numeric_limits<decltype(inflateStream->avail_in)>::max());
512
513 QByteArray input;
514 if (!compressedDataBuffer.isEmpty()) {
515 if (zlibMaxSize < size_t(compressedDataBuffer.sizeNextBlock()))
516 input = compressedDataBuffer.read(zlibMaxSize);
517 else
518 input = compressedDataBuffer.read();
519 }
520
521 inflateStream->avail_in = input.size();
522 inflateStream->next_in = reinterpret_cast<Bytef *>(input.data());
523
524 bool bigMaxSize = (zlibMaxSize < size_t(maxSize));
525 qsizetype adjustedAvailableOut = bigMaxSize ? qsizetype(zlibMaxSize) : maxSize;
526 inflateStream->avail_out = adjustedAvailableOut;
527 inflateStream->next_out = reinterpret_cast<Bytef *>(data);
528
529 qsizetype bytesDecoded = 0;
530 do {
531 auto previous_avail_out = inflateStream->avail_out;
532 int ret = inflate(inflateStream, Z_NO_FLUSH);
533 // All negative return codes are errors, in the context of HTTP compression, Z_NEED_DICT is
534 // also an error.
535 // in the case where we get Z_DATA_ERROR this could be because we received raw deflate
536 // compressed data.
537 if (ret == Z_DATA_ERROR && !triedRawDeflate) {
538 inflateEnd(inflateStream);
539 triedRawDeflate = true;
540 inflateStream->zalloc = Z_NULL;
541 inflateStream->zfree = Z_NULL;
542 inflateStream->opaque = Z_NULL;
543 inflateStream->avail_in = 0;
544 inflateStream->next_in = Z_NULL;
545 int ret = inflateInit2(inflateStream, -MAX_WBITS);
546 if (ret != Z_OK) {
547 return -1;
548 } else {
549 inflateStream->avail_in = input.size();
550 inflateStream->next_in = reinterpret_cast<Bytef *>(input.data());
551 continue;
552 }
553 } else if (ret < 0 || ret == Z_NEED_DICT) {
554 return -1;
555 }
556 bytesDecoded += qsizetype(previous_avail_out - inflateStream->avail_out);
557 if (ret == Z_STREAM_END) {
558
559 // If there's more data after the stream then this is probably composed of multiple
560 // streams.
561 if (inflateStream->avail_in != 0) {
562 inflateEnd(inflateStream);
563 Bytef *next_in = inflateStream->next_in;
564 uInt avail_in = inflateStream->avail_in;
565 inflateStream->zalloc = Z_NULL;
566 inflateStream->zfree = Z_NULL;
567 inflateStream->opaque = Z_NULL;
568 if (inflateInit2(inflateStream, MAX_WBITS + 32) != Z_OK) {
569 delete inflateStream;
570 decoderPointer = nullptr;
571 // Failed to reinitialize, so we'll just return what we have
572 return bytesDecoded;
573 } else {
574 inflateStream->next_in = next_in;
575 inflateStream->avail_in = avail_in;
576 // Keep going to handle the other cases below
577 }
578 } else {
579 // No extra data, stream is at the end. We're done.
580 return bytesDecoded;
581 }
582 }
583
584 if (bigMaxSize && inflateStream->avail_out == 0) {
585 // Need to adjust the next_out and avail_out parameters since we reached the end
586 // of the current range
587 bigMaxSize = (zlibMaxSize < size_t(maxSize - bytesDecoded));
588 inflateStream->avail_out = bigMaxSize ? qsizetype(zlibMaxSize) : maxSize - bytesDecoded;
589 inflateStream->next_out = reinterpret_cast<Bytef *>(data + bytesDecoded);
590 }
591
592 if (inflateStream->avail_in == 0 && inflateStream->avail_out > 0
593 && !compressedDataBuffer.isEmpty()) {
594 // Grab the next input!
595 if (zlibMaxSize < size_t(compressedDataBuffer.sizeNextBlock()))
596 input = compressedDataBuffer.read(zlibMaxSize);
597 else
598 input = compressedDataBuffer.read();
599 inflateStream->avail_in = input.size();
600 inflateStream->next_in = reinterpret_cast<Bytef *>(input.data());
601 }
602 } while (inflateStream->avail_out > 0 && inflateStream->avail_in > 0);
603
604 if (inflateStream->avail_in) {
605 // Some input was left unused; move back to the buffer
606 input = input.right(inflateStream->avail_in);
607 compressedDataBuffer.prepend(input);
608 }
609
610 return bytesDecoded;
611}
612
613qsizetype QDecompressHelper::readBrotli(char *data, const qsizetype maxSize)
614{
615#if !QT_CONFIG(brotli)
616 Q_UNUSED(data);
617 Q_UNUSED(maxSize);
618 Q_UNREACHABLE();
619#else
620 qint64 bytesDecoded = 0;
621
622 BrotliDecoderState *brotliDecoderState = toBrotliPointer(decoderPointer);
623
624 while (decoderHasData && bytesDecoded < maxSize) {
625 Q_ASSERT(brotliUnconsumedDataPtr || BrotliDecoderHasMoreOutput(brotliDecoderState));
626 if (brotliUnconsumedDataPtr) {
627 Q_ASSERT(brotliUnconsumedAmount);
628 size_t toRead = std::min(size_t(maxSize - bytesDecoded), brotliUnconsumedAmount);
629 memcpy(data + bytesDecoded, brotliUnconsumedDataPtr, toRead);
630 bytesDecoded += toRead;
631 brotliUnconsumedAmount -= toRead;
632 brotliUnconsumedDataPtr += toRead;
633 if (brotliUnconsumedAmount == 0) {
634 brotliUnconsumedDataPtr = nullptr;
635 decoderHasData = false;
636 }
637 }
638 if (BrotliDecoderHasMoreOutput(brotliDecoderState) == BROTLI_TRUE) {
639 brotliUnconsumedDataPtr =
640 BrotliDecoderTakeOutput(brotliDecoderState, &brotliUnconsumedAmount);
641 decoderHasData = true;
642 }
643 }
644 if (bytesDecoded == maxSize)
645 return bytesDecoded;
646 Q_ASSERT(bytesDecoded < maxSize);
647
648 QByteArray input;
649 if (!compressedDataBuffer.isEmpty())
650 input = compressedDataBuffer.read();
651 const uint8_t *encodedPtr = reinterpret_cast<const uint8_t *>(input.constData());
652 size_t encodedBytesRemaining = input.size();
653
654 uint8_t *decodedPtr = reinterpret_cast<uint8_t *>(data + bytesDecoded);
655 size_t unusedDecodedSize = size_t(maxSize - bytesDecoded);
656 while (unusedDecodedSize > 0) {
657 auto previousUnusedDecodedSize = unusedDecodedSize;
658 BrotliDecoderResult result = BrotliDecoderDecompressStream(
659 brotliDecoderState, &encodedBytesRemaining, &encodedPtr, &unusedDecodedSize,
660 &decodedPtr, nullptr);
661 bytesDecoded += previousUnusedDecodedSize - unusedDecodedSize;
662
663 switch (result) {
664 case BROTLI_DECODER_RESULT_ERROR:
665 qWarning("Brotli error: %s",
666 BrotliDecoderErrorString(BrotliDecoderGetErrorCode(brotliDecoderState)));
667 return -1;
668 case BROTLI_DECODER_RESULT_SUCCESS:
669 BrotliDecoderDestroyInstance(brotliDecoderState);
670 decoderPointer = nullptr;
671 return bytesDecoded;
672 case BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:
673 if (!compressedDataBuffer.isEmpty()) {
674 input = compressedDataBuffer.read();
675 encodedPtr = reinterpret_cast<const uint8_t *>(input.constData());
676 encodedBytesRemaining = input.size();
677 break;
678 }
679 return bytesDecoded;
680 case BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:
681 // Some data is leftover inside the brotli decoder, remember for next time
682 decoderHasData = BrotliDecoderHasMoreOutput(brotliDecoderState);
683 Q_ASSERT(unusedDecodedSize == 0);
684 break;
685 }
686 }
687 if (encodedBytesRemaining) {
688 // Some input was left unused; move back to the buffer
689 input = input.right(QByteArray::size_type(encodedBytesRemaining));
690 compressedDataBuffer.prepend(input);
691 }
692 return bytesDecoded;
693#endif
694}
695
696qsizetype QDecompressHelper::readZstandard(char *data, const qsizetype maxSize)
697{
698#if !QT_CONFIG(zstd)
699 Q_UNUSED(data);
700 Q_UNUSED(maxSize);
701 Q_UNREACHABLE();
702#else
703 ZSTD_DStream *zstdStream = toZstandardPointer(decoderPointer);
704
705 QByteArray input;
706 if (!compressedDataBuffer.isEmpty())
707 input = compressedDataBuffer.read();
708 ZSTD_inBuffer inBuf { input.constData(), size_t(input.size()), 0 };
709
710 ZSTD_outBuffer outBuf { data, size_t(maxSize), 0 };
711
712 qsizetype bytesDecoded = 0;
713 while (outBuf.pos < outBuf.size && (inBuf.pos < inBuf.size || decoderHasData)) {
714 size_t retValue = ZSTD_decompressStream(zstdStream, &outBuf, &inBuf);
715 if (ZSTD_isError(retValue)) {
716 qWarning("ZStandard error: %s", ZSTD_getErrorName(retValue));
717 return -1;
718 } else {
719 decoderHasData = false;
720 bytesDecoded = outBuf.pos;
721 // if pos == size then there may be data left over in internal buffers
722 if (outBuf.pos == outBuf.size) {
723 decoderHasData = true;
724 } else if (inBuf.pos == inBuf.size && !compressedDataBuffer.isEmpty()) {
725 input = compressedDataBuffer.read();
726 inBuf = { input.constData(), size_t(input.size()), 0 };
727 }
728 }
729 }
730 if (inBuf.pos < inBuf.size) {
731 // Some input was left unused; move back to the buffer
732 input = input.mid(QByteArray::size_type(inBuf.pos));
733 compressedDataBuffer.prepend(std::move(input));
734 }
735 return bytesDecoded;
736#endif
737}
738
739QT_END_NAMESPACE
740