#include "broker/font_broker.h" #include "common/font_contract.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #include #include #include #endif using namespace docview; namespace { QCborMap mapRequest(QByteArray family = "Helvetica", int weight = 400) { return {{"faceLocal", family}, {"weight", weight}, {"italic", false}, {"charset", 0}, {"pitchFamily", 0}}; } QCborMap call(FontBroker &broker, const QString &operation, const QCborMap &payload) { struct Result { QCborMap map; bool complete = false; QPointer loop; }; auto result = std::make_shared(); QEventLoop loop; result->loop = &loop; QTimer timer; timer.setSingleShot(true); QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); broker.request(operation, payload, [result](QCborMap map) { result->map = std::move(map); result->complete = true; if (result->loop) result->loop->quit(); }); timer.start(10000); if (!result->complete) loop.exec(); if (!result->complete) return {{"testTimeout", true}}; return result->map; } QString code(const QCborMap &result) { return result.value("error").toMap().value("code").toString(); } } class FontBrokerTest : public QObject { Q_OBJECT private slots: void cleanup() { QTest::qWait(10); } // Let revoked bounded OS threads release their permits. void asynchronousMappingAndSelectedBytes() { FontBroker broker; bool callback = false, returned = false; QCborMap mapped; broker.request("font.map", mapRequest(), [&](QCborMap result) { QVERIFY(returned); QCOMPARE(QThread::currentThread(), broker.thread()); callback = true; mapped = std::move(result); }); returned = true; QVERIFY(!callback); QTRY_VERIFY_WITH_TIMEOUT(callback, 10000); QVERIFY2(mapped.value("found").toBool(), qPrintable(QCborValue(mapped).toDiagnosticNotation())); QVERIFY(validFontId(mapped.value("fontId"))); QVERIFY(!mapped.contains(QStringLiteral("path"))); QVERIFY(!mapped.contains(QStringLiteral("fontRequestId"))); const auto id = mapped.value("fontId"); const auto size = mapped.value("size").toInteger(); QVERIFY(size > 0 && size <= MaxFontSnapshotBytes); QCryptographicHash hash(QCryptographicHash::Sha256); for (qint64 offset = 0; offset < size; offset += MaxFontReadBytes) { const auto length = qMin(MaxFontReadBytes, size - offset); const auto result = call(broker, "font.read", {{"fontId", id}, {"offset", offset}, {"length", length}}); QCOMPARE(result.value("data").toByteArray().size(), length); QCOMPARE(result.value("offset").toInteger(), offset); hash.addData(result.value("data").toByteArray()); } QCOMPARE(hash.result(), mapped.value("sha256").toByteArray()); QVERIFY(call(broker, "font.close", {{"fontId", id}}).value("released").toBool()); QCOMPARE(code(call(broker, "font.read", {{"fontId", id}, {"offset", 0}, {"length", 1}})), "E_FONT_FAILED"); QCOMPARE(code(call(broker, "font.close", {{"fontId", id}})), "E_FONT_FAILED"); } void namesAreValuesAndReferencesArePrivate() { QTemporaryDir directory; QVERIFY(directory.isValid()); QFile secret(directory.filePath("private.ttf")); QVERIFY(secret.open(QIODevice::WriteOnly)); const QByteArray canary("NOT AN INSTALLED FONT: PRIVATE FIXTURE"); secret.write(canary); secret.close(); FontBroker first, second; for (const auto &name : {secret.fileName().toLocal8Bit(), QByteArray("sans:file=") + secret.fileName().toLocal8Bit()}) { const auto result = call(first, "font.map", mapRequest(name)); QVERIFY(!result.contains(QStringLiteral("error"))); if (result.value("found").toBool()) { QVERIFY(result.value("sha256").toByteArray() != QCryptographicHash::hash(canary, QCryptographicHash::Sha256)); const auto id = result.value("fontId"); QCOMPARE(code(call(second, "font.read", {{"fontId", id}, {"offset", 0}, {"length", 1}})), "E_FONT_FAILED"); QVERIFY(call(first, "font.close", {{"fontId", id}}).value("released").toBool()); } } } void malformedInputAndReadBounds() { FontBroker broker; auto invalid = mapRequest(); invalid.insert(QStringLiteral("path"), "/etc/passwd"); QCOMPARE(code(call(broker, "font.map", invalid)), "E_FONT_FAILED"); QCOMPARE(code(call(broker, "font.map", mapRequest(QByteArray(257, 'a')))), "E_FONT_FAILED"); QCOMPARE(code(call(broker, "font.map", mapRequest(QByteArray("a\0b", 3)))), "E_FONT_FAILED"); invalid = mapRequest(); invalid.insert(QStringLiteral("weight"), 400.5); QCOMPARE(code(call(broker, "font.map", invalid)), "E_FONT_FAILED"); const auto font = call(broker, "font.map", mapRequest()); QVERIFY(font.value("found").toBool()); const auto id = font.value("fontId"); const auto size = font.value("size").toInteger(); for (const auto &request : {QCborMap{{"fontId", id}, {"offset", -1}, {"length", 1}}, QCborMap{{"fontId", id}, {"offset", 0}, {"length", 65537}}, QCborMap{{"fontId", id}, {"offset", size}, {"length", 1}}}) QCOMPARE(code(call(broker, "font.read", request)), "E_FONT_FAILED"); } void standardAliasesPreserveResolvedFamilyAndStyle() { FontBroker broker; for (const auto &family : {QByteArray("Helvetica"), QByteArray("Times-Roman"), QByteArray("Courier")}) { const auto regular = call(broker, "font.map", mapRequest(family)); QVERIFY(regular.value("found").toBool()); const auto actual = regular.value("actualFaceLocal").toByteArray(); QVERIFY(!actual.isEmpty()); const auto exact = call(broker, "font.map", mapRequest(actual)); QVERIFY(exact.value("found").toBool()); QVERIFY(!exact.value("substituted").toBool()); QCOMPARE(exact.value("sha256"), regular.value("sha256")); auto boldRequest = mapRequest(family, 700); boldRequest.insert(QStringLiteral("italic"), true); const auto bold = call(broker, "font.map", boldRequest); QVERIFY(bold.value("found").toBool()); QCOMPARE(bold.value("actualFaceLocal"), regular.value("actualFaceLocal")); // Standard OS alias families in the supported test environments // provide a distinct bold-italic face, rather than fake metadata. QVERIFY(bold.value("sha256") != regular.value("sha256")); for (const auto &font : {regular, exact, bold}) QVERIFY(call(broker, "font.close", {{"fontId", font.value("fontId")}}).value("released").toBool()); } } void opaqueHandleQuotaAndReuse() { FontBroker broker; QList ids; for (int i = 0; i < 16; ++i) { const auto result = call(broker, "font.map", mapRequest()); QVERIFY(result.value("found").toBool()); const auto id = result.value("fontId"); QVERIFY(!ids.contains(id)); ids << id; } QCOMPARE(code(call(broker, "font.map", mapRequest())), "E_FONT_LIMIT"); QVERIFY(call(broker, "font.close", {{"fontId", ids.takeLast()}}).value("released").toBool()); QVERIFY(call(broker, "font.map", mapRequest()).value("found").toBool()); } void distinctSelectionQuotaIncludesAbsentFonts() { FontBroker broker; auto request = mapRequest(); request.insert(QStringLiteral("charset"), 2); for (int i = 0; i < 256; ++i) { request.insert(QStringLiteral("faceLocal"), QByteArray("DocViewAbsentSymbol-") + QByteArray::number(i)); const auto result = call(broker, "font.map", request); QVERIFY2(!result.contains(QStringLiteral("error")), qPrintable(code(result))); if (result.value("found").toBool()) call(broker, "font.close", {{"fontId", result.value("fontId")}}); } request.insert(QStringLiteral("faceLocal"), QByteArray("DocViewAbsentSymbol-over-limit")); QCOMPARE(code(call(broker, "font.map", request)), "E_FONT_LIMIT"); } void transferredBytesAreChargedEvenForRepeatedReads() { FontBroker broker; const auto font = call(broker, "font.map", mapRequest()); QVERIFY(font.value("found").toBool()); const auto id = font.value("fontId"); const auto length = qMin(MaxFontReadBytes, font.value("size").toInteger()); qint64 read = 0; while (read < MaxFontTransferBytes) { const auto count = qMin(length, MaxFontTransferBytes - read); const auto result = call(broker, "font.read", {{"fontId", id}, {"offset", 0}, {"length", count}}); QCOMPARE(result.value("data").toByteArray().size(), count); read += count; } QCOMPARE(code(call(broker, "font.read", {{"fontId", id}, {"offset", 0}, {"length", 1}})), "E_FONT_LIMIT"); } void revokeDropsQueuedCompletionsWithoutWaiting() { bool called = false; auto broker = std::make_unique(); broker->request("font.map", mapRequest(), [&](QCborMap) { called = true; }); QElapsedTimer elapsed; elapsed.start(); broker->revoke(); broker.reset(); QVERIFY(elapsed.elapsed() < 500); QTest::qWait(100); QVERIFY(!called); } void processWideOsThreadLimit() { FontBroker first, second, third; QCOMPARE(code(call(third, "font.map", mapRequest())), "E_FONT_LIMIT"); QVERIFY(call(first, "font.map", mapRequest()).value("found").toBool()); QVERIFY(call(second, "font.map", mapRequest()).value("found").toBool()); } void globalCacheEvictsOtherServiceButProtectsIssuedHandles() { qint64 firstSize = 0, secondSize = 0; { FontBroker discovery; const auto regular = call(discovery, "font.map", mapRequest()); const auto bold = call(discovery, "font.map", mapRequest("Helvetica", 700)); QVERIFY(regular.value("found").toBool()); QVERIFY(bold.value("found").toBool()); firstSize = regular.value("size").toInteger(); secondSize = bold.value("size").toInteger(); } QTest::qWait(10); const auto limit = qMax(firstSize, secondSize); QVERIFY(limit > 0 && limit < MaxBrokerFontCacheBytes); // Both real OS fonts fit individually, but cannot fit together. The // production cap is only lowered; no fake backend or larger limit. FontBroker active(nullptr, limit), staging(nullptr, limit); const auto first = call(active, "font.map", mapRequest()); QVERIFY(first.value("found").toBool()); const auto firstId = first.value("fontId"); QCOMPARE(code(call(staging, "font.map", mapRequest("Helvetica", 700))), "E_FONT_LIMIT"); const auto original = call(active, "font.read", {{"fontId", firstId}, {"offset", 0}, {"length", 16}}); QCOMPARE(original.value("data").toByteArray().size(), 16); QVERIFY(call(active, "font.close", {{"fontId", firstId}}).value("released").toBool()); // This is the original bug: staging has no local cache victim, and // must reclaim the unused snapshot still cached by active. const auto second = call(staging, "font.map", mapRequest("Helvetica", 700)); QVERIFY2(second.value("found").toBool(), qPrintable(QCborValue(second).toDiagnosticNotation())); const auto secondId = second.value("fontId"); QCOMPARE(code(call(active, "font.map", mapRequest())), "E_FONT_LIMIT"); QCOMPARE(call(staging, "font.read", {{"fontId", secondId}, {"offset", 0}, {"length", 16}}).value("data").toByteArray().size(), 16); QVERIFY(call(staging, "font.close", {{"fontId", secondId}}).value("released").toBool()); const auto restored = call(active, "font.map", mapRequest()); QVERIFY(restored.value("found").toBool()); QCOMPARE(restored.value("sha256"), first.value("sha256")); } #ifdef Q_OS_LINUX void destroyingFacadeDoesNotWaitForBlockedFontconfig() { QTemporaryDir directory; QVERIFY(directory.isValid()); const auto fifo = directory.filePath("fontconfig.xml"); const auto path = QFile::encodeName(fifo); QVERIFY(::mkfifo(path.constData(), 0600) == 0); QProcess child; auto environment = QProcessEnvironment::systemEnvironment(); environment.insert("FONTCONFIG_FILE", fifo); child.setProcessEnvironment(environment); child.start(QCoreApplication::applicationFilePath(), {"--blocked-fontconfig"}); QVERIFY(child.waitForStarted(5000)); // A writer succeeds only after the backend opens the fixture FIFO for // reading. Leave it open without bytes so the actual OS font call waits. int writer = -1; QElapsedTimer wait; wait.start(); while (writer < 0 && wait.elapsed() < 2000 && child.state() != QProcess::NotRunning) { writer = ::open(path.constData(), O_WRONLY | O_NONBLOCK | O_CLOEXEC); if (writer < 0) QTest::qWait(2); } if (writer < 0) { child.kill(); child.waitForFinished(); } QVERIFY2(writer >= 0, "The real fontconfig call did not enter the controlled FIFO"); const bool finished = child.waitForFinished(5000); ::close(writer); if (!finished) { child.kill(); child.waitForFinished(); } QVERIFY(finished); QCOMPARE(child.exitStatus(), QProcess::NormalExit); QCOMPARE(child.exitCode(), 0); QCOMPARE(child.readAllStandardOutput().trimmed(), QByteArray("revoked-without-wait")); } #endif }; int main(int argc, char **argv) { QCoreApplication app(argc, argv); #ifdef Q_OS_LINUX if (app.arguments().contains("--blocked-fontconfig")) { auto broker = std::make_unique(); bool completed = false; broker->request("font.map", mapRequest(), [&](QCborMap) { completed = true; }); QTimer::singleShot(500, &app, [&] { QElapsedTimer elapsed; elapsed.start(); broker.reset(); if (elapsed.elapsed() >= 100 || completed) { app.exit(91); return; } std::puts("revoked-without-wait"); std::fflush(stdout); app.quit(); }); return app.exec(); } #endif FontBrokerTest test; return QTest::qExec(&test, argc, argv); } #include "test_font_broker.moc"