[インデックス 11473] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/xml
パッケージにおける、xml.Name
型のデコードに関するバグ修正です。具体的には、XML要素が xml.Name
型のフィールドにマッピングされる際に、そのXML要素が子要素を持っている場合にデコードが正しく行われない問題を解決します。
コミット
commit 9c497443ae0aca6ae5b66dfa6d3127c8605bd3a8
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date: Mon Jan 30 16:32:48 2012 -0200
encoding/xml: fix decoding of xml.Name with sub-elements
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5569090
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9c497443ae0aca6ae5b66dfa6d3127c8605bd3a8
元コミット内容
diff --git a/src/pkg/encoding/xml/marshal_test.go b/src/pkg/encoding/xml/marshal_test.go
index e0be332008..0f6c0f0795 100644
--- a/src/pkg/encoding/xml/marshal_test.go
+++ b/src/pkg/encoding/xml/marshal_test.go
@@ -532,6 +532,11 @@ var marshalTests = []struct {
\t\tValue: &NameInField{Name{Space: "ns", Local: "foo"}},\
\t\tExpectXML: `<NameInField><foo xmlns="ns"></foo></NameInField>`,\
\t},\
+\t{\
+\t\tValue: &NameInField{Name{Space: "ns", Local: "foo"}},\
+\t\tExpectXML: `<NameInField><foo xmlns="ns"><ignore></ignore></foo></NameInField>`,\
+\t\tUnmarshalOnly: true,\
+\t},\
\
// Marshaling zero xml.Name uses the tag or field name.\
\t{\
diff --git a/src/pkg/encoding/xml/read.go b/src/pkg/encoding/xml/read.go
index 871fe059cf..3193cda792 100644
--- a/src/pkg/encoding/xml/read.go
+++ b/src/pkg/encoding/xml/read.go
@@ -265,12 +265,13 @@ func (p *Decoder) unmarshal(val reflect.Value, start *StartElement) error {\
\t\tsaveData = v
\
\tcase reflect.Struct:\
-\t\tsv = v
-\t\ttyp := sv.Type()\
+\t\ttyp := v.Type()\
\t\tif typ == nameType {\
\t\t\tv.Set(reflect.ValueOf(start.Name))\
\t\t\tbreak\
\t\t}\
+\
+\t\tsv = v
\t\ttinfo, err = getTypeInfo(typ)\
\t\tif err != nil {\
\t\t\treturn err
変更の背景
Go言語の encoding/xml
パッケージは、XMLドキュメントとGoの構造体(struct)の間でデータを相互に変換するための機能を提供します。このパッケージは、XMLの要素名や属性をGoの構造体のフィールドにマッピングする際に、Goの reflect
パッケージを内部的に利用しています。
xml.Name
型は、XML要素のローカル名と名前空間を表現するために使用されます。Goの構造体の中に xml.Name
型のフィールドがある場合、encoding/xml
デコーダは、対応するXML要素のタグ名をそのフィールドにデコードします。
このコミット以前には、以下のような特定のシナリオで問題が発生していました。
Goの構造体のあるフィールドが xml.Name
型であり、かつ、その xml.Name
に対応するXML要素が、さらに子要素を持っている場合、デコーダが子要素を不適切に処理しようとすることがありました。具体的には、xml.Name
型のフィールドはXML要素の「名前」のみを保持すべきであり、その子要素の内容は保持すべきではありません。しかし、バグのある実装では、子要素の処理が xml.Name
フィールドの reflect.Value
に対して行われ、予期せぬエラーや不正なデコード結果を引き起こしていました。
このバグは、XML構造が複雑になり、特に xml.Name
フィールドが単なる空要素ではなく、子要素を持つ場合に顕在化しました。この修正は、encoding/xml
デコーダが xml.Name
型のフィールドを正しく扱い、その子要素を無視して、フィールドには要素名のみを割り当てるようにするために行われました。
前提知識の解説
このコミットの理解には、以下のGo言語の概念と encoding/xml
パッケージの基本的な知識が必要です。
-
Go言語の
reflect
パッケージ:reflect
パッケージは、Goプログラムが実行時に自身の構造を検査し、操作するための機能を提供します。reflect.Value
: Goの任意の型の値を表す型です。これを通じて、値の型、フィールド、メソッドなどにアクセスしたり、値を設定したりできます。reflect.Type
: Goの任意の型の型情報を表す型です。encoding/xml
のようなマーシャリング/アンマーシャリングライブラリは、reflect
を利用して、Goの構造体のフィールドとXML要素/属性を動的にマッピングします。
-
encoding/xml
パッケージ:- Goの標準ライブラリの一部で、XMLドキュメントのエンコード(Goの構造体からXMLへ)とデコード(XMLからGoの構造体へ)を提供します。
xml.Decoder
: XMLストリームを読み込み、Goの構造体にデコードするための型です。xml.StartElement
: XMLデコード中に遭遇する開始タグ(例:<tag>
) を表す構造体です。これには要素名(xml.Name
型)や属性が含まれます。xml.Name
: XML要素の修飾名(Qualified Name)を表す構造体です。Space
(名前空間URI)とLocal
(ローカル名)の2つのフィールドを持ちます。- アンマーシャリングの仕組み:
xml.Decoder
はXMLストリームを読み込み、Goの構造体のフィールドタグ(例:xml:"element_name"
)やフィールド名に基づいて、対応するXML要素の値を構造体のフィールドに割り当てます。このプロセスでreflect
パッケージが多用されます。
-
XMLの構造:
- XMLはツリー構造を持つマークアップ言語です。要素は子要素を持つことができます。
- 例:
<parent><child1/><child2/></parent>
の場合、<parent>
は<child1>
と<child2>
という子要素を持ちます。
このコミットの修正は、encoding/xml
デコーダが reflect
を使って構造体のフィールドを処理する際の、xml.Name
型のフィールドに対する特殊なケースのハンドリングに関するものです。
技術的詳細
この修正は、encoding/xml
パッケージの read.go
ファイルにある Decoder.unmarshal
メソッド内のロジック変更に焦点を当てています。unmarshal
メソッドは、XML要素をGoの構造体のフィールドにデコードする主要な役割を担っています。
問題の箇所は、reflect.Struct
型のフィールドを処理する switch
文の case
ブロック内にありました。
変更前のコードの挙動(問題点):
case reflect.Struct:
sv = v // ここでsvに現在のreflect.Value (xml.NameフィールドのValue) が代入される
typ := sv.Type()
if typ == nameType { // フィールドがxml.Name型であるかチェック
v.Set(reflect.ValueOf(start.Name)) // xml.Nameフィールドに要素名をセット
break // switch文を抜ける
}
// ... その他の構造体フィールドの処理 ...
このコードでは、reflect.Struct
型のフィールド(この場合は xml.Name
型のフィールド)を処理する際に、まず sv = v
が実行されます。ここで sv
には、現在デコード対象となっている xml.Name
型のフィールドの reflect.Value
が代入されます。
その後、if typ == nameType
の条件が真(つまり、フィールドが xml.Name
型である)の場合、v.Set(reflect.ValueOf(start.Name))
によってXML要素の名前が xml.Name
フィールドに正しく設定されます。そして break
によって switch
文を抜けます。
しかし、問題は sv = v
が if typ == nameType
のチェックの前に実行されている点にありました。もし xml.Name
型のフィールドに対応するXML要素が子要素を持っていた場合、break
によって switch
文を抜けた後、unmarshal
メソッドのさらに後続のロジック(このコミットの差分には含まれていませんが、sv
を利用して子要素を再帰的に処理する部分)が、誤って xml.Name
フィールドの reflect.Value
(sv
) を使って子要素をデコードしようとしていました。xml.Name
型は子要素を保持するようには設計されていないため、これは不正な操作となり、デコードエラーや予期せぬ動作を引き起こしていました。
変更後のコードの挙動(修正点):
case reflect.Struct:
typ := v.Type() // まず型をチェック
if typ == nameType { // フィールドがxml.Name型であるかチェック
v.Set(reflect.ValueOf(start.Name)) // xml.Nameフィールドに要素名をセット
break // switch文を抜ける
}
// xml.Name型でなければ、svに現在のreflect.Valueを代入し、通常の構造体フィールド処理へ進む
sv = v
// ... その他の構造体フィールドの処理 ...
修正では、sv = v
の行が if typ == nameType
ブロックの後に移動されました。
この変更により、以下のようになります。
- まず、現在の
reflect.Value
(v
) の型がxml.Name
型であるかどうかがチェックされます。 - もし
xml.Name
型であれば、v.Set(reflect.ValueOf(start.Name))
によって要素名がフィールドに設定され、すぐにbreak
されます。この際、sv = v
は実行されないため、sv
はxml.Name
フィールドのreflect.Value
を保持しません。 xml.Name
型でなければ、通常の構造体フィールドの処理に進むためにsv = v
が実行され、その後のロジックで子要素などが適切に処理されます。
この修正により、xml.Name
型のフィールドがデコードされる際には、そのフィールドには要素名のみが割り当てられ、子要素の処理は完全にスキップされるようになりました。これにより、xml.Name
フィールドが子要素を持つXML要素に対応する場合でも、デコードが正しく行われるようになります。
コアとなるコードの変更箇所
変更は主に2つのファイルで行われています。
-
src/pkg/encoding/xml/marshal_test.go
:- 新しいテストケースが追加されました。このテストケースは、
xml.Name
型のフィールドを持つ構造体が、子要素を持つXML要素からデコードされるシナリオを検証します。 UnmarshalOnly: true
が設定されており、マーシャリング(GoからXMLへの変換)ではなく、アンマーシャリング(XMLからGoへの変換)のみをテスト対象としていることを示します。ExpectXML
には、<foo xmlns="ns"><ignore></ignore></foo>
のように、xml.Name
に対応する<foo>
要素が<ignore>
という子要素を持つXMLが記述されています。このテストは、この子要素が存在してもxml.Name
フィールドが正しくデコードされることを保証します。
- 新しいテストケースが追加されました。このテストケースは、
-
src/pkg/encoding/xml/read.go
:Decoder.unmarshal
メソッド内のreflect.Struct
を処理するcase
ブロックで、sv = v
の行が移動されました。- 変更前:
case reflect.Struct: sv = v typ := sv.Type() if typ == nameType { v.Set(reflect.ValueOf(start.Name)) break }
- 変更後:
case reflect.Struct: typ := v.Type() if typ == nameType { v.Set(reflect.ValueOf(start.Name)) break } sv = v // この行が移動した
コアとなるコードの解説
src/pkg/encoding/xml/read.go
の Decoder.unmarshal
メソッドは、XMLデコードの核心部分です。このメソッドは、Goの構造体のフィールドの型に基づいて、XML要素の値をそのフィールドに割り当てます。
case reflect.Struct:
ブロックは、デコード対象のGoのフィールドが構造体型である場合に実行されます。この構造体フィールドが xml.Name
型である場合、特別な処理が必要です。xml.Name
はXML要素の「名前」を表すだけであり、その要素が持つ子要素の内容を保持するべきではありません。
変更前のコードでは、sv = v
が if typ == nameType
のチェックの前にありました。sv
は、現在のデコード対象の reflect.Value
を保持し、その後の構造体フィールドの再帰的な処理に使用されます。xml.Name
型のフィールドの場合、v.Set(reflect.ValueOf(start.Name))
で要素名が設定された後、break
で switch
文を抜けます。しかし、sv
には xml.Name
フィールドの reflect.Value
が残ったままになるため、unmarshal
メソッドのさらに後続のロジックが、誤って xml.Name
フィールドに対して子要素のデコードを試みていました。
変更後のコードでは、sv = v
が if typ == nameType
ブロックの後に移動されました。これにより、もしフィールドが xml.Name
型であれば、要素名が設定された後すぐに break
され、sv = v
は実行されません。結果として、sv
は xml.Name
フィールドの reflect.Value
を保持しないため、後続の子要素処理ロジックが xml.Name
フィールドに対して誤って実行されることがなくなります。
この修正は、encoding/xml
デコーダが xml.Name
型のフィールドを、そのセマンティクス(XML要素の名前のみを保持する)に従って正しく処理することを保証します。
関連リンク
- Go言語の
encoding/xml
パッケージのドキュメント: https://pkg.go.dev/encoding/xml - Go言語の
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5569090 (コミットメッセージに記載されているリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語の
encoding/xml
およびreflect
パッケージのソースコード - コミットメッセージと差分情報
- Goのコードレビューシステム (Gerrit) の関連する議論 (もし公開されていれば)
- XMLの基本概念に関する一般的な知識
- Goの
reflect
パッケージを用いた動的な型操作に関する一般的な知識