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

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

このコミットは、Go言語の実験的なHTMLパーサーである exp/html パッケージにおいて、HTML5仕様に準拠するように外部コンテンツ(Foreign Content)のパースロジックを調整するものです。具体的には、冗長なチェックの削除、ヌルバイトの無視、<font> タグの特殊な扱い、MathMLテキスト統合ポイントの考慮、そして自己終了タグの処理に関する修正が含まれています。

コミット

commit c23041efd99bc2cc7c6888ea6f6a83f5e13f8326
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Fri May 25 10:03:59 2012 +1000

    exp/html: adjust parseForeignContent to match spec
    
    Remove redundant checks for integration points.
    
    Ignore null bytes in text.
    
    Don't break out of foreign content for a <font> tag unless it
    has a color, face, or size attribute.
    
    Check for MathML text integration points when breaking out of
    foreign content.
    
    Pass two new tests.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6256045

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

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

元コミット内容

このコミットの元の内容は以下の通りです。

  • exp/html: adjust parseForeignContent to match spec
    • 統合ポイントに関する冗長なチェックを削除。
    • テキスト内のヌルバイトを無視。
    • <font> タグが color, face, size のいずれかの属性を持たない限り、外部コンテンツから抜け出さないように変更。
    • 外部コンテンツから抜け出す際に、MathMLテキスト統合ポイントをチェックするように変更。
    • 2つの新しいテストがパスするようになった。

変更の背景

この変更の背景には、ウェブブラウザがHTML5の仕様に厳密に従ってドキュメントをパースする必要があるという要件があります。特に、HTMLドキュメント内にSVG (Scalable Vector Graphics) やMathML (Mathematical Markup Language) のような「外部コンテンツ(Foreign Content)」が埋め込まれている場合、そのパースルールは通常のHTMLとは異なります。

HTML5のパース仕様は非常に複雑であり、ブラウザ間の互換性を保証するためには、その細部に至るまで正確に実装することが求められます。このコミットは、exp/html パッケージがこれらの複雑なルール、特に外部コンテンツからの脱出条件や、特定の要素(例: <font> タグ)の振る舞い、そしてテキストデータ内の特殊文字(例: ヌルバイト)の処理に関して、仕様との乖離を修正することを目的としています。

以前の実装では、外部コンテンツからの脱出条件が仕様と異なっていたり、ヌルバイトの処理が不適切であったり、<font> タグの扱いが厳しすぎたりする問題があったと考えられます。これらの不一致は、特定のHTMLドキュメントがGoのパーサーとブラウザで異なるDOMツリーを生成する可能性があり、結果としてウェブアプリケーションの互換性問題を引き起こす原因となり得ます。このコミットは、これらの問題を解消し、パーサーの堅牢性と正確性を向上させるために行われました。

前提知識の解説

