[インデックス 12564] ファイルの概要
このコミットは、Go言語の標準ライブラリ archive/tar
パッケージにおける、tarアーカイブの読み書きに関する改善を含んでいます。具体的には、writer.go
における「ショートライト(short writes)」の検出とエラー処理の強化、および reader.go
と writer.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
を返し、かつ err
が nil
である場合に発生します。これは、io.Writer
の仕様上許容される動作ですが、多くのアプリケーションでは、指定されたデータが完全に書き込まれることを期待します。特に、ファイルフォーマットを扱うライブラリでは、ショートライトはデータの不整合を引き起こす可能性があるため、適切に処理される必要があります。
3. tarアーカイブの構造
tarアーカイブは、基本的に一連の「ヘッダブロック」と「データブロック」から構成されます。各ファイルやディレクトリは、そのメタデータを含むヘッダブロックと、ファイルの内容を含むデータブロック(存在する場合)によって表現されます。これらのブロックは通常、512バイトの倍数でアラインされます。アーカイブの終端は、2つの連続するゼロブロック(すべてがヌルバイトのブロック)で示されます。
archive/tar
パッケージの Writer
は、この構造に従ってデータを書き込みます。WriteHeader
でヘッダを書き込み、Write
でファイルの内容を書き込みます。ファイルの内容がヘッダで宣言されたサイズと一致しない場合、またはショートライトが発生した場合、アーカイブは破損する可能性があります。
技術的詳細
このコミットでは、主に archive/tar/writer.go
の Flush
メソッドと Close
メソッドに重要な変更が加えられています。
writer.go
の変更点
-
エラーメッセージの一貫性:
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")
-
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 } // ... 既存のパディング処理 ... }
-
Close()
メソッドにおけるエラー伝播:Close()
メソッドは、アーカイブの書き込みを終了し、必要なトレーラーブロック(2つのゼロブロック)を書き込みます。変更前は、Close()
はFlush()
を呼び出した後、tw.err
の状態に関わらずnil
を返す可能性がありました。変更後、
Close()
はFlush()
を呼び出した後、tw.err
がnil
でない場合にそのエラーを返します。これにより、Flush()
で捕捉されたショートライトエラーがClose()
を通じて呼び出し元に適切に伝播されるようになります。func (tw *Writer) Close() error { // ... 既存の処理 ... tw.Flush() tw.closed = true if tw.err != nil { return tw.err } // ... 既存のトレーラーブロック書き込み処理 ... }
reader.go
の変更点
-
エラーメッセージの一貫性:
ErrHeader
エラー変数にarchive/tar:
というプレフィックスが追加されました。- ErrHeader = errors.New("invalid tar header") + ErrHeader = errors.New("archive/tar: invalid tar header")
writer_test.go
の変更点
これらの変更を検証するために、テストファイル writer_test.go
も更新されています。
-
strings
パッケージのインポート: 新しいテストケースでstrings.Repeat
を使用するためにstrings
パッケージがインポートされました。 -
テストケースの追加/修正:
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.err
がnil
でない場合にそのエラーを返すようになりました。これは、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アーカイブへの書き込みにおける堅牢性が向上し、不完全な書き込みによるアーカイブの破損リスクが低減されました。また、エラーメッセージの一貫性により、デバッグやエラーハンドリングが容易になっています。
関連リンク
- Go CL 5777064: https://golang.org/cl/5777064
参考にした情報源リンク
- Go
io.Writer
documentation: https://pkg.go.dev/io#Writer - Go
archive/tar
documentation: https://pkg.go.dev/archive/tar - Tar (computing) - Wikipedia: https://en.wikipedia.org/wiki/Tar_(computing)
- Go: Error handling and
io.Writer
short writes: https://dave.cheney.net/2019/01/27/error-handling-and-io-writer-short-writes (これはコミット後の情報ですが、ショートライトの概念を理解するのに役立ちます) - Go: Error messages in standard library: https://go.dev/wiki/CodeReviewComments#error-strings (エラーメッセージの慣習に関する情報)