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

[インデックス 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.TrimSpacenil スライスを返した場合、len(line) は0になります。この条件は「空行」として扱われ、end の計算が行われます。しかし、cap(line)nil スライスの場合も0を返します。問題は、end = cap(content) - cap(line) の計算自体ではなく、その後の end の値が、本来スキップすべきバイト数と一致しない可能性があったことです。

より根本的な問題は、end = cap(content) - cap(line) という計算が、linecontent のどの部分を指しているかという相対的な位置関係を正しく反映していなかった点にあります。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) では、linebytes.TrimSpace によってトリムされた後のスライスであり、その cap(line) は元の行の長さとは異なる可能性がありました。特に bytes.TrimSpacenil スライスを返した場合、cap(line) は0となり、計算が狂う原因となっていました。

新しいコード end = len(content) - len(p) では、len(p) を使用することで、トリム前の元の行の正確な長さを取得しています。これにより、content スライスから p の長さ分だけ進んだ位置が、次の行の開始位置として正しく end に設定されます。この変更により、bytes.TrimSpacenil スライスを返した場合でも、ファイルの読み込み位置の計算が狂うことなく、shouldBuild 関数が堅牢に動作するようになります。

追加されたテストケース TestShouldBuild は、この修正が意図通りに機能することを保証します。特に、file1file2file3 といった異なるビルドタグやコメントのパターンを持つ文字列を shouldBuild 関数に渡し、期待される結果(ビルドされるべきか、されないべきか)と比較することで、関数の正確性を検証しています。

関連リンク

参考にした情報源リンク

  • 上記のGitHub IssueおよびGo CLのページ
  • Go言語の公式ドキュメント(go/buildパッケージ、bytesパッケージ)
  • Go言語のスライスに関する一般的な情報源(lencapnilスライスの挙動など)