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

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

このコミットは、Go言語の標準ライブラリ archive/tar パッケージにおける、tarアーカイブの読み書きに関する改善を含んでいます。具体的には、writer.go における「ショートライト(short writes)」の検出とエラー処理の強化、および reader.gowriter.go 全体でのエラーメッセージの一貫性向上に焦点を当てています。また、これらの変更を検証するためのテストケースが writer_test.go に追加・修正されています。

変更されたファイルは以下の通りです。

  • src/pkg/archive/tar/reader.go
  • src/pkg/archive/tar/writer.go
  • src/pkg/archive/tar/writer_test.go

コミット

  • コミットハッシュ: d75abb7ca323ad8911b900cb4955e533e35f4559
  • 作者: David Symonds dsymonds@golang.org
  • コミット日時: Mon Mar 12 17:33:35 2012 +1100

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

https://github.com/golang/go/commit/d75abb7ca323ad8911b900cb4955e533e35f4559

元コミット内容

    archive/tar: catch short writes.
    
    Also make error messages consistent throughout.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5777064

変更の背景

このコミットの主な背景は、archive/tar パッケージがtarアーカイブへの書き込みを行う際に、予期せぬ「ショートライト」が発生した場合にそれを適切に検出し、エラーとして報告することにあります。

「ショートライト」とは、io.Writer インターフェースの実装において、Write メソッドが引数で指定されたバイト数よりも少ないバイト数を実際に書き込み、かつエラーを返さない状況を指します。これは、通常、書き込み先のバッファやストリームが一時的に満杯である、あるいは何らかの理由で一部しか書き込めなかったが、致命的なエラーではない場合に発生し得ます。しかし、tarアーカイブのような厳密なフォーマットでは、指定されたデータが完全に書き込まれないことは、アーカイブの破損や後続の読み取りエラーに直結します。

以前の実装では、このようなショートライトが発生しても、Writer がそれを検知してエラーとして報告するメカニズムが不十分でした。その結果、不完全なtarアーカイブが生成される可能性がありました。

また、エラーメッセージの一貫性も改善の対象となりました。Goの標準ライブラリでは、エラーメッセージにパッケージ名をプレフィックスとして含めることが推奨されており、これによりエラーの発生源が明確になります。このコミットでは、既存のエラーメッセージに archive/tar: というプレフィックスを追加し、パッケージ全体でのエラー報告の一貫性を高めています。

前提知識の解説

1. archive/tar パッケージ

archive/tar はGo言語の標準ライブラリの一部で、tarアーカイブ(テープアーカイブ)の読み書きをサポートします。tarは、複数のファイルを一つのアーカイブファイルにまとめるためのフォーマットであり、主にファイルのバックアップや配布に利用されます。このパッケージは、ファイルのメタデータ(パーミッション、タイムスタンプ、所有者など)と内容をtarフォーマットでエンコード・デコードする機能を提供します。

2. io.Writer インターフェースとショートライト

Go言語の io.Writer インターフェースは、データを書き込むための基本的な抽象化を提供します。その Write メソッドは (n int, err error) を返します。ここで n は実際に書き込まれたバイト数、err は書き込み中に発生したエラーです。

「ショートライト」は、Write メソッドが len(p) (書き込もうとしたバイト数) よりも小さい n を返し、かつ errnil である場合に発生します。これは、io.Writer の仕様上許容される動作ですが、多くのアプリケーションでは、指定されたデータが完全に書き込まれることを期待します。特に、ファイルフォーマットを扱うライブラリでは、ショートライトはデータの不整合を引き起こす可能性があるため、適切に処理される必要があります。

3. tarアーカイブの構造

tarアーカイブは、基本的に一連の「ヘッダブロック」と「データブロック」から構成されます。各ファイルやディレクトリは、そのメタデータを含むヘッダブロックと、ファイルの内容を含むデータブロック(存在する場合)によって表現されます。これらのブロックは通常、512バイトの倍数でアラインされます。アーカイブの終端は、2つの連続するゼロブロック(すべてがヌルバイトのブロック)で示されます。

