[インデックス 13971] ファイルの概要
このコミットは、Go言語の標準ライブラリ compress/lzw パッケージにおける lzw.Writer の Write メソッドが、書き込まれたバイト数を誤って返す問題を修正するものです。具体的には、Write メソッドの戻り値の処理を改善し、入力されたバイト列の長さを正確に返すように変更されています。この修正は、GoのIssue #4160 に対応しています。
コミット
commit 791ac65b8271ad374df3877d94c3b5eee9c09537
Author: Nigel Tao <nigeltao@golang.org>
Date: Thu Sep 27 13:29:39 2012 +1000
lzw: fix Write returning the wrong number of bytes written.
Fixes #4160.
R=rsc, r
CC=golang-dev
https://golang.org/cl/6564060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/791ac65b8271ad374df3877d94c3b5eee9c09537
元コミット内容
lzw: fix Write returning the wrong number of bytes written.
Fixes #4160.
R=rsc, r
CC=golang-dev
https://golang.org/cl/6564060
変更の背景
Go言語の io.Writer インターフェースは、Write(p []byte) (n int, err error) というシグネチャを持ちます。ここで n は実際に書き込まれたバイト数を示します。compress/lzw パッケージの lzw.Writer もこのインターフェースを実装しており、その Write メソッドは圧縮処理を行いながらデータを書き込みます。
しかし、元の実装では、Write メソッドが p の全バイトを処理した場合でも、len(p) を直接返さずに、内部的なエラー処理の都合で 0 を返してしまう可能性がありました。特に、e.incHi() メソッド(LZWの辞書サイズを増やす処理)が errOutOfCodes を返した場合、Write メソッドは continue して処理を続行しますが、最終的な戻り値が 0 になってしまうというバグがありました。これは、io.Writer の期待される振る舞い(書き込んだバイト数を正確に返す)に反しており、この lzw.Writer を利用する上位のアプリケーションで予期せぬ動作やデータ破損を引き起こす可能性がありました。
この問題はGoのIssue #4160として報告されており、このコミットはその問題を解決するために作成されました。
前提知識の解説
LZW (Lempel-Ziv-Welch) 圧縮アルゴリズム
LZWは、可逆圧縮アルゴリズムの一種で、特に繰り返しパターンが多いデータに対して高い圧縮率を発揮します。GIF画像、TIFF画像、PDFファイルなどで広く利用されています。
LZWの基本的な仕組みは以下の通りです。
- 辞書 (Dictionary): 既知のバイト列(シーケンス)とそれに対応するコード(インデックス)を格納する辞書を動的に構築します。初期状態では、すべての単一バイトが辞書に登録されています。
- 最長一致検索: 入力データストリームから、現在辞書に登録されている最長のバイト列を探します。
- コード出力: 見つかった最長一致のバイト列に対応するコードを出力ストリームに書き込みます。
- 辞書更新: 最長一致のバイト列に、次に続く1バイトを追加した新しいバイト列を辞書に登録します。これにより、より長いパターンを認識できるようになります。
- リセット: 辞書が一杯になった場合、辞書を初期状態に戻す(クリアする)「リセットコード」を出力し、辞書を再構築します。これは、データの内容が大きく変化した場合に圧縮効率を維持するために重要です。
Go言語の io.Writer インターフェース
Go言語の io パッケージは、入出力操作のための基本的なインターフェースを定義しています。io.Writer インターフェースは、データを書き込むための抽象化を提供し、以下のように定義されています。
type Writer interface {
Write(p []byte) (n int, err error)
}
Writeメソッドは、pのデータを書き込もうとします。nは、実際に書き込まれたバイト数です。0 <= n <= len(p)の範囲でなければなりません。errは、書き込み中に発生したエラーです。n < len(p)であっても、Writeは非nilのエラーを返すことがあります。n == len(p)であれば、Writeは成功したと見なされ、nilエラーを返します。
このインターフェースの規約に従うことは、Goのエコシステムにおける相互運用性にとって非常に重要です。
Go言語の compress/lzw パッケージ
compress/lzw パッケージは、Go言語でLZW圧縮および解凍を行うための実装を提供します。
lzw.NewWriter(w io.Writer, order Order, litWidth int) *Writer: LZW圧縮データをwに書き込む新しいWriterを作成します。orderはバイトオーダー(LSBまたはMSB)、litWidthはリテラルシンボルのビット幅を指定します。lzw.Writer型はio.Writerインターフェースを実装しており、そのWriteメソッドが入力データを圧縮し、基になるio.Writerに出力します。
技術的詳細
このコミットの技術的詳細は、lzw.Writer の内部構造と、Write メソッドの戻り値の処理に関するものです。
lzw.Writer は内部的に encoder 構造体を持っており、実際の圧縮ロジックはこの encoder が担当します。encoder.Write メソッドは、入力バイトスライス p を受け取り、LZWアルゴリズムに従って圧縮処理を行います。
問題の核心は、Write メソッドが io.Writer インターフェースの規約(書き込んだバイト数を正確に返す)を遵守していなかった点にあります。
元のコードでは、Write メソッドのシグネチャが func (e *encoder) Write(p []byte) (int, error) であり、戻り値に名前が付けられていませんでした。関数内で err というローカル変数が宣言されており、これが io.Writer インターフェースの戻り値の err と衝突する可能性がありました。
特に問題となったのは、LZWの辞書が一杯になり、e.incHi() が errOutOfCodes を返して辞書がリセットされるケースです。この場合、Write メソッドは continue ステートメントでループの先頭に戻り、処理を続行します。しかし、最終的に return len(p), nil が実行される前に、e.err = err のような行でローカル変数 err の値が e.err に代入され、その後の return 0, e.err が実行されると、たとえ len(p) が 0 でない場合でも 0 が返されてしまう可能性がありました。
この修正では、以下の点が改善されています。
- 名前付き戻り値の導入:
func (e *encoder) Write(p []byte) (n int, err error)とすることで、nとerrという名前付き戻り値が導入されました。これにより、関数内でnとerrを直接操作できるようになり、関数の最後に明示的にreturn n, errと書かなくても、関数が終了する際に現在のnとerrの値が自動的に返されるようになります。 nの初期化:n = len(p)という行が追加され、Writeメソッドの冒頭で、入力バイトスライスpの長さがnに初期値として設定されます。これにより、Writeメソッドが正常に終了した場合、常にlen(p)が返されることが保証されます。- 変数名の衝突回避:
e.incHi()から返されるエラーを格納するローカル変数の名前がerrからerr1に変更されました。これにより、名前付き戻り値のerrとの衝突が回避され、コードの可読性と正確性が向上しました。
これらの変更により、lzw.Writer.Write メソッドは io.Writer インターフェースの規約を正しく遵守し、常に書き込まれたバイト数(入力バイトスライスの長さ)を正確に返すようになりました。
コアとなるコードの変更箇所
src/pkg/compress/lzw/writer.go
--- a/src/pkg/compress/lzw/writer.go
+++ b/src/pkg/compress/lzw/writer.go
@@ -131,13 +131,14 @@ func (e *encoder) incHi() error {
}
// Write writes a compressed representation of p to e's underlying writer.
-func (e *encoder) Write(p []byte) (int, error) {
+func (e *encoder) Write(p []byte) (n int, err error) {
if e.err != nil {
return 0, e.err
}
if len(p) == 0 {
return 0, nil
}
+ n = len(p)
litMask := uint32(1<<e.litWidth - 1)
code := e.savedCode
if code == invalidCode {
@@ -167,11 +168,11 @@ loop:
code = literal
// Increment e.hi, the next implied code. If we run out of codes, reset
// the encoder state (including clearing the hash table) and continue.
- if err := e.incHi(); err != nil {
- if err == errOutOfCodes {
+ if err1 := e.incHi(); err1 != nil {
+ if err1 == errOutOfCodes {
continue
}
- e.err = err
+ e.err = err1
return 0, e.err
}
// Otherwise, insert key -> e.hi into the map that e.table represents.
@@ -184,7 +185,7 @@ loop:
}
}
e.savedCode = code
- return len(p), nil
+ return n, nil
}
// Close closes the encoder, flushing any pending output. It does not close or
src/pkg/compress/lzw/writer_test.go
--- a/src/pkg/compress/lzw/writer_test.go
+++ b/src/pkg/compress/lzw/writer_test.go
@@ -96,6 +96,14 @@ func TestWriter(t *testing.T) {
}
}
+func TestWriterReturnValues(t *testing.T) {
+ w := NewWriter(ioutil.Discard, LSB, 8)
+ n, err := w.Write([]byte("asdf"))
+ if n != 4 || err != nil {
+ t.Errorf("got %d, %v, want 4, nil", n, err)
+ }
+}
+
func benchmarkEncoder(b *testing.B, n int) {
b.StopTimer()
b.SetBytes(int64(n))
コアとなるコードの解説
src/pkg/compress/lzw/writer.go の変更点
-
Writeメソッドのシグネチャ変更:-func (e *encoder) Write(p []byte) (int, error) { +func (e *encoder) Write(p []byte) (n int, err error) {Writeメソッドの戻り値にn int, err errorという名前が付けられました。これにより、関数内でnとerrを明示的に設定し、関数終了時にそれらの値が自動的に返されるようになります。 -
nの初期化:if len(p) == 0 { return 0, nil } + n = len(p)入力スライス
pの長さが0でない場合、nにlen(p)が代入されます。これにより、Writeメソッドが正常に処理を完了した場合、常にpの元の長さがnとして返されることが保証されます。 -
ローカル変数名の変更:
- if err := e.incHi(); err != nil { - if err == errOutOfCodes { + if err1 := e.incHi(); err1 != nil { + if err1 == errOutOfCodes { continue } - e.err = err + e.err = err1 return 0, e.err }e.incHi()から返されるエラーを捕捉するローカル変数の名前がerrからerr1に変更されました。これは、名前付き戻り値のerrとの衝突を避けるためです。これにより、e.err = err1の行が、ローカル変数err1の値をencoderのエラーフィールドe.errに正しく代入するようになります。 -
最終的な戻り値の変更:
- return len(p), nil + return n, nil関数の最後に
len(p)を直接返す代わりに、初期化されたnを返すように変更されました。これにより、Writeメソッドの処理中にnの値が変更されることがないため、常にpの元の長さが返されるという意図が明確になります。
src/pkg/compress/lzw/writer_test.go の変更点
- 新しいテストケース
TestWriterReturnValuesの追加:
このテストケースは、func TestWriterReturnValues(t *testing.T) { w := NewWriter(ioutil.Discard, LSB, 8) n, err := w.Write([]byte("asdf")) if n != 4 || err != nil { t.Errorf("got %d, %v, want 4, nil", n, err) } }lzw.WriterのWriteメソッドがio.Writerインターフェースの規約に従って、書き込まれたバイト数を正確に返すことを検証します。ioutil.Discardを使用して、書き込まれたデータが破棄されるようにします。これにより、実際の圧縮出力の検証ではなく、Writeメソッドの戻り値の検証に焦点を当てることができます。w.Write([]byte("asdf"))を呼び出し、4バイトのデータ ("asdf") を書き込みます。nが4であり、errがnilであることをアサートします。これにより、Writeメソッドが期待通りにlen(p)を返していることを確認します。
この新しいテストケースの追加は、修正が正しく機能していることを保証し、将来のリグレッションを防ぐ上で非常に重要です。
関連リンク
- Go Issue #4160: https://code.google.com/p/go/issues/detail?id=4160 (古いGoのIssueトラッカーのリンクですが、GitHubのコミットメッセージに記載されています)
- Gerrit Change-Id:
6564060(GoプロジェクトのコードレビューシステムGerritの変更ID)
参考にした情報源リンク
- Go言語の
io.Writerインターフェースに関する公式ドキュメント: https://pkg.go.dev/io#Writer - LZW圧縮アルゴリズムに関する一般的な情報 (例: Wikipedia): https://ja.wikipedia.org/wiki/LZW%E圧縮
- Go言語の
compress/lzwパッケージに関する公式ドキュメント: https://pkg.go.dev/compress/lzw - Go言語における名前付き戻り値 (Named Return Values) の概念: https://go.dev/blog/declaration-syntax (Goのブログ記事「Go's Declaration Syntax」で触れられています)