[インデックス 10542] ファイルの概要
このコミットは、Go言語の標準ライブラリであるhtmlパッケージにおける<iframe>要素のパースとレンダリングの挙動を修正するものです。具体的には、<iframe>要素の内容を「生テキスト(raw text)」として扱うように変更し、HTML5の仕様に準拠させることが目的です。これにより、<iframe>内部のコンテンツがHTMLとしてではなく、プレーンテキストとして解釈されるようになります。
コミット
commit e32f4ba77d920411e916cece41b3a40e0db0a074
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Wed Nov 30 11:44:54 2011 +1100
html: parse the contents of <iframe> elements as raw text
Pass tests5.dat, test 4:
<iframe> <!---> </iframe>x
| <html>
| <head>
| <body>
| <iframe>
| " <!---> "
| "x"
Also pass tests through test 9:
<style> <!</-- </style>x
R=nigeltao
CC=golang-dev
https://golang.org/cl/5450044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e32f4ba77d920411e916cece41b3a40e0db0a074
元コミット内容
このコミットは、<iframe>要素のコンテンツをHTMLとしてパースするのではなく、<script>や<style>要素と同様に「生テキスト」としてパースするように変更します。これにより、<iframe>タグ内のHTMLコメントなどがそのままテキストとして扱われるようになります。
コミットメッセージには、tests5.datのテスト4とテスト9をパスすることが示されています。
- テスト4の例:
<iframe> <!---> </iframe>x- この入力は、
<iframe>タグ内の<!--->がHTMLコメントとしてではなく、そのままテキストコンテンツとしてパースされることを示しています。
- この入力は、
- テスト9の例:
<style> <!</-- </style>x- これは、
<style>タグのような既存の生テキスト要素においても、不正なコメント構文が正しく生テキストとして扱われることを確認しています。
- これは、
変更の背景
HTML5の仕様では、特定の要素(例: <script>, <style>, <textarea>, <title>, <noembed>, <noframes>, <noscript>, <plaintext>)は「生テキスト要素(raw text elements)」または「RCDATA要素(Raw Character Data elements)」として定義されています。これらの要素の内部コンテンツは、通常のHTMLマークアップとしてではなく、プレーンテキストとして扱われます。これは、セキュリティ上の理由(例: スクリプトインジェクションの防止)や、コンテンツの正確な表示(例: CSSルールをそのまま表示)のために重要です。
このコミット以前のGoのhtmlパッケージでは、<iframe>要素がこの生テキスト要素のリストに含まれていなかった可能性があります。その結果、<iframe>内部のコンテンツが意図せずHTMLとしてパースされ、予期せぬ挙動やセキュリティ上の問題を引き起こす可能性がありました。このコミットは、<iframe>要素のパース挙動をHTML5の仕様に厳密に合わせることで、これらの問題を解決し、パーサーの堅牢性と正確性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
- HTMLパーシング: HTMLドキュメントを解析し、ブラウザが理解できる構造(DOMツリーなど)に変換するプロセスです。このプロセスには、入力ストリームをトークンに分割する「トークナイザー(字句解析器)」と、トークンストリームをツリー構造に構築する「ツリー構築器(構文解析器)」が含まれます。
- HTMLの生テキスト要素(Raw Text Elements): HTML5仕様で定義されている特殊な要素群です。これらの要素(例:
<script>,<style>,<textarea>,<title>)の内部コンテンツは、通常のHTMLマークアップとしてではなく、単なるテキストデータとして扱われます。つまり、これらのタグの内部では、<や&のような文字が特殊な意味を持たず、リテラル文字として解釈されます。パーサーは、対応する終了タグ(例:</script>)が見つかるまで、コンテンツをテキストとして読み込み続けます。 - RCDATA要素(Raw Character Data Elements): 生テキスト要素と似ていますが、エンティティ参照(例:
<,&)が処理される点が異なります。<textarea>や<title>がこれに該当します。 - Go言語の
htmlパッケージ: Go言語の標準ライブラリの一部で、HTML5の仕様に準拠したHTMLのパースとレンダリング機能を提供します。このパッケージは、ウェブスクレイピング、HTMLテンプレートの処理、HTMLコンテンツのサニタイズなど、様々な用途で利用されます。 - トークナイザー(Lexer/Tokenizer): 入力文字列を、プログラミング言語やマークアップ言語の最小単位である「トークン」のシーケンスに変換するプログラムの一部です。HTMLパーサーにおいては、HTMLタグ、属性、テキストコンテンツなどをトークンとして識別します。
- レンダラー(Renderer): パースされたデータ構造(例: DOMツリー)を受け取り、それを元の形式(この場合はHTML文字列)に変換して出力するプログラムの一部です。
技術的詳細
このコミットの技術的な核心は、Goのhtmlパッケージが<iframe>要素のコンテンツをどのように処理するかを変更することにあります。
-
トークナイザーの変更 (
src/pkg/html/token.go):- HTMLパーサーのトークナイザーは、開始タグを読み込んだ際に、そのタグが「生テキスト要素」であるかどうかを判断します。生テキスト要素であれば、その後のコンテンツを通常のHTMLマークアップとしてではなく、プレーンテキストとして読み込むモードに切り替えます。
- このコミットでは、
readStartTag関数内の生テキスト要素を識別するロジックに"iframe"が追加されました。具体的には、タグ名の最初の文字をチェックするswitch文に'i'と'I'が追加され、さらにタグ名を文字列として比較するswitch文に"iframe"が追加されています。 - これにより、トークナイザーは
<iframe>開始タグを検出すると、内部状態(z.rawTag)を更新し、次に現れる対応する</iframe>終了タグまで、その間のすべての文字をテキストトークンとして生成するようになります。
-
レンダラーの変更 (
src/pkg/html/render.go):- HTML5の仕様では、生テキスト要素の内部にはテキストノードのみが存在することが許されています。もし生テキスト要素の内部にテキスト以外のノード(例: 他の要素、コメント、DOCTYPEなど)が存在する場合、それは不正なHTML構造と見なされます。
- このコミットでは、
render1関数内の、生テキスト要素の子供ノードをチェックするswitch文に"iframe"が追加されました。 - これにより、レンダラーは
<iframe>要素をレンダリングする際に、その子ノードがテキストノードであることを強制します。もしテキストノード以外の子供ノードが見つかった場合、"html: raw text element <%s> has non-text child node"というエラーが返され、不正な構造が検出されます。これは、パースされたDOMツリーの整合性を保つ上で重要です。
-
テストケースの追加 (
src/pkg/html/parse_test.go):- 変更されたパースロジックが正しく機能することを検証するために、
tests5.datがテストスイートに追加されました。特に、コミットメッセージに示されているテスト4(<iframe> <!---> </iframe>x)は、<iframe>内部のHTMLコメントが正しく生テキストとして扱われることを確認するものです。 - テストの追加は、コード変更の品質と信頼性を保証するための重要なステップです。
- 変更されたパースロジックが正しく機能することを検証するために、
これらの変更により、Goのhtmlパッケージは<iframe>要素をHTML5の仕様に沿って正確に処理できるようになり、パーサーの堅牢性と予測可能性が向上します。
コアとなるコードの変更箇所
src/pkg/html/render.go
--- a/src/pkg/html/render.go
+++ b/src/pkg/html/render.go
@@ -185,7 +185,7 @@ func render1(w writer, n *Node) error {
// Render any child nodes.
switch n.Data {
- case "noembed", "noframes", "noscript", "plaintext", "script", "style":
+ case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style":
for _, c := range n.Child {
if c.Type != TextNode {
return fmt.Errorf("html: raw text element <%s> has non-text child node", n.Data)
src/pkg/html/token.go
--- a/src/pkg/html/token.go
+++ b/src/pkg/html/token.go
@@ -405,14 +405,13 @@ func (z *Tokenizer) readStartTag() TokenType {
break
}
}
- // Any "<noembed>", "<noframes>", "<noscript>", "<plaintext", "<script>", "<style>",
- // "<textarea>" or "<title>" tag flags the tokenizer's next token as raw.
+ // Several tags flag the tokenizer's next token as raw.
// The tag name lengths of these special cases ranges in [5, 9].
if x := z.data.end - z.data.start; 5 <= x && x <= 9 {
switch z.buf[z.data.start] {
- case 'n', 'p', 's', 't', 'N', 'P', 'S', 'T':
+ case 'i', 'n', 'p', 's', 't', 'I', 'N', 'P', 'S', 'T':
\tswitch s := strings.ToLower(string(z.buf[z.data.start:z.data.end])); s {
- \tcase "noembed", "noframes", "noscript", "plaintext", "script", "style", "textarea", "title":
+ \tcase "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "textarea", "title":
\t\tz.rawTag = s
\t}
}
src/pkg/html/parse_test.go
--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -153,6 +153,8 @@ func TestParser(t *testing.T) {
{"tests1.dat", -1},
{"tests2.dat", -1},
{"tests3.dat", -1},
+ // tests4.dat is fragment cases.
+ {"tests5.dat", 10},
}
for _, tf := range testFiles {
f, err := os.Open("testdata/webkit/" + tf.filename)
コアとなるコードの解説
-
src/pkg/html/render.goの変更:render1関数は、HTMLノードをレンダリングする際のロジックを含んでいます。switch n.Dataブロックは、特定のタグ(生テキスト要素)に対して特別な処理を行います。この変更では、既存の生テキスト要素のリスト("noembed","noframes","noscript","plaintext","script","style")に"iframe"が追加されました。これにより、レンダラーは<iframe>要素の子供ノードがTextNode(テキストノード)であることを期待し、もしそれ以外のノードタイプが見つかった場合はエラーを返します。これは、HTML5仕様における生テキスト要素のコンテンツモデルを強制するものです。 -
src/pkg/html/token.goの変更:readStartTag関数は、HTMLトークナイザーの一部であり、開始タグを読み込む際に呼び出されます。この関数は、読み込んだタグが生テキスト要素であるかどうかを判断し、その後のパースモードを切り替えます。switch z.buf[z.data.start]の行では、タグ名の最初の文字に基づいて高速なフィルタリングを行います。この変更では、'i'と'I'が追加され、<iframe>タグがこの初期チェックで考慮されるようになりました。- その後の
switch s := strings.ToLower(string(z.buf[z.data.start:z.data.end])); sの行では、タグ名を小文字に変換して完全な文字列比較を行います。このswitch文のcaseリストに"iframe"が追加されました。 これらの変更により、トークナイザーは<iframe>開始タグを検出した際に、その後のコンテンツを通常のHTMLマークアップとしてではなく、生テキストとして処理するモードに正しく移行するようになります。z.rawTag = sは、トークナイザーが現在処理している生テキストタグの名前を記憶し、対応する終了タグを見つけるために使用されます。
-
src/pkg/html/parse_test.goの変更: このファイルは、HTMLパーサーのテストケースを定義しています。testFilesスライスに{"tests5.dat", 10}が追加されました。これは、testdata/webkit/tests5.datファイルから最初の10個のテストケースを実行することを意味します。このテストファイルには、<iframe>の生テキストパース挙動を検証するための具体的なテストケース(例:<iframe> <!---> </iframe>x)が含まれており、今回の変更がHTML5の仕様に準拠していることを確認します。
これらのコード変更は、Goのhtmlパッケージが<iframe>要素をHTML5の仕様に厳密に従ってパースおよびレンダリングするようにすることで、パーサーの正確性、堅牢性、およびセキュリティを向上させます。
関連リンク
- HTML5仕様: https://html.spec.whatwg.org/multipage/syntax.html#raw-text-elements
- Go言語の
htmlパッケージドキュメント: https://pkg.go.dev/golang.org/x/net/html (Go 1.12以降はgolang.org/x/net/htmlに移動)
参考にした情報源リンク
- Go CL 5450044: https://golang.org/cl/5450044 (コミットメッセージに記載されているGoのコードレビューリンク)
- HTML5 Parsing: https://www.w3.org/TR/html5/syntax.html#parsing (HTML5のパースに関するW3Cの古い仕様、WHATWGが最新)
- HTML5 Living Standard (WHATWG): https://html.spec.whatwg.org/ (HTML5の最新のLiving Standard)