archive/tar パッケージの Writer は、この構造に従ってデータを書き込みます。WriteHeader でヘッダを書き込み、Write でファイルの内容を書き込みます。ファイルの内容がヘッダで宣言されたサイズと一致しない場合、またはショートライトが発生した場合、アーカイブは破損する可能性があります。

技術的詳細

このコミットでは、主に archive/tar/writer.goFlush メソッドと Close メソッドに重要な変更が加えられています。

writer.go の変更点

  1. エラーメッセージの一貫性: ErrWriteTooLong, ErrFieldTooLong, ErrWriteAfterClose といった既存のエラー変数に archive/tar: というプレフィックスが追加されました。これにより、これらのエラーが archive/tar パッケージから発生したものであることが明確になります。

    -	ErrWriteTooLong    = errors.New("write too long")
    -	ErrFieldTooLong    = errors.New("header field too long")
    -	ErrWriteAfterClose = errors.New("write after close")
    +	ErrWriteTooLong    = errors.New("archive/tar: write too long")
    +	ErrFieldTooLong    = errors.New("archive/tar: header field too long")
    +	ErrWriteAfterClose = errors.New("archive/tar: write after close")
    
  2. Flush() メソッドにおけるショートライトの検出: Writer 構造体には nb フィールドがあり、これは現在のファイルエントリに対してまだ書き込まれていない(または書き込みが不足している)バイト数を追跡します。Flush() メソッドは、現在のファイルエントリの書き込みを完了させるために呼び出されます。

    変更前は、Flush() は主にパディングバイトの書き込みを処理していましたが、nb がゼロでない(つまり、ショートライトが発生した)場合にエラーを報告するメカニズムがありませんでした。

    変更後、Flush() の冒頭で tw.nb > 0 がチェックされます。もし nb が正の値であれば、それはヘッダで宣言されたファイルサイズに対して、実際に書き込まれたバイト数が不足していることを意味します。この場合、fmt.Errorf を使用して「missed writing %d bytes」というエラーを生成し、tw.err に設定して返します。これにより、不完全な書き込みが即座にエラーとして捕捉されるようになります。

    func (tw *Writer) Flush() error {
    	if tw.nb > 0 {
    		tw.err = fmt.Errorf("archive/tar: missed writing %d bytes", tw.nb)
    		return tw.err
    	}
    	// ... 既存のパディング処理 ...
    }
    
  3. Close() メソッドにおけるエラー伝播: Close() メソッドは、アーカイブの書き込みを終了し、必要なトレーラーブロック(2つのゼロブロック)を書き込みます。変更前は、Close()Flush() を呼び出した後、tw.err の状態に関わらず nil を返す可能性がありました。

    変更後、Close()Flush() を呼び出した後、tw.errnil でない場合にそのエラーを返します。これにより、Flush() で捕捉されたショートライトエラーが Close() を通じて呼び出し元に適切に伝播されるようになります。

    func (tw *Writer) Close() error {
    	// ... 既存の処理 ...
    	tw.Flush()
    	tw.closed = true
    	if tw.err != nil {
    		return tw.err
    	}
    	// ... 既存のトレーラーブロック書き込み処理 ...
    }
    

reader.go の変更点

  1. エラーメッセージの一貫性: ErrHeader エラー変数に archive/tar: というプレフィックスが追加されました。

    -	ErrHeader = errors.New("invalid tar header")
    +	ErrHeader = errors.New("archive/tar: invalid tar header")
    

writer_test.go の変更点

