[インデックス 13321] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html
において、HTMLフラグメントをパースする際の context
ノードの整合性チェックを追加するものです。これにより、不正な context
ノードが渡された場合に早期にエラーを検出し、パーサーの堅牢性を向上させます。
コミット
commit 6c204982e03fe69de59991aaa5b16a4fb21297d0
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Jun 8 13:55:15 2012 +1000
exp/html: check the context node for consistency when parsing fragments.
R=rsc
CC=golang-dev
https://golang.org/cl/6303053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6c204982e03fe69de59991aaa5b16a4fb21297d0
元コミット内容
exp/html: check the context node for consistency when parsing fragments.
このコミットは、HTMLフラグメントをパースする際に使用される context
ノードの整合性をチェックする機能を追加します。
変更の背景
HTMLパーサーにおいて、HTMLドキュメント全体ではなく、その一部(フラグメント)をパースする機能は非常に重要です。例えば、innerHTML
のようなDOM操作を行う際に、既存の要素(context
ノード)の内部に新しいHTMLコンテンツを挿入する場合に利用されます。
exp/html
パッケージの ParseFragment
関数は、このHTMLフラグメントのパースを担っています。この関数は、パース対象のHTML文字列と、そのHTMLが挿入されるべき親要素を示す context
ノードを受け取ります。context
ノードは、パースの挙動(例えば、特定の要素内では特定のタグが許可されない、または異なるパースモードが適用されるなど)を決定するために使用されます。
しかし、これまでの実装では、ParseFragment
に渡される context
ノードが、HTMLの仕様上または内部的なデータ構造として不正な状態である場合でも、その不正を検出せずに処理を続行してしまう可能性がありました。例えば、context
ノードが要素ノードではない場合や、ノードのタグ名を表す Data
フィールドと、そのタグ名に対応するアトム(DataAtom
)が矛盾している場合などです。このような不正な context
ノードが渡されると、パーサーが予期せぬ動作をしたり、誤ったDOMツリーを生成したり、最悪の場合クラッシュする可能性がありました。
このコミットは、このような潜在的な問題を未然に防ぐために、ParseFragment
関数に context
ノードの整合性チェックを追加することを目的としています。これにより、パーサーの堅牢性と信頼性が向上し、不正な入力に対する防御が強化されます。
前提知識の解説
このコミットを理解するためには、以下の概念を把握しておく必要があります。
-
HTMLパーシング: HTMLパーシングとは、HTMLドキュメントを読み込み、その構造を解析して、ブラウザがレンダリングできるようなツリー構造(DOMツリー)に変換するプロセスです。HTMLは非常に寛容な言語であり、不正なマークアップに対してもエラーを発生させずに可能な限りパースを試みる「エラー回復」のメカニズムを持っています。
-
HTMLフラグメントのパース: 通常、HTMLドキュメントは
<html>
タグから始まり、完全な構造を持っています。しかし、ウェブアプリケーションでは、既存のDOMツリーの一部に新しいHTMLコンテンツを動的に挿入する(例:element.innerHTML = "..."
)ことが頻繁にあります。この場合、完全なHTMLドキュメントではなく、<div><p>Hello</p></div>
のようなHTMLの「断片」(フラグメント)をパースする必要があります。 フラグメントのパースは、そのフラグメントが挿入される親要素(context
ノード)のコンテキストに大きく依存します。例えば、<table>
要素の内部に<td>
タグをパースする場合と、<div>
要素の内部に<td>
タグをパースする場合では、パーサーの挙動が異なります。これは、HTMLの仕様が、特定の要素の内部で許可される子要素を厳密に定義しているためです。 -
exp/html
パッケージ:exp/html
は、Go言語の標準ライブラリの一部として提供されているgolang.org/x/net/html
パッケージの初期の実験的なバージョンです。これは、HTML5の仕様に準拠したHTMLパーサーを提供することを目的としています。このパッケージは、HTMLドキュメントをトークン化し、DOMツリーを構築する機能を提供します。 -
Node
構造体:exp/html
パッケージでは、HTMLドキュメントの各要素、テキスト、コメントなどをNode
構造体で表現します。Node
構造体には、ノードの種類(Type
、例:ElementNode
,TextNode
)、タグ名(Data
)、タグ名に対応するアトム(DataAtom
)、属性(Attr
)、子ノード(FirstChild
,NextSibling
)などの情報が含まれます。 -
Data
とDataAtom
:Node
構造体におけるData
フィールドは、要素のタグ名(例: "div", "p")やコメントの内容、テキストノードのテキストデータなど、ノードの主要な文字列データを保持します。DataAtom
フィールドは、HTMLの既知のタグ名(例:div
,p
,a
など)に対応する数値ID(アトム)を保持します。アトム化は、文字列比較の代わりに数値比較を行うことで、パーサーのパフォーマンスを向上させるための一般的な最適化手法です。a.Lookup([]byte(context.Data))
は、Data
フィールドの文字列に対応するアトムを検索する関数です。 -
ElementNode
:Node
のType
フィールドがElementNode
である場合、そのノードはHTMLの要素(例:<div>
,<p>
) を表します。ParseFragment
のcontext
ノードは、通常、ElementNode
であることが期待されます。
技術的詳細
このコミットの技術的詳細は、ParseFragment
関数における context
ノードの入力検証にあります。
ParseFragment
関数は、以下のようなシグネチャを持っています。
func ParseFragment(r io.Reader, context *Node) ([]*Node, error)
ここで、context *Node
が、パースされるHTMLフラグメントが挿入されるべき親要素を表します。このコミットでは、context
が nil
でない場合に、以下の2つの整合性チェックが追加されました。
-
context
ノードのタイプチェック: 追加された最初のチェックは、context
ノードのType
がElementNode
であることを確認します。if context.Type != ElementNode { return nil, errors.New("html: ParseFragment of non-element Node") }
ParseFragment
は、HTMLフラグメントを既存のHTML要素の内部にパースすることを想定しています。したがって、context
ノードがElementNode
以外のタイプ(例えば、TextNode
やCommentNode
など)である場合、それは論理的に不正な入力であり、パーサーが正しく動作しない可能性があります。このチェックにより、このような不正なcontext
ノードが渡された場合に、明確なエラーメッセージと共に処理を中断することができます。 -
Data
とDataAtom
の整合性チェック: 追加された2番目のチェックは、context
ノードのData
フィールド(タグ名文字列)とDataAtom
フィールド(タグ名のアトム)が一致していることを確認します。if context.DataAtom != a.Lookup([]byte(context.Data)) { return nil, fmt.Errorf("html: inconsistent Node: DataAtom=%q, Data=%q", context.DataAtom, context.Data) }
Node
構造体において、Data
とDataAtom
は同じタグ名を表す異なる形式のデータです。通常、これらは一貫している必要があります。例えば、Data
が "table" であれば、DataAtom
はatom.Table
に対応するアトム値であるべきです。 このチェックは、context
ノードが外部から構築された際に、Data
とDataAtom
の値が誤って設定され、矛盾した状態になっていることを検出します。コメントにもあるように、「DataAtom == 0
かつData = "tagfromthefuture"
のようなケースは完全に一貫している」とされており、これは既知のアトムではないタグ名の場合でも、DataAtom
が0
でData
がそのタグ名文字列であれば問題ないことを意味します。このチェックは、あくまでData
からa.Lookup
で得られるアトムとDataAtom
が一致しない場合にエラーとします。 この整合性チェックにより、内部データの一貫性が保証され、パーサーが矛盾した情報に基づいて処理を進めることを防ぎます。
これらのチェックは、ParseFragment
が呼び出された直後、かつ context
ノードが実際にパースロジックに利用される前に実行されます。これにより、不正な入力がパーサーのより深い部分に影響を与える前に問題を特定し、エラーを返すことができます。
コアとなるコードの変更箇所
変更は主に src/pkg/exp/html/parse.go
ファイルの ParseFragment
関数内と、src/pkg/exp/html/parse_test.go
ファイルのテストコードにあります。
src/pkg/exp/html/parse.go
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -5,7 +5,9 @@
package html
import (
+ "errors"
a "exp/html/atom"
+ "fmt"
"io"
"strings"
)
@@ -2013,6 +2015,15 @@ func ParseFragment(r io.Reader, context *Node) ([]*Node, error) {
}
if context != nil {
+ if context.Type != ElementNode {
+ return nil, errors.New("html: ParseFragment of non-element Node")
+ }
+ // The next check isn't just context.DataAtom.String() == context.Data because
+ // it is valid to pass an element whose tag isn't a known atom. For example,
+ // DataAtom == 0 and Data = "tagfromthefuture" is perfectly consistent.
+ if context.DataAtom != a.Lookup([]byte(context.Data)) {
+ return nil, fmt.Errorf("html: inconsistent Node: DataAtom=%q, Data=%q", context.DataAtom, context.Data)
+ }
switch context.DataAtom {
case a.Iframe, a.Noembed, a.Noframes, a.Noscript, a.Plaintext, a.Script, a.Style, a.Title, a.Textarea, a.Xmp:
p.tokenizer.rawTag = context.DataAtom.String()
src/pkg/exp/html/parse_test.go
--- a/src/pkg/exp/html/parse_test.go
+++ b/src/pkg/exp/html/parse_test.go
@@ -391,6 +391,19 @@ var renderTestBlacklist = map[string]bool{\n `<table><plaintext><td>`: true,\n }\n \n+func TestNodeConsistency(t *testing.T) {\n+\t// inconsistentNode is a Node whose DataAtom and Data do not agree.\n+\tinconsistentNode := &Node{\n+\t\tType: ElementNode,\n+\t\tDataAtom: atom.Frameset,\n+\t\tData: "table",\n+\t}\n+\t_, err := ParseFragment(strings.NewReader("<p>hello</p>"), inconsistentNode)\n+\tif err == nil {\n+\t\tt.Errorf("got nil error, want non-nil")\n+\t}\n+}\n+\n func BenchmarkParser(b *testing.B) {\n buf, err := ioutil.ReadFile("testdata/go1.html")\n if err != nil {\n```
## コアとなるコードの解説
### `src/pkg/exp/html/parse.go` の変更
1. **新しいインポート**:
`"errors"` と `"fmt"` パッケージがインポートされています。これらは、新しいエラーを生成し、フォーマットするために使用されます。
2. **`ParseFragment` 関数内の変更**:
`if context != nil { ... }` ブロックの内部に、2つの新しい `if` ステートメントが追加されています。
* **`context.Type != ElementNode` チェック**:
`context` ノードの `Type` が `ElementNode` でない場合、`errors.New` を使用して `"html: ParseFragment of non-element Node"` というエラーメッセージを含む新しいエラーを生成し、`nil` ノードスライスと共に返します。これにより、`context` が要素ノードではないという不正な状態を捕捉します。
* **`context.DataAtom != a.Lookup([]byte(context.Data))` チェック**:
`context` ノードの `DataAtom` と、`context.Data` から `a.Lookup` で取得したアトムが一致しない場合、`fmt.Errorf` を使用して `"html: inconsistent Node: DataAtom=%q, Data=%q"` という詳細なエラーメッセージを生成し、`nil` ノードスライスと共に返します。このエラーメッセージには、矛盾している `DataAtom` と `Data` の値が含まれるため、デバッグが容易になります。
このチェックのコメントは、`DataAtom == 0` かつ `Data = "tagfromthefuture"` のようなケース(既知のアトムではないタグ)は一貫していると見なされることを明確にしています。これは、`a.Lookup` が既知のアトムを見つけられない場合に `0` を返すためです。したがって、このチェックは、`Data` と `DataAtom` が論理的に矛盾している場合にのみエラーを発生させます。
### `src/pkg/exp/html/parse_test.go` の変更
1. **`TestNodeConsistency` 関数の追加**:
この新しいテスト関数は、`ParseFragment` に不正な `context` ノードを渡した場合に、期待通りエラーが返されることを検証します。
* **`inconsistentNode` の作成**:
`Node` 構造体のインスタンス `inconsistentNode` が作成されます。このノードは、`Type` が `ElementNode` であるにもかかわらず、`DataAtom` が `atom.Frameset` で、`Data` が `"table"` と設定されています。これは、`DataAtom` と `Data` が矛盾している状態を意図的に作り出しています(`Frameset` と `table` は異なるHTMLタグです)。
* **`ParseFragment` の呼び出しとエラーチェック**:
`ParseFragment` 関数が、ダミーのHTML文字列(`<p>hello</p>`)と、先ほど作成した `inconsistentNode` を `context` として渡して呼び出されます。
テストは、`ParseFragment` がエラーを返すべきであると期待しており、`if err == nil { t.Errorf("got nil error, want non-nil") }` というアサーションで、エラーが返されなかった場合にテストを失敗させます。これにより、追加された整合性チェックが正しく機能していることを確認します。
これらの変更により、`exp/html` パッケージの `ParseFragment` 関数は、より堅牢になり、不正な `context` ノードが渡された場合の予期せぬ動作を防ぐことができるようになりました。
## 関連リンク
* Go言語の `exp/html` パッケージ(現在の `golang.org/x/net/html`)のドキュメント: [https://pkg.go.dev/golang.org/x/net/html](https://pkg.go.dev/golang.org/x/net/html)
* HTML5パーシングアルゴリズムの仕様: [https://html.spec.whatwg.org/multipage/parsing.html](https://html.spec.whatwg.org/multipage/parsing.html)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* HTML5仕様
* Go言語の `exp/html` パッケージのソースコード
* Go言語の `golang.org/x/net/html` パッケージのソースコード
* Go言語の `errors` パッケージと `fmt` パッケージのドキュメント
* Go言語の `testing` パッケージのドキュメント