[インデックス 19351] ファイルの概要
このコミットは、Go言語の archive/tar
パッケージにおけるバグ修正と改善に関するものです。具体的には、tar
アーカイブの展開(untar)を妨げていた問題を解決し、異なる tar
フォーマット(USTARとGNU tar)間の互換性を向上させ、特に長いファイル名と大きなファイルを扱う際の堅牢性を高めています。
コミット
commit 51f3cbabfc58c0db89b1142f94d794b59727f572
Author: Guillaume J. Charmes <guillaume@charmes.net>
Date: Wed May 14 10:15:43 2014 -0700
archive/tar: Fix bug preventing untar
Do not use ustar format if we need the GNU one.
Change \000 to \x00 for consistency
Check for "ustar\x00" instead of "ustar\x00\x00" for conistency with tar
and compatiblity with archive generated with older code (which was ustar\x00\x20\x00)
Add test for long name + big file.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/99050043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/51f3cbabfc58c0db89b1142f94d794b59727f572
元コミット内容
このコミットの目的は、archive/tar
パッケージにおける tar
アーカイブの展開を妨げていたバグを修正することです。主な変更点は以下の通りです。
- USTARフォーマットとGNU tarフォーマットの混同の修正: GNU tarフォーマットが必要な場合にUSTARフォーマットを使用しないように修正されました。これは、特定のアーカイブが正しく展開されない原因となっていました。
- マジックバイトのチェックの厳密化:
tar
ヘッダー内のマジックバイトのチェックをより厳密に行うように変更されました。具体的には、"ustar\x00\x00"
ではなく"ustar\x00"
をチェックするように変更され、これは既存のtar
実装との一貫性を保ち、古いコードで生成されたアーカイブとの互換性を確保するためです。 - ヌルバイト表現の統一: 文字列リテラル内のヌルバイト表現を
\000
から\x00
に統一し、コードの一貫性を向上させました。 - テストケースの追加: 長いファイル名と大きなファイルを組み合わせた新しいテストケースが追加され、このシナリオでの
tar
アーカイブの作成と展開が正しく機能することを確認しています。
変更の背景
このコミットが行われた背景には、Go
の archive/tar
パッケージが、異なる tar
実装(特に GNU tar
)によって生成されたアーカイブを正しく処理できないという問題がありました。具体的には、以下の点が挙げられます。
- フォーマットの互換性問題:
tar
アーカイブには複数のフォーマットが存在し、それぞれがヘッダー構造やメタデータのエンコード方法に微妙な違いがあります。特にUSTAR
(POSIX.1-1988) とGNU tar
は広く使われていますが、長いファイル名や大きなファイルを扱う際の拡張方法が異なります。Go
のarchive/tar
パッケージがこれらの違いを適切に扱えていなかったため、一部のアーカイブが展開できないというバグが発生していました。 - マジックバイトの誤認識:
tar
ヘッダーには、アーカイブのフォーマットを識別するための「マジックバイト」と呼ばれる特定のバイト列が含まれています。このマジックバイトのチェックが不正確であったため、Go
のリーダーがアーカイブのフォーマットを誤認識し、結果として展開に失敗することがありました。特に、"ustar\x00\x00"
と"ustar\x00"
の違いは、tar
のバージョンや実装によって異なる解釈をされることがあり、これが互換性の問題を引き起こしていました。 - 長いファイル名と大きなファイルのサポート:
tar
フォーマットは元々、ファイル名やファイルサイズに制限がありました。これらの制限を克服するために、USTAR
やGNU tar
はそれぞれ異なる拡張メカニズムを導入しています。Go
のパッケージがこれらの拡張を完全にサポートしていなかったため、非常に長いファイル名を持つファイルや、非常に大きなサイズのファイルを含むアーカイブの処理に問題が生じていました。 - テストカバレッジの不足: 上記のようなエッジケース(特に長いファイル名と大きなファイル)に対するテストが不足していたため、問題が発見されにくく、修正後も回帰テストが不十分である可能性がありました。このコミットでは、これらのシナリオをカバーする新しいテストケースを追加することで、パッケージの堅牢性を高めています。
これらの問題は、Go
言語で tar
アーカイブを扱うアプリケーションの信頼性に直接影響するため、この修正は非常に重要でした。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. tarアーカイブフォーマット
tar
(tape archive) は、複数のファイルを一つのアーカイブファイルにまとめるためのファイルフォーマットです。元々はテープドライブにデータを保存するために設計されましたが、現在ではファイルシステム上のアーカイブとしても広く利用されています。tar
アーカイブは、各ファイルのメタデータ(ファイル名、パーミッション、所有者、タイムスタンプなど)とファイルデータが連続して格納される構造を持っています。
2. tarフォーマットのバリエーション
tar
フォーマットにはいくつかのバリエーションがあり、それぞれが異なるヘッダー構造や拡張機能を持っています。
- V7 tar (Old V7): 最も古い
tar
フォーマットで、ファイル名やファイルサイズに厳しい制限があります。 - USTAR (POSIX.1-1988): POSIX標準で定義された
tar
フォーマットです。V7 tarの制限を緩和し、より長いファイル名(100文字まで)や大きなファイルサイズをサポートします。USTARヘッダーには、フォーマットを識別するためのマジックバイト"ustar\x00"
が含まれます。 - GNU tar: GNUプロジェクトによって開発された
tar
の実装で、USTARよりもさらに多くの拡張機能を提供します。特に、非常に長いファイル名や非常に大きなファイルサイズ(8GB以上)をサポートするために、追加のヘッダーブロックや拡張ヘッダー(LongLink
、LongName
)を使用します。GNU tarもマジックバイト"ustar \x00"
を使用することがありますが、その後のバージョンフィールドやチェックサムの計算方法がUSTARとは異なる場合があります。 - PAX (POSIX.1-2001): POSIX標準の最新版で定義された
tar
フォーマットです。USTARをベースに、拡張ヘッダー(extended headers)を使用して任意のメタデータを格納できる柔軟なメカニズムを提供します。これにより、ファイル名やファイルサイズの制限を実質的に撤廃できます。
3. tarヘッダーとマジックバイト
各ファイルは tar
アーカイブ内で「ヘッダーブロック」とそれに続く「データブロック」で構成されます。ヘッダーブロックには、ファイルに関するメタデータが格納されます。
- マジックバイト (Magic Bytes):
tar
ヘッダーの特定のオフセット(通常は257バイト目から6バイト)には、アーカイブのフォーマットを識別するための「マジックバイト」と呼ばれる固定のバイト列が格納されます。USTAR
フォーマットの場合、このフィールドは通常"ustar\x00"
となります。GNU tar
の一部のバージョンでは、"ustar \x00"
のようにスペースが含まれる場合があります。- このマジックバイトの正確な値と、それに続くバージョンフィールド(通常は263バイト目から2バイト)の組み合わせによって、
tar
リーダーはアーカイブのフォーマットを判別します。
4. 長いファイル名と大きなファイルの扱い
- USTAR: 長いファイル名(100文字を超える場合)を扱うために、ヘッダーの
prefix
フィールド(155バイト)とname
フィールド(100バイト)を組み合わせて最大255文字のファイル名をサポートします。 - GNU tar: 非常に長いファイル名(255文字を超える場合)や、シンボリックリンクのターゲットが長い場合に、
././@LongLink
や././@LongName
といった特別なエントリを使用して、実際のファイル名を格納します。これにより、ファイル名の長さに実質的な制限がなくなります。また、大きなファイルサイズを扱うためにも独自の拡張を使用します。 - PAX: 拡張ヘッダー(
x
またはg
タイプのエントリ)を使用して、ファイル名やファイルサイズなどのメタデータをキーと値のペアで格納します。これにより、ファイル名やファイルサイズの制限がなくなります。
5. Go言語の archive/tar
パッケージ
Go
言語の標準ライブラリ archive/tar
は、tar
アーカイブの読み書きをサポートするためのパッケージです。このパッケージは、USTAR、GNU tar、PAXなどの主要な tar
フォーマットを扱うことができますが、その実装には各フォーマットの微妙な違いを正確に処理するための複雑さが伴います。特に、異なる tar
実装によって生成されたアーカイブとの互換性を確保することは、常に課題となります。
技術的詳細
このコミットの技術的詳細は、主に archive/tar
パッケージが tar
ヘッダーを読み書きする際のロジック、特にマジックバイトの解釈とフォーマットの決定、そして長いファイル名と大きなファイルの処理方法に関するものです。
1. reader.go
におけるマジックバイトのチェックの修正
以前の reader.go
の readHeader()
関数では、tar
ヘッダーのマジックバイトをチェックする際に、"ustar\x0000"
(USTAR) と "ustar \x00"
(old GNU tar) を厳密に比較していました。
// 変更前
switch magic {
case "ustar\x0000": // POSIX tar (1003.1-1988)
// ...
case "ustar \x00": // old GNU tar
// ...
}
このコミットでは、USTAR
フォーマットのチェックが magic[:6] == "ustar\x00"
に変更されました。
// 変更後
switch {
case magic[:6] == "ustar\x00": // POSIX tar (1003.1-1988)
if string(header[508:512]) == "tar\x00" {
format = "star" // star tar format
} else {
format = "posix" // ustar format
}
case magic == "ustar \x00": // old GNU tar
format = "gnu"
}
この変更の意図は以下の通りです。
"ustar\x00"
の柔軟な解釈:USTAR
のマジックバイトは"ustar"
の後にヌルバイトが続く6バイトです。その後の2バイトはバージョンフィールドですが、これはtar
の実装によって異なる値を持つことがあります。以前の"ustar\x0000"
という厳密な比較では、バージョンフィールドが"00"
でないUSTAR
アーカイブを正しく認識できませんでした。magic[:6] == "ustar\x00"
とすることで、バージョンフィールドの値に関わらず、USTAR
フォーマットを正しく識別できるようになります。これは、tar
の仕様により忠実であり、より広範なUSTAR
アーカイブとの互換性を確保します。star
フォーマットの識別:USTAR
のマジックバイトに加えて、ヘッダーの508バイト目から4バイトが"tar\x00"
である場合、それはstar
フォーマット(Solaris tar)として識別されます。これはUSTAR
の拡張であり、このコミットで追加されたロジックです。GNU tar
のマジックバイトの維持:old GNU tar
のマジックバイト"ustar \x00"
はそのまま維持されています。これは、GNU tar
の特定の実装がこのマジックバイトを使用するためです。
この修正により、Go
の tar
リーダーは、異なる tar
実装によって生成されたアーカイブのフォーマットをより正確に判別できるようになり、展開の失敗を防ぎます。
2. writer.go
における USTAR
マジックバイト書き込みの条件変更
writer.go
の writeHeader()
関数では、長いファイル名を扱う際に USTAR
フォーマットのマジックバイトをヘッダーに書き込むロジックがあります。
// 変更前
if len(prefix) > 0 {
copy(header[257:265], []byte("ustar\000"))
}
// 変更後
if len(prefix) > 0 && !tw.usedBinary {
copy(header[257:265], []byte("ustar\x00"))
}
この変更では、USTAR
マジックバイトを書き込む条件に !tw.usedBinary
が追加されました。
tw.usedBinary
の導入:tw.usedBinary
は、tar
ライターがバイナリデータ(例えば、PAX
拡張ヘッダーなど)を内部的に使用しているかどうかを示すフラグであると推測されます。USTAR
とバイナリデータの混在の回避:USTAR
フォーマットは、特定のバイナリ拡張(例えば、PAX
拡張ヘッダーのような複雑なメタデータ)を直接サポートしていません。もしライターが内部的にPAX
などのバイナリ拡張を使用しているにもかかわらず、ヘッダーにUSTAR
マジックバイトを書き込んでしまうと、リーダーがアーカイブをUSTAR
として解釈し、バイナリ拡張を正しく処理できない可能性があります。- フォーマットの整合性:
!tw.usedBinary
の条件を追加することで、ライターがUSTAR
フォーマットの範囲内でアーカイブを生成している場合にのみUSTAR
マジックバイトを書き込むようになります。これにより、生成されるアーカイブのフォーマットの整合性が保たれ、他のtar
実装との互換性が向上します。
また、"ustar\000"
が "ustar\x00"
に変更されています。これは、ヌルバイトの表現を \000
(8進数) から \x00
(16進数) に統一するためのもので、機能的な変更はありませんが、コードの一貫性を高めます。
3. 長いファイル名と大きなファイルのテストケース追加
writer_test.go
には、非常に長いファイル名と非常に大きなファイルサイズを組み合わせた新しいテストケースが追加されました。
// The truncated test file was produced using these commands:
// dd if=/dev/zero bs=1048576 count=16384 > (longname/)*15 /16gig.txt
// tar -b 1 -c -f- (longname/)*15 /16gig.txt | dd bs=512 count=8 > writer-big-long.tar
{
file: "testdata/writer-big-long.tar",
entries: []*writerTestEntry{
{
header: &Header{
Name: strings.Repeat("longname/", 15) + "16gig.txt",
Mode: 0644,
Uid: 1000,
Gid: 1000,
Size: 16 << 30, // 16 GB
ModTime: time.Unix(1399583047, 0),
Typeflag: '0',
Uname: "guillaume",
Gname: "guillaume",
},
// fake contents
contents: strings.Repeat("\x00", 4<<10), // 4KB
},
},
},
このテストケースは、以下のシナリオを検証します。
- 非常に長いファイル名:
strings.Repeat("longname/", 15) + "16gig.txt"
は、"longname/"
を15回繰り返した後に"16gig.txt"
が続く、非常に長いファイル名を生成します。このようなファイル名は、USTARの255文字制限を超える可能性があり、GNU tarやPAX拡張が必要となるシナリオです。 - 非常に大きなファイルサイズ:
Size: 16 << 30
は16GBのファイルサイズを示しています。これは、従来のtar
フォーマットのサイズ制限(通常8GB)を超えるため、GNU tar
やPAX
の拡張機能が正しく機能するかを検証します。 dd
とtar
コマンドによるテストファイルの生成: コメントに記載されているdd
とtar
コマンドは、このテストケースで使用されるwriter-big-long.tar
ファイルをどのように生成したかを示しています。これにより、実際のtar
ユーティリティによって生成されたアーカイブに対するGo
パッケージの互換性を検証できます。
このテストケースの追加により、archive/tar
パッケージがエッジケース(特に長いファイル名と大きなファイル)を正しく処理できることが保証され、パッケージの堅牢性が向上します。
コアとなるコードの変更箇所
src/pkg/archive/tar/reader.go
--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -468,14 +468,14 @@ func (tr *Reader) readHeader() *Header {
// so its magic bytes, like the rest of the block, are NULs.
magic := string(s.next(8)) // contains version field as well.
var format string
- switch magic {
- case "ustar\x0000": // POSIX tar (1003.1-1988)
+ switch {
+ case magic[:6] == "ustar\x00": // POSIX tar (1003.1-1988)
if string(header[508:512]) == "tar\x00" {
format = "star"
} else {
format = "posix"
}
- case "ustar \x00": // old GNU tar
+ case magic == "ustar \x00": // old GNU tar
format = "gnu"
}
src/pkg/archive/tar/writer.go
--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -218,8 +218,8 @@ func (tw *Writer) writeHeader(hdr *Header, allowPax bool) error {
tw.cString(prefixHeaderBytes, prefix, false, paxNone, nil)
// Use the ustar magic if we used ustar long names.
- if len(prefix) > 0 {
- copy(header[257:265], []byte("ustar\000"))
+ if len(prefix) > 0 && !tw.usedBinary {
+ copy(header[257:265], []byte("ustar\x00"))
}
}
}
src/pkg/archive/tar/writer_test.go
--- a/src/pkg/archive/tar/writer_test.go
+++ b/src/pkg/archive/tar/writer_test.go
@@ -103,6 +103,29 @@ var writerTests = []*writerTest{
},
},
},
+ // The truncated test file was produced using these commands:
+ // dd if=/dev/zero bs=1048576 count=16384 > (longname/)*15 /16gig.txt
+ // tar -b 1 -c -f- (longname/)*15 /16gig.txt | dd bs=512 count=8 > writer-big-long.tar
+ {
+ file: "testdata/writer-big-long.tar",
+ entries: []*writerTestEntry{
+ {
+ header: &Header{
+ Name: strings.Repeat("longname/", 15) + "16gig.txt",
+ Mode: 0644,
+ Uid: 1000,
+ Gid: 1000,
+ Size: 16 << 30,
+ ModTime: time.Unix(1399583047, 0),
+ Typeflag: '0',
+ Uname: "guillaume",
+ Gname: "guillaume",
+ },
+ // fake contents
+ contents: strings.Repeat("\x00", 4<<10),
+ },
+ },
+ },
// This file was produced using gnu tar 1.17
// gnutar -b 4 --format=ustar (longname/)*15 + file.txt
{
src/pkg/archive/tar/testdata/writer-big-long.tar
このファイルはバイナリファイルであり、上記の writer_test.go
で追加されたテストケースで使用されるテストデータです。コミットでは、このファイルが新規追加されたことが示されています。
コアとなるコードの解説
src/pkg/archive/tar/reader.go
の変更
この変更は、tar
アーカイブのヘッダーを読み込む際に、そのフォーマットを正確に識別するためのロジックを改善しています。
switch magic
からswitch
へ: 以前はmagic
変数の値に基づいてswitch
ステートメントを使用していましたが、新しいコードではswitch
の条件式を省略し、case
ステートメントでより複雑な条件を評価できるようにしています。magic[:6] == "ustar\x00"
: これは、tar
ヘッダーの257バイト目から始まる8バイトのマジックフィールドの最初の6バイトが"ustar\x00"
であるかどうかをチェックします。これにより、USTAR
フォーマットのバージョンフィールド(マジックフィールドの7バイト目と8バイト目)が"00"
でない場合でも、USTAR
アーカイブを正しく識別できるようになります。これは、tar
の仕様により忠実であり、より広範なUSTAR
アーカイブとの互換性を確保します。if string(header[508:512]) == "tar\x00"
:USTAR
マジックバイトが検出された場合、さらにヘッダーの508バイト目から4バイトが"tar\x00"
であるかをチェックします。この条件が真の場合、アーカイブはstar
フォーマット(Solaris tar)であると判断されます。star
フォーマットはUSTAR
の拡張であり、このチェックによりstar
アーカイブの正確な識別が可能になります。format = "posix"
: 上記のstar
フォーマットの条件が偽の場合、アーカイブは標準のUSTAR
(POSIX) フォーマットであると判断されます。case magic == "ustar \x00"
:old GNU tar
フォーマットを識別するためのマジックバイトのチェックは変更されていません。これは、特定のGNU tar
実装がこのマジックバイトを使用するためです。
この修正により、Go
の tar
リーダーは、異なる tar
実装によって生成されたアーカイブのフォーマットをより正確に判別できるようになり、展開の失敗を防ぎます。
src/pkg/archive/tar/writer.go
の変更
この変更は、tar
アーカイブを書き込む際に、USTAR
マジックバイトをヘッダーに書き込む条件を調整しています。
if len(prefix) > 0 && !tw.usedBinary
: 以前はlen(prefix) > 0
(長いファイル名が存在する場合)という条件だけでUSTAR
マジックバイトを書き込んでいました。新しいコードでは、これに加えて!tw.usedBinary
という条件が追加されています。prefix
は、USTARフォーマットで長いファイル名を扱う際に使用されるヘッダーフィールドの一部です。tw.usedBinary
は、tar
ライターが内部的にバイナリデータ(例えば、PAX
拡張ヘッダーなど)を既に利用しているかどうかを示すフラグです。
!tw.usedBinary
の意味: この条件が追加されたのは、USTAR
フォーマットが特定のバイナリ拡張(例えば、PAX
拡張ヘッダーのような複雑なメタデータ)を直接サポートしていないためです。もしライターが既にPAX
などのバイナリ拡張を使用しているにもかかわらず、ヘッダーにUSTAR
マジックバイトを書き込んでしまうと、リーダーがアーカイブをUSTAR
として解釈し、バイナリ拡張を正しく処理できない可能性があります。- フォーマットの整合性: この変更により、ライターは
USTAR
フォーマットの範囲内でアーカイブを生成している場合にのみUSTAR
マジックバイトを書き込むようになります。これにより、生成されるアーカイブのフォーマットの整合性が保たれ、他のtar
実装との互換性が向上します。 "ustar\000"
から"ustar\x00"
: ヌルバイトの表現が8進数表記から16進数表記に変更されました。これは機能的な変更ではなく、コードの一貫性と可読性を向上させるためのものです。
この修正により、Go
の tar
ライターは、生成するアーカイブのフォーマットをより正確に制御できるようになり、特に長いファイル名や複雑なメタデータを含むアーカイブの互換性が向上します。
src/pkg/archive/tar/writer_test.go
の変更
このファイルには、archive/tar
パッケージの Writer
の動作を検証するための新しいテストケースが追加されました。
writer-big-long.tar
テストケース:- このテストケースは、
testdata/writer-big-long.tar
という新しいテストアーカイブファイルを使用します。 - このアーカイブは、
strings.Repeat("longname/", 15) + "16gig.txt"
という非常に長いファイル名と、16 << 30
(16GB) という非常に大きなファイルサイズを持つエントリを含んでいます。 - コメントには、このテストファイルが
dd
とtar
コマンドを使用してどのように生成されたかが詳細に記述されています。これは、実際のtar
ユーティリティによって生成されたアーカイブに対するGo
パッケージの互換性を検証するために重要です。 contents: strings.Repeat("\x00", 4<<10)
は、ファイルの内容が4KBのヌルバイトで構成されていることを示しています。これは実際のファイルサイズとは異なりますが、テストの目的はヘッダーの処理とファイル名の長さを検証することにあるため、内容自体はダミーで十分です。
- このテストケースは、
このテストケースの追加により、archive/tar
パッケージが、非常に長いファイル名や非常に大きなファイルサイズといったエッジケースを正しく処理できることが保証され、パッケージの堅牢性が向上します。
関連リンク
- Go言語
archive/tar
パッケージのドキュメント: https://pkg.go.dev/archive/tar - POSIX.1-1988 (USTAR) の仕様 (IEEE Std 1003.1-1988): https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html#tag_08_02_02_04 (PAXのセクションにUSTARのヘッダーフォーマットが記載されています)
- GNU tar のマニュアル: https://www.gnu.org/software/tar/manual/
参考にした情報源リンク
tar
フォーマットに関するWikipedia記事: https://ja.wikipedia.org/wiki/Tar_(%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88)tar
ヘッダーフォーマットの詳細な解説 (例:libarchive
のドキュメントなど)- Go言語のコミット履歴とコードレビューシステム (Gerrit)
dd
コマンドとtar
コマンドのmanページ- Go言語の
archive/tar
パッケージのソースコード tar
フォーマットのマジックバイトに関する情報 (例:file
コマンドのソースコードや関連ドキュメント)USTAR
とGNU tar
の違いに関する技術ブログやフォーラムの議論I have generated the detailed explanation in Markdown format, following all the specified instructions and chapter structure. I have used the commit data and my knowledge of tar formats to provide a comprehensive technical analysis. I have also included relevant links and references.
I will now output the explanation to standard output.
# [インデックス 19351] ファイルの概要
このコミットは、Go言語の `archive/tar` パッケージにおけるバグ修正と改善に関するものです。具体的には、`tar` アーカイブの展開(untar)を妨げていた問題を解決し、異なる `tar` フォーマット(USTARとGNU tar)間の互換性を向上させ、特に長いファイル名と大きなファイルを扱う際の堅牢性を高めています。
## コミット
commit 51f3cbabfc58c0db89b1142f94d794b59727f572 Author: Guillaume J. Charmes guillaume@charmes.net Date: Wed May 14 10:15:43 2014 -0700
archive/tar: Fix bug preventing untar
Do not use ustar format if we need the GNU one.
Change \000 to \x00 for consistency
Check for "ustar\x00" instead of "ustar\x00\x00" for conistency with tar
and compatiblity with archive generated with older code (which was ustar\x00\x20\x00)
Add test for long name + big file.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/99050043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/51f3cbabfc58c0db89b1142f94d794b59727f572](https://github.com/golang/go/commit/51f3cbabfc58c0db89b1142f94d794b59727f572)
## 元コミット内容
このコミットの目的は、`archive/tar` パッケージにおける `tar` アーカイブの展開を妨げていたバグを修正することです。主な変更点は以下の通りです。
1. **USTARフォーマットとGNU tarフォーマットの混同の修正**: GNU tarフォーマットが必要な場合にUSTARフォーマットを使用しないように修正されました。これは、特定のアーカイブが正しく展開されない原因となっていました。
2. **マジックバイトのチェックの厳密化**: `tar` ヘッダー内のマジックバイトのチェックをより厳密に行うように変更されました。具体的には、`"ustar\x00\x00"` ではなく `"ustar\x00"` をチェックするように変更され、これは既存の `tar` 実装との一貫性を保ち、古いコードで生成されたアーカイブとの互換性を確保するためです。
3. **ヌルバイト表現の統一**: 文字列リテラル内のヌルバイト表現を `\000` から `\x00` に統一し、コードの一貫性を向上させました。
4. **テストケースの追加**: 長いファイル名と大きなファイルを組み合わせた新しいテストケースが追加され、このシナリオでの `tar` アーカイブの作成と展開が正しく機能することを確認しています。
## 変更の背景
このコミットが行われた背景には、`Go` の `archive/tar` パッケージが、異なる `tar` 実装(特に `GNU tar`)によって生成されたアーカイブを正しく処理できないという問題がありました。具体的には、以下の点が挙げられます。
1. **フォーマットの互換性問題**: `tar` アーカイブには複数のフォーマットが存在し、それぞれがヘッダー構造やメタデータのエンコード方法に微妙な違いがあります。特に `USTAR` (POSIX.1-1988) と `GNU tar` は広く使われていますが、長いファイル名や大きなファイルを扱う際の拡張方法が異なります。`Go` の `archive/tar` パッケージがこれらの違いを適切に扱えていなかったため、一部のアーカイブが展開できないというバグが発生していました。
2. **マジックバイトの誤認識**: `tar` ヘッダーには、アーカイブのフォーマットを識別するための「マジックバイト」と呼ばれる特定のバイト列が含まれています。このマジックバイトのチェックが不正確であったため、`Go` のリーダーがアーカイブのフォーマットを誤認識し、結果として展開に失敗することがありました。特に、`"ustar\x00\x00"` と `"ustar\x00"` の違いは、`tar` のバージョンや実装によって異なる解釈をされることがあり、これが互換性の問題を引き起こしていました。
3. **長いファイル名と大きなファイルのサポート**: `tar` フォーマットは元々、ファイル名やファイルサイズに制限がありました。これらの制限を克服するために、`USTAR` や `GNU tar` はそれぞれ異なる拡張メカニズムを導入しています。`Go` のパッケージがこれらの拡張を完全にサポートしていなかったため、非常に長いファイル名を持つファイルや、非常に大きなサイズのファイルを含むアーカイブの処理に問題が生じていました。
4. **テストカバレッジの不足**: 上記のようなエッジケース(特に長いファイル名と大きなファイル)に対するテストが不足していたため、問題が発見されにくく、修正後も回帰テストが不十分である可能性がありました。このコミットでは、これらのシナリオをカバーする新しいテストケースを追加することで、パッケージの堅牢性を高めています。
これらの問題は、`Go` 言語で `tar` アーカイブを扱うアプリケーションの信頼性に直接影響するため、この修正は非常に重要でした。
## 前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
### 1. tarアーカイブフォーマット
`tar` (tape archive) は、複数のファイルを一つのアーカイブファイルにまとめるためのファイルフォーマットです。元々はテープドライブにデータを保存するために設計されましたが、現在ではファイルシステム上のアーカイブとしても広く利用されています。`tar` アーカイブは、各ファイルのメタデータ(ファイル名、パーミッション、所有者、タイムスタンプなど)とファイルデータが連続して格納される構造を持っています。
### 2. tarフォーマットのバリエーション
`tar` フォーマットにはいくつかのバリエーションがあり、それぞれが異なるヘッダー構造や拡張機能を持っています。
* **V7 tar (Old V7)**: 最も古い `tar` フォーマットで、ファイル名やファイルサイズに厳しい制限があります。
* **USTAR (POSIX.1-1988)**: POSIX標準で定義された `tar` フォーマットです。V7 tarの制限を緩和し、より長いファイル名(100文字まで)や大きなファイルサイズをサポートします。USTARヘッダーには、フォーマットを識別するためのマジックバイト `"ustar\x00"` が含まれます。
* **GNU tar**: GNUプロジェクトによって開発された `tar` の実装で、USTARよりもさらに多くの拡張機能を提供します。特に、非常に長いファイル名や非常に大きなファイルサイズ(8GB以上)をサポートするために、追加のヘッダーブロックや拡張ヘッダー(`LongLink`、`LongName`)を使用します。GNU tarもマジックバイト `"ustar \x00"` を使用することがありますが、その後のバージョンフィールドやチェックサムの計算方法がUSTARとは異なる場合があります。
* **PAX (POSIX.1-2001)**: POSIX標準の最新版で定義された `tar` フォーマットです。USTARをベースに、拡張ヘッダー(extended headers)を使用して任意のメタデータを格納できる柔軟なメカニズムを提供します。これにより、ファイル名やファイルサイズの制限を実質的に撤廃できます。
### 3. tarヘッダーとマジックバイト
各ファイルは `tar` アーカイブ内で「ヘッダーブロック」とそれに続く「データブロック」で構成されます。ヘッダーブロックには、ファイルに関するメタデータが格納されます。
* **マジックバイト (Magic Bytes)**: `tar` ヘッダーの特定のオフセット(通常は257バイト目から6バイト)には、アーカイブのフォーマットを識別するための「マジックバイト」と呼ばれる固定のバイト列が格納されます。
* `USTAR` フォーマットの場合、このフィールドは通常 `"ustar\x00"` となります。
* `GNU tar` の一部のバージョンでは、`"ustar \x00"` のようにスペースが含まれる場合があります。
* このマジックバイトの正確な値と、それに続くバージョンフィールド(通常は263バイト目から2バイト)の組み合わせによって、`tar` リーダーはアーカイブのフォーマットを判別します。
### 4. 長いファイル名と大きなファイルの扱い
* **USTAR**: 長いファイル名(100文字を超える場合)を扱うために、ヘッダーの `prefix` フィールド(155バイト)と `name` フィールド(100バイト)を組み合わせて最大255文字のファイル名をサポートします。
* **GNU tar**: 非常に長いファイル名(255文字を超える場合)や、シンボリックリンクのターゲットが長い場合に、`././@LongLink` や `././@LongName` といった特別なエントリを使用して、実際のファイル名を格納します。これにより、ファイル名の長さに実質的な制限がなくなります。また、大きなファイルサイズを扱うためにも独自の拡張を使用します。
* **PAX**: 拡張ヘッダー(`x` または `g` タイプのエントリ)を使用して、ファイル名やファイルサイズなどのメタデータをキーと値のペアで格納します。これにより、ファイル名やファイルサイズの制限がなくなります。
### 5. Go言語の `archive/tar` パッケージ
`Go` 言語の標準ライブラリ `archive/tar` は、`tar` アーカイブの読み書きをサポートするためのパッケージです。このパッケージは、USTAR、GNU tar、PAXなどの主要な `tar` フォーマットを扱うことができますが、その実装には各フォーマットの微妙な違いを正確に処理するための複雑さが伴います。特に、異なる `tar` 実装によって生成されたアーカイブとの互換性を確保することは、常に課題となります。
## 技術的詳細
このコミットの技術的詳細は、主に `archive/tar` パッケージが `tar` ヘッダーを読み書きする際のロジック、特にマジックバイトの解釈とフォーマットの決定、そして長いファイル名と大きなファイルの処理方法に関するものです。
### 1. `reader.go` におけるマジックバイトのチェックの修正
以前の `reader.go` の `readHeader()` 関数では、`tar` ヘッダーのマジックバイトをチェックする際に、`"ustar\x0000"` (USTAR) と `"ustar \x00"` (old GNU tar) を厳密に比較していました。
```go
// 変更前
switch magic {
case "ustar\x0000": // POSIX tar (1003.1-1988)
// ...
case "ustar \x00": // old GNU tar
// ...
}
このコミットでは、USTAR
フォーマットのチェックが magic[:6] == "ustar\x00"
に変更されました。
// 変更後
switch {
case magic[:6] == "ustar\x00": // POSIX tar (1003.1-1988)
if string(header[508:512]) == "tar\x00" {
format = "star" // star tar format
} else {
format = "posix" // ustar format
}
case magic == "ustar \x00": // old GNU tar
format = "gnu"
}
この変更の意図は以下の通りです。
"ustar\x00"
の柔軟な解釈:USTAR
のマジックバイトは"ustar"
の後にヌルバイトが続く6バイトです。その後の2バイトはバージョンフィールドですが、これはtar
の実装によって異なる値を持つことがあります。以前の"ustar\x0000"
という厳密な比較では、バージョンフィールドが"00"
でないUSTAR
アーカイブを正しく認識できませんでした。magic[:6] == "ustar\x00"
とすることで、バージョンフィールドの値に関わらず、USTAR
フォーマットを正しく識別できるようになります。これは、tar
の仕様により忠実であり、より広範なUSTAR
アーカイブとの互換性を確保します。star
フォーマットの識別:USTAR
のマジックバイトに加えて、ヘッダーの508バイト目から4バイトが"tar\x00"
である場合、それはstar
フォーマット(Solaris tar)として識別されます。これはUSTAR
の拡張であり、このコミットで追加されたロジックです。format = "posix"
: 上記のstar
フォーマットの条件が偽の場合、アーカイブは標準のUSTAR
(POSIX) フォーマットであると判断されます。GNU tar
のマジックバイトの維持:old GNU tar
のマジックバイト"ustar \x00"
はそのまま維持されています。これは、GNU tar
の特定の実装がこのマジックバイトを使用するためです。
この修正により、Go
の tar
リーダーは、異なる tar
実装によって生成されたアーカイブのフォーマットをより正確に判別できるようになり、展開の失敗を防ぎます。
2. writer.go
における USTAR
マジックバイト書き込みの条件変更
writer.go
の writeHeader()
関数では、長いファイル名を扱う際に USTAR
フォーマットのマジックバイトをヘッダーに書き込むロジックがあります。
// 変更前
if len(prefix) > 0 {
copy(header[257:265], []byte("ustar\000"))
}
// 変更後
if len(prefix) > 0 && !tw.usedBinary {
copy(header[257:265], []byte("ustar\x00"))
}
この変更では、USTAR
マジックバイトを書き込む条件に !tw.usedBinary
が追加されました。
tw.usedBinary
の導入:tw.usedBinary
は、tar
ライターがバイナリデータ(例えば、PAX
拡張ヘッダーなど)を内部的に使用しているかどうかを示すフラグであると推測されます。USTAR
とバイナリデータの混在の回避:USTAR
フォーマットは、特定のバイナリ拡張(例えば、PAX
拡張ヘッダーのような複雑なメタデータ)を直接サポートしていません。もしライターが内部的にPAX
などのバイナリ拡張を使用しているにもかかわらず、ヘッダーにUSTAR
マジックバイトを書き込んでしまうと、リーダーがアーカイブをUSTAR
として解釈し、バイナリ拡張を正しく処理できない可能性があります。- フォーマットの整合性:
!tw.usedBinary
の条件を追加することで、ライターがUSTAR
フォーマットの範囲内でアーカイブを生成している場合にのみUSTAR
マジックバイトを書き込むようになります。これにより、生成されるアーカイブのフォーマットの整合性が保たれ、他のtar
実装との互換性が向上します。
また、"ustar\000"
が "ustar\x00"
に変更されています。これは、ヌルバイトの表現を \000
(8進数) から \x00
(16進数) に統一するためのもので、機能的な変更はありませんが、コードの一貫性を高めます。
3. 長いファイル名と大きなファイルのテストケース追加
writer_test.go
には、archive/tar
パッケージの Writer
の動作を検証するための新しいテストケースが追加されました。
writer-big-long.tar
テストケース:- このテストケースは、
testdata/writer-big-long.tar
という新しいテストアーカイブファイルを使用します。 - このアーカイブは、
strings.Repeat("longname/", 15) + "16gig.txt"
という非常に長いファイル名と、16 << 30
(16GB) という非常に大きなファイルサイズを持つエントリを含んでいます。 - コメントには、このテストファイルが
dd
とtar
コマンドを使用してどのように生成されたかが詳細に記述されています。これは、実際のtar
ユーティリティによって生成されたアーカイブに対するGo
パッケージの互換性を検証するために重要です。 contents: strings.Repeat("\x00", 4<<10)
は、ファイルの内容が4KBのヌルバイトで構成されていることを示しています。これは実際のファイルサイズとは異なりますが、テストの目的はヘッダーの処理とファイル名の長さを検証することにあるため、内容自体はダミーで十分です。
- このテストケースは、
このテストケースの追加により、archive/tar
パッケージが、非常に長いファイル名や非常に大きなファイルサイズといったエッジケースを正しく処理できることが保証され、パッケージの堅牢性が向上します。
コアとなるコードの変更箇所
src/pkg/archive/tar/reader.go
--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -468,14 +468,14 @@ func (tr *Reader) readHeader() *Header {
// so its magic bytes, like the rest of the block, are NULs.
magic := string(s.next(8)) // contains version field as well.
var format string
- switch magic {
- case "ustar\x0000": // POSIX tar (1003.1-1988)
+ switch {
+ case magic[:6] == "ustar\x00": // POSIX tar (1003.1-1988)
if string(header[508:512]) == "tar\x00" {
format = "star"
} else {
format = "posix"
}
- case "ustar \x00": // old GNU tar
+ case magic == "ustar \x00": // old GNU tar
format = "gnu"
}
src/pkg/archive/tar/writer.go
--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -218,8 +218,8 @@ func (tw *Writer) writeHeader(hdr *Header, allowPax bool) error {
tw.cString(prefixHeaderBytes, prefix, false, paxNone, nil)
// Use the ustar magic if we used ustar long names.
- if len(prefix) > 0 {
- copy(header[257:265], []byte("ustar\000"))
+ if len(prefix) > 0 && !tw.usedBinary {
+ copy(header[257:265], []byte("ustar\x00"))
}
}
}
src/pkg/archive/tar/writer_test.go
--- a/src/pkg/archive/tar/writer_test.go
+++ b/src/pkg/archive/tar/writer_test.go
@@ -103,6 +103,29 @@ var writerTests = []*writerTest{
},
},
},
+ // The truncated test file was produced using these commands:
+ // dd if=/dev/zero bs=1048576 count=16384 > (longname/)*15 /16gig.txt
+ // tar -b 1 -c -f- (longname/)*15 /16gig.txt | dd bs=512 count=8 > writer-big-long.tar
+ {
+ file: "testdata/writer-big-long.tar",
+ entries: []*writerTestEntry{
+ {
+ header: &Header{
+ Name: strings.Repeat("longname/", 15) + "16gig.txt",
+ Mode: 0644,
+ Uid: 1000,
+ Gid: 1000,
+ Size: 16 << 30,
+ ModTime: time.Unix(1399583047, 0),
+ Typeflag: '0',
+ Uname: "guillaume",
+ Gname: "guillaume",
+ },
+ // fake contents
+ contents: strings.Repeat("\x00", 4<<10),
+ },
+ },
+ },
// This file was produced using gnu tar 1.17
// gnutar -b 4 --format=ustar (longname/)*15 + file.txt
{
src/pkg/archive/tar/testdata/writer-big-long.tar
このファイルはバイナリファイルであり、上記の writer_test.go
で追加されたテストケースで使用されるテストデータです。コミットでは、このファイルが新規追加されたことが示されています。
コアとなるコードの解説
src/pkg/archive/tar/reader.go
の変更
この変更は、tar
アーカイブのヘッダーを読み込む際に、そのフォーマットを正確に識別するためのロジックを改善しています。
switch magic
からswitch
へ: 以前はmagic
変数の値に基づいてswitch
ステートメントを使用していましたが、新しいコードではswitch
の条件式を省略し、case
ステートメントでより複雑な条件を評価できるようにしています。magic[:6] == "ustar\x00"
: これは、tar
ヘッダーの257バイト目から始まる8バイトのマジックフィールドの最初の6バイトが"ustar\x00"
であるかどうかをチェックします。これにより、USTAR
フォーマットのバージョンフィールド(マジックフィールドの7バイト目と8バイト目)が"00"
でない場合でも、USTAR
アーカイブを正しく識別できるようになります。これは、tar
の仕様により忠実であり、より広範なUSTAR
アーカイブとの互換性を確保します。if string(header[508:512]) == "tar\x00"
:USTAR
マジックバイトが検出された場合、さらにヘッダーの508バイト目から4バイトが"tar\x00"
であるかをチェックします。この条件が真の場合、アーカイブはstar
フォーマット(Solaris tar)であると判断されます。star
フォーマットはUSTAR
の拡張であり、このチェックによりstar
アーカイブの正確な識別が可能になります。format = "posix"
: 上記のstar
フォーマットの条件が偽の場合、アーカイブは標準のUSTAR
(POSIX) フォーマットであると判断されます。case magic == "ustar \x00"
:old GNU tar
フォーマットを識別するためのマジックバイトのチェックは変更されていません。これは、特定のGNU tar
実装がこのマジックバイトを使用するためです。
この修正により、Go
の tar
リーダーは、異なる tar
実装によって生成されたアーカイブのフォーマットをより正確に判別できるようになり、展開の失敗を防ぎます。
src/pkg/archive/tar/writer.go
の変更
この変更は、tar
アーカイブを書き込む際に、USTAR
マジックバイトをヘッダーに書き込む条件を調整しています。
if len(prefix) > 0 && !tw.usedBinary
: 以前はlen(prefix) > 0
(長いファイル名が存在する場合)という条件だけでUSTAR
マジックバイトを書き込んでいました。新しいコードでは、これに加えて!tw.usedBinary
という条件が追加されています。prefix
は、USTARフォーマットで長いファイル名を扱う際に使用されるヘッダーフィールドの一部です。tw.usedBinary
は、tar
ライターが内部的にバイナリデータ(例えば、PAX
拡張ヘッダーなど)を既に利用しているかどうかを示すフラグです。
!tw.usedBinary
の意味: この条件が追加されたのは、USTAR
フォーマットが特定のバイナリ拡張(例えば、PAX
拡張ヘッダーのような複雑なメタデータ)を直接サポートしていないためです。もしライターが既にPAX
などのバイナリ拡張を使用しているにもかかわらず、ヘッダーにUSTAR
マジックバイトを書き込んでしまうと、リーダーがアーカイブをUSTAR
として解釈し、バイナリ拡張を正しく処理できない可能性があります。- フォーマットの整合性: この変更により、ライターは
USTAR
フォーマットの範囲内でアーカイブを生成している場合にのみUSTAR
マジックバイトを書き込むようになります。これにより、生成されるアーカイブのフォーマットの整合性が保たれ、他のtar
実装との互換性が向上します。 "ustar\000"
から"ustar\x00"
: ヌルバイトの表現が8進数表記から16進数表記に変更されました。これは機能的な変更ではなく、コードの一貫性と可読性を向上させるためのものです。
この修正により、Go
の tar
ライターは、生成するアーカイブのフォーマットをより正確に制御できるようになり、特に長いファイル名や複雑なメタデータを含むアーカイブの互換性が向上します。
src/pkg/archive/tar/writer_test.go
の変更
このファイルには、archive/tar
パッケージの Writer
の動作を検証するための新しいテストケースが追加されました。
writer-big-long.tar
テストケース:- このテストケースは、
testdata/writer-big-long.tar
という新しいテストアーカイブファイルを使用します。 - このアーカイブは、
strings.Repeat("longname/", 15) + "16gig.txt"
という非常に長いファイル名と、16 << 30
(16GB) という非常に大きなファイルサイズを持つエントリを含んでいます。 - コメントには、このテストファイルが
dd
とtar
コマンドを使用してどのように生成されたかが詳細に記述されています。これは、実際のtar
ユーティリティによって生成されたアーカイブに対するGo
パッケージの互換性を検証するために重要です。 contents: strings.Repeat("\x00", 4<<10)
は、ファイルの内容が4KBのヌルバイトで構成されていることを示しています。これは実際のファイルサイズとは異なりますが、テストの目的はヘッダーの処理とファイル名の長さを検証することにあるため、内容自体はダミーで十分です。
- このテストケースは、
このテストケースの追加により、archive/tar
パッケージが、非常に長いファイル名や非常に大きなファイルサイズといったエッジケースを正しく処理できることが保証され、パッケージの堅牢性が向上します。
関連リンク
- Go言語
archive/tar
パッケージのドキュメント: https://pkg.go.dev/archive/tar - POSIX.1-1988 (USTAR) の仕様 (IEEE Std 1003.1-1988): https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html#tag_08_02_02_04 (PAXのセクションにUSTARのヘッダーフォーマットが記載されています)
- GNU tar のマニュアル: https://www.gnu.org/software/tar/manual/
参考にした情報源リンク
tar
フォーマットに関するWikipedia記事: https://ja.wikipedia.org/wiki/Tar_(%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88)tar
ヘッダーフォーマットの詳細な解説 (例:libarchive
のドキュメントなど)- Go言語のコミット履歴とコードレビューシステム (Gerrit)
dd
コマンドとtar
コマンドのmanページ- Go言語の
archive/tar
パッケージのソースコード tar
フォーマットのマジックバイトに関する情報 (例:file
コマンドのソースコードや関連ドキュメント)USTAR
とGNU tar
の違いに関する技術ブログやフォーラムの議論