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

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

このコミットは、Go言語のhtmlパッケージにおけるHTMLコメントのパース処理に関するバグ修正です。具体的には、コメントの終端(-->)がファイルの終端(EOF)と重なる場合に、末尾のハイフン(-)の処理がHTML5仕様に準拠していなかった問題を修正しています。これにより、W3CのHTML5テストスイートの一部(tests2.datのテスト57)が正しくパスするようになります。

コミット

  • コミットハッシュ: 57ed39fd3bca9c69c32e55eb0a1873ab7f20bcfc
  • Author: Andrew Balholm andybalholm@gmail.com
  • Date: Wed Nov 23 09:26:37 2011 +1100

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

https://github.com/golang/go/commit/57ed39fd3bca9c69c32e55eb0a1873ab7f20bcfc

元コミット内容

html: on EOF in a comment, ignore final dashes (up to 2)

Pass tests2.dat, test 57:
<!DOCTYPE html><!--x--

| <!DOCTYPE html>
| <!-- x -->
| <html>
|   <head>
|   <body>

Also pass test 58:
<!DOCTYPE html><table><tr><td></p></table>

R=nigeltao
CC=golang-dev
https://golang.org/cl/5436048

変更の背景

この変更の背景には、Go言語のhtmlパッケージがHTML5の仕様に厳密に準拠することを目指しているという目標があります。HTML5の仕様は、HTMLドキュメントのパースに関する非常に詳細なルールを定めており、特にエラーハンドリングや不完全なマークアップの処理について厳格です。

元の実装では、HTMLコメント(<!-- ... -->)のパースにおいて、コメントの途中でファイルの終端(EOF)に達した場合の挙動がHTML5仕様と異なっていました。具体的には、コメントの終了を示す-->の直前でEOFになった場合、仕様では末尾のハイフンを最大2つまで無視してコメントを終了するとされています。しかし、既存の実装ではこのルールが正しく適用されていなかったため、W3CのHTML5テストスイートに含まれるtests2.datのテスト57のような特定のケースでパース結果が期待と異なっていました。

テスト57の例: <!DOCTYPE html><!--x-- この入力に対して、HTML5仕様では<!-- x -->というコメントとしてパースされるべきです。これは、コメントの途中でEOFに達した場合、末尾のハイフンが2つまでであれば、それらを無視してコメントを閉じると解釈されるためです。

この不一致を解消し、より堅牢で仕様に準拠したHTMLパーサーを提供するために、この修正が導入されました。

前提知識の解説

HTMLコメントの構文

HTMLコメントは<!--で始まり、-->で終わります。コメントの内容はブラウザによって表示されません。

例: <!-- これはコメントです -->

HTMLパーシング

HTMLパーシングは、HTMLドキュメントを読み込み、その構造を解析して、ブラウザがレンダリングできるような内部表現(DOMツリーなど)に変換するプロセスです。このプロセスは通常、以下の2つの主要な段階に分けられます。

  1. トークナイゼーション(Tokenization): 入力されたHTML文字列を、意味のある小さな単位(トークン)に分割する段階です。例えば、開始タグ、終了タグ、テキスト、コメント、DOCTYPE宣言などがトークンとして識別されます。このコミットで修正されているのは、このトークナイゼーションの段階、特にコメントトークンの処理です。
  2. ツリー構築(Tree Construction): トークナイザーによって生成されたトークンを基に、DOMツリーを構築する段階です。

EOF (End Of File)

EOFは「End Of File」の略で、ファイルや入力ストリームの終端を意味します。パーサーが入力のEOFに達したとき、それはそれ以上読み込むデータがないことを示します。HTMLパーシングにおいては、EOFに達した際の未完了のタグやコメントの処理が仕様で厳密に定義されています。

Go言語のhtmlパッケージ

Go言語の標準ライブラリには、HTML5の仕様に準拠したHTMLパーサーを提供するgolang.org/x/net/htmlパッケージ(このコミット当時はsrc/pkg/html)が含まれています。このパッケージは、ウェブスクレイピング、HTMLの変換、サーバーサイドでのHTML生成など、様々な用途で利用されます。このコミットは、その内部のトークナイザーの挙動を改善するものです。

W3C HTML5テストスイート

W3C (World Wide Web Consortium) は、ウェブ標準を策定する国際的なコミュニティです。HTML5の仕様には、その実装が正しく行われているかを検証するための広範なテストスイートが含まれています。これらのテストは、様々な有効なHTMLと無効なHTMLの入力に対して、パーサーがどのようなDOMツリーを構築すべきかを定義しています。tests2.datのようなファイルは、これらのテストケースを記述したデータファイルの一部です。パーサーがこれらのテストをパスすることは、そのパーサーがHTML5仕様に準拠していることの重要な指標となります。

