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

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

コミット

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html において、HTML属性値内のエンティティ(実体参照)の処理方法を改善するものです。具体的には、セミコロンで終わらないエンティティが、イコール記号(=)、英字、または数字の後に続く場合に、それらをエンティティとしてアンエスケープしないようにする特殊なハンドリングが導入されました。これにより、Webkitのテストスイートにおける6つのテストと、token_test.go 内でコメントアウトされていた1つのテストがパスするようになりました。

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

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

元コミット内容

exp/html: special handling for entities in attributes

Don't unescape entities in attributes when they don't end with
a semicolon and they are followed by '=', a letter, or a digit.

Pass 6 more tests from the WebKit test suite, plus one that was
commented out in token_test.go.

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

変更の背景

HTMLのパースにおいて、エンティティの処理は非常に複雑です。特に属性値内でのエンティティの解釈は、HTML5の仕様で厳密に定義されており、ブラウザ間の互換性を保つ上で正確な実装が求められます。

このコミットの背景には、exp/html パッケージがWebkitのテストスイート(HTMLパーサーの標準的なテストセットの一つ)の一部をパスできていなかったという問題があります。具体的には、属性値内でセミコロンで終わらないエンティティ(例: &gt)が、その直後に特定の文字(=, 英字, 数字)を伴う場合に、誤ってエンティティとして解釈されてしまう挙動がありました。

HTML5の仕様では、数値文字参照や名前付き文字参照は、その直後に数字や英字が続く場合、セミコロンで終端されない限り、エンティティとして解釈されないというルールがあります。例えば、<a href="foo&bar">&bar はエンティティではなく、単なるリテラル文字列として扱われるべきです。しかし、&amp; のようにセミコロンがなくてもエンティティとして認識される特殊なケースも存在します。

このコミットは、このようなHTML5の仕様に準拠し、Webkitのテストをパスするために、属性値内のエンティティ処理ロジックをより厳密に、かつ正確に修正することを目的としています。

前提知識の解説

HTMLエンティティ(実体参照)

HTMLエンティティは、HTMLドキュメント内で特殊文字(例: <>)や、キーボードから直接入力できない文字(例: ©)を表現するための仕組みです。主に以下の2種類があります。

  1. 名前付き文字参照 (Named Character References): &lt; (less than), &gt; (greater than), &amp; (ampersand), &quot; (double quote), &apos; (apostrophe, HTML5で導入) など、特定の名前で参照されるもの。
  2. 数値文字参照 (Numeric Character References): 文字のUnicodeコードポイントを数値で指定するもの。
    • 10進数: &#DDDD; (例: &#60;<)
    • 16進数: &#xHHHH; (例: &#x3C;<)

エンティティの終端と曖昧性

HTMLの仕様では、エンティティは通常セミコロン(;)で終端されます。しかし、一部のエンティティ(特に &amp;, &lt;, &gt;, &quot;, &apos;)は、歴史的な理由からセミコロンがなくても認識される場合があります。

さらに重要なのは、HTML5のパースルールにおいて、エンティティの直後に特定の文字が続く場合の曖昧性の解消です。例えば、&amp のようにセミコロンがないエンティティの直後に英数字が続く場合、それはエンティティとして解釈されず、リテラル文字列として扱われるべきです。 例:

  • &amp; -> & (エンティティとして解釈)
  • &amp -> &amp (リテラル文字列として解釈、セミコロンがないため)
  • &gt; -> > (エンティティとして解釈)
  • &gt -> &gt (リテラル文字列として解釈、セミコロンがないため)
  • &gt=YY -> &gt=YY (リテラル文字列として解釈、=が続くため)

このルールは、特に属性値内で重要になります。属性値は、HTML要素の動作や表示を制御するための情報を含み、そのパースは厳密でなければなりません。

WebKitテストスイート

WebKitは、Apple SafariやGoogle Chrome(かつて)などで使用されているレンダリングエンジンです。WebKitプロジェクトは、HTML、CSS、JavaScriptなどのWeb標準に準拠したレンダリングを行うための広範なテストスイートを維持しています。これらのテストは、Webブラウザの互換性を確保するために、HTMLパーサーの実装がWeb標準にどれだけ忠実であるかを検証する上で非常に重要です。exp/html のようなHTMLパーサーは、これらのテストをパスすることで、その正確性と互換性が保証されます。

技術的詳細

このコミットの核心は、src/pkg/exp/html/escape.go 内の unescape および unescapeEntity 関数における attribute フラグの導入と、それに基づくエンティティ処理ロジックの変更です。

従来の unescape 関数は、テキストノードと属性値の区別なくエンティティをアンエスケープしていました。しかし、HTML5の仕様では、属性値内のエンティティ処理には特別なルールが適用されます。

