[インデックス 19374] ファイルの概要
このコミットは、Go言語の標準ライブラリ archive/tar
パッケージにおける tar.Reader.Read()
メソッドの挙動を修正するものです。具体的には、tar.Reader
が適切に初期化されていない状態で Read()
メソッドが呼び出された際に発生していたパニック(panic)を回避し、代わりに io.EOF
を返すように変更しています。これにより、未初期化状態での Read()
呼び出しがより安全かつ予測可能な挙動となります。
コミット
commit 6d63d4f3be32bfd3dbc57fe6872d315369c59c6d
Author: Guillaume J. Charmes <guillaume@charmes.net>
Date: Thu May 15 15:18:05 2014 -0700
archive/tar: Do not panic on Read if uninitialized
Calling tar.Reader.Read() used to work fine, but without this patch it panics.
Simply return EOF to indicate the tar.Reader.Next() needs to be called.
LGTM=iant, bradfitz
R=golang-codereviews, bradfitz, iant, mikioh.mikioh, dominik.honnef
CC=golang-codereviews
https://golang.org/cl/94530043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6d63d4f3be32bfd3dbc57fe6872d315369c59c6d
元コミット内容
archive/tar: Do not panic on Read if uninitialized
tar.Reader.Read()
を呼び出すと、以前は問題なく動作していましたが、このパッチがないとパニックを起こしていました。
単に io.EOF
を返すことで、tar.Reader.Next()
を呼び出す必要があることを示します。
変更の背景
この変更の背景には、archive/tar
パッケージの tar.Reader
の Read()
メソッドが、特定の条件下で予期せぬパニックを引き起こすという問題がありました。コミットメッセージによると、「以前は問題なく動作していたが、このパッチがないとパニックを起こす」とあります。これは、おそらくGoランタイムや他の関連パッケージの変更により、tar.Reader
の内部状態が未初期化のまま Read()
が呼び出されるシナリオが発生するようになったことを示唆しています。
tar.Reader
は、Next()
メソッドを呼び出すことで次のアーカイブエントリに進み、そのエントリのデータを Read()
メソッドで読み込むのが通常のワークフローです。しかし、Next()
が一度も呼び出されていない、あるいは何らかの理由で tr.curr
(現在のエントリを指す内部フィールド) が nil
の状態のまま Read()
が呼び出された場合、以前の実装では nil
ポインタデリファレンスなどが発生し、プログラムがクラッシュする「パニック」状態に陥っていました。
パニックはGoプログラムの予期せぬ終了を意味し、堅牢なアプリケーション開発においては極力避けるべき挙動です。このコミットは、このような未初期化状態での Read()
呼び出しを検出し、パニックではなく io.EOF
(End Of File) を返すことで、より穏やかで予測可能なエラーハンドリングを可能にすることを目的としています。io.EOF
を返すことで、呼び出し元はデータがないことを認識し、適切に Next()
を呼び出すなどの次のアクションを決定できます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と archive/tar
パッケージの基本的な使い方を理解しておく必要があります。
-
archive/tar
パッケージ: Go言語の標準ライブラリの一部で、TARアーカイブ(テープアーカイブ)の読み書きをサポートします。TARファイルは、複数のファイルを一つのアーカイブにまとめるための一般的な形式です。 -
tar.Reader
構造体: TARアーカイブからデータを読み込むための型です。NewReader(r io.Reader)
関数を使って作成され、基となるio.Reader
からTARデータを読み取ります。 -
**
tar.Reader.Next()</code> メソッド**:
tar.Readerの最も重要なメソッドの一つです。このメソッドを呼び出すことで、TARアーカイブ内の次のファイルまたはディレクトリのエントリに進みます。成功すると、そのエントリのヘッダ情報(ファイル名、サイズなど)を含む
*tar.Headerを返します。アーカイブの終わりに達すると
io.EOFを返します。このメソッドが呼び出されることで、
tar.Readerの内部状態(特に
tr.currフィールド)が更新され、次に
Read()` が呼び出されたときに正しいエントリのデータを読み込めるようになります。 -
tar.Reader.Read(b []byte) (n int, err error)
メソッド: 現在のTARエントリからデータを読み込み、バイトスライスb
に書き込みます。読み込んだバイト数n
とエラーerr
を返します。現在のエントリの終わりに達するとio.EOF
を返します。このメソッドは、Next()
が呼び出されて現在のエントリが設定された後に使用されることを想定しています。 -
io.EOF
(End Of File): Go言語のio
パッケージで定義されている特別なエラー値です。入力ストリームの終わりに達したことを示すために使用されます。ファイルやネットワーク接続など、データを読み込む操作でこれ以上データがない場合に返されます。io.EOF
はエラーではありますが、通常はプログラムの異常終了を意味するものではなく、正常な終了条件として扱われます。 -
panic
(パニック): Go言語における回復不可能なエラーの一種です。プログラムの実行中に予期せぬ、通常はプログラマの論理的誤りによって発生するような状況(例:nil
ポインタデリファレンス、配列の範囲外アクセス)で発生します。パニックが発生すると、通常のプログラムフローは中断され、defer
関数が実行された後、プログラムは終了します。パニックは、エラーを適切に処理できない場合にのみ使用されるべきであり、通常のエラーハンドリングにはerror
インターフェースが使用されます。 -
nil
ポインタ: Go言語において、ポインタが何も指していない状態を表す値です。nil
ポインタをデリファレンス(*p
のようにポインタが指す値にアクセスしようとすること)しようとすると、ランタイムパニックが発生します。
技術的詳細
このコミットの技術的な核心は、tar.Reader
構造体の Read
メソッドが、その内部状態 tr.curr
が nil
である場合にパニックを起こす問題を解決することにあります。
tar.Reader
は、TARアーカイブ内の各エントリを順に処理するために、内部的に tr.curr
というフィールドを持っています。この tr.curr
は、現在のエントリのデータを読み込むための io.Reader
インターフェースを実装したオブジェクトを指します。通常、ユーザーが tr.Next()
を呼び出すと、tr.curr
は次のエントリのリーダーに設定されます。
問題は、tr.Next()
が一度も呼び出されていない、あるいは何らかの理由で tr.curr
が nil
の状態のまま tr.Read()
が呼び出された場合に発生していました。以前の実装では、tr.Read()
は無条件に tr.curr.Read(b)
を呼び出していました。tr.curr
が nil
である場合、この呼び出しは nil
ポインタデリファレンスとなり、Goランタイムがパニックを引き起こしていました。
このコミットでは、tr.Read()
メソッドの冒頭に以下のチェックを追加することでこの問題を解決しています。
if tr.curr == nil {
return 0, io.EOF
}
このコードは、tr.Read()
が呼び出された際に、まず tr.curr
が nil
であるかどうかを確認します。
- もし
tr.curr
がnil
であれば、それは現在のエントリが設定されていない、つまりNext()
がまだ呼び出されていないか、あるいはアーカイブの終わりに達していることを意味します。このような状況でデータを読み込もうとするのは不適切であるため、パニックを起こす代わりに、読み込んだバイト数n
を0
とし、エラーとしてio.EOF
を返します。 io.EOF
を返すことで、呼び出し元はこれ以上読み込むデータがないことを認識し、Next()
を呼び出して次のエントリに進むべきである、あるいはアーカイブの処理を終了すべきであると判断できます。これは、パニックによるプログラムの強制終了よりもはるかに優雅で予測可能なエラーハンドリングです。
この変更により、tar.Reader
の Read()
メソッドは、未初期化状態での呼び出しに対しても堅牢になり、Goの標準ライブラリとしての信頼性が向上しました。
テストファイル src/pkg/archive/tar/reader_test.go
にも TestUninitializedRead
という新しいテストケースが追加されています。このテストは、NewReader
で tar.Reader
を作成した直後(つまり Next()
を呼び出す前)に Read()
を呼び出し、その際にパニックが発生せず、期待通り io.EOF
が返されることを検証しています。これにより、修正が正しく機能していることが保証されます。
コアとなるコードの変更箇所
変更は src/pkg/archive/tar/reader.go
ファイルと src/pkg/archive/tar/reader_test.go
ファイルにあります。
src/pkg/archive/tar/reader.go
の変更点:
--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -724,6 +724,9 @@ func (tr *Reader) numBytes() int64 {
// It returns 0, io.EOF when it reaches the end of that entry,
// until Next is called to advance to the next entry.
func (tr *Reader) Read(b []byte) (n int, err error) {
+\tif tr.curr == nil {\n+\t\treturn 0, io.EOF\n+\t}\n n, err = tr.curr.Read(b)
if err != nil && err != io.EOF {
tr.err = err
src/pkg/archive/tar/reader_test.go
の変更点:
--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -725,3 +725,19 @@ func TestReadGNUSparseMap1x0(t *testing.T) {
t.Errorf("Incorrect sparse map: got %v, wanted %v", sp, expected)
}\n }\n+\n+func TestUninitializedRead(t *testing.T) {\n+\ttest := gnuTarTest\n+\tf, err := os.Open(test.file)\n+\tif err != nil {\n+\t\tt.Fatalf("Unexpected error: %v", err)\n+\t}\n+\tdefer f.Close()\n+\n+\ttr := NewReader(f)\n+\t_, err = tr.Read([]byte{})\n+\tif err == nil || err != io.EOF {\n+\t\tt.Errorf("Unexpected error: %v, wanted %v", err, io.EOF)\n+\t}\n+\n+}\n```
## コアとなるコードの解説
`src/pkg/archive/tar/reader.go` の変更は、`tar.Reader` 型の `Read` メソッド内で行われています。
```go
func (tr *Reader) Read(b []byte) (n int, err error) {
if tr.curr == nil {
return 0, io.EOF
}
n, err = tr.curr.Read(b)
if err != nil && err != io.EOF {
tr.err = err
}
return
}
追加された3行のコードは以下の通りです。
if tr.curr == nil {
return 0, io.EOF
}
tr.curr
はtar.Reader
構造体の内部フィールドで、現在読み込み中のTARエントリのリーダー(io.Reader
インターフェースを実装)を保持しています。- この
if
文は、Read
メソッドが呼び出された時点でtr.curr
がnil
であるかどうかをチェックします。 - もし
tr.curr
がnil
であれば、それはNext()
メソッドがまだ呼び出されていないか、あるいは以前のエントリの読み込みが完了し、次のエントリに進む準備ができていない状態であることを意味します。このような状況でtr.curr.Read(b)
を直接呼び出すと、nil
ポインタデリファレンスによるパニックが発生する可能性がありました。 - この修正では、パニックを回避するために、
tr.curr
がnil
の場合は直ちに0
バイトを読み込んだことと、エラーとしてio.EOF
を返します。これにより、呼び出し元はデータがないことを安全に認識し、適切にNext()
を呼び出すなどの次のアクションを取ることができます。
src/pkg/archive/tar/reader_test.go
に追加された TestUninitializedRead
テスト関数は、この修正が正しく機能することを保証します。
func TestUninitializedRead(t *testing.T) {
test := gnuTarTest
f, err := os.Open(test.file)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defer f.Close()
tr := NewReader(f) // tar.Readerを初期化するが、Next()は呼び出さない
_, err = tr.Read([]byte{}) // 未初期化状態でRead()を呼び出す
if err == nil || err != io.EOF {
t.Errorf("Unexpected error: %v, wanted %v", err, io.EOF)
}
}
- このテストでは、まずテスト用のTARファイルを開きます。
- 次に、
NewReader(f)
を使ってtar.Reader
のインスタンスtr
を作成します。この時点では、tr.Next()
はまだ呼び出されていないため、tr.curr
はnil
の状態です。 - その直後に
tr.Read([]byte{})
を呼び出し、空のバイトスライスを渡して読み込みを試みます。 - テストの目的は、この
Read()
呼び出しがパニックを起こさず、かつio.EOF
エラーを返すことを検証することです。if err == nil || err != io.EOF
の条件は、「エラーがnil
であるか、またはio.EOF
ではない場合」にテストが失敗することを示しています。つまり、期待される挙動はerr
が正確にio.EOF
であることです。
このテストの追加により、将来的に同様の回帰バグが発生するのを防ぐことができます。
関連リンク
- Go CL 94530043: https://golang.org/cl/94530043
- GitHubコミットページ: https://github.com/golang/go/commit/6d63d4f3be32bfd3dbc57fe6872d315369c59c6d
参考にした情報源リンク
- Go言語公式ドキュメント
archive/tar
パッケージ: https://pkg.go.dev/archive/tar - Go言語公式ドキュメント
io
パッケージ: https://pkg.go.dev/io - Go言語におけるエラーハンドリング: https://go.dev/blog/error-handling-and-go
- Go言語におけるパニックとリカバリ: https://go.dev/blog/defer-panic-and-recover