Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTest>
++
++ #include "qbinaryjson.h"
++
++ #include <QtCore/qendian.h>
++ #include "qjsonarray.h"
++ #include "qjsonobject.h"
++ #include "qjsonvalue.h"
++ #include "qjsondocument.h"
++ #include "qregularexpression.h"
++ #include "qscopeguard.h"
++
++ #include <limits>
++
++ using namespace Qt::StringLiterals;
++
++ #define INVALID_UNICODE "\xCE\xBA\xE1"
++ #define UNICODE_NON_CHARACTER "\xEF\xBF\xBF"
++ #define UNICODE_DJE "\320\202" // Character from the Serbian Cyrillic alphabet
++
++ class tst_QtJson: public QObject
++ {
++ Q_OBJECT
++
++ private Q_SLOTS:
++ void initTestCase();
++
++ void fromBinary();
++ void toAndFromBinary_data();
++ void toAndFromBinary();
++ void invalidBinaryData_data();
++ void invalidBinaryData();
++ void compactArray();
++ void compactObject();
++ void validation();
++ void testCompactionError();
++
++ void binaryDataOutOfBoundsRead_data();
++ void binaryDataOutOfBoundsRead();
++ void binaryDataDeepNestingArray_data();
++ void binaryDataDeepNestingArray();
++ void binaryDataDeepNestingObject_data();
++ void binaryDataDeepNestingObject();
++ private:
++ QString testDataDir;
++ };
++
++ void tst_QtJson::initTestCase()
++ {
++ testDataDir = QFileInfo(QFINDTESTDATA("test.json")).absolutePath();
++ if (testDataDir.isEmpty())
++ testDataDir = QCoreApplication::applicationDirPath();
++ }
++
++ void tst_QtJson::fromBinary()
++ {
++ QFile file(testDataDir + "/test.json");
++ QVERIFY(file.open(QFile::ReadOnly));
++ QByteArray testJson = file.readAll();
++
++ QJsonDocument doc = QJsonDocument::fromJson(testJson);
++ QJsonDocument outdoc = QBinaryJson::fromBinaryData(QBinaryJson::toBinaryData(doc));
++ QVERIFY(!outdoc.isNull());
++ QCOMPARE(doc, outdoc);
++
++ QFile bfile(testDataDir + "/test.bjson");
++ QVERIFY(bfile.open(QFile::ReadOnly));
++ QByteArray binary = bfile.readAll();
++
++ QJsonDocument bdoc = QBinaryJson::fromBinaryData(binary);
++ QVERIFY(!bdoc.isNull());
++ QCOMPARE(doc.toVariant(), bdoc.toVariant());
++ QCOMPARE(doc, bdoc);
++ }
++
++ void tst_QtJson::toAndFromBinary_data()
++ {
++ QTest::addColumn<QString>("filename");
++ QTest::newRow("test.json") << (testDataDir + "/test.json");
++ QTest::newRow("test2.json") << (testDataDir + "/test2.json");
++ }
++
++ void tst_QtJson::toAndFromBinary()
++ {
++ QFETCH(QString, filename);
++ QFile file(filename);
++ QVERIFY(file.open(QFile::ReadOnly));
++ QByteArray data = file.readAll();
++
++ QJsonDocument doc = QJsonDocument::fromJson(data);
++ QVERIFY(!doc.isNull());
++ {
++ QJsonDocument outdoc = QBinaryJson::fromBinaryData(QBinaryJson::toBinaryData(doc));
++ QVERIFY(!outdoc.isNull());
++ QCOMPARE(doc, outdoc);
++ }
++ #if QT_DEPRECATED_SINCE(6, 12)
++ {
++ int size = -1;
++ QTest::ignoreMessage(QtWarningMsg,
++ "QBinaryJson: In Qt 6, unlike Qt 5, toRawData() transfers ownership "
++ "of the pointer to the caller. Prefer toBinaryData() instead.");
++ QT_IGNORE_DEPRECATIONS(
++ auto rawData = QBinaryJson::toRawData(doc, &size);
++ )
++ // we own the returned data
++ const auto releaseRawData = qScopeGuard([rawData]() {
++ free(const_cast<char *>(rawData));
++ });
++ QVERIFY(size > 0);
++ QJsonDocument outdoc = QBinaryJson::fromRawData(rawData, size);
++ QVERIFY(!outdoc.isNull());
++ QCOMPARE(doc, outdoc);
++ }
++ #endif // QT_DEPRECATED_SINCE(6, 12)
++ }
++
++ void tst_QtJson::invalidBinaryData_data()
++ {
++ QTest::addColumn<QString>("name");
++ QDir dir(testDataDir + "/invalidBinaryData");
++ const QStringList files = dir.entryList({u"*.bjson"_s},
++ QDir::Filter::Files, QDir::SortFlag::Name);
++ for (const QString &file : files)
++ QTest::addRow("%s", QFile::encodeName(file).constData()) << file;
++ }
++
++ void tst_QtJson::invalidBinaryData()
++ {
++ QFETCH(const QString, name);
++ const QDir dir(testDataDir + "/invalidBinaryData");
++ {
++ QFile file(dir.filePath(name));
++ QVERIFY(file.open(QIODevice::ReadOnly));
++ QByteArray bytes = file.readAll();
++ bytes.squeeze();
++ QJsonDocument document = QBinaryJson::fromRawData(bytes.constData(), bytes.size());
++ QVERIFY(document.isNull());
++ }
++ }
++
++ void tst_QtJson::compactArray()
++ {
++ QJsonArray array;
++ array.append(QLatin1String("First Entry"));
++ array.append(QLatin1String("Second Entry"));
++ array.append(QLatin1String("Third Entry"));
++ QJsonDocument doc(array);
++ int s = QBinaryJson::toBinaryData(doc).size();
++ array.removeAt(1);
++ doc.setArray(array);
++ QVERIFY(s > QBinaryJson::toBinaryData(doc).size());
++ s = QBinaryJson::toBinaryData(doc).size();
++ QCOMPARE(doc.toJson(),
++ QByteArray("[\n"
++ " \"First Entry\",\n"
++ " \"Third Entry\"\n"
++ "]\n"));
++
++ array.removeAt(0);
++ doc.setArray(array);
++ QVERIFY(s > QBinaryJson::toBinaryData(doc).size());
++ s = QBinaryJson::toBinaryData(doc).size();
++ QCOMPARE(doc.toJson(),
++ QByteArray("[\n"
++ " \"Third Entry\"\n"
++ "]\n"));
++
++ array.removeAt(0);
++ doc.setArray(array);
++ QVERIFY(s > QBinaryJson::toBinaryData(doc).size());
++ s = QBinaryJson::toBinaryData(doc).size();
++ QCOMPARE(doc.toJson(),
++ QByteArray("[\n"
++ "]\n"));
++
++ }
++
++ void tst_QtJson::compactObject()
++ {
++ QJsonObject object;
++ object.insert(QLatin1String("Key1"), QLatin1String("First Entry"));
++ object.insert(QLatin1String("Key2"), QLatin1String("Second Entry"));
++ object.insert(QLatin1String("Key3"), QLatin1String("Third Entry"));
++ QJsonDocument doc(object);
++ int s = QBinaryJson::toBinaryData(doc).size();
++ object.remove(QLatin1String("Key2"));
++ doc.setObject(object);
++ QVERIFY(s > QBinaryJson::toBinaryData(doc).size());
++ s = QBinaryJson::toBinaryData(doc).size();
++ QCOMPARE(doc.toJson(),
++ QByteArray("{\n"
++ " \"Key1\": \"First Entry\",\n"
++ " \"Key3\": \"Third Entry\"\n"
++ "}\n"));
++
++ object.remove(QLatin1String("Key1"));
++ doc.setObject(object);
++ QVERIFY(s > QBinaryJson::toBinaryData(doc).size());
++ s = QBinaryJson::toBinaryData(doc).size();
++ QCOMPARE(doc.toJson(),
++ QByteArray("{\n"
++ " \"Key3\": \"Third Entry\"\n"
++ "}\n"));
++
++ object.remove(QLatin1String("Key3"));
++ doc.setObject(object);
++ QVERIFY(s > QBinaryJson::toBinaryData(doc).size());
++ s = QBinaryJson::toBinaryData(doc).size();
++ QCOMPARE(doc.toJson(),
++ QByteArray("{\n"
++ "}\n"));
++
++ }
++
++ void tst_QtJson::validation()
++ {
++ // this basically tests that we don't crash on corrupt data
++ QFile file(testDataDir + "/test.json");
++ QVERIFY(file.open(QFile::ReadOnly));
++ QByteArray testJson = file.readAll();
++ QVERIFY(!testJson.isEmpty());
++
++ QJsonDocument doc = QJsonDocument::fromJson(testJson);
++ QVERIFY(!doc.isNull());
++
++ QByteArray binary = QBinaryJson::toBinaryData(doc);
++
++ // only test the first 1000 bytes. Testing the full file takes too long
++ for (int i = 0; i < 1000; ++i) {
++ QByteArray corrupted = binary;
++ corrupted[i] = char(0xff);
++ QJsonDocument doc = QBinaryJson::fromBinaryData(corrupted);
++ if (doc.isNull())
++ continue;
++ QByteArray json = doc.toJson();
++ }
++
++
++ QFile file2(testDataDir + "/test3.json");
++ QVERIFY(file2.open(QFile::ReadOnly));
++ testJson = file2.readAll();
++ QVERIFY(!testJson.isEmpty());
++
++ doc = QJsonDocument::fromJson(testJson);
++ QVERIFY(!doc.isNull());
++
++ binary = QBinaryJson::toBinaryData(doc);
++
++ for (int i = 0; i < binary.size(); ++i) {
++ QByteArray corrupted = binary;
++ corrupted[i] = char(0xff);
++ QJsonDocument doc = QBinaryJson::fromBinaryData(corrupted);
++ if (doc.isNull())
++ continue;
++ QByteArray json = doc.toJson();
++
++ corrupted = binary;
++ corrupted[i] = 0x00;
++ doc = QBinaryJson::fromBinaryData(corrupted);
++ if (doc.isNull())
++ continue;
++ json = doc.toJson();
++ }
++ }
++
++ void tst_QtJson::testCompactionError()
++ {
++ QJsonObject schemaObject;
++ schemaObject.insert("_Type", QLatin1String("_SchemaType"));
++ schemaObject.insert("name", QLatin1String("Address"));
++ schemaObject.insert("schema", QJsonObject());
++ {
++ QJsonObject content(schemaObject);
++ QJsonDocument doc(content);
++ QVERIFY(!doc.isNull());
++ QByteArray hash = QCryptographicHash::hash(QBinaryJson::toBinaryData(doc), QCryptographicHash::Md5).toHex();
++ schemaObject.insert("_Version", QString::fromLatin1(hash.constData(), hash.size()));
++ }
++
++ QJsonObject schema;
++ schema.insert("streetNumber", schema.value("number").toObject());
++ schemaObject.insert("schema", schema);
++ {
++ QJsonObject content(schemaObject);
++ content.remove("_Uuid");
++ content.remove("_Version");
++ QJsonDocument doc(content);
++ QVERIFY(!doc.isNull());
++ QByteArray hash = QCryptographicHash::hash(QBinaryJson::toBinaryData(doc), QCryptographicHash::Md5).toHex();
++ schemaObject.insert("_Version", QString::fromLatin1(hash.constData(), hash.size()));
++ }
++ }
++
++ enum class CallType : quint8
++ {
++ Binary,
++ Raw,
++ };
++
++ void tst_QtJson::binaryDataOutOfBoundsRead_data()
++ {
++ QTest::addColumn<quint32>("corruptedSize");
++ QTest::addColumn<int>("corruptedTableOffset");
++ QTest::addColumn<CallType>("callType");
++
++ // The case with 0xFFFFFFF4 should be handled properly, because the
++ // calculation does not roundtrip
++ QTest::newRow("0xFFFFFFF4_128_binary") << 0xFFFFFFF4u << 128 << CallType::Binary;
++ QTest::newRow("0xFFFFFFF8_32_binary") << 0xFFFFFFF8u << 32 << CallType::Binary;
++ QTest::newRow("0xFFFFFFFC_512_binary") << 0xFFFFFFFCu << 512 << CallType::Binary;
++ QTest::newRow("0xFFFFFFFF_4088_binary") << 0xFFFFFFFFu<< 4088 << CallType::Binary;
++
++ QTest::newRow("0xFFFFFFF4_128_raw") << 0xFFFFFFF4u << 128 << CallType::Raw;
++ QTest::newRow("0xFFFFFFF8_32_raw") << 0xFFFFFFF8u << 32 << CallType::Raw;
++ QTest::newRow("0xFFFFFFFC_512_raw") << 0xFFFFFFFCu << 512 << CallType::Raw;
++ QTest::newRow("0xFFFFFFFF_4088_raw") << 0xFFFFFFFFu<< 4088 << CallType::Raw;
++ }
++
++ void tst_QtJson::binaryDataOutOfBoundsRead()
++ {
++ QFETCH(const quint32, corruptedSize);
++ QFETCH(const int, corruptedTableOffset);
++ QFETCH(const CallType, callType);
++
++ // Two single element arrays whose value is small enough to be stored inline
++ // in the root's value table: Value packs a 27 bit signed int, and
++ // Value::isValid() accepts an inline int without checking any offset. Both
++ // therefore encode to Header(8) + Base(12) + one 4 byte Value = 24 bytes
++ // with no out-of-line data, so moving the table moves the value with it.
++ const QJsonDocument original = QJsonDocument::fromJson("[1]");
++ const QJsonDocument planted = QJsonDocument::fromJson("[424242]");
++ QVERIFY(!original.isNull());
++ QVERIFY(!planted.isNull());
++
++ const QByteArray originalBlob = QBinaryJson::toBinaryData(original);
++ const QByteArray plantedBlob = QBinaryJson::toBinaryData(planted);
++
++ const qsizetype originalBlobSize = originalBlob.size();
++
++ constexpr qsizetype HeaderSize = 8; // Header { qle_uint tag; qle_uint version; }
++ constexpr qsizetype RootSizeOffset = HeaderSize; // Base::size
++ constexpr qsizetype TableOffsetOffset = HeaderSize + 8; // Base::tableOffset
++
++ // A backing allocation much larger than the buffer the parser will be
++ // given. Everything past the first originalBlobSize bytes is memory that
++ // a correct parser must never touch; planting known bytes there is what
++ // makes the out-of-bounds read observable rather than merely undefined.
++ // Pointing tableOffset past a tightly sized allocation instead would be a
++ // genuine heap-buffer-overflow, but it would only be diagnosed under a
++ // sanitizer and would otherwise crash the test run.
++ QByteArray buffer(4096, '\0');
++ Q_ASSERT(corruptedTableOffset + qsizetype(sizeof(quint32)) <= buffer.size());
++ // Copy the original blob at the beginning
++ memcpy(buffer.data(), originalBlob.constData(), originalBlobSize);
++
++ // Copy the other document's 4 byte table entry out of bounds.
++ const quint32 plantedBlobTableOffset =
++ qFromLittleEndian<quint32>(plantedBlob.constData() + TableOffsetOffset);
++ memcpy(buffer.data() + corruptedTableOffset,
++ plantedBlob.constData() + HeaderSize + plantedBlobTableOffset,
++ sizeof(quint32));
++
++ // fromBinaryData() computes the document bound as
++ // const uint size = sizeof(Header) + root.size;
++ // sizeof() is 64 bit while root.size is a 32 bit field taken straight from
++ // the input, so the sum is evaluated in 64 bits and then truncated on
++ // assignment to uint. Every root.size in [0xfffffff8, 0xffffffff] collapses
++ // to 0..7 and trivially passes the "size > uint(data.size())" check that is
++ // supposed to keep the root inside the buffer. ConstData::isValid() then
++ // recomputes maxSize as "alloc - sizeof(Header)", which underflows straight
++ // back to root.size, so the root ends up being validated against its own
++ // attacker-supplied length instead of against the real buffer.
++ //
++ // Once that happens nothing constrains tableOffset either: the guard
++ // "tableOffset + length() * sizeof(offset) > size" now compares against
++ // root.size instead of against the buffer, so both Array::isValid() and
++ // Array::toJsonArray() read the value table from 32 bytes past the end.
++
++ // Modify the size of the original blob
++ qToLittleEndian(corruptedSize, buffer.data() + RootSizeOffset);
++ // Modify the tableOffset to point to a newly-injected table
++ qToLittleEndian(quint32(corruptedTableOffset - HeaderSize),
++ buffer.data() + TableOffsetOffset);
++
++ // Hand the parser a view over the first origianlBlobSize bytes only.
++ // That size is the only bound it is given, so anything it reads beyond it
++ // is out of bounds.
++ const QByteArray nonOwningArray =
++ QByteArray::fromRawData(buffer.constData(), originalBlobSize);
++ QCOMPARE(nonOwningArray.size(), originalBlobSize);
++
++ // The code below passes a correct size to fromRawData() call.
++ // Surely we could observe the same problem, if we passed corruptedSize
++ // instead, but that's a typical problem for all APIs that take a pointer
++ // and a size.
++ const QJsonDocument doc = (callType == CallType::Binary)
++ ? QBinaryJson::fromBinaryData(nonOwningArray)
++ : QBinaryJson::fromRawData(nonOwningArray.constData(), int(nonOwningArray.size()));
++ if (!doc.isNull()) {
++ QFAIL(qPrintable(QString::asprintf(
++ "fromBinaryData() was handed %lld bytes but read the root array's value "
++ "table from offset %lld: it returned %s, which was planted out of bounds, "
++ "instead of rejecting the document",
++ qlonglong(nonOwningArray.size()), qlonglong(corruptedTableOffset),
++ doc.toJson(QJsonDocument::Compact).simplified().constData())));
++ }
++
++ // Check that the original blob can still be parsed correctly
++ QCOMPARE(QBinaryJson::fromBinaryData(originalBlob), original);
++ }
++
++ // Builds a binary JSON document made of `depth` arrays nested directly inside
++ // one another, the innermost one empty. The format lays the Bases out
++ // consecutively from the outermost inwards, followed by the one-entry value
++ // tables in the opposite order:
++ //
++ // [Header][Base 0][Base 1]...[Base d-1][table d-2]...[table 0]
++ //
++ // so every extra level costs exactly sizeof(Base) + sizeof(offset) = 16 bytes.
++ // This has to be written by hand: going through QJsonArray instead would
++ // recurse in the Qt API long before the code under test got a chance to.
++ static QByteArray nestedArrayBinaryJson(int depth)
++ {
++ Q_ASSERT(depth >= 1);
++
++ constexpr qsizetype HeaderSize = 8; // Header { qle_uint tag; qle_uint version; }
++ constexpr qsizetype BaseSize = 12; // Base { size; isObjectAndLength; tableOffset; }
++ constexpr qsizetype EntrySize = 4; // one Value
++
++ // Borrow a real document's header rather than spelling out the format tag.
++ // The document has to be non-empty.
++ const QByteArray header =
++ QBinaryJson::toBinaryData(QJsonDocument::fromJson("[0]")).left(HeaderSize);
++ Q_ASSERT(header.size() == HeaderSize);
++
++ QByteArray blob(HeaderSize + BaseSize * depth + EntrySize * (depth - 1), '\0');
++ memcpy(blob.data(), header.constData(), HeaderSize);
++
++ char *const root = blob.data() + HeaderSize;
++ const qsizetype tablesStart = BaseSize * depth;
++
++ for (int i = 0; i < depth; ++i) {
++ char *const base = root + BaseSize * i;
++ const bool innermost = (i == depth - 1);
++
++ // Everything belonging to this array: its own Base, every level nested
++ // below it, and their tables.
++ const quint32 size = quint32(BaseSize + 16 * (depth - i - 1));
++ // isObject is bit 0 -- zero for an array -- and length is bits 1..31.
++ const quint32 isObjectAndLength = innermost ? 0u : (1u << 1);
++ const quint32 tableOffset = innermost
++ ? quint32(BaseSize)
++ : quint32(tablesStart + EntrySize * (depth - 2 - i) - BaseSize * i);
++
++ qToLittleEndian(size, base);
++ qToLittleEndian(isObjectAndLength, base + 4);
++ qToLittleEndian(tableOffset, base + 8);
++
++ if (!innermost) {
++ // A Value naming the next Base, 12 bytes further in: the type goes
++ // in bits 0..2 and the payload offset in bits 5..31.
++ const quint32 entry = quint32(QJsonValue::Array) | (quint32(BaseSize) << 5);
++ qToLittleEndian(entry, base + tableOffset);
++ }
++ }
++
++ return blob;
++ }
++
++ void tst_QtJson::binaryDataDeepNestingArray_data()
++ {
++ QTest::addColumn<QBinaryJson::DataValidation>("mode");
++ QTest::addColumn<int>("depth");
++
++ QTest::newRow("validate_4096")
++ << QBinaryJson::Validate << 4096;
++ QTest::newRow("bypass_validation_4096")
++ << QBinaryJson::BypassValidation << 4096;
++ }
++
++ void tst_QtJson::binaryDataDeepNestingArray()
++ {
++ QFETCH(const QBinaryJson::DataValidation, mode);
++ QFETCH(const int, depth);
++
++ // nestedArrayBinaryJson() is hand-rolled, so check it really does produce a
++ // well formed document at a depth that is obviously fine. Without this the
++ // deep case below could pass for the wrong reason: a malformed blob that is
++ // rejected on its first field never recurses at all.
++ QCOMPARE(QBinaryJson::fromBinaryData(nestedArrayBinaryJson(3), mode),
++ QJsonDocument::fromJson("[[[]]]"));
++
++ // Depending on the mode, this checks either the validation path, or the
++ // JSON construction path. Both could recurse and cause stack overflow with
++ // deeply nested documents.
++ const QJsonDocument doc = QBinaryJson::fromBinaryData(nestedArrayBinaryJson(depth), mode);
++ QVERIFY(!doc.isNull());
++ QVERIFY(doc.isArray());
++
++ QJsonArray arr = doc.array();
++ // We have depth-1 nested arrays
++ for (int i = 0; i < depth - 1; ++i) {
++ QVERIFY(!arr.isEmpty());
++ QJsonValue val = arr.first();
++ QVERIFY(val.isArray());
++ arr = val.toArray();
++ }
++ // And the last array is empty
++ QVERIFY(arr.isEmpty());
++ }
++
++ // The object counterpart of nestedArrayBinaryJson(): `depth` objects nested
++ // directly inside one another, each holding the single key "a", the innermost
++ // one empty -- {"a":{"a":...{}}}.
++ //
++ // Objects do not store their values in the table the way arrays do. The table
++ // holds offsets to Entry records, and an Entry is a Value followed by its key,
++ // so a level costs Base(12) + Entry(8) + table(4) = 24 bytes rather than 16.
++ // Relative to each Base the layout is
++ //
++ // [Base][child subtree][Entry][table]
++ //
++ // which satisfies Object::isValid(): the Entry sits immediately before the
++ // table, so the "table()[i] + sizeof(Entry) >= tableOffset" guard passes and
++ // Entry::isValid() is handed exactly the 8 bytes the Entry occupies.
++ static QByteArray nestedObjectBinaryJson(int depth)
++ {
++ Q_ASSERT(depth >= 1);
++
++ constexpr qsizetype HeaderSize = 8; // Header { qle_uint tag; qle_uint version; }
++ constexpr qsizetype BaseSize = 12; // Base { size; isObjectAndLength; tableOffset; }
++ constexpr qsizetype EntrySize = 8; // Value(4) + latin1 key "a" (2 + 1), padded to 4
++ constexpr qsizetype TableSize = 4; // one offset
++ constexpr qsizetype LevelSize = BaseSize + EntrySize + TableSize;
++
++ const QByteArray header =
++ QBinaryJson::toBinaryData(QJsonDocument::fromJson("[0]")).left(HeaderSize);
++ Q_ASSERT(header.size() == HeaderSize);
++
++ // Bytes belonging to the object at level i: its own Base, every level
++ // nested below it, and their Entries and tables.
++ const auto subtreeSize = [depth](int i) {
++ return quint32(BaseSize + LevelSize * (depth - 1 - i));
++ };
++
++ QByteArray blob(HeaderSize + subtreeSize(0), '\0');
++ memcpy(blob.data(), header.constData(), HeaderSize);
++
++ char *const root = blob.data() + HeaderSize;
++
++ for (int i = 0; i < depth; ++i) {
++ char *const base = root + BaseSize * i;
++ const bool innermost = (i == depth - 1);
++
++ const quint32 size = subtreeSize(i);
++ // isObject is bit 0 -- one for an object -- and length is bits 1..31.
++ const quint32 isObjectAndLength = innermost ? 1u : (1u | (1u << 1));
++ const quint32 entryOffset =
++ innermost ? 0u : quint32(BaseSize) + subtreeSize(i + 1);
++ const quint32 tableOffset =
++ innermost ? quint32(BaseSize) : entryOffset + quint32(EntrySize);
++
++ qToLittleEndian(size, base);
++ qToLittleEndian(isObjectAndLength, base + 4);
++ qToLittleEndian(tableOffset, base + 8);
++
++ if (innermost)
++ continue;
++
++ // The table holds one offset, naming the Entry.
++ qToLittleEndian(entryOffset, base + tableOffset);
++
++ // Entry: a Value of type Object pointing at the next Base 12 bytes
++ // further in, with the latin1-key bit set, followed by the key stored
++ // as Latin1String::Data { qle_ushort length; char latin1[1]; }.
++ constexpr quint32 LatinKeyBit = 1u << 4;
++ const quint32 value = quint32(QJsonValue::Object) | LatinKeyBit
++ | (quint32(BaseSize) << 5);
++ qToLittleEndian(value, base + entryOffset);
++ qToLittleEndian(quint16(1), base + entryOffset + 4);
++ base[entryOffset + 6] = 'a';
++ }
++
++ return blob;
++ }
++
++ void tst_QtJson::binaryDataDeepNestingObject_data()
++ {
++ binaryDataDeepNestingArray_data();
++ }
++
++ void tst_QtJson::binaryDataDeepNestingObject()
++ {
++ QFETCH(const QBinaryJson::DataValidation, mode);
++ QFETCH(const int, depth);
++
++ QCOMPARE(QBinaryJson::fromBinaryData(nestedObjectBinaryJson(3), mode),
++ QJsonDocument::fromJson(R"({"a":{"a":{}}})"));
++
++ // Similarly to the array case, this could recurse either in validation
++ // or JSON construction path, potentially causing stack overflow.
++ const QJsonDocument doc = QBinaryJson::fromBinaryData(nestedObjectBinaryJson(depth), mode);
++ QVERIFY(!doc.isNull());
++ QVERIFY(doc.isObject());
++
++ QJsonObject obj = doc.object();
++ // We have depth-1 objects with key "a"...
++ for (int i = 0; i < depth - 1; ++i) {
++ QVERIFY(obj.contains("a"));
++ QJsonValue val = obj.value("a");
++ QVERIFY(val.isObject());
++ obj = val.toObject();
++ }
++ // And the last one is an empty object
++ QVERIFY(obj.isEmpty());
++ }
++
++ QTEST_MAIN(tst_QtJson)
++ #include "tst_qtjson.moc"