HTML検出ロジックの改善:ファイル先頭の厳密なマッチングで誤検知を解消
JSONファイル内にHTMLタグが含まれる場合に text/html と誤検知される問題を修正しました。マジックバイトの照合をファイル先頭に限定することで、コンテンツの意味的な判定精度が向上しています。
背景
Issue #79 で報告されたように、{"a": "<html"} というJSONファイルがv1.0.0以降で text/html と判定されるという回帰バグが存在していました。v0.3.3では同じファイルを正しく application/json と判定できていたため、バージョンアップ時に導入された挙動の変化が原因でした。
Issueの再現コードによると、問題は検出範囲の設定にあります。変更前の定義では [0..64, "<html"] のように先頭64バイト以内に <html 文字列が含まれれば text/html と判定していました。JSONの値フィールドやCSVのセル内にHTMLタグが含まれるだけで誤検知が発生するのは、この広すぎる照合範囲が原因です。
この問題はファイルの拡張子情報が与えられない場合や、MIMEタイプをコンテンツのみから推論するケースで顕在化していました。
技術的な変更
lib/marcel/mime_type/definitions.rb の text/html 定義が全面的に書き直され、マジックバイトの照合範囲がオフセット 0(ファイル先頭)に限定されました。
変更前:
Marcel::MimeType.extend "text/html", magic: [[0..64, "<!DOCTYPE HTML"], [0..64, "<!DOCTYPE html"], [0..64, "<!doctype HTML"], [0..64, "<!doctype html"]]
変更後:
Marcel::Magic.remove("text/html")
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"]
]
変更点は3点あります。第一に、Marcel::Magic.remove("text/html") を呼び出して既存の定義を完全にリセットしてから再定義しています。これにより、ベースとなるmagicデータベースのエントリと独自定義が混在しないようにしています。第二に、照合オフセットを 0..64 の範囲から 0(完全一致)に絞り込むことで、ファイル先頭以外に現れる <html タグを無視するようにしました。第三に、extensions: %w( html htm ) を明示的に追加し、拡張子ベースの判定も同じ定義エントリで管理するようになっています。
テストフィクスチャも更新されています。test/fixtures/name/application/json/json.json には "tag": "<html></html>" フィールドが追加され、test/fixtures/name/text/csv/csv.csv にも <html> タグを含むセルが追加されました。これらはいずれも、HTMLタグを含んでいても拡張子で正しくJSONやCSVと判定されるべきケースの回帰テストとして機能します。
設計判断
照合範囲を「先頭Nバイト以内」から「先頭完全一致」に変更するという方針が採用されました。
マジックバイト検出の本来の目的は、ファイルの先頭に現れるシグネチャを識別することです。HTMLファイルは <!DOCTYPE html> や <html> でドキュメントが始まるため、先頭以外に現れる場合はHTMLドキュメントとは見なさないという判断は理にかなっています。一方で、空白・タブ・改行・CRLFのバリエーションを個別のエントリとして列挙しているのは、先頭にBOM相当のホワイトスペースが入るケースへの対応です。
また、<html と <!DOCTYPE html の両パターンをゼロオフセットで定義することで、DOCTYPE宣言なしのHTMLファイルも先頭タグから検出できるようにしています。変更前の定義にはこのパターンが 0..64 の範囲で含まれていましたが、今回の変更でオフセット 0 に厳格化されています。
まとめ
この変更は、マジックバイト照合の原則「シグネチャはファイル先頭にある」を再徹底したものです。JSONやCSVのようにHTMLタグを値として含み得るフォーマットとの共存において、拡張子ヒントがない場合でも誤検知しないための最小限の変更となっています。