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

[インデックス 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 アーカイブの展開を妨げていたバグを修正することです。主な変更点は以下の通りです。

  1. USTARフォーマットとGNU tarフォーマットの混同の修正: GNU tarフォーマットが必要な場合にUSTARフォーマットを使用しないように修正されました。これは、特定のアーカイブが正しく展開されない原因となっていました。
  2. マジックバイトのチェックの厳密化: tar ヘッダー内のマジックバイトのチェックをより厳密に行うように変更されました。具体的には、"ustar\x00\x00" ではなく "ustar\x00" をチェックするように変更され、これは既存の tar 実装との一貫性を保ち、古いコードで生成されたアーカイブとの互換性を確保するためです。
  3. ヌルバイト表現の統一: 文字列リテラル内のヌルバイト表現を \000 から \x00 に統一し、コードの一貫性を向上させました。
  4. テストケースの追加: 長いファイル名と大きなファイルを組み合わせた新しいテストケースが追加され、このシナリオでの tar アーカイブの作成と展開が正しく機能することを確認しています。

変更の背景

このコミットが行われた背景には、Goarchive/tar パッケージが、異なる tar 実装(特に GNU tar)によって生成されたアーカイブを正しく処理できないという問題がありました。具体的には、以下の点が挙げられます。

  1. フォーマットの互換性問題: tar アーカイブには複数のフォーマットが存在し、それぞれがヘッダー構造やメタデータのエンコード方法に微妙な違いがあります。特に USTAR (POSIX.1-1988) と GNU tar は広く使われていますが、長いファイル名や大きなファイルを扱う際の拡張方法が異なります。Goarchive/tar パッケージがこれらの違いを適切に扱えていなかったため、一部のアーカイブが展開できないというバグが発生していました。
  2. マジックバイトの誤認識: tar ヘッダーには、アーカイブのフォーマットを識別するための「マジックバイト」と呼ばれる特定のバイト列が含まれています。このマジックバイトのチェックが不正確であったため、Go のリーダーがアーカイブのフォーマットを誤認識し、結果として展開に失敗することがありました。特に、"ustar\x00\x00""ustar\x00" の違いは、tar のバージョンや実装によって異なる解釈をされることがあり、これが互換性の問題を引き起こしていました。
  3. 長いファイル名と大きなファイルのサポート: tar フォーマットは元々、ファイル名やファイルサイズに制限がありました。これらの制限を克服するために、USTARGNU 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以上)をサポートするために、追加のヘッダーブロックや拡張ヘッダー(LongLinkLongName)を使用します。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.goreadHeader() 関数では、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 の特定の実装がこのマジックバイトを使用するためです。

この修正により、Gotar リーダーは、異なる tar 実装によって生成されたアーカイブのフォーマットをより正確に判別できるようになり、展開の失敗を防ぎます。

2. writer.go における USTAR マジックバイト書き込みの条件変更

writer.gowriteHeader() 関数では、長いファイル名を扱う際に 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 tarPAX の拡張機能が正しく機能するかを検証します。
  • ddtar コマンドによるテストファイルの生成: コメントに記載されている ddtar コマンドは、このテストケースで使用される 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 実装がこのマジックバイトを使用するためです。

この修正により、Gotar リーダーは、異なる 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進数表記に変更されました。これは機能的な変更ではなく、コードの一貫性と可読性を向上させるためのものです。

この修正により、Gotar ライターは、生成するアーカイブのフォーマットをより正確に制御できるようになり、特に長いファイル名や複雑なメタデータを含むアーカイブの互換性が向上します。

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) という非常に大きなファイルサイズを持つエントリを含んでいます。
    • コメントには、このテストファイルが ddtar コマンドを使用してどのように生成されたかが詳細に記述されています。これは、実際の tar ユーティリティによって生成されたアーカイブに対する Go パッケージの互換性を検証するために重要です。
    • contents: strings.Repeat("\x00", 4<<10) は、ファイルの内容が4KBのヌルバイトで構成されていることを示しています。これは実際のファイルサイズとは異なりますが、テストの目的はヘッダーの処理とファイル名の長さを検証することにあるため、内容自体はダミーで十分です。

このテストケースの追加により、archive/tar パッケージが、非常に長いファイル名や非常に大きなファイルサイズといったエッジケースを正しく処理できることが保証され、パッケージの堅牢性が向上します。

関連リンク

参考にした情報源リンク

  • 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 コマンドのソースコードや関連ドキュメント)
  • USTARGNU 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 の特定の実装がこのマジックバイトを使用するためです。

この修正により、Gotar リーダーは、異なる tar 実装によって生成されたアーカイブのフォーマットをより正確に判別できるようになり、展開の失敗を防ぎます。

2. writer.go における USTAR マジックバイト書き込みの条件変更

writer.gowriteHeader() 関数では、長いファイル名を扱う際に 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) という非常に大きなファイルサイズを持つエントリを含んでいます。
    • コメントには、このテストファイルが ddtar コマンドを使用してどのように生成されたかが詳細に記述されています。これは、実際の 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 実装がこのマジックバイトを使用するためです。

この修正により、Gotar リーダーは、異なる 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進数表記に変更されました。これは機能的な変更ではなく、コードの一貫性と可読性を向上させるためのものです。

この修正により、Gotar ライターは、生成するアーカイブのフォーマットをより正確に制御できるようになり、特に長いファイル名や複雑なメタデータを含むアーカイブの互換性が向上します。

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) という非常に大きなファイルサイズを持つエントリを含んでいます。
    • コメントには、このテストファイルが ddtar コマンドを使用してどのように生成されたかが詳細に記述されています。これは、実際の tar ユーティリティによって生成されたアーカイブに対する Go パッケージの互換性を検証するために重要です。
    • contents: strings.Repeat("\x00", 4<<10) は、ファイルの内容が4KBのヌルバイトで構成されていることを示しています。これは実際のファイルサイズとは異なりますが、テストの目的はヘッダーの処理とファイル名の長さを検証することにあるため、内容自体はダミーで十分です。

このテストケースの追加により、archive/tar パッケージが、非常に長いファイル名や非常に大きなファイルサイズといったエッジケースを正しく処理できることが保証され、パッケージの堅牢性が向上します。

関連リンク

参考にした情報源リンク

  • 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 コマンドのソースコードや関連ドキュメント)
  • USTARGNU tar の違いに関する技術ブログやフォーラムの議論