`tika.xml` の正規表現ルールをMIMEタイプ判定に組み込む

rails/marcel

Apache Tikaの tika.xml に定義された正規表現マッチルールをMarcelのMIMEタイプ判定に取り込み、バイト列マッチだけでは困難だった text/htmlapplication/x-bzip2 の検出精度を向上させます。あわせて、これまで無効なまま MAGIC 定数に残留していたゴミエントリを除去し、コードベースの健全性も高めます。

背景

MarcelはMIMEタイプの判定にApache Tikaの tika.xml を変換した MAGIC テーブルを利用していますが、Tikaのルール定義には type="regex" による正規表現マッチが含まれます。これまでMarcelはこのマッチタイプを解釈できず、正規表現パターンをバイト列としてそのまま扱っていました。

問題の深刻さは生成スクリプトのログに表れていました。script/generate_tables.rb はテーブル生成時に warn "#{mime['type']}: unsupported #{type} match: #{match.to_s}" と警告を出力していましたが、変換後の文字列は 依然として MAGIC 定数に格納され続けていました。その結果、application/x-dbfmessage/rfc822application/illustrator+ps などのエントリが、正規表現パターン文字列をバイト列として比較するという誤った形で判定ロジックに混入しており、これらが実際にマッチすることはないものの、メモリと演算を無駄に消費する状態が続いていました。

text/html については、この問題への対処として lib/marcel/mime_type/definitions.rb に独自のマジックバイトルールが別途定義されていました。今回のPRはその暫定定義を不要にし、Tikaの正規表現ルールで代替します。

技術的な変更

正規表現サポートの中核は、生成スクリプトへの TikaRegex モジュールの追加と、ランタイムへの match_regex メソッドの追加という2つの変更によって実現されています。

Javaの正規表現をRubyへ変換する TikaRegex モジュール

script/generate_tables.rb に追加された TikaRegex.to_ruby_regexp は、TikaのJava正規表現構文をRubyの Regexp オブジェクトに変換します。変換が必要な主な差異は以下のとおりです:

  • (?s) フラグ: Javaのdotallモード(.が改行にもマッチ)をRubyの Regexp::MULTILINE に変換
  • 二重エスケープ: XMLとJava文字列の二重エスケープ \\xHH をRubyの \xHH に変換
  • 8進数エスケープ: \\OOO を16進数 \xHH に変換(TruffleRubyが後方参照と誤解するのを防ぐため)
  • 非対応パターン: Javaの可変長後読みなど、Rubyで解釈できないパターンは nil を返して除外

変換後のパターンはバイナリエンコーディング(Encoding::BINARY)で処理され、\xff などのバイト列にも対応します。

def self.to_ruby_regexp(pattern)
  return nil if pattern.nil? || pattern.empty?

  processed = pattern.dup
  flags = 0

  if processed.include?('(?s)')
    processed = processed.gsub('(?s)', '')
    flags |= Regexp::MULTILINE
  end

  processed = processed.gsub(/\\\\(x[0-9a-fA-F]{2})/, '\\\\\1')     # \\xHH -> \xHH
                       .gsub(/\\\\(u[0-9a-fA-F]{4})/, '\\\\\1')     # \\uHHHH -> \uHHHH
                       .gsub(/\\\\([0-7]{1,3})/) { "\\x#{$1.to_i(8).to_s(16).rjust(2, '0')}" }
                       .gsub(/\\\\([WDS])/i, '\\\\\1')
                       .gsub(/\\\\([farbentv])/, '\\\\\1')
                       .gsub(/\\\\([()|*+?.^$\\\\])/, '\\\\\1')

  processed = processed.force_encoding(Encoding::BINARY)
  Regexp.new(processed, flags).freeze
end

ランタイムでの正規表現マッチ: match_regex

lib/marcel/magic.rbmagic_match_io メソッドに、valueRegexp インスタンスである場合の分岐が追加されました。

変更前:

match =
  if value
    if Range === offset
      io.read(offset.begin, buffer)
      x = io.read(offset.end - offset.begin + value.bytesize, buffer)
      x && x.include?(value)
    else
      # ...
    end
  end

変更後:

match =
  if value
    if value.is_a?(Regexp)
      match_regex(io, offset, value, buffer)
    elsif Range === offset
      io.read(offset.begin, buffer)
      x = io.read(offset.end - offset.begin + value.bytesize, buffer)
      x && x.include?(value)
    else
      # ...
    end
  end

追加された match_regex メソッドは、指定されたオフセットまでシークしてから先頭256バイトを読み出し、そのデータに対して match? を実行します。

def self.match_regex(io, offset, regexp, buffer)
  start = offset.is_a?(Range) ? offset.begin : offset
  io.read(start, buffer) if start > 0
  data = io.read(256, buffer)
  return false unless data

  data.match?(regexp)
