[インデックス 13615] ファイルの概要
このコミットは、Go言語のビルドシステムにおける go/build パッケージ内の shouldBuild 関数に存在したバグを修正するものです。具体的には、ファイルのコンテンツを読み込む際に bytes.TrimSpace 関数が nil スライスを返す可能性があり、それが原因で shouldBuild 関数が誤った動作をする問題に対処しています。この修正により、ビルドタグの解釈がより堅牢になります。
コミット
commit f087764abcba7e041fb5c8c59f6122861b00ddba
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Aug 9 23:22:51 2012 +0200
go/build: correct shouldBuild bug reading whole contents of file.
It was caused by bytes.TrimSpace being able to return a nil
slice.
Fixes #3914.
R=golang-dev, r
CC=golang-dev, remy
https://golang.org/cl/6458091
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f087764abcba7e041fb5c8c59f6122861b00ddba
元コミット内容
go/build: correct shouldBuild bug reading whole contents of file.
It was caused by bytes.TrimSpace being able to return a nil
slice.
Fixes #3914.
R=golang-dev, r
CC=golang-dev, remy
https://golang.org/cl/6458091
変更の背景
Go言語のビルドプロセスでは、ソースファイルの先頭に記述されるビルドタグ(例: // +build linux,amd64)を解析し、現在のビルド環境に適したファイルのみをコンパイル対象とします。この解析は go/build パッケージ内の shouldBuild 関数によって行われます。
このコミットが行われる以前、shouldBuild 関数はファイルのコンテンツをバイトスライスとして読み込み、各行を bytes.TrimSpace 関数でトリムしていました。しかし、bytes.TrimSpace は、入力スライスが完全に空白文字で構成されている場合や、空のスライスが与えられた場合に、nil スライスを返すという特性を持っていました。
この nil スライスが返された際に、shouldBuild 関数内でスライスの長さ(len(line))をチェックするロジックが適切に機能せず、結果としてファイルの読み込み位置の計算(end = cap(content) - cap(line))が誤ってしまうバグが存在しました。特に、空行や空白のみの行が連続する場合に問題が発生し、ビルドタグの誤認識や、最悪の場合パニックを引き起こす可能性がありました。
このバグは Issue 3914 として報告されており、本コミットはその修正を目的としています。
前提知識の解説
- Go言語のビルドタグ: Go言語のソースファイルの先頭に
// +build tag1,tag2の形式で記述される特殊なコメントです。これは、特定のビルド条件(OS、アーキテクチャ、カスタムタグなど)が満たされた場合にのみそのファイルをビルド対象に含めるためのディレクティブとして機能します。例えば、// +build linuxと書かれたファイルはLinux環境でのみビルドされます。 go/buildパッケージ: Go言語の標準ライブラリの一部で、Goのソースコードのビルドプロセスを管理するための機能を提供します。パッケージの依存関係の解決、ソースファイルの検索、ビルドタグの解釈などを行います。bytes.TrimSpace関数:bytesパッケージに含まれる関数で、バイトスライスの先頭と末尾からすべての空白文字(スペース、タブ、改行など)を削除した新しいバイトスライスを返します。重要なのは、入力がすべて空白文字の場合や空の場合にnilスライスを返す可能性があるという点です。- Go言語のスライスと
nil: Go言語のスライスは、基盤となる配列の一部を参照するデータ構造です。スライスはnilになることがあり、nilスライスは長さも容量も0です。nilスライスに対してlen()やcap()を呼び出すことは可能で、どちらも0を返します。しかし、nilスライスに対して要素へのアクセス(例:line[0])を試みるとパニックが発生します。 len()とcap():len(s): スライスsの現在の要素数を返します。cap(s): スライスsが基盤となる配列から拡張できる最大容量を返します。
技術的詳細
shouldBuild 関数は、Goのソースファイルのバイトコンテンツを受け取り、そのファイルが現在のビルドコンテキストでビルドされるべきかどうかを判断します。この関数は、ファイルの先頭から行ごとに読み込み、// +build ディレクティブを探します。
問題の箇所は、各行を処理するループ内で bytes.TrimSpace(line) の結果を line 変数に再代入し、その後の処理で line の長さに基づいてファイルの読み込み位置を更新する部分でした。
元のコード:
line = bytes.TrimSpace(line)
if len(line) == 0 { // Blank line
end = cap(content) - cap(line) // &line[0] - &content[0]
continue
}
ここで、bytes.TrimSpace が nil スライスを返した場合、len(line) は0になります。この条件は「空行」として扱われ、end の計算が行われます。しかし、cap(line) は nil スライスの場合も0を返します。問題は、end = cap(content) - cap(line) の計算自体ではなく、その後の end の値が、本来スキップすべきバイト数と一致しない可能性があったことです。
より根本的な問題は、end = cap(content) - cap(line) という計算が、line が content のどの部分を指しているかという相対的な位置関係を正しく反映していなかった点にあります。cap(line) は line スライスの容量であり、元の content スライスにおける line の開始位置からのオフセットを正確に表すものではありませんでした。
修正では、end = len(content) - len(p) という計算に変わっています。ここで p は、content スライスから現在処理中の行を切り出した元のスライスを指します。これにより、len(p) はトリム前の行の実際の長さを正確に表すため、content の残りの部分の長さを正しく計算できるようになります。
コアとなるコードの変更箇所
src/pkg/go/build/build.go ファイルの shouldBuild 関数内の以下の行が変更されました。
--- a/src/pkg/go/build/build.go
+++ b/src/pkg/go/build/build.go
@@ -689,7 +689,7 @@ func (ctxt *Context) shouldBuild(content []byte) bool {
}
line = bytes.TrimSpace(line)
if len(line) == 0 { // Blank line
- end = cap(content) - cap(line) // &line[0] - &content[0]
+ end = len(content) - len(p)
continue
}
if !bytes.HasPrefix(line, slashslash) { // Not comment line
また、src/pkg/go/build/build_test.go には、TestShouldBuild という新しいテスト関数が追加され、このバグ修正が正しく機能することを確認しています。このテストは、様々なビルドタグの組み合わせや、空行を含むファイルコンテンツに対して shouldBuild の動作を検証します。
コアとなるコードの解説
変更された行 end = len(content) - len(p) は、ファイルの読み込み位置を更新するロジックの核心です。
content: ファイル全体のバイトスライスです。p: 現在処理している行の、トリムされる前の元のバイトスライスです。shouldBuild関数内のループでは、contentスライスから各行を切り出す際にpという変数に元の行のスライスが保持されています。
元のコード end = cap(content) - cap(line) では、line が bytes.TrimSpace によってトリムされた後のスライスであり、その cap(line) は元の行の長さとは異なる可能性がありました。特に bytes.TrimSpace が nil スライスを返した場合、cap(line) は0となり、計算が狂う原因となっていました。
新しいコード end = len(content) - len(p) では、len(p) を使用することで、トリム前の元の行の正確な長さを取得しています。これにより、content スライスから p の長さ分だけ進んだ位置が、次の行の開始位置として正しく end に設定されます。この変更により、bytes.TrimSpace が nil スライスを返した場合でも、ファイルの読み込み位置の計算が狂うことなく、shouldBuild 関数が堅牢に動作するようになります。
追加されたテストケース TestShouldBuild は、この修正が意図通りに機能することを保証します。特に、file1、file2、file3 といった異なるビルドタグやコメントのパターンを持つ文字列を shouldBuild 関数に渡し、期待される結果(ビルドされるべきか、されないべきか)と比較することで、関数の正確性を検証しています。
関連リンク
- Go Issue 3914: go/build: shouldBuild bug reading whole contents of file
- Go CL 6458091: go/build: correct shouldBuild bug reading whole contents of file.
参考にした情報源リンク
- 上記のGitHub IssueおよびGo CLのページ
- Go言語の公式ドキュメント(
go/buildパッケージ、bytesパッケージ) - Go言語のスライスに関する一般的な情報源(
len、cap、nilスライスの挙動など)