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

[インデックス 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 ノードの整合性チェックを追加することを目的としています。これにより、パーサーの堅牢性と信頼性が向上し、不正な入力に対する防御が強化されます。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  1. HTMLパーシング: HTMLパーシングとは、HTMLドキュメントを読み込み、その構造を解析して、ブラウザがレンダリングできるようなツリー構造(DOMツリー)に変換するプロセスです。HTMLは非常に寛容な言語であり、不正なマークアップに対してもエラーを発生させずに可能な限りパースを試みる「エラー回復」のメカニズムを持っています。

  2. HTMLフラグメントのパース: 通常、HTMLドキュメントは <html> タグから始まり、完全な構造を持っています。しかし、ウェブアプリケーションでは、既存のDOMツリーの一部に新しいHTMLコンテンツを動的に挿入する(例: element.innerHTML = "...")ことが頻繁にあります。この場合、完全なHTMLドキュメントではなく、<div><p>Hello</p></div> のようなHTMLの「断片」(フラグメント)をパースする必要があります。 フラグメントのパースは、そのフラグメントが挿入される親要素(context ノード)のコンテキストに大きく依存します。例えば、<table> 要素の内部に <td> タグをパースする場合と、<div> 要素の内部に <td> タグをパースする場合では、パーサーの挙動が異なります。これは、HTMLの仕様が、特定の要素の内部で許可される子要素を厳密に定義しているためです。

  3. exp/html パッケージ: exp/html は、Go言語の標準ライブラリの一部として提供されている golang.org/x/net/html パッケージの初期の実験的なバージョンです。これは、HTML5の仕様に準拠したHTMLパーサーを提供することを目的としています。このパッケージは、HTMLドキュメントをトークン化し、DOMツリーを構築する機能を提供します。

  4. Node 構造体: exp/html パッケージでは、HTMLドキュメントの各要素、テキスト、コメントなどを Node 構造体で表現します。Node 構造体には、ノードの種類(Type、例: ElementNode, TextNode)、タグ名(Data)、タグ名に対応するアトム(DataAtom)、属性(Attr)、子ノード(FirstChild, NextSibling)などの情報が含まれます。

  5. DataDataAtom: Node 構造体における Data フィールドは、要素のタグ名(例: "div", "p")やコメントの内容、テキストノードのテキストデータなど、ノードの主要な文字列データを保持します。 DataAtom フィールドは、HTMLの既知のタグ名(例: div, p, a など)に対応する数値ID(アトム)を保持します。アトム化は、文字列比較の代わりに数値比較を行うことで、パーサーのパフォーマンスを向上させるための一般的な最適化手法です。a.Lookup([]byte(context.Data)) は、Data フィールドの文字列に対応するアトムを検索する関数です。

  6. ElementNode: NodeType フィールドが ElementNode である場合、そのノードはHTMLの要素(例: <div>, <p>) を表します。ParseFragmentcontext ノードは、通常、ElementNode であることが期待されます。

技術的詳細

このコミットの技術的詳細は、ParseFragment 関数における context ノードの入力検証にあります。

ParseFragment 関数は、以下のようなシグネチャを持っています。

func ParseFragment(r io.Reader, context *Node) ([]*Node, error)

ここで、context *Node が、パースされるHTMLフラグメントが挿入されるべき親要素を表します。このコミットでは、contextnil でない場合に、以下の2つの整合性チェックが追加されました。

  1. context ノードのタイプチェック: 追加された最初のチェックは、context ノードの TypeElementNode であることを確認します。

    if context.Type != ElementNode {
        return nil, errors.New("html: ParseFragment of non-element Node")
    }
    

    ParseFragment は、HTMLフラグメントを既存のHTML要素の内部にパースすることを想定しています。したがって、context ノードが ElementNode 以外のタイプ(例えば、TextNodeCommentNode など)である場合、それは論理的に不正な入力であり、パーサーが正しく動作しない可能性があります。このチェックにより、このような不正な context ノードが渡された場合に、明確なエラーメッセージと共に処理を中断することができます。

  2. DataDataAtom の整合性チェック: 追加された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 構造体において、DataDataAtom は同じタグ名を表す異なる形式のデータです。通常、これらは一貫している必要があります。例えば、Data が "table" であれば、DataAtomatom.Table に対応するアトム値であるべきです。 このチェックは、context ノードが外部から構築された際に、DataDataAtom の値が誤って設定され、矛盾した状態になっていることを検出します。コメントにもあるように、「DataAtom == 0 かつ Data = "tagfromthefuture" のようなケースは完全に一貫している」とされており、これは既知のアトムではないタグ名の場合でも、DataAtom0Data がそのタグ名文字列であれば問題ないことを意味します。このチェックは、あくまで 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` パッケージのドキュメント