このコミットを理解するためには、以下のHTML5パースに関する前提知識が必要です。

  1. HTML5パースアルゴリズム:

    • HTML5のパースは、トークン化(Tokenization)とツリー構築(Tree Construction)の2つの主要なフェーズに分かれます。
    • トークン化: 入力ストリーム(HTML文字列)を、開始タグ、終了タグ、テキスト、コメントなどの「トークン」に分解するプロセスです。
    • ツリー構築: トークナイザーから受け取ったトークンに基づいて、DOM(Document Object Model)ツリーを構築するプロセスです。このフェーズでは、「挿入モード(Insertion Mode)」という概念が重要になります。挿入モードは、現在のパース状態に応じて、どのトークンをどのように処理するかを決定します。
    • 要素スタック(List of active elements): 現在開いている要素(まだ対応する終了タグが来ていない要素)を追跡するためのスタック構造です。DOMツリーの階層構造を管理するために使用されます。
  2. 外部コンテンツ(Foreign Content):

    • HTMLドキュメント内に埋め込まれた、HTMLとは異なるXML名前空間を持つコンテンツを指します。主にSVG (Scalable Vector Graphics) とMathML (Mathematical Markup Language) がこれに該当します。
    • 外部コンテンツ内では、通常のHTMLのパースルールとは異なるXMLのパースルールが適用されます。例えば、HTMLでは大文字・小文字を区別しないタグ名が、SVGやMathMLでは区別されるなど、厳密なXMLの構文規則が適用されます。
  3. 統合ポイント(Integration Points):

    • 外部コンテンツのパース中に、特定の条件が満たされた場合に、再びHTMLのパースルールに戻る(またはその逆)ための「境界」となるポイントです。
    • HTML統合ポイント(HTML integration point): 外部コンテンツ内に特定のHTML要素(例: <p>, <a> など)が出現した場合に、その要素とその子孫をHTMLとしてパースし直すためのポイントです。
    • MathMLテキスト統合ポイント(MathML text integration point): MathMLコンテンツ内で、テキストとして扱われるべき特定の要素(例: <mtext>, <mn>, <mo> など)が出現した場合に、その内部のテキストをHTMLのテキストパースルールで処理するためのポイントです。これは、MathMLの要素がHTMLのテキストノードとして扱われるべき場合に特に重要です。
  4. ヌルバイト(Null Byte, \x00)の扱い:

    • HTML5の仕様では、テキストデータ内にヌルバイトが出現した場合、それを無視するか、特定の文字に置き換えるように規定されています。これは、ヌルバイトが文字列の終端を示すために使われることがあり、セキュリティ上の問題(例: 文字列切り詰めによるパス検証の回避)や、予期せぬパースエラーを引き起こす可能性があるためです。HTML5では、ヌルバイトは通常、U+FFFD REPLACEMENT CHARACTERに置き換えられるか、単に無視されます。このコミットでは、単純に無視(削除)する実装が採用されています。
  5. <font> タグの特殊性:

    • <font> タグはHTML4以前の要素であり、HTML5では非推奨(deprecated)とされています。しかし、後方互換性のためにブラウザはこれをパースする必要があります。
    • 外部コンテンツ内での<font>タグの扱いは、HTML5仕様で特殊なルールが定められています。特に、color, face, size といった属性を持つ<font>タグは、外部コンテンツからHTMLコンテンツへの「脱出」を引き起こす可能性があります。これは、これらの属性がHTMLのレンダリングに直接影響を与えるため、外部コンテンツの文脈から切り離してHTMLとして処理する必要があるためです。

これらの概念を理解することで、parseForeignContent 関数がなぜ、どのように変更されたのか、そしてそれがHTMLパースの正確性にどのように寄与するのかを深く把握することができます。

技術的詳細

このコミットは、Goの exp/html パッケージにおけるHTML5パースアルゴリズムの parseForeignContent 関数に焦点を当てています。この関数は、パーサーがSVGやMathMLなどの外部コンテンツを処理しているときに呼び出されます。

