QCborStreamReader: add toString() and toByteArray()

They've been a long time coming. I had them in TinyCBOR before the
chunked reading; in fact, I added the chunked reading specifically for
Qt's CBOR support.

[ChangeLog][QtCore][QCborStreamReader] Added toString() and
toByteArray(), which read a full string whether it is chunked or
not. They also guard against attempting to allocate more memory than the
underlying stream could provide.

Change-Id: Icfe44ecf285a480fafe4fffd174c5815f153d5b5
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Thiago Macieira 2023-03-14 10:06:52 -07:00
parent 8e8815b688
commit 8af346c1f6
3 changed files with 178 additions and 2 deletions

View File

@ -630,6 +630,7 @@ public:
};
static QCborStreamReader::StringResultCode appendStringChunk(QCborStreamReader &reader, QByteArray *data);
bool readFullString(ReadStringChunk params);
QCborStreamReader::StringResult<qsizetype> readStringChunk(ReadStringChunk params);
qsizetype readStringChunk_byte(ReadStringChunk params, qsizetype len);
qsizetype readStringChunk_unicode(ReadStringChunk params, qsizetype utf8len);
@ -1295,11 +1296,15 @@ bool QCborStreamReader::leaveContainer()
\snippet code/src_corelib_serialization_qcborstream.cpp 27
The toString() function implements the above loop and some extra checks.
//! [string-no-type-conversions]
This function does not perform any type conversions, including from integers
or from byte arrays. Therefore, it may only be called if isString() returned
true; calling it in any other condition is an error.
//! [string-no-type-conversions]
\sa readByteArray(), isString(), readStringChunk()
\sa toString(), readByteArray(), isString(), readStringChunk()
*/
QCborStreamReader::StringResult<QString> QCborStreamReader::_readString_helper()
{
@ -1327,11 +1332,15 @@ QCborStreamReader::StringResult<QString> QCborStreamReader::_readString_helper()
\snippet code/src_corelib_serialization_qcborstream.cpp 28
The toByteArray() function implements the above loop and some extra checks.
//! [bytearray-no-type-conversions]
This function does not perform any type conversions, including from integers
or from strings. Therefore, it may only be called if isByteArray() is true;
calling it in any other condition is an error.
//! [bytearray-no-type-conversions]
\sa readString(), isByteArray(), readStringChunk()
\sa toByteArray(), readString(), isByteArray(), readStringChunk()
*/
QCborStreamReader::StringResult<QByteArray> QCborStreamReader::_readByteArray_helper()
{
@ -1380,6 +1389,108 @@ qsizetype QCborStreamReader::_currentStringChunkSize() const
return -1;
}
bool QCborStreamReaderPrivate::readFullString(ReadStringChunk params)
{
auto r = readStringChunk(params);
while (r.status == QCborStreamReader::Ok) {
// keep appending
r = readStringChunk(params);
}
bool ok = r.status == QCborStreamReader::EndOfString;
Q_ASSERT(ok == !lastError);
return ok;
}
/*!
\fn QCborStreamReader::toString()
\since 6.7
Decodes the current text string and returns it. If the string is chunked,
this function will iterate over all chunks and concatenate them. If an
error happens, this function returns a default-constructed QString(), but
that may not be distinguishable from certain empty text strings. Instead,
check lastError() to determine if an error has happened.
\include qcborstreamreader.cpp string-no-type-conversions
//! [note-not-restartable]
\note This function cannot be resumed. That is, this function should not
be used in contexts where the CBOR data may still be received, for example
from a socket or pipe. It should only be used when the full data has
already been received and is available in the input QByteArray or
QIODevice.
//! [note-not-restartable]
\sa readString(), readStringChunk(), isString(), toByteArray()
*/
/*!
\fn QCborStreamReader::toString(QString &dst)
\overload
\since 6.7
Decodes the current text string and appends to \a dst. If the string is
chunked, this function will iterate over all chunks and concatenate them.
If an error happens during decoding, other chunks that could be decoded
successfully may have been written to \a dst nonetheless. Returns \c true
if the decoding happened without errors, \c false otherwise.
\include qcborstreamreader.cpp string-no-type-conversions
\include qcborstreamreader.cpp note-not-restartable
\sa readString(), readStringChunk(), isString(), toByteArray()
*/
bool QCborStreamReader::_toString_helper(QString &dst)
{
bool ok = d->readFullString(&dst);
if (ok)
preparse();
return ok;
}
/*!
\fn QCborStreamReader::toByteArray()
\since 6.7
Decodes the current byte string and returns it. If the string is chunked,
this function will iterate over all chunks and concatenate them. If an
error happens, this function returns a default-constructed QByteArray(),
but that may not be distinguishable from certain empty byte strings.
Instead, check lastError() to determine if an error has happened.
\include qcborstreamreader.cpp bytearray-no-type-conversions
\include qcborstreamreader.cpp note-not-restartable
\sa readByteArray(), readStringChunk(), isByteArray(), toString()
*/
/*!
\fn QCborStreamReader::toByteArray(QByteArray &dst)
\overload
\since 6.7
Decodes the current byte string and appends to \a dst. If the string is
chunked, this function will iterate over all chunks and concatenate them.
If an error happens during decoding, other chunks that could be decoded
successfully may have been written to \a dst nonetheless. Returns \c true
if the decoding happened without errors, \c false otherwise.
\include qcborstreamreader.cpp bytearray-no-type-conversions
\include qcborstreamreader.cpp note-not-restartable
\sa readByteArray(), readStringChunk(), isByteArray(), toString()
*/
bool QCborStreamReader::_toByteArray_helper(QByteArray &dst)
{
bool ok = d->readFullString(&dst);
if (ok)
preparse();
return ok;
}
/*!
Reads the current string chunk into the buffer pointed to by \a ptr, whose
size is \a maxlen. This function returns a \l StringResult object, with the
@ -1452,6 +1563,12 @@ QCborStreamReaderPrivate::readStringChunk(ReadStringChunk params)
// qt_cbor_decoder_transfer_string() enforces that
// QIODevice::bytesAvailable() be bigger than the amount we're about to
// read.
//
// This is an important security gate: if the CBOR stream is corrupt or
// malicious, and has an impossibly large string size, we only go past it
// if the transfer to the destination buffer will succeed (modulo QIODevice
// I/O failures).
#if 1
// Using internal TinyCBOR API!
err = _cbor_value_get_string_chunk(&currentElement, &content, &len, &currentElement);

View File

@ -120,6 +120,8 @@ public:
bool enterContainer() { Q_ASSERT(isContainer()); return _enterContainer_helper(); }
bool leaveContainer();
bool toString(QString &dst) { Q_ASSERT(isString()); return _toString_helper(dst); }
bool toByteArray(QByteArray &dst) { Q_ASSERT(isByteArray()); return _toByteArray_helper(dst); }
StringResult<QString> readString() { Q_ASSERT(isString()); return _readString_helper(); }
StringResult<QByteArray> readByteArray(){ Q_ASSERT(isByteArray()); return _readByteArray_helper(); }
qsizetype currentStringChunkSize() const{ Q_ASSERT(isString() || isByteArray()); return _currentStringChunkSize(); }
@ -142,6 +144,20 @@ public:
return -v - 1;
return v;
}
QString toString()
{
QString dst;
if (!toString(dst))
dst.clear();
return dst;
}
QByteArray toByteArray()
{
QByteArray dst;
if (!toByteArray(dst))
dst.clear();
return dst;
}
private:
void preparse();
@ -149,6 +165,8 @@ private:
StringResult<QString> _readString_helper();
StringResult<QByteArray> _readByteArray_helper();
qsizetype _currentStringChunkSize() const;
bool _toString_helper(QString &);
bool _toByteArray_helper(QByteArray &);
template <typename FP> FP _toFloatingPoint() const noexcept
{

View File

@ -657,6 +657,7 @@ void tst_QCborStreamReader::strings()
QCOMPARE(reader.currentStringChunkSize(), qsizetype(reader.length()));
int chunks = 0;
QByteArray fullString;
forever {
QCborStreamReader::StringResult<QByteArray> controlData;
if (reader.isString()) {
@ -667,6 +668,7 @@ void tst_QCborStreamReader::strings()
controlData = controlReader.readByteArray();
}
QVERIFY(controlData.status != QCborStreamReader::Error);
fullString += controlData.data;
for (int i = 0; i < 10; ++i) {
// this call must work several times with the same result
@ -689,6 +691,22 @@ void tst_QCborStreamReader::strings()
if (!isChunked)
QCOMPARE(chunks, 1);
// Now re-do and compare with toString() and toByteArray(), against
// the control data we calculated above
reader.reset();
QVERIFY(reader.isString() || reader.isByteArray());
if (reader.isByteArray()) {
QByteArray prefix("some prefix");
QByteArray ba = prefix;
QVERIFY(reader.toByteArray(ba));
QCOMPARE(ba, prefix + fullString);
} else {
QString prefix("some prefix");
QString str = prefix;
QVERIFY(reader.toString(str));
QCOMPARE(str, prefix + QString::fromUtf8(fullString));
}
}
void tst_QCborStreamReader::tags_data()
@ -908,6 +926,29 @@ void tst_QCborStreamReader::validation()
reader.reset();
QVERIFY(!reader.next());
QCOMPARE(reader.lastError(), error);
// check toString() and toByteArray() too
if (reader.isString() || reader.isByteArray()) {
reader.reset();
if (reader.isString()) {
QString prefix = "some prefix";
QString str = prefix;
QVERIFY(!reader.toString(str));
QVERIFY(str.startsWith(prefix)); // but may have decoded some
} else if (reader.isByteArray()) {
QByteArray prefix = "some prefix";
QByteArray ba = prefix;
QVERIFY(!reader.toByteArray(ba));
QVERIFY(ba.startsWith(prefix)); // but may have decoded some
}
QCOMPARE(reader.lastError(), error);
reader.reset();
if (reader.isString())
QVERIFY(reader.toString().isNull());
else
QVERIFY(reader.toByteArray().isNull());
}
}
void tst_QCborStreamReader::hugeDeviceValidation_data()