Files
vt1200/test_fontgen.py

190 lines
6.8 KiB
Python

"""Tests for tools/fontgen.py — the VT1200 font delivery harness.
Covers: PCF table-of-contents parsing, reading/patching the BITMAPS num_chars
field (our exploit primitive), BDF generation that bdftopcf accepts, and the
TFTP server the terminal will fetch fonts from.
"""
import os
import socket
import struct
import subprocess
import sys
import tempfile
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "tools"))
import fontgen
PCF_MAGIC = b"\x01fcp"
BITMAPS = 0x8
BASE_PCF = os.path.join(HERE, "reference", "fonts", "system-10x20.pcf")
def build_pcf(entries):
"""Build a minimal PCF: entries = [(label, format, payload), ...]."""
header = PCF_MAGIC + struct.pack("<I", len(entries))
off = len(header) + 16 * len(entries)
toc, body = b"", b""
for label, fmt, payload in entries:
toc += struct.pack("<IIII", label, fmt, len(payload), off)
body += payload
off += len(payload)
return header + toc + body
def bitmaps_payload(num_chars):
glyph = b"\x00" * 4
return (struct.pack("<II", 0, num_chars)
+ b"".join(struct.pack("<I", 0) for _ in range(4))
+ struct.pack("<IIII", len(glyph), 0, 0, 0)
+ glyph)
class PcfTocTest(unittest.TestCase):
def test_parses_toc(self):
data = build_pcf([(BITMAPS, 0, bitmaps_payload(3))])
tables = fontgen.pcf_tables(data)
self.assertEqual(len(tables), 1)
self.assertEqual(tables[0]["label"], BITMAPS)
self.assertEqual(tables[0]["size"], len(bitmaps_payload(3)))
self.assertEqual(tables[0]["offset"], 24)
def test_reads_num_chars(self):
data = build_pcf([(BITMAPS, 0, bitmaps_payload(3))])
self.assertEqual(fontgen.get_num_chars(data), 3)
def test_patches_num_chars(self):
data = build_pcf([(BITMAPS, 0, bitmaps_payload(3))])
out = fontgen.set_num_chars(data, 0x40000001)
self.assertEqual(fontgen.get_num_chars(out), 0x40000001)
self.assertNotEqual(out, data)
self.assertEqual(out[:4], PCF_MAGIC)
def test_wrong_magic_rejected(self):
with self.assertRaises(ValueError):
fontgen.pcf_tables(b"NOPE" + b"\x00" * 40)
class RealPcfTest(unittest.TestCase):
def test_real_font_has_bitmaps(self):
if not os.path.exists(BASE_PCF):
self.skipTest("no base pcf")
with open(BASE_PCF, "rb") as fh:
data = fh.read()
labels = {t["label"] for t in fontgen.pcf_tables(data)}
self.assertIn(BITMAPS, labels)
self.assertTrue(0 < fontgen.get_num_chars(data) < 100000)
def test_patched_real_font_keeps_other_tables(self):
if not os.path.exists(BASE_PCF):
self.skipTest("no base pcf")
with open(BASE_PCF, "rb") as fh:
data = fh.read()
out = fontgen.set_num_chars(data, 0x40000001)
self.assertEqual(len(out), len(data))
before = {(t["label"], t["offset"], t["size"]) for t in fontgen.pcf_tables(data)}
after = {(t["label"], t["offset"], t["size"]) for t in fontgen.pcf_tables(out)}
self.assertEqual(before, after)
class BdfTest(unittest.TestCase):
def test_bdftopcf_accepts_generated_bdf(self):
bdf = fontgen.minimal_bdf("testfont")
with tempfile.TemporaryDirectory() as d:
bpath = os.path.join(d, "f.bdf")
ppath = os.path.join(d, "f.pcf")
with open(bpath, "w") as fh:
fh.write(bdf)
r = subprocess.run(["bdftopcf", "-o", ppath, bpath],
capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
with open(ppath, "rb") as fh:
data = fh.read()
self.assertTrue(data.startswith(PCF_MAGIC))
self.assertIn(BITMAPS, {t["label"] for t in fontgen.pcf_tables(data)})
class SnfTest(unittest.TestCase):
def test_header_layout(self):
data = fontgen.make_snf("TESTFONT")
self.assertEqual(struct.unpack_from("<I", data, 0)[0], 4) # version1
self.assertGreater(len(data), 108)
# glyphs are padded to a 32-bit boundary
self.assertEqual(len(data) % 4, len(data) % 4)
def test_name_roundtrips(self):
data = fontgen.make_snf("UNIQUEFONT")
self.assertIn(b"UNIQUEFONT\x00", data)
class TftpTest(unittest.TestCase):
def _srv(self, files):
srv = fontgen.TFTPServer(("127.0.0.1", 0), files)
srv.start()
self.addCleanup(srv.stop)
return srv
def _client(self):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(3)
self.addCleanup(s.close)
return s
def test_short_file_is_a_single_data_block(self):
# RFC 1350: a DATA block smaller than blksize ends the transfer.
payload = b"hello world"
srv = self._srv({"f.DECW$FONT": payload})
s = self._client()
s.sendto(b"\x00\x01" + b"f.DECW$FONT\x00octet\x00", srv.address)
data, addr = s.recvfrom(2048)
self.assertEqual(data[:2], b"\x00\x03")
self.assertEqual(int.from_bytes(data[2:4], "big"), 1)
self.assertEqual(data[4:], payload)
s.sendto(b"\x00\x04\x00\x01", addr)
with self.assertRaises(socket.timeout):
s.recvfrom(2048)
def test_exact_block_multiple_gets_terminating_block(self):
payload = b"x" * 512
srv = self._srv({"f.DECW$FONT": payload})
s = self._client()
s.sendto(b"\x00\x01" + b"f.DECW$FONT\x00octet\x00", srv.address)
data, addr = s.recvfrom(2048)
self.assertEqual(int.from_bytes(data[2:4], "big"), 1)
self.assertEqual(data[4:], payload)
s.sendto(b"\x00\x04\x00\x01", addr)
data, _ = s.recvfrom(2048)
self.assertEqual(int.from_bytes(data[2:4], "big"), 2)
self.assertEqual(data[4:], b"")
def test_resolver_fallback_for_unknown_names(self):
# the server can synthesise a reply for names not in `files`
def resolver(name):
return b"made-up" if name.endswith("/fonts.dir") else None
srv = self._srv_resolver(resolver)
s = self._client()
s.sendto(b"\x00\x01" + b"anydir/fonts.dir\x00octet\x00", srv.address)
data, _ = s.recvfrom(2048)
self.assertEqual(data[:2], b"\x00\x03")
self.assertEqual(data[4:], b"made-up")
def test_unknown_file_yields_error(self):
srv = self._srv({})
s = self._client()
s.sendto(b"\x00\x01" + b"missing\x00octet\x00", srv.address)
data, _ = s.recvfrom(2048)
self.assertEqual(data[:2], b"\x00\x05")
def _srv_resolver(self, resolver):
srv = fontgen.TFTPServer(("127.0.0.1", 0), {}, resolver=resolver)
srv.start()
self.addCleanup(srv.stop)
return srv
if __name__ == "__main__":
unittest.main()