これらの変更を検証するために、テストファイル writer_test.go も更新されています。

  1. strings パッケージのインポート: 新しいテストケースで strings.Repeat を使用するために strings パッケージがインポートされました。

  2. テストケースの追加/修正: writerTests の中に、iotest.TruncateWriter を使用して意図的にショートライトを発生させるテストケースが追加されました。iotest.TruncateWriter は、指定されたバイト数までしか書き込みを許可しない io.Writer のラッパーです。

    このテストでは、大きなコンテンツを持つエントリ(4KBのゼロバイト)を書き込もうとしますが、TruncateWriter によって書き込みが4KBに制限されます。これにより、Writer がショートライトを検出し、Flush および Close メソッドがエラーを返すことを期待します。

    また、Close() のエラーチェックが修正され、big フラグ(エントリのサイズが1KBより大きいかどうかを示す)が導入されました。big なテストケースでは、TruncateWriter の使用により Close() がエラーを返すことが期待されるため、そのエラーは無視されません。逆に、big でない(小さい)テストケースでは、Close() がエラーを返すべきではないため、エラーが発生した場合はテストが失敗します。

    // writerTests の一部
    {
        // ...
        // fake contents
        contents: strings.Repeat("\x00", 4<<10), // 4KBのゼロバイト
    },
    // ...
    
    // testLoop 内
    tw := NewWriter(iotest.TruncateWriter(buf, 4<<10)) // 最初の4KBのみを許可
    big := false
    for j, entry := range test.entries {
        big = big || entry.header.Size > 1<<10 // エントリが1KBより大きいか
        // ... WriteHeader, Write の呼び出し ...
    }
    // Only interested in Close failures for the small tests.
    if err := tw.Close(); err != nil && !big { // bigでないテストでのみCloseエラーをチェック
        t.Errorf("test %d: Failed closing archive: %v", i, err)
        continue testLoop
    }
    

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

src/pkg/archive/tar/reader.go

--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -18,7 +18,7 @@ import (
 )
 
 var (
-	ErrHeader = errors.New("invalid tar header")
+	ErrHeader = errors.New("archive/tar: invalid tar header")
 )
 
 // A Reader provides sequential access to the contents of a tar archive.

src/pkg/archive/tar/writer.go

--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -5,18 +5,19 @@
 package tar
 
 // TODO(dsymonds):
-// - catch more errors (no first header, write after close, etc.)
+// - catch more errors (no first header, etc.)
 
 import (
 	"errors"
+\t"fmt"
 	"io"
 	"strconv"
 )
 
 var (
-\tErrWriteTooLong    = errors.New("write too long")
-\tErrFieldTooLong    = errors.New("header field too long")
-\tErrWriteAfterClose = errors.New("write after close")
+\tErrWriteTooLong    = errors.New("archive/tar: write too long")
+\tErrFieldTooLong    = errors.New("archive/tar: header field too long")
+\tErrWriteAfterClose = errors.New("archive/tar: write after close")
 )
 
 // A Writer provides sequential writing of a tar archive in POSIX.1 format.
@@ -48,6 +49,11 @@ func NewWriter(w io.Writer) *Writer { return &Writer{w: w} }
 
 // Flush finishes writing the current file (optional).
 func (tw *Writer) Flush() error {
+\tif tw.nb > 0 {
+\t\ttw.err = fmt.Errorf("archive/tar: missed writing %d bytes", tw.nb)
+\t\treturn tw.err
+\t}\
+\
 	n := tw.nb + tw.pad
 	for n > 0 && tw.err == nil {
 		nr := n
@@ -193,6 +199,9 @@ func (tw *Writer) Close() error {
 	}
 	tw.Flush()
 	tw.closed = true
+\tif tw.err != nil {
+\t\treturn tw.err
+\t}\
 
 	// trailer: two zero blocks
 	for i := 0; i < 2; i++ {

src/pkg/archive/tar/writer_test.go

--- a/src/pkg/archive/tar/writer_test.go
+++ b/src/pkg/archive/tar/writer_test.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+\t"strings"
 	"testing"
 	"testing/iotest"
 	"time"
@@ -95,7 +96,8 @@ var writerTests = []*writerTest{
 					Uname:    "dsymonds",
 					Gname:    "eng",
 				},
-\t\t\t\t// no contents
+\t\t\t\t// fake contents
+\t\t\t\tcontents: strings.Repeat("\x00", 4<<10),\n
 			},\n
 		},\n
 	},\n
