[インデックス 10526] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template パッケージにおけるいくつかの重要な修正と機能追加を扱っています。特に、html/template パッケージとの連携を改善し、テンプレートの初期化状態やエラーハンドリングに関する問題を解決することを目的としています。
コミット
commit 5f6027e9ad9a6f115399a93c5d330cbf2d66e85f
Author: Rob Pike <r@golang.org>
Date: Mon Nov 28 10:42:57 2011 -0800
text/template: address a couple of issues for html/template
- allow Lookup to work on uninitialized templates
- fix bug in add: can't error after parser is stopped
- add Add method for html/template
R=adg, rogpeppe, r, rsc
CC=golang-dev
https://golang.org/cl/5436080
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5f6027e9ad9a6f115399a93c5d330cbf2d66e85f
元コミット内容
このコミットは、text/template パッケージに対して以下の3つの主要な変更を加えています。
- 未初期化テンプレートに対する
Lookupメソッドの動作改善:Templateオブジェクトが適切に初期化されていない場合でも、Lookupメソッドが安全に動作するように修正されました。これにより、nilポインタ参照によるパニックを防ぎます。 add処理におけるバグ修正: テンプレートのパース処理中にadd操作が行われる際、パーサーが停止した後にエラーが発生する可能性があったバグが修正されました。これは、t.add(treeSet)の呼び出し順序を変更することで対応されています。html/templateのためのAddメソッドの追加:html/templateパッケージがtext/templateの機能を拡張して利用する際に、複数のテンプレートを関連付け、相互に呼び出せるようにするためのAddメソッドがTemplate型に追加されました。
変更の背景
Go言語の text/template パッケージは、テキストベースの出力を生成するための汎用的なテンプレートエンジンを提供します。一方、html/template パッケージは、text/template を基盤としつつ、HTML出力におけるクロスサイトスクリプティング (XSS) などのセキュリティ脆弱性を自動的にエスケープする機能を追加したものです。
このコミットが行われた2011年当時、html/template はまだ比較的新しいパッケージであり、text/template との連携においていくつかの課題を抱えていました。具体的には、以下のような問題が考えられます。
- テンプレートのライフサイクルと状態管理: テンプレートがどのように初期化され、他のテンプレートと関連付けられるかという点で、
text/templateの既存の設計ではhtml/templateの要件を完全に満たせていなかった可能性があります。特に、未初期化状態のテンプレートに対する操作は、予期せぬパニックを引き起こす原因となります。 - エラーハンドリングの厳密性: テンプレートのパース処理は複雑であり、途中でエラーが発生した場合の挙動は非常に重要です。パーサーが「停止」したと判断された後にエラーが発生すると、そのエラーが適切に伝播されず、デバッグが困難になる可能性があります。
html/templateの特殊な要件:html/templateは、セキュリティ上の理由から、複数のテンプレートを組み合わせて利用する際に、それらのテンプレートが互いに安全に参照し合えるメカニズムを必要とします。既存のtext/templateには、このような「相互参照」を明示的に管理する高レベルなAPIが不足していたと考えられます。
これらの背景から、html/template の堅牢性と使いやすさを向上させるために、text/template 側に基盤となる修正と機能追加が必要とされました。
前提知識の解説
このコミットを理解するためには、以下のGo言語のテンプレートパッケージに関する知識が役立ちます。
text/templateパッケージ:- Go言語の標準ライブラリで提供される、テキストベースのテンプレートエンジン。
{{.Field}}のようなアクションを使って、データ構造のフィールドやメソッドをテンプレートに埋め込むことができます。{{range .Slice}}...{{end}}や{{if .Condition}}...{{end}}のような制御構造もサポートしています。Template型は、個々のテンプレートを表し、Parseメソッドでテンプレート文字列を解析します。- 複数のテンプレートを名前で管理し、
Lookupメソッドで取得したり、Executeメソッドで実行したりできます。 - 内部的には、テンプレート文字列を字句解析(lexing)し、構文解析(parsing)して抽象構文木(AST: Abstract Syntax Tree)を構築します。
html/templateパッケージ:text/templateと同じAPIを提供しますが、HTML出力に特化しており、自動エスケープ機能(コンテキストに応じたエスケープ処理)を備えています。- これにより、ユーザー入力などをテンプレートに埋め込む際に、意図しないHTMLタグやJavaScriptコードが挿入されることによるXSS攻撃を防ぎます。
text/templateのTemplate型を内部的に利用し、その上にセキュリティ層を追加しています。
Template型のcommonフィールド:text/templateパッケージのTemplate型には、commonという内部フィールドが存在します。これは、複数の関連するテンプレート間で共有される状態(例えば、名前付きテンプレートのマップtmplや関数マップfuncMapなど)を保持するためのものです。- テンプレートが初期化されると、この
commonフィールドが設定され、テンプレートが「使える状態」になります。
parse.Treeとlex/parse/add/stopParse:text/templateの内部では、parseサブパッケージがテンプレートの構文解析を担当します。lexはテンプレート文字列をトークンに分割する字句解析器です。parseはトークンストリームからASTを構築します。Tree型は、解析されたテンプレートのASTを表します。t.add(treeSet)は、解析されたテンプレート(t)を、関連するテンプレートのセット(treeSet)に追加する操作です。これにより、他のテンプレートから名前で参照できるようになります。t.stopParse()は、パース処理の終了をマークする内部的な操作です。
技術的詳細
このコミットは、text/template パッケージの2つのファイル、src/pkg/text/template/parse/parse.go と src/pkg/text/template/template.go に変更を加えています。
src/pkg/text/template/parse/parse.go の変更
このファイルでは、Tree 型の Parse メソッド内の処理順序が変更されています。
変更前:
func (t *Tree) Parse(...) (_ *Tree, err error) {
defer t.recover(&err)
t.startParse(...)
t.parse(treeSet)
t.stopParse() // ここでパーサーが停止
t.add(treeSet) // その後でテンプレートを追加
return t, nil
}
変更後:
func (t *Tree) Parse(...) (_ *Tree, err error) {
defer t.recover(&err)
t.startParse(...)
t.parse(treeSet)
t.add(treeSet) // テンプレートを追加
t.stopParse() // その後でパーサーが停止
return t, nil
}
この変更の目的は、コミットメッセージにある「fix bug in add: can't error after parser is stopped」を解決することです。t.add(treeSet) は、解析されたテンプレートをテンプレートセットに追加する操作であり、この操作中にエラー(例えば、同じ名前のテンプレートが既に存在する場合など)が発生する可能性があります。もし t.stopParse() が先に呼び出されてしまうと、パーサーが既に「停止」状態にあるため、add で発生したエラーが適切に処理されない、あるいは無視されてしまう可能性がありました。t.add(treeSet) を t.stopParse() の前に移動することで、add 処理中に発生したエラーが defer t.recover(&err) によって捕捉され、適切に err 変数に設定されるようになります。これにより、テンプレートのパースと追加のプロセスがより堅牢になります。
src/pkg/text/template/template.go の変更
このファイルでは、Template 型に新しい Add メソッドが追加され、既存の Lookup メソッドが修正されています。
-
Addメソッドの追加:// Add associates the argument template, arg, with t, and vice versa, // so they may invoke each other. To do this, it also removes any // prior associations arg may have. Except for losing the link to // arg, templates associated with arg are otherwise unaffected. It // is an error if the argument template's name is already associated // with t. Add is here to support html/template and is not intended // for other uses. // TODO: make this take a parse.Tree argument instead of a template. func (t *Template) Add(arg *Template) error { if t.tmpl[arg.name] != nil { return fmt.Errorf("template: redefinition of template %q", arg.name) } arg.common = t.common t.tmpl[arg.name] = arg return nil }- このメソッドは、引数として渡された
argテンプレートを、レシーバーであるtテンプレートに関連付けます。これにより、tからargを名前で参照できるようになります。 t.tmpl[arg.name] != nilのチェックにより、同じ名前のテンプレートが既に存在しないことを確認し、再定義エラーを防ぎます。arg.common = t.commonは非常に重要です。これにより、argテンプレートがtテンプレートと同じ共有状態(commonフィールド)を持つようになります。これは、html/templateが複数のテンプレートを安全に連携させるために必要とするメカニズムの一部です。例えば、html/templateでは、あるテンプレートが別のテンプレートを{{template "name"}}のように呼び出す際に、両者が同じセキュリティコンテキストを共有している必要があります。commonフィールドを共有することで、この要件が満たされます。- コメントに「Add is here to support html/template and is not intended for other uses.」と明記されており、このメソッドが
html/templateの特定のニーズのために導入されたことがわかります。 TODO: make this take a parse.Tree argument instead of a template.というコメントは、将来的にはTemplateオブジェクトそのものではなく、より低レベルなparse.Treeを引数として受け取るように変更される可能性があることを示唆しています。これは、AddメソッドがテンプレートのASTレベルでの操作をより直接的に反映するように設計されるべきだという考えに基づいているかもしれません。
- このメソッドは、引数として渡された
-
Lookupメソッドの修正:func (t *Template) Lookup(name string) *Template { if t.common == nil { // 新しく追加されたチェック return nil } return t.tmpl[name] }Lookupメソッドは、指定された名前のテンプレートを検索して返します。- 追加された
if t.common == nil { return nil }のチェックは、コミットメッセージにある「allow Lookup to work on uninitialized templates」に対応します。 Templateオブジェクトがまだ初期化されておらず、commonフィールドがnilの場合、t.tmplにアクセスしようとするとパニックが発生します。このチェックにより、未初期化のテンプレートに対してLookupが呼び出された場合でも、安全にnilを返すようになります。これにより、呼び出し元はnilチェックを行うことで、テンプレートが利用可能かどうかを判断できるようになります。
コアとなるコードの変更箇所
src/pkg/text/template/parse/parse.go
--- a/src/pkg/text/template/parse/parse.go
+++ b/src/pkg/text/template/parse/parse.go
@@ -170,8 +170,8 @@ func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree,
defer t.recover(&err)
t.startParse(funcs, lex(t.Name, s, leftDelim, rightDelim))
t.parse(treeSet)
- t.stopParse()
t.add(treeSet)
+ t.stopParse()
return t, nil
}
src/pkg/text/template/template.go
--- a/src/pkg/text/template/template.go
+++ b/src/pkg/text/template/template.go
@@ -103,6 +103,23 @@ func (t *Template) copy(c *common) *Template {
return nt
}
+// Add associates the argument template, arg, with t, and vice versa,
+// so they may invoke each other. To do this, it also removes any
+// prior associations arg may have. Except for losing the link to
+// arg, templates associated with arg are otherwise unaffected. It
+// is an error if the argument template's name is already associated
+// with t. Add is here to support html/template and is not intended
+// for other uses.
+// TODO: make this take a parse.Tree argument instead of a template.
+func (t *Template) Add(arg *Template) error {
+ if t.tmpl[arg.name] != nil {
+ return fmt.Errorf("template: redefinition of template %q", arg.name)
+ }
+ arg.common = t.common
+ t.tmpl[arg.name] = arg
+ return nil
+}
+
// Templates returns a slice of the templates associated with t, including t
// itself.
func (t *Template) Templates() []*Template {
@@ -139,6 +156,9 @@ func (t *Template) Funcs(funcMap FuncMap) *Template {\n // Lookup returns the template with the given name that is associated with t,\n // or nil if there is no such template.\n func (t *Template) Lookup(name string) *Template {\n+\tif t.common == nil {\n+\t\treturn nil\n+\t}\n return t.tmpl[name]\n }\
\n```
## コアとなるコードの解説
### `src/pkg/text/template/parse/parse.go` の変更点
`Parse` メソッドは、テンプレート文字列を解析し、`Tree` オブジェクトを構築する主要な関数です。この変更は、`t.add(treeSet)` と `t.stopParse()` の呼び出し順序を入れ替えることで、エラーハンドリングのタイミングを修正しています。
* **`t.add(treeSet)`**: この関数は、現在解析中のテンプレート(`t`)を、そのテンプレートが属するテンプレートセット(`treeSet`)に追加します。これにより、他のテンプレートからこのテンプレートを名前で参照できるようになります。この操作中に、例えば同じ名前のテンプレートが既に存在する場合など、エラーが発生する可能性があります。
* **`t.stopParse()`**: この関数は、パース処理が完了したことを内部的にマークします。通常、パース処理が停止した後は、新たなエラーを生成したり、既存のエラーを伝播させたりするメカニズムが閉じられることがあります。
変更前は、`t.stopParse()` が先に呼び出されていたため、`t.add(treeSet)` でエラーが発生しても、そのエラーが `Parse` メソッドの戻り値として適切に返されない可能性がありました。変更後は、`t.add(treeSet)` が `t.stopParse()` の前に実行されるため、`add` 処理中に発生したエラーは `defer t.recover(&err)` によって捕捉され、`Parse` メソッドの呼び出し元に正確に伝達されるようになります。これは、テンプレートのパースと登録のプロセスにおける堅牢性を高めるための重要な修正です。
### `src/pkg/text/template/template.go` の変更点
#### `Add` メソッド
`Add` メソッドは、`Template` 型に新しく追加された公開メソッドです。
```go
func (t *Template) Add(arg *Template) error {
if t.tmpl[arg.name] != nil {
return fmt.Errorf("template: redefinition of template %q", arg.name)
}
arg.common = t.common
t.tmpl[arg.name] = arg
return nil
}
if t.tmpl[arg.name] != nil: この行は、argテンプレートの名前が、レシーバーtが管理するテンプレートマップt.tmpl内で既に使われていないかを確認します。もし既に存在すれば、fmt.Errorfを使って「テンプレートの再定義」エラーを返します。これは、テンプレートの名前空間の整合性を保つために重要です。arg.common = t.common: この行がAddメソッドの最も重要な部分です。argテンプレートのcommonフィールドを、レシーバーtのcommonフィールドと同じものに設定します。前述の通り、commonフィールドは複数のテンプレート間で共有される状態(テンプレートマップ、関数マップなど)を保持します。この操作により、argテンプレートはtテンプレートと同じコンテキストを共有するようになり、特にhtml/templateにおいて、セキュリティコンテキストの継承や、関連するテンプレート間の安全な相互呼び出しが可能になります。t.tmpl[arg.name] = arg: 最後に、argテンプレートをtのテンプレートマップにその名前で登録します。これにより、tを通じてargをLookupできるようになります。
この Add メソッドは、html/template が複数のテンプレートを組み合わせて、それらが互いに安全に参照し合えるようにするための基盤を提供します。
Lookup メソッドの修正
Lookup メソッドは、指定された名前のテンプレートを検索して返します。
func (t *Template) Lookup(name string) *Template {
if t.common == nil {
return nil
}
return t.tmpl[name]
}
if t.common == nil: この条件が新しく追加されました。t.commonがnilであるということは、tテンプレートがまだ適切に初期化されていない状態であることを意味します。return nil: 未初期化のテンプレートに対してLookupが呼び出された場合、以前はt.tmpl[name]にアクセスしようとしてパニックを引き起こす可能性がありました。この修正により、commonがnilであれば即座にnilを返すことで、安全に未初期化状態を処理し、パニックを防ぎます。呼び出し元はLookupの戻り値がnilであるかどうかをチェックすることで、テンプレートが利用可能かどうかを判断できます。
これらの変更は、Goのテンプレートシステム、特に html/template の堅牢性と使いやすさを向上させる上で不可欠なものでした。
関連リンク
- Go言語の
text/templateパッケージのドキュメント: https://pkg.go.dev/text/template - Go言語の
html/templateパッケージのドキュメント: https://pkg.go.dev/html/template - Go言語のテンプレートに関する公式ブログ記事 (古いものですが、概念理解に役立ちます): https://go.dev/blog/go-and-html-templates
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
text/templateおよびhtml/templateパッケージ) - コミットメッセージと差分情報
- Go言語のテンプレートに関する一般的な解説記事
- Go言語の
commonフィールドに関する議論 (Goの内部実装に関する情報源) - GoのテンプレートにおけるXSS対策に関する情報