技術的詳細

このコミットの技術的詳細は、HTML5のトークナイゼーションアルゴリズムにおけるコメントのパースルール、特に「コメント状態(Comment state)」と「コメント終了ダッシュ状態(Comment end dash state)」、「コメント終了状態(Comment end state)」に関連しています。

HTML5仕様(HTML Standard - 13.2.5.60 Comment state など)によると、コメントのパース中にEOFに遭遇した場合の処理は以下のようになります。

  1. コメント状態(Comment state): 通常のコメント内容を読み込む状態。
  2. コメント終了ダッシュ状態(Comment end dash state): ハイフン(-)を読み込んだ直後の状態。次の文字がハイフンであれば「コメント終了ダッシュダッシュ状態」へ、そうでなければコメント内容として処理を続ける。
  3. コメント終了ダッシュダッシュ状態(Comment end dash dash state): 2つのハイフン(--)を読み込んだ直後の状態。次の文字が>であればコメント終了。そうでなければ、読み込んだ--をコメント内容の一部として扱い、コメント状態に戻る。

このコミットが修正しているのは、特にEOFに遭遇した場合の挙動です。

  • コメント終了ダッシュ状態(Comment end dash state)でEOFに遭遇した場合:
    • 仕様では、現在のコメントトークンを終了させ、そのトークンを発行します。つまり、末尾のハイフンはコメントの一部として扱われず、コメントはそこで閉じられたと見なされます。
  • コメント終了ダッシュダッシュ状態(Comment end dash dash state)でEOFに遭遇した場合:
    • 仕様では、現在のコメントトークンを終了させ、そのトークンを発行します。この場合も、末尾の--はコメントの一部として扱われず、コメントはそこで閉じられたと見なされます。

