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

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

このコミットは、Go言語の標準ライブラリである encoding/xml パッケージ内のXMLデコーダの挙動を修正するものです。具体的には、XMLドキュメントを非厳格モードでパースする際に、未知のエンティティ参照(例: &unknown;)のデコード処理が正しく行われるように変更が加えられています。

変更されたファイルは以下の2つです。

  • src/pkg/encoding/xml/xml.go: XMLデコーダの主要なロジックが含まれるファイルです。未知のエンティティの処理に関する修正がここで行われています。
  • src/pkg/encoding/xml/xml_test.go: encoding/xml パッケージのテストファイルです。今回の修正によって導入された新しいテストケースが含まれており、非厳格モードでの未知のエンティティのデコードが期待通りに行われることを検証しています。

コミット

commit fdc45367f97b9cea81b3f8c045b426d6e4a11766
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date:   Thu May 17 00:04:00 2012 -0300

    encoding/xml: fix decoding of unknown entities in non-strict mode
    
    Fixes #3447.
    
    R=rsc, gustavo
    CC=golang-dev
    https://golang.org/cl/6039045

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

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

元コミット内容

encoding/xml: fix decoding of unknown entities in non-strict mode

このコミットは、非厳格モードにおける未知のエンティティのデコードを修正します。

Fixes #3447.

変更の背景

この変更は、Go言語の encoding/xml パッケージがXMLドキュメントをパースする際に、未知のエンティティ参照(例: &unknown;)を非厳格モードでどのように扱うかという問題に対処しています。

