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

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

このコミットは、Go言語の encoding/xml パッケージにおけるXMLエンコーディングとデコーディングの挙動を修正するものです。具体的には、XMLデータ内の改行、キャリッジリターン、タブ文字のエスケープ処理と、デコード時のこれらの文字の解釈を改善しています。

コミット

commit 1e6d9f49da330e61d29588b1e2f0f3685c03f359
Author: Ian Lance Taylor <iant@golang.org>
Date:   Thu Oct 18 13:40:45 2012 -0700

    encoding/xml: correctly escape newline, carriage return, and tab
    
    The generated encodings are those from
    http://www.w3.org/TR/2000/WD-xml-c14n-20000119.html#charescaping
    
    The change to the decoder ensures that we turn &#xD; in the
    input into \r, not \n.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6747043

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

https://github.com/golang/go/commit/1e6d9f49da330e61d29588b1e2f0f3685c03f359

元コミット内容

encoding/xml: correctly escape newline, carriage return, and tab

このコミットは、encoding/xml パッケージがXMLデータ内の改行 (\n)、キャリッジリターン (\r)、タブ (\t) 文字を正しくエスケープし、またデコード時に正しく解釈するように修正します。

生成されるエスケープ形式は、http://www.w3.org/TR/2000/WD-xml-c14n-20000119.html#charescaping で定義されているものに準拠します。

デコーダへの変更は、入力中の &#xD; (キャリッジリターンの数値文字参照) を \r (キャリッジリターン) として解釈し、誤って \n (改行) として解釈しないようにすることを保証します。

変更の背景

