forked from opencode.energia.arti.ee/vt1200
checkpoint: VT1200 reverse-engineering workspace before board-model work
This commit is contained in:
commit
acb92d3e44
325 files changed
+951894
No files matched your search
+189
@@ -0,0 +1,189 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user