[インデックス 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パーサーの標準的なテストセットの一つ)の一部をパスできていなかったという問題があります。具体的には、属性値内でセミコロンで終わらないエンティティ(例: >
)が、その直後に特定の文字(=
, 英字, 数字)を伴う場合に、誤ってエンティティとして解釈されてしまう挙動がありました。
HTML5の仕様では、数値文字参照や名前付き文字参照は、その直後に数字や英字が続く場合、セミコロンで終端されない限り、エンティティとして解釈されないというルールがあります。例えば、<a href="foo&bar">
の &bar
はエンティティではなく、単なるリテラル文字列として扱われるべきです。しかし、&
のようにセミコロンがなくてもエンティティとして認識される特殊なケースも存在します。
このコミットは、このようなHTML5の仕様に準拠し、Webkitのテストをパスするために、属性値内のエンティティ処理ロジックをより厳密に、かつ正確に修正することを目的としています。
前提知識の解説
HTMLエンティティ(実体参照)
HTMLエンティティは、HTMLドキュメント内で特殊文字(例: <
や >
)や、キーボードから直接入力できない文字(例: ©
)を表現するための仕組みです。主に以下の2種類があります。
- 名前付き文字参照 (Named Character References):
<
(less than),>
(greater than),&
(ampersand),"
(double quote),'
(apostrophe, HTML5で導入) など、特定の名前で参照されるもの。 - 数値文字参照 (Numeric Character References): 文字のUnicodeコードポイントを数値で指定するもの。
- 10進数:
&#DDDD;
(例:<
は<
) - 16進数:
&#xHHHH;
(例:<
は<
)
- 10進数:
エンティティの終端と曖昧性
HTMLの仕様では、エンティティは通常セミコロン(;
)で終端されます。しかし、一部のエンティティ(特に &
, <
, >
, "
, '
)は、歴史的な理由からセミコロンがなくても認識される場合があります。
さらに重要なのは、HTML5のパースルールにおいて、エンティティの直後に特定の文字が続く場合の曖昧性の解消です。例えば、&
のようにセミコロンがないエンティティの直後に英数字が続く場合、それはエンティティとして解釈されず、リテラル文字列として扱われるべきです。
例:
&
->&
(エンティティとして解釈)&
->&
(リテラル文字列として解釈、セミコロンがないため)>
->>
(エンティティとして解釈)>
->>
(リテラル文字列として解釈、セミコロンがないため)>=YY
->>=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の仕様では、属性値内のエンティティ処理には特別なルールが適用されます。
変更後のロジックは以下のようになります。
-
unescape
関数のシグネチャ変更:func unescape(b []byte) []byte
からfunc unescape(b []byte, attribute bool) []byte
へと変更されました。attribute
引数は、現在処理しているバイト列がHTML属性値であるかどうかを示します。 -
unescapeEntity
関数へのattribute
フラグの伝播:unescape
関数内でエンティティのアンエスケープを行うunescapeEntity
関数が呼び出される際に、この新しいattribute
フラグが渡されるようになりました。 -
unescapeEntity
内の条件付きアンエスケープ:unescapeEntity
関数(変更差分には直接示されていませんが、この関数内でロジックが変更されたと推測されます)は、attribute
がtrue
の場合、つまり属性値を処理している場合に、より厳密なエンティティ終端ルールを適用します。 具体的には、セミコロンで終端されていないエンティティが、その直後にイコール記号(=
)、英字(a-z
,A-Z
)、または数字(0-9
)を伴う場合、それはエンティティとしてアンエスケープされず、リテラル文字列として扱われます。 -
呼び出し元の調整:
src/pkg/exp/html/token.go
のText()
メソッド(テキストノードを処理)では、unescape(s, false)
と呼び出され、属性値ではないことを明示します。src/pkg/exp/html/token.go
のTagAttr()
メソッド(タグの属性値を処理)では、unescape(convertNewlines(val), true)
と呼び出され、属性値であることを明示します。src/pkg/exp/html/escape.go
のUnescapeString()
関数(汎用的な文字列アンエスケープ)では、unescape([]byte(s), false)
と呼び出され、デフォルトで属性値ではないものとして処理します。
この変更により、例えば <div bar="ZZ>=YY"></div>
の >=YY
は、以前は >
にアンエスケープされていましたが、変更後は >=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
このファイルはテスト結果のログであり、変更によって複数の FAIL
が PASS
に変わったことを示しています。
--- 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&\"></div>"
PASS "<div bar='ZZ&'></div>"
PASS "<div bar=ZZ&></div>"
-FAIL "<div bar=\"ZZ>=YY\"></div>"
-FAIL "<div bar=\"ZZ>0YY\"></div>"
-FAIL "<div bar=\"ZZ>9YY\"></div>"
-FAIL "<div bar=\"ZZ>aYY\"></div>"
-FAIL "<div bar=\"ZZ>ZYY\"></div>"
+PASS "<div bar=\"ZZ>=YY\"></div>"
+PASS "<div bar=\"ZZ>0YY\"></div>"
+PASS "<div bar=\"ZZ>9YY\"></div>"
+PASS "<div bar=\"ZZ>aYY\"></div>"
+PASS "<div bar=\"ZZ>ZYY\"></div>"
PASS "<div bar=\"ZZ> YY\"></div>"
PASS "<div bar=\"ZZ>\"></div>"
PASS "<div bar='ZZ>'></div>"
@@ -15,7 +15,7 @@ PASS "<div bar=\"ZZ£_id=23\"></div>"
PASS "<div bar=\"ZZ&prod_id=23\"></div>"
PASS "<div bar=\"ZZ£_id=23\"></div>"
PASS "<div bar=\"ZZ∏_id=23\"></div>"
-FAIL "<div bar=\"ZZ£=23\"></div>"
+PASS "<div bar=\"ZZ£=23\"></div>"
PASS "<div bar=\"ZZ&prod=23\"></div>"
PASS "<div>ZZ£_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"><&alsoDoesntExist;&`,\
`<a b="c&noSuchEntity;d">$<&alsoDoesntExist;&`,\
},\
-\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`¬it;∉<a b="q=z&=5¬ice=hello¬=world">`,\
-\t\t\t`¬it;∉$<a b="q=z&amp=5&notice=hello¬=world\">`,\
-\t\t},\
-\t*/
+\t{\
+\t\t"entity without semicolon",
+\t\t`¬it;∉<a b="q=z&=5¬ice=hello¬=world">`,\
+\t\t`¬it;∉$<a b="q=z&amp=5&notice=hello¬=world">`,\
+\t},\
{\
"entity with digits",
"½",
コアとなるコードの解説
このコミットの主要な変更は、unescape
関数に attribute
というブーリアン引数を追加したことです。この引数は、現在処理中の文字列がHTML属性値であるかどうかを示します。
-
unescape
関数の役割: この関数は、入力されたバイトスライスb
内のHTMLエンティティをインプレースでアンエスケープ(実体参照を実際の文字に変換)します。例えば、"a<b"
を"a<b"
に変換します。 -
attribute
引数の導入: HTMLの仕様では、通常のテキストコンテンツと属性値ではエンティティの解釈ルールが異なります。特に、セミコロンで終端されていないエンティティの扱いに違いがあります。attribute
引数を導入することで、この違いをunescape
関数内で適切に処理できるようになりました。 -
unescapeEntity
への引数伝播:unescape
関数は、実際にエンティティのアンエスケープ処理を行うunescapeEntity
関数を呼び出します。変更前はunescapeEntity
には常にfalse
が渡されていましたが、変更後はunescape
に渡されたattribute
の値がそのままunescapeEntity
に渡されるようになりました。これにより、unescapeEntity
関数内で属性値特有のエンティティ処理ロジックが適用されるようになります。 -
呼び出し元の修正:
token.go
のText()
メソッドは、HTMLのテキストコンテンツを処理するため、unescape(s, false)
と呼び出し、attribute
をfalse
に設定します。token.go
のTagAttr()
メソッドは、HTMLタグの属性値を処理するため、unescape(convertNewlines(val), true)
と呼び出し、attribute
をtrue
に設定します。escape.go
のUnescapeString()
関数は、汎用的な文字列のアンエスケープを行うため、デフォルトでunescape([]byte(s), false)
と呼び出し、attribute
をfalse
に設定します。これは、この関数が主にHTMLコンテンツ全体や、属性値ではない一般的な文字列のアンエスケープに使用されることを想定しているためです。
この一連の変更により、exp/html
パッケージはHTML5のエンティティ処理仕様、特に属性値における曖昧なエンティティの解釈ルールに、より正確に準拠するようになりました。これにより、Webkitのテストスイートで以前失敗していたテストがパスするようになり、パーサーの堅牢性と互換性が向上しました。
関連リンク
- Go言語の
exp/html
パッケージ: https://pkg.go.dev/golang.org/x/net/html (現在はgolang.org/x/net/html
に移動しています) - HTML Living Standard - 13.2.5.7 Named character references: https://html.spec.whatwg.org/multipage/parsing.html#named-character-references
- HTML Living Standard - 13.2.5.70 Tokenizing character references: https://html.spec.whatwg.org/multipage/parsing.html#tokenizing-character-references
参考にした情報源リンク
- HTML Living Standard (WHATWG): https://html.spec.whatwg.org/
- WebKit: https://webkit.org/
- Go Code Review Comments (CL 6405073): https://golang.org/cl/6405073 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
- HTML Entities - W3Schools: https://www.w3schools.com/html/html_entities.asp
- Character entities in HTML - MDN Web Docs: https://developer.mozilla.org/en-US/docs/Glossary/Character_entity
- HTML5 Parsing: Character References - Stack Overflow: https://stackoverflow.com/questions/13100100/html5-parsing-character-references
- HTML5: The difference between & and & - Stack Overflow: https://stackoverflow.com/questions/1060513/html5-the-difference-between-amp-and-amp-amp