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

[インデックス 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.ReaderRead() メソッドが、特定の条件下で予期せぬパニックを引き起こすという問題がありました。コミットメッセージによると、「以前は問題なく動作していたが、このパッチがないとパニックを起こす」とあります。これは、おそらく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 パッケージの基本的な使い方を理解しておく必要があります。

  1. archive/tar パッケージ: Go言語の標準ライブラリの一部で、TARアーカイブ(テープアーカイブ)の読み書きをサポートします。TARファイルは、複数のファイルを一つのアーカイブにまとめるための一般的な形式です。

  2. tar.Reader 構造体: TARアーカイブからデータを読み込むための型です。NewReader(r io.Reader) 関数を使って作成され、基となる io.Reader からTARデータを読み取ります。

  3. **tar.Reader.Next()</code> メソッド**: tar.Readerの最も重要なメソッドの一つです。このメソッドを呼び出すことで、TARアーカイブ内の次のファイルまたはディレクトリのエントリに進みます。成功すると、そのエントリのヘッダ情報(ファイル名、サイズなど)を含む*tar.Headerを返します。アーカイブの終わりに達するとio.EOF を返します。このメソッドが呼び出されることで、tar.Readerの内部状態(特にtr.currフィールド)が更新され、次にRead()` が呼び出されたときに正しいエントリのデータを読み込めるようになります。

  4. tar.Reader.Read(b []byte) (n int, err error) メソッド: 現在のTARエントリからデータを読み込み、バイトスライス b に書き込みます。読み込んだバイト数 n とエラー err を返します。現在のエントリの終わりに達すると io.EOF を返します。このメソッドは、Next() が呼び出されて現在のエントリが設定された後に使用されることを想定しています。

  5. io.EOF (End Of File): Go言語の io パッケージで定義されている特別なエラー値です。入力ストリームの終わりに達したことを示すために使用されます。ファイルやネットワーク接続など、データを読み込む操作でこれ以上データがない場合に返されます。io.EOF はエラーではありますが、通常はプログラムの異常終了を意味するものではなく、正常な終了条件として扱われます。

  6. panic (パニック): Go言語における回復不可能なエラーの一種です。プログラムの実行中に予期せぬ、通常はプログラマの論理的誤りによって発生するような状況(例: nil ポインタデリファレンス、配列の範囲外アクセス)で発生します。パニックが発生すると、通常のプログラムフローは中断され、defer 関数が実行された後、プログラムは終了します。パニックは、エラーを適切に処理できない場合にのみ使用されるべきであり、通常のエラーハンドリングには error インターフェースが使用されます。

  7. nil ポインタ: Go言語において、ポインタが何も指していない状態を表す値です。nil ポインタをデリファレンス(*p のようにポインタが指す値にアクセスしようとすること)しようとすると、ランタイムパニックが発生します。

技術的詳細

このコミットの技術的な核心は、tar.Reader 構造体の Read メソッドが、その内部状態 tr.currnil である場合にパニックを起こす問題を解決することにあります。

tar.Reader は、TARアーカイブ内の各エントリを順に処理するために、内部的に tr.curr というフィールドを持っています。この tr.curr は、現在のエントリのデータを読み込むための io.Reader インターフェースを実装したオブジェクトを指します。通常、ユーザーが tr.Next() を呼び出すと、tr.curr は次のエントリのリーダーに設定されます。

問題は、tr.Next() が一度も呼び出されていない、あるいは何らかの理由で tr.currnil の状態のまま tr.Read() が呼び出された場合に発生していました。以前の実装では、tr.Read() は無条件に tr.curr.Read(b) を呼び出していました。tr.currnil である場合、この呼び出しは nil ポインタデリファレンスとなり、Goランタイムがパニックを引き起こしていました。

このコミットでは、tr.Read() メソッドの冒頭に以下のチェックを追加することでこの問題を解決しています。

if tr.curr == nil {
    return 0, io.EOF
}

このコードは、tr.Read() が呼び出された際に、まず tr.currnil であるかどうかを確認します。

  • もし tr.currnil であれば、それは現在のエントリが設定されていない、つまり Next() がまだ呼び出されていないか、あるいはアーカイブの終わりに達していることを意味します。このような状況でデータを読み込もうとするのは不適切であるため、パニックを起こす代わりに、読み込んだバイト数 n0 とし、エラーとして io.EOF を返します。
  • io.EOF を返すことで、呼び出し元はこれ以上読み込むデータがないことを認識し、Next() を呼び出して次のエントリに進むべきである、あるいはアーカイブの処理を終了すべきであると判断できます。これは、パニックによるプログラムの強制終了よりもはるかに優雅で予測可能なエラーハンドリングです。

この変更により、tar.ReaderRead() メソッドは、未初期化状態での呼び出しに対しても堅牢になり、Goの標準ライブラリとしての信頼性が向上しました。

テストファイル src/pkg/archive/tar/reader_test.go にも TestUninitializedRead という新しいテストケースが追加されています。このテストは、NewReadertar.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.currtar.Reader 構造体の内部フィールドで、現在読み込み中のTARエントリのリーダー(io.Reader インターフェースを実装)を保持しています。
  • この if 文は、Read メソッドが呼び出された時点で tr.currnil であるかどうかをチェックします。
  • もし tr.currnil であれば、それは Next() メソッドがまだ呼び出されていないか、あるいは以前のエントリの読み込みが完了し、次のエントリに進む準備ができていない状態であることを意味します。このような状況で tr.curr.Read(b) を直接呼び出すと、nil ポインタデリファレンスによるパニックが発生する可能性がありました。
  • この修正では、パニックを回避するために、tr.currnil の場合は直ちに 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.currnil の状態です。
  • その直後に tr.Read([]byte{}) を呼び出し、空のバイトスライスを渡して読み込みを試みます。
  • テストの目的は、この Read() 呼び出しがパニックを起こさず、かつ io.EOF エラーを返すことを検証することです。if err == nil || err != io.EOF の条件は、「エラーが nil であるか、または io.EOF ではない場合」にテストが失敗することを示しています。つまり、期待される挙動は err が正確に io.EOF であることです。

このテストの追加により、将来的に同様の回帰バグが発生するのを防ぐことができます。

関連リンク

参考にした情報源リンク