Core: reject U+FEFF in emoji icon payloads to match the JS resolver

Python's validate_icon accepted emoji: (not str.isspace(), category
Cf) while the generated JS isEmojiPayload rejects it via \s, so the backend
could store a key the admin and panel silently fall back on. An exhaustive
code-point sweep shows U+FEFF is the only divergence; add it to the
forbidden set. ZWJ U+200D stays valid.

Add tools/icons/emoji-payload-cases.json, a shared verdict table asserted
by both tests/test_core_icons.py (validate_icon) and the admin Vitest
suite (resolveIcon), so any future divergence fails one of the suites.

Task: 8286a894-133f-4582-90fb-88f47eef6628 (review finding NQ-ICON-01)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 08:44:44 -04:00
co-authored by Claude Opus 5.5
parent 7eed8fdbf1
commit 23d21977c6
4 changed files with 101 additions and 4 deletions
+9 -3
View File
@@ -6,8 +6,9 @@ namespace is one of :data:`ICON_NAMESPACES`:
- ``lucide:<name>`` / ``fa:<name>`` — ``<name>`` is a lowercase kebab
name (ASCII ``[a-z0-9-]``, not starting or ending with ``-``).
- ``emoji:<payload>`` — a short emoji string of at most
:data:`EMOJI_MAX_CODE_POINTS` code points, with no whitespace, no
control characters and none of ``<``, ``>``, ``&``.
:data:`EMOJI_MAX_CODE_POINTS` code points, with no whitespace (JS
``\\s``, so including U+FEFF), no control characters and none of
``<``, ``>``, ``&``.
Rows written before namespacing hold a bare Lucide name (``dog``).
:func:`validate_icon` (writes) normalizes such a legacy value to
@@ -33,7 +34,12 @@ ICON_NAMESPACES = frozenset(
EMOJI_MAX_CODE_POINTS = 8
_KEBAB_NAME = re.compile(r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
_EMOJI_FORBIDDEN = frozenset("<>&")
# U+FEFF (BOM / zero-width no-break space) is neither str.isspace() nor
# ``Cc`` in Python, but JS ``\s`` matches it, so it is listed here to keep
# this rule identical to the frontends' isEmojiPayload (/[\s<>&]|\p{Cc}/u).
# Other format characters, notably the ZWJ U+200D that emoji sequences
# need, stay valid in both runtimes.
_EMOJI_FORBIDDEN = frozenset("<>&\ufeff")
def _is_kebab_name(value: str) -> bool: