Read no list at all where a GSUB or GPOS header states a NULL offset for it (#253) - #262
Open
jakejackson1 wants to merge 3 commits into
Open
jakejackson1 wants to merge 3 commits into
jakejackson1 wants to merge 3 commits into
Conversation
…for it (#253) A GSUB or GPOS whose ScriptList, FeatureList and LookupList are absent stores 0 for each of the three offsets. Adding the table offset to that points at the table's own header, where the majorVersion of 0x0001 reads as a count of one, so the table came back carrying a script whose tag is four NUL bytes and a lookup with no subtables. That cost four warnings a render and a fabricated script tag; a caller that promotes warnings to exceptions could not parse the font at all. readListOffsets() leaves a NULL offset as it stands rather than making it absolute, and listCount() answers nothing for one. All three list reads are counted there, so the guard is in one place rather than at each of them. readLookupList() also states Subtables before the loop that fills it. The format lets a Lookup state no subtables at all, and the seven walks of the list - two here, four in Otl and one in OtlDump - read the key without asking whether it is there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…reams (#253) The lookup list is written into .mtx.json, so the Subtables key this branch adds and the phantom entries it removes are a change to a persisted shape: without the version bump a warm tmp/ttfontdata keeps serving the old one, and Otl keeps reading an absent Subtables for a lookup that states none. assertDrawsSilently() already draws and asserts nothing was raised, so the shaping test no longer builds its own document, and a fontkey of its own per parse replaces the temporary directory the parsing tests were deleting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
_getGSUBtables()and_getGPOStables()added the table offset to each of the three list offsets inthe header without testing for NULL. A GSUB or GPOS whose lists are absent stores 0 in all three, and 0
plus the table offset is the table's own start — so the parser read the header as a list and
majorVersion0x0001 became a count of one: one script whose tag is four NUL bytes, and one lookup withno subtables. Four warnings a render, a fabricated script tag, and a caller that promotes warnings to
exceptions cannot parse the font at all.
readListOffsets()leaves a NULL offset as0rather than making it absolute, andlistCount()answers
0for one. All three list reads now take their count throughlistCount(), so the rule isstated once where it cannot be bypassed rather than three times where each has to remember it.
_getGDEFtables()is the in-repo precedent: it has always treated a NULL subtable offset as an absentsubtable.
The absent
Subtableskey is separately reachablereadLookupList()now statesSubtablesbefore the loop that fills it. The format sets no minimum onsubTableCount, and seven walks of the list index that key with noisset()— two inTTFontFile,four in
Otl, one inOtlDump. So a conforming font that states a lookup with no subtables warnsfour times at parse and twice per character while shaping, quite apart from the phantom lookup this PR
removes.
NotoSans-EmptyLookup-Syntheticis that font.The key is stated at construction rather than guarded at the reads because the lookup list is persisted
into
.mtx.json: a read-site guard would still write the malformed shape to the cache.MetricsGenerator::CACHE_FORMAT3 → 4.mtx.jsonstoresGSUBScriptLang,GSUBLookupsandGPOSLookups, and this change alters all three —a new
Subtableskey on every lookup, and the phantom script and lookup gone. The constant's owndocblock names "the keys of an array" as the trigger, and it was raised 1 → 3 for exactly this class of
change. Without the bump the cache is regenerated only when the font file's size changes, so a warm
tmp/ttfontdatakeeps serving the phantom script and the absent key after upgrade — the render-timehalf of the same defect.
Fonts
Two three-glyph subsets of
tests/data/ttf/NotoSans-Regular.ttf, whose OFL 1.1 notice("Copyright 2015-2021 Google LLC") reserves no name, so clause 3 leaves a modified version free to be
named for itself, as
NotoSansTC-*-Syntheticalready is. Built with fontTools andTTFont(src, recalcTimestamp=False).NotoSans-NullOtlLists-Synthetic— GSUB and GPOS are nothing but the ten bytes00 01 00 00 00 00 00 00 00 00, byte-for-byte what the fivegoogle/fontsfamilies in the issue ship.It carries a GDEF, without which
useOTLwould refuse the font before the GSUB read. Covers both_getGSUBtables()and_getGPOStables().NotoSans-EmptyLookup-Synthetic— a reallatnScriptList, a realligaFeatureList, and aLookupList whose one Lookup states no subtables. The conforming-font route to the same absent key.
The five
google/fontsfamilies themselves are not added to the repository.hb-shape14.3.1 shapesboth synthetics:
[A=0+639|B=1+650], so neither is malformed.Fixtures
No existing master moves. The eight new ones are the two fonts'
subset,otldump,shapingandfontcachefiles.fontcache/NotoSans-NullOtlLists-Synthetic.jsonis itself a regression test for the phantom tag: allsix of
GSUB/GPOSScriptLang,FeaturesandLookupsare[], and the two.datblobs are "10bytes".
otldumpreports "No entries in GSUB table" and records no diagnostics.fontcache/NotoSans-EmptyLookup-Synthetic.jsonkeepslatn/ligaand one lookup with"Subtables": [].The two
shapingfiles differ only on the Indic runs, and each matches an existing master for a font inthe same position: NullOtlLists reorders Devanagari
0915 094D 0937 093F→0915 094D 093F 0937exactlyas
NotoSans-GPOS3-Syntheticdoes, whoseGSUBScriptLangis also[]; EmptyLookup leaves it aloneexactly as
NotoSans-GSUBClassZero-Syntheticdoes, whose GSUB also haslatn.Verified
All six tests in
tests/Mpdf/NullOtlListOffsetsTest.phpfail against the previousTTFontFile.php,each on its own observable from the issue:
Upstream
The issue had not measured this.
mpdf/mpdfdevelopmentat744f75ashows both phantoms and alsowarns four times, but at different sites —
TTFontFile.php:1535and:3377, the feature-ordering linethis fork already guards for Sedan SC's empty
smcp, rather than at the twoSubtableswalks, whoseloops upstream bounds by the count. Upstream also leaves
Subtablesabsent for a zero-subtable lookup,raising nothing at parse but carrying the same malformed shape.
The same rule is unguarded one level down, in
MarkBasePosandMarkMarkPos, and is not fixed here:#260.
Closes #253
🤖 Generated with Claude Code