[インデックス 19237] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/ascii85
パッケージ内のデコーダの挙動を修正するものです。具体的には、ascii85.go
のデコーダ実装と、そのテストファイルである ascii85_test.go
に変更が加えられています。
コミット
commit 9d7b9fb7d01c831e67b82f352676097835af01fc
Author: Rui Ueyama <ruiu@google.com>
Date: Sat Apr 26 19:56:06 2014 -0700
encoding/ascii85: handle non-data bytes correctly
Previously Read wouldn't return once its internal input buffer
is filled with non-data bytes.
Fixes #7875.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/90820043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9d7b9fb7d01c831e67b82f352676097835af01fc
元コミット内容
encoding/ascii85: handle non-data bytes correctly
以前は、Read
メソッドが内部入力バッファが非データバイトで満たされると、処理を返さなくなっていました。このコミットは、この問題を修正します。
Fixes #7875.
変更の背景
このコミットの背景には、encoding/ascii85
パッケージのデコーダ (ascii85.Read
メソッド) におけるバグが存在しました。具体的には、デコーダが入力ストリームからデータを読み込む際に、ASCII85エンコードされた実際のデータではないバイト(例えば、空白文字や改行などの「非データバイト」)が連続して大量に現れると、デコーダの内部バッファがこれらの非データバイトで満たされてしまい、それ以上有効なデータを処理できなくなるという問題がありました。
この状態に陥ると、Read
メソッドは新しいデータを読み込むためのスペースを確保できず、結果として呼び出し元にデータを返さなくなってしまう(ハングアップする、または期待通りに動作しない)という不具合が発生していました。コミットメッセージにある Fixes #7875
は、この問題がGoのIssueトラッカーで報告されていたことを示しています。この修正は、デコーダが非データバイトを適切にスキップまたは破棄し、内部バッファを効率的に管理できるようにすることで、この問題を解決することを目的としています。
前提知識の解説
ASCII85エンコーディング
ASCII85(またはBase85)は、バイナリデータをASCII文字列にエンコードする方式の一つです。主にPostScriptやPDFファイルでバイナリデータを埋め込むために使用されます。4バイトのバイナリデータを5文字のASCII文字に変換するため、Base64(3バイトを4文字に変換)よりも効率的です。
ASCII85のエンコードされたデータは、通常、!
から u
までの範囲の文字を使用します。また、z
は4バイトすべてがゼロの場合の特殊な短縮形として使われます。重要なのは、ASCII85デコーダはエンコードされたデータストリーム内の空白文字(スペース、タブ、改行など)を無視するように設計されている点です。これらは「非データバイト」として扱われ、デコード処理には影響を与えません。
Go言語の io.Reader
インターフェース
Go言語では、データの読み込み操作は io.Reader
インターフェースによって抽象化されています。このインターフェースは単一のメソッド Read(p []byte) (n int, err error)
を持ちます。
p []byte
: 読み込んだデータを格納するためのバイトスライス。n int
: 実際にp
に読み込まれたバイト数。err error
: 読み込み中に発生したエラー。ファイルの終端(EOF)に達した場合はio.EOF
が返されます。
Read
メソッドは、要求されたバイト数(len(p)
)をすべて読み込むことを保証しません。利用可能なデータが少ない場合や、内部バッファの都合などにより、len(p)
よりも少ないバイト数を返すことがあります。しかし、データが利用可能であるにもかかわらず n=0
かつ err=nil
を返すことは、通常、不正な状態と見なされます。これは、呼び出し元がデータが利用可能であると期待して Read
を呼び出したにもかかわらず、何も得られなかったことを意味し、デッドロックやハングアップの原因となる可能性があります。
デコーダの内部バッファ管理
多くのデコーダやストリーム処理コンポーネントは、効率のために内部バッファを使用します。入力ストリームから一度にまとまったデータを読み込み、それを処理して出力バッファに書き出します。この際、デコーダは入力データの中から有効なデータとそうでないデータ(例えば、ASCII85における非データバイト)を区別し、有効なデータのみを処理し、不要なデータはスキップまたは破棄する必要があります。内部バッファが不要なデータで満たされてしまうと、新しい有効なデータを読み込むスペースがなくなり、処理が停止する可能性があります。
技術的詳細
このコミットが修正する問題は、encoding/ascii85
パッケージの decoder
型の Read
メソッドが、入力ストリームに大量の非データバイト(主に空白文字)が含まれている場合に発生していました。
decoder
は内部に d.buf
というバイトスライスをバッファとして持っており、入力から読み込んだバイトを一時的にここに格納します。d.nbuf
は d.buf
内の有効なバイト数を示します。デコーダは d.buf
からASCII85データをデコードし、結果を呼び出し元が提供する出力バッファ p
に書き込みます。
問題のシナリオは以下の通りです。
Read
メソッドが呼び出され、入力ストリームからデータを読み込もうとします。- 読み込んだデータの中に、ASCII85の有効なデータ文字ではなく、大量の空白文字などの非データバイトが含まれています。
- これらの非データバイトはデコード処理では無視されますが、
d.buf
には蓄積されていきます。 d.buf
が非データバイトで満杯になると、デコーダはそれ以上新しい入力データを読み込むことができません。- しかし、デコードすべき有効なデータがないため、
Read
メソッドはn=0
を返そうとしますが、内部バッファが詰まっているため、新しいデータを読み込むこともできず、結果としてRead
がブロックされるか、期待通りに動作しなくなるという状態に陥っていました。
この修正は、Read
メソッドのループ内で、出力バッファ p
に何も書き込めなかった場合 (ndst == 0
) かつエラーがない場合に、内部バッファ d.buf
を検査し、非データバイトを積極的に取り除くロジックを追加することで問題を解決します。これにより、内部バッファにスペースが確保され、デコーダが新しい入力データを読み込み続けられるようになります。
コアとなるコードの変更箇所
変更は主に src/pkg/encoding/ascii85/ascii85.go
の decoder
型の Read
メソッド内と、src/pkg/encoding/ascii85/ascii85_test.go
に追加された新しいテストケースです。
src/pkg/encoding/ascii85/ascii85.go
の変更点:
--- a/src/pkg/encoding/ascii85/ascii85.go
+++ b/src/pkg/encoding/ascii85/ascii85.go
@@ -281,6 +281,18 @@ func (d *decoder) Read(p []byte) (n int, err error) {
d.nbuf = copy(d.buf[0:], d.buf[nsrc:d.nbuf])
continue // copy out and return
}
+ if ndst == 0 && d.err == nil {
+ // Special case: input buffer is mostly filled with non-data bytes.
+ // Filter out such bytes to make room for more input.
+ off := 0
+ for i := 0; i < d.nbuf; i++ {
+ if d.buf[i] > ' ' {
+ d.buf[off] = d.buf[i]
+ off++
+ }
+ }
+ d.nbuf = off
+ }
}
// Out of input, out of decoded output. Check errors.
src/pkg/encoding/ascii85/ascii85_test.go
の変更点:
--- a/src/pkg/encoding/ascii85/ascii85_test.go
+++ b/src/pkg/encoding/ascii85/ascii85_test.go
@@ -197,3 +197,14 @@ func TestBig(t *testing.T) {
t.Errorf("Decode(Encode(%d-byte string)) failed at offset %d", n, i)
}
}
+
+func TestDecoderInternalWhitespace(t *testing.T) {
+ s := strings.Repeat(" ", 2048) + "z"
+ decoded, err := ioutil.ReadAll(NewDecoder(strings.NewReader(s)))
+ if err != nil {
+ t.Errorf("Decode gave error %v", err)
+ }
+ if want := []byte("\000\000\000\000"); !bytes.Equal(want, decoded) {
+ t.Errorf("Decode failed: got %v, want %v", decoded, want)
+ }
+}
コアとなるコードの解説
src/pkg/encoding/ascii85/ascii85.go
の修正
追加されたコードブロックは、decoder
の Read
メソッドのメインループ内にあります。
if ndst == 0 && d.err == nil {
// Special case: input buffer is mostly filled with non-data bytes.
// Filter out such bytes to make room for more input.
off := 0
for i := 0; i < d.nbuf; i++ {
if d.buf[i] > ' ' { // ' ' (スペース) よりも大きい文字、つまり非データバイトではない文字をチェック
d.buf[off] = d.buf[i]
off++
}
}
d.nbuf = off
}
このコードブロックは、以下の条件が満たされた場合に実行されます。
ndst == 0
: 今回のRead
呼び出しで、出力バッファp
に有効なデータが1バイトも書き込まれなかった場合。d.err == nil
: デコーダに現在エラーが発生していない場合。
この条件は、「デコーダが有効なデータを生成できなかったが、エラー状態でもない」という、内部バッファが非データバイトで詰まっている可能性のある状況を捉えています。
ブロック内の処理は以下の通りです。
off := 0
:d.buf
内の有効なデータバイトを書き込むためのオフセット(インデックス)を初期化します。for i := 0; i < d.nbuf; i++
: 内部バッファd.buf
の現在有効な範囲をループします。if d.buf[i] > ' '
: 各バイトをチェックし、それがASCII85の非データバイト(スペース、タブ、改行など、ASCII値がスペース以下)ではないかどうかを判断します。ASCII85では、スペース文字(ASCII値 32)以下の文字は通常、非データバイトとして扱われます。d.buf[off] = d.buf[i]
: もし現在のバイトd.buf[i]
が非データバイトではない場合、それをd.buf
の先頭 (off
が指す位置) にコピーします。off++
: 次の有効なデータバイトを書き込む位置を更新します。d.nbuf = off
: ループが終了した後、d.nbuf
をoff
の値に更新します。これにより、d.buf
内の非データバイトが効果的に削除され、有効なデータバイトがバッファの先頭に詰められます。これにより、内部バッファに新しい入力データを読み込むためのスペースが確保されます。
この修正により、デコーダは大量の非データバイトを効率的にスキップし、内部バッファが詰まることなく、後続の有効なASCII85データを処理できるようになります。
src/pkg/encoding/ascii85/ascii85_test.go
の追加テスト
TestDecoderInternalWhitespace
という新しいテスト関数が追加されました。
func TestDecoderInternalWhitespace(t *testing.T) {
s := strings.Repeat(" ", 2048) + "z"
decoded, err := ioutil.ReadAll(NewDecoder(strings.NewReader(s)))
if err != nil {
t.Errorf("Decode gave error %v", err)
}
if want := []byte("\000\000\000\000"); !bytes.Equal(want, decoded) {
t.Errorf("Decode failed: got %v, want %v", decoded, want)
}
}
このテストは、修正されたデコーダの挙動を検証するために特別に設計されています。
s := strings.Repeat(" ", 2048) + "z"
: 2048個のスペース文字の後に、ASCII85の特殊な文字z
を連結した文字列を作成します。z
はASCII85において4バイトのゼロ (\000\000\000\000
) を表します。この入力は、大量の非データバイト(スペース)が先行し、その後に有効なASCII85データが続くという、以前バグを引き起こしたシナリオを再現します。decoded, err := ioutil.ReadAll(NewDecoder(strings.NewReader(s)))
: 作成した文字列s
をascii85.NewDecoder
に渡し、ioutil.ReadAll
を使用してデコードされたすべてのデータを読み込もうとします。if err != nil
: エラーが発生しないことを確認します。以前のバグでは、ここでハングアップするか、予期せぬエラーが発生する可能性がありました。if want := []byte("\000\000\000\000"); !bytes.Equal(want, decoded)
: デコードされた結果が期待される4バイトのゼロ (\000\000\000\000
) と一致するかどうかを確認します。これは、デコーダが大量のスペースを正しくスキップし、その後のz
を正確にデコードできたことを保証します。
このテストの追加により、デコーダが非データバイトを適切に処理し、期待通りに動作することが保証されるようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/9d7b9fb7d01c831e67b82f352676097835af01fc
- Go Change-ID (CL): https://golang.org/cl/90820043
参考にした情報源リンク
- この解説は、主に提供されたコミットメッセージとコードの差分情報に基づいて作成されました。
- Go言語の
io.Reader
インターフェースに関する一般的な知識。 - ASCII85エンコーディングに関する一般的な知識。