XMLは、構造化されたデータを表現するためのマークアップ言語であり、その仕様は文字のエンコーディングとエスケープに関して厳格なルールを定めています。特に、XMLドキュメント内で特定の特殊文字(例: <, >, &, ', ")や制御文字(例: 改行、キャリッジリターン、タブ)を扱う際には、それらをXMLパーサーが正しく解釈できるようにエスケープする必要があります。

このコミットが行われた背景には、Go言語の encoding/xml パッケージが、XMLの標準的なエスケープルール、特にW3CのXML正規化(Canonical XML)勧告で定義されている文字エスケープの要件を完全に満たしていなかったという問題がありました。

具体的には、以下の点が問題となっていました。

  1. 改行、キャリッジリターン、タブのエスケープの不正確さ: 従来の encoding/xml パッケージは、これらの文字をXMLエンティティ参照(例: &#xA;&#xD;&#x9;)として適切にエスケープしていませんでした。これにより、生成されたXMLが他のXMLパーサーで正しく解釈されない可能性がありました。
  2. デコード時のキャリッジリターンの誤った解釈: XMLの仕様では、属性値や要素の内容に含まれるキャリッジリターン (\r) は、正規化された形式では改行 (\n) として扱われることがありますが、特定のコンテキスト(特に正規化されたXML出力)では &#xD; としてエスケープされたものが \r として正確にデコードされる必要があります。従来のデコーダは、&#xD;\n として誤って解釈してしまう可能性がありました。

これらの問題は、Goで生成されたXMLが他のシステムとの相互運用性を損なったり、XMLの正規化処理において予期せぬ結果を引き起こしたりする原因となります。このコミットは、これらの問題を解決し、encoding/xml パッケージがXML標準に準拠した、より堅牢なXML処理を提供することを目的としています。

前提知識の解説

XML (Extensible Markup Language)

XMLは、情報を構造化するために設計されたマークアップ言語です。HTMLがウェブページの表示に特化しているのに対し、XMLはデータの記述と転送に重点を置いています。XMLは、ユーザーが独自のタグを定義できるため、「拡張可能」と呼ばれます。

XMLの文字エスケープ

XMLドキュメント内では、特定の文字は特別な意味を持つため、そのまま記述するとXMLパーサーが誤解釈する可能性があります。これらの文字をXMLドキュメント内でリテラルとして表現するためには、エスケープ処理が必要です。

  • < (小なり記号): &lt;
  • > (大なり記号): &gt;
  • & (アンパサンド): &amp;
  • ' (アポストロフィ): &apos;
  • " (引用符): &quot;

これらは「実体参照(Entity Reference)」と呼ばれます。

数値文字参照 (Numeric Character Reference)

上記の実体参照以外にも、Unicode文字をその数値コードで参照する「数値文字参照」があります。これは、特にキーボードから直接入力できない文字や、XMLの予約文字を表現する際に便利です。形式は &#DecimalCode; または &#xHexCode; です。

このコミットで特に重要となるのは、以下の制御文字の数値文字参照です。

  • 改行 (Line Feed, LF): \n&#xA; (16進数A、10進数10)
  • キャリッジリターン (Carriage Return, CR): \r&#xD; (16進数D、10進数13)
  • タブ (Horizontal Tab, HT): \t&#x9; (16進数9、10進数9)

XML正規化 (Canonical XML)

XML正規化(Canonical XML, C14N)は、XMLドキュメントの「正規形式」を定義するW3C勧告です。これは、XMLドキュメントの内容が同じであるにもかかわらず、異なる物理的表現を持つ可能性がある問題を解決するために導入されました。例えば、属性の順序、空白文字の扱い、文字エンコーディングの違いなどが、ドキュメントのバイト表現に影響を与えます。

Canonical XMLの目的は、これらの違いを排除し、同じ論理的コンテンツを持つXMLドキュメントが常に同じバイト表現を持つようにすることです。これにより、デジタル署名やハッシュ計算など、ドキュメントの同一性を保証する必要がある場合に非常に重要になります。

このコミットで参照されている http://www.w3.org/TR/2000/WD-xml-c14n-20000119.html#charescaping は、Canonical XMLの初期のワーキングドラフトであり、特に文字エスケープに関するルールを定義しています。このルールでは、特定の文字(特に制御文字)は数値文字参照としてエスケープされるべきであると規定されています。

Go言語の encoding/xml パッケージ

Go言語の標準ライブラリには、XMLデータのエンコード(Goの構造体からXMLへ)およびデコード(XMLからGoの構造体へ)を行うための encoding/xml パッケージが含まれています。このパッケージは、XMLのマーシャリング(GoオブジェクトからXMLへの変換)とアンマーシャリング(XMLからGoオブジェクトへの変換)の機能を提供します。

このコミットは、このパッケージがXMLの標準的なエスケープルール、特に制御文字の扱いに準拠するように改善することを目的としています。

技術的詳細

このコミットは、Go言語の encoding/xml パッケージにおけるXMLのエンコード(マーシャリング)とデコード(アンマーシャリング)のロジックを修正し、XMLの文字エスケープに関するW3Cの勧告(特にCanonical XMLの初期ドラフト)に準拠させるものです。

エンコード(マーシャリング)側の変更

XMLの仕様では、特定の文字はXMLドキュメント内で特別な意味を持つため、そのまま記述すると構文エラーになったり、意図しない解釈をされたりする可能性があります。そのため、これらの文字はエスケープする必要があります。

従来の encoding/xml パッケージは、一般的なXML予約文字(<, >, &, ', ")については適切にエスケープしていましたが、改行 (\n)、キャリッジリターン (\r)、タブ (\t) といった制御文字については、XMLの正規化(Canonical XML)の文脈で推奨される数値文字参照形式でのエスケープを行っていませんでした。

このコミットでは、xml.go ファイル内の Escape 関数(XML出力ストリームにバイトをエスケープして書き込む関数)に以下の変更が加えられました。

  1. 新しいエスケープバイト列の定義:

    • esc_tab = []byte("&#x9;") (タブ)
    • esc_nl = []byte("&#xA;") (改行)
    • esc_cr = []byte("&#xD;") (キャリッジリターン) これらの定数が追加され、対応する制御文字を数値文字参照としてエスケープするために使用されます。
  2. Escape 関数のロジック変更: Escape 関数内の switch ステートメントに、\t\n\r の各ケースが追加されました。これにより、これらの文字が出現した場合に、それぞれ &#x9;&#xA;&#xD; という数値文字参照に変換されて出力されるようになります。

この変更により、encoding/xml パッケージが生成するXML出力は、改行、キャリッジリターン、タブ文字をXML標準に準拠した形式でエスケープするようになり、他のXMLパーサーとの相互運用性が向上します。

デコード(アンマーシャリング)側の変更

XMLのデコードにおいては、エンコードされた数値文字参照を元の文字に正しく戻す必要があります。特に、XMLの仕様では、属性値や要素の内容に含まれるキャリッジリターン (\r) は、通常、正規化された形式では改行 (\n) として扱われるという複雑なルールがあります。しかし、このコミットの目的は、&#xD; という数値文字参照が入力された場合に、それを \r として正確にデコードすることです。

従来のデコーダは、入力ストリーム中の \r\r\n シーケンスを \n に変換するロジックを持っていました。これは、XMLパーサーが通常行う正規化処理の一部ですが、&#xD;\r を表す数値文字参照として入力された場合に、それを \n に誤って変換してしまう可能性がありました。

このコミットでは、xml.go ファイル内の d.readElement メソッド(XML要素の内容を読み込む部分)のデコードロジックが変更されました。

  1. d.buf.WriteByte(b) の変更: 従来のコードでは、読み込んだバイト b を直接バッファ d.buf に書き込んでいました。

    d.buf.WriteByte(b)
    

    これが、以下の条件付きロジックに置き換えられました。

    // We must rewrite unescaped \r and \r\n into \n.
    if b == '\r' {
        d.buf.WriteByte('\n')
    } else if b1 == '\r' && b == '\n' {
        // Skip \r\n--we already wrote \n.
    } else {
        d.buf.WriteByte(b)
    }
    

    この変更は、入力ストリーム中のエスケープされていない \r\r\n シーケンスを \n に変換するというXMLの正規化ルールを実装しています。

  2. bytes スライスに対する後処理の削除: 従来のコードには、d.buf.Bytes() で取得した data スライスに対して、再度 \r\r\n\n に変換する後処理のループがありました。

    // Must rewrite \r and \r\n into \n.
    w := 0
    for r := 0; r < len(data); r++ {
        b := data[r]
        if b == '\r' {
            if r+1 < len(data) && data[r+1] == '\n' {
                continue
            }
            b = '\n'
        }
        data[w] = b
        w++
    }
    return data[0:w]
    

    この後処理のループが完全に削除され、return data となりました。

これらのデコーダ側の変更の意図は、&#xD; のような数値文字参照としてエスケープされたキャリッジリターンが、デコード時に \r として正しく保持されるようにすることです。同時に、XMLの仕様に従い、エスケープされていない \r\r\n\n に正規化されるという挙動も維持しています。

テストケースの追加

marshal_test.go には、新しいエスケープとデコードの挙動を検証するためのテストケースが追加されました。

  • エスケープテスト:
    • newline: &#xA;; cr: &#xD;; tab: &#x9;; のような文字列が、期待される数値文字参照としてエスケープされることを確認するテスト。
  • デコードテスト:
    • 1\r2\r\n3\n\r4\n5 のような入力が、デコード後に 1\n2\n3\n\n4\n5 となることを確認するテスト。これは、UnmarshalOnly: true となっており、デコード時の \r\r\n の正規化挙動を検証しています。

これらのテストケースは、コミットの変更が意図した通りに機能し、XMLのエンコードとデコードがW3Cの勧告に準拠していることを保証します。

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

src/pkg/encoding/xml/marshal_test.go

--- a/src/pkg/encoding/xml/marshal_test.go
+++ b/src/pkg/encoding/xml/marshal_test.go
@@ -687,6 +687,27 @@ var marshalTests = []struct {
 		UnmarshalOnly: true,
 	},
+\n+\t// Test escaping.
+\t{\n+\t\tExpectXML: `<a><nested><value>dquote: &#34;; squote: &#39;; ampersand: &amp;; less: &lt;; greater: &gt;;</value></nested></a>`,\n+\t\tValue: &AnyTest{\n+\t\t\tNested: `dquote: \"; squote: \'; ampersand: &; less: <; greater: >;`,\n+\t\t},\n+\t},\n+\t{\n+\t\tExpectXML: `<a><nested><value>newline: &#xA;; cr: &#xD;; tab: &#x9;;</value></nested></a>`,\n+\t\tValue: &AnyTest{\n+\t\t\tNested: "newline: \\n; cr: \\r; tab: \\t;",\n+\t\t},\n+\t},\n+\t{\n+\t\tExpectXML: "<a><nested><value>1\\r2\\r\\n3\\n\\r4\\n5</value></nested></a>",\n+\t\tValue: &AnyTest{\n+\t\t\tNested: "1\\n2\\n3\\n\\n4\\n5",\n+\t\t},\n+\t\tUnmarshalOnly: true,\n+\t},\
 }
  • XMLエスケープのテストケースが追加されました。特に、改行 (\n)、キャリッジリターン (\r)、タブ (\t) がそれぞれ &#xA;&#xD;&#x9; としてエスケープされることを検証するテストが追加されています。
  • デコード時の \r\r\n の正規化挙動を検証するテストも追加されています。

src/pkg/encoding/xml/xml.go

--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -964,7 +964,16 @@ Input:
 			tb0, b1 = 0, 0
 			continue Input
 		}
-		d.buf.WriteByte(b)
+
+		// We must rewrite unescaped \r and \r\n into \n.
+		if b == '\r' {
+			d.buf.WriteByte('\n')
+		} else if b1 == '\r' && b == '\n' {
+			// Skip \r\n--we already wrote \n.
+		} else {
+			d.buf.WriteByte(b)
+		}
+
 		b0, b1 = b1, b
 	}
 	data := d.buf.Bytes()
