253 lines
14 KiB
C++
253 lines
14 KiB
C++
#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"
|