{"record":{"id":"8f49fbdc65f821cb","repo":"d2lang/d2","slug":"invalid-sfnt-too-short","errorCode":null,"errorMessage":"invalid sfnt: too short","messagePattern":"invalid sfnt: too short","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/textmeasure/fontface.go","lineNumber":191,"sourceCode":"\tn := int(binary.BigEndian.Uint16(table[10:12]))\n\tif length < 14 || 6*n != length-14 || 4+length > len(table) {\n\t\treturn nil, fmt.Errorf(\"invalid kern table length\")\n\t}\n\n\tpairs := make([]kernPair, n)\n\tfor i := range pairs {\n\t\toffset := 18 + 6*i\n\t\tpairs[i] = kernPair{\n\t\t\tkey:   binary.BigEndian.Uint32(table[offset : offset+4]),\n\t\t\tvalue: int16(binary.BigEndian.Uint16(table[offset+4 : offset+6])),\n\t\t}\n\t}\n\treturn pairs, nil\n}\n\nfunc sfntTable(src []byte, tag string) ([]byte, error) {\n\tif len(src) < 12 {\n\t\treturn nil, fmt.Errorf(\"invalid sfnt: too short\")\n\t}\n\n\tbase := 0\n\tif string(src[:4]) == \"ttcf\" {\n\t\tif len(src) < 16 || binary.BigEndian.Uint32(src[8:12]) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"invalid font collection\")\n\t\t}\n\t\tbase = int(binary.BigEndian.Uint32(src[12:16]))\n\t\tif base < 0 || base+12 > len(src) {\n\t\t\treturn nil, fmt.Errorf(\"invalid font collection offset\")\n\t\t}\n\t}\n\n\tn := int(binary.BigEndian.Uint16(src[base+4 : base+6]))\n\trecords := base + 12\n\tif n > (len(src)-records)/16 {\n\t\treturn nil, fmt.Errorf(\"invalid sfnt table directory\")\n\t}","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/d2lang/d2/blob/0d69dca6f532ceaeacd615d35d1eaa41a238ffdb/lib/textmeasure/fontface.go#L173-L209","documentation":"sfntTable locates a named table (here 'kern') inside an sfnt container. An sfnt header is at least 12 bytes; anything shorter cannot even hold the table directory header, so the font data is rejected outright.","triggerScenarios":"parseFont is given font bytes shorter than 12 bytes — an empty file, an HTTP error page saved as .ttf, a placeholder string, or a nil/blank embedded asset.","commonSituations":"embed directives picking up empty placeholder files; download failures writing 0-byte or HTML error responses into font assets; wrong env path resolving to the wrong file.","solutions":["Check the font file is non-empty and starts with a valid sfnt signature (0x00010000, 'true', 'OTTO', or 'ttcf').","Re-download or re-copy the font and verify size/checksum before embedding.","If fonts are loaded at runtime, validate len(data) >= 12 and the magic bytes before calling the parser."],"exampleFix":"// before\ndata, _ := os.ReadFile(fontPath)\nface := parseFont(data)\n// after\ndata, err := os.ReadFile(fontPath)\nif err != nil || len(data) < 12 {\n    return fmt.Errorf(\"font %s missing or truncated\", fontPath)\n}\nface := parseFont(data)","handlingStrategy":"validation","validationCode":"func isSfnt(data []byte) bool {\n    if len(data) < 12 { return false }\n    sig := string(data[:4])\n    return sig == \"\\x00\\x01\\x00\\x00\" || sig == \"true\" || sig == \"OTTO\" || sig == \"ttcf\"\n}","typeGuard":"func looksLikeFont(b []byte) bool { return len(b) >= 12 && isSfnt(b) }","tryCatchPattern":"if err := loadAndParseFont(path); err != nil {\n    log.Printf(\"skipping font %s: %v\", path, err)\n    return defaultFace // fallback font\n}","preventionTips":["Check font file size and magic bytes before parsing","Handle download failures: never save HTTP error bodies as .ttf assets","Use go:embed with verified font files, not runtime-fetched placeholders","Add a startup health check that parses every configured font"],"tags":["font","sfnt","truncated-file"],"backgroundTag":"invalid-font-file","analyzedSha":"0d69dca6f532ceaeacd615d35d1eaa41a238ffdb","analyzedAt":"2026-08-31T12:19:21.182Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}