#!/usr/bin/env python3 """Exercise the packaged PDF worker without initializing UI or account databases. Usage: python3 scripts/test-pdf-preview.py target/debug/flectar-mail [output.png] Creates an original two-page vector/text fixture and checks page selection, colors, dimensions, malformed inputs and protocol limits. Requires staged PDFium. On Linux it repeats the checks as on a kernel without Landlock, where the worker's sandbox blocks all file access instead. """ import ctypes import pathlib import platform import struct import subprocess import sys import time import zlib def fixture(): streams = [b"0.1 0.4 0.8 rg 20 20 160 100 re f BT /F1 16 Tf 20 150 Td (Flectar PDF preview) Tj ET", b"0.8 0.2 0.1 rg 20 20 160 100 re f BT /F1 16 Tf 20 150 Td (Second page) Tj ET"] objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>"] for content in (6, 7): objects.append(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents {content} 0 R >>".encode()) objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") objects += [b"<< /Length " + str(len(s)).encode() + b" >>\nstream\n" + s + b"\nendstream" for s in streams] data = b"%PDF-1.4\n"; offsets = [0] for i, obj in enumerate(objects, 1): offsets.append(len(data)); data += f"{i} 0 obj\n".encode() + obj + b"\nendobj\n" xref = len(data) data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode() data += b"".join(f"{offset:010} 00000 n \n".encode() for offset in offsets[1:]) return data + f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode() def png(path, width, height, pixels): def chunk(kind, value): return struct.pack(">I", len(value)) + kind + value + struct.pack(">I", zlib.crc32(kind + value)) raw = b"".join(b"\0" + pixels[y * width * 4:(y + 1) * width * 4] for y in range(height)) pathlib.Path(path).write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")) # seccomp_data.arch for the architectures whose landlock_create_ruleset is 444. AUDIT_ARCHES = {"x86_64": 0xC000003E, "aarch64": 0xC00000B7} def without_landlock(): """Make landlock_create_ruleset fail with ENOSYS in the worker, as it does on kernels built without Landlock, such as Raspberry Pi OS's.""" def instruction(code, k, jump_true=0, jump_false=0): return struct.pack("=HBBI", code, jump_true, jump_false, k) load, jump_equal, ret = 0x20, 0x15, 0x06 program = b"".join([ instruction(load, 4), # seccomp_data.arch instruction(jump_equal, AUDIT_ARCHES[platform.machine()], 0, 3), instruction(load, 0), # seccomp_data.nr instruction(jump_equal, 444, 0, 1), instruction(ret, 0x00050000 | 38), # SECCOMP_RET_ERRNO | ENOSYS instruction(ret, 0x7FFF0000), # SECCOMP_RET_ALLOW ]) class Program(ctypes.Structure): _fields_ = [("length", ctypes.c_ushort), ("filter", ctypes.c_void_p)] def install(): libc = ctypes.CDLL(None, use_errno=True) buffer = ctypes.create_string_buffer(program) filter_program = Program(len(program) // 8, ctypes.cast(buffer, ctypes.c_void_p)) # PR_SET_NO_NEW_PRIVS, then PR_SET_SECCOMP with SECCOMP_MODE_FILTER. if libc.prctl(38, 1, 0, 0, 0) or libc.prctl(22, 2, ctypes.byref(filter_program), 0, 0): raise OSError(ctypes.get_errno(), "could not hide Landlock from the PDF worker") return install def check(binary, output=None): check_mode(binary, output, None) if sys.platform == "linux": check_mode(binary, None, without_landlock()) def check_mode(binary, output, preexec): mode = " without Landlock" if preexec else "" data = fixture() for index, zoom in [(0, 100), (1, 100), (0, 200)]: start = time.monotonic() result = subprocess.run([binary, "--render-pdf-page"], input=struct.pack(" 2 else None)