strict_locals の非ASCII文字デフォルト値で発生するエンコーディングエラーを修正
File.binread でロードされたテンプレートで strict locals に非ASCII文字のデフォルト値(例: label: "café")を使用した際に発生する Encoding::CompatibilityError を修正しました。原因となっていた @strict_locals のエンコーディングタグを、force_encoding で再設定する1ブロックの追加で解消しています。
背景
File.binread でテンプレートを読み込むと、ソース文字列は ASCII-8BIT(BINARY)タグで生成されます。ActionView::Template はその後 encode! メソッドで Encoding.default_external(通常 UTF-8)に再タグ付けしますが、strict locals の抽出はこの encode! よりも前に行われていました。
strict_locals! は正規表現キャプチャ($1)で locals 宣言を取り出しますが、キャプチャ文字列はソースのエンコーディングタグを引き継ぐため、@strict_locals は ASCII-8BIT タグのままになります。続いて compiled_source のヒアドキュメントがこの ASCII-8BIT 文字列を UTF-8 タグの ERB ハンドラ出力と文字列結合しようとすると、両者に 0x7F を超えるバイトが含まれている場合に Ruby のエンコーディング調停が Encoding::CompatibilityError を送出します。
なお、テンプレート本文に 0x7F を超えるバイトが含まれない場合はエラーは発生しませんが、コンパイル済みソースのエンコーディングタグが UTF-8 ではなく ASCII-8BIT のままになるという別の問題が残ります。
技術的な変更
strict_locals! に force_encoding(Encoding.default_external) の呼び出しを追加し、@strict_locals のエンコーディングタグを encode! がソース本体に対して行うものと同じ処理で揃えます。
変更前:
def strict_locals!
# ...
return if @strict_locals.nil? # Magic comment not found
@strict_locals = "**nil" if @strict_locals.blank?
end
変更後:
def strict_locals!
# ...
return if @strict_locals.nil? # Magic comment not found
# Tag with the assumed encoding before encode! runs, same as
# encode! does for the source itself (see above).
if @strict_locals.encoding == Encoding::BINARY
@strict_locals.force_encoding(Encoding.default_external)
end
@strict_locals = "**nil" if @strict_locals.blank?
end
これは ラベル変更のみ(force_encoding)であり、バイト列の変換(encode)は行いません。File.binread が UTF-8 バイト列を ASCII-8BIT タグで保持しているだけであり、バイト列自体は正しい UTF-8 エンコードのため、タグを付け替えるだけで十分です。
テストでは、\xC3\xA9(U+00E9、é)と \xC3\xBC(U+00FC、ü)を含む ASCII-8BIT 文字列を直接生成し、レンダリング結果のエンコーディングが UTF-8 になること、および内容が正しくレンダリングされることを検証しています。
def test_strict_locals_with_non_ascii_default_values
# \xC3\xA9 = U+00E9 (e with acute), \xC3\xBC = U+00FC (u with diaeresis)
source = "<%# locals: (label: \"caf\xC3\xA9\") -%>\n<p><%= label %> \xC3\xBC</p>"
assert_equal Encoding::ASCII_8BIT, source.encoding
@template = new_template(source)
assert_equal Encoding::UTF_8, render.encoding
assert_equal "<p>caf\u{E9} \u{FC}</p>", render
end
設計判断
encode! と同じ再タグ付けパターンを strict_locals! にも適用するという 一貫性のある修正 が採用されました。
修正箇所は @strict_locals.encoding == Encoding::BINARY の条件ガードを設けた上で force_encoding を呼び出すだけであり、ASCII-8BIT 以外のエンコーディングで読み込まれたテンプレートには影響しません。また encode(バイト変換)ではなく force_encoding(タグ付け替え)を選択したのは、File.binread が保持するバイト列がすでに正しい UTF-8 バイト列であるという前提に基づいています。この前提は encode! がソース本体に対して行う処理と同一であり、既存の設計と整合的です。
まとめ
本修正は、strict_locals! が encode! より先に実行されることで生じるエンコーディングタグの不一致を、6行の追加で解消しています。force_encoding という最小限の手術により、バイト列を変換せずにエンコーディングの一貫性を回復する Ruby のエンコーディング設計の活用例として参考になる変更です。