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

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

このコミットは、Go言語の標準ライブラリbufioパッケージにおけるReader型のUnreadByteメソッドのバグを修正するものです。具体的には、UnreadByteが複数回連続して呼び出された場合に、期待される動作(直前に読み込んだ1バイトのみをアンリードする)に反して、それ以前のバイトもアンリードできてしまう問題を解決しています。また、関連するテストのエラーメッセージの改善と対称性の向上も行われています。

コミット

commit 8bd9242f7cd27f69449b2cdae8acbf25a952134c
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Apr 9 14:19:13 2014 -0700

    bufio: fix UnreadByte
    
    Also:
    - fix error messages in tests
    - make tests more symmetric
    
    Fixes #7607.
    
    LGTM=r
    R=r
    CC=golang-codereviews
    https://golang.org/cl/86180043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8bd9242f7cd27f69449b2cdae8acbf25a952134c

元コミット内容

bufio: fix UnreadByte

Also:
- fix error messages in tests
- make tests more symmetric

Fixes #7607.

LGTM=r
R=r
CC=golang-codereviews
https://golang.org/cl/86180043

変更の背景

このコミットは、GoのbufioパッケージのReader型が提供するUnreadByteメソッドの既存のバグ(Issue #7607)を修正するために行われました。UnreadByteメソッドのドキュメントには、「直前に読み込んだバイトのみをアンリードできる」と明記されています。しかし、実際の動作では、このメソッドを連続して複数回呼び出すことが可能であり、その結果、直前に読み込んだバイトだけでなく、それ以前に読み込んだバイトまでバッファを巻き戻してしまうという問題がありました。

この予期せぬ動作は、bufio.Readerの内部状態管理の不備に起因していました。特に、UnreadByteが呼び出された際に、アンリードできるバイトが本当に直前に読み込まれたものだけであるかを厳密にチェックするメカニズムが不足していました。これにより、APIの契約と実際の挙動との間に乖離が生じ、bufio.Readerを利用するアプリケーションで予期せぬバグやデータ破損が発生する可能性がありました。

このコミットは、この問題を解決し、UnreadByteメソッドがそのドキュメントに記載された通りの振る舞いをするように修正することを目的としています。

前提知識の解説

bufioパッケージ

bufioパッケージは、Go言語におけるバッファリングされたI/O操作を提供します。これにより、ディスクI/OやネットワークI/Oなどの低レベルなI/O操作の回数を減らし、パフォーマンスを向上させることができます。bufio.Readerは、io.Readerインターフェースをラップし、内部バッファを使用してデータを効率的に読み込む機能を提供します。

bufio.Readerの内部構造と動作

bufio.Readerは、内部にバイトスライス(buf)をバッファとして持ち、以下のフィールドでその状態を管理します。

  • buf []byte: データを保持する内部バッファ。
  • r int: バッファ内の読み込み位置(次に読み込むバイトのインデックス)。
  • w int: バッファ内の書き込み位置(バッファにデータが読み込まれた最後のバイトの次のインデックス)。
  • lastByte int: 直前にReadByteまたはReadRuneで読み込まれたバイトを保持します。アンリード操作のために使用されます。初期値は-1。
  • lastRuneSize int: 直前にReadRuneで読み込まれたルーンのバイトサイズを保持します。アンリード操作のために使用されます。初期値は-1。

ReadByte()メソッドが呼び出されると、b.buf[b.r]から1バイトが読み込まれ、b.rがインクリメントされます。読み込まれたバイトはb.lastByteに保存されます。

UnreadByte()メソッド

UnreadByte()メソッドは、直前に読み込んだ1バイトをバッファに戻す(アンリードする)ために使用されます。これにより、次にReadByte()が呼び出されたときに、同じバイトが再度読み込まれるようになります。このメソッドの重要な制約は、「直前に読み込んだバイトのみをアンリードできる」という点です。つまり、ReadByte()が呼び出された直後に一度だけUnreadByte()を呼び出すことが期待されます。複数回呼び出すとエラーになるか、予期せぬ動作を引き起こすべきではありません。

技術的詳細

修正前のUnreadByteメソッドは、b.r(読み込みポインタ)を単純にデクリメントすることでアンリードを実現していました。しかし、b.lastByteフィールドが適切に管理されていなかったため、UnreadByteが複数回呼び出された場合でも、b.rがどんどん巻き戻されてしまい、直前に読み込んだバイト以外のデータもアンリードできてしまうという問題がありました。

このコミットでは、UnreadByteのロジックが大幅に改善され、b.lastByteb.rb.wの状態をより厳密にチェックするようになりました。

主な変更点は以下の通りです。

  1. UnreadByteの前提条件の強化: 修正前はif b.r <= 0という条件でエラーを返していましたが、修正後はif b.lastByte < 0 || b.r == 0 && b.w > 0というより厳密な条件でアンリードが不正であるかを判断します。

    • b.lastByte < 0: これは、直前にReadByteまたはReadRuneが呼び出されていない、または既にアンリードされていることを意味します。この場合、アンリードできるバイトが存在しないため、エラーを返します。
    • b.r == 0 && b.w > 0: これは、バッファの先頭にいるにもかかわらず、バッファにデータが残っている状態を示します。この状態でのアンリードは通常、不正な操作です。
  2. b.rの適切なデクリメント: b.rが0より大きい場合は単純にb.r--で読み込みポインタを戻します。 しかし、b.r == 0 && b.w == 0(バッファが空の状態)の場合、これはReadByteがバッファを使い切った後に呼び出されたことを意味します。この場合、UnreadByteはバッファの先頭にバイトを書き戻す必要があります。そのため、b.w = 1としてバッファの書き込み位置を1に設定し、b.buf[0]にバイトを書き込みます。これにより、バッファが空の状態からでも正しくアンリードされたバイトを処理できるようになります。

  3. b.lastByteb.lastRuneSizeのリセット: アンリードが成功した後、b.lastByte = -1b.lastRuneSize = -1を設定することで、アンリードされたバイトが再度アンリードされるのを防ぎます。これにより、「直前に読み込んだバイトのみをアンリードできる」という制約が厳密に守られます。

これらの変更により、UnreadByteは、直前に読み込んだバイトが存在し、かつそれがまだアンリードされていない場合にのみ成功するようになり、複数回の連続した呼び出しはErrInvalidUnreadByteエラーを返すようになります。

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

このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。

  1. src/pkg/bufio/bufio.go:

    • ReadByteメソッド内のループ条件がfor b.w == b.rからfor b.r == b.wに変更されました。これは機能的な変更ではなく、慣習的な記述への修正です。
    • UnreadByteメソッドのロジックが大幅に修正されました。特に、エラー条件のチェックと、b.rおよびb.wの更新ロジックが変更されています。
    • UnreadRuneメソッドのエラー条件がif b.lastRuneSize < 0 || b.r == 0からif b.lastRuneSize < 0 || b.r < b.lastRuneSizeに変更されました。これはUnreadRuneのバグ修正も兼ねています。
  2. src/pkg/bufio/bufio_test.go:

    • TestUnreadRuneTestUnreadByte内のエラーメッセージがより具体的で分かりやすいものに修正されました。
    • テストの対称性を向上させるために、TestUnreadByteMultipleという新しいテストケースが追加されました。このテストは、UnreadByteが複数回呼び出された場合に正しくエラーを返すことを検証します。

コアとなるコードの解説

src/pkg/bufio/bufio.go の変更点

UnreadByte メソッド

 // UnreadByte unreads the last byte. Only the most recently read byte can be unread.
 func (b *Reader) UnreadByte() error {
-	b.lastRuneSize = -1
-	if b.r == b.w && b.lastByte >= 0 {
-		b.w = 1
-		b.r = 0
-		b.buf[0] = byte(b.lastByte)
-		b.lastByte = -1
-		return nil
-	}
-	if b.r <= 0 {
+	if b.lastByte < 0 || b.r == 0 && b.w > 0 {
 		return ErrInvalidUnreadByte
 	}
-	b.r--
+	// b.r > 0 || b.w == 0
+	if b.r > 0 {
+		b.r--
+	} else {
+		// b.r == 0 && b.w == 0
+		b.w = 1
+	}
+	b.buf[b.r] = byte(b.lastByte)
 	b.lastByte = -1
+	b.lastRuneSize = -1
 	return nil
 }
  • 旧ロジックの問題点: 以前のコードでは、b.r == b.w && b.lastByte >= 0という条件で特殊なケースを処理していましたが、これが複数回のアンリードを許してしまう原因となっていました。また、b.r <= 0というエラーチェックも不十分でした。
  • 新ロジックの改善点:
    • if b.lastByte < 0 || b.r == 0 && b.w > 0:この新しい条件が、アンリードが不正な操作であるかを判断する主要なゲートとなります。
      • b.lastByte < 0: ReadByteReadRuneが直前に呼び出されていない、または既にアンリードされている場合、アンリードできるバイトがないためエラーを返します。
      • b.r == 0 && b.w > 0: バッファの先頭にいるにもかかわらず、バッファにまだ読み込まれていないデータがある場合、これは通常、不正な状態でありエラーを返します。
    • if b.r > 0: 通常のケースで、読み込みポインタb.rをデクリメントしてバイトをアンリードします。
    • else { b.w = 1 }: b.r == 0 && b.w == 0(バッファが完全に空の状態)の場合、これはReadByteがバッファから最後のバイトを読み取った直後であることを意味します。この場合、アンリードされたバイトをバッファの先頭(インデックス0)に書き戻すために、b.wを1に設定します。
    • b.buf[b.r] = byte(b.lastByte): アンリードされたバイトをバッファの正しい位置に書き戻します。
    • b.lastByte = -1b.lastRuneSize = -1: アンリードが成功した後、これらの値をリセットすることで、同じバイトが再度アンリードされるのを防ぎ、UnreadByteの「直前に読み込んだバイトのみ」という制約を厳密に強制します。

UnreadRune メソッド

 // UnreadRune unreads the last rune. If the most recent read operation was not
 // a ReadRune, it returns an error. (In this regard it is stricter than UnreadByte,
 // which will unread the last byte from any read operation.)
 func (b *Reader) UnreadRune() error {
-	if b.lastRuneSize < 0 || b.r == 0 {
+	if b.lastRuneSize < 0 || b.r < b.lastRuneSize {
 		return ErrInvalidUnreadRune
 	}
 	b.r -= b.lastRuneSize
  • UnreadRuneのエラー条件も改善されました。b.r < b.lastRuneSizeという条件が追加され、アンリードしようとしているルーンのサイズ分、読み込みポインタを戻せるかどうかがより正確にチェックされるようになりました。

src/pkg/bufio/bufio_test.go の変更点

  • 既存のテストケース(TestUnreadRune, TestUnreadByte)のエラーメッセージが、t.Errorからt.Fatalやより詳細なメッセージに変更され、テスト失敗時のデバッグが容易になりました。
  • TestUnreadByteMultiple の追加: この新しいテストは、UnreadByteが複数回呼び出された場合の挙動を検証します。具体的には、ReadByteでNバイト読み込んだ後、UnreadByteを一度呼び出し、その後再度UnreadByteを呼び出してエラーが返されることを確認します。これにより、UnreadByteが一度しか機能しないという新しい制約が正しく実装されていることが保証されます。

これらの変更により、bufio.ReaderUnreadByteおよびUnreadRuneメソッドは、より堅牢で、ドキュメントに記載された通りの正確な動作をするようになりました。

関連リンク

参考にした情報源リンク