[インデックス 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
スライスの挙動など)