[インデックス 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