end

有効化対象と well_known_regex_types

36個の正規表現ルールのうち、現時点で有効化されているのは application/x-bzip2text/html のみです。PR説明によれば、application/x-dbf のパターンはRubyの正規表現エンジンとの非互換により変換に失敗します。フィクスチャファイルが存在しテストで検証されているものだけを有効化する well_known_regex_types という変数が導入され、実用上の安全性が確保されています。

この変更により、lib/marcel/mime_type/definitions.rb に独自定義されていた text/html のマジックバイトルールは削除されました。また image/x-raw-sony には、ソニーRAWの誤検知を防ぐための魔法バイト定義(IFDヘッダと 'SONY' 文字列の組み合わせ)が新たに追加されています。

MAGIC テーブルから不正エントリを除去

生成スクリプトの修正により、to_ruby_regexpnil を返した正規表現パターン(Rubyと非互換なパターン)はテーブルに含まれなくなりました。これにより、これまで MAGIC 定数に混入していた以下のような無効エントリが除去されます:

  • application/illustrator+ps: [\r\n]%AI5_FileFormat のような正規表現文字列がバイト列として格納されていた
  • message/rfc822: 複雑なネスト構造の正規表現が誤ったルールとして存在していた
  • application/x-dbf: 可変長後読みを含むパターンがRubyと非互換なまま残存していた

その他 tables.rb では、Markdownファイル拡張子(markdownmdmdtextmkd)のMIMEタイプが text/x-web-markdown から text/markdown に更新され、data/tika.xml がアップストリームの最新版(2026-05-19付け)に同期されています。

設計判断

全正規表現を一度に有効化せず、テスト済みのものだけを段階的に導入する戦略が採られています。

RubyはCRuby、JRuby、TruffleRubyでそれぞれ正規表現エンジンが異なるため、Javaで動作していたパターンがすべてのRuby実装で安全に動作するとは限りません。実際にPR開発中にTruffleRubyとJRubyで失敗が発生したことが説明に記されており、8進数エスケープを16進数に変換する処理はTruffleRuby対策として追加されています。well_known_regex_types によるホワイトリスト方式は、この現実的な制約に対する慎重な対応です。

またテストには「ランダムなテストデータがいずれの正規表現にもマッチしないこと」を確認するケースと、「親と子のネストマッチが両方成立する場合にのみマッチすること」を確認するユニットテストが追加されています。正規表現マッチの誤検知リスクを継続的に監視できる仕組みが同時に整備されており、機能追加と品質保証がセットで行われている点が特徴的です。

まとめ

このPRは、Tikaの正規表現ルールという長年未活用だった情報を段階的かつ安全に取り込む仕組みをMarcelに追加しました。well_known_regex_types によるホワイトリスト方式と、to_ruby_regexp による変換失敗時の nil 返却を組み合わせることで、互換性リスクを抑えながら拡張の余地を残した設計になっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6f116e1a

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という理想的な3部構成が明確に適用されています。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```言語:ファイルパス)およびPR番号のリンク記法([#132](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

MIMEタイプ判定の内部実装やRubyの正規表現エンジンの差異など、専門知識を持つエンジニアを対象とした適切な内容と粒度で記述されています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内で引用されている `script/generate_tables.rb` および `lib/marcel/magic.rb` のコードブロックは、提供されたDiffの内容と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「dotallモード」「二重エスケープ」「8進数エスケープ」「可変長後読み」など、正規表現に関する専門用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

Java正規表現からRuby正規表現への変換ロジックや、`match_regex`メソッドの動作、`well_known_regex_types`の役割に関する説明が、Diffのコードと完全に整合しています。

事実の突合 ✓ PASS

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

「有効化されているのは2つのMIMEタイプのみ」「TruffleRubyで失敗した経緯」など、記事内のすべての主張がPR DescriptionやDiffで裏付けられており、ハルシネーションは見られません。

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

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

PR番号(#132)、正規表現ルール数(36個)、日付(2026-05-19)などの数値や固有名詞はすべて正確です。

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

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

記事タイトル「`tika.xml` の正規表現ルールをMIMEタイプ判定に組み込む」は、PRの主題である「add `tika.xml` regex support」を的確に反映しています。

外部知識の正確性 ✓ PASS

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

記事に含まれる情報はすべてPR情報(Title, Description, Diff)に基づいており、サポート状況やリリース日程といったPR外の知識の追加はありません。

時間表現の正確性 ✓ PASS

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

「これまで...解釈できず」「...残留していたゴミエントリを除去し」など、過去の状態と今回の変更点を区別する時間表現がPRの文脈と一致しており、正確です。