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

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

このコミットは、Go言語のencoding/xmlパッケージにおけるXMLテキストのエスケープ処理に関するバグ修正です。具体的には、無効なUTF-8バイトがXML出力にそのまま渡されてしまう問題を解決し、XMLの仕様に準拠した正しいエスケープが行われるように変更されています。

コミット

commit 789e1c351e6fca96fe3cec467029732c03d55d64
Author: Alex A Skinner <alex@lx.lc>
Date:   Tue Jul 30 14:11:47 2013 +1000

    encoding/xml: Do not pass through invalid utf8 bytes
    
    EscapeText now escapes 0xFFFD returned from DecodeRune as 0xFFFD, rather than passing through the original byte.
    Fixes #5880.
    
    R=golang-dev, r, bradfitz, adg
    CC=golang-dev
    https://golang.org/cl/11975043

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

https://github.com/golang/go/commit/789e1c351e6fca96fe3cec467029732c03d55d64

元コミット内容

encoding/xml: 無効なUTF8バイトをそのまま渡さない

EscapeTextは、DecodeRuneから返される0xFFFDを、元のバイトをそのまま渡すのではなく、0xFFFDとしてエスケープするようになりました。 Fixes #5880.

変更の背景

このコミットは、Go言語のencoding/xmlパッケージが、入力されたバイト列が無効なUTF-8シーケンスを含んでいた場合に、その無効なバイトをXML出力にそのまま含めてしまうという問題(Issue 5880)を修正するために行われました。

XMLの仕様では、特定の文字範囲(Unicodeの「文字」として定義される範囲)のみが許可されており、無効なバイトシーケンスや制御文字の一部はXMLドキュメント内に直接含めることができません。Goのunicode/utf8パッケージのDecodeRune関数は、無効なUTF-8シーケンスを検出した場合に、Unicodeの「Replacement Character」(U+FFFD)と、その無効なシーケンスを構成するバイト数(通常は1)を返します。

しかし、このコミット以前のencoding/xmlパッケージのEscapeText関数は、DecodeRune0xFFFDを返した場合でも、それが元の無効なバイトから変換されたものであることを適切に認識せず、その0xFFFDが単一のバイト(width == 1)として扱われる場合に、元の無効なバイトをそのままXML出力に含めてしまう可能性がありました。これにより、生成されるXMLドキュメントがXML仕様に違反し、他のXMLパーサーで正しく処理できないという問題が発生していました。

この問題は、特にバイナリデータや、文字エンコーディングが不適切な外部ソースからのデータをXMLに埋め込む際に顕在化します。XMLパーサーは、XMLドキュメントが整形式(well-formed)であることを要求するため、無効な文字が含まれているとパースエラーを引き起こします。

前提知識の解説

  • XML (Extensible Markup Language): 構造化されたデータを表現するためのマークアップ言語です。XMLドキュメントは、特定の文字セット(通常はUnicode)と文字範囲に準拠する必要があります。無効な文字やバイトシーケンスが含まれていると、XMLパーサーはエラーを報告します。
  • UTF-8: Unicode文字をエンコードするための可変長文字エンコーディングです。ASCII互換性があり、世界中のほとんどの文字を表現できます。UTF-8では、特定のバイトシーケンスが無効と見なされます(例: 継続バイトが先行バイトなしで出現する場合など)。
  • Unicode Replacement Character (U+FFFD): 無効な、または表現できない文字シーケンスを置き換えるために使用される特殊なUnicode文字です。UTF-8デコーダは、入力ストリームで無効なバイトシーケンスを検出すると、通常この文字を返します。
  • unicode/utf8.DecodeRune: Go言語の標準ライブラリunicode/utf8パッケージに含まれる関数で、バイトスライスから次のUTF-8エンコードされたルーン(Unicodeコードポイント)をデコードします。この関数は、デコードされたルーンと、そのルーンを構成するバイト数を返します。入力が無効なUTF-8シーケンスの場合、DecodeRuneU+FFFDと、その無効なシーケンスを構成するバイト数(通常は1)を返します。
  • encoding/xmlパッケージ: Go言語の標準ライブラリで、XMLドキュメントのエンコード(書き込み)とデコード(読み込み)を提供します。このパッケージは、Goの構造体とXML要素間のマッピングを容易にします。
  • EscapeText関数: encoding/xmlパッケージ内の関数で、XMLドキュメントに書き込むテキストコンテンツをエスケープする役割を担います。XMLの予約文字(<, >, &, ', ")や、XMLで許可されていない文字(制御文字など)を適切なエンティティ参照(例: <&lt;に)に変換します。

技術的詳細

このコミットの核心は、encoding/xmlパッケージのEscapeText関数における文字範囲チェックのロジックの変更です。

変更前のコードでは、EscapeText関数は各ルーン(Unicodeコードポイント)を処理し、isInCharacterRange(r)というヘルパー関数を使って、そのルーンがXMLで許可される文字範囲内にあるかどうかをチェックしていました。もしルーンが許可範囲外であれば、esc_fffd&#xFFFD;)としてエスケープされることになっていました。

しかし、問題はunicode/utf8.DecodeRuneが無効なUTF-8バイトシーケンスを検出した際に0xFFFDを返す点にありました。この0xFFFD自体はisInCharacterRange関数によって「有効な文字」と判断されてしまうため、EscapeText0xFFFDをエスケープせずにそのまま出力しようとしました。さらに、この0xFFFDが元の無効なバイトから生成されたもので、かつその無効なバイトが単一のバイトとして扱われた場合(width == 1)、元の無効なバイトがXML出力にそのまま含まれてしまうという挙動になっていました。これは、XMLの仕様に違反するものでした。

このコミットでは、EscapeText関数の条件式が以下のように変更されました。

変更前:

if !isInCharacterRange(r) {
    esc = esc_fffd
    break
}

変更後:

if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
    esc = esc_fffd
    break
}

