initial commit
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
#include "broker/font_broker.h"
|
||||
#include "common/font_contract.h"
|
||||
#include <QCryptographicHash>
|
||||
#include <QElapsedTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QFile>
|
||||
#include <QPointer>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QTest>
|
||||
#include <cstdio>
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#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<QEventLoop> loop; };
|
||||
auto result = std::make_shared<Result>();
|
||||
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<qint64>(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<QCborValue> 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<qint64>(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<FontBroker>();
|
||||
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<FontBroker>();
|
||||
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"
|
||||
Reference in New Issue
Block a user