[インデックス 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」で触れられています)