mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
190 lines
6.8 KiB
Python
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()
|