#include "common/font_contract.h" #include "common/ipc.h" #include "common/worker_font_client.h" #include "common/worker_runtime.h" #include #include #include #include #include #include #include using namespace docview; namespace { const QString fontId(32, 'a'); QCborMap mapPayload(qint64 id = 1) { return {{"fontRequestId", id}, {"faceLocal", QByteArray("Helvetica")}, {"weight", 400}, {"italic", false}, {"charset", 0}, {"pitchFamily", 0}}; } QCborMap selected(qint64 id = 1) { return {{"fontRequestId", id}, {"found", true}, {"fontId", fontId}, {"actualFaceLocal", QByteArray("Actual Face")}, {"charset", 0}, {"size", 4}, {"sha256", QByteArray(32, 'x')}, {"faceIndex", 0}, {"substituted", true}}; } QCborMap parentRequest() { return {{"protocolVersion", 1}, {"sessionId", "font-session"}, {"generation", 3}, {"requestId", 7}, {"operation", "render"}, {"payload", QCborMap{{"page", 1}}}}; } QCborMap resultReply(QCborMap request, QCborMap result) { request.remove(QStringLiteral("payload")); request.insert(QStringLiteral("result"), result); return request; } struct ChannelPair { QLocalServer server; WorkerRuntime runtime; QLocalSocket *peer = nullptr; bool open() { server.setSocketOptions(QLocalServer::UserAccessOption); if (!server.listen("font-contract-" + QUuid::createUuid().toString(QUuid::Id128))) return false; auto socket = std::make_unique(); socket->connectToServer(server.fullServerName()); if (!socket->waitForConnected(1000) || !server.waitForNewConnection(1000)) return false; peer = server.nextPendingConnection(); runtime.transport = std::move(socket); return peer; } }; } // namespace class FontContractTest : public QObject { Q_OBJECT private slots: void strictMapSchema() { const auto request = mapPayload(); QVERIFY(validateFontPayload("font.map", request)); auto emptyName = request; emptyName.insert(QStringLiteral("faceLocal"), QByteArray{}); QVERIFY(validateFontPayload("font.map", emptyName)); const QList> bad{{"faceLocal", "string coercion"}, {"faceLocal", QByteArray(257, 'a')}, {"faceLocal", QByteArray("a\0b", 3)}, {"weight", -1}, {"weight", 1001}, {"weight", 400.0}, {"italic", 1}, {"charset", 130}, {"charset", "0"}, {"pitchFamily", 3}, {"pitchFamily", 0x60}, {"fontRequestId", 0}, {"fontRequestId", 1.0}, {"path", "/secret/font.ttf"}}; for (const auto &[field, value] : bad) { auto invalid = request; invalid.insert(field, value); QVERIFY2(!validateFontPayload("font.map", invalid), qPrintable(field)); } auto missing = request; missing.remove(QStringLiteral("charset")); QVERIFY(!validateFontPayload("font.map", missing)); QVERIFY(!validateFontPayload("font.list", request)); } void boundedReadSchema() { QCborMap request{ {"fontRequestId", 2}, {"fontId", fontId}, {"offset", 0}, {"length", MaxFontReadBytes}}; QVERIFY(validateFontPayload("font.read", request)); for (const auto badLength : {qint64(-1), qint64(0), qint64(MaxFontReadBytes + 1), std::numeric_limits::max()}) { auto invalid = request; invalid.insert(QStringLiteral("length"), badLength); QVERIFY(!validateFontPayload("font.read", invalid)); } for (const auto badOffset : {qint64(-1), MaxFontSnapshotBytes - 1, std::numeric_limits::max()}) { auto invalid = request; invalid.insert(QStringLiteral("offset"), badOffset); QVERIFY(!validateFontPayload("font.read", invalid)); } request.insert(QStringLiteral("offset"), MaxFontSnapshotBytes - MaxFontReadBytes); QVERIFY(validateFontPayload("font.read", request)); request.insert(QStringLiteral("fontId"), fontId.toUpper()); QVERIFY(!validateFontPayload("font.read", request)); QVERIFY(validateFontPayload("font.close", {{"fontRequestId", 3}, {"fontId", fontId}})); } void selectedMetadataAndReadRepliesAreExact() { const auto request = mapPayload(); QVERIFY(validateFontResult("font.map", request, selected())); auto offset = selected(); offset.remove(QStringLiteral("faceIndex")); offset.insert(QStringLiteral("faceOffset"), 3); QVERIFY(validateFontResult("font.map", request, offset)); offset.insert(QStringLiteral("faceOffset"), 4); QVERIFY(!validateFontResult("font.map", request, offset)); const QList> bad{{"fontRequestId", 2}, {"fontId", QByteArray(32, 'a')}, {"fontId", QString(31, 'a')}, {"actualFaceLocal", QByteArray{}}, {"charset", 255}, {"size", 0}, {"size", MaxFontSnapshotBytes + 1}, {"sha256", QByteArray(31, 'x')}, {"sha256", QString(64, 'a')}, {"faceIndex", -1}, {"faceIndex", 65536}, {"faceOffset", 0}, {"substituted", 1}, {"more", false}}; for (const auto &[field, value] : bad) { auto invalid = selected(); invalid.insert(field, value); QVERIFY2(!validateFontResult("font.map", request, invalid), qPrintable(field)); } QVERIFY(validateFontResult("font.map", request, {{"fontRequestId", 1}, {"found", false}})); QVERIFY(!validateFontResult("font.map", request, {{"fontRequestId", 1}, {"found", false}, {"fontId", fontId}})); const QCborMap read{{"fontRequestId", 2}, {"fontId", fontId}, {"offset", 1}, {"length", 3}}; const QCborMap good{ {"fontRequestId", 2}, {"fontId", fontId}, {"offset", 1}, {"data", QByteArray("abc")}}; QVERIFY(validateFontResult("font.read", read, good)); for (const auto &[field, value] : QList>{{"fontId", QString(32, 'b')}, {"offset", 0}, {"offset", 1.0}, {"data", QByteArray("ab")}, {"data", "abc"}, {"more", true}}) { auto invalid = good; invalid.insert(field, value); QVERIFY(!validateFontResult("font.read", read, invalid)); } } void errorAndEnvelopeRoleValidation() { QCborMap error{{"fontRequestId", 1}, {"code", "E_FONT_LIMIT"}, {"message", "Font quota reached"}}; QVERIFY(validateFontError(error)); error.insert(QStringLiteral("code"), "E_UNREGISTERED"); QVERIFY(!validateFontError(error)); auto envelope = parentRequest(); envelope.insert(QStringLiteral("operation"), "font.map"); envelope.insert(QStringLiteral("payload"), mapPayload()); QVERIFY(validateEnvelope(envelope)); envelope.insert(QStringLiteral("unexpected"), true); QVERIFY(!validateEnvelope(envelope)); } void synchronousClientRoutesMultipleQueuedResponses() { ChannelPair pair; QVERIFY(pair.open()); IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer); WorkerFontClient client(&incoming, &pair.runtime); QSignalSpy errors(&client, &WorkerFontClient::protocolError); connect(&incoming, &IpcChannel::messageReceived, &client, [&](const QCborMap &reply) { QVERIFY(client.routeReply(reply)); }); QList ids; connect(&broker, &IpcChannel::messageReceived, &broker, [&](const QCborMap &request) { QCOMPARE(request.value("requestId").toInteger(), 7); QCOMPARE(request.value("generation").toInteger(), 3); const auto id = request.value("payload").toMap().value("fontRequestId").toInteger(); ids << id; QTimer::singleShot(0, &broker, [&, request, id] { QVERIFY(broker.send(resultReply(request, {{"fontRequestId", id}, {"found", false}}))); }); }); QVERIFY(client.setParentRequest(parentRequest())); auto payload = mapPayload(); payload.remove(QStringLiteral("fontRequestId")); QCOMPARE(client.call("font.map", payload), QCborMap({{"found", false}})); QCOMPARE(client.call("font.map", payload), QCborMap({{"found", false}})); QCOMPARE(ids, QList({1, 2})); QVERIFY(!client.failed()); QCOMPARE(errors.size(), 0); client.clearParentRequest(); } void clientRejectsMismatchedReply() { ChannelPair pair; QVERIFY(pair.open()); IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer); WorkerFontClient client(&incoming, &pair.runtime); QSignalSpy errors(&client, &WorkerFontClient::protocolError); connect(&incoming, &IpcChannel::messageReceived, &client, [&](const QCborMap &reply) { client.routeReply(reply); }); connect(&broker, &IpcChannel::messageReceived, &broker, [&](const QCborMap &request) { QVERIFY(broker.send(resultReply(request, {{"fontRequestId", 9}, {"found", false}}))); }); QVERIFY(client.setParentRequest(parentRequest())); auto payload = mapPayload(); payload.remove(QStringLiteral("fontRequestId")); QCOMPARE(client.call("font.map", payload).value("error").toMap().value("code").toString(), "E_WORKER_CRASH"); QCOMPARE(errors.size(), 1); QVERIFY(client.failed()); client.clearParentRequest(); } void clientTimeoutIsStickyAndDoesNotSendAnotherRequest() { ChannelPair pair; QVERIFY(pair.open()); IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer); WorkerFontClient client(&incoming, &pair.runtime); QSignalSpy requests(&broker, &IpcChannel::messageReceived); QVERIFY(client.setParentRequest(parentRequest())); auto payload = mapPayload(); payload.remove(QStringLiteral("fontRequestId")); QElapsedTimer elapsed; elapsed.start(); const auto failure = client.call("font.map", payload, 20); QCOMPARE(failure.value("error").toMap().value("code").toString(), "E_WORKER_TIMEOUT"); QVERIFY(elapsed.elapsed() < 1000); QCOMPARE(client.call("font.map", payload), failure); QCOMPARE(requests.size(), 1); client.clearParentRequest(); } void clientTransportDisconnectEndsWaitImmediately() { ChannelPair pair; QVERIFY(pair.open()); IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer); WorkerFontClient client(&incoming, &pair.runtime); QVERIFY(client.setParentRequest(parentRequest())); auto payload = mapPayload(); payload.remove(QStringLiteral("fontRequestId")); QTimer::singleShot(10, pair.peer, &QLocalSocket::close); QElapsedTimer elapsed; elapsed.start(); QCOMPARE(client.call("font.map", payload).value("error").toMap().value("code").toString(), "E_WORKER_CRASH"); QVERIFY(elapsed.elapsed() < 1000); client.clearParentRequest(); } }; QTEST_GUILESS_MAIN(FontContractTest) #include "test_font_contract.moc"