[インデックス 12993] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/base64
パッケージにおけるデコーダのバグ修正に関するものです。具体的には、Base64デコーダが基になるリーダーからの読み取りエラーを適切に処理せず、エラーを無視してしまう可能性があった問題を解決しています。これにより、デコード処理中に発生したエラーが呼び出し元に伝播されず、データが破損したり、予期せぬ動作を引き起こす可能性がありました。
コミット
- コミットハッシュ:
ed90fbc747b384a355ded46ff5c9164ca69b6590
- 作者: Brad Fitzpatrick bradfitz@golang.org
- コミット日時: 2012年4月30日 月曜日 17:14:41 +1000
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ed90fbc747b384a355ded46ff5c9164ca69b6590
元コミット内容
encoding/base64: don't ignore underlying souce read error in decode
Fixes #3577
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6137054
変更の背景
この変更は、Go言語のIssue #3577を修正するために行われました。encoding/base64
パッケージのデコーダ (decoder
型の Read
メソッド) は、基となる io.Reader
からデータを読み取る際にエラーが発生した場合、そのエラーを適切に伝播しないという問題がありました。
具体的には、io.ReadAtLeast
関数がエラーを返した場合でも、デコーダは d.nbuf < 4
(バッファに十分なデータが読み込まれていない) という条件のみをチェックし、d.err
(基となるリーダーからのエラー) の状態を無視していました。このため、リーダーがエラーを返しても、デコーダはエラーを返さずに処理を続行しようとし、結果として不完全なデータや誤ったデータを生成する可能性がありました。
このバグは、特にストリーム処理において、下流のコンポーネントがエラーを検知できず、データの整合性が損なわれる原因となり得ました。
前提知識の解説
Base64エンコーディング
Base64は、バイナリデータをASCII文字列の形式に変換するエンコーディング方式です。主に、バイナリデータをテキストベースのプロトコル(例: 電子メール、HTTP)で安全に転送するために使用されます。3バイトのバイナリデータを4文字のBase64文字列に変換します。
io.Reader
インターフェース
Go言語における io.Reader
インターフェースは、データを読み取るための基本的なインターフェースです。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドは、p
スライスに最大 len(p)
バイトのデータを読み込み、読み込んだバイト数 n
とエラー err
を返します。エラーが nil
でない場合、読み取り中に問題が発生したことを示します。特に、ファイルの終端に達した場合は io.EOF
エラーが返されます。
io.ReadAtLeast
関数
io.ReadAtLeast
は、指定されたリーダーから少なくとも指定されたバイト数を読み取ることを保証するヘルパー関数です。
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
この関数は、r
から buf
にデータを読み込み、min
バイト以上読み込むまで試行します。min
バイトを読み込む前にエラーが発生した場合、またはファイルの終端に達した場合は、そのエラーを返します。
Go言語のエラーハンドリング
Go言語では、エラーは関数の戻り値として明示的に扱われます。慣例として、関数の最後の戻り値は error
型であり、エラーが発生しなかった場合は nil
が返されます。呼び出し元は、このエラー値をチェックして、適切なエラー処理を行う責任があります。
技術的詳細
このコミットの技術的な詳細は、encoding/base64
パッケージの decoder
型の Read
メソッドにおけるエラー処理の改善にあります。
修正前の問題点
修正前の decoder.Read
メソッドの関連部分は以下のようになっていました。
func (d *decoder) Read(p []byte) (n int, err error) {
// ... (前略)
nn, d.err = io.ReadAtLeast(d.r, d.buf[d.nbuf:nn], 4-d.nbuf)
d.nbuf += nn
if d.nbuf < 4 { // ここが問題
return 0, d.err
}
// ... (後略)
}
ここで問題だったのは、if d.nbuf < 4
という条件文です。io.ReadAtLeast
がエラーを返した場合、そのエラーは d.err
に格納されます。しかし、この if
文は d.nbuf
(バッファに読み込まれたバイト数) が4未満であるかどうかのみをチェックしていました。
もし io.ReadAtLeast
がエラーを返したが、同時に d.nbuf
が4以上になってしまった場合(例えば、部分的に読み込んだ後にエラーが発生し、その部分的な読み込みでたまたま4バイト以上になった場合)、この if
文の条件は false
となり、デコーダはエラーを無視して処理を続行してしまいます。これは、基となるリーダーからのエラーが適切に伝播されないことを意味します。
修正内容
修正後のコードは以下のようになります。
func (d *decoder) Read(p []byte) (n int, err error) {
// ... (前略)
nn, d.err = io.ReadAtLeast(d.r, d.buf[d.nbuf:nn], 4-d.nbuf)
d.nbuf += nn
if d.err != nil || d.nbuf < 4 { // ここが修正点
return 0, d.err
}
// ... (後略)
}
修正点として、if d.nbuf < 4
の条件に d.err != nil
が追加されました。これにより、io.ReadAtLeast
がエラーを返した場合(d.err
が nil
でない場合)は、d.nbuf
の値に関わらず、直ちに 0
バイトと d.err
を返して処理を中断するようになりました。
この変更により、基となるリーダーからのエラーが即座にデコーダの Read
メソッドの呼び出し元に伝播されるようになり、エラーが無視される問題が解決されました。
テストケースの追加
この修正を検証するために、base64_test.go
に TestDecoderIssue3577
という新しいテストケースが追加されました。このテストケースは、faultInjectReader
というカスタムの io.Reader
実装を使用しています。
faultInjectReader
は、指定されたバイト数を読み込んだ後に意図的にエラーを発生させることができます。TestDecoderIssue3577
では、この faultInjectReader
を base64.NewDecoder
に渡し、デコード処理中にエラーが発生した場合に、デコーダがそのエラーを正しく返すことを検証しています。
具体的には、faultInjectReader
は最初の5バイトを正常に返し、次に10バイトを読み込む際にカスタムエラー (wantErr
) を発生させるように設定されています。テストは、ioutil.ReadAll(d)
を呼び出してデコード処理を実行し、最終的に返されるエラーが wantErr
と一致するかどうかを確認します。これにより、デコーダが基となるリーダーからのエラーを適切に捕捉し、伝播していることが保証されます。
コアとなるコードの変更箇所
src/pkg/encoding/base64/base64.go
--- a/src/pkg/encoding/base64/base64.go
+++ b/src/pkg/encoding/base64/base64.go
@@ -318,7 +318,7 @@ func (d *decoder) Read(p []byte) (n int, err error) {
}\n \tnn, d.err = io.ReadAtLeast(d.r, d.buf[d.nbuf:nn], 4-d.nbuf)\n \td.nbuf += nn\n-\tif d.nbuf < 4 {\n+\tif d.err != nil || d.nbuf < 4 {\n \t\treturn 0, d.err\n \t}\n \n```
### `src/pkg/encoding/base64/base64_test.go`
```diff
--- a/src/pkg/encoding/base64/base64_test.go
+++ b/src/pkg/encoding/base64/base64_test.go
@@ -6,9 +6,11 @@ package base64
import (
"bytes"
+ "errors"
"io"
"io/ioutil"
"testing"
+ "time"
)
type testpair struct {
@@ -226,3 +228,50 @@ func TestNewLineCharacters(t *testing.T) {
}\n }\n }\n+\n+type nextRead struct {\n+\tn int // bytes to return\n+\terr error // error to return\n+}\n+\n+// faultInjectReader returns data from source, rate-limited
+// and with the errors as written to nextc.
+type faultInjectReader struct {\n+\tsource string\n+\tnextc <-chan nextRead\n+}\n+\n+func (r *faultInjectReader) Read(p []byte) (int, error) {\n+\tnr := <-r.nextc\n+\tif len(p) > nr.n {\n+\t\tp = p[:nr.n]\n+\t}\n+\tn := copy(p, r.source)\n+\tr.source = r.source[n:]\n+\treturn n, nr.err\n+}\n+\n+// tests that we don't ignore errors from our underlying reader
+func TestDecoderIssue3577(t *testing.T) {\n+\tnext := make(chan nextRead, 10)\n+\twantErr := errors.New("my error")\n+\tnext <- nextRead{5, nil}\n+\tnext <- nextRead{10, wantErr}\n+\td := NewDecoder(StdEncoding, &faultInjectReader{\n+\t\tsource: "VHdhcyBicmlsbGlnLCBhbmQgdGhlIHNsaXRoeSB0b3Zlcw==", // twas brillig...\n+\t\tnextc: next,\n+\t})\n+\terrc := make(chan error)\n+\tgo func() {\n+\t\t_, err := ioutil.ReadAll(d)\n+\t\terrc <- err\n+\t}()\n+\tselect {\n+\tcase err := <-errc:\n+\t\tif err != wantErr {\n+\t\t\tt.Errorf("got error %v; want %v", err, wantErr)\n+\t\t}\n+\tcase <-time.After(5 * time.Second):\n+\t\tt.Errorf("timeout; Decoder blocked without returning an error")\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/encoding/base64/base64.go` の変更
この変更は、`decoder` 型の `Read` メソッド内の条件文を修正しています。
- **変更前**: `if d.nbuf < 4 {`
- **変更後**: `if d.err != nil || d.nbuf < 4 {`
この一行の変更が、デコーダが基となる `io.Reader` からのエラーを適切に処理するための鍵となります。
`io.ReadAtLeast` の呼び出し後、`d.err` には読み取り中に発生したエラーが格納されます。変更前は、`d.nbuf` (バッファに読み込まれたバイト数) が4未満の場合にのみ `d.err` を返していました。しかし、`io.ReadAtLeast` がエラーを返しても、同時に4バイト以上読み込んでしまうケースでは、この条件が満たされず、エラーが無視されていました。
変更後は、`d.err != nil` という条件が追加されたことで、`io.ReadAtLeast` がエラーを返した場合は、`d.nbuf` の値に関わらず、直ちにそのエラーを呼び出し元に伝播するようになりました。これにより、デコード処理の途中で発生した基となるリーダーからのエラーが確実に捕捉され、適切なエラーハンドリングが可能になります。
### `src/pkg/encoding/base64/base64_test.go` の追加
追加された `TestDecoderIssue3577` テストケースは、このバグ修正の有効性を検証するために非常に重要です。
1. **`nextRead` 構造体**:
読み取り操作ごとに返すべきバイト数 (`n`) とエラー (`err`) を定義するためのヘルパー構造体です。
2. **`faultInjectReader` 構造体**:
`io.Reader` インターフェースを実装するカスタムリーダーです。
- `source`: 読み取り元のデータ文字列。
- `nextc`: `nextRead` 型のチャネルで、次に `Read` メソッドが呼び出されたときに返すバイト数とエラーを制御します。
`Read` メソッドは `nextc` から `nextRead` の値を受け取り、それに基づいてデータを読み込み、指定されたエラーを返します。これにより、テスト中に任意のタイミングでエラーを注入することが可能になります。
3. **`TestDecoderIssue3577` 関数**:
- `next` チャネルに、最初の読み取りで5バイトを正常に返し、次の読み取りで10バイトを読み込む際に `wantErr` (カスタムエラー) を発生させる指示を送信します。
- `base64.NewDecoder` を `StdEncoding` と `faultInjectReader` のインスタンスで初期化します。`faultInjectReader` の `source` には、Base64エンコードされた文字列が設定されています。
- ゴルーチン内で `ioutil.ReadAll(d)` を呼び出し、デコード処理を実行します。この関数は、デコーダからすべてのデータを読み取り、最終的なエラーを返します。
- `errc` チャネルを通じてゴルーチンから返されたエラーを受け取ります。
- `select` ステートメントを使用して、エラーが返されるか、タイムアウトが発生するかを待ちます。
- 返されたエラーが `wantErr` と一致するかどうかを検証します。これにより、デコーダが基となるリーダーからのエラーを正しく伝播していることを確認します。
- もしタイムアウトが発生した場合、デコーダがエラーを返さずにブロックしてしまったことを意味し、テストは失敗します。
このテストケースは、実際の読み取りエラーが発生した場合に `base64.Decoder` が期待通りに動作し、エラーを無視しないことを保証します。
## 関連リンク
- **GitHubコミットページ**: [https://github.com/golang/go/commit/ed90fbc747b384a355ded46ff5c9164ca69b6590](https://github.com/golang/go/commit/ed90fbc747b384a355ded46ff5c9164ca69b6590)
- **Go Change List (CL)**: [https://golang.org/cl/6137054](https://golang.org/cl/6137054) (このリンクはGoの内部的な変更リストシステムへのもので、一般公開されていない場合があります。)
- **Go Issue #3577**: このコミットメッセージに記載されているIssue番号ですが、現在のGoのIssueトラッカーでは直接この番号のIssueを見つけることができませんでした。これは、古いIssueトラッカーの番号であるか、またはIssueが統合・クローズされた可能性があります。
## 参考にした情報源リンク
- コミットメッセージと差分 (`./commit_data/12993.txt`)
- Go言語の `io` パッケージのドキュメント (一般的な `io.Reader` および `io.ReadAtLeast` の動作理解のため)
- Go言語の `encoding/base64` パッケージのドキュメント (一般的な Base64 エンコーディング/デコーディングの理解のため)
- Go言語のエラーハンドリングに関する一般的な知識