不正なコードポイントによるコンパイラクラッシュを修正
CSSエスケープシーケンスの展開時に不正なコードポイントが渡されると String.fromCodePoint が例外を投げてコンパイラがクラッシュする問題を修正しました。仕様に従い、不正なコードポイントを \uFFFD(置換文字)に置き換えることで、安全に処理を継続できるようになります。
背景
Windowsの開発環境でTailwind CSS v4とVite、Tauriを組み合わせて使用した際に、コンパイラがクラッシュするバグが #19786 として報告されました。
クラッシュの引き金となったのは、Tauriのdev serverがランタイムスクリプトに埋め込む以下のようなパス+UUID文字列です。
--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924
この文字列は -- で始まるためCSSカスタム変数の候補として認識されます。その後、コンパイラが変数名をアンエスケープする際に、Windowsのパス区切り文字 \ と直後の6桁の16進数 d88195 がCSSの16進エスケープシーケンスとして解釈されます。parseInt("d88195", 16) は 14188949(0xD88195)となり、Unicodeの最大コードポイント 0x10FFFF を超えるため String.fromCodePoint(14188949) が例外を投げていました。
同様の問題は #19801 でも報告されており、グローバルな .gitignore で除外されたファイルがスキャン対象になった場合にも発生していました。回避策として @source not "…"; でファイルを除外する方法はありましたが、根本的な修正が必要でした。
技術的な変更
修正の核心は packages/tailwindcss/src/utils/escape.ts の unescape 関数で、String.fromCodePoint を呼び出す前にコードポイントの妥当性を検証するガード節を追加した点です。
変更前:
export function unescape(escaped: string) {
return escaped.replace(/\\([\dA-Fa-f]{1,6}[\t\n\f\r ]?|[\S\s])/g, (match) => {
return match.length > 2
? String.fromCodePoint(Number.parseInt(match.slice(1).trim(), 16))
: match[1]
})
}
変更後:
export function unescape(escaped: string) {
return escaped.replace(/\\([\dA-Fa-f]{1,6}[\t\n\f\r ]?|[\S\s])/g, (match) => {
if (match.length <= 2) {
return match[1]
}
let codePoint = Number.parseInt(match.slice(1).trim(), 16)
if (
// Invalid codepoint: https://infra.spec.whatwg.org/#code-point
codePoint === 0x0000 ||
codePoint > 0x10ffff ||
// Is surrogate: https://infra.spec.whatwg.org/#leading-surrogate
// - A leading surrogate is a code point that is in the range U+D800 to U+DBFF, inclusive.
// - A trailing surrogate is a code point that is in the range U+DC00 to U+DFFF, inclusive.
(codePoint >= 0xd800 && codePoint <= 0xdfff)
) {
return '\uFFFD'
}
return String.fromCodePoint(codePoint)
})
}
バリデーションは以下の3条件でコードポイントの不正を判定します。
-
codePoint === 0x0000:NULLコードポイント -
codePoint > 0x10FFFF:Unicodeの有効範囲外(0x0000〜0x10FFFFが有効) -
codePoint >= 0xD800 && codePoint <= 0xDFFF:サロゲートコードポイント(leadingサロゲートとtrailingサロゲートの両範囲をカバー)
これらの条件に該当する場合は '\uFFFD'(Unicode置換文字)を返し、String.fromCodePoint の呼び出しを回避します。これはCSSパーサ仕様 CSS Syntax Level 3 で規定されている動作と一致します。
テストは2箇所に追加されています。packages/tailwindcss/src/index.test.ts では問題の文字列を候補として渡した際にクラッシュしないことを検証し、packages/tailwindcss/src/utils/escape.test.ts ではアンエスケープ後の文字列が \uFFFD を含む期待値と一致することを確認しています。
設計判断
CSSパーサ仕様に準拠した フェイルセーフな変換を選択した点が、この修正の設計上の要点です。
不正なコードポイントを検出した際の選択肢としては「例外を再スローする」「空文字列を返す」「置換文字を返す」などが考えられます。今回は仕様に明示された \uFFFD への置換が採用されました。これにより、候補文字列のアンエスケープが失敗してもコンパイル処理全体を中断せず、不正なCSSカスタム変数候補は最終的に無効な変数として無視される流れになります。
また、既存の正規表現とロジックの大枠は変えず、ガード節の挿入のみで修正を完結させています。変更行数を最小化しながら、仕様準拠のバリデーションを一か所に集約した構成です。
まとめ
String.fromCodePoint 呼び出し前にCSSパーサ仕様準拠のバリデーションを追加することで、Windowsパスを含む文字列やグローバルgitignoreで除外されたファイルなど、予期しない入力によるクラッシュを根本的に解消しました。仕様の置換文字 \uFFFD を使用する設計により、コンパイラの堅牢性が向上しています。