XMLの仕様では、&lt; (<), &gt; (>), &amp; (&), &apos; ('), &quot; (") の5つの事前定義されたエンティティ参照以外は、DTD(Document Type Definition)で宣言されているか、数値文字参照(例: &#123;)である必要があります。しかし、現実世界のXMLデータには、DTDで宣言されていないカスタムエンティティや、誤って記述されたエンティティ参照が含まれることがあります。

encoding/xml パッケージには、厳格モード(Strict mode)と非厳格モード(Non-strict mode)があります。

  • 厳格モード: XML仕様に厳密に従い、未知のエンティティ参照や不正なXML構造をエラーとして扱います。
  • 非厳格モード: 多少のXML仕様違反を許容し、可能な限りパースを続行しようとします。これは、特にウェブスクレイピングや、厳密なXMLではないがXMLライクなデータを扱う場合に有用です。

このコミット以前の非厳格モードでは、未知のエンティティ参照に遭遇した場合、そのエンティティがセミコロンで終わっていない場合に、デコーダが誤ったエラーを報告したり、期待しない挙動を示したりするバグが存在していました。具体的には、エンティティ参照が途中で途切れている場合(例: &entity)や、不正な文字が含まれている場合(例: &#zzz;)に、非厳格モードであってもエラーとして処理されてしまうことがありました。

この修正は、非厳格モードの意図、つまり「可能な限りパースを続行し、不正なエンティティ参照はそのままの形で出力する」という挙動を保証するために行われました。これにより、より堅牢なXMLパースが可能になります。

前提知識の解説

XMLエンティティ参照

XMLエンティティ参照は、特定の文字や文字列をXMLドキュメント内で表現するためのメカニズムです。これらは & で始まり、; で終わる形式を取ります。

  • 事前定義エンティティ: XML仕様で定義されている5つのエンティティです。
    • &lt; : < (小なり記号)
    • &gt; : > (大なり記号)
    • &amp; : & (アンパサンド)
    • &apos; : ' (アポストロフィ)
    • &quot; : " (引用符)
  • 数値文字参照: Unicode文字をその数値コードで参照する方法です。
    • &#DDDD; : 10進数で指定
    • &#xHHHH; : 16進数で指定 例: &#123;{ を表します。
  • 一般エンティティ: DTDで宣言されるカスタムエンティティです。例: <!ENTITY copyright "Copyright &#xA9; 2023."> と宣言されていれば、ドキュメント内で &copyright; と記述できます。

encoding/xml パッケージのデコード処理

Go言語の encoding/xml パッケージは、XMLドキュメントをGoのデータ構造にデコードするための機能を提供します。内部的には、XMLストリームをトークン(要素の開始/終了、文字データ、コメントなど)に分割するデコーダ(Decoder)が動作しています。

デコーダは、XMLドキュメントを読み込みながら、エンティティ参照に遭遇するとそれを解決しようとします。

  • 厳格モード (d.Strict = true): XML仕様に厳密に従い、未知のエンティティ参照や不正な形式のエンティティ参照はエラーとして処理されます。
  • 非厳格モード (d.Strict = false): 厳格なXML仕様に準拠しないドキュメントも処理できるように、エラーを抑制し、可能な限りパースを続行します。このモードでは、未知のエンティティ参照は、解決せずにそのままの文字列として文字データの一部として扱われることが期待されます。例えば、<tag>&unknown;</tag>unknown という文字列を含む CharData トークンとして扱われるべきです。

syntaxError

syntaxError は、XMLパース中に構文エラーが発生した場合に Decoder が返すエラーです。このエラーは、XMLドキュメントがXML仕様に準拠していないことを示します。非厳格モードでは、可能な限り syntaxError を回避し、不正な部分を文字データとして扱うことが望ましいです。

技術的詳細

このコミットの技術的な核心は、encoding/xml/xml.go 内の Decoder がエンティティ参照を処理するロジック、特に非厳格モードでの挙動の改善にあります。

変更前は、エンティティ参照のパース中に、セミコロンが見つからなかった場合や、エンティティ名が長すぎる場合に、非厳格モードであってもエラーとして扱われる可能性がありました。これは、非厳格モードの目的(可能な限りパースを続行する)に反する挙動でした。

今回の修正では、以下の点が改善されています。

  1. セミコロンの有無の正確な判定: エンティティ参照のパース中に、セミコロン (';') が見つかったかどうかを semicolon という新しいブール変数で追跡するようになりました。これにより、エンティティ参照がセミコロンで終わっているかどうかを正確に判断できます。

  2. 有効なエンティティ名の判定: エンティティ名が少なくとも1文字以上で構成されているかを valid という新しいブール変数で判定するようになりました。これにより、&; のようにエンティティ名が空の場合を適切に処理できます。

  3. 非厳格モードでのエラー処理の改善:

    • !valid (エンティティ名が空または不正) かつ !d.Strict (非厳格モード) の場合、以前はエラーとして扱われる可能性がありましたが、修正後は & とその後に続く文字(セミコロンを含む)をそのまま d.buf に書き込み、Input ループを続行するようになりました。これにより、不正なエンティティ参照が文字データとして扱われます。
    • 特に、d.buf.WriteByte(';') が追加され、非厳格モードで未知のエンティティをそのまま出力する際に、セミコロンも正しく出力されるようになりました。
  4. 厳格モードでのエラーメッセージの改善: 厳格モード (d.Strict) で不正なエンティティ参照に遭遇した場合のエラーメッセージがより詳細になりました。

    • セミコロンがない場合 (!semicolon) は、エラーメッセージに (no semicolon) が追加されます。
    • エンティティ名が途中で途切れている場合 (i < len(d.tmp)) と、エンティティ名が長すぎる場合 (else) で、異なるエラーメッセージが生成されるようになりました。これにより、デバッグが容易になります。
  5. テストケースの追加: src/pkg/encoding/xml/xml_test.goTestNonStrictRawToken という新しいテスト関数が追加されました。このテストは、非厳格モードで様々な形式の未知のエンティティ参照(例: &entity, &unknown;entity, &#123, &#zzz;)を含むXMLをパースし、それらが期待通りに CharData として扱われることを検証します。これにより、修正が正しく機能していることが保証されます。

これらの変更により、encoding/xml パッケージは、非厳格モードにおいて、より堅牢かつ期待通りのXMLパース挙動を提供するようになりました。

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

src/pkg/encoding/xml/xml.goInput ラベルが付いたループ内のエンティティ処理部分が主な変更箇所です。

--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -850,6 +850,8 @@ Input:
 			// Parsers are required to recognize lt, gt, amp, apos, and quot
 			// even if they have not been declared.  That's all we allow.
 			var i int
+			var semicolon bool
+			var valid bool
 			for i = 0; i < len(d.tmp); i++ {
 				var ok bool
 				d.tmp[i], ok = d.getc()
@@ -861,6 +863,8 @@ Input:
 				}
 				c := d.tmp[i]
 				if c == ';' {
+					semicolon = true
+					valid = i > 0
 					break
 				}
 				if 'a' <= c && c <= 'z' ||
@@ -873,14 +877,25 @@ Input:
 				break
 			}
 			s := string(d.tmp[0:i])
-			if i >= len(d.tmp) {
+			if !valid {
 				if !d.Strict {
 					b0, b1 = 0, 0
 					d.buf.WriteByte('&')
 					d.buf.Write(d.tmp[0:i])
+					if semicolon {
+						d.buf.WriteByte(';')
+					}
 					continue Input
 				}
-				d.err = d.syntaxError("character entity expression &" + s + "... too long")
+				semi := ";"
+				if !semicolon {
+					semi = " (no semicolon)"
+				}
+				if i < len(d.tmp) {
+					d.err = d.syntaxError("invalid character entity &" + s + semi)
+				} else {
+					d.err = d.syntaxError("invalid character entity &" + s + "... too long")
+				}
 				return nil
 			}
 			var haveText bool
@@ -910,6 +925,7 @@ Input:
 				b0, b1 = 0, 0
 				d.buf.WriteByte('&')
 				d.buf.Write(d.tmp[0:i])
+				d.buf.WriteByte(';')
 				continue Input
 			}
 			d.err = d.syntaxError("invalid character entity &" + s + ";")

また、src/pkg/encoding/xml/xml_test.go に以下のテストが追加されています。

--- a/src/pkg/encoding/xml/xml_test.go
+++ b/src/pkg/encoding/xml/xml_test.go
@@ -5,6 +5,7 @@
 package xml
 
 import (
+	"fmt"
 	"io"
 	"reflect"
 	"strings"
@@ -158,6 +159,39 @@ func TestRawToken(t *testing.T) {
 	testRawToken(t, d, rawTokens)
 }
 
+const nonStrictInput = `
+<tag>non&entity</tag>
+<tag>&unknown;entity</tag>
+<tag>&#123</tag>
+<tag>&#zzz;</tag>
+`
+
+var nonStrictTokens = []Token{
+	CharData("\n"),
+	StartElement{Name{"", "tag"}, []Attr{}},
+	CharData("non&entity"),
+	EndElement{Name{"", "tag"}},
+	CharData("\n"),
+	StartElement{Name{"", "tag"}, []Attr{}},
+	CharData("&unknown;entity"),
+	EndElement{Name{"", "tag"}},
+	CharData("\n"),
+	StartElement{Name{"", "tag"}, []Attr{}},
+	CharData("&#123"),
+	EndElement{Name{"", "tag"}},
+	CharData("\n"),
+	StartElement{Name{"", "tag"}, []Attr{}},
+	CharData("&#zzz;"),
+	EndElement{Name{"", "tag"}},
+	CharData("\n"),
+}
+
+func TestNonStrictRawToken(t *testing.T) {
+	d := NewDecoder(strings.NewReader(nonStrictInput))
+	d.Strict = false
+	testRawToken(t, d, nonStrictTokens)
+}
+
 type downCaser struct {
 	t *testing.T
 	r io.ByteReader
@@ -219,7 +253,18 @@ func testRawToken(t *testing.T, d *Decoder, rawTokens []Token) {
 			t.Fatalf("token %d: unexpected error: %s", i, err)
 		}
 		if !reflect.DeepEqual(have, want) {
-			t.Errorf("token %d = %#v want %#v", i, have, want)
+			var shave, swant string
+			if _, ok := have.(CharData); ok {
+				shave = fmt.Sprintf("CharData(%q)", have)
+			} else {
+				shave = fmt.Sprintf("%#v", have)
+			}
+			if _, ok := want.(CharData); ok {
+				swant = fmt.Sprintf("CharData(%q)", want)
+			} else {
+				swant = fmt.Sprintf("%#v", want)
+			}
+			t.Errorf("token %d = %s, want %s", i, shave, swant)
 		}
 	}
 }
@@ -531,8 +576,8 @@ var characterTests = []struct {
 	{"\xef\xbf\xbe<doc/>", "illegal character code U+FFFE"},
 	{"<?xml version=\"1.0\"?><doc>\r\n<hiya/>\x07<toots/></doc>", "illegal character code U+0007"},
 	{"<?xml version=\"1.0\"?><doc \x12='value'>what's up</doc>", "expected attribute name in element"},
-	{"<doc>&\x01;</doc>", "invalid character entity &;"},
-	{"<doc>&\xef\xbf\xbe;</doc>", "invalid character entity &;"},
+	{"<doc>&\x01;</doc>", "invalid character entity & (no semicolon)"},
+	{"<doc>&\xef\xbf\xbe;</doc>", "invalid character entity & (no semicolon)"},
 }
 
 func TestDisallowedCharacters(t *testing.T) {

コアとなるコードの解説

src/pkg/encoding/xml/xml.go の変更点

  1. 新しい変数の導入:

    • var semicolon bool: エンティティ参照のパース中にセミコロンが見つかったかどうかを追跡します。
    • var valid bool: エンティティ名が有効(少なくとも1文字以上)であるかどうかを追跡します。
  2. セミコロンと有効性の判定ロジック: エンティティ参照の文字を読み込むループ内で、文字がセミコロン (';') であった場合に semicolon = truevalid = i > 0 を設定し、ループを抜けます。i > 0 は、エンティティ名が空でないことを意味します(例: &; のような不正な形式を除外)。

  3. 非厳格モードでの未知エンティティの処理改善: 変更前は if i >= len(d.tmp) という条件でエンティティ名が長すぎる場合をチェックしていましたが、これを if !valid に変更しました。

    • if !d.Strict (非厳格モードの場合):
      • b0, b1 = 0, 0: バッファをリセットします。
      • d.buf.WriteByte('&'): アンパサンド (&) をバッファに書き込みます。
      • d.buf.Write(d.tmp[0:i]): パースされたエンティティ名(またはその一部)をバッファに書き込みます。
      • if semicolon { d.buf.WriteByte(';') }: ここが重要な変更点です。 セミコロンが見つかっていた場合、そのセミコロンもバッファに書き込みます。これにより、&unknown; のような未知のエンティティが、非厳格モードで &unknown; という文字データとして正しく出力されるようになります。
      • continue Input: 現在のエンティティ処理をスキップし、次の入力のパースを続行します。これにより、エラーを発生させずにパースが継続されます。
  4. 厳格モードでのエラーメッセージの改善:

    • semi := ";"if !semicolon { semi = " (no semicolon)" } により、エラーメッセージにセミコロンの有無が明示されるようになりました。
    • if i < len(d.tmp)else の分岐により、エンティティ名が途中で途切れている場合と、エンティティ名が長すぎる場合で、より具体的なエラーメッセージが生成されるようになりました。これにより、厳格モードでのデバッグが容易になります。
  5. 既存の非厳格モード処理の修正: case TokenCharData: のブロック内で、未知のエンティティを処理する部分にも d.buf.WriteByte(';') が追加されました。これにより、非厳格モードで未知のエンティティが文字データとして扱われる際に、セミコロンが常に含まれるようになります。

src/pkg/encoding/xml/xml_test.go の変更点

  1. nonStrictInput 定数の追加: 非厳格モードでのテストに使用するXML文字列が定義されています。これには、non&entity (セミコロンなしの不正なエンティティ)、&unknown;entity (未知のエンティティ)、&#123 (セミコロンなしの数値文字参照)、&#zzz; (不正な数値文字参照) など、様々なケースが含まれています。

  2. nonStrictTokens 変数の追加: nonStrictInput を非厳格モードでパースした場合に期待されるトークンのシーケンスが定義されています。注目すべきは、不正なエンティティ参照が CharData トークンとしてそのまま含まれている点です。例えば、&unknown;entityCharData("&unknown;entity") となります。

  3. TestNonStrictRawToken 関数の追加:

    • NewDecoder を作成し、d.Strict = false を設定して非厳格モードを有効にします。
    • testRawToken ヘルパー関数を呼び出し、nonStrictInputnonStrictTokens を渡して、非厳格モードでのパース結果が期待通りであることを検証します。
  4. testRawToken 関数のエラーメッセージ改善: reflect.DeepEqual でトークンが一致しない場合のエラーメッセージが改善されました。特に CharData トークンについては、fmt.Sprintf("CharData(%q)", have) のように引用符で囲んで表示することで、文字列の内容がより明確にわかるようになりました。

  5. characterTests のエラーメッセージ修正: 既存の characterTests の中で、不正な文字エンティティに関するテストケースのエラーメッセージが、新しい厳格モードのエラーメッセージ形式に合わせて "invalid character entity & (no semicolon)" に変更されました。

これらの変更により、encoding/xml パッケージは、非厳格モードでの未知のエンティティ処理において、より堅牢で予測可能な挙動を提供するようになりました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (git diff)
  • Go言語の encoding/xml パッケージのドキュメントとソースコードに関する一般的な知識
  • XMLのエンティティ参照に関する一般的な知識