186 lines
13 KiB
C++
186 lines
13 KiB
C++
#include <QtTest>
|
|
#include <QCryptographicHash>
|
|
#include <QtEndian>
|
|
#include <stdexcept>
|
|
#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<uchar *>(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<int>("index"); QTest::addColumn<bool>("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<unsigned long>(128 - offset));
|
|
QByteArray bytes(1024, '?'); QCOMPARE(api->GetFontData(api, font, 0x74746366, reinterpret_cast<uchar *>(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<uchar *>(bytes.data()), 4), static_cast<unsigned long>(128 - offset));
|
|
QCOMPARE(bytes.first(4), QByteArray::fromHex("00010000")); QCOMPARE(bytes[4], '?');
|
|
bytes.resize(8); QCOMPARE(api->GetFontData(api, font, 0x68656164, reinterpret_cast<uchar *>(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<uchar *>(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<uchar *>(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<uchar *>(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<QString>("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<QString>("fault"); QTest::addColumn<QString>("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"
|