この変更により、以下の条件のいずれかが満たされた場合に、ルーンが&#xFFFD;としてエスケープされるようになりました。

  1. !isInCharacterRange(r): ルーンがXMLで許可される文字範囲外である場合(これは以前から存在した条件)。
  2. (r == 0xFFFD && width == 1): ルーンが0xFFFD(Replacement Character)であり、かつその0xFFFDが単一のバイトからデコードされたものである場合。

後者の条件が追加されたことで、unicode/utf8.DecodeRuneが無効な単一バイトを処理して0xFFFDを返した場合に、その0xFFFDがXML出力にそのまま含まれるのではなく、明示的に&#xFFFD;というXMLエンティティとしてエスケープされるようになりました。これにより、生成されるXMLドキュメントは常に整形式となり、無効なバイトが直接XMLに混入することを防ぎます。

テストケースTestIssue5880では、無効なUTF-8バイトシーケンス({192, 168, 0, 1}は、UTF-8の仕様では無効なバイトシーケンスです。特に192は2バイトシーケンスの開始バイトですが、それに続くバイトが不正です)を含むバイトスライスをxml.Marshalに渡し、その結果が有効なUTF-8であるかどうかをutf8.Validで検証しています。このテストが追加されたことで、将来的に同様の問題が再発しないように保証されます。

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

src/pkg/encoding/xml/xml.goEscapeText 関数内の条件式が変更されました。

