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

[インデックス 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+0008U+000BU+000CU+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エンティティ(例: <&lt;)にエスケープします。このコミットでは、この EscapeText 関数が不正なコードポイントの処理も担当するように変更されました。

utf8.DecodeRune

Go言語の unicode/utf8 パッケージに含まれる utf8.DecodeRune 関数は、UTF-8エンコードされたバイトスライスから次のUnicodeコードポイント(rune)とそのバイト幅をデコードします。これにより、マルチバイト文字を正しく処理し、各文字が有効なUnicodeコードポイントであるかを確認することができます。

技術的詳細

このコミットの主要な変更点は、encoding/xml パッケージ内の EscapeText 関数に、入力テキストに含まれる不正なUnicodeコードポイントを検出して U+FFFD に置き換えるロジックを追加したことです。

  1. esc_fffd の追加: xml.go のグローバル変数として、esc_fffd = []byte("\uFFFD") が追加されました。これは、不正なコードポイントを置き換えるためのバイト列です。

  2. 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 + 1last = i に変更されました。これは、i が既に次の文字の開始位置を指しているためです。
  3. isInCharacterRange 関数の利用: コミットの差分には直接含まれていませんが、!isInCharacterRange(r) という条件が追加されていることから、XML 1.0で有効な文字範囲をチェックする isInCharacterRange 関数が内部的に使用されていることが示唆されます。この関数は、与えられたUnicodeコードポイントがXML 1.0の仕様で許可されている範囲内にあるかどうかを判定します。

  4. テストケースの追加: xml_test.goTestEscapeTextInvalidChar という新しいテストケースが追加されました。このテストは、ヌル文字 (\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("&#x9;")
 	esc_nl   = []byte("&#xA;")
 	esc_cr   = []byte("&#xD;")
+	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 の変更点

  1. esc_fffd の追加: esc_fffd = []byte("\uFFFD") は、Unicodeの置換文字 U+FFFD をバイトスライスとして定義しています。これは、XMLの仕様で許可されていない文字が検出された場合に、その文字の代わりにXML出力に書き込まれるものです。

  2. 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 は、次のイテレーションで処理を開始する位置を、現在の文字の終端の直後に移動させます。これにより、文字単位での正確な処理が可能になります。
  3. 不正な文字の検出と置換ロジック:

    • switch r { ... default: ... }default ブロック内に if !isInCharacterRange(r) { esc = esc_fffd; break } が追加されました。
    • isInCharacterRange(r) は、デコードされたUnicodeコードポイント r がXML 1.0の有効な文字範囲内にあるかどうかをチェックする関数です。
    • もし r が有効な文字範囲外であれば、esc 変数に esc_fffd が設定され、その不正な文字が U+FFFD に置き換えられるようにマークされます。breakswitch 文を抜け、エスケープ処理に進みます。
  4. w.Write の引数変更:

    • w.Write(s[last:i]) から w.Write(s[last : i-width]) への変更は、utf8.DecodeRune によって i が既に次の文字の開始位置を指しているためです。i-width は、現在処理中の文字の開始位置を正確に指し、その文字の直前までのバイトを書き込むことを保証します。
  5. last 変数の更新:

    • last = i + 1 から last = i への変更も、i が既に次の文字の開始位置を指しているためです。これにより、次のループで s[last:] が正しく次の文字から始まるように設定されます。

これらの変更により、EscapeText 関数は、入力されたバイトスライスをUTF-8として正しく解釈し、XML 1.0の仕様に違反する文字を U+FFFD に置き換えることで、堅牢なXMLエスケープ処理を実現しています。

xml_test.go の変更点

  1. bytes パッケージのインポート: 新しいテストケースで bytes.Buffer を使用するため、"bytes" パッケージがインポートされました。

  2. TestEscapeTextInvalidChar テストケースの追加:

    • input := []byte("A \x00 terminated string.") は、ヌル文字 (\x00、Unicode U+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 に置き換えられるという新しいロジックが機能していることを確認します。

このテストケースの追加は、変更が正しく機能し、将来のリグレッションを防ぐための重要なステップです。

関連リンク

参考にした情報源リンク