Environment
main @ 54b93e33, reproduced on Node 22.
Description
#371 added a grapheme-cluster snap with a length ceiling — GRAPHEME_SNAP_MAX_LENGTH = 8192 — past which the snap degrades to surrogate pairs only. The ceiling is documented, and tested, as a bound on the field text.
It is not measured against the field text. truncateHTMLFromStart builds its snapper from html, which is content: the value after escaping and after <mark> insertion. content.length is always >= value.length, and escaping inflates it without any fixed bound — & becomes five characters, " becomes six.
So the ceiling bites early, and how early depends on the content:
const FLAG = '\u{1F1FA}\u{1F1F8}' // 🇺🇸
// value.length = 8185 — seven characters under the documented ceiling
'a'.repeat(8140) + FLAG.repeat(10) + 'match'
// value.length = 1805 — 22% of the documented ceiling
'&'.repeat(1600) + FLAG.repeat(50) + 'match'
Both produce:
"...🇸🇺🇸🇺🇸🇺🇸🇺🇸🇺🇸🇺🇸<mark>match</mark>"
The flags re-pair into 🇸🇺 with an orphaned regional indicator at the end — a different country, with nothing to signal the loss. That is exactly the defect #364 describes and #371 exists to prevent; it simply reappears above a threshold that no reader can predict from the constant.
Why the tests did not catch it
The degraded paths block does test a 13-character discrepancy — but every fixture in it uses 'a'.repeat(...) as filler. No fixture contains an escapable character, so only the fixed single-mark overhead is covered, never the unbounded part.
.sync/PORTING.md records the discrepancy as "the threshold is crossed 13 characters earlier than a reading of value.length suggests". That understates it: the real bound is O(escapable characters), not a constant.
Additional context
Not a security issue — escaping happens before this, and the output is escaped either way. The consequence is wrong visible text.
Found by an independent review pass over #371.
Environment
main@54b93e33, reproduced on Node 22.Description
#371 added a grapheme-cluster snap with a length ceiling —
GRAPHEME_SNAP_MAX_LENGTH = 8192— past which the snap degrades to surrogate pairs only. The ceiling is documented, and tested, as a bound on the field text.It is not measured against the field text.
truncateHTMLFromStartbuilds its snapper fromhtml, which iscontent: the value after escaping and after<mark>insertion.content.lengthis always>= value.length, and escaping inflates it without any fixed bound —&becomes five characters,"becomes six.So the ceiling bites early, and how early depends on the content:
Both produce:
The flags re-pair into 🇸🇺 with an orphaned regional indicator at the end — a different country, with nothing to signal the loss. That is exactly the defect #364 describes and #371 exists to prevent; it simply reappears above a threshold that no reader can predict from the constant.
Why the tests did not catch it
The
degraded pathsblock does test a 13-character discrepancy — but every fixture in it uses'a'.repeat(...)as filler. No fixture contains an escapable character, so only the fixed single-mark overhead is covered, never the unbounded part..sync/PORTING.mdrecords the discrepancy as "the threshold is crossed 13 characters earlier than a reading ofvalue.lengthsuggests". That understates it: the real bound isO(escapable characters), not a constant.Additional context
Not a security issue — escaping happens before this, and the output is escaped either way. The consequence is wrong visible text.
Found by an independent review pass over #371.