変更の技術的な詳細は以下の通りです。

  1. ヌルバイトの無視 (src/pkg/exp/html/parse.go):

    • TextToken の処理において、以前はHTML統合ポイントのチェックを行っていましたが、これが削除され、代わりに以下の行が追加されました。
      p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
      
    • これは、入力されたテキストデータ (p.tok.Data) 内に含まれるすべてのヌルバイト (\x00) を空文字列に置換することで、実質的にヌルバイトを無視(削除)する処理です。strings.Replace の最後の引数 -1 は、すべての出現箇所を置換することを意味します。これにより、HTML5仕様で規定されているヌルバイトの処理に準拠します。
  2. <font> タグの外部コンテンツからの脱出条件の変更 (src/pkg/exp/html/foreign.go および src/pkg/exp/html/parse.go):

    • src/pkg/exp/html/foreign.go 内の breakout マップから "font": true, のエントリが削除されました。
      • breakout マップは、特定のタグが外部コンテンツ内で出現した場合に、無条件に外部コンテンツから脱出(HTMLパースモードに戻る)させるかどうかを定義していると考えられます。この変更により、<font> タグは単独では外部コンテンツからの脱出を引き起こさなくなります。
    • src/pkg/exp/html/parse.goStartTagToken 処理において、<font> タグに対する特別なロジックが追加されました。
      b := breakout[p.tok.Data]
      if p.tok.Data == "font" {
          loop:
              for _, attr := range p.tok.Attr {
                  switch attr.Key {
                  case "color", "face", "size":
                      b = true
                      break loop
                  }
              }
      }
      if b {
          // ... 外部コンテンツからの脱出ロジック ...
      }
      
      • まず、breakout マップに基づいて b の初期値を設定します。
      • もし現在のタグが <font> であれば、その属性をループでチェックします。
      • 属性の中に color, face, size のいずれかが見つかった場合、btrue に設定し、ループを抜けます。
      • この結果、<font> タグがこれらの属性のいずれかを持つ場合にのみ、btrue となり、その後の if b ブロックで外部コンテンツからの脱出処理が実行されます。これにより、HTML5仕様の<font>タグに関する特殊な脱出ルールが正確に実装されます。
  3. MathMLテキスト統合ポイントの考慮 (src/pkg/exp/html/parse.go):

    • StartTagToken 処理における外部コンテンツからの脱出ロジック内で、要素スタック (p.oe) を遡る際に、以下の条件が追加されました。
      if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
          // ... 外部コンテンツからの脱出ロジック ...
      }
      
    • 以前は htmlIntegrationPoint(p.oe[i]) のチェックのみでしたが、これに mathMLTextIntegrationPoint(n) が追加されました。これは、要素スタックを遡る際に、現在の要素がMathMLテキスト統合ポイントである場合も、外部コンテンツからの脱出条件として考慮することを意味します。これにより、MathMLコンテンツ内の特定の要素がHTMLのテキストとして扱われるべき場合に、正しくパースモードが切り替わるようになります。
  4. 自己終了タグの処理の修正 (src/pkg/exp/html/parse.go):

    • StartTagToken 処理の最後に、以下の新しいブロックが追加されました。
      if p.hasSelfClosingToken {
          p.oe.pop()
          p.acknowledgeSelfClosingTag()
      }
      
    • p.hasSelfClosingToken は、現在のトークンが自己終了タグ(例: <br/>, <img/> など)であるかどうかを示すフラグです。
    • 外部コンテンツ内では、XMLのルールに従い、自己終了タグは要素スタックにプッシュされた後、すぐにポップされる必要があります。このコードは、自己終了タグが検出された場合に、要素スタックからその要素をポップし、自己終了タグが処理されたことをパーサーに通知する (acknowledgeSelfClosingTag) ことで、スタックの状態を正しく維持します。これにより、外部コンテンツ内での自己終了タグのパースが仕様に準拠するようになります。

これらの変更は、HTML5の複雑なパース仕様、特に外部コンテンツと統合ポイントに関するルールを正確に実装するために不可欠です。

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

このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/exp/html/parse.gosrc/pkg/exp/html/foreign.go の2つのファイルに集中しています。

src/pkg/exp/html/foreign.go

