Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13621] ファイルの概要

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html における、不正なHTMLタグの処理方法に関する改善です。具体的には、閉じタグの > が欠落しており、ファイルの終端 (EOF) で終了するような不完全なタグを、有効なタグとして扱わずに破棄するよう変更されました。これにより、パーサーの堅牢性が向上し、より標準的なHTMLパーシングの挙動に近づきました。

コミット

commit aa9a81b1b098f3482bd648fbb634756cfa403fd5
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Mon Aug 13 12:07:44 2012 +1000

    exp/html: discard tags that are terminated by EOF instead of by '>'
    
    If a tag doesn't have a closing '>', it isn't considered a tag;
    it is just ignored and EOF is returned instead.
    
    Pass one additional test in the test suite.
    
    Change tokenizer tests to match correct behavior.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6454131

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/aa9a81b1b098f3482bd648fbb634756cfa403fd5

元コミット内容

このコミットは、HTMLパーサーがEOF(ファイルの終端)によって終了するタグを、> によって終了するタグではなく、破棄するように変更します。

もしタグが閉じの > を持たない場合、それはタグとは見なされず、単に無視され、代わりにEOFが返されます。

これにより、テストスイートで追加の1つのテストがパスするようになります。

トークナイザーのテストを正しい挙動に合わせるように変更します。

変更の背景

HTMLのパースにおいて、不正な形式のタグ、特に閉じの > が欠落したまま入力の終端(EOF)に達するようなケースは頻繁に発生します。従来の exp/html パッケージのトークナイザーは、このような不完全なタグを部分的にパースしてしまったり、意図しないトークンを生成したりする可能性がありました。

この挙動は、WebブラウザがHTMLをパースする際の標準的な挙動(HTML5パーシングアルゴリズム)とは異なる場合があり、結果として生成されるDOMツリーやトークンストリームが期待と異なるものになる可能性がありました。

このコミットの目的は、このような不完全なタグを適切に「無視」し、エラーとして処理することで、パーサーの堅牢性を高め、より標準に準拠した挙動を実現することにあります。これにより、不正なHTML入力に対しても予測可能で安定した結果を提供できるようになります。

前提知識の解説

HTMLパーシング

HTMLパーシングとは、HTMLドキュメントの文字列を読み込み、その構造を解析して、Webブラウザがレンダリングできるような内部表現(通常はDOMツリー)に変換するプロセスです。このプロセスは、大きく分けて「トークナイゼーション(字句解析)」と「ツリー構築」の2つのフェーズに分かれます。

トークナイザー (Lexer)

トークナイザー(または字句解析器)は、パーシングプロセスの最初の段階です。入力されたHTML文字列を、意味のある最小単位である「トークン」のストリームに分解します。HTMLにおけるトークンには、開始タグ(例: <p>)、終了タグ(例: </p>)、テキストノード、コメント、DOCTYPE宣言などがあります。トークナイザーは、これらのトークンを識別し、その種類と内容を次のツリー構築フェーズに渡します。

HTML5パーシングアルゴリズム

HTML5仕様は、WebブラウザがHTMLドキュメントをどのようにパースすべきかについて、非常に詳細なアルゴリズムを定義しています。これは、特にエラー処理や、不正なHTML構造(例えば、閉じタグの欠落、属性値の引用符の欠落など)に遭遇した場合の挙動を標準化することを目的としています。このアルゴリズムは、ブラウザ間の互換性を保証し、開発者がどのブラウザでも一貫したHTMLの解釈を期待できるようにするために不可欠です。不完全なタグの扱いについても、このアルゴリズムで詳細に規定されています。

Go言語の exp/html パッケージ

exp/html は、Go言語の標準ライブラリの一部として提供されているHTMLパーサーパッケージです。このパッケージは、HTML5パーシングアルゴリズムに準拠したHTMLの解析機能を提供することを目的としています。exp というプレフィックスは「experimental(実験的)」を意味し、初期段階ではAPIや挙動が変更される可能性があることを示唆しています。このパッケージは、HTMLドキュメントをトークンに分解するトークナイザーと、それらのトークンからDOMツリーを構築するパーサーの両方を含んでいます。

EOF (End Of File)

EOFは「End Of File」の略で、ファイルや入力ストリームの終端に達したことを示すマーカーです。パーサーがHTML文字列を読み進める際に、これ以上読み込む文字がない状態を指します。不正なHTMLでは、タグが閉じられる前にEOFに達することがあり、パーサーはこの状況を適切に処理する必要があります。

技術的詳細