変更後のロジックは以下のようになります。

  1. unescape 関数のシグネチャ変更: func unescape(b []byte) []byte から func unescape(b []byte, attribute bool) []byte へと変更されました。attribute 引数は、現在処理しているバイト列がHTML属性値であるかどうかを示します。

  2. unescapeEntity 関数への attribute フラグの伝播: unescape 関数内でエンティティのアンエスケープを行う unescapeEntity 関数が呼び出される際に、この新しい attribute フラグが渡されるようになりました。

  3. unescapeEntity 内の条件付きアンエスケープ: unescapeEntity 関数(変更差分には直接示されていませんが、この関数内でロジックが変更されたと推測されます)は、attributetrue の場合、つまり属性値を処理している場合に、より厳密なエンティティ終端ルールを適用します。 具体的には、セミコロンで終端されていないエンティティが、その直後にイコール記号(=)、英字(a-z, A-Z)、または数字(0-9)を伴う場合、それはエンティティとしてアンエスケープされず、リテラル文字列として扱われます。

  4. 呼び出し元の調整:

    • src/pkg/exp/html/token.goText() メソッド(テキストノードを処理)では、unescape(s, false) と呼び出され、属性値ではないことを明示します。
    • src/pkg/exp/html/token.goTagAttr() メソッド(タグの属性値を処理)では、unescape(convertNewlines(val), true) と呼び出され、属性値であることを明示します。
    • src/pkg/exp/html/escape.goUnescapeString() 関数(汎用的な文字列アンエスケープ)では、unescape([]byte(s), false) と呼び出され、デフォルトで属性値ではないものとして処理します。

この変更により、例えば <div bar="ZZ&gt=YY"></div>&gt=YY は、以前は > にアンエスケープされていましたが、変更後は &gt=YY のまま保持されるようになります。これは、HTML5の仕様に準拠した正しい挙動です。

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

src/pkg/exp/html/escape.go

--- a/src/pkg/exp/html/escape.go
+++ b/src/pkg/exp/html/escape.go
@@ -163,14 +163,15 @@ func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) {
 }
 
 // unescape unescapes b's entities in-place, so that "a<b" becomes "a<b".
