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

[インデックス 13971] ファイルの概要

このコミットは、Go言語の標準ライブラリ compress/lzw パッケージにおける lzw.WriterWrite メソッドが、書き込まれたバイト数を誤って返す問題を修正するものです。具体的には、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の基本的な仕組みは以下の通りです。

  1. 辞書 (Dictionary): 既知のバイト列(シーケンス)とそれに対応するコード(インデックス)を格納する辞書を動的に構築します。初期状態では、すべての単一バイトが辞書に登録されています。
  2. 最長一致検索: 入力データストリームから、現在辞書に登録されている最長のバイト列を探します。
  3. コード出力: 見つかった最長一致のバイト列に対応するコードを出力ストリームに書き込みます。
  4. 辞書更新: 最長一致のバイト列に、次に続く1バイトを追加した新しいバイト列を辞書に登録します。これにより、より長いパターンを認識できるようになります。
  5. リセット: 辞書が一杯になった場合、辞書を初期状態に戻す(クリアする)「リセットコード」を出力し、辞書を再構築します。これは、データの内容が大きく変化した場合に圧縮効率を維持するために重要です。

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 が返されてしまう可能性がありました。

この修正では、以下の点が改善されています。

  1. 名前付き戻り値の導入: func (e *encoder) Write(p []byte) (n int, err error) とすることで、nerr という名前付き戻り値が導入されました。これにより、関数内で nerr を直接操作できるようになり、関数の最後に明示的に return n, err と書かなくても、関数が終了する際に現在の nerr の値が自動的に返されるようになります。
  2. n の初期化: n = len(p) という行が追加され、Write メソッドの冒頭で、入力バイトスライス p の長さが n に初期値として設定されます。これにより、Write メソッドが正常に終了した場合、常に len(p) が返されることが保証されます。
  3. 変数名の衝突回避: 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 の変更点

  1. Write メソッドのシグネチャ変更:

    -func (e *encoder) Write(p []byte) (int, error) {
    +func (e *encoder) Write(p []byte) (n int, err error) {
    

    Write メソッドの戻り値に n int, err error という名前が付けられました。これにより、関数内で nerr を明示的に設定し、関数終了時にそれらの値が自動的に返されるようになります。

  2. n の初期化:

    	if len(p) == 0 {
    		return 0, nil
    	}
    +	n = len(p)
    

    入力スライス p の長さが 0 でない場合、nlen(p) が代入されます。これにより、Write メソッドが正常に処理を完了した場合、常に p の元の長さが n として返されることが保証されます。

  3. ローカル変数名の変更:

    -		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 に正しく代入するようになります。

  4. 最終的な戻り値の変更:

    -	return len(p), nil
    +	return n, nil
    

    関数の最後に len(p) を直接返す代わりに、初期化された n を返すように変更されました。これにより、Write メソッドの処理中に n の値が変更されることがないため、常に p の元の長さが返されるという意図が明確になります。

src/pkg/compress/lzw/writer_test.go の変更点

  1. 新しいテストケース 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.WriterWrite メソッドが io.Writer インターフェースの規約に従って、書き込まれたバイト数を正確に返すことを検証します。
    • ioutil.Discard を使用して、書き込まれたデータが破棄されるようにします。これにより、実際の圧縮出力の検証ではなく、Write メソッドの戻り値の検証に焦点を当てることができます。
    • w.Write([]byte("asdf")) を呼び出し、4バイトのデータ ("asdf") を書き込みます。
    • n4 であり、errnil であることをアサートします。これにより、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)

参考にした情報源リンク