Revision control
++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtJsonRpc/private/qjsonrpctransport_p.h>
++ #include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
++ #include <QtJsonRpc/private/qhttpmessagestreamparser_p.h>
++
++ #include <QtCore/qjsonarray.h>
++ #include <QtCore/qjsonobject.h>
++ #include <QtCore/qobject.h>
++ #include <QtCore/qstring.h>
++ #include <QtCore/qtimer.h>
++ #include <QtTest/qtest.h>
++
++ #include <queue>
++
++ QT_BEGIN_NAMESPACE
++
++ class EchoTransport : public QJsonRpcTransport
++ {
++ public:
++ using EchoHandler = std::function<void(const QByteArray &)>;
++
++ void sendMessage(const QJsonDocument &message) final;
++ void receiveData(const QByteArray &bytes) final;
++
++ void setEchoHandler(const EchoHandler &handler) { m_messageHandler = handler; }
++
++ private:
++ EchoHandler m_messageHandler;
++ };
++
++ struct ScopedConnection
++ {
++ Q_DISABLE_COPY_MOVE(ScopedConnection);
++ ScopedConnection(QMetaObject::Connection connection);
++ ~ScopedConnection();
++
++ QMetaObject::Connection connection;
++ };
++
++ class SumHandler : public QJsonRpcProtocol::MessageHandler
++ {
++ public:
++ void handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler) final;
++
++ private:
++ static QJsonRpcProtocol::Response plus(const QJsonRpcProtocol::Request &request);
++ };
++
++ class SubtractHandler : public QJsonRpcProtocol::MessageHandler
++ {
++ public:
++ void handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler) final;
++
++ private:
++ static QJsonRpcProtocol::Response minus(const QJsonRpcProtocol::Request &request);
++ };
++
++ class UpdateHandler : public QJsonRpcProtocol::MessageHandler
++ {
++ public:
++ void handleNotification(const QJsonRpcProtocol::Notification ¬ification) final;
++
++ QJsonValue lastUpdate = QJsonValue::Undefined;
++ int numUpdates = 0;
++ };
++
++ class GetDataHandler : public QJsonRpcProtocol::MessageHandler
++ {
++ public:
++ void handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler) final;
++ };
++
++ class tst_QJsonRpcProtocol : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ tst_QJsonRpcProtocol();
++
++ private slots:
++ void specRequests_data();
++ void specRequests();
++
++ void specNotifications_data();
++ void specNotifications();
++
++ void clientRequests_data();
++ void clientRequests();
++
++ void clientNotifications_data();
++ void clientNotifications();
++
++ void testHttpMessagesSplits_data();
++ void testHttpMessagesSplits();
++
++ void testHttpMessagesBrokenInputs_data();
++ void testHttpMessagesBrokenInputs();
++
++ void badResponses();
++
++ private:
++ EchoTransport transport;
++ QJsonRpcProtocol protocol;
++ };
++
++ tst_QJsonRpcProtocol::tst_QJsonRpcProtocol()
++ {
++ protocol.setTransport(&transport);
++ protocol.setMessageHandler("sum", new SumHandler);
++ protocol.setMessageHandler("subtract", new SubtractHandler);
++ protocol.setMessageHandler("update", new UpdateHandler);
++ protocol.setMessageHandler("get_data", new GetDataHandler);
++ protocol.setMessageHandler("notify_hello", new UpdateHandler);
++ }
++
++ void tst_QJsonRpcProtocol::specRequests_data()
++ {
++ QTest::addColumn<QByteArray>("request");
++ QTest::addColumn<QByteArray>("response");
++
++ QTest::newRow("rpc call with positional parameters 1")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "subtract",
++ "params": [42, 23], "id": 1})")
++ << QByteArray(R"({"jsonrpc": "2.0", "result": 19, "id": 1})");
++ QTest::newRow("rpc call with positional parameters 2")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "subtract",
++ "params": [23, 42], "id": 2})")
++ << QByteArray(R"({"jsonrpc": "2.0", "result": -19, "id": 2})");
++
++ QTest::newRow("rpc call with named parameters 1")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "subtract",
++ "params": {"subtrahend": 23, "minuend": 42},
++ "id": 3})")
++ << QByteArray(R"({"jsonrpc": "2.0", "result": 19, "id": 3})");
++ QTest::newRow("rpc call with named parameters 2")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "subtract",
++ "params": {"minuend": 42, "subtrahend": 23},
++ "id": 4})")
++ << QByteArray(R"({"jsonrpc": "2.0", "result": 19, "id": 4})");
++
++ QTest::newRow("rpc call of non-existent method")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "foobar", "id": "1"})")
++ << QByteArray(R"({"jsonrpc": "2.0", "error": {"code": -32601,
++ "message": "Method not found"},
++ "id": "1"})");
++
++ QTest::newRow("rpc call with invalid JSON")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz])")
++ << QByteArray(R"({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"},
++ "id": null})");
++
++ QTest::newRow("rpc call with invalid Request object")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": 1, "params": "bar"})")
++ << QByteArray(R"({"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null})");
++
++ QTest::newRow("rpc call Batch, invalid JSON")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
++ {"jsonrpc": "2.0", "method" ])")
++ << QByteArray(R"({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"},
++ "id": null})");
++
++ QTest::newRow("rpc call with an empty Array")
++ << QByteArray("[]") << QByteArray(R"({"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null})");
++
++ QTest::newRow("rpc call with an invalid Batch (but not empty)")
++ << QByteArray("[1]") << QByteArray(R"([ {"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null} ])");
++
++ QTest::newRow("rpc call with invalid Batch")
++ << QByteArray("[1,2,3]") << QByteArray(R"([ {"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null},
++ {"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null},
++ {"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null} ])");
++
++ QTest::newRow("rpc call Batch")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
++ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
++ {"jsonrpc": "2.0", "method": "subtract",
++ "params": [42,23], "id": "2"},
++ {"foo": "boo"},
++ {"jsonrpc": "2.0", "method": "foo.get",
++ "params": {"name": "myself"}, "id": "5"},
++ {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ])")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "result": 7, "id": "1"},
++ {"jsonrpc": "2.0", "result": 19, "id": "2"},
++ {"jsonrpc": "2.0", "error": {"code": -32600,
++ "message": "Invalid Request"},
++ "id": null},
++ {"jsonrpc": "2.0", "error": {"code": -32601,
++ "message": "Method not found"},
++ "id": "5"},
++ {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} ])");
++ QTest::newRow("rpc call Batch (all valid)")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
++ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
++ {"jsonrpc": "2.0", "method": "subtract",
++ "params": [42,23], "id": "2"},
++ {"jsonrpc": "2.0", "method": "foo.get",
++ "params": {"name": "myself"}, "id": "5"},
++ {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ])")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "result": 7, "id": "1"},
++ {"jsonrpc": "2.0", "result": 19, "id": "2"},
++ {"jsonrpc": "2.0", "error": {"code": -32601,
++ "message": "Method not found"}, "id": "5"},
++ {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"} ])");
++
++ QTest::newRow("rpc call Batch double free")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "method": "update", "id": "10"} ])")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "error": {"code": -32601,
++ "message": "Method not found"},
++ "id": "10"} ])");
++ QTest::newRow("rpc call Batch use-after-free")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "method": "update", "id": "11"},
++ {"jsonrpc": "2.0", "method": "get_data", "id": "12"} ])")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "error": {"code": -32601,
++ "message": "Method not found"},
++ "id": "11"},
++ {"jsonrpc": "2.0", "result": ["hello", 5], "id": "12"} ])");
++ }
++
++ static bool operator<(const QJsonValue &a, const QJsonValue &b)
++ {
++ if (a.type() != b.type())
++ return a.type() < b.type();
++
++ QJsonValue idA = a.isObject() ? a.toObject().value("id") : QJsonValue::Undefined;
++ QJsonValue idB = b.isObject() ? b.toObject().value("id") : QJsonValue::Undefined;
++
++ if (idA.type() != idB.type())
++ return idA.type() < idB.type();
++
++ switch (idA.type()) {
++ // If you have multiple null ids, that won't work. But we don't, for now.
++ case QJsonValue::Null:
++ return false;
++
++ case QJsonValue::Bool:
++ return idA.toBool() < idB.toBool();
++ case QJsonValue::Double:
++ return idA.toDouble() < idB.toDouble();
++ case QJsonValue::String:
++ return idA.toString() < idB.toString();
++ case QJsonValue::Array:
++ return false;
++ case QJsonValue::Object:
++ return false;
++ case QJsonValue::Undefined:
++ return false;
++ }
++ return false;
++ }
++
++ void swap(QJsonValueRef a, QJsonValueRef b)
++ {
++ // Note: Do not assign a QJsonValueRef to another one. It does not do what you think it does.
++ QJsonValue x = a;
++ QJsonValue y = b;
++ a = std::move(y);
++ b = std::move(x);
++ }
++
++ static QJsonDocument sortDocument(const QJsonDocument &doc)
++ {
++ if (doc.isArray()) {
++ QJsonArray array = doc.array();
++ std::sort(array.begin(), array.end());
++ return QJsonDocument(array);
++ }
++ return doc;
++ }
++
++ static bool operator<(const QJsonRpcProtocol::Response &a, const QJsonRpcProtocol::Response &b)
++ {
++ return a.id < b.id;
++ }
++
++ void tst_QJsonRpcProtocol::specRequests()
++ {
++ QFETCH(QByteArray, request);
++ QFETCH(QByteArray, response);
++
++ int responses = 0;
++ transport.setEchoHandler([&](const QByteArray &received) {
++ QJsonDocument document = sortDocument(QJsonDocument::fromJson(response));
++ QJsonDocument actual = sortDocument(QJsonDocument::fromJson(received));
++ QCOMPARE(actual, document);
++ ++responses;
++ });
++
++ transport.receiveData(request);
++ QTRY_COMPARE(responses, 1);
++
++ transport.setEchoHandler(nullptr);
++ }
++
++ void tst_QJsonRpcProtocol::specNotifications_data()
++ {
++ QTest::addColumn<QByteArray>("notification");
++ QTest::addColumn<QString>("method");
++ QTest::addColumn<QByteArray>("params");
++
++ QTest::newRow("notification 1")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]})")
++ << "update" << QByteArray("[1,2,3,4,5]");
++
++ QTest::newRow("notification 2")
++ << QByteArray(R"({"jsonrpc": "2.0", "method": "foobar"})") << QString() << QByteArray();
++
++ // Silently ignore notify_sum, but trigger notify_hello.
++ QTest::newRow("rpc call Batch (all notifications)")
++ << QByteArray(R"([ {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
++ {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}])")
++ << "notify_hello" << QByteArray("[7]");
++ }
++
++ void tst_QJsonRpcProtocol::specNotifications()
++ {
++ QFETCH(QByteArray, notification);
++ QFETCH(QString, method);
++ QFETCH(QByteArray, params);
++
++ transport.setEchoHandler([](const QByteArray &received) { QFAIL(received); });
++
++ if (!method.isEmpty()) {
++ const UpdateHandler *handler =
++ static_cast<UpdateHandler *>(protocol.messageHandler(method));
++ QVERIFY(handler != nullptr);
++ const int prevUpdates = handler->numUpdates;
++ transport.receiveData(notification);
++ QTRY_COMPARE(handler->numUpdates, prevUpdates + 1);
++
++ auto doc = QJsonDocument::fromJson(params);
++ if (doc.isArray())
++ QCOMPARE(handler->lastUpdate, doc.array());
++ else if (doc.isObject())
++ QCOMPARE(handler->lastUpdate, doc.object());
++ else
++ QFAIL("Use arrays or objects as parameters");
++ } else {
++ transport.receiveData(notification);
++ }
++
++ QTest::qWait(20); // Check that nothing is sent back.
++
++ transport.setEchoHandler(nullptr);
++ }
++
++ void tst_QJsonRpcProtocol::clientRequests_data()
++ {
++ return specRequests_data();
++ }
++
++ void tst_QJsonRpcProtocol::clientRequests()
++ {
++ QFETCH(QByteArray, request);
++ QFETCH(QByteArray, response);
++
++ int requests = 0;
++
++ transport.setEchoHandler([&](const QByteArray &data) {
++ QJsonDocument document = sortDocument(QJsonDocument::fromJson(request));
++ QJsonDocument actual = sortDocument(QJsonDocument::fromJson(data));
++ QCOMPARE(actual, document);
++ ++requests;
++ transport.receiveData(response);
++ });
++
++ auto verifyObject = [&](const QJsonRpcProtocol::Response &actual,
++ const QJsonObject &expectedObject) {
++ QCOMPARE(expectedObject.value("id"), actual.id);
++ if (expectedObject.contains("error")) {
++ const QJsonObject expectedError = expectedObject.value("error").toObject();
++ QCOMPARE(actual.errorCode, expectedError.value("code"));
++ QCOMPARE(actual.errorMessage, expectedError.value("message").toString());
++ QCOMPARE(actual.data, expectedError.value("data"));
++ } else {
++ QCOMPARE(actual.data, expectedObject.value("result"));
++ }
++ };
++
++ const QJsonDocument doc = QJsonDocument::fromJson(request);
++ const QJsonDocument expected = QJsonDocument::fromJson(response);
++ if (doc.isObject()) {
++ QVERIFY(expected.isObject());
++ const QJsonObject object = doc.object();
++ QJsonRpcProtocol::Request message;
++ message.id = object.value("id");
++ QVERIFY(object.contains("method"));
++ if (!object.value("method").isString())
++ QSKIP("Cannot send invalid non-string method through QJsonRpcProtocol.");
++ message.method = object.value("method").toString();
++ message.params = object.value("params");
++ protocol.sendRequest(message, [&](const QJsonRpcProtocol::Response &result) {
++ verifyObject(result, expected.object());
++ });
++ QCOMPARE(requests, 1);
++ } else if (doc.isArray()) {
++ const QJsonArray array = doc.array();
++ QJsonRpcProtocol::Batch batch;
++ for (const QJsonValue &value : array) {
++ if (!value.isObject())
++ QSKIP("Cannot send non-object requests through QJsonRpcProtocol.");
++ const QJsonObject object = value.toObject();
++ if (!object.value("method").isString())
++ QSKIP("Cannot send invalid non-string method through QJsonRpcProtocol.");
++
++ if (object.contains("id")) {
++ QJsonRpcProtocol::Request message;
++ message.id = object.value("id");
++ message.method = object.value("method").toString();
++ message.params = object.value("params");
++ batch.addRequest(message);
++ } else {
++ QJsonRpcProtocol::Notification message;
++ message.method = object.value("method").toString();
++ message.params = object.value("params");
++ batch.addNotification(message);
++ }
++ }
++
++ QVector<QJsonRpcProtocol::Response> responses;
++ protocol.sendBatch(std::move(batch), [&](const QJsonRpcProtocol::Response &response) {
++ responses.append(response);
++ });
++
++ if (array.isEmpty()) {
++ // QJsonRpcProtocol ignores empty batches, so we don't get the expected error.
++ QCOMPARE(requests, 0);
++ } else {
++ QVERIFY(expected.isArray());
++ std::sort(responses.begin(), responses.end());
++ QJsonArray sorted = sortDocument(expected).array();
++ QCOMPARE(responses.size(), sorted.size());
++ for (int i = 0; i < responses.size(); ++i)
++ verifyObject(responses.at(i), sorted.at(i).toObject());
++ QCOMPARE(requests, 1);
++ }
++ } else {
++ QSKIP("Cannot send malformed JSON through QJsonRpcProtocol.");
++ }
++
++ transport.setEchoHandler(nullptr);
++ }
++
++ void tst_QJsonRpcProtocol::clientNotifications_data()
++ {
++ return specNotifications_data();
++ }
++
++ void tst_QJsonRpcProtocol::clientNotifications()
++ {
++ QFETCH(QByteArray, notification);
++ QFETCH(QString, method);
++ QFETCH(QByteArray, params);
++
++ Q_UNUSED(method);
++ Q_UNUSED(params);
++
++ int notifications = 0;
++ transport.setEchoHandler([&](const QByteArray &data) {
++ QJsonDocument document = QJsonDocument::fromJson(notification);
++ QJsonDocument actual = QJsonDocument::fromJson(data);
++ QCOMPARE(actual, document);
++ ++notifications;
++ });
++
++ QJsonDocument doc = QJsonDocument::fromJson(notification);
++ if (doc.isObject()) {
++ const QJsonObject object = doc.object();
++ QJsonRpcProtocol::Notification message;
++ message.method = object.value("method").toString();
++ message.params = object.value("params");
++ protocol.sendNotification(message);
++ } else if (doc.isArray()) {
++ const QJsonArray array = doc.array();
++ QJsonRpcProtocol::Batch batch;
++ for (const QJsonValue &value : array) {
++ QVERIFY(value.isObject());
++ const QJsonObject object = value.toObject();
++ QJsonRpcProtocol::Notification message;
++ message.method = object.value("method").toString();
++ message.params = object.value("params");
++ batch.addNotification(message);
++ }
++ protocol.sendBatch(std::move(batch), [](const QJsonRpcProtocol::Response &) {
++ QFAIL("Response received for notification");
++ });
++ } else {
++ QFAIL("malformed JSON");
++ }
++
++ QTRY_COMPARE(notifications, 1);
++ transport.setEchoHandler(nullptr);
++ }
++
++ void tst_QJsonRpcProtocol::badResponses()
++ {
++ int invalids = 0;
++ int errors = 0;
++
++ protocol.setInvalidResponseHandler([&](const QJsonRpcProtocol::Response &response) {
++ ++invalids;
++ QCOMPARE(response.id, 1);
++ QCOMPARE(response.errorCode, -32601);
++ QCOMPARE(response.errorMessage, "Method not found");
++ QVERIFY(response.data.isUndefined());
++ });
++
++ protocol.setProtocolErrorHandler([&](const QJsonRpcProtocol::Response &response) {
++ ++errors;
++ QCOMPARE(response.errorCode, -32700);
++ QCOMPARE(response.errorMessage, "Parse error");
++ QVERIFY(response.id.isNull());
++ QVERIFY(response.data.isUndefined());
++ });
++
++ transport.receiveData("{\"jsonrpc\": \"2.0\", \"error\": "
++ "{\"code\": -32700, \"message\": \"Parse error\"}, \"id\": null}");
++
++ QTRY_COMPARE(errors, 1);
++ QCOMPARE(invalids, 0);
++
++ transport.receiveData("{\"jsonrpc\": \"2.0\", \"error\": "
++ "{\"code\": -32601, \"message\": \"Method not found\"}, \"id\": 1}");
++
++ QTRY_COMPARE(invalids, 1);
++ QCOMPARE(errors, 1);
++
++ protocol.setInvalidResponseHandler(nullptr);
++ protocol.setProtocolErrorHandler(nullptr);
++ }
++
++ void tst_QJsonRpcProtocol::testHttpMessagesSplits_data()
++ {
++ static const QByteArray payload1 = "{\"some\":\"json\"}";
++ static const QByteArray msg1 = QByteArray { "Bla-bla: bla!bla!\r\n"
++ " bla&bla\r\n"
++ "Content-Length: 15\r\n\r\n" }
++ + payload1;
++ static const QByteArray payload2 = "a message";
++ static const QByteArray msg2 = QByteArray { "Content-Length: 9\r\n"
++ "Bla-bla: bla!bla!\r\n"
++ " bla&bla\r\n\r\n" }
++ + payload2;
++ static const QByteArray msg12 = msg1 + msg2;
++ QTest::addColumn<qsizetype>("offset");
++ QTest::addColumn<qsizetype>("splitSize");
++ QTest::addColumn<QList<QList<QByteArray>>>("splits");
++ QTest::addColumn<QList<QByteArray>>("payloads");
++
++ QList<QByteArray> payloads { payload1, payload2 };
++ for (qsizetype splitSize = 1; splitSize < 4; ++splitSize) {
++ for (qsizetype offset = 0; offset < splitSize; ++offset) {
++ char rowName[4] = { 'r', '0', '0', 0 };
++ rowName[1] = '0' + splitSize;
++ rowName[2] = '0' + offset;
++ QList<QList<QByteArray>> splits = { {}, {} };
++ qsizetype pos = 0;
++ if (offset != 0) {
++ splits[0].append(msg12.mid(0, offset));
++ pos = offset;
++ }
++ while (pos < msg1.size()) {
++ splits[0].append(msg12.mid(pos, splitSize));
++ pos += splitSize;
++ }
++ while (pos < msg12.size()) {
++ splits[1].append(msg12.mid(pos, splitSize));
++ pos += splitSize;
++ }
++ QTest::newRow(rowName) << offset << splitSize << splits << payloads;
++ }
++ }
++ }
++
++ void tst_QJsonRpcProtocol::testHttpMessagesSplits()
++ {
++ QFETCH(qsizetype, offset);
++ QFETCH(qsizetype, splitSize);
++ QFETCH(QList<QList<QByteArray>>, splits);
++ QFETCH(QList<QByteArray>, payloads);
++ Q_UNUSED(offset);
++ Q_UNUSED(splitSize);
++ int nMessages = 0;
++ int nHeaders = 0;
++ int iMsg = 0;
++ QByteArray lastMessage;
++ QHttpMessageStreamParser parser(
++ [&nHeaders](const QByteArray &, const QByteArray &) { ++nHeaders; },
++ [&nMessages, &lastMessage](const QByteArray &body) {
++ ++nMessages;
++ lastMessage = body;
++ },
++ [](QtMsgType t, const QString &msg) {
++ QDebug(t) << "QHttpMessageStreamParser" << msg;
++ });
++ while (iMsg < splits.size()) {
++ QCOMPARE(nMessages, iMsg);
++ const auto pieces = splits.at(iMsg);
++ for (const auto &piece : pieces) {
++ QCOMPARE(nMessages, iMsg);
++ parser.receiveData(piece);
++ }
++ QCOMPARE(nMessages, ++iMsg);
++ QCOMPARE(nHeaders, 2 * iMsg);
++ QCOMPARE(lastMessage, payloads.at(iMsg - 1));
++ }
++ QCOMPARE(nMessages, 2);
++ QCOMPARE(lastMessage, payloads.last());
++ QVERIFY(parser.receiveEof());
++ }
++
++ void tst_QJsonRpcProtocol::testHttpMessagesBrokenInputs_data()
++ {
++ QTest::addColumn<QByteArray>("input");
++ QTest::addColumn<bool>("shouldWarn");
++ // valid content-length header and valid JSON body
++ QTest::newRow("valid content-length header and valid JSON body")
++ << QByteArray("Content-Length: 15\r\n\r\n{\"some\":\"json\"}") << false;
++ // correct content-length header but invalid JSON body
++ QTest::newRow("correct content-length header but invalid JSON body")
++ << QByteArray("Content-Length: 11\r\n\r\n{\"foo\":bar}") << true;
++ // content-length header is less than the actual body length
++ // warning 1: broken json, warning 2: partial message at the end
++ QTest::newRow("content-length header is less than the actual body length")
++ << QByteArray("Content-Length: 3\r\n\r\n{\"foo\":bar}") << true;
++ // misc invalid content-length headers
++ QTest::newRow("invalid content-length non-numeric")
++ << QByteArray("Content-Length: abc\r\n\r\n{\"some\":\"json\"}") << true;
++ QTest::newRow("invalid content-length negative")
++ << QByteArray("Content-Length: -10\r\n\r\n{\"some\":\"json\"}") << true;
++ QTest::newRow("missing content-length header")
++ << QByteArray("Foo: bar\r\n\r\n{\"some\":\"json\"}") << true;
++ // fuzz test input from libFuzzer, shouldn't crash
++ /*
++ O-�-- : n
++ Content-Length: 60
++
++ {"jsonrpc": 2.0", "method": "foo", ��"params
++ */
++ QByteArray fromFuzzer;
++ fromFuzzer.append("O-�-- :\t\tn\r\nContent-Length: 60\r\n\r\n{\"jsonrpc\": 2.0\", \"method\": "
++ "\"foo\", ��\"params");
++ QTest::newRow("libFuzzer") << fromFuzzer << true;
++ }
++
++ void tst_QJsonRpcProtocol::testHttpMessagesBrokenInputs()
++ {
++ QFETCH(QByteArray, input);
++ QFETCH(bool, shouldWarn);
++
++ int warnings = 0;
++ const auto headerHandler = [](const QByteArray &, const QByteArray &) { };
++ const auto diagnosticHandler = [&](QtMsgType error, const QString &msg) {
++ ++warnings;
++ QDebug(error) << "Message:" << msg;
++ };
++ const auto bodyHandler = [&](const QByteArray &body) {
++ QJsonParseError error = { 0, QJsonParseError::NoError };
++ const QJsonDocument doc = QJsonDocument::fromJson(body, &error);
++ if (error.error != QJsonParseError::NoError) {
++ diagnosticHandler(QtDebugMsg,
++ QString("Errors in JSON parsing: %1").arg(QString::fromUtf8(body)));
++ }
++ };
++ QHttpMessageStreamParser parser(headerHandler, bodyHandler, diagnosticHandler);
++
++ parser.receiveData(input);
++ QCOMPARE(warnings > 0, shouldWarn);
++ }
++
++ void SumHandler::handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler)
++ {
++ if (request.id.toInt() & 0x1)
++ handler(plus(request));
++ else
++ QTimer::singleShot(10, [handler, request]() { handler(plus(request)); });
++ }
++
++ QJsonRpcProtocol::Response SumHandler::plus(const QJsonRpcProtocol::Request &request)
++ {
++ if (request.params.isArray()) {
++ const QJsonArray array = request.params.toArray();
++
++ if (array.count() == 0)
++ return error(QJsonRpcProtocol::ErrorCode::InvalidParams);
++
++ double sum = 0;
++ for (const QJsonValue &value : array) {
++ if (!value.isDouble())
++ return error(QJsonRpcProtocol::ErrorCode::InvalidParams);
++ sum += value.toDouble();
++ }
++ return result(sum);
++ }
++ return error(QJsonRpcProtocol::ErrorCode::InvalidParams);
++ }
++
++ void SubtractHandler::handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler)
++ {
++ if (request.id.toInt() & 0x1)
++ handler(minus(request));
++ else
++ QTimer::singleShot(10, [handler, request]() { handler(minus(request)); });
++ }
++
++ QJsonRpcProtocol::Response SubtractHandler::minus(const QJsonRpcProtocol::Request &request)
++ {
++ QJsonValue minuend;
++ QJsonValue subtrahend;
++ if (request.params.isArray()) {
++ const QJsonArray array = request.params.toArray();
++
++ if (array.count() != 2)
++ return error(QJsonRpcProtocol::ErrorCode::InvalidParams);
++
++ minuend = array.at(0);
++ subtrahend = array.at(1);
++ } else if (request.params.isObject()) {
++ const QJsonObject object = request.params.toObject();
++ minuend = object.value("minuend");
++ subtrahend = object.value("subtrahend");
++ } else {
++ return error(QJsonRpcProtocol::ErrorCode::InvalidParams);
++ }
++
++ if (!minuend.isDouble() || !subtrahend.isDouble())
++ return error(QJsonRpcProtocol::ErrorCode::InvalidParams);
++
++ return result(minuend.toDouble() - subtrahend.toDouble());
++ }
++
++ void UpdateHandler::handleNotification(const QJsonRpcProtocol::Notification ¬ification)
++ {
++ lastUpdate = notification.params;
++ ++numUpdates;
++ }
++
++ void GetDataHandler::handleRequest(const QJsonRpcProtocol::Request &request,
++ const ResponseHandler &handler)
++ {
++ QTimer::singleShot(10, [request, handler]() {
++ if (!request.params.isUndefined())
++ return handler(error(QJsonRpcProtocol::ErrorCode::InvalidParams));
++
++ QJsonRpcProtocol::Response response;
++ QJsonArray array;
++ array.append("hello");
++ array.append(5);
++ response.data = array;
++ return handler(response);
++ });
++ }
++
++ void EchoTransport::sendMessage(const QJsonDocument &message)
++ {
++ if (m_messageHandler)
++ m_messageHandler(message.toJson(QJsonDocument::Compact));
++ }
++
++ void EchoTransport::receiveData(const QByteArray &bytes)
++ {
++ QJsonParseError error = QJsonParseError();
++ const QJsonDocument doc = QJsonDocument::fromJson(bytes, &error);
++ messageHandler()(doc, error);
++ }
++
++ ScopedConnection::ScopedConnection(QMetaObject::Connection connection)
++ : connection(std::move(connection))
++ {
++ }
++
++ ScopedConnection::~ScopedConnection()
++ {
++ QObject::disconnect(connection);
++ }
++
++ QT_END_NAMESPACE
++
++ QTEST_MAIN(tst_QJsonRpcProtocol)
++
++ #include "tst_qjsonrpcprotocol.moc"