package question import ( "encoding/binary" "testing" "github.com/stretchr/testify/require" ) // buildTTC 拼一个最小的 TrueType Collection:两个字面共用一套表, // 表数据刻意按「与目录相反」的顺序摆放,用来证明提取逻辑是按目录里的 offset // 搬数据,而不是简单地顺序切片。 func buildTTC(t *testing.T) []byte { t.Helper() // 表内容 headData := []byte{0xDE, 0xAD, 0xBE, 0xEF} cmapData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} const ttcHeaderLen = 16 const sfntHeaderLen = 12 const recLen = 16 const numTables = 2 sfntStart := ttcHeaderLen dirStart := sfntStart + sfntHeaderLen dataStart := dirStart + recLen*numTables // 先放 cmap 再放 head(与目录顺序相反) cmapOff := dataStart headOff := cmapOff + len(cmapData) buf := make([]byte, headOff+len(headData)) copy(buf[0:4], "ttcf") binary.BigEndian.PutUint32(buf[4:8], 0x00010000) // version binary.BigEndian.PutUint32(buf[8:12], 1) // numFonts binary.BigEndian.PutUint32(buf[12:16], uint32(sfntStart)) binary.BigEndian.PutUint32(buf[sfntStart:sfntStart+4], 0x00010000) // sfntVersion binary.BigEndian.PutUint16(buf[sfntStart+4:sfntStart+6], numTables) binary.BigEndian.PutUint16(buf[sfntStart+6:sfntStart+8], 16) // searchRange(随便填,提取时应重算) binary.BigEndian.PutUint16(buf[sfntStart+8:sfntStart+10], 0) binary.BigEndian.PutUint16(buf[sfntStart+10:sfntStart+12], 0) // 第 1 条记录:head → 数据在末尾 p := dirStart copy(buf[p:p+4], "head") binary.BigEndian.PutUint32(buf[p+4:p+8], 0x11111111) // checksum binary.BigEndian.PutUint32(buf[p+8:p+12], uint32(headOff)) binary.BigEndian.PutUint32(buf[p+12:p+16], uint32(len(headData))) // 第 2 条记录:cmap → 数据在前 p = dirStart + recLen copy(buf[p:p+4], "cmap") binary.BigEndian.PutUint32(buf[p+4:p+8], 0x22222222) binary.BigEndian.PutUint32(buf[p+8:p+12], uint32(cmapOff)) binary.BigEndian.PutUint32(buf[p+12:p+16], uint32(len(cmapData))) copy(buf[cmapOff:], cmapData) copy(buf[headOff:], headData) return buf } func TestTTCFirstFont(t *testing.T) { ttc := buildTTC(t) out, err := ttcFirstFont(ttc) require.NoError(t, err) // sfnt header require.Equal(t, uint32(0x00010000), binary.BigEndian.Uint32(out[0:4])) require.Equal(t, uint16(2), binary.BigEndian.Uint16(out[4:6])) // searchRange = 最大 2 的幂 × 16 = 2 × 16 = 32;entrySelector = 1;rangeShift = 32-32 = 0 require.Equal(t, uint16(32), binary.BigEndian.Uint16(out[6:8])) require.Equal(t, uint16(1), binary.BigEndian.Uint16(out[8:10])) require.Equal(t, uint16(0), binary.BigEndian.Uint16(out[10:12])) // 目录:tag / checksum / length 原样保留,offset 指向新位置 type rec struct { tag string checksum uint32 off, ln uint32 } var recs []rec for i := 0; i < 2; i++ { p := 12 + 16*i recs = append(recs, rec{ tag: string(out[p : p+4]), checksum: binary.BigEndian.Uint32(out[p+4 : p+8]), off: binary.BigEndian.Uint32(out[p+8 : p+12]), ln: binary.BigEndian.Uint32(out[p+12 : p+16]), }) } require.Equal(t, "head", recs[0].tag) require.Equal(t, "cmap", recs[1].tag) require.Equal(t, uint32(0x11111111), recs[0].checksum) require.Equal(t, uint32(0x22222222), recs[1].checksum) // 表数据按新 offset 读出来必须与原始一致 require.Equal(t, []byte{0xDE, 0xAD, 0xBE, 0xEF}, out[recs[0].off:recs[0].off+recs[0].ln]) require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, out[recs[1].off:recs[1].off+recs[1].ln]) // 每张表在新文件里都要 4 字节对齐(sfnt 规范要求) require.Zero(t, recs[0].off%4) require.Zero(t, recs[1].off%4) } func TestTTCFirstFontRejects(t *testing.T) { cases := map[string][]byte{ "空数据": nil, "不是 TTC": []byte("not a font file at all"), "头部被截断": []byte("ttcf\x00\x01\x00\x00"), "numFonts 为 0": func() []byte { b := make([]byte, 16) copy(b, "ttcf") binary.BigEndian.PutUint32(b[4:8], 0x00010000) return b }(), "子字体偏移越界": func() []byte { b := make([]byte, 16) copy(b, "ttcf") binary.BigEndian.PutUint32(b[4:8], 0x00010000) binary.BigEndian.PutUint32(b[8:12], 1) binary.BigEndian.PutUint32(b[12:16], 4096) return b }(), } for name, data := range cases { t.Run(name, func(t *testing.T) { _, err := ttcFirstFont(data) require.Error(t, err) }) } } // TestTTCFirstFontRejectsBadTableOffset 表目录里的 offset 指向文件外时必须报错, // 而不是把越界切片交给上层(否则就是一次 panic 而不是 500)。 func TestTTCFirstFontRejectsBadTableOffset(t *testing.T) { ttc := buildTTC(t) // 第 1 条记录位于 ttcHeader(16) + sfntHeader(12),其 length 字段在记录内第 12 字节 p := 16 + 12 + 12 binary.BigEndian.PutUint32(ttc[p:p+4], 1<<20) _, err := ttcFirstFont(ttc) require.Error(t, err) }