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

[インデックス 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.nbufd.buf 内の有効なバイト数を示します。デコーダは d.buf からASCII85データをデコードし、結果を呼び出し元が提供する出力バッファ p に書き込みます。

問題のシナリオは以下の通りです。

  1. Read メソッドが呼び出され、入力ストリームからデータを読み込もうとします。
  2. 読み込んだデータの中に、ASCII85の有効なデータ文字ではなく、大量の空白文字などの非データバイトが含まれています。
  3. これらの非データバイトはデコード処理では無視されますが、d.buf には蓄積されていきます。
  4. d.buf が非データバイトで満杯になると、デコーダはそれ以上新しい入力データを読み込むことができません。
  5. しかし、デコードすべき有効なデータがないため、Read メソッドは n=0 を返そうとしますが、内部バッファが詰まっているため、新しいデータを読み込むこともできず、結果として Read がブロックされるか、期待通りに動作しなくなるという状態に陥っていました。

この修正は、Read メソッドのループ内で、出力バッファ p に何も書き込めなかった場合 (ndst == 0) かつエラーがない場合に、内部バッファ d.buf を検査し、非データバイトを積極的に取り除くロジックを追加することで問題を解決します。これにより、内部バッファにスペースが確保され、デコーダが新しい入力データを読み込み続けられるようになります。

コアとなるコードの変更箇所

変更は主に src/pkg/encoding/ascii85/ascii85.godecoder 型の 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 の修正

追加されたコードブロックは、decoderRead メソッドのメインループ内にあります。

		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: デコーダに現在エラーが発生していない場合。

この条件は、「デコーダが有効なデータを生成できなかったが、エラー状態でもない」という、内部バッファが非データバイトで詰まっている可能性のある状況を捉えています。

ブロック内の処理は以下の通りです。

  1. off := 0: d.buf 内の有効なデータバイトを書き込むためのオフセット(インデックス)を初期化します。
  2. for i := 0; i < d.nbuf; i++: 内部バッファ d.buf の現在有効な範囲をループします。
  3. if d.buf[i] > ' ': 各バイトをチェックし、それがASCII85の非データバイト(スペース、タブ、改行など、ASCII値がスペース以下)ではないかどうかを判断します。ASCII85では、スペース文字(ASCII値 32)以下の文字は通常、非データバイトとして扱われます。
  4. d.buf[off] = d.buf[i]: もし現在のバイト d.buf[i] が非データバイトではない場合、それを d.buf の先頭 (off が指す位置) にコピーします。
  5. off++: 次の有効なデータバイトを書き込む位置を更新します。
  6. d.nbuf = off: ループが終了した後、d.nbufoff の値に更新します。これにより、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))): 作成した文字列 sascii85.NewDecoder に渡し、ioutil.ReadAll を使用してデコードされたすべてのデータを読み込もうとします。
  • if err != nil: エラーが発生しないことを確認します。以前のバグでは、ここでハングアップするか、予期せぬエラーが発生する可能性がありました。
  • if want := []byte("\000\000\000\000"); !bytes.Equal(want, decoded): デコードされた結果が期待される4バイトのゼロ (\000\000\000\000) と一致するかどうかを確認します。これは、デコーダが大量のスペースを正しくスキップし、その後の z を正確にデコードできたことを保証します。

このテストの追加により、デコーダが非データバイトを適切に処理し、期待通りに動作することが保証されるようになりました。

関連リンク

参考にした情報源リンク

  • この解説は、主に提供されたコミットメッセージとコードの差分情報に基づいて作成されました。
  • Go言語の io.Reader インターフェースに関する一般的な知識。
  • ASCII85エンコーディングに関する一般的な知識。