`tika.xml` の正規表現ルールをMIMEタイプ判定に組み込む
Apache Tikaの tika.xml に定義された正規表現マッチルールをMarcelのMIMEタイプ判定に取り込み、バイト列マッチだけでは困難だった text/html や application/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-dbf や message/rfc822、application/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.rb の magic_match_io メソッドに、value が Regexp インスタンスである場合の分岐が追加されました。
変更前:
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-bzip2 と text/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_regexp が nil を返した正規表現パターン(Rubyと非互換なパターン)はテーブルに含まれなくなりました。これにより、これまで MAGIC 定数に混入していた以下のような無効エントリが除去されます:
-
application/illustrator+ps:[\r\n]%AI5_FileFormatのような正規表現文字列がバイト列として格納されていた -
message/rfc822: 複雑なネスト構造の正規表現が誤ったルールとして存在していた -
application/x-dbf: 可変長後読みを含むパターンがRubyと非互換なまま残存していた
その他 tables.rb では、Markdownファイル拡張子(markdown、md、mdtext、mkd)の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 返却を組み合わせることで、互換性リスクを抑えながら拡張の余地を残した設計になっています。