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

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

このコミットは、Go言語の標準ライブラリである encoding/xml パッケージにおけるXML名前空間(namespace)に関する複数のバグ修正と機能改善を目的としています。具体的には、XMLのマーシャリング(Goの構造体からXMLへの変換)とアンマーシャリング(XMLからGoの構造体への変換)において、名前空間の扱いをより正確かつ柔軟にするための変更が含まれています。

変更されたファイルは以下の通りです。

  • src/pkg/encoding/xml/marshal.go: XMLへのマーシャリング(エンコード)に関するロジックが含まれています。
  • src/pkg/encoding/xml/read.go: XMLからのアンマーシャリング(デコード)に関するロジックが含まれています。
  • src/pkg/encoding/xml/read_test.go: encoding/xml パッケージのテストコードであり、今回の名前空間関連の修正を検証するための新しいテストケースが多数追加されています。
  • src/pkg/encoding/xml/typeinfo.go: Goの型情報とXMLタグのマッピングに関するロジックが含まれています。
  • src/pkg/encoding/xml/xml.go: encoding/xml パッケージの主要な定義とユーティリティ関数が含まれています。

コミット

commit 4dd3e1e844cfab83b73b4ca417a0095ae7b0ef66
Author: Russ Cox <rsc@golang.org>
Date:   Tue Mar 12 11:46:12 2013 -0400

    encoding/xml: name space bug fixes
    
    If two fields have the same name but different explicit name spaces,
    treat as non-conflicting. This allows parsing common XML formats
    that have ns1:tag and ns2:tag in the same XML element.
    Fixes #4691.
    
    Allow setting the default name space for unadorned tags, by
    writing to Decoder.DefaultSpace. This allows turned the job of
    parsing common XML formats that have tag and ns2:tag in the
    same XML element into the first case by setting DefaultSpace="ns1".
    Fixes #3703.
    
    Use name space attributes when decoding.
    Attach name space to attributes when encoding.
    Could be done with fewer annotations, but semantically correct as is.
    Fixes #3526.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7227056

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

https://github.com/golang/go/commit/4dd3e1e844cfab83b73b4ca417a0095ae7b0ef66

元コミット内容

encoding/xml: name space bug fixes

If two fields have the same name but different explicit name spaces,
treat as non-conflicting. This allows parsing common XML formats
that have ns1:tag and ns2:tag in the same XML element.
Fixes #4691.

Allow setting the default name space for unadorned tags, by
writing to Decoder.DefaultSpace. This allows turned the job of
parsing common XML formats that have tag and ns2:tag in the
same XML element into the first case by setting DefaultSpace="ns1".
Fixes #3703.

Use name space attributes when decoding.
Attach name space to attributes when encoding.
Could be done with fewer annotations, but semantically correct as is.
Fixes #3526.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7227056

変更の背景