@@ -985,20 +994,7 @@ Input:
 		}
 	}
 
-	// Must rewrite \r and \r\n into \n.
-	w := 0
-	for r := 0; r < len(data); r++ {
-		b := data[r]
-		if b == '\r' {
-			if r+1 < len(data) && data[r+1] == '\n' {
-				continue
-			}
-			b = '\n'
-		}
-		data[w] = b
-		w++
-	}
-	return data[0:w]
+	return data
 }
 
 // Decide whether the given rune is in the XML Character Range, per
@@ -1689,6 +1685,9 @@ var (
 	esc_amp  = []byte("&amp;")
 	esc_lt   = []byte("&lt;")
 	esc_gt   = []byte("&gt;")
+	esc_tab  = []byte("&#x9;")
+	esc_nl   = []byte("&#xA;")
+	esc_cr   = []byte("&#xD;")
 )
 
 // Escape writes to w the properly escaped XML equivalent
@@ -1708,6 +1707,12 @@ func Escape(w io.Writer, s []byte) {
 		case '>':
 			esc = esc_gt
+		case '\t':
+			esc = esc_tab
+		case '\n':
+			esc = esc_nl
+		case '\r':
+			esc = esc_cr
 		default:
 			continue
 		}
  • d.readElement メソッド内で、デコード時の \r および \r\n の正規化ロジックが変更されました。入力ストリームから直接バッファに書き込む際に、これらの文字を \n に変換するようになりました。
  • 以前の bytes スライスに対する後処理のループが削除されました。
  • Escape 関数で使用される新しいエスケープバイト列 (esc_tab, esc_nl, esc_cr) が定義されました。
  • Escape 関数に、\t\n\r をそれぞれ対応する数値文字参照 (&#x9;, &#xA;, &#xD;) にエスケープするロジックが追加されました。

コアとなるコードの解説

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

このコミットの核となる変更は、xml.go ファイル内の Escape 関数と、XMLデコードロジックの一部です。

Escape 関数の変更

Escape 関数は、Goのデータ構造からXMLを生成する際に、文字列内の特殊文字をXMLエンティティ参照または数値文字参照に変換する役割を担っています。

変更前: 従来の Escape 関数は、XMLの基本的な予約文字(<, >, &, ', ")のみをエスケープしていました。改行 (\n)、キャリッジリターン (\r)、タブ (\t) といった制御文字は、そのまま出力されるか、あるいはXMLの仕様に厳密に準拠しない形で処理されていました。

変更後:

  1. 新しいエスケープ定数の追加:

    var (
        // ... 既存のエスケープ定数 ...
        esc_tab  = []byte("&#x9;")
        esc_nl   = []byte("&#xA;")
        esc_cr   = []byte("&#xD;")
    )
    

    ここで、タブ (\t)、改行 (\n)、キャリッジリターン (\r) をそれぞれXMLの数値文字参照形式 (&#x9;, &#xA;, &#xD;) で表現するためのバイトスライスが定義されました。これは、W3CのXML正規化勧告で推奨される形式です。

  2. Escape 関数の switch 文の拡張:

    func Escape(w io.Writer, s []byte) {
        // ...
        for i, b := range s {
            // ...
            switch b {
            // ... 既存のケース ...
            case '\t':
                esc = esc_tab
            case '\n':
                esc = esc_nl
            case '\r':
                esc = esc_cr
            default:
                continue
            }
            // ...
        }
    }
    

    switch b 文に、\t\n\r の各ケースが追加されました。これにより、これらの文字が入力バイトスライス s 内で見つかった場合、対応する数値文字参照 (esc_tab, esc_nl, esc_cr) に置き換えられて出力ライター w に書き込まれるようになります。

この変更により、encoding/xml パッケージは、XMLの標準的なエスケープルール、特に制御文字の扱いに完全に準拠するようになり、生成されるXMLの相互運用性と正確性が向上します。

XMLデコードロジックの変更

XMLデコードロジックは、XML入力ストリームをGoのデータ構造に変換する際に、エスケープされた文字を元の文字に戻す役割を担っています。特に、XMLの仕様では、要素の内容や属性値に含まれるキャリッジリターン (\r) は、通常、改行 (\n) に正規化されるという複雑なルールがあります。しかし、このコミットの目的は、&#xD; のような数値文字参照としてエスケープされたキャリッジリターンが、デコード時に \r として正確に保持されるようにすることです。

変更前: 従来のデコードロジックは、入力ストリーム中の \r\r\n シーケンスを \n に変換する後処理のループを持っていました。これは、XMLパーサーが通常行う正規化処理の一部ですが、&#xD;\r を表す数値文字参照として入力された場合に、それを \n に誤って変換してしまう可能性がありました。

変更後:

  1. d.buf.WriteByte(b) の変更: d.readElement メソッド内で、入力バイト b をバッファ d.buf に書き込む部分が変更されました。

    // We must rewrite unescaped \r and \r\n into \n.
    if b == '\r' {
        d.buf.WriteByte('\n')
    } else if b1 == '\r' && b == '\n' {
        // Skip \r\n--we already wrote \n.
    } else {
        d.buf.WriteByte(b)
    }
    

    この新しいロジックは、入力ストリーム中のエスケープされていない \r\n に変換し、\r\n シーケンスを単一の \n に変換します。これは、XMLの仕様における「行末処理」の正規化ルールを実装しています。この処理は、数値文字参照としてエスケープされた &#xD; には適用されません。

  2. 後処理ループの削除:

    -	// Must rewrite \r and \r\n into \n.
    -	w := 0
    -	for r := 0; r < len(data); r++ {
    -		b := data[r]
    -		if b == '\r' {
    -			if r+1 < len(data) && data[r+1] == '\n' {
    -				continue
    -			}
    -			b = '\n'
    -		}
    -		data[w] = b
    -		w++
    -	}
    -	return data[0:w]
    +	return data
    

    以前のコードにあった、data スライスに対して再度 \r\r\n\n に変換する後処理のループが完全に削除されました。これは、上記の新しい d.buf.WriteByte ロジックが、必要な正規化をその場で実行するため、後処理が不要になったためです。

これらのデコーダ側の変更により、encoding/xml パッケージは、XMLの仕様に従い、エスケープされていない \r\r\n\n に正規化しつつ、&#xD; のような数値文字参照としてエスケープされたキャリッジリターンは、デコード時に \r として正しく保持されるようになりました。これにより、XMLのデコードの正確性が向上し、特にXML正規化の文脈での挙動が改善されます。

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

このファイルへの変更は、主に新しいテストケースの追加です。

// Test escaping.
{
    ExpectXML: `<a><nested><value>dquote: &#34;; squote: &#39;; ampersand: &amp;; less: &lt;; greater: &gt;;</value></nested></a>`,
    Value: &AnyTest{
        Nested: `dquote: "; squote: '; ampersand: &; less: <; greater: >;`,
    },
},
{
    ExpectXML: `<a><nested><value>newline: &#xA;; cr: &#xD;; tab: &#x9;;</value></nested></a>`,
    Value: &AnyTest{
        Nested: "newline: \\n; cr: \\r; tab: \\t;",
    },
},
{
    ExpectXML: "<a><nested><value>1\\r2\\r\\n3\\n\\r4\\n5</value></nested></a>",
    Value: &AnyTest{
        Nested: "1\\n2\\n3\\n\\n4\\n5",
    },
    UnmarshalOnly: true,
},
  • 最初の2つのテストケースは、Goの文字列に含まれる特殊文字(引用符、アポストロフィ、アンパサンド、小なり、大なり、改行、キャリッジリターン、タブ)が、XMLにマーシャリングされた際に、それぞれ対応するXMLエンティティ参照または数値文字参照に正しくエスケープされることを検証します。
  • 3番目のテストケースは、デコード(アンマーシャリング)に特化したテストです。XML入力中の \r\r\n シーケンスが、Goの文字列にアンマーシャリングされた際に、XMLの行末処理ルールに従って \n に正規化されることを検証します。UnmarshalOnly: true は、このテストケースがマーシャリングではなく、アンマーシャリングの挙動のみを対象としていることを示します。

これらのテストケースは、encoding/xml パッケージのエンコードおよびデコードロジックが、XMLの標準的な文字エスケープと行末処理のルールに準拠していることを保証するための重要な検証手段となります。

関連リンク

参考にした情報源リンク

  • W3C Working Draft 19 January 2000 - Canonical XML Version 1.0: http://www.w3.org/TR/2000/WD-xml-c14n-20000119.html#charescaping
    • コミットメッセージで直接参照されている、文字エスケープに関するセクション。
  • Go CL 6747043: https://golang.org/cl/6747043
    • このコミットに対応するGoのコードレビューシステム(Gerrit)のチェンジリスト。詳細な議論や変更履歴が含まれている場合があります。
  • XMLの特殊文字とエスケープ: https://www.w3.org/TR/xml/#sec-predefined-entities
    • XMLの仕様における実体参照の定義。
  • XMLの行末処理: https://www.w3.org/TR/xml/#sec-line-ends
    • XMLの仕様における行末(\r, \n, \r\n)の正規化に関するルール。
  • Go言語のXML処理に関するブログ記事やチュートリアル:
    • Go言語の encoding/xml パッケージの基本的な使い方や、XML処理における一般的な課題について解説している記事は多数存在します。これらの記事は、このコミットの変更が解決しようとしている問題の文脈を理解するのに役立ちます。
    • 例: "Working with XML in Go" (Go言語におけるXMLの扱い) などで検索すると良いでしょう。
    • (具体的なURLは検索結果によって変動するため、ここでは一般的な検索クエリを提示します。)
  • XML正規化に関する解説記事:
    • Canonical XMLの概念やその重要性について解説している記事も、このコミットの背景を深く理解するために有用です。
    • 例: "What is Canonical XML?" (Canonical XMLとは何か?) などで検索すると良いでしょう。
    • (具体的なURLは検索結果によって変動するため、ここでは一般的な検索クエリを提示します。)