--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -1758,7 +1758,7 @@ func EscapeText(w io.Writer, s []byte) error {
 		case '\r':
 			esc = esc_cr
 		default:
-			if !isInCharacterRange(r) {
+			if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
 				esc = esc_fffd
 				break
 			}

また、src/pkg/encoding/xml/xml_test.go に新しいテストケース TestIssue5880 が追加されました。

--- a/src/pkg/encoding/xml/xml_test.go
+++ b/src/pkg/encoding/xml/xml_test.go
@@ -11,6 +11,7 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"unicode/utf8"
 )
 
 const testInput = `
@@ -714,3 +715,14 @@ func TestEscapeTextInvalidChar(t *testing.T) {
 		t.Errorf("have %v, want %v", text, expected)
 	}
 }
+
+func TestIssue5880(t *testing.T) {
+	type T []byte
+	data, err := Marshal(T{192, 168, 0, 1})
+	if err != nil {
+		t.Errorf("Marshal error: %v", err)
+	}
+	if !utf8.Valid(data) {
+		t.Errorf("Marshal generated invalid UTF-8: %x", data)
+	}
+}

コアとなるコードの解説

xml.goの変更は、EscapeText関数がXMLテキストをエスケープする際のロジックを強化しています。 EscapeText関数は、入力バイトスライスsをルーンごとに処理し、XMLの仕様に準拠するように特殊文字をエスケープします。 変更された行は、defaultケース内の条件分岐です。

if !isInCharacterRange(r) || (r == 0xFFFD && width == 1)

この条件式は、以下のいずれかの状況でesc_fffd&#xFFFD;)をエスケープシーケンスとして設定します。

  1. !isInCharacterRange(r): 現在処理しているルーンrが、XMLの文字として許可されていない範囲にある場合。これは、XML 1.0の仕様で許可されている文字(#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF])以外の文字を指します。
  2. (r == 0xFFFD && width == 1): 現在処理しているルーンrがUnicodeのReplacement Character(U+FFFD)であり、かつそのルーンが入力バイトスライスから1バイトとしてデコードされた場合。これは、unicode/utf8.DecodeRuneが無効な単一バイトを検出した際に0xFFFDwidth=1を返す挙動に対応しています。この条件を追加することで、無効なバイトが0xFFFDとしてデコードされたとしても、それがXMLに直接含まれるのではなく、XMLエンティティとして適切にエスケープされるようになります。

この修正により、encoding/xmlパッケージは、無効なUTF-8バイトを含む入力に対しても、常に整形式のXMLを生成するようになります。

xml_test.goに追加されたTestIssue5880は、この修正が正しく機能することを検証するためのものです。 このテストでは、{192, 168, 0, 1}という無効なUTF-8バイトシーケンスを含むバイトスライスを定義し、これをxml.Marshal関数に渡してXMLにエンコードします。 その後、utf8.Valid(data)を使って、Marshalによって生成されたXML出力が有効なUTF-8であるかどうかをチェックします。 もし修正がなければ、Marshalは無効なバイトをそのまま出力に含めてしまい、utf8.Validfalseを返してテストが失敗します。修正が適用されたことで、無効なバイトは&#xFFFD;としてエスケープされ、結果として生成されるXMLは有効なUTF-8となるため、テストは成功します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのコミット履歴とIssueトラッカー
  • Unicode標準
  • XML仕様 (W3C勧告)
  • UTF-8エンコーディングに関する一般的な情報源
  • unicode/utf8パッケージのドキュメント
  • encoding/xmlパッケージのドキュメント
  • Go言語のテストに関するドキュメント# [インデックス 16920] ファイルの概要

このコミットは、Go言語のencoding/xmlパッケージにおけるXMLテキストのエスケープ処理に関するバグ修正です。具体的には、無効なUTF-8バイトがXML出力にそのまま渡されてしまう問題を解決し、XMLの仕様に準拠した正しいエスケープが行われるように変更されています。

コミット

commit 789e1c351e6fca96fe3cec467029732c03d55d64
Author: Alex A Skinner <alex@lx.lc>
Date:   Tue Jul 30 14:11:47 2013 +1000

    encoding/xml: Do not pass through invalid utf8 bytes
    
    EscapeText now escapes 0xFFFD returned from DecodeRune as 0xFFFD, rather than passing through the original byte.
    Fixes #5880.
    
    R=golang-dev, r, bradfitz, adg
    CC=golang-dev
    https://golang.org/cl/11975043

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

https://github.com/golang/go/commit/789e1c351e6fca96fe3cec467029732c03d55d64

元コミット内容

encoding/xml: 無効なUTF8バイトをそのまま渡さない

EscapeTextは、DecodeRuneから返される0xFFFDを、元のバイトをそのまま渡すのではなく、0xFFFDとしてエスケープするようになりました。 Fixes #5880.

変更の背景

このコミットは、Go言語のencoding/xmlパッケージが、入力されたバイト列が無効なUTF-8シーケンスを含んでいた場合に、その無効なバイトをXML出力にそのまま含めてしまうという問題(Issue 5880)を修正するために行われました。

XMLの仕様では、特定の文字範囲(Unicodeの「文字」として定義される範囲)のみが許可されており、無効なバイトシーケンスや制御文字の一部はXMLドキュメント内に直接含めることができません。Goのunicode/utf8パッケージのDecodeRune関数は、無効なUTF-8シーケンスを検出した場合に、Unicodeの「Replacement Character」(U+FFFD)と、その無効なシーケンスを構成するバイト数(通常は1)を返します。

しかし、このコミット以前のencoding/xmlパッケージのEscapeText関数は、DecodeRune0xFFFDを返した場合でも、それが元の無効なバイトから変換されたものであることを適切に認識せず、その0xFFFDが単一のバイト(width == 1)として扱われる場合に、元の無効なバイトをそのままXML出力に含めてしまう可能性がありました。これにより、生成されるXMLドキュメントがXML仕様に違反し、他のXMLパーサーで正しく処理できないという問題が発生していました。

この問題は、特にバイナリデータや、文字エンコーディングが不適切な外部ソースからのデータをXMLに埋め込む際に顕在化します。XMLパーサーは、XMLドキュメントが整形式(well-formed)であることを要求するため、無効な文字が含まれているとパースエラーを引き起こします。

前提知識の解説

  • XML (Extensible Markup Language): 構造化されたデータを表現するためのマークアップ言語です。XMLドキュメントは、特定の文字セット(通常はUnicode)と文字範囲に準拠する必要があります。無効な文字やバイトシーケンスが含まれていると、XMLパーサーはエラーを報告します。
  • UTF-8: Unicode文字をエンコードするための可変長文字エンコーディングです。ASCII互換性があり、世界中のほとんどの文字を表現できます。UTF-8では、特定のバイトシーケンスが無効と見なされます(例: 継続バイトが先行バイトなしで出現する場合など)。
  • Unicode Replacement Character (U+FFFD): 無効な、または表現できない文字シーケンスを置き換えるために使用される特殊なUnicode文字です。UTF-8デコーダは、入力ストリームで無効なバイトシーケンスを検出すると、通常この文字を返します。
  • unicode/utf8.DecodeRune: Go言語の標準ライブラリunicode/utf8パッケージに含まれる関数で、バイトスライスから次のUTF-8エンコードされたルーン(Unicodeコードポイント)をデコードします。この関数は、デコードされたルーンと、そのルーンを構成するバイト数を返します。入力が無効なUTF-8シーケンスの場合、DecodeRuneU+FFFDと、その無効なシーケンスを構成するバイト数(通常は1)を返します。
  • encoding/xmlパッケージ: Go言語の標準ライブラリで、XMLドキュメントのエンコード(書き込み)とデコード(読み込み)を提供します。このパッケージは、Goの構造体とXML要素間のマッピングを容易にします。
  • EscapeText関数: encoding/xmlパッケージ内の関数で、XMLドキュメントに書き込むテキストコンテンツをエスケープする役割を担います。XMLの予約文字(<, >, &, ', ")や、XMLで許可されていない文字(制御文字など)を適切なエンティティ参照(例: <&lt;に)に変換します。

技術的詳細

このコミットの核心は、encoding/xmlパッケージのEscapeText関数における文字範囲チェックのロジックの変更です。

変更前のコードでは、EscapeText関数は各ルーン(Unicodeコードポイント)を処理し、isInCharacterRange(r)というヘルパー関数を使って、そのルーンがXMLで許可される文字範囲内にあるかどうかをチェックしていました。もしルーンが許可範囲外であれば、esc_fffd&#xFFFD;)としてエスケープされることになっていました。

しかし、問題はunicode/utf8.DecodeRuneが無効なUTF-8バイトシーケンスを検出した際に0xFFFDを返す点にありました。この0xFFFD自体はisInCharacterRange関数によって「有効な文字」と判断されてしまうため、EscapeText0xFFFDをエスケープせずにそのまま出力しようとしました。さらに、この0xFFFDが元の無効なバイトから生成されたもので、かつその無効なバイトが単一のバイトとして扱われた場合(width == 1)、元の無効なバイトがXML出力にそのまま含まれてしまうという挙動になっていました。これは、XMLの仕様に違反するものでした。

このコミットでは、EscapeText関数の条件式が以下のように変更されました。

変更前:

if !isInCharacterRange(r) {
    esc = esc_fffd
    break
}

変更後:

if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
    esc = esc_fffd
    break
}

この変更により、以下の条件のいずれかが満たされた場合に、ルーンが&#xFFFD;としてエスケープされるようになりました。

  1. !isInCharacterRange(r): ルーンがXMLで許可される文字範囲外である場合(これは以前から存在した条件)。
  2. (r == 0xFFFD && width == 1): ルーンが0xFFFD(Replacement Character)であり、かつその0xFFFDが単一のバイトからデコードされたものである場合。

後者の条件が追加されたことで、unicode/utf8.DecodeRuneが無効な単一バイトを処理して0xFFFDを返した場合に、その0xFFFDがXML出力にそのまま含まれるのではなく、明示的に&#xFFFD;というXMLエンティティとしてエスケープされるようになりました。これにより、生成されるXMLドキュメントは常に整形式となり、無効なバイトが直接XMLに混入することを防ぎます。

テストケースTestIssue5880では、無効なUTF-8バイトシーケンス({192, 168, 0, 1}は、UTF-8の仕様では無効なバイトシーケンスです。特に192は2バイトシーケンスの開始バイトですが、それに続くバイトが不正です)を含むバイトスライスをxml.Marshalに渡し、その結果が有効なUTF-8であるかどうかをutf8.Validで検証しています。このテストが追加されたことで、将来的に同様の問題が再発しないように保証されます。

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

src/pkg/encoding/xml/xml.goEscapeText 関数内の条件式が変更されました。

--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -1758,7 +1758,7 @@ func EscapeText(w io.Writer, s []byte) error {
 		case '\r':
 			esc = esc_cr
 		default:
-			if !isInCharacterRange(r) {
+			if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
 				esc = esc_fffd
 				break
 			}

また、src/pkg/encoding/xml/xml_test.go に新しいテストケース TestIssue5880 が追加されました。

--- a/src/pkg/encoding/xml/xml_test.go
+++ b/src/pkg/encoding/xml/xml_test.go
@@ -11,6 +11,7 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"unicode/utf8"
 )
 
 const testInput = `
