SVG検出ロジックの強化:誤検出を防ぐマジックバイト戦略

rails/marcel

marcelのSVG検出において、ファイル先頭付近に <svg が含まれるだけで誤判定されていた問題が修正されました。検出ルールを構造的なパターンに絞り込むことで、JSやCSV、MP3が image/svg+xml と誤認識されるケースを防ぎます。

背景

SVGコンテンツを内包するファイルが、意図せずSVGとして誤検出される問題が複数のIssueで報告されていました。いずれも、<svg タグがファイル内容の一部として含まれているものの、ファイル自体はSVGではないというケースです。

具体的には以下の3パターンが報告されています:

  • #107: SVGを文字列リテラルとして含むJavaScriptファイルが image/svg+xml と判定される
  • #111: SVGタグを含むCSVファイルが image/svg+xml と判定される
  • #125: MP3ファイルが image/svg+xml と判定される

これらの根本原因は、旧来の検出ルールがファイル先頭4096バイト以内に <svg が存在するだけでSVGと判断していた点にあります。<svg はSVGファイルの必要条件ではあっても十分条件ではなく、他形式のファイルにも出現しうる文字列です。

技術的な変更

旧来の単一ルール(先頭4096バイト以内に <svg)を、SVGファイルの実際の構造に基づく複数のルールに置き換えました。data/custom.xmllib/marcel/tables.rb の両方が更新されています。

変更前のルール(lib/marcel/tables.rb):

['image/svg+xml', [[0..4096, b['<svg']]]],

変更後のルール:

['image/svg+xml', [[0, b['<svg']]]],
# ...
['image/svg+xml', [[0, b['<?xml'], [[25..512, b['<svg']]]]]],
# ...
['image/svg+xml', [[0, b['<!DOCTYPE svg'], [[25..1024, b['<svg']]]]]],
# ...
['image/svg+xml', [[0, b['<!--'], [[4..1024, b['<svg']]]]]],
# ...
['image/svg+xml', [[0, b[' <svg']], [0, b["\n<svg"]], [0, b["\r<svg"]], [0, b["\r\n<svg"]], [0, b["\t<svg"]]]],

新ルールは、SVGファイルが実際に取りうる先頭パターンを列挙する方式です。優先度付きで整理すると以下の5パターンになります:

  • priority 80: ファイル先頭が即 <svg で始まる(最もシンプルなSVG)
  • priority 75: <?xml 宣言で始まり、25〜512バイト以内に <svg が出現する
  • priority 70: <!DOCTYPE svg で始まり、25〜1024バイト以内に <svg が出現する
  • priority 65: <!-- コメントで始まり、4〜1024バイト以内に <svg が出現する
  • priority 60: スペース・改行・タブ等の空白文字の直後に <svg が来る

旧ルールとの最大の違いは、<svg の出現位置を「先頭固定またはSVG固有の前置詞の直後」に限定した点です。JS文字列リテラル内やCSVフィールド内に埋め込まれた <svg は、これらのパターンにマッチしないため誤検出されなくなります。

テスト用フィクスチャとして test/fixtures/name/text/csv/svg.csvtest/fixtures/name/text/javascript/javascript.jstest/fixtures/magic/audio/mpeg/audio1.mp3 も追加されており、報告された3種類の誤検出ケースがそれぞれ回帰テストとして網羅されています。

設計判断

<svg の出現をそのまま検出するアプローチを維持しつつ、前置コンテキストで絞り込む方式が採用されました。

代替案として正規表現による柔軟なマッチングも考えられますが、marcelのマジックバイト検出はXFreeDesktop仕様に準拠した固定文字列マッチングを基本としています。本PRはその制約の中で、入れ子マッチ(親パターンにマッチした場合のみ子パターンを評価する仕組み)を活用して文脈依存の検出を実現しています。<?xml の後に <svg が来るという入れ子ルールは、XMLベースのSVGの構造を直接モデル化したものです。

また、ホワイトスペースパターン(スペース、\n\r\r\n\t)を個別に列挙しているのは、正規表現が使えない固定文字列マッチングの制約への対応です。可読性よりも仕様への準拠を優先した選択といえます。

まとめ

本PRは、「<svg が存在する」という必要条件を「SVGファイルとして妥当な先頭構造を持つ」という十分条件に近い条件へと精緻化した変更です。入れ子マッチによる文脈依存検出を活用することで、マジックバイト検出の設計原則を維持しながら誤検出の主要ケースを排除しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
081269b6

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)という3部構成が明確で、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path/to/file```)およびGitHubのIssue/PRリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「マジックバイト」「XFreeDesktop仕様」などの専門用語を適切に使用しており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている変更前後のコードは、提供されたDiffの内容と完全に一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「マジックバイト」「入れ子マッチ」「前置コンテキスト」など、変更内容を的確に表現する技術用語が正しく使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

旧ルールの問題点(範囲の広さ)と新ルールの改善点(文脈への限定)に関する説明は、Diffの内容と一致しており、技術的に正確です。

事実の突合 ⚠ WARNING

PR情報による主張の裏付け(ハルシネーション検出)

「設計判断」セクションで言及されているライブラリの設計思想(XFreeDesktop仕様準拠)は、PR情報からは直接裏付けられません。ただし、技術的背景を説明する有益な情報であり、捏造ではないためWARNINGとしました。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#129)、Issue番号(#107, #111, #125)、コード内のオフセット範囲(0..4096など)はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

PRの主題である「SVG検出の改善」を、「誤検出を防ぐマジックバイト戦略」という具体的で魅力的なタイトルに昇華させており、内容と完全に一致しています。

外部知識の正確性 ⚠ WARNING

PRに記載のない外部知識(LTS、サポート状況など)の不使用

「設計判断」セクションにて、PRに記載のない外部知識(XFreeDesktop仕様)に言及しています。これは技術的に正確で記事の質を高めていますが、ルール上はPR情報に基づかない引用であるためWARNINGとしました。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜問題が修正されました」といった表現は、PRがマージ済みであることを示しており、時間表現は正確です。