このコミットの核心は、exp/html パッケージのトークナイザーが、不完全なHTMLタグ(特に閉じの > が欠落し、入力の終端で終わるタグ)をどのように扱うかを変更した点にあります。

以前の実装では、例えば <p id のようなタグが入力の最後に現れた場合、トークナイザーはこれを部分的にパースし、<p id=""> のような不完全ながらも何らかのタグトークンとして扱ってしまう可能性がありました。これは、HTML5の仕様が、このようなケースではタグ全体を無視すべきであると定めていることと矛盾していました。

今回の変更により、トークナイザーは、タグのパース中にエラーが発生した場合(例えば、閉じの > が見つからずにEOFに達した場合)、そのタグを有効なものとは見なさず、ErrorToken として処理するか、完全に無視するようになりました。

具体的には、token.go 内の readStartTag 関数において、エラーチェックの条件がより厳密になりました。以前は z.err != nil && len(z.attr) == 0 という条件でエラーをチェックしていましたが、これは「エラーが発生し、かつ属性が一つも読み込まれていない場合」に限定されていました。この条件が z.err != nil に変更されたことで、「エラーが発生した場合は常に」ErrorToken を返すようになり、不完全なタグが誤って部分的に有効なものとして扱われることを防ぎます。

また、終了タグを読み込む readTag 関数(EndTagToken を設定する部分)にも同様の変更が加えられました。終了タグのパース中にエラーが発生した場合も、その終了タグは ErrorToken として扱われ、有効な終了タグとしては認識されなくなりました。これにより、例えば </p id のような不正な終了タグも適切に無視されるようになります。

これらの変更は、token_test.go 内の複数のテストケースの修正によって検証されています。特に、malformed tag とマークされたテストケースの期待される出力が、以前は部分的にパースされたタグ(例: <p id="">)であったものが、空文字列 ("") に変更されています。これは、これらの不完全なタグが完全に破棄されるようになったことを明確に示しています。また、新たに malformed tag #9 (<p></p id) というテストケースが追加され、これも期待される出力が <p> となっており、不正な終了タグが無視されることを確認しています。

結果として、exp/html パッケージは、不正なHTML入力に対してより堅牢になり、Webブラウザの挙動により近い、標準に準拠したHTMLパーシングを提供できるようになりました。

コアとなるコードの変更箇所

src/pkg/exp/html/testlogs/webkit02.dat.log

--- a/src/pkg/exp/html/testlogs/webkit02.dat.log
+++ b/src/pkg/exp/html/testlogs/webkit02.dat.log
@@ -1,7 +1,7 @@
 PASS "<foo bar=qux/>"
 PASS "<p id=\"status\"><noscript><strong>A</strong></noscript><span>B</span></p>"
 PASS "<div><sarcasm><div></div></sarcasm></div>"
-FAIL "<html><body><img src=\"\" border=\"0\" alt=\"><div>A</div></body></html>"
+PASS "<html><body><img src=\"\" border=\"0\" alt=\"><div>A</div></body></html>"
 PASS "<table><td></tbody>A"
 PASS "<table><td></thead>A"
 PASS "<table><td></tfoot>A"

特定のテストケース <html><body><img src="" border="0" alt=""><div>A</div></body></html> の結果が FAIL から PASS に変更されました。これは、このコミットによる修正が、この特定の不正なHTML構造を正しく処理できるようになったことを示しています。

src/pkg/exp/html/token.go

--- a/src/pkg/exp/html/token.go
+++ b/src/pkg/exp/html/token.go
@@ -692,7 +692,7 @@ loop:
 // been consumed, where 'a' means anything in [A-Za-z].
 func (z *Tokenizer) readStartTag() TokenType {
 	z.readTag(true)
-	if z.err != nil && len(z.attr) == 0 {
+	if z.err != nil {
 		return ErrorToken
 	}
 	// Several tags flag the tokenizer's next token as raw.
@@ -948,7 +948,11 @@ loop:
 			}
 			if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
 				z.readTag(false)
-				z.tt = EndTagToken
+				if z.err != nil {
+					z.tt = ErrorToken
+				} else {
+					z.tt = EndTagToken
+				}
 				return z.tt
 			}
 			z.raw.end--
  • readStartTag 関数内のエラーチェック条件が z.err != nil && len(z.attr) == 0 から z.err != nil に変更されました。
  • 終了タグを読み込むロジック内で、z.readTag(false) の呼び出し後に、z.err が発生した場合は ErrorToken を設定し、そうでない場合にのみ EndTagToken を設定する条件分岐が追加されました。

src/pkg/exp/html/token_test.go