@@ -714,3 +715,14 @@ func TestEscapeTextInvalidChar(t *testing.T) {
 		t.Errorf("have %v, want %v", text, expected)
 	}
 }
+
+func TestIssue5880(t *testing.T) {
+	type T []byte
+	data, err := Marshal(T{192, 168, 0, 1})
+	if err != nil {
+		t.Errorf("Marshal error: %v", err)
+	}
+	if !utf8.Valid(data) {
+		t.Errorf("Marshal generated invalid UTF-8: %x", data)
+	}
+}

コアとなるコードの解説

xml.goの変更は、EscapeText関数がXMLテキストをエスケープする際のロジックを強化しています。 EscapeText関数は、入力バイトスライスsをルーンごとに処理し、XMLの仕様に準拠するように特殊文字をエスケープします。 変更された行は、defaultケース内の条件分岐です。

if !isInCharacterRange(r) || (r == 0xFFFD && width == 1)

この条件式は、以下のいずれかの状況でesc_fffd&#xFFFD;)をエスケープシーケンスとして設定します。

  1. !isInCharacterRange(r): 現在処理しているルーンrが、XMLの文字として許可されていない範囲にある場合。これは、XML 1.0の仕様で許可されている文字(#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF])以外の文字を指します。
  2. (r == 0xFFFD && width == 1): 現在処理しているルーンrがUnicodeのReplacement Character(U+FFFD)であり、かつそのルーンが入力バイトスライスから1バイトとしてデコードされた場合。これは、unicode/utf8.DecodeRuneが無効な単一バイトを検出した際に0xFFFDwidth=1を返す挙動に対応しています。この条件を追加することで、無効なバイトが0xFFFDとしてデコードされたとしても、それがXMLに直接含まれるのではなく、XMLエンティティとして適切にエスケープされるようになります。

この修正により、encoding/xmlパッケージは、無効なUTF-8バイトを含む入力に対しても、常に整形式のXMLを生成するようになります。

xml_test.goに追加されたTestIssue5880は、この修正が正しく機能することを検証するためのものです。 このテストでは、{192, 168, 0, 1}という無効なUTF-8バイトシーケンスを含むバイトスライスを定義し、これをxml.Marshal関数に渡してXMLにエンコードします。 その後、utf8.Valid(data)を使って、Marshalによって生成されたXML出力が有効なUTF-8であるかどうかをチェックします。 もし修正がなければ、Marshalは無効なバイトをそのまま出力に含めてしまい、utf8.Validfalseを返してテストが失敗します。修正が適用されたことで、無効なバイトは&#xFFFD;としてエスケープされ、結果として生成されるXMLは有効なUTF-8となるため、テストは成功します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのコミット履歴とIssueトラッカー
  • Unicode標準
  • XML仕様 (W3C勧告)
  • UTF-8エンコーディングに関する一般的な情報源
  • unicode/utf8パッケージのドキュメント
  • encoding/xmlパッケージのドキュメント
  • Go言語のテストに関するドキュメント