[インデックス 13507] ファイルの概要
このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html
における重要な改善を導入しています。具体的には、外部コンテンツ(Foreign Content)内のCDATAセクションのパース処理を修正し、コメント内のNULLバイト(\x00
)をUnicodeの置換文字(U+FFFD)に変換するようになりました。これにより、HTML5のパース仕様への準拠が向上し、23の追加テストがパスするようになっています。
コミット
commit a1f340fa1a26fd29f1369cbc91755e7519813dd0
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Fri Jul 27 16:05:25 2012 +1000
exp/html: parse CDATA sections in foreign content
Also convert NUL to U+FFFD in comments.
Pass 23 additional tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6446055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a1f340fa1a26fd29f1369cbc91755e7519813dd0
元コミット内容
exp/html: parse CDATA sections in foreign content
Also convert NUL to U+FFFD in comments.
Pass 23 additional tests.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6446055
変更の背景
HTML5のパース仕様は非常に複雑であり、特にXML由来の要素(SVGやMathMLなど)がHTMLドキュメント内に埋め込まれる「外部コンテンツ(Foreign Content)」の扱いには、HTMLとは異なるルールが適用されます。XMLではCDATAセクションがテキストデータをマークアップとして解釈させずに含めるために使用されますが、HTMLの文脈では通常はサポートされません。しかし、外部コンテンツ内ではCDATAセクションが有効であるため、HTMLパーサーはこれを正しく処理する必要があります。
このコミット以前のexp/html
パッケージは、外部コンテンツ内のCDATAセクションを適切にパースできていませんでした。これにより、SVGやMathMLなどのXMLベースのコンテンツがHTMLドキュメントに埋め込まれた際に、その内部のテキストが正しく解釈されない、あるいはパースエラーを引き起こす可能性がありました。
また、HTML5の仕様では、コメント内に含まれるNULLバイト(U+0000)は、Unicodeの置換文字(U+FFFD)に変換されるべきだと定められています。これは、NULLバイトが文字列の終端を示すために使われることがあり、セキュリティ上の問題やパースの不整合を引き起こす可能性があるためです。このコミットは、これらの仕様不適合を解消し、パーサーの堅牢性と標準への準拠を向上させることを目的としています。
前提知識の解説
HTML5パース仕様
HTML5のパース仕様は、WHATWGによって詳細に定義されており、ブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを厳密に規定しています。この仕様は、XMLとは異なり、非常に寛容なエラー処理ルールを持つことが特徴です。
外部コンテンツ (Foreign Content)
HTML5において「外部コンテンツ(Foreign Content)」とは、HTML名前空間以外の名前空間に属する要素を指します。最も一般的な例は、SVG (Scalable Vector Graphics) や MathML (Mathematical Markup Language) です。これらのコンテンツはXMLの構文規則に従うため、HTMLの通常のパースルールとは異なる特別な処理が必要になります。
CDATAセクション
CDATA (Character Data) セクションはXMLの構文の一部であり、<
や &
のような特殊文字を含むテキストブロックを、マークアップとしてではなく純粋な文字データとして扱いたい場合に使用されます。構文は <![CDATA[...]]>
です。XMLパーサーは、このセクション内の内容をそのまま文字データとして読み込み、内部のマークアップを無視します。HTMLでは通常CDATAセクションはサポートされませんが、外部コンテンツ(SVGやMathML)内では有効です。
NULLバイト (U+0000) と Unicode置換文字 (U+FFFD)
- NULLバイト (U+0000): ASCIIコードで0x00に相当する文字で、C言語などでは文字列の終端を示すために使われることがあります。HTMLやXMLの文脈では、NULLバイトは通常許可されず、パースエラーやセキュリティ脆弱性の原因となる可能性があります。
- Unicode置換文字 (U+FFFD): Unicodeの「Replacement Character」で、不正な文字エンコーディングや、文字セットで表現できない文字を検出した際に、その文字の代わりに表示される特殊な記号です()。HTML5の仕様では、パース中に不正なバイトシーケンスやNULLバイトが検出された場合、U+FFFDに変換することが求められています。
技術的詳細
このコミットは、exp/html
パッケージのトークナイザー(token.go
)とパーサー(parse.go
)に修正を加えています。
-
CDATAセクションのパース:
parse.go
のread()
メソッド内で、現在の要素が外部コンテンツ(n.Namespace != ""
)である場合にのみ、トークナイザーのcdataOK
フラグをtrue
に設定するように変更されました。これにより、CDATAセクションのパースが適切なコンテキストでのみ有効になります。token.go
にreadCDATA()
という新しいメソッドが追加されました。このメソッドは、<![CDATA[
シーケンスを検出し、その後の]]>
までをCDATAセクションの内容として読み込みます。readMarkupDeclaration()
メソッドが修正され、<!
の後に[CDATA[
が続く場合にreadCDATA()
を呼び出すようになりました。CDATAセクションが検出された場合、convertNUL
フラグがtrue
に設定され、TextToken
として返されます。
-
コメント内のNULLバイト変換:
token.go
のTokenizer
構造体にcdataOK
という新しいフィールドが追加されました。Text()
メソッド内のNULLバイト変換ロジックが変更されました。以前はz.convertNUL
がtrue
の場合にのみNULLバイトを置換していましたが、今回の変更でCommentToken
の場合もNULLバイトを置換するようになりました。これにより、コメント内のNULLバイトもU+FFFDに変換されるようになります。
これらの変更により、パーサーはHTML5の仕様に厳密に準拠し、外部コンテンツ内のCDATAセクションを正しく処理し、コメント内のNULLバイトを適切に置換できるようになりました。
コアとなるコードの変更箇所
src/pkg/exp/html/parse.go
--- a/src/pkg/exp/html/parse.go
+++ b/src/pkg/exp/html/parse.go
@@ -390,6 +390,10 @@ func (p *parser) reconstructActiveFormattingElements() {
// read reads the next token from the tokenizer.
func (p *parser) read() error {
+ // CDATA sections are allowed only in foreign content.
+ n := p.oe.top()
+ p.tokenizer.cdataOK = n != nil && n.Namespace != ""
+
p.tokenizer.Next()
p.tok = p.tokenizer.Token()
if p.tok.Type == ErrorToken {
src/pkg/exp/html/token.go
--- a/src/pkg/exp/html/token.go
+++ b/src/pkg/exp/html/token.go
@@ -155,6 +155,8 @@ type Tokenizer struct {
// convertNUL is whether NUL bytes in the current token's data should
// be converted into \ufffd replacement characters.
convertNUL bool
+ // cdataOK is whether CDATA sections are allowed in the current context.
+ cdataOK bool
}
// Err returns the error associated with the most recent ErrorToken token.
@@ -347,8 +349,8 @@ func (z *Tokenizer) readUntilCloseAngle() {
}
// readMarkupDeclaration reads the next token starting with "!". It might be
-// a "<!--comment-->", a "<!DOCTYPE foo>", or "<!a bogus comment". The opening
-// "<!" has already been consumed.
+// a "<!--comment-->", a "<!DOCTYPE foo>", a "<![CDATA[section]]>" or
+// "<!a bogus comment". The opening "<!" has already been consumed.
func (z *Tokenizer) readMarkupDeclaration() TokenType {
z.data.start = z.raw.end
var c [2]byte
@@ -364,27 +366,81 @@ func (z *Tokenizer) readMarkupDeclaration() TokenType {
return CommentToken
}
z.raw.end -= 2
+ if z.readDoctype() {
+ return DoctypeToken
+ }
+ if z.cdataOK && z.readCDATA() {
+ z.convertNUL = true
+ return TextToken
+ }
+ // It's a bogus comment.
+ z.readUntilCloseAngle()
+ return CommentToken
+}
+
+// readDoctype attempts to read a doctype declaration and returns true if
+// successful. The opening "<!" has already been consumed.
+func (z *Tokenizer) readDoctype() bool {
const s = "DOCTYPE"
for i := 0; i < len(s); i++ {
c := z.readByte()
if z.err != nil {
z.data.end = z.raw.end
- return CommentToken
+ return false
}
if c != s[i] && c != s[i]+('a'-'A') {
// Back up to read the fragment of "DOCTYPE" again.
z.raw.end = z.data.start
- z.readUntilCloseAngle()
- return CommentToken
+ return false
}
}
if z.skipWhiteSpace(); z.err != nil {
z.data.start = z.raw.end
z.data.end = z.raw.end
- return DoctypeToken
+ return true
}
z.readUntilCloseAngle()
- return DoctypeToken
+ return true
+}
+
+// readCDATA attempts to read a CDATA section and returns true if
+// successful. The opening "<!" has already been consumed.
+func (z *Tokenizer) readCDATA() bool {
+ const s = "[CDATA["
+ for i := 0; i < len(s); i++ {
+ c := z.readByte()
+ if z.err != nil {
+ z.data.end = z.raw.end
+ return false
+ }
+ if c != s[i] {
+ // Back up to read the fragment of "[CDATA[" again.
+ z.raw.end = z.data.start
+ return false
+ }
+ }
+ z.data.start = z.raw.end
+ brackets := 0
+ for {
+ c := z.readByte()
+ if z.err != nil {
+ z.data.end = z.raw.end
+ return true
+ }
+ switch c {
+ case ']':
+ brackets++
+ case '>':
+ if brackets >= 2 {
+ z.data.end = z.raw.end - len("]]>")
+ return true
+ }
+ brackets = 0
+ default:
+ brackets = 0
+ }
+ }
+ panic("unreachable")
}
// startTagIn returns whether the start tag in z.buf[z.data.start:z.data.end]
@@ -751,7 +807,7 @@ func (z *Tokenizer) Text() []byte {
z.data.start = z.raw.end
z.data.end = z.raw.end
s = convertNewlines(s)
- if z.convertNUL && bytes.Contains(s, nul) {
+ if (z.convertNUL || z.tt == CommentToken) && bytes.Contains(s, nul) {
s = bytes.Replace(s, nul, replacement, -1)
}
if !z.textIsRaw {
テストログの変更
src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
と src/pkg/exp/html/testlogs/tests21.dat.log
の両方で、以前はFAIL
となっていたCDATAセクション関連のテストケースがPASS
に変わっています。これは、変更が正しく機能し、パーサーがこれらのケースを適切に処理できるようになったことを示しています。特にtests21.dat.log
では、SVG内のCDATAセクションに関する多数のテストがパスに変わっています。
コアとなるコードの解説
parse.go
の変更点
parse.go
のread()
関数は、トークナイザーから次のトークンを読み込む前に、現在のパースコンテキストに基づいてtokenizer.cdataOK
フラグを設定します。
n := p.oe.top()
は、現在開いている要素スタックの最上位要素を取得します。
p.tokenizer.cdataOK = n != nil && n.Namespace != ""
の行は、現在の要素が存在し、かつその要素がHTML名前空間以外の名前空間(つまり外部コンテンツ)に属している場合にのみ、CDATAセクションのパースを許可するようにcdataOK
フラグをtrue
に設定します。これにより、HTMLの通常のコンテキストでCDATAセクションが誤ってパースされるのを防ぎ、HTML5の仕様に準拠します。
token.go
の変更点
-
Tokenizer
構造体へのcdataOK
フィールド追加:cdataOK bool
は、現在のパースコンテキストでCDATAセクションが許可されているかどうかを示すフラグです。これはparse.go
から設定されます。 -
readMarkupDeclaration()
関数の修正: この関数は、<!
で始まるマークアップ宣言を読み込む際に呼び出されます。 新しいロジックでは、まずreadDoctype()
を試み、次にz.cdataOK
がtrue
であり、かつreadCDATA()
が成功した場合にTextToken
を返します。z.convertNUL = true
が設定されるのは、CDATAセクション内のNULLバイトも置換対象とするためです。 これにより、<!
の後にDOCTYPE
や[CDATA[
が続くパターンを適切に識別し、それ以外のパターンは「不正なコメント(bogus comment)」として処理されます。 -
readCDATA()
関数の追加: この新しい関数は、<!
の後に[CDATA[
が続くシーケンスを読み込み、CDATAセクションの開始を検出します。 その後、]]>
シーケンスが見つかるまで文字を読み込み、その間の内容をCDATAセクションのデータとして扱います。brackets
変数は、]
文字の連続をカウントし、]]>
の終端シーケンスを検出するために使用されます。 -
Text()
関数のNULLバイト変換ロジックの修正:if (z.convertNUL || z.tt == CommentToken) && bytes.Contains(s, nul)
の行が変更されました。 以前はz.convertNUL
がtrue
の場合にのみNULLバイトを置換していましたが、z.tt == CommentToken
という条件が追加されました。これは、トークンのタイプがコメントである場合にも、NULLバイトをU+FFFDに変換することを意味します。これにより、HTML5のコメント内のNULLバイト処理に関する仕様に準拠します。
これらの変更は、HTML5のパース仕様、特に外部コンテンツとNULLバイトの処理に関する厳密な要件を満たすために不可欠です。
関連リンク
- HTML Living Standard - 13.2.5.1 "Foreign content" in the HTML syntax
- HTML Living Standard - 13.2.5.10 "CDATA sections"
- HTML Living Standard - 13.2.5.1 "The rules for parsing comments" (NULLバイトの処理に関する記述が含まれる)
- Unicode Character 'REPLACEMENT CHARACTER' (U+FFFD)
参考にした情報源リンク
- Go exp/html package documentation (GoDoc) (当時のドキュメントは現在のものと異なる可能性がありますが、パッケージの目的を理解するのに役立ちます)
- WHATWG HTML Living Standard (HTML5の最新仕様)
- XML 1.0 Specification - CDATA Sections
- Wikipedia: Unicode replacement character