元の実装では、EOFに遭遇した際に、これらの末尾のハイフンを適切に「無視」する(つまり、コメントデータの一部として含めない)処理が欠けていたと考えられます。readComment関数内のdashCount変数は、連続するハイフンの数を追跡するために使用されます。この修正では、EOFに達した際にdashCountが2より大きい場合(つまり、--以上のハイフンが連続していた場合)、dashCountを2に制限することで、末尾の余分なハイフンがコメントデータに含まれないようにしています。これにより、z.data.end(コメントデータの終端)が適切に調整され、HTML5仕様に準拠したパース結果が得られます。

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

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

  1. src/pkg/html/parse_test.go:

    • TestParser関数内のtestFilesスライスにおいて、"tests2.dat"のテストケースの実行上限が57から59に変更されています。これは、修正によってテスト57がパスするようになったため、さらに多くのテストケースを実行できるようにしたことを示唆しています。
    --- a/src/pkg/html/parse_test.go
    +++ b/src/pkg/html/parse_test.go
    @@ -134,7 +134,7 @@ func TestParser(t *testing.T) {
     	}{\n \t\t// TODO(nigeltao): Process all the test cases from all the .dat files.\n \t\t{\"tests1.dat\", -1},\n-\t\t{\"tests2.dat\", 57},\n+\t\t{\"tests2.dat\", 59},\n     \t{\"tests3.dat\", 0},\n     \t}\n     \tfor _, tf := range testFiles {
    
  2. src/pkg/html/token.go:

    • Tokenizer構造体のreadCommentメソッドに修正が加えられています。このメソッドはHTMLコメントをパースするロジックを担当しています。EOFに達した場合の処理が変更されています。
    --- a/src/pkg/html/token.go
    +++ b/src/pkg/html/token.go
    @@ -289,7 +289,11 @@ func (z *Tokenizer) readComment() {
     	for dashCount := 2; ; {
     \t\tc := z.readByte()\n \t\tif z.err != nil {\n-\t\t\tz.data.end = z.raw.end\n+\t\t\t// Ignore up to two dashes at EOF.\n+\t\t\tif dashCount > 2 {\n+\t\t\t\tdashCount = 2\n+\t\t\t}\n+\t\t\tz.data.end = z.raw.end - dashCount\n     \t\treturn\n     \t}\n     \tswitch c {
    
  3. src/pkg/html/token_test.go:

    • tokenTestsスライスに、コメントのEOF処理に関する新しいテストケースが5つ追加されています。これらのテストは、コメントの末尾に様々な数のハイフンがある状態でEOFに達した場合のTokenizerの挙動を検証します。
    --- a/src/pkg/html/token_test.go
    +++ b/src/pkg/html/token_test.go
    @@ -325,6 +325,26 @@ var tokenTests = []tokenTest{
     \t},\n \t{\n \t\t\"comment9\",\n+\t\t\"a<!--z-\",\n+\t\t\"a$<!--z-->\",\n+\t},\n+\t{\n+\t\t\"comment10\",\n+\t\t\"a<!--z--\",\n+\t\t\"a$<!--z-->\",\n+\t},\n+\t{\n+\t\t\"comment11\",\n+\t\t\"a<!--z---\",\n+\t\t\"a$<!--z--->\",\n+\t},\n+\t{\n+\t\t\"comment12\",\n+\t\t\"a<!--z----\",\n+\t\t\"a$<!--z---->\",\n+\t},\n+\t{\n+\t\t\"comment13\",\n \t\t\"a<!--x--!>z\",\n \t\t\"a$<!--x-->$z\",\n \t},
    

コアとなるコードの解説

src/pkg/html/token.goreadComment メソッドの変更

readComment関数は、HTMLコメント(<!-- ... -->)をパースする役割を担っています。この関数は、コメントの開始シーケンス<!--を読み込んだ後、コメントの内容を読み進め、-->という終了シーケンスを探します。

変更の核心は、EOF(ファイルの終端)に達した場合のdashCountの処理です。

	for dashCount := 2; ; {
		c := z.readByte()
		if z.err != nil {
			// Ignore up to two dashes at EOF.
			if dashCount > 2 {
				dashCount = 2
			}
			z.data.end = z.raw.end - dashCount
			return
		}
		// ... (既存のswitch文による文字処理ロジック)
	}
  • if z.err != nil: これは、z.readByte()がエラー(通常はEOF)を返した場合の処理ブロックです。
  • if dashCount > 2 { dashCount = 2 }: この行が追加された主要なロジックです。
    • dashCountは、現在までに連続して読み込んだハイフンの数を追跡しています。
    • HTML5仕様では、コメントのパース中にEOFに達した場合、末尾のハイフンを最大2つまで無視してコメントを終了すると規定されています。
    • もしdashCountが2より大きい場合(例: -------でEOFになった場合)、それは3つ以上のハイフンが連続していたことを意味します。この場合、仕様に従い、dashCountを2に強制的に設定します。これにより、コメントデータから余分なハイフンが除外されます。
  • z.data.end = z.raw.end - dashCount:
    • z.raw.endは、トークナイザーが現在読み込んでいる生のバイト列の終端インデックスです。
    • z.data.endは、パースされたコメントデータの終端インデックスです。
    • この行は、コメントデータの終端を調整しています。dashCountの値をz.raw.endから引くことで、末尾のハイフン(最大2つ)がコメントデータに含まれないようにします。例えば、<!--x--という入力でEOFになった場合、dashCountは2に設定され、z.data.endz.raw.end - 2となり、コメントデータはxのみとなります。

この変更により、readComment関数は、コメントの途中でEOFに遭遇した場合でも、HTML5仕様に準拠した正しいコメントデータを生成するようになりました。

src/pkg/html/token_test.go の新しいテストケース

追加されたテストケースは、この修正の妥当性を検証するために非常に重要です。

  • "comment9", "a<!--z-", "a$<!--z-->": コメントの末尾にハイフンが1つある状態でEOF。期待される結果は<!--z-->(末尾のハイフンが無視される)。
  • "comment10", "a<!--z--", "a$": コメントの末尾にハイフンが2つある状態でEOF。期待される結果は`(末尾のハイフンが無視される)。
  • "comment11", "a<!--z---", "a$": コメントの末尾にハイフンが3つある状態でEOF。期待される結果は`(末尾のハイフンが1つだけコメントデータに含まれる)。
  • "comment12", "a<!--z----", "a$": コメントの末尾にハイフンが4つある状態でEOF。期待される結果は`(末尾のハイフンが2つだけコメントデータに含まれる)。

これらのテストは、dashCountの調整ロジックが、ハイフンの数に応じて正しく機能することを確認しています。特に、dashCountが2を超えた場合に2に制限されることで、仕様で定められた「最大2つのハイフンを無視する」という挙動が実現されていることがわかります。

関連リンク

  • Go Code Review: https://golang.org/cl/5436048 - このコミットに対応するGoのコードレビューページです。詳細な議論や変更履歴を確認できます。

参考にした情報源リンク