[インデックス 12163] ファイルの概要
このコミットは、Go言語のパーサー(go/parser
パッケージ)において、インポートパスの妥当性チェックを導入するものです。具体的には、インポートパスとして指定される文字列リテラルが、Go言語の仕様で許可されていない文字を含んでいないか、あるいは空でないかを検証する機能が追加されました。これにより、不正なインポートパスがコンパイル時に検出され、より堅牢なコードベースの構築に貢献します。
コミット
commit bcc38625654c451d68e057650a412157d3bc4659
Author: Robert Griesemer <gri@golang.org>
Date: Wed Feb 22 23:21:56 2012 -0800
go/parser: check import path restrictions
Replaces pending CL 5674097.
Thanks to ality@pbrane.org for spearheading
the effort.
R=rsc, r
CC=golang-dev
https://golang.org/cl/5683077
---
src/pkg/go/parser/parser.go | 17 +++++++++++++++++
src/pkg/go/parser/parser_test.go | 30 ++++++++++++++++++++++++++++++
2 files changed, 47 insertions(+)
diff --git a/src/pkg/go/parser/parser.go b/src/pkg/go/parser/parser.go
index c1e6190448..a122baf087 100644
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -14,6 +14,9 @@ import (
"go/ast"
"go/scanner"
"go/token"
+ "strconv"
+ "strings"
+ "unicode"
)
// The parser structure holds the parser's internal state.
@@ -1913,6 +1916,17 @@ func (p *parser) parseStmt() (s ast.Stmt) {
type parseSpecFunction func(p *parser, doc *ast.CommentGroup, iota int) ast.Spec
+func isValidImport(lit string) bool {
+ const illegalChars = `!"#$%&'()*,:;<=>?[\\]^{|}` + "`\uFFFD"
+ s, _ := strconv.Unquote(lit) // go/scanner returns a legal string literal
+ for _, r := range s {
+ if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
+ return false
+ }
+ }
+ return s != ""
+}
+
func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
if p.trace {
defer un(trace(p, "ImportSpec"))
@@ -1929,6 +1943,9 @@ func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
var path *ast.BasicLit
if p.tok == token.STRING {
+ if !isValidImport(p.lit) {
+ p.error(p.pos, "invalid import path: "+p.lit)
+ }
path = &ast.BasicLit{ValuePos: p.pos, Kind: p.tok, Value: p.lit}
p.next()
} else {
diff --git a/src/pkg/go/parser/parser_test.go b/src/pkg/go/parser/parser_test.go
index a3ee8525de..da0df14741 100644
--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -5,6 +5,7 @@
package parser
import (
+ "fmt"
"go/ast"
"go/token"
"os"
@@ -204,3 +205,32 @@ func TestVarScope(t *testing.T) {
}
}\n
}\n
+\n+var imports = map[string]bool{\n+\t"a": true,\n+\t"a/b": true,\n+\t"a.b": true,\n+\t"m\\x61th": true,\n+\t"greek/αβ": true,\n+\t"": false,\n+\t"\\x00": false,\n+\t"\\x7f": false,\n+\t"a!": false,\n+\t"a b": false,\n+\t`a\\b`: false,\n+\t"`a`": false,\n+\t"\\x80\\x80": false,\n+}\n+\n+func TestImports(t *testing.T) {\n+\tfor path, isValid := range imports {\n+\t\tsrc := fmt.Sprintf("package p; import %q", path)\n+\t\t_, err := ParseFile(fset, "", src, 0)\n+\t\tswitch {\n+\t\tcase err != nil && isValid:\n+\t\t\tt.Errorf("ParseFile(%s): got %v; expected no error", src, err)\n+\t\tcase err == nil && !isValid:\n+\t\t\tt.Errorf("ParseFile(%s): got no error; expected one", src)\n+\t\t}\n+\t}\n+}\n
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bcc38625654c451d68e057650a412157d3bc4659
元コミット内容
go/parser: check import path restrictions
Replaces pending CL 5674097.
Thanks to ality@pbrane.org for spearheading
the effort.
R=rsc, r
CC=golang-dev
https://golang.org/cl/5683077
変更の背景
Go言語のインポートパスは、パッケージを一意に識別するための重要な要素です。しかし、これまでのパーサーでは、インポートパスとして指定される文字列リテラルに対する厳密な検証が行われていませんでした。これにより、以下のような問題が発生する可能性がありました。
- 不正なパスによるコンパイルエラーや実行時エラー: 許可されていない文字や形式のインポートパスが使用された場合、コンパイルエラーが発生したり、最悪の場合、実行時に予期せぬ動作を引き起こす可能性がありました。
- セキュリティ上の懸念: 特定の特殊文字や制御文字を含むインポートパスが、ツールやシステムに脆弱性を引き起こす可能性も考えられます。
- 一貫性の欠如: Go言語の仕様ではインポートパスに特定の制限が設けられていますが、パーサーがそれを強制しない場合、開発者が誤ったパスを使用するリスクがありました。
このコミットは、これらの問題を解決するために、インポートパスの妥当性をパーサーレベルでチェックする機能を追加します。これにより、Goプログラムの堅牢性とセキュリティが向上し、開発者がより安全で予測可能なコードを書けるようになります。コミットメッセージにあるように、これは以前から検討されていた変更(pending CL 5674097
)を置き換えるものであり、コミュニティからの貢献(Thanks to ality@pbrane.org
)によって実現されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連する概念の知識が必要です。
Go言語のパッケージとインポート
Go言語のプログラムは「パッケージ」という単位で構成されます。パッケージは関連する機能の集まりであり、他のパッケージからその機能を利用するためには「インポート」宣言が必要です。インポート宣言は、import "path/to/package"
の形式で記述され、path/to/package
がインポートパスとなります。このパスは通常、Goモジュールのルートからの相対パス、または標準ライブラリのパッケージ名です。
go/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。ASTは、プログラムの構造を木構造で表現したもので、コンパイラや各種ツールがコードを理解し、処理するために利用します。go/parser
は、字句解析(トークン化)と構文解析(AST構築)の両方を行います。
go/token
, go/ast
, go/scanner
パッケージ
go/token
: Go言語のソースコードにおけるトークン(キーワード、識別子、演算子など)とその位置情報(ファイル名、行番号、列番号)を定義するパッケージです。go/scanner
: ソースコードを読み込み、トークンに分割する字句解析器(スキャナー)を提供するパッケージです。このパッケージは、文字列リテラルを解析し、その内部の値を抽出する機能も持っています。go/ast
: 抽象構文木(AST)のノード構造を定義するパッケージです。go/parser
が生成するASTは、このパッケージで定義された型を使用します。
Go言語における文字列リテラルとエスケープシーケンス (strconv.Unquote
)
Go言語では、文字列リテラルはダブルクォート("
)またはバッククォート(```)で囲まれます。ダブルクォートで囲まれた文字列では、\n
(改行)、\t
(タブ)、\xNN
(16進数エスケープ)、\uNNNN
(Unicodeコードポイントエスケープ)などのエスケープシーケンスが使用できます。
strconv.Unquote
関数は、Go言語の文字列リテラル(引用符で囲まれた形式)を受け取り、その引用符を外し、エスケープシーケンスを解釈した「生の」文字列値を返します。例えば、"m\\x61th"
は math
に変換されます。インポートパスの妥当性をチェックする際には、この「生の」文字列値に対して検証を行う必要があります。
Unicodeの概念 (unicode.IsGraphic
, unicode.IsSpace
)
Unicodeは、世界中の文字を統一的に扱うための文字コード標準です。Go言語の文字列はUTF-8でエンコードされたUnicodeコードポイントのシーケンスとして扱われます。
unicode.IsGraphic(r rune)
: 指定されたルーン(Unicodeコードポイント)が「グラフィック文字」であるかどうかを判定します。グラフィック文字とは、画面に表示される文字(文字、数字、記号など)を指し、制御文字やスペース文字は含まれません。unicode.IsSpace(r rune)
: 指定されたルーンがスペース文字(空白、タブ、改行など)であるかどうかを判定します。
これらの関数は、インポートパスに表示できない文字や不適切な空白が含まれていないかをチェックするために使用されます。
Go言語のテストフレームワーク (testing
パッケージ)
Go言語には、標準ライブラリとしてtesting
パッケージが提供されており、ユニットテストやベンチマークテストを記述するために使用されます。テスト関数はTestXxx(*testing.T)
という形式で定義され、t.Errorf
などのメソッドを使ってテストの失敗を報告します。このコミットでは、追加されたインポートパスの妥当性チェック機能が正しく動作するかを確認するためのテストが追加されています。
技術的詳細
このコミットの主要な変更点は、src/pkg/go/parser/parser.go
に isValidImport
関数が追加され、それが parseImportSpec
関数内で呼び出されるようになったことです。また、src/pkg/go/parser/parser_test.go
には、この新しい検証ロジックをテストするための網羅的なテストケースが追加されています。
isValidImport
関数のロジック
isValidImport
関数は、インポートパスとして与えられた文字列リテラル(引用符を含む)が有効であるかを判定します。
-
不正文字の定義:
const illegalChars =
!"#$%&'()*,:;<=>?[\]^{|}+ "
\uFFFD"この定数には、インポートパスとして許可されない特定の記号文字が定義されています。
\uFFFD`はUnicodeのReplacement Characterで、不正なUTF-8シーケンスをデコードした際に現れる文字です。これは、不正なエンコーディングがインポートパスに紛れ込むのを防ぐ目的で含まれています。 -
文字列リテラルのアンクォート:
s, _ := strconv.Unquote(lit)
go/scanner
は常に合法的な文字列リテラルを返すため、エラーは無視されます。ここで、引用符で囲まれた文字列リテラル(例:"a/b"
や"m\\x61th"
)から、エスケープシーケンスが解釈された「生の」文字列値(例:a/b
やmath
)を取得します。この「生の」文字列値に対して妥当性チェックが行われます。 -
文字ごとの検証:
for _, r := range s { ... }
アンクォートされた文字列s
の各ルーン(Unicodeコードポイント)に対して以下のチェックを行います。!unicode.IsGraphic(r)
: ルーンがグラフィック文字でない場合(例: 制御文字)。unicode.IsSpace(r)
: ルーンがスペース文字である場合(例: 半角スペース、タブ、改行)。strings.ContainsRune(illegalChars, r)
: ルーンが事前に定義されたillegalChars
のいずれかである場合。
これらの条件のいずれかに合致した場合、そのインポートパスは不正と判断され、関数は
false
を返します。 -
空文字列のチェック:
return s != ""
全ての文字が妥当であったとしても、最終的にアンクォートされた文字列s
が空である場合はfalse
を返します。これは、空のインポートパスが許可されないことを意味します。
parseImportSpec
関数への統合
parseImportSpec
関数は、Goのソースコード中のインポート宣言を解析し、ASTノード(ast.ImportSpec
)を構築する役割を担っています。このコミットでは、インポートパスの文字列リテラルを処理する際に、新しく追加された isValidImport
関数が呼び出されるようになりました。
var path *ast.BasicLit
if p.tok == token.STRING {
if !isValidImport(p.lit) { // ここで妥当性チェックが追加
p.error(p.pos, "invalid import path: "+p.lit) // 不正な場合はエラーを報告
}
path = &ast.BasicLit{ValuePos: p.pos, Kind: p.tok, Value: p.lit}
p.next()
} else {
// ... (エラー処理)
}
p.lit
は現在のトークン(この場合は文字列リテラル)の生のテキスト値(引用符を含む)です。isValidImport
が false
を返した場合、パーサーは p.error
メソッドを呼び出して、指定された位置(p.pos
)とエラーメッセージ("invalid import path: "+p.lit
)で構文エラーを報告します。これにより、コンパイル時に不正なインポートパスが早期に検出されるようになります。
テストケース (TestImports
)
src/pkg/go/parser/parser_test.go
に追加された TestImports
関数は、isValidImport
関数のロジックと、それがパーサーに統合された際の挙動を検証します。
imports
マップ: このマップには、様々なインポートパスの文字列と、それらが有効であるか(true
)または無効であるか(false
)を示すブール値のペアが定義されています。これには、通常のパス、スラッシュやドットを含むパス、エスケープシーケンスを含むパス、Unicode文字を含むパス、そして空文字列、制御文字、削除文字、スペース、禁止記号、バッククォート、不正なUTF-8シーケンスなど、多岐にわたる不正なパスが含まれています。- テストループ:
TestImports
関数は、このimports
マップをイテレートし、各パスに対して以下の処理を行います。src := fmt.Sprintf("package p; import %q", path)
: テスト対象のインポートパスを含むGoソースコードの文字列を動的に生成します。%q
は文字列をGoの文字列リテラル形式で引用符付きでフォーマットします。_, err := ParseFile(fset, "", src, 0)
: 生成されたソースコードをgo/parser.ParseFile
関数で解析します。- 結果の検証:
err != nil && isValid
: エラーが発生したが、パスが有効であると期待される場合、テストは失敗します。err == nil && !isValid
: エラーが発生しなかったが、パスが無効であると期待される場合、テストは失敗します。 この網羅的なテストにより、インポートパスの妥当性チェックがGo言語の仕様に沿って正しく機能することが保証されます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。
-
src/pkg/go/parser/parser.go
:import
文の追加:strconv
,strings
,unicode
- 新関数
isValidImport(lit string) bool
の追加 (L1916-L1927) parseImportSpec
関数内でのisValidImport
の呼び出しとエラーハンドリングの追加 (L1943-L1945)
-
src/pkg/go/parser/parser_test.go
:import
文の追加:fmt
- テストデータ
imports
マップの追加 (L207-L219) - テスト関数
TestImports(t *testing.T)
の追加 (L221-L230)
コアとなるコードの解説
src/pkg/go/parser/parser.go
// L14-L17: 新しいパッケージのインポート
import (
"go/ast"
"go/scanner"
"go/token"
"strconv" // 文字列リテラルのアンクォート用
"strings" // 文字列操作用 (ContainsRune)
"unicode" // Unicode文字のプロパティチェック用 (IsGraphic, IsSpace)
)
// L1916-L1927: isValidImport 関数の定義
func isValidImport(lit string) bool {
// インポートパスとして許可されない文字の定義
const illegalChars = `!"#$%&'()*,:;<=>?[\\]^{|}` + "`\uFFFD"
// 文字列リテラルから引用符を外し、エスケープシーケンスを解釈した「生の」文字列を取得
s, _ := strconv.Unquote(lit) // go/scanner は常に合法的な文字列リテラルを返すため、エラーは無視
// 生の文字列の各ルーン(Unicodeコードポイント)をチェック
for _, r := range s {
// グラフィック文字でない、またはスペース文字である、または不正文字リストに含まれる場合
if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
return false // 不正なパスと判定
}
}
// 文字列が空でないことを確認(空のインポートパスは不正)
return s != ""
}
// L1943-L1945: parseImportSpec 関数内での isValidImport の呼び出し
func parseImportSpec(p *parser, doc *ast.CommentGroup, _ int) ast.Spec {
// ... (既存のコード)
var path *ast.BasicLit
if p.tok == token.STRING { // 現在のトークンが文字列リテラルの場合
if !isValidImport(p.lit) { // 新しく追加された妥当性チェック
// 不正なインポートパスの場合、パーサーのエラーハンドラを呼び出す
p.error(p.pos, "invalid import path: "+p.lit)
}
// 妥当であれば、ASTノードを構築
path = &ast.BasicLit{ValuePos: p.pos, Kind: p.tok, Value: p.lit}
p.next() // 次のトークンへ進む
} else {
// ... (エラー処理)
}
// ... (既存のコード)
}
src/pkg/go/parser/parser_test.go
// L5: 新しいパッケージのインポート
import (
"fmt" // 文字列フォーマット用
"go/ast"
"go/token"
"os"
"testing"
)
// L207-L219: テストデータ 'imports' マップの定義
var imports = map[string]bool{
"a": true, // 有効なパス
"a/b": true, // 有効なパス (スラッシュ含む)
"a.b": true, // 有効なパス (ドット含む)
"m\\x61th": true, // 有効なパス (エスケープシーケンス含む)
"greek/αβ": true, // 有効なパス (Unicode文字含む)
"": false, // 無効なパス (空文字列)
"\\x00": false, // 無効なパス (NULL文字)
"\\x7f": false, // 無効なパス (DEL文字)
"a!": false, // 無効なパス (禁止記号 '!')
"a b": false, // 無効なパス (スペース)
`a\\b`: false, // 無効なパス (バックスラッシュ)
"`a`": false, // 無効なパス (バッククォート)
"\\x80\\x80": false, // 無効なパス (不正なUTF-8シーケンス)
}
// L221-L230: TestImports テスト関数の定義
func TestImports(t *testing.T) {
// 'imports' マップの各エントリをイテレート
for path, isValid := range imports {
// テスト対象のインポートパスを含むGoソースコードを生成
src := fmt.Sprintf("package p; import %q", path)
// 生成されたソースコードをParseFileで解析
_, err := ParseFile(fset, "", src, 0)
// 解析結果と期待される妥当性を比較
switch {
case err != nil && isValid: // エラーが発生したが、有効であると期待される場合
t.Errorf("ParseFile(%s): got %v; expected no error", src, err) // テスト失敗
case err == nil && !isValid: // エラーが発生しなかったが、無効であると期待される場合
t.Errorf("ParseFile(%s): got no error; expected one", src) // テスト失敗
}
}
}
関連リンク
- Gerrit Change-ID:
https://golang.org/cl/5683077
(GoプロジェクトのコードレビューシステムであるGerritの変更リンク)
参考にした情報源リンク
- Go言語の公式ドキュメント (特に
go/parser
,go/token
,go/ast
,go/scanner
,strconv
,strings
,unicode
,testing
パッケージのドキュメント) - Go言語のインポートパスに関する仕様
- Unicodeのグラフィック文字とスペース文字の定義