--- a/src/pkg/exp/html/foreign.go
+++ b/src/pkg/exp/html/foreign.go
@@ -82,7 +82,6 @@ var breakout = map[string]bool{\
 	"dt":         true,
 	"em":         true,
 	"embed":      true,
-	"font":       true,
 	"h1":         true,
 	"h2":         true,
 	"h3":         true,
  • breakout マップから "font": true, の行が削除されています。これにより、<font> タグはデフォルトでは外部コンテンツからの脱出を引き起こさなくなります。

src/pkg/exp/html/parse.go

--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -1785,12 +1785,7 @@ func afterAfterFramesetIM(p *parser) bool {
 func parseForeignContent(p *parser) bool {
 	switch p.tok.Type {
 	case TextToken:
-		// TODO: HTML integration points.
-		if p.top().Namespace == "" {
-			inBodyIM(p)
-			p.resetInsertionMode()
-			return true
-		}
+		p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
 		if p.framesetOK {
 			p.framesetOK = strings.TrimLeft(p.tok.Data, whitespace) == ""
 		}
@@ -1801,15 +1796,21 @@ func parseForeignContent(p *parser) bool {
 			Data: p.tok.Data,
 		})
 	case StartTagToken:
-		if htmlIntegrationPoint(p.top()) {
-			inBodyIM(p)
-			p.resetInsertionMode()
-			return true
-		}
-		if breakout[p.tok.Data] {
+		b := breakout[p.tok.Data]
+		if p.tok.Data == "font" {
+		loop:
+			for _, attr := range p.tok.Attr {
+				switch attr.Key {
+				case "color", "face", "size":
+					b = true
+					break loop
+				}
+			}
+		}
+		if b {
 			for i := len(p.oe) - 1; i >= 0; i-- {
-				// TODO: MathML integration points.
-				if p.oe[i].Namespace == "" || htmlIntegrationPoint(p.oe[i]) {
+				n := p.oe[i]
+				if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
 					p.oe = p.oe[:i+1]
 					break
 				}
@@ -1833,6 +1834,10 @@ func parseForeignContent(p *parser) bool {
 		namespace := p.top().Namespace
 		p.addElement(p.tok.Data, p.tok.Attr)
 		p.top().Namespace = namespace
+		if p.hasSelfClosingToken {
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+		}
 	case EndTagToken:
 		for i := len(p.oe) - 1; i >= 0; i-- {
 			if p.oe[i].Namespace == "" {
  • TextToken の処理で、ヌルバイトを削除する strings.Replace が追加され、冗長なHTML統合ポイントのチェックが削除されています。
  • StartTagToken の処理で、<font> タグの属性 (color, face, size) に応じて外部コンテンツからの脱出を制御するロジックが追加されています。
  • 要素スタックを遡る際に、mathMLTextIntegrationPoint(n) のチェックが追加され、MathMLテキスト統合ポイントも脱出条件として考慮されるようになっています。
  • 自己終了タグ (p.hasSelfClosingToken) の処理が追加され、要素スタックの管理が修正されています。

src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log

--- a/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
+++ b/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
@@ -16,8 +16,8 @@ FAIL "<svg>\x00filler\x00text"
 FAIL "<svg>\x00<frameset>"
 FAIL "<svg>\x00 <frameset>"
 FAIL "<svg>\x00a<frameset>"
-FAIL "<svg>\x00</svg><frameset>"
-FAIL "<svg>\x00 </svg><frameset>"
+PASS "<svg>\x00</svg><frameset>"
+PASS "<svg>\x00 </svg><frameset>"
 FAIL "<svg>\x00a</svg><frameset>"
 PASS "<svg><path></path></svg><frameset>"
 PASS "<svg><p><frameset>"
  • ヌルバイトを含む2つのテストケースが FAIL から PASS に変更されています。これは、ヌルバイトの無視処理が正しく機能していることを示しています。

コアとなるコードの解説

parseForeignContent 関数

この関数は、HTMLパーサーが外部コンテンツ(SVGやMathML)の内部をパースしているときに呼び出される主要な関数です。HTML5仕様では、外部コンテンツのパースには通常のHTMLとは異なるルールが適用されますが、特定の条件でHTMLパースモードに「切り替える」必要があります。この切り替えが「外部コンテンツからの脱出」です。

TextToken の処理

case TextToken:
    p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
    // ... (framesetOKのチェックなど、他のテキスト処理)
  • 変更前: 以前は、テキストトークンがHTML統合ポイントであるかどうかをチェックし、もしそうであればHTMLパースモードに戻るというロジックがありました。これは冗長であったか、仕様の解釈が不正確であったため削除されました。
  • 変更後: 最も重要な変更は、strings.Replace(p.tok.Data, "\x00", "", -1) の追加です。これは、テキストデータ内に含まれるすべてのヌルバイト (\x00) を削除します。HTML5仕様では、ヌルバイトはテキストデータ内で特別な意味を持たず、通常は無視されるか、U+FFFD REPLACEMENT CHARACTERに置き換えられます。この修正により、パーサーはヌルバイトを含む不正な入力に対してより堅牢になり、仕様に準拠した振る舞いをします。

StartTagToken の処理

このセクションは、開始タグが外部コンテンツ内で検出された場合の処理を定義しており、最も複雑な変更が含まれています。

case StartTagToken:
    b := breakout[p.tok.Data]
    if p.tok.Data == "font" {
    loop:
        for _, attr := range p.tok.Attr {
            switch attr.Key {
            case "color", "face", "size":
                b = true
                break loop
            }
        }
    }
    if b {
        for i := len(p.oe) - 1; i >= 0; i-- {
            n := p.oe[i]
            if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
                p.oe = p.oe[:i+1]
                break
            }
        }
        inBodyIM(p)
        p.resetInsertionMode()
        return true
    }
    // ... (要素の追加、名前空間の設定など)
    if p.hasSelfClosingToken {
        p.oe.pop()
        p.acknowledgeSelfClosingTag()
    }
  1. <font> タグの特殊処理:

    • foreign.go から "font"breakout マップから削除されたため、デフォルトでは <font> タグは外部コンテンツからの脱出を引き起こしません。
    • しかし、このコードブロックでは、もしタグが <font> であり、かつ color, face, size のいずれかの属性を持つ場合、明示的に btrue に設定します。
    • btrue の場合、パーサーは外部コンテンツからの脱出処理(要素スタックを遡り、適切な統合ポイントを見つけてHTMLパースモードに戻る)を実行します。これにより、HTML5仕様で規定されている <font> タグの特殊な脱出ルールが正確に実装されます。これは、これらの属性がHTMLのレンダリングに直接影響するため、外部コンテンツの文脈から切り離してHTMLとして処理する必要があるという仕様の意図を反映しています。
  2. MathMLテキスト統合ポイントのチェック:

    • 外部コンテンツから脱出する条件をチェックするループ内で、mathMLTextIntegrationPoint(n) が追加されました。
    • p.oe は要素スタック(List of active elements)を表します。このループは、スタックを現在の要素からルートに向かって遡り、外部コンテンツからの脱出を引き起こす可能性のある統合ポイントを探します。
    • n.Namespace == "" はHTML名前空間の要素を意味し、htmlIntegrationPoint(n) はHTML統合ポイントをチェックします。
    • mathMLTextIntegrationPoint(n) の追加により、MathMLコンテンツ内の特定の要素(例: <mtext>, <mn>, <mo> など)がHTMLのテキストとして扱われるべき場合に、正しくパースモードが切り替わるようになります。これにより、MathMLとHTMLの間のセマンティックな境界が正確に処理されます。
  3. 自己終了タグの処理:

    • if p.hasSelfClosingToken { p.oe.pop(); p.acknowledgeSelfClosingTag() } のブロックが追加されました。
    • 外部コンテンツ内では、XMLのルールが適用されるため、自己終了タグ(例: <path/> in SVG)は、要素スタックにプッシュされた直後にポップされる必要があります。
    • このコードは、トークンが自己終了タグであるとマークされている場合 (p.hasSelfClosingTokentrue)、要素スタックからその要素をポップし、パーサーに自己終了タグが処理されたことを通知します。これにより、要素スタックが常に正しい状態に保たれ、後続のパースが正確に行われるようになります。

これらの変更は、HTML5の複雑な外部コンテンツパースルールを正確に実装し、GoのHTMLパーサーがより多くの現実世界のHTMLドキュメントを堅牢かつ正確に処理できるようにするために不可欠です。

関連リンク

参考にした情報源リンク