#include #include #include #include #include "pdf/system_font_adapter.h" using namespace docview; namespace { void put16(QByteArray &bytes, int offset, quint16 value) { qToBigEndian(value, bytes.data() + offset); } void put32(QByteArray &bytes, int offset, quint32 value) { qToBigEndian(value, bytes.data() + offset); } void face(QByteArray &bytes, int offset, int dataOffset) { put32(bytes, offset, 0x00010000); put16(bytes, offset + 4, 1); put32(bytes, offset + 12, 0x68656164); // head, arbitrary contents for adapter tests only put32(bytes, offset + 20, dataOffset); put32(bytes, offset + 24, 8); } QByteArray sfnt(int size = 64) { QByteArray bytes(size, '\0'); face(bytes, 0, 32); bytes.replace(32, 8, "HEAD0000"); return bytes; } QByteArray ttc() { QByteArray bytes(128, '\0'); bytes.replace(0, 4, "ttcf"); put32(bytes, 4, 0x00010000); put32(bytes, 8, 2); put32(bytes, 12, 32); put32(bytes, 16, 80); face(bytes, 32, 112); face(bytes, 80, 120); bytes.replace(112, 8, "FACE0000"); bytes.replace(120, 8, "FACE1111"); return bytes; } struct Provider { QByteArray bytes = sfnt(); int index = 0; qint64 offset = -1; int maps = 0, reads = 0, closes = 0; int maxRead = 0; bool released = false; QByteArray requestedName; QString fault; QCborMap run(const QString &operation, const QCborMap &payload, int timeout) { if (timeout <= 0 || timeout > 5000) qFatal("invalid font timeout"); const QString id = QString(32, 'a'); if (operation == "font.map") { ++maps; released = false; requestedName = payload.value("faceLocal").toByteArray(); if (fault == "not-found") return {{"found", false}}; if (fault == "timeout") return {{"error", QCborMap{{"code", "E_WORKER_TIMEOUT"}, {"message", "timeout"}}}}; if (fault == "exception") throw std::runtime_error("provider failed"); QCborMap result{{"found", true}, {"fontId", id}, {"actualFaceLocal", QByteArray("Actual Face")}, {"charset", 128}, {"size", bytes.size()}, {"sha256", QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)}, {"substituted", true}}; if (offset < 0) result.insert(QStringLiteral("faceIndex"), index); else result.insert(QStringLiteral("faceOffset"), offset); if (fault == "oversize") result.insert(QStringLiteral("size"), 64 * 1024 * 1024 + 1); if (fault == "hash") result.insert(QStringLiteral("sha256"), QByteArray(32, 'z')); if (fault == "two-selectors") result.insert(QStringLiteral("faceOffset"), 0); return result; } if (operation == "font.close") { ++closes; released = true; return {{"released", fault != "close"}}; } ++reads; const auto at = payload.value("offset").toInteger(), size = payload.value("length").toInteger(); maxRead = std::max(maxRead, int(size)); QByteArray data = bytes.mid(at, size); if (fault == "short") data.chop(1); return {{"fontId", fault == "identity" ? QString(32, 'b') : id}, {"offset", at}, {"data", data}}; } SystemFontAdapter::Fetch fetch() { return [this](const QString &op, const QCborMap &payload, int timeout) { return run(op, payload, timeout); }; } }; void *map(SystemFontAdapter &adapter, const char *name = "Requested Face") { auto *api = adapter.interface(); return api->MapFont(api, 400, false, 128, 0, name, nullptr); } } class SystemFontAdapterTest : public QObject { Q_OBJECT private slots: void staticFontAndShortBuffer() { Provider provider; SystemFontAdapter adapter(provider.fetch()); auto *api = adapter.interface(); QCOMPARE(api->version, 2); QVERIFY(!api->EnumFonts); auto *font = map(adapter); QVERIFY(font); QVERIFY(provider.released); QCOMPARE(provider.closes, 1); QVERIFY(!adapter.failed()); QCOMPARE(api->GetFontCharset(api, font), 128); QCOMPARE(api->GetFontData(api, font, 0, nullptr, 0), 64ul); QCOMPARE(api->GetFontData(api, font, 0x74746366, nullptr, 0), 0ul); QCOMPARE(api->GetFontData(api, font, 0x68656164, nullptr, 0), 8ul); QByteArray buffer(4, '?'); QCOMPARE(api->GetFontData(api, font, 0x68656164, reinterpret_cast(buffer.data()), 4), 8ul); QCOMPARE(buffer, QByteArray("HEAD")); buffer.fill('?'); QCOMPARE(api->GetFaceName(api, font, buffer.data(), buffer.size()), 12ul); QCOMPARE(buffer, QByteArray("Actu")); buffer.resize(12); QCOMPARE(api->GetFaceName(api, font, buffer.data(), buffer.size()), 12ul); QCOMPARE(buffer, QByteArray("Actual Face\0", 12)); api->Release(api); // No deletion of the enclosing adapter or live handle. QCOMPARE(api->GetFontData(api, font, 0, nullptr, 0), 64ul); api->DeleteFont(api, font); api->DeleteFont(api, font); QCOMPARE(api->GetFontData(api, font, 0, nullptr, 0), 0ul); } void collectionSelectedFace_data() { QTest::addColumn("index"); QTest::addColumn("byOffset"); QTest::newRow("face0-index") << 0 << false; QTest::newRow("face1-index") << 1 << false; QTest::newRow("face0-offset") << 0 << true; QTest::newRow("face1-offset") << 1 << true; } void collectionSelectedFace() { QFETCH(int, index); QFETCH(bool, byOffset); Provider provider; provider.bytes = ttc(); provider.index = index; const int offset = index ? 80 : 32; if (byOffset) provider.offset = offset; SystemFontAdapter adapter(provider.fetch()); auto *api = adapter.interface(); auto *font = map(adapter); QVERIFY(font); QCOMPARE(api->GetFontData(api, font, 0x74746366, nullptr, 0), 128ul); QCOMPARE(api->GetFontData(api, font, 0, nullptr, 0), static_cast(128 - offset)); QByteArray bytes(1024, '?'); QCOMPARE(api->GetFontData(api, font, 0x74746366, reinterpret_cast(bytes.data()), bytes.size()), 128ul); QCOMPARE(bytes.first(128), provider.bytes); QCOMPARE(bytes.sliced(128), QByteArray(896, '\0')); bytes.fill('?'); QCOMPARE(api->GetFontData(api, font, 0, reinterpret_cast(bytes.data()), 4), static_cast(128 - offset)); QCOMPARE(bytes.first(4), QByteArray::fromHex("00010000")); QCOMPARE(bytes[4], '?'); bytes.resize(8); QCOMPARE(api->GetFontData(api, font, 0x68656164, reinterpret_cast(bytes.data()), 8), 8ul); QCOMPARE(bytes, index ? QByteArray("FACE1111") : QByteArray("FACE0000")); api->DeleteFont(api, font); } void chunkedTransferCacheAndRawName() { Provider provider; provider.bytes = sfnt(2 * 65536 + 19); SystemFontAdapter adapter(provider.fetch()); auto *api = adapter.interface(); const QByteArray local = QByteArray::fromHex("824f829082b1"); auto *first = map(adapter, local.constData()); QVERIFY(first); QCOMPARE(provider.requestedName, local); QCOMPARE(provider.reads, 3); QCOMPARE(provider.maxRead, 65536); QCOMPARE(adapter.transferredBytes(), provider.bytes.size()); QCOMPARE(adapter.cacheBytes(), provider.bytes.size()); provider.bytes[32] = '!'; // A different hash creates a distinct immutable snapshot. auto *second = map(adapter); QVERIFY(second); QCOMPARE(provider.reads, 6); char old[8]; QCOMPARE(api->GetFontData(api, first, 0x68656164, reinterpret_cast(old), 8), 8ul); QCOMPARE(QByteArray(old, 8), QByteArray("HEAD0000")); auto *third = map(adapter); QVERIFY(third); QCOMPARE(provider.reads, 6); QCOMPARE(provider.maps, 3); QCOMPARE(provider.closes, 3); QCOMPARE(adapter.cacheBytes(), 2 * provider.bytes.size()); QVERIFY(!adapter.warnings().isEmpty()); api->DeleteFont(api, first); api->DeleteFont(api, second); api->DeleteFont(api, third); } void collectionChecksumPrefix() { Provider provider; provider.bytes = ttc(); provider.bytes.append(QByteArray(2048 - provider.bytes.size(), '\0')); SystemFontAdapter adapter(provider.fetch()); auto *api = adapter.interface(); auto *font = map(adapter); QVERIFY(font); QByteArray sample(1024, '?'); QCOMPARE(api->GetFontData(api, font, 0x74746366, reinterpret_cast(sample.data()), sample.size()), 2048ul); QCOMPARE(sample, provider.bytes.first(1024)); api->DeleteFont(api, font); } void cacheEvictsOnlyUnreferencedSnapshots() { Provider provider; SystemFontAdapter adapter(provider.fetch(), 128); auto *api = adapter.interface(); auto *first = map(adapter); QVERIFY(first); provider.bytes[32] = '2'; auto *second = map(adapter); QVERIFY(second); QCOMPARE(adapter.cacheBytes(), 128); api->DeleteFont(api, first); provider.bytes[32] = '3'; auto *third = map(adapter); QVERIFY(third); QCOMPARE(adapter.cacheBytes(), 128); QByteArray contents(8, '?'); api->GetFontData(api, second, 0x68656164, reinterpret_cast(contents.data()), 8); QCOMPARE(contents, QByteArray("2EAD0000")); // The still-referenced second snapshot survives. provider.bytes[32] = '4'; const auto reads = provider.reads; QVERIFY(!map(adapter)); QCOMPARE(adapter.errorCode(), "E_FONT_LIMIT"); QCOMPARE(adapter.cacheBytes(), 128); QCOMPARE(provider.reads, reads); QCOMPARE(provider.closes, 4); api->DeleteFont(api, second); api->DeleteFont(api, third); } void malformedData_data() { QTest::addColumn("damage"); for (const auto *name : {"short-header", "directory", "too-many-tables", "table-offset", "overflow-length", "duplicate-tag", "face-count", "face-offset", "face-index", "duplicate-face", "other-face-directory", "signature-range", "variable", "color"}) QTest::newRow(name) << QString::fromLatin1(name); } void malformedData() { QFETCH(QString, damage); Provider provider; if (damage == "short-header") provider.bytes = QByteArray(4, '\0'); if (damage == "directory") provider.bytes.resize(20); if (damage == "too-many-tables") put16(provider.bytes, 4, 4097); if (damage == "table-offset") put32(provider.bytes, 20, provider.bytes.size()); if (damage == "overflow-length") put32(provider.bytes, 24, 0xffffffff); if (damage == "duplicate-tag") { put16(provider.bytes, 4, 2); provider.bytes.replace(28, 16, provider.bytes.mid(12, 16)); } if (damage.startsWith("face") || damage == "duplicate-face" || damage == "other-face-directory") provider.bytes = ttc(); if (damage == "face-count") put32(provider.bytes, 8, 0xffffffff); if (damage == "face-offset") provider.offset = 33; if (damage == "face-index") provider.index = 2; if (damage == "duplicate-face") put32(provider.bytes, 16, 32); if (damage == "other-face-directory") put16(provider.bytes, 84, 4096); if (damage == "signature-range") { provider.bytes = ttc(); put32(provider.bytes, 4, 0x00020000); put32(provider.bytes, 20, 0x44534947); put32(provider.bytes, 24, 0xffffffff); put32(provider.bytes, 28, 120); } if (damage == "variable") put32(provider.bytes, 12, 0x66766172); if (damage == "color") put32(provider.bytes, 12, 0x434f4c52); SystemFontAdapter adapter(provider.fetch()); QVERIFY(!map(adapter)); QVERIFY(adapter.failed()); QCOMPARE(adapter.errorCode(), "E_FONT_FAILED"); QCOMPARE(provider.closes, 1); QCOMPARE(adapter.cacheBytes(), 0); } void brokenTransfer_data() { QTest::addColumn("fault"); QTest::addColumn("code"); for (const auto *fault : {"short", "identity", "hash", "close", "oversize", "two-selectors"}) QTest::newRow(fault) << QString(fault) << QString("E_WORKER_CRASH"); QTest::newRow("timeout") << QString("timeout") << QString("E_WORKER_TIMEOUT"); QTest::newRow("exception") << QString("exception") << QString("E_FONT_FAILED"); } void brokenTransfer() { QFETCH(QString, fault); QFETCH(QString, code); Provider provider; provider.fault = fault; SystemFontAdapter adapter(provider.fetch()); QVERIFY(!map(adapter)); QVERIFY(adapter.failed()); QCOMPARE(adapter.errorCode(), code); QCOMPARE(adapter.cacheBytes(), 0); const auto maps = provider.maps; QVERIFY(!map(adapter)); QCOMPARE(provider.maps, maps); // Fatal state stays explicit. } void absenceAndNoProviderAreDistinctFromFailure() { Provider provider; provider.fault = "not-found"; SystemFontAdapter adapter(provider.fetch()); QVERIFY(!map(adapter)); QVERIFY(!adapter.failed()); QCOMPARE(provider.reads, 0); QVERIFY(!adapter.warnings().empty()); SystemFontAdapter absent; QVERIFY(!map(absent)); QVERIFY(!absent.failed()); QVERIFY(!absent.warnings().empty()); } void distinctRequestQuota() { Provider provider; provider.fault = "not-found"; SystemFontAdapter adapter(provider.fetch()); for (int i = 0; i < 256; ++i) { QVERIFY(!map(adapter, QByteArray::number(i).constData())); QVERIFY(!adapter.failed()); } QVERIFY(!map(adapter, "too many")); QCOMPARE(adapter.errorCode(), "E_FONT_LIMIT"); QCOMPARE(provider.maps, 256); } }; QTEST_GUILESS_MAIN(SystemFontAdapterTest) #include "test_system_font_adapter.moc"