--- a/src/pkg/exp/html/token_test.go
+++ b/src/pkg/exp/html/token_test.go
@@ -128,7 +128,7 @@ var tokenTests = []tokenTest{
 	{
 		"tag name eof #4",
 		`<a x`,
-		`<a x="">`,
+		``,
 	},
 	// Some malformed tags that are missing a '>'.
 	{
@@ -144,12 +144,12 @@ var tokenTests = []tokenTest{
 	{
 		"malformed tag #2",
 		`<p id`,
-		`<p id="">`,
+		``,
 	},
 	{
 		"malformed tag #3",
 		`<p id=`,
-		`<p id="">`,
+		``,
 	},
 	{
 		"malformed tag #4",
@@ -159,7 +159,7 @@ var tokenTests = []tokenTest{\n 	{
 		"malformed tag #5",
 		`<p id=0`,
-		`<p id="0">`,
+		``,
 	},
 	{
 		"malformed tag #6",
@@ -169,13 +169,18 @@ var tokenTests = []tokenTest{\n 	{
 		"malformed tag #7",
 		`<p id="0</p>`,
-		`<p id="0&lt;/p&gt;">`,
+		``,
 	},
 	{
 		"malformed tag #8",
 		`<p id="0"</p>`,
 		`<p id="0" <=\"\" p=\"\">`,
 	},\n+\t{\n+\t\t\"malformed tag #9\",\n+\t\t`<p></p id`,\n+\t\t`<p>`,\n+\t},\n 	// Raw text and RCDATA.
 	{
 		"basic raw text",
 		"<textarea>a</textarea>",
@@ -205,7 +210,7 @@ var tokenTests = []tokenTest{\n 	{
 		"' ' completes script end tag",
 		"<SCRIPT>a</SCRipt ",
-		"<script>$a$</script>",
+		"<script>$a",
 	},
 	{
 		"'>' completes script end tag",
  • 複数の malformed tag テストケース(#4, #2, #3, #5, #7)で、期待される出力が、以前の部分的にパースされたタグから空文字列 ("") に変更されました。
  • 新しいテストケース malformed tag #9 (<p></p id) が追加され、期待される出力が <p> となっています。
  • ' ' completes script end tag テストケースの期待される出力が変更されました。

コアとなるコードの解説

このコミットにおける主要な変更は、src/pkg/exp/html/token.go ファイル内の Tokenizer の挙動、特に readStartTag 関数と、終了タグを処理する部分に集中しています。

  1. readStartTag 関数のエラー処理の強化: 以前の readStartTag 関数では、開始タグのパース中にエラーが発生した場合でも、z.err != nil && len(z.attr) == 0 という条件が真である場合にのみ ErrorToken を返していました。これは、「エラーが発生し、かつタグに属性が一つも読み込まれていない場合」という限定的な条件でした。 今回の変更で、この条件が z.err != nil に簡素化されました。これにより、タグのパース中に何らかのエラーが発生した場合は、属性の有無にかかわらず、常にそのタグを無効なものとして ErrorToken を返すようになりました。例えば、<img src=" のように属性値が閉じられていないままEOFに達した場合でも、以前は部分的にパースされてしまう可能性がありましたが、この変更により適切に ErrorToken として処理され、タグ全体が破棄されるようになります。

  2. 終了タグ処理におけるエラーの伝播と破棄: token.go の別の箇所(readTag 関数が EndTagToken を設定するロジック内)では、終了タグのパースが行われます。以前は、z.readTag(false) の呼び出し後に無条件で z.tt = EndTagToken と設定されていました。 今回の変更では、z.readTag(false) の呼び出し後に if z.err != nil { z.tt = ErrorToken } else { z.tt = EndTagToken } という条件分岐が追加されました。これは、終了タグのパース中にエラーが発生した場合(例えば、</p id のように閉じの > が欠落している場合)、その終了タグも有効なものとは見なさず、ErrorToken として処理することを意味します。エラーがなければ通常通り EndTagToken を設定します。これにより、不正な終了タグが誤って有効なものとして扱われることを防ぎ、パーサーがより堅牢になります。

これらのコード変更は、src/pkg/exp/html/token_test.go 内のテストケースの変更によってその効果が確認されています。特に、多くの malformed tag テストケースで期待される出力が空文字列 ("") に変更されたことは、不完全なタグが完全に無視され、トークンストリームに現れなくなったことを明確に示しています。また、webkit02.dat.log のテスト結果が FAIL から PASS に変わったことは、実際のWebコンテンツにおける複雑な不正HTMLに対しても、この修正が有効であることを裏付けています。

関連リンク

参考にした情報源リンク