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

[インデックス 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 パッケージの基本的な知識が必要です。

  1. Go言語の reflect パッケージ:

    • reflect パッケージは、Goプログラムが実行時に自身の構造を検査し、操作するための機能を提供します。
    • reflect.Value: Goの任意の型の値を表す型です。これを通じて、値の型、フィールド、メソッドなどにアクセスしたり、値を設定したりできます。
    • reflect.Type: Goの任意の型の型情報を表す型です。
    • encoding/xml のようなマーシャリング/アンマーシャリングライブラリは、reflect を利用して、Goの構造体のフィールドとXML要素/属性を動的にマッピングします。
  2. 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 パッケージが多用されます。
  3. 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 = vif 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 ブロックのに移動されました。

この変更により、以下のようになります。

  1. まず、現在の reflect.Value (v) の型が xml.Name 型であるかどうかがチェックされます。
  2. もし xml.Name 型であれば、v.Set(reflect.ValueOf(start.Name)) によって要素名がフィールドに設定され、すぐに break されます。この際、sv = v は実行されないため、svxml.Name フィールドの reflect.Value を保持しません。
  3. xml.Name 型でなければ、通常の構造体フィールドの処理に進むために sv = v が実行され、その後のロジックで子要素などが適切に処理されます。

この修正により、xml.Name 型のフィールドがデコードされる際には、そのフィールドには要素名のみが割り当てられ、子要素の処理は完全にスキップされるようになりました。これにより、xml.Name フィールドが子要素を持つXML要素に対応する場合でも、デコードが正しく行われるようになります。

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

変更は主に2つのファイルで行われています。

  1. 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 フィールドが正しくデコードされることを保証します。
  2. 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.goDecoder.unmarshal メソッドは、XMLデコードの核心部分です。このメソッドは、Goの構造体のフィールドの型に基づいて、XML要素の値をそのフィールドに割り当てます。

case reflect.Struct: ブロックは、デコード対象のGoのフィールドが構造体型である場合に実行されます。この構造体フィールドが xml.Name 型である場合、特別な処理が必要です。xml.Name はXML要素の「名前」を表すだけであり、その要素が持つ子要素の内容を保持するべきではありません。

変更前のコードでは、sv = vif typ == nameType のチェックの前にありました。sv は、現在のデコード対象の reflect.Value を保持し、その後の構造体フィールドの再帰的な処理に使用されます。xml.Name 型のフィールドの場合、v.Set(reflect.ValueOf(start.Name)) で要素名が設定された後、breakswitch 文を抜けます。しかし、sv には xml.Name フィールドの reflect.Value が残ったままになるため、unmarshal メソッドのさらに後続のロジックが、誤って xml.Name フィールドに対して子要素のデコードを試みていました。

変更後のコードでは、sv = vif typ == nameType ブロックのに移動されました。これにより、もしフィールドが xml.Name 型であれば、要素名が設定された後すぐに break され、sv = v は実行されません。結果として、svxml.Name フィールドの reflect.Value を保持しないため、後続の子要素処理ロジックが xml.Name フィールドに対して誤って実行されることがなくなります。

この修正は、encoding/xml デコーダが xml.Name 型のフィールドを、そのセマンティクス(XML要素の名前のみを保持する)に従って正しく処理することを保証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語の encoding/xml および reflect パッケージのソースコード
  • コミットメッセージと差分情報
  • Goのコードレビューシステム (Gerrit) の関連する議論 (もし公開されていれば)
  • XMLの基本概念に関する一般的な知識
  • Goの reflect パッケージを用いた動的な型操作に関する一般的な知識