Perl覚え書き20041126

 ふー。やっと予定通りの動作をするようになってくれた。時間かかったなあ。

 さて、前回ブラウザは柔軟な解釈をしてくれるから困ったと書いた。通常HTMLを書くときにはそれでもいいんだけど、こういったプログラムを作るときにはそれが問題になってくることもある。今回のプログラムでどこが問題になるのかはあえて書かなかったが、つまりは " や ' の対応が合っていない場合に不具合が出てくる。

 処理対象のサンプルとして、はてなの「質問一覧」のHTMLソースを使っていた。最初どうして途中でいきなり許可していないはずのタグがそのまま残っているのが不思議でならなかった。途中でおかしくなり、その後しばらくしたら元に戻っている。

 タグ以外の部分を表示しないようにし、タグを一行ごとに分けて表示するようにしたらわかった。ある部分でかなり広い範囲でタグとしてマッチしてしまっているところがあった。質問者の名前が書いてある部分。

<td><a href="user?userid=******">******</a><font size=-2">

 ここだ。font size=の後が -2" となってしまっている。ここは "-2" と書くべきだ*1。これが原因で、-2の後ろの " 以降がquoted stringsとして認識されてしまい、おかしなことになっていた。

 最初FONTタグは通す設定にしていなかったため、ここは&lt; &gt;に変換される。だが、上記の通りタグと認識されている範囲が広すぎて、その途中にブラウザではタグと解釈されてしまう部分が残ってしまう。たとえば以下のような。

&lt;font size=-2"><b>abcdefg</b></font><font size=-2"&gt;<b>hijklmn</b></font>

 あくまで例だから、実際このタグをこの関数に入れてもこうならないかも…。まあともかくこういう結果になったとする。この場合は間に入っているのがBタグだから問題ないが、ここにSCRIPTと書かれていたらどうなるだろう。ほとんどの場合、SCRIPTを許可するような状況はあり得ない。それがこういう書き方をすることで間違って通ってしまう。それはまずい。かなりまずい。どれぐらいまずいかっていうと、…まあここではあえて書かないけど。いや思いつかなかったなんてことじゃないよ。

 そこで、この&lt; &gt;に囲まれた部分については< >を全て&lt; &gt;に変換してしまうことにした。故意にしろ偶然にしろ間違った書式で書かれたタグであるため、そこがブラウザに解釈されなくとも問題はない。単に利用者側にその部分をちゃんとした物に書き換えてもらうべきだ。前回アップしたファイルでいうと、

 67: 	if($ignore && !$iscomment) {			# コメントの場合は通す
 68: 		$res =~ s!^<!&lt;!;
 69: 		$res =~ s!>$!&gt;!;
 70: 	}

 この部分を

	if($ignore && !$iscomment) {			# コメントの場合は通す
		$res =~ s!&!&amp;!g;
		$res =~ s!<!&lt;!g;
		$res =~ s!>!&gt;!g;
	}

 という風に書き換えるだけ。行頭の<と行末の>だけを変換するようにしていたのを、全部ひっくるめるようにしただけ。ついでにもしかしたら影響を与えるかもしれないということで&も変換してしまうことにした。

 ところが、これでは問題は解決しない。無効にするタグでちゃんと閉じていない"や'があった場合はこれでいいんだけど、有効にしたいタグでここに引っかかった場合は、全てを変換してしまうわけにはいかない。タグの開き括弧はそのままで、そのタグの閉じ括弧だけが変換されてしまう。それではタグとして認められない。

<font size=-2"&gt;&lt;b&gt;abcdefg&lt;/b&gt;&lt;/font&gt;&lt;font size=-2"><b>hijklmn</b></font>

 ちょっと見づらいが、こんなことになってしまう。「言ってる事がさっきと違うじゃないか。間違ってる物はちゃんと解釈されなくてもいいんじゃなかったのか?」と突っ込まれるかもしれない。確かにそうだけど、それとは別の問題がある。

 それは「どうやって正しい範囲がマッチしているのか間違って広範囲がマッチしているのかを知る術がない」ということ。もしそれが判断できるなら、そもそも「間違った広範囲マッチ」が起こるはずがない。できないからこういう状況も生まれてしまうわけだ。

 さて、どうしよう。ここに一番時間が掛かった。

 最初"や'の対応具合をチェックしようかと考えた。偶数なら対応している、奇数なら対応がおかしいだろう。だけど実際やってみるとうまくいかなかった。上記の例で言えば

<font size=-2"><b>abcdefg</b></font><font size=-2">

 この部分が一つのタグとして抜き出されてくる。"の数を数えてみると偶数だ。でもこれは正しい対応ではない。というわけでダメ。

 色々試行錯誤してみた。ブラウザに色々変なHTMLを食わせてみるが、いい感じで適当に無視している。ブラウザだから当然全てのタグリストを持っているし、そこに該当するタグが出てくれば新しいタグが来たという解析をしているのかとも思ったが、たとえば<abc>なんていう存在しないタグを書いてもちゃんと分かっているようだ。うーん。タグそのものの解析をやりたい訳じゃないから、一文字ずつチェックしていくなんてことはやってられない。時間もかかるし。

 そこで別のアプローチを考えることにしてみた。

<font size=-2"><b>abcdefg</b></font>

 という文字列を設定し、例のタグ正規表現に掛けて配列に書き出し、内容を見てみるとBと/Bと/FONTの三つしか入っていなかった。つまり、先頭のFONTはタグとして認識されていなかった。

 というわけで、与えられた文字列を改行で分割し、行単位で処理をすることにした。同じ行に対応がおかしいタグが複数あると対応できないが、まあそれでも前よりはましだろう。

 弊害として、一つのタグを複数の行に分けて書くことができなくなった。まあそんな書き方をしている人はいないだろうけど、それでは困るのはスタイルシートや、まあもしSCRIPTタグを有効にする場合だ。通常これらは対応していないブラウザでスタイルシートスクリプトがそのまま画面に出てしまわないようにコメントとして記述する。そしてそれが一行で終わることはまずない。

 仕方がないから、コメントの場合はコメントが終わるまでを一つにまとめてそのまま出力するようにした。

 とりあえず今回はこれで終わり。

 しかし、今日これを書いている途中で不具合に気づいたのは二回目。一回目は「有効にするタグの場合に< >の変換がおかしくなる」と言う部分。二回目は、「同じ行に"や'の対応がおかしいタグが複数存在する場合におかしくなる」という部分。

 一個目は解決したが二個目は…。かなり難しそう。

 とりあえず今回のスクリプトここからどうぞ。

*1:数値の場合は " は要らないんだっけ??それにしたって後ろの " は不要