HTMLマジックバイト検出を「末尾チェック」で強化し、コメント先行問題を解消
HTMLファイルの先頭にコメントが置かれると application/xml と誤判定される問題を、正規表現と負のオフセットによる末尾検索の組み合わせで解決しました。
背景
marcelのHTMLマジックバイト検出は、ファイル先頭の固定オフセットと固定文字列の組み合わせで実装されており、先頭付近に <html> や <!DOCTYPE html> が現れることを前提としていました。しかしIssue #102 が報告したように、先頭に長いコメントが置かれると <html> タグが検索範囲外に押し出され、代わりに <!-- で始まるXMLのマジック定義が先にマッチしてしまいます。
問題の核心は、HTMLマジック定義が lib/marcel/tables.rb 内のXMLマジック定義よりも後に評価されることです。先頭128バイト以内に <html> が収まらない場合、XML判定が優先されてしまいます。先頭オフセットをいくらか広げるワークアラウンドは一時的な回避策に過ぎず、コメントは任意の長さを取り得るため根本的な解決にはなりません。
技術的な変更
マジック定義の抜本的な書き換え により、先頭の固定文字列マッチングから正規表現ベースの柔軟な検出へ移行しました。
変更前:
Marcel::MimeType.extend "text/html",
extensions: %w( html htm ),
magic: [
[0, "<!DOCTYPE html"],
[0, "<!DOCTYPE HTML"],
[0, "<!doctype html"],
[0, "<!doctype HTML"],
[0, "<html"],
[0, "<HTML"],
[0, " <!DOCTYPE html"],
[0, "\n<!DOCTYPE html"],
[0, "\r<!DOCTYPE html"],
[0, "\r\n<!DOCTYPE html"],
[0, "\t<!DOCTYPE html"],
[0, " <html"],
[0, "\n<html"],
[0, "\r<html"],
[0, "\r\n<html"],
[0, "\t<html"]
]
変更後:
Marcel::MimeType.extend "text/html",
extensions: %w( html htm ),
magic: [
[64, %r{\A\s*<(!DOCTYPE html|html)}mi],
[-64, %r{</html>\s*\z}mi],
]
変更後の定義は2つのルールで構成されます。1つ目はオフセット 64 から \A\s*<(!DOCTYPE html|html) にマッチさせる正規表現で、/mi フラグにより大文字小文字と改行を問いません。2つ目はオフセット -64(末尾から64バイト)から </html>\s*\z にマッチさせる正規表現で、ファイル末尾に閉じタグが存在することをもって確認を取ります。
負のオフセットを扱うために lib/marcel/magic.rb の io_seek メソッドも拡張されました。
def self.io_seek(io, offset, buffer)
return if offset == 0
offset = io.size + offset if offset < 0
return if offset < 0
if io.respond_to?(:seek)
io.seek(offset, IO::SEEK_CUR)
offset < 0 の場合に io.size + offset で末尾からの絶対位置へ変換し、それでも負になる(ファイルサイズ自体が小さすぎる)場合はシークをスキップします。追加は2行のみで、既存のシーク処理へのオーバーヘッドはありません。
設計判断
「先頭でなく末尾を見る」という発想の転換 が本PRの設計上の要点です。
HTMLコメントが先頭に来る場合、<html> の位置は任意になるため先頭チェックだけでは原理的に網羅できません。一方で、整形式のHTMLドキュメントなら </html> は末尾付近に現れるという不変の構造的特性があります。末尾64バイトを見るルールはこの特性を利用しており、コメントの長さに依存しません。
16個の固定文字列パターンを正規表現2つに集約したことも重要な判断です。大文字小文字の組み合わせや前置ホワイトスペースの種類ごとに列挙する必要がなくなり、定義が宣言的になりました。また、先頭ルールにオフセット 64 を指定することで、短いBOMやメタ情報が先行する一般的なケースにも余裕を持って対応しています。
まとめ
16パターンの固定文字列から正規表現2つへの置き換えは、単なるリファクタリングを超えた設計の改善です。先頭と末尾の両端からHTMLの構造を確認するアプローチにより、コメント長に依存しない堅牢な検出が実現され、同様の誤判定が根本から排除されています。