-func unescape(b []byte) []byte {
+// attribute should be true if parsing an attribute value.
+func unescape(b []byte, attribute bool) []byte {
 	for i, c := range b {
 		if c == '&' {
-\t\t\tdst, src := unescapeEntity(b, i, i, false)
+\t\t\tdst, src := unescapeEntity(b, i, i, attribute)
 			for src < len(b) {
 				c := b[src]
 				if c == '&' {
-\t\t\t\t\tdst, src = unescapeEntity(b, dst, src, false)
+\t\t\t\t\tdst, src = unescapeEntity(b, dst, src, attribute)
 				} else {
 					b[dst] = c
 					dst, src = dst+1, src+1
@@ -250,7 +251,7 @@ func EscapeString(s string) string {
 func UnescapeString(s string) string {
 	for _, c := range s {
 		if c == '&' {
-\t\t\treturn string(unescape([]byte(s)))
+\t\t\treturn string(unescape([]byte(s), false))
 		}
 	}
 	return s

src/pkg/exp/html/token.go

--- a/src/pkg/exp/html/token.go
+++ b/src/pkg/exp/html/token.go
@@ -741,7 +741,7 @@ func (z *Tokenizer) Text() []byte {
 		z.data.end = z.raw.end
 		s = convertNewlines(s)
 		if !z.textIsRaw {
-\t\t\ts = unescape(s)
+\t\t\ts = unescape(s, false)
 		}
 		return s
 	}
@@ -775,7 +775,7 @@ func (z *Tokenizer) TagAttr() (key, val []byte, moreAttr bool) {
 			z.nAttrReturned++
 			key = z.buf[x[0].start:x[0].end]
 			val = z.buf[x[1].start:x[1].end]
-\t\t\treturn lower(key), unescape(convertNewlines(val)), z.nAttrReturned < len(z.attr)
+\t\t\treturn lower(key), unescape(convertNewlines(val), true), z.nAttrReturned < len(z.attr)
 		}
 	}
 	return nil, nil, false

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

このファイルはテスト結果のログであり、変更によって複数の FAILPASS に変わったことを示しています。

--- a/src/pkg/exp/html/testlogs/entities02.dat.log
+++ b/src/pkg/exp/html/testlogs/entities02.dat.log
@@ -2,11 +2,11 @@ PASS "<div bar=\"ZZ>YY\"></div>"
 PASS "<div bar=\"ZZ&amp;\"></div>"
 PASS "<div bar='ZZ&amp;'></div>"
 PASS "<div bar=ZZ&amp;></div>"
-FAIL "<div bar=\"ZZ&gt=YY\"></div>"
-FAIL "<div bar=\"ZZ&gt0YY\"></div>"
-FAIL "<div bar=\"ZZ&gt9YY\"></div>"
-FAIL "<div bar=\"ZZ&gtaYY\"></div>"
-FAIL "<div bar=\"ZZ&gtZYY\"></div>"
+PASS "<div bar=\"ZZ&gt=YY\"></div>"
+PASS "<div bar=\"ZZ&gt0YY\"></div>"
+PASS "<div bar=\"ZZ&gt9YY\"></div>"
+PASS "<div bar=\"ZZ&gtaYY\"></div>"
+PASS "<div bar=\"ZZ&gtZYY\"></div>"
 PASS "<div bar=\"ZZ&gt YY\"></div>"
 PASS "<div bar=\"ZZ&gt\"></div>"
 PASS "<div bar='ZZ&gt'></div>"
@@ -15,7 +15,7 @@ PASS "<div bar=\"ZZ&pound_id=23\"></div>"
 PASS "<div bar=\"ZZ&prod_id=23\"></div>"
 PASS "<div bar=\"ZZ&pound;_id=23\"></div>"
 PASS "<div bar=\"ZZ&prod;_id=23\"></div>"
-FAIL "<div bar=\"ZZ&pound=23\"></div>"
+PASS "<div bar=\"ZZ&pound=23\"></div>"
 PASS "<div bar=\"ZZ&prod=23\"></div>"
 PASS "<div>ZZ&pound_id=23</div>"
 PASS "<div>ZZ&prod_id=23</div>"

src/pkg/exp/html/token_test.go

コメントアウトされていたテストケースが有効化されました。

--- a/src/pkg/exp/html/token_test.go
+++ b/src/pkg/exp/html/token_test.go
@@ -370,14 +370,11 @@ var tokenTests = []tokenTest{
 		`<a b="c&noSuchEntity;d">&lt;&alsoDoesntExist;&`,\
 		`<a b="c&amp;noSuchEntity;d">$&lt;&amp;alsoDoesntExist;&amp;`,\
 	},\
-\t/*
-\t\t// TODO: re-enable this test when it works. This input/output matches html5lib's behavior.
-\t\t{\
-\t\t\t"entity without semicolon",
-\t\t\t`&notit;&notin;<a b="q=z&amp=5&notice=hello&not;=world">`,\
-\t\t\t`¬it;∉$<a b="q=z&amp;amp=5&amp;notice=hello¬=world\">`,\
-\t\t},\
-\t*/
+\t{\
+\t\t"entity without semicolon",
+\t\t`&notit;&notin;<a b="q=z&amp=5&notice=hello&not;=world">`,\
+\t\t`¬it;∉$<a b="q=z&amp;amp=5&amp;notice=hello¬=world">`,\
+\t},\
 	{\
 		"entity with digits",
 		"&frac12;",

コアとなるコードの解説

このコミットの主要な変更は、unescape 関数に attribute というブーリアン引数を追加したことです。この引数は、現在処理中の文字列がHTML属性値であるかどうかを示します。

  1. unescape 関数の役割: この関数は、入力されたバイトスライス b 内のHTMLエンティティをインプレースでアンエスケープ(実体参照を実際の文字に変換)します。例えば、"a&lt;b""a<b" に変換します。

  2. attribute 引数の導入: HTMLの仕様では、通常のテキストコンテンツと属性値ではエンティティの解釈ルールが異なります。特に、セミコロンで終端されていないエンティティの扱いに違いがあります。attribute 引数を導入することで、この違いを unescape 関数内で適切に処理できるようになりました。

  3. unescapeEntity への引数伝播: unescape 関数は、実際にエンティティのアンエスケープ処理を行う unescapeEntity 関数を呼び出します。変更前は unescapeEntity には常に false が渡されていましたが、変更後は unescape に渡された attribute の値がそのまま unescapeEntity に渡されるようになりました。これにより、unescapeEntity 関数内で属性値特有のエンティティ処理ロジックが適用されるようになります。

  4. 呼び出し元の修正:

    • token.goText() メソッドは、HTMLのテキストコンテンツを処理するため、unescape(s, false) と呼び出し、attributefalse に設定します。
    • token.goTagAttr() メソッドは、HTMLタグの属性値を処理するため、unescape(convertNewlines(val), true) と呼び出し、attributetrue に設定します。
    • escape.goUnescapeString() 関数は、汎用的な文字列のアンエスケープを行うため、デフォルトで unescape([]byte(s), false) と呼び出し、attributefalse に設定します。これは、この関数が主にHTMLコンテンツ全体や、属性値ではない一般的な文字列のアンエスケープに使用されることを想定しているためです。

この一連の変更により、exp/html パッケージはHTML5のエンティティ処理仕様、特に属性値における曖昧なエンティティの解釈ルールに、より正確に準拠するようになりました。これにより、Webkitのテストスイートで以前失敗していたテストがパスするようになり、パーサーの堅牢性と互換性が向上しました。

関連リンク

参考にした情報源リンク