[インデックス 15757] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/xml パッケージ内の xml.go および xml_test.go ファイルに対する変更です。encoding/xml パッケージは、GoのプログラムとXMLデータの間で構造体のマーシャリング(Goのデータ構造からXMLへの変換)およびアンマーシャリング(XMLからGoのデータ構造への変換)を行う機能を提供します。この変更は、特にXMLへのマーシャリングおよびエスケープ処理において、不正なコードポイント(文字)がXML出力に含まれる問題を修正することを目的としています。
コミット
commit f74eb6dbf73ff7c3caebcd339d250d6e4630a848
Author: Olivier Saingre <osaingre@gmail.com>
Date: Wed Mar 13 23:26:03 2013 -0400
encoding/xml: rewrite invalid code points to U+FFFD in Marshal, Escape
Fixes #4235.
R=rsc, dave, r, dr.volker.dobler
CC=golang-dev
https://golang.org/cl/7438051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f74eb6dbf73ff7c3caebcd339d250d6e4630a848
元コミット内容
encoding/xml: rewrite invalid code points to U+FFFD in Marshal, Escape
Fixes #4235.
変更の背景
この変更は、Goの encoding/xml パッケージが、XML 1.0の仕様で許可されていない不正な文字(コードポイント)をXML出力に含めてしまうという問題(Issue #4235)を修正するために行われました。XML 1.0の仕様では、特定の範囲のUnicode文字のみが有効とされており、例えばヌル文字(U+0000)や一部の制御文字はXMLドキュメント内で使用することができません。
以前の実装では、これらの不正な文字が入力データに含まれていた場合、そのままXML出力に書き出されてしまい、結果としてXMLパーサーがエラーを発生させたり、予期せぬ動作を引き起こしたりする可能性がありました。このコミットは、このような不正な文字をXML仕様に準拠した形式に変換することで、生成されるXMLの堅牢性と互換性を向上させることを目的としています。具体的には、不正なコードポイントをUnicodeの置換文字である U+FFFD (REPLACEMENT CHARACTER) に置き換えることで、XMLの妥当性を保証します。
前提知識の解説
XML (Extensible Markup Language)
XMLは、情報を構造化するためのマークアップ言語です。HTMLがウェブページの表示に特化しているのに対し、XMLはデータの記述と転送に重点を置いています。XMLドキュメントは、要素、属性、テキストコンテンツなどから構成され、厳格な構文規則に従う必要があります。
XML 1.0 文字範囲
XML 1.0の仕様では、XMLドキュメント内で使用できる文字の範囲が厳密に定義されています。 有効な文字は以下のUnicodeコードポイントです。
#x9(タブ)#xA(改行)#xD(復帰)#x20から#xD7FFまで#xE000から#xFFFDまで#x10000から#x10FFFFまで
これ以外のコードポイント、特に U+0000 (NULL) や U+0001 から U+0008、U+000B、U+000C、U+000E から U+001F などの制御文字はXMLドキュメント内で直接使用することはできません。
Unicode 置換文字 (U+FFFD)
U+FFFD (REPLACEMENT CHARACTER) は、Unicodeにおいて、文字コードの変換エラーや、不正なバイトシーケンスが検出された場合に使用される特殊な文字です。この文字は、元の文字が何であったかを特定できない場合に、その位置に「何か不正なものがあった」ことを示すために用いられます。XMLの文脈では、不正な文字をこの U+FFFD に置き換えることで、XMLドキュメント全体の構造を壊すことなく、不正な文字の存在を示すことができます。
Go言語の encoding/xml パッケージ
Go言語の encoding/xml パッケージは、Goの構造体とXMLドキュメントの間でデータを変換するための機能を提供します。
xml.Marshal: Goの構造体をXMLバイト列に変換(マーシャリング)します。xml.EscapeText: テキストコンテンツ内の特殊文字(<,>,&,',")をXMLエンティティ(例:<を<)にエスケープします。このコミットでは、このEscapeText関数が不正なコードポイントの処理も担当するように変更されました。
utf8.DecodeRune
Go言語の unicode/utf8 パッケージに含まれる utf8.DecodeRune 関数は、UTF-8エンコードされたバイトスライスから次のUnicodeコードポイント(rune)とそのバイト幅をデコードします。これにより、マルチバイト文字を正しく処理し、各文字が有効なUnicodeコードポイントであるかを確認することができます。
技術的詳細
このコミットの主要な変更点は、encoding/xml パッケージ内の EscapeText 関数に、入力テキストに含まれる不正なUnicodeコードポイントを検出して U+FFFD に置き換えるロジックを追加したことです。
-
esc_fffdの追加:xml.goのグローバル変数として、esc_fffd = []byte("\uFFFD")が追加されました。これは、不正なコードポイントを置き換えるためのバイト列です。 -
EscapeText関数の変更:- 以前は
for i, c := range sを使用してバイトスライスをイテレートしていましたが、これはUTF-8のマルチバイト文字を正しく扱えません。 - 変更後、
for i := 0; i < len(s); { r, width := utf8.DecodeRune(s[i:]); i += width; ... }の形式に変更されました。これにより、utf8.DecodeRuneを使用して各Unicodeコードポイント(rune)とそのバイト幅を正確にデコードできるようになりました。 - デコードされた
r(rune) が、XML 1.0の文字範囲内にない場合(!isInCharacterRange(r))、esc変数にesc_fffdが設定され、その不正な文字がU+FFFDに置き換えられるようになりました。 w.Write(s[last:i])の部分がw.Write(s[last : i-width])に変更されました。これは、utf8.DecodeRuneが読み取った現在の文字のバイト幅を考慮して、エスケープが必要な文字の直前までのバイトを書き込むためです。last = i + 1がlast = iに変更されました。これは、iが既に次の文字の開始位置を指しているためです。
- 以前は
-
isInCharacterRange関数の利用: コミットの差分には直接含まれていませんが、!isInCharacterRange(r)という条件が追加されていることから、XML 1.0で有効な文字範囲をチェックするisInCharacterRange関数が内部的に使用されていることが示唆されます。この関数は、与えられたUnicodeコードポイントがXML 1.0の仕様で許可されている範囲内にあるかどうかを判定します。 -
テストケースの追加:
xml_test.goにTestEscapeTextInvalidCharという新しいテストケースが追加されました。このテストは、ヌル文字 (\x00) を含む文字列をEscapeTextに渡し、その結果がU+FFFDに正しく置き換えられていることを検証します。これにより、不正な文字のハンドリングが期待通りに行われることが保証されます。
これらの変更により、encoding/xml パッケージは、入力データに不正なコードポイントが含まれていても、XML 1.0の仕様に準拠した有効なXMLを生成できるようになりました。
コアとなるコードの変更箇所
src/pkg/encoding/xml/xml.go
--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -1729,6 +1729,7 @@ var (
esc_tab = []byte("	")
esc_nl = []byte("
")
esc_cr = []byte("
")
+ esc_fffd = []byte("\uFFFD") // Unicode replacement character
)
// EscapeText writes to w the properly escaped XML equivalent
@@ -1736,8 +1737,10 @@ func EscapeText(w io.Writer, s []byte) error {
var esc []byte
last := 0
- for i, c := range s {
- switch c {
+ for i := 0; i < len(s); {
+ r, width := utf8.DecodeRune(s[i:])
+ i += width
+ switch r {
case '"':
esc = esc_quot
case '\'':
@@ -1755,15 +1758,19 @@ func EscapeText(w io.Writer, s []byte) error {
case '\r':
esc = esc_cr
default:
+ if !isInCharacterRange(r) {
+ esc = esc_fffd
+ break
+ }
continue
}
- if _, err := w.Write(s[last:i]); err != nil {
+ if _, err := w.Write(s[last : i-width]); err != nil {
return err
}
if _, err := w.Write(esc); err != nil {
return err
}
- last = i + 1
+ last = i
}
if _, err := w.Write(s[last:]); err != nil {
return err
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 (
+ "bytes"
"fmt"
"io"
"reflect"
@@ -695,6 +696,21 @@ func TestEscapeTextIOErrors(t *testing.T) {
err := EscapeText(errWriter{}, []byte{'A'})
if err == nil || err.Error() != expectErr {
- t.Errorf("EscapeTest = [error] %v, want %v", err, expectErr)
+ t.Errorf("have %v, want %v", err, expectErr)
+ }
+}
+
+func TestEscapeTextInvalidChar(t *testing.T) {
+ input := []byte("A \x00 terminated string.")
+ expected := "A \uFFFD terminated string."
+
+ buff := new(bytes.Buffer)
+ if err := EscapeText(buff, input); err != nil {
+ t.Fatalf("have %v, want nil", err)
+ }
+ text := buff.String()
+
+ if text != expected {
+ t.Errorf("have %v, want %v", text, expected)
}
}
コアとなるコードの解説
xml.go の変更点
-
esc_fffdの追加:esc_fffd = []byte("\uFFFD")は、Unicodeの置換文字U+FFFDをバイトスライスとして定義しています。これは、XMLの仕様で許可されていない文字が検出された場合に、その文字の代わりにXML出力に書き込まれるものです。 -
EscapeText関数のループ処理の変更:for i, c := range sからfor i := 0; i < len(s); { r, width := utf8.DecodeRune(s[i:]); i += width; ... }への変更は非常に重要です。元のrangeループはバイト単位でイテレートするため、UTF-8のマルチバイト文字を正しく処理できませんでした。例えば、日本語の文字は複数のバイトで構成されるため、rangeループでは1文字が複数のcとして扱われてしまい、文字単位での処理が困難でした。utf8.DecodeRune(s[i:])は、現在の位置iから始まるバイトスライスsから次の完全なUnicodeコードポイント(r)と、そのコードポイントが占めるバイト数(width)をデコードします。i += widthは、次のイテレーションで処理を開始する位置を、現在の文字の終端の直後に移動させます。これにより、文字単位での正確な処理が可能になります。
-
不正な文字の検出と置換ロジック:
switch r { ... default: ... }のdefaultブロック内にif !isInCharacterRange(r) { esc = esc_fffd; break }が追加されました。isInCharacterRange(r)は、デコードされたUnicodeコードポイントrがXML 1.0の有効な文字範囲内にあるかどうかをチェックする関数です。- もし
rが有効な文字範囲外であれば、esc変数にesc_fffdが設定され、その不正な文字がU+FFFDに置き換えられるようにマークされます。breakはswitch文を抜け、エスケープ処理に進みます。
-
w.Writeの引数変更:w.Write(s[last:i])からw.Write(s[last : i-width])への変更は、utf8.DecodeRuneによってiが既に次の文字の開始位置を指しているためです。i-widthは、現在処理中の文字の開始位置を正確に指し、その文字の直前までのバイトを書き込むことを保証します。
-
last変数の更新:last = i + 1からlast = iへの変更も、iが既に次の文字の開始位置を指しているためです。これにより、次のループでs[last:]が正しく次の文字から始まるように設定されます。
これらの変更により、EscapeText 関数は、入力されたバイトスライスをUTF-8として正しく解釈し、XML 1.0の仕様に違反する文字を U+FFFD に置き換えることで、堅牢なXMLエスケープ処理を実現しています。
xml_test.go の変更点
-
bytesパッケージのインポート: 新しいテストケースでbytes.Bufferを使用するため、"bytes"パッケージがインポートされました。 -
TestEscapeTextInvalidCharテストケースの追加:input := []byte("A \x00 terminated string.")は、ヌル文字 (\x00、UnicodeU+0000) を含むバイトスライスを定義しています。ヌル文字はXML 1.0で許可されていない文字です。expected := "A \uFFFD terminated string."は、期待される出力文字列を定義しています。ヌル文字がU+FFFDに置き換えられていることが期待されます。buff := new(bytes.Buffer)は、EscapeText関数の出力先となるバッファを作成します。if err := EscapeText(buff, input); err != nil { ... }は、EscapeText関数を呼び出し、エラーがないことを確認します。text := buff.String()は、バッファに書き込まれた結果の文字列を取得します。if text != expected { ... }は、実際の出力が期待される出力と一致するかどうかを検証します。これにより、不正な文字が正しくU+FFFDに置き換えられるという新しいロジックが機能していることを確認します。
このテストケースの追加は、変更が正しく機能し、将来のリグレッションを防ぐための重要なステップです。
関連リンク
- Go Issue #4235: https://golang.org/issue/4235
- Go CL 7438051: https://golang.org/cl/7438051