@@ -150,7 +152,9 @@ testLoop:\n 
 		buf := new(bytes.Buffer)
 		tw := NewWriter(iotest.TruncateWriter(buf, 4<<10)) // only catch the first 4 KB
+\t\tbig := false
 		for j, entry := range test.entries {
+\t\t\tbig = big || entry.header.Size > 1<<10
 			if err := tw.WriteHeader(entry.header); err != nil {
 				t.Errorf("test %d, entry %d: Failed writing header: %v", i, j, err)
 				continue testLoop
@@ -160,7 +164,8 @@ testLoop:\n 			continue testLoop
 			}
 		}
-\t\tif err := tw.Close(); err != nil {\n+\t\t// Only interested in Close failures for the small tests.\n+\t\tif err := tw.Close(); err != nil && !big {\n 			t.Errorf("test %d: Failed closing archive: %v", i, err)
 			continue testLoop
 		}

コアとなるコードの解説

writer.go

  • エラーメッセージの変更: ErrWriteTooLong, ErrFieldTooLong, ErrWriteAfterClose の各エラーメッセージに archive/tar: というプレフィックスが追加されました。これは、Goの標準ライブラリにおけるエラーメッセージの慣習に従い、エラーの発生源を明確にするための変更です。これにより、ユーザーはどのパッケージからエラーが返されたのかを容易に識別できます。

  • Flush() メソッドの変更: Flush() メソッドは、現在のファイルエントリの書き込みが完了したことを保証するために呼び出されます。この変更の核心は、tw.nb (number of bytes) がゼロより大きい場合にエラーを返すようになった点です。tw.nb は、Writer が現在のファイルエントリに対して書き込むべき残りのバイト数を追跡します。もし Flush() が呼び出された時点で tw.nb が正の値であれば、それは Write メソッドがヘッダで宣言されたファイルサイズ分のデータを完全に書き込めなかった(ショートライトが発生した)ことを意味します。この場合、fmt.Errorf を使って具体的なエラーメッセージ(例: "archive/tar: missed writing 123 bytes")を生成し、それを tw.err に設定して返します。これにより、不完全な書き込みが早期に検出され、アーカイブの破損を防ぐことができます。

  • Close() メソッドの変更: Close() メソッドは、アーカイブ全体の書き込みを終了し、tarアーカイブの終端を示す2つのゼロブロックを書き込みます。この変更では、Close()Flush() を呼び出した後に、tw.errnil でない場合にそのエラーを返すようになりました。これは、Flush() で捕捉されたショートライトエラーが Close() を通じて呼び出し元に適切に伝播されるようにするためです。これにより、Writer のライフサイクル全体で発生したエラーが確実に報告されるようになります。

reader.go

  • エラーメッセージの変更: ErrHeader エラーメッセージにも archive/tar: というプレフィックスが追加されました。これは writer.go と同様に、エラーメッセージの一貫性を保つための変更です。

writer_test.go

  • テストケースの追加と修正: iotest.TruncateWriter を使用して、Writer がショートライトを適切に処理できるかを検証するテストが追加されました。TruncateWriter は、基になる io.Writer への書き込みを指定されたバイト数で打ち切ることで、意図的にショートライトをシミュレートします。このテストは、Flush() および Close() メソッドがショートライトを検出し、期待されるエラーを返すことを確認します。 また、Close() のエラーチェックロジックが修正され、big フラグが導入されました。これは、大きなファイル(1KB超)のテストケースでは、TruncateWriter の使用により Close() がエラーを返すことが期待されるため、そのエラーを無視しないようにするためです。これにより、テストがより正確になり、ショートライトの検出ロジックが正しく機能していることを確認できます。

これらの変更により、archive/tar パッケージは、tarアーカイブへの書き込みにおける堅牢性が向上し、不完全な書き込みによるアーカイブの破損リスクが低減されました。また、エラーメッセージの一貫性により、デバッグやエラーハンドリングが容易になっています。

関連リンク

参考にした情報源リンク