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

[インデックス 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言語のインポートパスは、パッケージを一意に識別するための重要な要素です。しかし、これまでのパーサーでは、インポートパスとして指定される文字列リテラルに対する厳密な検証が行われていませんでした。これにより、以下のような問題が発生する可能性がありました。

  1. 不正なパスによるコンパイルエラーや実行時エラー: 許可されていない文字や形式のインポートパスが使用された場合、コンパイルエラーが発生したり、最悪の場合、実行時に予期せぬ動作を引き起こす可能性がありました。
  2. セキュリティ上の懸念: 特定の特殊文字や制御文字を含むインポートパスが、ツールやシステムに脆弱性を引き起こす可能性も考えられます。
  3. 一貫性の欠如: 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.goisValidImport 関数が追加され、それが parseImportSpec 関数内で呼び出されるようになったことです。また、src/pkg/go/parser/parser_test.go には、この新しい検証ロジックをテストするための網羅的なテストケースが追加されています。

isValidImport 関数のロジック

isValidImport 関数は、インポートパスとして与えられた文字列リテラル(引用符を含む)が有効であるかを判定します。

  1. 不正文字の定義: const illegalChars = !"#$%&'()*,:;<=>?[\]^{|} + "\uFFFD" この定数には、インポートパスとして許可されない特定の記号文字が定義されています。\uFFFD`はUnicodeのReplacement Characterで、不正なUTF-8シーケンスをデコードした際に現れる文字です。これは、不正なエンコーディングがインポートパスに紛れ込むのを防ぐ目的で含まれています。

  2. 文字列リテラルのアンクォート: s, _ := strconv.Unquote(lit) go/scannerは常に合法的な文字列リテラルを返すため、エラーは無視されます。ここで、引用符で囲まれた文字列リテラル(例: "a/b""m\\x61th")から、エスケープシーケンスが解釈された「生の」文字列値(例: a/bmath)を取得します。この「生の」文字列値に対して妥当性チェックが行われます。

  3. 文字ごとの検証: for _, r := range s { ... } アンクォートされた文字列 s の各ルーン(Unicodeコードポイント)に対して以下のチェックを行います。

    • !unicode.IsGraphic(r): ルーンがグラフィック文字でない場合(例: 制御文字)。
    • unicode.IsSpace(r): ルーンがスペース文字である場合(例: 半角スペース、タブ、改行)。
    • strings.ContainsRune(illegalChars, r): ルーンが事前に定義された illegalChars のいずれかである場合。

    これらの条件のいずれかに合致した場合、そのインポートパスは不正と判断され、関数は false を返します。

  4. 空文字列のチェック: 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 は現在のトークン(この場合は文字列リテラル)の生のテキスト値(引用符を含む)です。isValidImportfalse を返した場合、パーサーは 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 マップをイテレートし、各パスに対して以下の処理を行います。
    1. src := fmt.Sprintf("package p; import %q", path): テスト対象のインポートパスを含むGoソースコードの文字列を動的に生成します。%q は文字列をGoの文字列リテラル形式で引用符付きでフォーマットします。
    2. _, err := ParseFile(fset, "", src, 0): 生成されたソースコードを go/parser.ParseFile 関数で解析します。
    3. 結果の検証:
      • 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のグラフィック文字とスペース文字の定義