このコミットは、encoding/xml パッケージがXML名前空間を扱う上での既存の課題とバグを解決するために導入されました。主な背景は以下の3点です。

  1. 異なる名前空間を持つ同名フィールドの競合解消 (Fixes #4691): 従来の encoding/xml パッケージでは、Goの構造体フィールドがXMLタグにマッピングされる際、同じローカル名を持つが異なるXML名前空間に属するタグ(例: <ns1:tag><ns2:tag>) がXMLドキュメント内に存在する場合、これらを正しく区別してアンマーシャリングすることが困難でした。これにより、一般的なXMLフォーマットのパースに問題が生じていました。この修正は、名前空間が異なる場合は競合しないものとして扱うことで、この問題を解決します。

  2. デフォルト名前空間の設定機能の追加 (Fixes #3703): XMLでは、プレフィックスを持たないタグ(unadorned tags)は、その要素のスコープ内で定義されたデフォルト名前空間に属します。しかし、encoding/xml パッケージには、このようなデフォルト名前空間を明示的に設定するメカニズムが不足していました。これにより、特定のデフォルト名前空間を持つXMLドキュメントを正確にアンマーシャリングすることが困難でした。Decoder.DefaultSpace フィールドの導入により、この問題が解決されます。

  3. 属性の名前空間のエンコード/デコードの改善 (Fixes #3526): XML属性も名前空間を持つことができます。従来の encoding/xml パッケージでは、属性の名前空間のデコード(アンマーシャリング時)とエンコード(マーシャリング時)の扱いが不十分でした。特に、属性に名前空間を正しく付与してエンコードする機能が不足しており、生成されるXMLのセマンティックな正確性に影響を与えていました。この修正は、属性の名前空間を適切に処理することで、より標準に準拠したXMLの生成と解析を可能にします。

これらの問題は、Go言語でXMLを扱う多くのアプリケーションにとって重要な課題であり、特に複雑なXMLスキーマやWebサービスとの連携において、正確な名前空間の処理は不可欠でした。

前提知識の解説

XML名前空間 (XML Namespaces)

XML名前空間は、XMLドキュメント内で要素名や属性名の衝突を避けるためのメカニズムです。異なるXML語彙(スキーマ)から要素や属性を組み合わせる際に特に重要になります。

  • 名前空間の宣言: 名前空間は xmlns 属性を使って宣言されます。

    • xmlns:prefix="URI": プレフィックス付きの名前空間を宣言します。このプレフィックスは、その要素とその子孫要素内で、指定されたURIに関連付けられた要素名や属性名に使用されます。
    • xmlns="URI": デフォルト名前空間を宣言します。プレフィックスなしの要素名や属性名が、このURIに関連付けられます。
  • 修飾名 (Qualified Name): プレフィックスとローカル名(例: prefix:localname)で構成されます。

  • ローカル名 (Local Name): 修飾名からプレフィックスを除いた部分(例: localname)。

  • 名前空間URI (Namespace URI): 名前空間を一意に識別するURI。

例:

<html:table xmlns:html="http://www.w3.org/TR/html4/" xmlns:f="http://www.w3schools.com/furniture">
  <html:tr>
    <html:td>HTML Table</html:td>
    <f:td>Furniture Table</f:td>
  </html:tr>
</html:table>

この例では、html プレフィックスは http://www.w3.org/TR/html4/ に、f プレフィックスは http://www.w3schools.com/furniture に関連付けられています。

Goの encoding/xml パッケージ

Goの encoding/xml パッケージは、Goの構造体とXMLドキュメントの間でデータを変換(マーシャリングとアンマーシャリング)するための機能を提供します。

  • マーシャリング (Marshalling): Goの構造体のデータをXML形式のバイト列に変換するプロセスです。xml.Marshal 関数や xml.Encoder を使用します。
  • アンマーシャリング (Unmarshalling): XML形式のバイト列をGoの構造体に変換するプロセスです。xml.Unmarshal 関数や xml.Decoder を使用します。

Goの構造体フィールドには、XMLタグとのマッピングを制御するための構造体タグ(xml:"tagname,attr" など)を付与できます。名前空間を指定するには、xml:"http://example.com/ns tagname" のようにURIを記述します。

xml.Name 構造体

encoding/xml パッケージでは、XMLの要素名や属性名を xml.Name 構造体で表現します。

type Name struct {
    Space string // 名前空間URI
    Local string // ローカル名
}

Space フィールドには名前空間のURIが格納され、Local フィールドには要素や属性のローカル名が格納されます。

技術的詳細

このコミットは、encoding/xml パッケージのXML名前空間処理における以下の具体的な技術的課題に対処しています。

  1. 同名異名前空間タグの競合解消 (Fixes #4691):

    • 問題点: 以前のバージョンでは、Goの構造体フィールドがXMLタグにマッピングされる際、xml:"tag" のように名前空間が指定されていない場合、同じローカル名を持つタグが異なる名前空間に属していても、それらを区別せずに競合とみなしてしまう可能性がありました。例えば、<ns1:table><ns2:table> が同じ構造体フィールドにマッピングされようとした場合、意図しない挙動を引き起こすことがありました。
    • 解決策: src/pkg/encoding/xml/typeinfo.gotypeInfo.add メソッドにおいて、フィールドの名前が同じであっても、明示的に異なる名前空間が指定されている場合は、それらを競合しないものとして扱うようにロジックが変更されました。これにより、ns1:tagns2:tag のような形式のXMLを正しくパースできるようになります。
  2. デフォルト名前空間のサポート (Fixes #3703):

    • 問題点: XMLでは、xmlns="http://example.com/default" のようにデフォルト名前空間が宣言されると、そのスコープ内のプレフィックスなしのタグはそのデフォルト名前空間に属します。しかし、encoding/xmlDecoder は、このデフォルト名前空間を明示的に設定する手段がなく、プレフィックスなしのタグを常に空の名前空間(no namespace)として扱っていました。
    • 解決策:
      • src/pkg/encoding/xml/xml.goDecoder.DefaultSpace フィールドが追加されました。このフィールドにURIを設定することで、プレフィックスを持たないタグが属するデフォルト名前空間を指定できるようになります。
      • Decoder.translate メソッド(XMLトークンの名前空間を解決する内部関数)が変更され、タグの名前空間が空の場合に Decoder.DefaultSpace の値を使用するように修正されました。これにより、Decoder はデフォルト名前空間を考慮してタグを正しく解決できるようになります。
  3. 属性の名前空間のエンコード/デコード (Fixes #3526):

    • 問題点: 属性も名前空間を持つことができます(例: <element xsi:type="string">)。従来の encoding/xml パッケージでは、属性の名前空間のデコード(アンマーシャリング時)とエンコード(マーシャリング時)の処理が不完全でした。特に、Goの構造体からXMLにマーシャリングする際に、属性に名前空間を正しく付与する機能が不足していました。
    • 解決策:
      • デコード側 (read.go): Decoder.unmarshal メソッド内の属性検索ロジックが変更され、属性のローカル名だけでなく、名前空間も考慮してマッチングを行うようになりました。これにより、異なる名前空間に属する同名属性を正しく区別してアンマーシャリングできるようになります。
      • エンコード側 (marshal.go): printer 構造体に seq フィールドが追加され、エンコード時に動的に名前空間プレフィックス(例: _1, _2)を生成し、xmlns:prefix="URI" 形式の名前空間宣言を属性として出力するロジックが追加されました。これにより、Goの構造体からXMLにマーシャリングする際に、属性に名前空間を正しく付与できるようになります。

これらの変更により、encoding/xml パッケージは、より複雑で標準に準拠したXMLドキュメントの名前空間を、より正確かつ柔軟に処理できるようになりました。

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

src/pkg/encoding/xml/marshal.go

--- a/src/pkg/encoding/xml/marshal.go
+++ b/src/pkg/encoding/xml/marshal.go
@@ -120,6 +120,7 @@ func (enc *Encoder) Encode(v interface{}) error {
 
 type printer struct {
 	*bufio.Writer
+	seq        int
 	indent     string
 	prefix     string
 	depth      int
@@ -210,6 +211,20 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo) error {
 			continue
 		}
 		p.WriteByte(' ')
+		if finfo.xmlns != "" {
+			p.WriteString("xmlns:")
+			p.seq++
+			id := "_" + strconv.Itoa(p.seq)
+			p.WriteString(id)
+			p.WriteString(`="`)
+			// TODO: EscapeString, to avoid the allocation.
+			if err := EscapeText(p, []byte(finfo.xmlns)); err != nil {
+				return err
+			}
+			p.WriteString(`" `)
+			p.WriteString(id)
+			p.WriteByte(':')
+		}
 		p.WriteString(finfo.name)
 		p.WriteString(`="`)
 		if err := p.marshalSimple(fv.Type(), fv); err != nil {
  • printer 構造体に seq フィールドが追加されました。これは、動的に生成される名前空間プレフィックス(例: _1, _2)のシーケンス番号を管理するために使用されます。
  • marshalValue 関数内で、フィールドに名前空間 (finfo.xmlns) が設定されている場合、xmlns:prefix="URI" 形式の名前空間宣言属性と、そのプレフィックスを使用した属性名(例: _1:table)をXML出力に追加するロジックが追加されました。

src/pkg/encoding/xml/read.go

--- a/src/pkg/encoding/xml/read.go
+++ b/src/pkg/encoding/xml/read.go
@@ -263,7 +263,7 @@ func (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {
 				strv := finfo.value(sv)
 				// Look for attribute.
 				for _, a := range start.Attr {
-					if a.Name.Local == finfo.name {
+					if a.Name.Local == finfo.name && (finfo.xmlns == "" || finfo.xmlns == a.Name.Space) {
 						copyValue(strv, []byte(a.Value))
 						break
 					}
@@ -441,7 +441,7 @@ func (p *Decoder) unmarshalPath(tinfo *typeInfo, sv reflect.Value, parents []str
 Loop:
 	for i := range tinfo.fields {
 		finfo := &tinfo.fields[i]
-		if finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) {
+		if finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) || finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
 			continue
 		}
 		for j := range parents {
  • unmarshal 関数内で、属性を検索する際に、属性のローカル名 (a.Name.Local) だけでなく、フィールドに指定された名前空間 (finfo.xmlns) と属性の名前空間 (a.Name.Space) が一致するかどうかもチェックするようになりました。これにより、名前空間を考慮した属性のアンマーシャリングが可能になります。
  • unmarshalPath 関数内で、要素を検索する際に、フィールドに名前空間が指定されており、かつその名前空間が現在の要素の名前空間 (start.Name.Space) と一致しない場合、そのフィールドをスキップする条件が追加されました。これにより、異なる名前空間を持つ同名要素の競合が解消されます。

src/pkg/encoding/xml/read_test.go

このファイルには、名前空間に関する新しいテストケースが多数追加されています。

  • Tables 構造体とそれに対応するテストデータ tables が追加され、異なる名前空間を持つ同名要素のアンマーシャリングが正しく行われることを検証しています。
  • TestUnmarshalNS 関数が追加され、Decoder.DefaultSpace を設定した場合としない場合のアンマーシャリングの挙動をテストしています。
  • TestMarshalNS 関数が追加され、名前空間を持つ要素のマーシャリングが正しく行われることを検証しています。
  • TableAttrs 構造体とそれに対応するテストデータ tableAttrs が追加され、名前空間を持つ属性のアンマーシャリングが正しく行われることを検証しています。
  • TestUnmarshalNSAttr 関数が追加され、名前空間を持つ属性のアンマーシャリングの挙動をテストしています。
  • TestMarshalNSAttr 関数が追加され、名前空間を持つ属性のマーシャリングが正しく行われることを検証しています。

src/pkg/encoding/xml/typeinfo.go

--- a/src/pkg/encoding/xml/typeinfo.go
+++ b/src/pkg/encoding/xml/typeinfo.go
@@ -267,6 +267,9 @@ Loop:
 		if oldf.flags&fMode != newf.flags&fMode {
 			continue
 		}
+		if oldf.xmlns != "" && newf.xmlns != "" && oldf.xmlns != newf.xmlns {
+			continue
+		}
 		minl := min(len(newf.parents), len(oldf.parents))\n \t\tfor p := 0; p < minl; p++ {\n \t\t\tif oldf.parents[p] != newf.parents[p] {\n```
*   `typeInfo.add` メソッド内の競合チェックロジックが変更されました。既存のフィールド (`oldf`) と新しいフィールド (`newf`) が両方とも名前空間を持ち、かつその名前空間が異なる場合 (`oldf.xmlns != newf.xmlns`) は、競合しないものとして処理を続行するようになりました。これにより、異なる名前空間を持つ同名フィールドが正しく扱われます。

### `src/pkg/encoding/xml/xml.go`

```diff
--- a/src/pkg/encoding/xml/xml.go
+++ b/src/pkg/encoding/xml/xml.go
@@ -169,6 +169,11 @@ type Decoder struct {\n 	// the CharsetReader's result values must be non-nil.\n 	CharsetReader func(charset string, input io.Reader) (io.Reader, error)\n \n+\t// DefaultSpace sets the default name space used for unadorned tags,\n+\t// as if the entire XML stream were wrapped in an element containing\n+\t// the attribute xmlns="DefaultSpace".\n+\tDefaultSpace string\n+\n \tr         io.ByteReader\n \tbuf       bytes.Buffer\n \tsaved     *bytes.Buffer\n@@ -282,6 +287,8 @@ func (d *Decoder) translate(n *Name, isElementName bool) {\n \t}\n \tif v, ok := d.ns[n.Space]; ok {\n \t\tn.Space = v\n+\t} else if n.Space == "" {\n+\t\tn.Space = d.DefaultSpace\n \t}\n }\n \n```
*   `Decoder` 構造体に `DefaultSpace` フィールドが追加されました。これは、プレフィックスを持たないタグのデフォルト名前空間URIを設定するために使用されます。
*   `translate` 関数内で、XML要素または属性の名前空間 (`n.Space`) が空の場合に、`Decoder.DefaultSpace` の値を使用するように変更されました。これにより、デフォルト名前空間が正しく適用されるようになります。

## コアとなるコードの解説

### `src/pkg/encoding/xml/marshal.go` の変更

この変更は、Goの構造体をXMLにマーシャリングする際に、属性に名前空間を正しく付与するためのものです。

*   **`printer` 構造体への `seq` フィールド追加**:
    ```go
    type printer struct {
    	*bufio.Writer
    	seq        int // 新しく追加されたフィールド
    	indent     string
    	prefix     string
    	depth      int
    	// ...
    }
    ```
    `seq` は、XML出力時に動的に生成される名前空間プレフィックス(例: `_1`, `_2`)が一意になるようにするためのカウンターです。

*   **`marshalValue` 関数内の名前空間属性生成ロジック**:
    ```go
    		if finfo.xmlns != "" { // フィールドに名前空間が指定されている場合
    			p.WriteString("xmlns:") // xmlns: を書き込む
    			p.seq++ // シーケンス番号をインクリメント
    			id := "_" + strconv.Itoa(p.seq) // 一意のプレフィックス(例: _1)を生成
    			p.WriteString(id) // 生成したプレフィックスを書き込む
    			p.WriteString(`="`) // =" を書き込む
    			// ... finfo.xmlns の値をエスケープして書き込む ...
    			p.WriteString(`" `) // " とスペースを書き込む
    			p.WriteString(id) // 生成したプレフィックスを再度書き込む
    			p.WriteByte(':') // : を書き込む
    		}
    		p.WriteString(finfo.name) // フィールド名を書き込む
    		p.WriteString(`="`) // =" を書き込む
    		// ...
    ```
    このコードブロックは、Goの構造体フィールドがXML属性としてマーシャリングされる際に実行されます。もしそのフィールドが `xml:"http://example.com/ns attrname,attr"` のように名前空間を持つ属性としてタグ付けされている場合、`xmlns:prefix="URI"` 形式の名前空間宣言属性がXML要素に追加されます。さらに、実際の属性名も `prefix:attrname` の形式で出力され、名前空間が正しく関連付けられます。これにより、生成されるXMLが名前空間のセマンティクスに準拠するようになります。

### `src/pkg/encoding/xml/read.go` の変更

これらの変更は、XMLをGoの構造体にアンマーシャリングする際に、名前空間を考慮して要素や属性を正しくマッピングするためのものです。

*   **`unmarshal` 関数内の属性検索ロジック**:
    ```go
    				for _, a := range start.Attr {
    					if a.Name.Local == finfo.name && (finfo.xmlns == "" || finfo.xmlns == a.Name.Space) {
    						copyValue(strv, []byte(a.Value))
    						break
    					}
    				}
    ```
    以前は `a.Name.Local == finfo.name` のみで属性を検索していましたが、変更後は `(finfo.xmlns == "" || finfo.xmlns == a.Name.Space)` という条件が追加されました。これは、Goの構造体フィールドに名前空間が指定されている場合 (`finfo.xmlns != ""`)、XML属性の名前空間 (`a.Name.Space`) がその名前空間と一致する場合にのみマッチングを行うことを意味します。これにより、同じローカル名を持つが異なる名前空間に属する属性が混在していても、正しい属性が選択されるようになります。

*   **`unmarshalPath` 関数内の要素検索ロジック**:
    ```go
    	for i := range tinfo.fields {
    		finfo := &tinfo.fields[i]
    		if finfo.flags&fElement == 0 || len(finfo.parents) < len(parents) || finfo.xmlns != "" && finfo.xmlns != start.Name.Space {
    			continue
    		}
    		// ...
    	}
    ```
    この変更は、XML要素をGoの構造体フィールドにマッピングする際に、名前空間を考慮するためのものです。`finfo.xmlns != "" && finfo.xmlns != start.Name.Space` という条件が追加されました。これは、Goの構造体フィールドに名前空間が指定されており (`finfo.xmlns != ""`)、かつ現在のXML要素の名前空間 (`start.Name.Space`) がその名前空間と一致しない場合、そのフィールドは現在のXML要素にはマッピングされないことを意味します。これにより、異なる名前空間を持つ同名要素が正しく区別され、意図しないマッピングが防がれます。

### `src/pkg/encoding/xml/typeinfo.go` の変更

この変更は、Goの構造体フィールドの型情報を処理する際に、異なる名前空間を持つ同名フィールドを競合とみなさないようにするためのものです。

```go
		if oldf.flags&fMode != newf.flags&fMode {
			continue
		}
		if oldf.xmlns != "" && newf.xmlns != "" && oldf.xmlns != newf.xmlns {
			continue
		}

typeInfo.add メソッドは、Goの構造体からXMLへのマッピング情報を構築する際に、フィールドの競合をチェックします。追加された if 文は、既存のフィールド (oldf) と新しいフィールド (newf) が両方とも名前空間を持ち (xmlns != "")、かつその名前空間が異なる場合 (oldf.xmlns != newf.xmlns)、それらを競合とはみなさずに処理を続行するように指示します。これにより、xml:"http://ns1/ tag"xml:"http://ns2/ tag" のように、同じローカル名だが異なる名前空間を持つフィールドがGoの構造体内に共存できるようになります。

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

この変更は、xml.Decoder がデフォルト名前空間を認識し、プレフィックスを持たないタグを正しく処理できるようにするためのものです。

  • Decoder 構造体への DefaultSpace フィールド追加:

    type Decoder struct {
        // ...
        DefaultSpace string // 新しく追加されたフィールド
        // ...
    }
    

    DefaultSpace は、XMLドキュメント内でプレフィックスを持たないタグが属するデフォルト名前空間のURIを保持します。ユーザーはこのフィールドに値を設定することで、デコード時のデフォルト名前空間の挙動を制御できます。

  • translate 関数内のデフォルト名前空間適用ロジック:

    	if v, ok := d.ns[n.Space]; ok {
    		n.Space = v
    	} else if n.Space == "" { // 名前空間が空の場合
    		n.Space = d.DefaultSpace // DefaultSpace の値を適用
    	}
    

    translate 関数は、XMLトークン(要素名や属性名)の名前空間を解決する内部ヘルパー関数です。この変更により、もしXMLトークンの名前空間 (n.Space) が空文字列(つまり、プレフィックスなしのタグ)である場合、Decoder.DefaultSpace に設定された値がその名前空間として使用されるようになります。これにより、encoding/xml はデフォルト名前空間を持つXMLドキュメントを正しく解釈できるようになります。

これらの変更は、encoding/xml パッケージがXML名前空間をより堅牢かつ正確に処理するための基盤を強化し、Go言語でXMLを扱うアプリケーションの互換性と信頼性を向上させます。

関連リンク

参考にした情報源リンク