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

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

このコミットは、Go言語の標準ライブラリarchive/tarパッケージに、GNU sparseファイルのサポートを追加するものです。具体的には、以下のファイルが変更されています。

  • src/pkg/archive/tar/common.go: Tarヘッダのタイプフラグに新しい定数が追加されました。
  • src/pkg/archive/tar/reader.go: Tarアーカイブの読み取りロジックが大幅に拡張され、GNU sparseファイルの解析と読み取りに対応しました。新しいデータ構造、インターフェース、および複数のヘルパー関数が導入されています。
  • src/pkg/archive/tar/reader_test.go: GNU sparseファイルの読み取り機能が正しく動作することを確認するための、広範なテストケースが追加されました。
  • src/pkg/archive/tar/testdata/sparse-formats.tar: テスト用のバイナリファイルで、様々なGNU sparseフォーマットのデータが含まれています。

コミット

commit 730db0affc642530daf9129f4fbc89a4e40f9c95
Author: David Thomas <davidthomas426@gmail.com>
Date:   Thu Apr 3 20:01:04 2014 +0000

    archive/tar: add support for GNU sparse files.
    
    Supports all the current GNU tar sparse formats, including the
    old GNU format and the GNU PAX format versions 0.0, 0.1, and 1.0.
    Fixes #3864.
    
    LGTM=rsc
    R=golang-codereviews, dave, gobot, dsymonds, rsc
    CC=golang-codereviews
    https://golang.org/cl/64740043

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

https://github.com/golang/go/commit/730db0affc642530daf9129f4fbc89a4e40f9c95

元コミット内容

archive/tar: add support for GNU sparse files.

Supports all the current GNU tar sparse formats, including the
old GNU format and the GNU PAX format versions 0.0, 0.1, and 1.0.
Fixes #3864.

LGTM=rsc
R=golang-codereviews, dave, gobot, dsymonds, rsc
CC=golang-codereviews
https://golang.org/cl/64740043

変更の背景

このコミットの主な目的は、Go言語のarchive/tarパッケージがGNU tarによって作成された疎(sparse)ファイルを正しく読み取れるようにすることです。疎ファイルは、ファイル内に連続するゼロのブロック(「穴」または「スパース領域」と呼ばれる)がある場合に、その領域を実際にディスクに書き込まずにメタデータとして記録することで、ディスクスペースを節約する特殊なファイル形式です。

従来のarchive/tarパッケージでは、このような疎ファイルを適切に処理できず、アーカイブから展開する際にデータが破損したり、予期せぬ動作を引き起こす可能性がありました。特に、大きなデータベースファイルや仮想ディスクイメージなど、多くのゼロバイトを含むファイルでは、疎ファイル形式が頻繁に利用されます。これらのファイルをGoプログラムで扱う際に、互換性の問題が生じていました。

この問題は、GoのIssue #3864として報告されており、このコミットはその問題を解決するために実装されました。GNU tarは、疎ファイルを扱うためのいくつかの異なるフォーマット(古いGNUフォーマット、PAXフォーマットのバージョン0.0、0.1、1.0)をサポートしており、このコミットはこれらすべての主要なフォーマットに対応することで、幅広い互換性を提供します。これにより、GoアプリケーションがGNU tarで作成されたアーカイブをより堅牢に処理できるようになります。

前提知識の解説

Tarアーカイブ

Tar(Tape Archive)は、複数のファイルを一つのアーカイブファイルにまとめるためのファイル形式です。元々は磁気テープにデータを保存するために設計されましたが、現在ではファイルシステム上のアーカイブや配布形式として広く利用されています。Tarアーカイブは、各ファイルのメタデータ(ファイル名、パーミッション、タイムスタンプなど)とファイルデータが連続して格納されるシンプルな構造を持っています。

GNU Tar

GNU Tarは、標準的なTar形式に加えて、いくつかの拡張機能を持つTarの実装です。これらの拡張機能には、長いファイル名のサポート、ACL(Access Control List)や拡張属性の保存、そして本コミットの主題である疎ファイルのサポートなどが含まれます。GNU TarはLinuxシステムで広く使われており、その拡張機能は多くのTarアーカイブで利用されています。

Sparse Files (疎ファイル)

疎ファイルは、ファイルシステムが提供する機能の一つで、ファイル内の連続するゼロバイトのブロック(「穴」または「スパース領域」)を、実際にディスク上の物理ブロックとして割り当てずに表現するファイルです。これにより、ファイルが論理的には大きくても、実際にディスクを消費する量はデータが書き込まれている部分のみとなり、ディスクスペースを大幅に節約できます。

例えば、1GBのファイルのうち、ほとんどがゼロで、一部にだけデータが書き込まれている場合、疎ファイルとして保存すれば、実際にディスクに書き込まれるのはデータ部分とメタデータのみとなり、残りのゼロ部分はディスクスペースを消費しません。ファイルを読み出す際には、ファイルシステムが「穴」の部分をゼロとして返します。

疎ファイルの利点:

  • ディスクスペースの節約: ゼロバイトの領域を物理的に保存しないため、ストレージ容量を効率的に利用できます。
  • 効率的なデータ転送: ネットワーク経由で疎ファイルを転送する際、ゼロの領域をスキップできるため、転送時間を短縮できます。
  • 高速なファイル作成: 巨大なファイルを初期化する際に、すべてのバイトをゼロで埋める必要がないため、ファイル作成が高速になります。

PAX (Portable Archive eXchange) フォーマット

PAXは、POSIX標準によって定義されたTarアーカイブの拡張フォーマットです。従来のTar形式の制限(例えば、ファイル名の長さ、UID/GIDの範囲、タイムスタンプの精度など)を克服するために導入されました。PAXは、拡張ヘッダと呼ばれる特別なエントリを使用して、追加のメタデータや長いファイル名などをキーと値のペアの形式で格納します。GNU Tarの疎ファイルフォーマットの一部は、このPAX拡張ヘッダを利用して疎ファイル情報を表現します。

GNU Sparse Formats (0.0, 0.1, 1.0)

GNU Tarは、疎ファイルをアーカイブに格納するためにいくつかの異なるフォーマットを使用します。これらは主に、疎ファイルの「穴」と「データブロック」のマップ情報をどのように表現するかの違いです。

  • Old GNU Format: これはPAX以前の古いGNU Tarの疎ファイルフォーマットです。疎マップ情報は、Tarヘッダの特定のオフセットに直接格納されるか、または追加の拡張ヘッダブロックに格納されます。
  • GNU PAX Format 0.0: PAX拡張ヘッダを使用しますが、疎マップ情報はGNU.sparse.offsetGNU.sparse.numbytesというキーで個別に格納されます。このフォーマットは、後続のバージョン0.1でより効率的な表現に移行しました。
  • GNU PAX Format 0.1: PAX拡張ヘッダを使用し、疎マップ情報はGNU.sparse.mapという単一のキーに、カンマ区切りのオフセットとバイト数のペアのリストとして格納されます。これはバージョン0.0よりもコンパクトな表現です。
  • GNU PAX Format 1.0: このフォーマットでは、疎マップ情報がPAX拡張ヘッダではなく、ファイルデータ本体の直前に専用のブロックとして格納されます。これにより、非常に大きな疎マップを持つファイルでも効率的に処理できます。

これらの異なるフォーマットに対応することで、Goのarchive/tarパッケージは、GNU Tarによって作成された様々な疎ファイルを網羅的にサポートできるようになります。

技術的詳細

このコミットは、archive/tarパッケージのReader構造体と関連メソッドを大幅に拡張し、GNU sparseファイルの読み取りを可能にしています。

  1. 新しいタイプフラグの追加: src/pkg/archive/tar/common.goTypeGNUSparse = 'S'が追加されました。これは、TarヘッダのTypeflagフィールドが'S'である場合に、そのエントリが古いGNU sparseファイルであることを示します。

  2. Reader構造体の変更と新しいインターフェース/構造体:

    • Reader構造体は、現在のファイルエントリの読み取りを担当するcurr numBytesReaderフィールドを持つようになりました。
    • numBytesReaderインターフェースが導入され、io.ReadernumBytes() int64メソッド(残りのバイト数を返す)を定義します。
    • regFileReader構造体は、通常のファイルデータを読み取るためのnumBytesReaderの実装です。
    • sparseFileReader構造体は、疎ファイルデータを読み取るためのnumBytesReaderの実装です。これは内部にregFileReaderを持ち、疎マップ([]sparseEntry)と現在の読み取り位置(pos)、ファイルの合計サイズ(tot)を管理します。
    • sparseEntry構造体は、疎マップ内の単一のエントリ(オフセットとバイト数)を表します。
  3. PAX GNU Sparseヘッダのキーワード定数: reader.goに、PAX拡張ヘッダ内でGNU sparseファイル情報を識別するための多数の定数(例: paxGNUSparseNumBlocks, paxGNUSparseOffset, paxGNUSparseMapなど)が追加されました。

  4. Old GNU Sparseヘッダのキーワード定数: 古いGNU sparseフォーマットのヘッダ内のオフセットやサイズを示す定数も追加されました。

  5. Reader.Next()メソッドの拡張:

    • Next()メソッドは、次のTarエントリを読み込む際に、まず通常のヘッダを解析します。
    • その後、checkForGNUSparsePAXHeaders関数を呼び出して、現在のエントリがPAX形式のGNU sparseファイルであるかどうかをチェックします。もしそうであれば、その疎マップを読み込み、tr.currsparseFileReaderのインスタンスに設定します。
    • 古いGNU sparseフォーマット(TypeGNUSparse)の場合も、readHeader()内で同様の処理が行われ、tr.currsparseFileReaderに設定されます。
  6. 疎マップの解析ロジック:

    • checkForGNUSparsePAXHeaders(hdr *Header, headers map[string]string) ([]sparseEntry, error): PAXヘッダを調べて、どのGNU sparseフォーマット(0.0, 0.1, 1.0)が適用されるかを識別し、対応する疎マップ読み取り関数を呼び出します。不明なフォーマットは無視されます。
    • readOldGNUSparseMap(header []byte) []sparseEntry: 古いGNU sparseフォーマットの疎マップを、メインヘッダおよび必要に応じて拡張ヘッダから読み取ります。
    • readGNUSparseMap1x0(r io.Reader) ([]sparseEntry, error): GNU PAX sparseフォーマット1.0の疎マップを読み取ります。このフォーマットでは、疎マップがファイルデータ本体の直前に独自のブロックとして格納されているため、io.Readerから直接読み込みます。
    • readGNUSparseMap0x1(headers map[string]string) ([]sparseEntry, error): GNU PAX sparseフォーマット0.1の疎マップを読み取ります。このフォーマットでは、疎マップがPAXヘッダのGNU.sparse.mapキーに格納されています。
  7. parsePAX()でのGNU sparse format 0.0の変換:

    • parsePAX()関数は、PAX拡張ヘッダを解析する際に、GNU sparse format 0.0のGNU.sparse.offsetGNU.sparse.numbytesキーを特別に処理します。これらのキーの値を一時的なbytes.Bufferに書き込み、最終的にGNU.sparse.mapキーとして結合することで、内部的にバージョン0.1の形式に変換します。これにより、後続の処理が簡素化されます。
  8. Reader.Read()メソッドの変更:

    • Reader.Read()は、実際の読み取り処理をtr.currnumBytesReaderインターフェース)に委譲するようになりました。
    • regFileReader.Read()は、通常のファイルデータを読み取ります。
    • sparseFileReader.Read()は、疎ファイルの「穴」の部分をゼロで埋め、データブロックの部分を内部のregFileReaderから読み取ることで、疎ファイルを展開された形式で提供します。これにより、アプリケーションは疎ファイルであることを意識せずに、通常のファイルとしてデータを読み取ることができます。
  9. テストケースの追加: reader_test.goには、様々なGNU sparseフォーマットのTarアーカイブを読み込み、その内容が期待通りであることを検証する多数のテストが追加されています。これには、エンドツーエンドのテスト、sparseFileReaderの単体テスト、および各疎マップ読み取り関数のテストが含まれます。

これらの変更により、Goのarchive/tarパッケージは、GNU tarによって作成された疎ファイルを透過的に処理できるようになり、Goアプリケーションがより広範なTarアーカイブを扱う際の互換性と堅牢性が向上しました。

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

src/pkg/archive/tar/common.go

--- a/src/pkg/archive/tar/common.go
+++ b/src/pkg/archive/tar/common.go
@@ -38,6 +38,7 @@ const (
 	TypeXGlobalHeader = 'g'    // global extended header
 	TypeGNULongName   = 'L'    // Next file has a long name
 	TypeGNULongLink   = 'K'    // Next file symlinks to a file w/ a long name
+	TypeGNUSparse     = 'S'    // sparse file
 )
 
 // A Header represents a single header in a tar archive.

src/pkg/archive/tar/reader.go

--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -29,12 +29,57 @@ const maxNanoSecondIntSize = 9
 // The Next method advances to the next file in the archive (including the first),
 // and then it can be treated as an io.Reader to access the file's data.
 type Reader struct {
-	r   io.Reader
-	err error
-	nb  int64 // number of unread bytes for current file entry
-	pad int64 // amount of padding (ignored) after current file entry
+	r    io.Reader
+	err  error
+	pad  int64          // amount of padding (ignored) after current file entry
+	curr numBytesReader // reader for current file entry
 }
 
+// A numBytesReader is an io.Reader with a numBytes method, returning the number
+// of bytes remaining in the underlying encoded data.
+type numBytesReader interface {
+	io.Reader
+	numBytes() int64
+}
+
+// A regFileReader is a numBytesReader for reading file data from a tar archive.
+type regFileReader struct {
+	r  io.Reader // underlying reader
+	nb int64     // number of unread bytes for current file entry
+}
+
+// A sparseFileReader is a numBytesReader for reading sparse file data from a tar archive.
+type sparseFileReader struct {
+	rfr *regFileReader // reads the sparse-encoded file data
+	sp  []sparseEntry  // the sparse map for the file
+	pos int64          // keeps track of file position
+	tot int64          // total size of the file
+}
+
+// Keywords for GNU sparse files in a PAX extended header
+const (
+	paxGNUSparseNumBlocks = "GNU.sparse.numblocks"
+	paxGNUSparseOffset    = "GNU.sparse.offset"
+	paxGNUSparseNumBytes  = "GNU.sparse.numbytes"
+	paxGNUSparseMap       = "GNU.sparse.map"
+	paxGNUSparseName      = "GNU.sparse.name"
+	paxGNUSparseMajor     = "GNU.sparse.major"
+	paxGNUSparseMinor     = "GNU.sparse.minor"
+	paxGNUSparseSize      = "GNU.sparse.size"
+	paxGNUSparseRealSize  = "GNU.sparse.realsize"
+)
+
+// Keywords for old GNU sparse headers
+const (
+	oldGNUSparseMainHeaderOffset               = 386
+	oldGNUSparseMainHeaderIsExtendedOffset     = 482
+	oldGNUSparseMainHeaderNumEntries           = 4
+	oldGNUSparseExtendedHeaderIsExtendedOffset = 504
+	oldGNUSparseExtendedHeaderNumEntries       = 21
+	oldGNUSparseOffsetSize                     = 12
+	oldGNUSparseNumBytesSize                   = 12
+)
+
 // NewReader creates a new Reader reading from r.
 func NewReader(r io.Reader) *Reader { return &Reader{r: r} }
 
@@ -64,6 +109,18 @@ func (tr *Reader) Next() (*Header, error) {
 		tr.skipUnread()
 		hdr = tr.readHeader()
 		mergePAX(hdr, headers)
+
+		// Check for a PAX format sparse file
+		sp, err := tr.checkForGNUSparsePAXHeaders(hdr, headers)
+		if err != nil {
+			tr.err = err
+			return nil, err
+		}
+		if sp != nil {
+			// Current file is a PAX format GNU sparse file.
+			// Set the current file reader to a sparse file reader.
+			tr.curr = &sparseFileReader{rfr: tr.curr.(*regFileReader), sp: sp, tot: hdr.Size}
+		}
 		return hdr, nil
 	case TypeGNULongName:
 		// We have a GNU long name header. Its contents are the real file name.
@@ -87,6 +144,67 @@ func (tr *Reader) Next() (*Header, error) {
 	return hdr, tr.err
 }
 
+// checkForGNUSparsePAXHeaders checks the PAX headers for GNU sparse headers. If they are found, then
+// this function reads the sparse map and returns it. Unknown sparse formats are ignored, causing the file to
+// be treated as a regular file.
+func (tr *Reader) checkForGNUSparsePAXHeaders(hdr *Header, headers map[string]string) ([]sparseEntry, error) {
+	var sparseFormat string
+
+	// Check for sparse format indicators
+	major, majorOk := headers[paxGNUSparseMajor]
+	minor, minorOk := headers[paxGNUSparseMinor]
+	sparseName, sparseNameOk := headers[paxGNUSparseName]
+	_, sparseMapOk := headers[paxGNUSparseMap]
+	sparseSize, sparseSizeOk := headers[paxGNUSparseSize]
+	sparseRealSize, sparseRealSizeOk := headers[paxGNUSparseRealSize]
+
+	// Identify which, if any, sparse format applies from which PAX headers are set
+	if majorOk && minorOk {
+		sparseFormat = major + "." + minor
+	} else if sparseNameOk && sparseMapOk {
+		sparseFormat = "0.1"
+	} else if sparseSizeOk {
+		sparseFormat = "0.0"
+	} else {
+		// Not a PAX format GNU sparse file.
+		return nil, nil
+	}
+
+	// Check for unknown sparse format
+	if sparseFormat != "0.0" && sparseFormat != "0.1" && sparseFormat != "1.0" {
+		return nil, nil
+	}
+
+	// Update hdr from GNU sparse PAX headers
+	if sparseNameOk {
+		hdr.Name = sparseName
+	}
+	if sparseSizeOk {
+		realSize, err := strconv.ParseInt(sparseSize, 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		hdr.Size = realSize
+	} else if sparseRealSizeOk {
+		realSize, err := strconv.ParseInt(sparseRealSize, 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		hdr.Size = realSize
+	}
+
+	// Set up the sparse map, according to the particular sparse format in use
+	var sp []sparseEntry
+	var err error
+	switch sparseFormat {
+	case "0.0", "0.1":
+		sp, err = readGNUSparseMap0x1(headers)
+	case "1.0":
+		sp, err = readGNUSparseMap1x0(tr.curr)
+	}
+	return sp, err
+}
+
 // mergePAX merges well known headers according to PAX standard.
 // In general headers with the same name as those found
 // in the header struct overwrite those found in the header
@@ -194,6 +312,11 @@ func parsePAX(r io.Reader) (map[string]string, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	// For GNU PAX sparse format 0.0 support.
+	// This function transforms the sparse format 0.0 headers into sparse format 0.1 headers.
+	var sparseMap bytes.Buffer
+
 	headers := make(map[string]string)
 	// Each record is constructed as
 	//     "%d %s=%s\n", length, keyword, value
@@ -221,7 +344,21 @@ func parsePAX(r io.Reader) (map[string]string, error) {
 		if err != nil {
 			return nil, ErrHeader
 		}
 		key, value := record[:eq], record[eq+1:]
-		headers[string(key)] = string(value)
+
+		keyStr := string(key)
+		if keyStr == paxGNUSparseOffset || keyStr == paxGNUSparseNumBytes {
+			// GNU sparse format 0.0 special key. Write to sparseMap instead of using the headers map.
+			sparseMap.Write(value)
+			sparseMap.Write([]byte{','})
+		} else {
+			// Normal key. Set the value in the headers map.
+			headers[keyStr] = string(value)
+		}
+	}
+	if sparseMap.Len() != 0 {
+		// Add sparse info to headers, chopping off the extra comma
+		sparseMap.Truncate(sparseMap.Len() - 1)
+		headers[paxGNUSparseMap] = sparseMap.String()
 	}
 	return headers, nil
 }
@@ -268,8 +405,8 @@ func (tr *Reader) octal(b []byte) int64 {
 
 // skipUnread skips any unread bytes in the existing file entry, as well as any alignment padding.
 func (tr *Reader) skipUnread() {
-	nr := tr.nb + tr.pad // number of bytes to skip
-	tr.nb, tr.pad = 0, 0
+	nr := tr.numBytes() + tr.pad // number of bytes to skip
+	tr.curr, tr.pad = nil, 0
 	if sr, ok := tr.r.(io.Seeker); ok {
 		if _, err := sr.Seek(nr, os.SEEK_CUR); err == nil {
 			return
@@ -373,30 +510,305 @@ func (tr *Reader) readHeader() *Header {
 
 	// Maximum value of hdr.Size is 64 GB (12 octal digits),
 	// so there's no risk of int64 overflowing.
-	tr.nb = int64(hdr.Size)
-	tr.pad = -tr.nb & (blockSize - 1) // blockSize is a power of two
+	nb := int64(hdr.Size)
+	tr.pad = -nb & (blockSize - 1) // blockSize is a power of two
+
+	// Set the current file reader.
+	tr.curr = &regFileReader{r: tr.r, nb: nb}
+
+	// Check for old GNU sparse format entry.
+	if hdr.Typeflag == TypeGNUSparse {
+		// Get the real size of the file.
+		hdr.Size = tr.octal(header[483:495])
+
+		// Read the sparse map.
+		sp := tr.readOldGNUSparseMap(header)
+		if tr.err != nil {
+			return nil
+		}
+		// Current file is a GNU sparse file. Update the current file reader.
+		tr.curr = &sparseFileReader{rfr: tr.curr.(*regFileReader), sp: sp, tot: hdr.Size}
+	}
 
 	return hdr
 }
 
+// A sparseEntry holds a single entry in a sparse file's sparse map.
+// A sparse entry indicates the offset and size in a sparse file of a
+// block of data.
+type sparseEntry struct {
+	offset   int64
+	numBytes int64
+}
+
+// readOldGNUSparseMap reads the sparse map as stored in the old GNU sparse format.
+// The sparse map is stored in the tar header if it's small enough. If it's larger than four entries,
+// then one or more extension headers are used to store the rest of the sparse map.
+func (tr *Reader) readOldGNUSparseMap(header []byte) []sparseEntry {
+	isExtended := header[oldGNUSparseMainHeaderIsExtendedOffset] != 0
+	spCap := oldGNUSparseMainHeaderNumEntries
+	if isExtended {
+		spCap += oldGNUSparseExtendedHeaderNumEntries
+	}
+	sp := make([]sparseEntry, 0, spCap)
+	s := slicer(header[oldGNUSparseMainHeaderOffset:])
+
+	// Read the four entries from the main tar header
+	for i := 0; i < oldGNUSparseMainHeaderNumEntries; i++ {
+		offset := tr.octal(s.next(oldGNUSparseOffsetSize))
+		numBytes := tr.octal(s.next(oldGNUSparseNumBytesSize))
+		if tr.err != nil {
+			tr.err = ErrHeader
+			return nil
+		}
+		if offset == 0 && numBytes == 0 {
+			break
+		}
+		sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+	}
+
+	for isExtended {
+		// There are more entries. Read an extension header and parse its entries.
+		sparseHeader := make([]byte, blockSize)
+		if _, tr.err = io.ReadFull(tr.r, sparseHeader); tr.err != nil {
+			return nil
+		}
+		isExtended = sparseHeader[oldGNUSparseExtendedHeaderIsExtendedOffset] != 0
+		s = slicer(sparseHeader)
+		for i := 0; i < oldGNUSparseExtendedHeaderNumEntries; i++ {
+			offset := tr.octal(s.next(oldGNUSparseOffsetSize))
+			numBytes := tr.octal(s.next(oldGNUSparseNumBytesSize))
+			if tr.err != nil {
+				tr.err = ErrHeader
+				return nil
+			}
+			if offset == 0 && numBytes == 0 {
+				break
+			}
+			sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+		}
+	}
+	return sp
+}
+
+// readGNUSparseMap1x0 reads the sparse map as stored in GNU's PAX sparse format version 1.0.
+// The sparse map is stored just before the file data and padded out to the nearest block boundary.
+func readGNUSparseMap1x0(r io.Reader) ([]sparseEntry, error) {
+	buf := make([]byte, 2*blockSize)
+	sparseHeader := buf[:blockSize]
+
+	// readDecimal is a helper function to read a decimal integer from the sparse map
+	// while making sure to read from the file in blocks of size blockSize
+	readDecimal := func() (int64, error) {
+		// Look for newline
+		nl := bytes.IndexByte(sparseHeader, '\n')
+		if nl == -1 {
+			if len(sparseHeader) >= blockSize {
+				// This is an error
+				return 0, ErrHeader
+			}
+			oldLen := len(sparseHeader)
+			newLen := oldLen + blockSize
+			if cap(sparseHeader) < newLen {
+				// There's more header, but we need to make room for the next block
+				copy(buf, sparseHeader)
+				sparseHeader = buf[:newLen]
+			} else {
+				// There's more header, and we can just reslice
+				sparseHeader = sparseHeader[:newLen]
+			}
+
+			// Now that sparseHeader is large enough, read next block
+			if _, err := io.ReadFull(r, sparseHeader[oldLen:newLen]); err != nil {
+				return 0, err
+			}
+
+			// Look for a newline in the new data
+			nl = bytes.IndexByte(sparseHeader[oldLen:newLen], '\n')
+			if nl == -1 {
+				// This is an error
+				return 0, ErrHeader
+			}
+			nl += oldLen // We want the position from the beginning
+		}
+		// Now that we've found a newline, read a number
+		n, err := strconv.ParseInt(string(sparseHeader[:nl]), 10, 0)
+		if err != nil {
+			return 0, ErrHeader
+		}
+
+		// Update sparseHeader to consume this number
+		sparseHeader = sparseHeader[nl+1:]
+		return n, nil
+	}
+
+	// Read the first block
+	if _, err := io.ReadFull(r, sparseHeader); err != nil {
+		return nil, err
+	}
+
+	// The first line contains the number of entries
+	numEntries, err := readDecimal()
+	if err != nil {
+		return nil, err
+	}
+
+	// Read all the entries
+	sp := make([]sparseEntry, 0, numEntries)
+	for i := int64(0); i < numEntries; i++ {
+		// Read the offset
+		offset, err := readDecimal()
+		if err != nil {
+			return nil, err
+		}
+		// Read numBytes
+		numBytes, err := readDecimal()
+		if err != nil {
+			return nil, err
+		}
+
+		sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+	}
+
+	return sp, nil
+}
+
+// readGNUSparseMap0x1 reads the sparse map as stored in GNU's PAX sparse format version 0.1.
+// The sparse map is stored in the PAX headers.
+func readGNUSparseMap0x1(headers map[string]string) ([]sparseEntry, error) {
+	// Get number of entries
+	numEntriesStr, ok := headers[paxGNUSparseNumBlocks]
+	if !ok {
+		return nil, ErrHeader
+	}
+	numEntries, err := strconv.ParseInt(numEntriesStr, 10, 0)
+	if err != nil {
+		return nil, ErrHeader
+	}
+
+	sparseMap := strings.Split(headers[paxGNUSparseMap], ",")
+
+	// There should be two numbers in sparseMap for each entry
+	if int64(len(sparseMap)) != 2*numEntries {
+		return nil, ErrHeader
+	}
+
+	// Loop through the entries in the sparse map
+	sp := make([]sparseEntry, 0, numEntries)
+	for i := int64(0); i < numEntries; i++ {
+		offset, err := strconv.ParseInt(sparseMap[2*i], 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		numBytes, err := strconv.ParseInt(sparseMap[2*i+1], 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+	}
+
+	return sp, nil
+}
+
+// numBytes returns the number of bytes left to read in the current file's entry
+// in the tar archive, or 0 if there is no current file.
+func (tr *Reader) numBytes() int64 {
+	if tr.curr == nil {
+		// No current file, so no bytes
+		return 0
+	}
+	return tr.curr.numBytes()
+}
+
 // Read reads from the current entry in the tar archive.
 // It returns 0, io.EOF when it reaches the end of that entry,
 // until Next is called to advance to the next entry.
 func (tr *Reader) Read(b []byte) (n int, err error) {
-	if tr.nb == 0 {
+	n, err = tr.curr.Read(b)
+	if err != nil && err != io.EOF {
+		tr.err = err
+	}
+	return
+}
+
+func (rfr *regFileReader) Read(b []byte) (n int, err error) {
+	if rfr.nb == 0 {
 		// file consumed
 		return 0, io.EOF
 	}
-
-	if int64(len(b)) > tr.nb {
-		b = b[0:tr.nb]
-	}
-	n, err = tr.r.Read(b)
-	tr.nb -= int64(n)
-
-	if err == io.EOF && tr.nb > 0 {
+	if int64(len(b)) > rfr.nb {
+		b = b[0:rfr.nb]
+	}
+	n, err = rfr.r.Read(b)
+	rfr.nb -= int64(n)
+
+	if err == io.EOF && rfr.nb > 0 {
 		err = io.ErrUnexpectedEOF
 	}
-	tr.err = err
 	return
 }
+
+// numBytes returns the number of bytes left to read in the file's data in the tar archive.
+func (rfr *regFileReader) numBytes() int64 {
+	return rfr.nb
+}
+
+// readHole reads a sparse file hole ending at offset toOffset
+func (sfr *sparseFileReader) readHole(b []byte, toOffset int64) int {
+	n64 := toOffset - sfr.pos
+	if n64 > int64(len(b)) {
+		n64 = int64(len(b))
+	}
+	n := int(n64)
+	for i := 0; i < n; i++ {
+		b[i] = 0
+	}
+	sfr.pos += n64
+	return n
+}
+
+// Read reads the sparse file data in expanded form.
+func (sfr *sparseFileReader) Read(b []byte) (n int, err error) {
+	if len(sfr.sp) == 0 {
+		// No more data fragments to read from.
+		if sfr.pos < sfr.tot {
+			// We're in the last hole
+			n = sfr.readHole(b, sfr.tot)
+			return
+		}
+		// Otherwise, we're at the end of the file
+		return 0, io.EOF
+	}
+	if sfr.pos < sfr.sp[0].offset {
+		// We're in a hole
+		n = sfr.readHole(b, sfr.sp[0].offset)
+		return
+	}
+
+	// We're not in a hole, so we'll read from the next data fragment
+	posInFragment := sfr.pos - sfr.sp[0].offset
+	bytesLeft := sfr.sp[0].numBytes - posInFragment
+	if int64(len(b)) > bytesLeft {
+		b = b[0:bytesLeft]
+	}
+
+	n, err = sfr.rfr.Read(b)
+	sfr.pos += int64(n)
+
+	if int64(n) == bytesLeft {
+		// We're done with this fragment
+		sfr.sp = sfr.sp[1:]
+	}
+
+	if err == io.EOF && sfr.pos < sfr.tot {
+		// We reached the end of the last fragment's data, but there's a final hole
+		err = nil
+	}
+	return
+}
+
+// numBytes returns the number of bytes left to read in the sparse file's
+// sparse-encoded data in the tar archive.
+func (sfr *sparseFileReader) numBytes() int64 {
+	return sfr.rfr.nb
+}

src/pkg/archive/tar/reader_test.go

--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -9,6 +9,7 @@ import (
 	"crypto/md5"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"reflect"
 	"strings"
@@ -54,8 +55,92 @@ var gnuTarTest = &untarTest{
 	},
 }
 
+var sparseTarTest = &untarTest{
+	file: "testdata/sparse-formats.tar",
+	headers: []*Header{
+		{
+			Name:     "sparse-gnu",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392395740, 0),
+			Typeflag: 0x53,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "sparse-posix-0.0",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392342187, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "sparse-posix-0.1",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392340456, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "sparse-posix-1.0",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392337404, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "end",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     4,
+			ModTime:  time.Unix(1392398319, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+	},
+	cksums: []string{
+		"6f53234398c2449fe67c1812d993012f",
+		"6f53234398c2449fe67c1812d993012f",
+		"6f53234398c2449fe67c1812d993012f",
+		"6f53234398c2449fe67c1812d993012f",
+		"b0061974914468de549a2af8ced10316",
+	},
+}
+
 var untarTests = []*untarTest{
 	gnuTarTest,
+	sparseTarTest,
 	{
 		file: "testdata/star.tar",
 		headers: []*Header{
@@ -423,3 +508,220 @@ func TestMergePAX(t *testing.T) {
 	\tt.Errorf("incorrect merge: got %+v, want %+v", hdr, want)\n \t}\n }\n+\n+func TestSparseEndToEnd(t *testing.T) {\n+\ttest := sparseTarTest\n+\tf, err := os.Open(test.file)\n+\tif err != nil {\n+\t\tt.Fatalf("Unexpected error: %v", err)\n+\t}\n+\tdefer f.Close()\n+\n+\ttr := NewReader(f)\n+\n+\theaders := test.headers\n+\tcksums := test.cksums\n+\tnread := 0\n+\n+\t// loop over all files\n+\tfor ; ; nread++ {\n+\t\thdr, err := tr.Next()\n+\t\tif hdr == nil || err == io.EOF {\n+\t\t\tbreak\n+\t\t}\n+\n+\t\t// check the header\n+\t\tif !reflect.DeepEqual(*hdr, *headers[nread]) {\n+\t\t\tt.Errorf("Incorrect header:\\nhave %+v\\nwant %+v",\n+\t\t\t\t*hdr, headers[nread])\n+\t\t}\n+\n+\t\t// read and checksum the file data\n+\t\th := md5.New()\n+\t\t_, err = io.Copy(h, tr)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf("Unexpected error: %v", err)\n+\t\t}\n+\n+\t\t// verify checksum\n+\t\thave := fmt.Sprintf("%x", h.Sum(nil))\n+\t\twant := cksums[nread]\n+\t\tif want != have {\n+\t\t\tt.Errorf("Bad checksum on file %s:\\nhave %+v\\nwant %+v", hdr.Name, have, want)\n+\t\t}\n+\t}\n+\tif nread != len(headers) {\n+\t\tt.Errorf("Didn't process all files\\nexpected: %d\\nprocessed %d\\n", len(headers), nread)\n+\t}\n+}\n+\n+type sparseFileReadTest struct {\n+\tsparseData []byte\n+\tsparseMap  []sparseEntry\n+\trealSize   int64\n+\texpected   []byte\n+}\n+\n+var sparseFileReadTests = []sparseFileReadTest{\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 0, numBytes: 2},\n+\t\t\t{offset: 5, numBytes: 3},\n+\t\t},\n+\t\trealSize: 8,\n+\t\texpected: []byte("ab\\x00\\x00\\x00cde"),\n+\t},\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 0, numBytes: 2},\n+\t\t\t{offset: 5, numBytes: 3},\n+\t\t},\n+\t\trealSize: 10,\n+\t\texpected: []byte("ab\\x00\\x00\\x00cde\\x00\\x00"),\n+\t},\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 1, numBytes: 3},\n+\t\t\t{offset: 6, numBytes: 2},\n+\t\t},\n+\t\trealSize: 8,\n+\t\texpected: []byte("\\x00abc\\x00\\x00de"),\n+\t},\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 1, numBytes: 3},\n+\t\t\t{offset: 6, numBytes: 2},\n+\t\t},\n+\t\trealSize: 10,\n+\t\texpected: []byte("\\x00abc\\x00\\x00de\\x00\\x00"),\n+\t},\n+\t{\n+\t\tsparseData: []byte(""),\n+\t\tsparseMap:  nil,\n+\t\trealSize:   2,\n+\t\texpected:   []byte("\\x00\\x00"),\n+\t},\n+}\n+\n+func TestSparseFileReader(t *testing.T) {\n+\tfor i, test := range sparseFileReadTests {\n+\t\tr := bytes.NewReader(test.sparseData)\n+\t\tnb := int64(r.Len())\n+\t\tsfr := &sparseFileReader{\n+\t\t\trfr: &regFileReader{r: r, nb: nb},\n+\t\t\tsp:  test.sparseMap,\n+\t\t\tpos: 0,\n+\t\t\ttot: test.realSize,\n+\t\t}\n+\t\tif sfr.numBytes() != nb {\n+\t\t\tt.Errorf("test %d: Before reading, sfr.numBytes() = %d, want %d", i, sfr.numBytes, nb)\n+\t\t}\n+\t\tbuf, err := ioutil.ReadAll(sfr)\n+\t\tif err != nil {\n+\t\t\tt.Errorf("test %d: Unexpected error: %v", i, err)\n+\t\t}\n+\t\tif e := test.expected; !bytes.Equal(buf, e) {\n+\t\t\tt.Errorf("test %d: Contents = %v, want %v", i, buf, e)\n+\t\t}\n+\t\tif sfr.numBytes() != 0 {\n+\t\t\tt.Errorf("test %d: After draining the reader, numBytes() was nonzero", i)\n+\t\t}\n+\t}\n+}\n+\n+func TestSparseIncrementalRead(t *testing.T) {\n+\tsparseMap := []sparseEntry{{10, 2}}\n+\tsparseData := []byte("Go")\n+\texpected := "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00Go\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"\n+\n+\tr := bytes.NewReader(sparseData)\n+\tnb := int64(r.Len())\n+\tsfr := &sparseFileReader{\n+\t\trfr: &regFileReader{r: r, nb: nb},\n+\t\tsp:  sparseMap,\n+\t\tpos: 0,\n+\t\ttot: int64(len(expected)),\n+\t}\n+\n+\t// We'll read the data 6 bytes at a time, with a hole of size 10 at\n+\t// the beginning and one of size 8 at the end.\n+\tvar outputBuf bytes.Buffer\n+\tbuf := make([]byte, 6)\n+\tfor {\n+\t\tn, err := sfr.Read(buf)\n+\t\tif err == io.EOF {\n+\t\t\tbreak\n+\t\t}\n+\t\tif err != nil {\n+\t\t\tt.Errorf("Read: unexpected error %v\\n", err)\n+\t\t}\n+\t\tif n > 0 {\n+\t\t\t_, err := outputBuf.Write(buf[:n])\n+\t\t\tif err != nil {\n+\t\t\t\tt.Errorf("Write: unexpected error %v\\n", err)\n+\t\t\t}\n+\t\t}\n+\t}\n+\tgot := outputBuf.String()\n+\tif got != expected {\n+\t\tt.Errorf("Contents = %v, want %v", got, expected)\n+\t}\n+}\n+\n+func TestReadGNUSparseMap0x1(t *testing.T) {\n+\theaders := map[string]string{\n+\t\tpaxGNUSparseNumBlocks: "4",\n+\t\tpaxGNUSparseMap:       "0,5,10,5,20,5,30,5",\n+\t}\n+\texpected := []sparseEntry{\n+\t\t{offset: 0, numBytes: 5},\n+\t\t{offset: 10, numBytes: 5},\n+\t\t{offset: 20, numBytes: 5},\n+\t\t{offset: 30, numBytes: 5},\n+\t}\n+\n+\tsp, err := readGNUSparseMap0x1(headers)\n+\tif err != nil {\n+\t\tt.Errorf("Unexpected error: %v", err)\n+\t}\n+\tif !reflect.DeepEqual(sp, expected) {\n+\t\tt.Errorf("Incorrect sparse map: got %v, wanted %v", sp, expected)\n+\t}\n+}\n+\n+func TestReadGNUSparseMap1x0(t *testing.T) {\n+\t// This test uses lots of holes so the sparse header takes up more than two blocks\n+\tnumEntries := 100\n+\texpected := make([]sparseEntry, 0, numEntries)\n+\tsparseMap := new(bytes.Buffer)\n+\n+\tfmt.Fprintf(sparseMap, "%d\\n", numEntries)\n+\tfor i := 0; i < numEntries; i++ {\n+\t\toffset := int64(2048 * i)\n+\t\tnumBytes := int64(1024)\n+\t\texpected = append(expected, sparseEntry{offset: offset, numBytes: numBytes})\n+\t\tfmt.Fprintf(sparseMap, "%d\\n%d\\n", offset, numBytes)\n+\t}\n+\n+\t// Make the header the smallest multiple of blockSize that fits the sparseMap\n+\theaderBlocks := (sparseMap.Len() + blockSize - 1) / blockSize\n+\tbufLen := blockSize * headerBlocks\n+\tbuf := make([]byte, bufLen)\n+\tcopy(buf, sparseMap.Bytes())\n+\n+\t// Get an reader to read the sparse map\n+\tr := bytes.NewReader(buf)\n+\n+\t// Read the sparse map\n+\tsp, err := readGNUSparseMap1x0(r)\n+\tif err != nil {\n+\t\tt.Errorf("Unexpected error: %v", err)\n+\t}\n+\tif !reflect.DeepEqual(sp, expected) {\n+\t\tt.Errorf("Incorrect sparse map: got %v, wanted %v", sp, expected)\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/pkg/archive/tar/common.go`

-   **`TypeGNUSparse = 'S'`の追加**:
    この定数は、Tarアーカイブ内のエントリが古いGNU sparseファイルであることを示す新しいタイプフラグを定義します。Tarヘッダの`Typeflag`フィールドがこの値を持つ場合、`archive/tar`パッケージは、そのファイルが疎ファイルとして特別に処理される必要があることを認識します。

### `src/pkg/archive/tar/reader.go`

-   **`Reader`構造体の変更**:
    -   `r io.Reader`, `err error`, `pad int64`は既存のフィールドですが、`nb int64`(未読バイト数)が削除され、代わりに`curr numBytesReader`が追加されました。これは、ファイルデータの読み取りロジックが、通常のファイルと疎ファイルで異なる実装を持つ`numBytesReader`インターフェースに抽象化されたことを意味します。

-   **`numBytesReader`インターフェース**:
    -   `io.Reader`と`numBytes() int64`メソッドを持つ新しいインターフェースです。`numBytes()`は、現在のファイルエントリに残っている未読バイト数を返します。これにより、`Reader`は具体的なファイルタイプ(通常ファイルか疎ファイルか)を意識せずに、統一された方法で残りのバイト数を取得し、データを読み取ることができます。

-   **`regFileReader`構造体**:
    -   `numBytesReader`インターフェースの基本的な実装で、通常のファイルデータを読み取ります。`r`は基になる`io.Reader`(Tarアーカイブ全体)、`nb`は現在のファイルエントリの未読バイト数です。

-   **`sparseFileReader`構造体**:
    -   `numBytesReader`インターフェースの疎ファイル特化実装です。
    -   `rfr *regFileReader`: 疎ファイル内の実際のデータブロックを読み取るために使用されます。
    -   `sp []sparseEntry`: 疎ファイルのオフセットとバイト数のマップ(疎マップ)を保持します。
    -   `pos int64`: 疎ファイル内の現在の読み取り位置を追跡します。
    -   `tot int64`: 疎ファイルの論理的な合計サイズ(穴を含む)を保持します。
    -   この構造体は、`Read`メソッドが呼び出されたときに、疎マップに基づいて「穴」の部分をゼロで埋め、データブロックの部分を`rfr`から読み取ることで、疎ファイルを展開された形式で提供します。

-   **PAX GNU SparseヘッダおよびOld GNU Sparseヘッダのキーワード定数**:
    -   これらの定数は、PAX拡張ヘッダや古いGNU Tarヘッダ内で疎ファイル関連の情報を識別するために使用されます。例えば、`paxGNUSparseMap`はPAX 0.1フォーマットで疎マップが格納されるキーを示し、`oldGNUSparseMainHeaderOffset`は古いGNUフォーマットで疎マップが始まるオフセットを示します。

-   **`Reader.Next()`メソッドの変更**:
    -   `Next()`メソッドは、Tarヘッダを読み込んだ後、まず`checkForGNUSparsePAXHeaders`を呼び出してPAX形式の疎ファイルを検出します。検出された場合、`tr.curr`を`sparseFileReader`のインスタンスに設定し、疎ファイルとして処理を開始します。
    -   また、`readHeader()`内で古いGNU sparseファイル(`TypeGNUSparse`)が検出された場合も、同様に`tr.curr`が`sparseFileReader`に設定されます。これにより、`Reader`は次の`Read`呼び出しから、疎ファイルの特性を考慮した読み取りを行うことができます。

-   **`checkForGNUSparsePAXHeaders()`関数**:
    -   この関数は、PAXヘッダ内の特定のキー(`GNU.sparse.major`, `GNU.sparse.minor`, `GNU.sparse.map`など)の存在をチェックすることで、どのGNU PAX sparseフォーマット(0.0, 0.1, 1.0)が使用されているかを識別します。
    -   識別されたフォーマットに基づいて、適切な疎マップ読み取り関数(`readGNUSparseMap0x1`または`readGNUSparseMap1x0`)を呼び出し、疎マップを解析して返します。
    -   これにより、`archive/tar`パッケージは、異なるPAX sparseフォーマットのTarアーカイブを透過的に処理できます。

-   **`parsePAX()`でのGNU sparse format 0.0の変換ロジック**:
    -   `parsePAX()`関数は、PAX拡張ヘッダを解析する際に、`paxGNUSparseOffset`と`paxGNUSparseNumBytes`というキーを特別に扱います。これらはGNU sparse format 0.0で使用されるキーで、オフセットとバイト数が個別のエントリとして格納されます。
    -   このコミットでは、これらの個別のエントリを読み取り、カンマで区切られた文字列として`sparseMap bytes.Buffer`に結合します。最終的に、この結合された文字列が`paxGNUSparseMap`キーの値として`headers`マップに追加されます。
    -   この変換により、内部的にはGNU sparse format 0.0のデータがバージョン0.1の形式に統一され、後続の`readGNUSparseMap0x1`関数で一貫して処理できるようになります。

-   **`readOldGNUSparseMap()`関数**:
    -   古いGNU sparseフォーマットの疎マップを読み取るための関数です。このフォーマットでは、疎マップがTarヘッダの固定オフセットに格納されるか、または追加の拡張ヘッダブロックに格納されます。
    -   関数は、まずメインヘッダから疎マップエントリを読み取り、必要に応じて追加の拡張ヘッダブロックを読み込んで残りのエントリを解析します。

-   **`readGNUSparseMap1x0()`関数**:
    -   GNU PAX sparseフォーマット1.0の疎マップを読み取るための関数です。このフォーマットでは、疎マップがファイルデータ本体の直前に、改行区切りの数値(エントリ数、オフセット、バイト数)のリストとして格納されます。
    -   関数は、基になる`io.Reader`からブロック単位でデータを読み込み、改行を区切りとして数値を解析し、疎マップを構築します。

-   **`readGNUSparseMap0x1()`関数**:
    -   GNU PAX sparseフォーマット0.1の疎マップを読み取るための関数です。このフォーマットでは、疎マップがPAXヘッダの`GNU.sparse.map`キーに、カンマ区切りのオフセットとバイト数のペアのリストとして格納されています。
    -   関数は、`headers`マップから`paxGNUSparseMap`の値を取得し、カンマで分割して数値を解析し、疎マップを構築します。

-   **`Reader.Read()`、`regFileReader.Read()`、`sparseFileReader.Read()`メソッド**:
    -   `Reader.Read()`は、実際の読み取り処理を`tr.curr`(`numBytesReader`インターフェース)に委譲します。
    -   `regFileReader.Read()`は、通常のファイルデータを基になる`io.Reader`から読み込み、未読バイト数を更新します。
    -   `sparseFileReader.Read()`は、疎ファイルの読み取りの中核をなす部分です。
        -   現在の読み取り位置`sfr.pos`と疎マップ`sfr.sp`を比較し、次に読み取るべき領域が「穴」であるか「データブロック」であるかを判断します。
        -   「穴」の場合、要求されたバイト数または次のデータブロックまでのバイト数分、出力バッファをゼロで埋めます。
        -   「データブロック」の場合、内部の`regFileReader`(`sfr.rfr`)から実際のデータを読み込みます。
        -   データブロックの読み取りが完了すると、疎マップからそのエントリを削除し、次のデータブロックまたは穴の処理に備えます。
        -   これにより、疎ファイルはアプリケーションに対して、あたかもすべてのゼロバイトが実際に存在するかのように透過的に提供されます。

### `src/pkg/archive/tar/reader_test.go`

-   **`sparseTarTest`の追加**:
    -   様々なGNU sparseフォーマット(Old GNU, PAX 0.0, 0.1, 1.0)を含む`sparse-formats.tar`ファイルに対するテストデータセットが追加されました。これには、各エントリの期待されるヘッダ情報とMD5チェックサムが含まれます。

-   **`TestSparseEndToEnd()`関数**:
    -   `sparseTarTest`データセットを使用して、`archive/tar`パッケージが`sparse-formats.tar`ファイルを正しく読み取り、各エントリのヘッダとデータが期待通りであることを検証するエンドツーエンドテストです。ファイルデータのMD5チェックサムを計算し、期待値と比較することで、疎ファイルが正しく展開されていることを確認します。

-   **`sparseFileReadTest`構造体と`sparseFileReadTests`**:
    -   `sparseFileReader`の単体テストのためのデータ構造とテストケースの配列です。疎データ、疎マップ、実際のファイルサイズ、そして期待される展開後のバイト列を定義します。これにより、様々な疎マップのパターンと読み取りシナリオがカバーされます。

-   **`TestSparseFileReader()`関数**:
    -   `sparseFileReadTests`を使用して、`sparseFileReader`の`Read`メソッドが正しく動作するかを検証する単体テストです。疎ファイルが正しく展開され、穴がゼロで埋められていることを確認します。

-   **`TestSparseIncrementalRead()`関数**:
    -   `sparseFileReader`が、小さなバッファで段階的に読み取られた場合でも正しく動作するかを検証するテストです。これにより、`Read`メソッドが部分的な読み取り要求にも対応できることが保証されます。

-   **`TestReadGNUSparseMap0x1()`および`TestReadGNUSparseMap1x0()`関数**:
    -   それぞれGNU PAX sparseフォーマット0.1と1.0の疎マップ読み取り関数(`readGNUSparseMap0x1`と`readGNUSparseMap1x0`)の単体テストです。これらのテストは、特定の入力ヘッダまたはリーダーから、期待される疎マップが正しく解析されることを確認します。

これらのテストは、GNU sparseファイルの複雑なフォーマットと読み取りロジックが、Goの`archive/tar`パッケージによって正確に実装されていることを保証します。

## 関連リンク

-   Go Issue #3864: [archive/tar: add support for GNU sparse files](https://github.com/golang/go/issues/3864)
-   Gerrit Code Review: [https://golang.org/cl/64740043](https://golang.org/cl/64740043)

## 参考にした情報源リンク

-   GNU Tar Manual: [https://www.gnu.org/software/tar/manual/](https://www.gnu.org/software/tar/manual/) (特に"Sparse Files"のセクション)
-   POSIX.1-2001 (PAX): [https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html](https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html)
-   Sparse file - Wikipedia: [https://en.wikipedia.org/wiki/Sparse_file](https://en.wikipedia.org/wiki/Sparse_file)
-   Tar (file format) - Wikipedia: [https://en.wikipedia.org/wiki/Tar_(file_format)](https://en.wikipedia.org/wiki/Tar_(file_format))
```markdown
# [インデックス 19022] ファイルの概要

このコミットは、Go言語の標準ライブラリ`archive/tar`パッケージに、GNU sparseファイルのサポートを追加するものです。具体的には、以下のファイルが変更されています。

-   `src/pkg/archive/tar/common.go`: Tarヘッダのタイプフラグに新しい定数が追加されました。
-   `src/pkg/archive/tar/reader.go`: Tarアーカイブの読み取りロジックが大幅に拡張され、GNU sparseファイルの解析と読み取りに対応しました。新しいデータ構造、インターフェース、および複数のヘルパー関数が導入されています。
-   `src/pkg/archive/tar/reader_test.go`: GNU sparseファイルの読み取り機能が正しく動作することを確認するための、広範なテストケースが追加されました。
-   `src/pkg/archive/tar/testdata/sparse-formats.tar`: テスト用のバイナリファイルで、様々なGNU sparseフォーマットのデータが含まれています。

## コミット

commit 730db0affc642530daf9129f4fbc89a4e40f9c95 Author: David Thomas davidthomas426@gmail.com Date: Thu Apr 3 20:01:04 2014 +0000

archive/tar: add support for GNU sparse files.

Supports all the current GNU tar sparse formats, including the
old GNU format and the GNU PAX format versions 0.0, 0.1, and 1.0.
Fixes #3864.

LGTM=rsc
R=golang-codereviews, dave, gobot, dsymonds, rsc
CC=golang-codereviews
https://golang.org/cl/64740043

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

[https://github.com/golang/go/commit/730db0affc642530daf9129f4fbc89a4e40f9c95](https://github.com/golang/go/commit/730db0affc642530daf9129f4fbc89a4e40f9c95)

## 元コミット内容

archive/tar: add support for GNU sparse files.

Supports all the current GNU tar sparse formats, including the old GNU format and the GNU PAX format versions 0.0, 0.1, and 1.0. Fixes #3864.

LGTM=rsc R=golang-codereviews, dave, gobot, dsymonds, rsc CC=golang-codereviews https://golang.org/cl/64740043


## 変更の背景

このコミットの主な目的は、Go言語の`archive/tar`パッケージがGNU tarによって作成された疎(sparse)ファイルを正しく読み取れるようにすることです。疎ファイルは、ファイル内に連続するゼロのブロック(「穴」または「スパース領域」と呼ばれる)がある場合に、その領域を実際にディスクに書き込まずにメタデータとして記録することで、ディスクスペースを節約する特殊なファイル形式です。

従来の`archive/tar`パッケージでは、このような疎ファイルを適切に処理できず、アーカイブから展開する際にデータが破損したり、予期せぬ動作を引き起こす可能性がありました。特に、大きなデータベースファイルや仮想ディスクイメージなど、多くのゼロバイトを含むファイルでは、疎ファイル形式が頻繁に利用されます。これらのファイルをGoプログラムで扱う際に、互換性の問題が生じていました。

この問題は、GoのIssue #3864として報告されており、このコミットはその問題を解決するために実装されました。GNU tarは、疎ファイルを扱うためのいくつかの異なるフォーマット(古いGNUフォーマット、PAXフォーマットのバージョン0.0、0.1、1.0)をサポートしており、このコミットはこれらすべての主要なフォーマットに対応することで、幅広い互換性を提供します。これにより、GoアプリケーションがGNU tarで作成されたアーカイブをより堅牢に処理できるようになります。

## 前提知識の解説

### Tarアーカイブ

Tar(Tape Archive)は、複数のファイルを一つのアーカイブファイルにまとめるためのファイル形式です。元々は磁気テープにデータを保存するために設計されましたが、現在ではファイルシステム上のアーカイブや配布形式として広く利用されています。Tarアーカイブは、各ファイルのメタデータ(ファイル名、パーミッション、タイムスタンプなど)とファイルデータが連続して格納されるシンプルな構造を持っています。

### GNU Tar

GNU Tarは、標準的なTar形式に加えて、いくつかの拡張機能を持つTarの実装です。これらの拡張機能には、長いファイル名のサポート、ACL(Access Control List)や拡張属性の保存、そして本コミットの主題である疎ファイルのサポートなどが含まれます。GNU TarはLinuxシステムで広く使われており、その拡張機能は多くのTarアーカイブで利用されています。

### Sparse Files (疎ファイル)

疎ファイルは、ファイルシステムが提供する機能の一つで、ファイル内の連続するゼロバイトのブロック(「穴」または「スパース領域」)を、実際にディスク上の物理ブロックとして割り当てずに表現するファイルです。これにより、ファイルが論理的には大きくても、実際にディスクを消費する量はデータが書き込まれている部分のみとなり、ディスクスペースを大幅に節約できます。

例えば、1GBのファイルのうち、ほとんどがゼロで、一部にだけデータが書き込まれている場合、疎ファイルとして保存すれば、実際にディスクに書き込まれるのはデータ部分とメタデータのみとなり、残りのゼロ部分はディスクスペースを消費しません。ファイルを読み出す際には、ファイルシステムが「穴」の部分をゼロとして返します。

疎ファイルの利点:
-   **ディスクスペースの節約**: ゼロバイトの領域を物理的に保存しないため、ストレージ容量を効率的に利用できます。
-   **効率的なデータ転送**: ネットワーク経由で疎ファイルを転送する際、ゼロの領域をスキップできるため、転送時間を短縮できます。
-   **高速なファイル作成**: 巨大なファイルを初期化する際に、すべてのバイトをゼロで埋める必要がないため、ファイル作成が高速になります。

### PAX (Portable Archive eXchange) フォーマット

PAXは、POSIX標準によって定義されたTarアーカイブの拡張フォーマットです。従来のTar形式の制限(例えば、ファイル名の長さ、UID/GIDの範囲、タイムスタンプの精度など)を克服するために導入されました。PAXは、拡張ヘッダと呼ばれる特別なエントリを使用して、追加のメタデータや長いファイル名などをキーと値のペアの形式で格納します。GNU Tarの疎ファイルフォーマットの一部は、このPAX拡張ヘッダを利用して疎ファイル情報を表現します。

### GNU Sparse Formats (0.0, 0.1, 1.0)

GNU Tarは、疎ファイルをアーカイブに格納するためにいくつかの異なるフォーマットを使用します。これらは主に、疎ファイルの「穴」と「データブロック」のマップ情報をどのように表現するかの違いです。

-   **Old GNU Format**: これはPAX以前の古いGNU Tarの疎ファイルフォーマットです。疎マップ情報は、Tarヘッダの特定のオフセットに直接格納されるか、または追加の拡張ヘッダブロックに格納されます。
-   **GNU PAX Format 0.0**: PAX拡張ヘッダを使用しますが、疎マップ情報は`GNU.sparse.offset`と`GNU.sparse.numbytes`というキーで個別に格納されます。このフォーマットは、後続のバージョン0.1でより効率的な表現に移行しました。
-   **GNU PAX Format 0.1**: PAX拡張ヘッダを使用し、疎マップ情報は`GNU.sparse.map`という単一のキーに、カンマ区切りのオフセットとバイト数のペアのリストとして格納されます。これはバージョン0.0よりもコンパクトな表現です。
-   **GNU PAX Format 1.0**: このフォーマットでは、疎マップ情報がPAX拡張ヘッダではなく、ファイルデータ本体の直前に専用のブロックとして格納されます。これにより、非常に大きな疎マップを持つファイルでも効率的に処理できます。

これらの異なるフォーマットに対応することで、Goの`archive/tar`パッケージは、GNU Tarによって作成された様々な疎ファイルを網羅的にサポートできるようになります。

## 技術的詳細

このコミットは、`archive/tar`パッケージの`Reader`構造体と関連メソッドを大幅に拡張し、GNU sparseファイルの読み取りを可能にしています。

1.  **新しいタイプフラグの追加**: `src/pkg/archive/tar/common.go`に`TypeGNUSparse = 'S'`が追加されました。これは、Tarヘッダの`Typeflag`フィールドが`'S'`である場合に、そのエントリが古いGNU sparseファイルであることを示します。

2.  **`Reader`構造体の変更と新しいインターフェース/構造体**:
    *   `Reader`構造体は、現在のファイルエントリの読み取りを担当する`curr numBytesReader`フィールドを持つようになりました。
    *   `numBytesReader`インターフェースが導入され、`io.Reader`と`numBytes() int64`メソッド(残りのバイト数を返す)を定義します。
    *   `regFileReader`構造体は、通常のファイルデータを読み取るための`numBytesReader`の実装です。
    *   `sparseFileReader`構造体は、疎ファイルデータを読み取るための`numBytesReader`の実装です。これは内部に`regFileReader`を持ち、疎マップ(`[]sparseEntry`)と現在の読み取り位置(`pos`)、ファイルの合計サイズ(`tot`)を管理します。
    *   `sparseEntry`構造体は、疎マップ内の単一のエントリ(オフセットとバイト数)を表します。

3.  **PAX GNU Sparseヘッダのキーワード定数**: `reader.go`に、PAX拡張ヘッダ内でGNU sparseファイル情報を識別するための多数の定数(例: `paxGNUSparseNumBlocks`, `paxGNUSparseOffset`, `paxGNUSparseMap`など)が追加されました。

4.  **Old GNU Sparseヘッダのキーワード定数**: 古いGNU sparseフォーマットのヘッダ内のオフセットやサイズを示す定数も追加されました。

5.  **`Reader.Next()`メソッドの拡張**:
    *   `Next()`メソッドは、次のTarエントリを読み込む際に、まず通常のヘッダを解析します。
    *   その後、`checkForGNUSparsePAXHeaders`関数を呼び出して、現在のエントリがPAX形式のGNU sparseファイルであるかどうかをチェックします。もしそうであれば、その疎マップを読み込み、`tr.curr`を`sparseFileReader`のインスタンスに設定します。
    *   古いGNU sparseフォーマット(`TypeGNUSparse`)の場合も、`readHeader()`内で同様の処理が行われ、`tr.curr`が`sparseFileReader`に設定されます。

6.  **疎マップの解析ロジック**:
    *   `checkForGNUSparsePAXHeaders(hdr *Header, headers map[string]string) ([]sparseEntry, error)`: PAXヘッダを調べて、どのGNU sparseフォーマット(0.0, 0.1, 1.0)が適用されるかを識別し、対応する疎マップ読み取り関数を呼び出します。不明なフォーマットは無視されます。
    *   `readOldGNUSparseMap(header []byte) []sparseEntry`: 古いGNU sparseフォーマットの疎マップを、メインヘッダおよび必要に応じて拡張ヘッダから読み取ります。
    *   `readGNUSparseMap1x0(r io.Reader) ([]sparseEntry, error)`: GNU PAX sparseフォーマット1.0の疎マップを読み取ります。このフォーマットでは、疎マップがファイルデータ本体の直前に独自のブロックとして格納されているため、`io.Reader`から直接読み込みます。
    *   `readGNUSparseMap0x1(headers map[string]string) ([]sparseEntry, error)`: GNU PAX sparseフォーマット0.1の疎マップを読み取ります。このフォーマットでは、疎マップがPAXヘッダの`GNU.sparse.map`キーに格納されています。

7.  **`parsePAX()`でのGNU sparse format 0.0の変換**:
    *   `parsePAX()`関数は、PAX拡張ヘッダを解析する際に、GNU sparse format 0.0の`GNU.sparse.offset`と`GNU.sparse.numbytes`キーを特別に処理します。これらのキーの値を一時的な`bytes.Buffer`に書き込み、最終的に`GNU.sparse.map`キーとして結合することで、内部的にバージョン0.1の形式に変換します。これにより、後続の処理が簡素化されます。

8.  **`Reader.Read()`メソッドの変更**:
    *   `Reader.Read()`は、実際の読み取り処理を`tr.curr`(`numBytesReader`インターフェース)に委譲するようになりました。
    *   `regFileReader.Read()`は、通常のファイルデータを読み取ります。
    *   `sparseFileReader.Read()`は、疎ファイルの「穴」の部分をゼロで埋め、データブロックの部分を内部の`regFileReader`から読み取ることで、疎ファイルを展開された形式で提供します。これにより、アプリケーションは疎ファイルであることを意識せずに、通常のファイルとしてデータを読み取ることができます。

9.  **テストケースの追加**: `reader_test.go`には、様々なGNU sparseフォーマットのTarアーカイブを読み込み、その内容が期待通りであることを検証する多数のテストが追加されています。これには、エンドツーエンドのテスト、`sparseFileReader`の単体テスト、および各疎マップ読み取り関数のテストが含まれます。

これらの変更により、Goの`archive/tar`パッケージは、GNU tarによって作成された疎ファイルを透過的に処理できるようになり、Goアプリケーションがより広範なTarアーカイブを扱う際の互換性と堅牢性が向上しました。

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

### `src/pkg/archive/tar/common.go`

```diff
--- a/src/pkg/archive/tar/common.go
+++ b/src/pkg/archive/tar/common.go
@@ -38,6 +38,7 @@ const (
 	TypeXGlobalHeader = 'g'    // global extended header
 	TypeGNULongName   = 'L'    // Next file has a long name
 	TypeGNULongLink   = 'K'    // Next file symlinks to a file w/ a long name
+	TypeGNUSparse     = 'S'    // sparse file
 )
 
 // A Header represents a single header in a tar archive.

src/pkg/archive/tar/reader.go

--- a/src/pkg/archive/tar/reader.go
+++ b/src/pkg/archive/tar/reader.go
@@ -29,12 +29,57 @@ const maxNanoSecondIntSize = 9
 // The Next method advances to the next file in the archive (including the first),
 // and then it can be treated as an io.Reader to access the file's data.
 type Reader struct {
-	r   io.Reader
-	err error
-	nb  int64 // number of unread bytes for current file entry
-	pad int64 // amount of padding (ignored) after current file entry
+	r    io.Reader
+	err  error
+	pad  int64          // amount of padding (ignored) after current file entry
+	curr numBytesReader // reader for current file entry
 }
 
+// A numBytesReader is an io.Reader with a numBytes method, returning the number
+// of bytes remaining in the underlying encoded data.
+type numBytesReader interface {
+	io.Reader
+	numBytes() int64
+}
+
+// A regFileReader is a numBytesReader for reading file data from a tar archive.
+type regFileReader struct {
+	r  io.Reader // underlying reader
+	nb int64     // number of unread bytes for current file entry
+}
+
+// A sparseFileReader is a numBytesReader for reading sparse file data from a tar archive.
+type sparseFileReader struct {
+	rfr *regFileReader // reads the sparse-encoded file data
+	sp  []sparseEntry  // the sparse map for the file
+	pos int64          // keeps track of file position
+	tot int64          // total size of the file
+}
+
+// Keywords for GNU sparse files in a PAX extended header
+const (
+	paxGNUSparseNumBlocks = "GNU.sparse.numblocks"
+	paxGNUSparseOffset    = "GNU.sparse.offset"
+	paxGNUSparseNumBytes  = "GNU.sparse.numbytes"
+	paxGNUSparseMap       = "GNU.sparse.map"
+	paxGNUSparseName      = "GNU.sparse.name"
+	paxGNUSparseMajor     = "GNU.sparse.major"
+	paxGNUSparseMinor     = "GNU.sparse.minor"
+	paxGNUSparseSize      = "GNU.sparse.size"
+	paxGNUSparseRealSize  = "GNU.sparse.realsize"
+)
+
+// Keywords for old GNU sparse headers
+const (
+	oldGNUSparseMainHeaderOffset               = 386
+	oldGNUSparseMainHeaderIsExtendedOffset     = 482
+	oldGNUSparseMainHeaderNumEntries           = 4
+	oldGNUSparseExtendedHeaderIsExtendedOffset = 504
+	oldGNUSparseExtendedHeaderNumEntries       = 21
+	oldGNUSparseOffsetSize                     = 12
+	oldGNUSparseNumBytesSize                   = 12
+)
+
 // NewReader creates a new Reader reading from r.
 func NewReader(r io.Reader) *Reader { return &Reader{r: r} }
 
@@ -64,6 +109,18 @@ func (tr *Reader) Next() (*Header, error) {
 		tr.skipUnread()
 		hdr = tr.readHeader()
 		mergePAX(hdr, headers)
+
+		// Check for a PAX format sparse file
+		sp, err := tr.checkForGNUSparsePAXHeaders(hdr, headers)
+		if err != nil {
+			tr.err = err
+			return nil, err
+		}
+		if sp != nil {
+			// Current file is a PAX format GNU sparse file.
+			// Set the current file reader to a sparse file reader.
+			tr.curr = &sparseFileReader{rfr: tr.curr.(*regFileReader), sp: sp, tot: hdr.Size}
+		}
 		return hdr, nil
 	case TypeGNULongName:
 		// We have a GNU long name header. Its contents are the real file name.
@@ -87,6 +144,67 @@ func (tr *Reader) Next() (*Header, error) {
 	return hdr, tr.err
 }
 
+// checkForGNUSparsePAXHeaders checks the PAX headers for GNU sparse headers. If they are found, then
+// this function reads the sparse map and returns it. Unknown sparse formats are ignored, causing the file to
+// be treated as a regular file.
+func (tr *Reader) checkForGNUSparsePAXHeaders(hdr *Header, headers map[string]string) ([]sparseEntry, error) {
+	var sparseFormat string
+
+	// Check for sparse format indicators
+	major, majorOk := headers[paxGNUSparseMajor]
+	minor, minorOk := headers[paxGNUSparseMinor]
+	sparseName, sparseNameOk := headers[paxGNUSparseName]
+	_, sparseMapOk := headers[paxGNUSparseMap]
+	sparseSize, sparseSizeOk := headers[paxGNUSparseSize]
+	sparseRealSize, sparseRealSizeOk := headers[paxGNUSparseRealSize]
+
+	// Identify which, if any, sparse format applies from which PAX headers are set
+	if majorOk && minorOk {
+		sparseFormat = major + "." + minor
+	} else if sparseNameOk && sparseMapOk {
+		sparseFormat = "0.1"
+	} else if sparseSizeOk {
+		sparseFormat = "0.0"
+	} else {
+		// Not a PAX format GNU sparse file.
+		return nil, nil
+	}
+
+	// Check for unknown sparse format
+	if sparseFormat != "0.0" && sparseFormat != "0.1" && sparseFormat != "1.0" {
+		return nil, nil
+	}
+
+	// Update hdr from GNU sparse PAX headers
+	if sparseNameOk {
+		hdr.Name = sparseName
+	}
+	if sparseSizeOk {
+		realSize, err := strconv.ParseInt(sparseSize, 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		hdr.Size = realSize
+	} else if sparseRealSizeOk {
+		realSize, err := strconv.ParseInt(sparseRealSize, 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		hdr.Size = realSize
+	}
+
+	// Set up the sparse map, according to the particular sparse format in use
+	var sp []sparseEntry
+	var err error
+	switch sparseFormat {
+	case "0.0", "0.1":
+		sp, err = readGNUSparseMap0x1(headers)
+	case "1.0":
+		sp, err = readGNUSparseMap1x0(tr.curr)
+	}
+	return sp, err
+}
+
 // mergePAX merges well known headers according to PAX standard.
 // In general headers with the same name as those found
 // in the header struct overwrite those found in the header
@@ -194,6 +312,11 @@ func parsePAX(r io.Reader) (map[string]string, error) {
 	if err != nil {
 		return nil, err
 	}
+
+	// For GNU PAX sparse format 0.0 support.
+	// This function transforms the sparse format 0.0 headers into sparse format 0.1 headers.
+	var sparseMap bytes.Buffer
+
 	headers := make(map[string]string)
 	// Each record is constructed as
 	//     "%d %s=%s\n", length, keyword, value
@@ -221,7 +344,21 @@ func parsePAX(r io.Reader) (map[string]string, error) {
 		if err != nil {
 			return nil, ErrHeader
 		}
 		key, value := record[:eq], record[eq+1:]
-		headers[string(key)] = string(value)
+
+		keyStr := string(key)
+		if keyStr == paxGNUSparseOffset || keyStr == paxGNUSparseNumBytes {
+			// GNU sparse format 0.0 special key. Write to sparseMap instead of using the headers map.
+			sparseMap.Write(value)
+			sparseMap.Write([]byte{','})
+		} else {
+			// Normal key. Set the value in the headers map.
+			headers[keyStr] = string(value)
+		}
+	}
+	if sparseMap.Len() != 0 {
+		// Add sparse info to headers, chopping off the extra comma
+		sparseMap.Truncate(sparseMap.Len() - 1)
+		headers[paxGNUSparseMap] = sparseMap.String()
 	}
 	return headers, nil
 }
@@ -268,8 +405,8 @@ func (tr *Reader) octal(b []byte) int64 {
 
 // skipUnread skips any unread bytes in the existing file entry, as well as any alignment padding.
 func (tr *Reader) skipUnread() {
-	nr := tr.nb + tr.pad // number of bytes to skip
-	tr.nb, tr.pad = 0, 0
+	nr := tr.numBytes() + tr.pad // number of bytes to skip
+	tr.curr, tr.pad = nil, 0
 	if sr, ok := tr.r.(io.Seeker); ok {
 		if _, err := sr.Seek(nr, os.SEEK_CUR); err == nil {
 			return
@@ -373,30 +510,305 @@ func (tr *Reader) readHeader() *Header {
 
 	// Maximum value of hdr.Size is 64 GB (12 octal digits),
 	// so there's no risk of int64 overflowing.
-	tr.nb = int64(hdr.Size)
-	tr.pad = -tr.nb & (blockSize - 1) // blockSize is a power of two
+	nb := int64(hdr.Size)
+	tr.pad = -nb & (blockSize - 1) // blockSize is a power of two
+
+	// Set the current file reader.
+	tr.curr = &regFileReader{r: tr.r, nb: nb}
+
+	// Check for old GNU sparse format entry.
+	if hdr.Typeflag == TypeGNUSparse {
+		// Get the real size of the file.
+		hdr.Size = tr.octal(header[483:495])
+
+		// Read the sparse map.
+		sp := tr.readOldGNUSparseMap(header)
+		if tr.err != nil {
+			return nil
+		}
+		// Current file is a GNU sparse file. Update the current file reader.
+		tr.curr = &sparseFileReader{rfr: tr.curr.(*regFileReader), sp: sp, tot: hdr.Size}
+	}
 
 	return hdr
 }
 
+// A sparseEntry holds a single entry in a sparse file's sparse map.
+// A sparse entry indicates the offset and size in a sparse file of a
+// block of data.
+type sparseEntry struct {
+	offset   int64
+	numBytes int64
+}
+
+// readOldGNUSparseMap reads the sparse map as stored in the old GNU sparse format.
+// The sparse map is stored in the tar header if it's small enough. If it's larger than four entries,
+// then one or more extension headers are used to store the rest of the sparse map.
+func (tr *Reader) readOldGNUSparseMap(header []byte) []sparseEntry {
+	isExtended := header[oldGNUSparseMainHeaderIsExtendedOffset] != 0
+	spCap := oldGNUSparseMainHeaderNumEntries
+	if isExtended {
+		spCap += oldGNUSparseExtendedHeaderNumEntries
+	}
+	sp := make([]sparseEntry, 0, spCap)
+	s := slicer(header[oldGNUSparseMainHeaderOffset:])
+
+	// Read the four entries from the main tar header
+	for i := 0; i < oldGNUSparseMainHeaderNumEntries; i++ {
+		offset := tr.octal(s.next(oldGNUSparseOffsetSize))
+		numBytes := tr.octal(s.next(oldGNUSparseNumBytesSize))
+		if tr.err != nil {
+			tr.err = ErrHeader
+			return nil
+		}
+		if offset == 0 && numBytes == 0 {
+			break
+		}
+		sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+	}
+
+	for isExtended {
+		// There are more entries. Read an extension header and parse its entries.
+		sparseHeader := make([]byte, blockSize)
+		if _, tr.err = io.ReadFull(tr.r, sparseHeader); tr.err != nil {
+			return nil
+		}
+		isExtended = sparseHeader[oldGNUSparseExtendedHeaderIsExtendedOffset] != 0
+		s = slicer(sparseHeader)
+		for i := 0; i < oldGNUSparseExtendedHeaderNumEntries; i++ {
+			offset := tr.octal(s.next(oldGNUSparseOffsetSize))
+			numBytes := tr.octal(s.next(oldGNUSparseNumBytesSize))
+			if tr.err != nil {
+				tr.err = ErrHeader
+				return nil
+			}
+			if offset == 0 && numBytes == 0 {
+				break
+			}
+			sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+		}
+	}
+	return sp
+}
+
+// readGNUSparseMap1x0 reads the sparse map as stored in GNU's PAX sparse format version 1.0.
+// The sparse map is stored just before the file data and padded out to the nearest block boundary.
+func readGNUSparseMap1x0(r io.Reader) ([]sparseEntry, error) {
+	buf := make([]byte, 2*blockSize)
+	sparseHeader := buf[:blockSize]
+
+	// readDecimal is a helper function to read a decimal integer from the sparse map
+	// while making sure to read from the file in blocks of size blockSize
+	readDecimal := func() (int64, error) {
+		// Look for newline
+		nl := bytes.IndexByte(sparseHeader, '\n')
+		if nl == -1 {
+			if len(sparseHeader) >= blockSize {
+				// This is an error
+				return 0, ErrHeader
+			}
+			oldLen := len(sparseHeader)
+			newLen := oldLen + blockSize
+			if cap(sparseHeader) < newLen {
+				// There's more header, but we need to make room for the next block
+				copy(buf, sparseHeader)
+				sparseHeader = buf[:newLen]
+			} else {
+				// There's more header, and we can just reslice
+				sparseHeader = sparseHeader[:newLen]
+			}
+
+			// Now that sparseHeader is large enough, read next block
+			if _, err := io.ReadFull(r, sparseHeader[oldLen:newLen]); err != nil {
+				return 0, err
+			}
+
+			// Look for a newline in the new data
+			nl = bytes.IndexByte(sparseHeader[oldLen:newLen], '\n')
+			if nl == -1 {
+				// This is an error
+				return 0, ErrHeader
+			}
+			nl += oldLen // We want the position from the beginning
+		}
+		// Now that we've found a newline, read a number
+		n, err := strconv.ParseInt(string(sparseHeader[:nl]), 10, 0)
+		if err != nil {
+			return 0, ErrHeader
+		}
+
+		// Update sparseHeader to consume this number
+		sparseHeader = sparseHeader[nl+1:]
+		return n, nil
+	}
+
+	// Read the first block
+	if _, err := io.ReadFull(r, sparseHeader); err != nil {
+		return nil, err
+	}
+
+	// The first line contains the number of entries
+	numEntries, err := readDecimal()
+	if err != nil {
+		return nil, err
+	}
+
+	// Read all the entries
+	sp := make([]sparseEntry, 0, numEntries)
+	for i := int64(0); i < numEntries; i++ {
+		// Read the offset
+		offset, err := readDecimal()
+		if err != nil {
+			return nil, err
+		}
+		// Read numBytes
+		numBytes, err := readDecimal()
+		if err != nil {
+			return nil, err
+		}
+
+		sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+	}
+
+	return sp, nil
+}
+
+// readGNUSparseMap0x1 reads the sparse map as stored in GNU's PAX sparse format version 0.1.
+// The sparse map is stored in the PAX headers.
+func readGNUSparseMap0x1(headers map[string]string) ([]sparseEntry, error) {
+	// Get number of entries
+	numEntriesStr, ok := headers[paxGNUSparseNumBlocks]
+	if !ok {
+		return nil, ErrHeader
+	}
+	numEntries, err := strconv.ParseInt(numEntriesStr, 10, 0)
+	if err != nil {
+		return nil, ErrHeader
+	}
+
+	sparseMap := strings.Split(headers[paxGNUSparseMap], ",")
+
+	// There should be two numbers in sparseMap for each entry
+	if int64(len(sparseMap)) != 2*numEntries {
+		return nil, ErrHeader
+	}
+
+	// Loop through the entries in the sparse map
+	sp := make([]sparseEntry, 0, numEntries)
+	for i := int64(0); i < numEntries; i++ {
+		offset, err := strconv.ParseInt(sparseMap[2*i], 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		numBytes, err := strconv.ParseInt(sparseMap[2*i+1], 10, 0)
+		if err != nil {
+			return nil, ErrHeader
+		}
+		sp = append(sp, sparseEntry{offset: offset, numBytes: numBytes})
+	}
+
+	return sp, nil
+}
+
+// numBytes returns the number of bytes left to read in the current file's entry
+// in the tar archive, or 0 if there is no current file.
+func (tr *Reader) numBytes() int64 {
+	if tr.curr == nil {
+		// No current file, so no bytes
+		return 0
+	}
+	return tr.curr.numBytes()
+}
+
 // Read reads from the current entry in the tar archive.
 // It returns 0, io.EOF when it reaches the end of that entry,
 // until Next is called to advance to the next entry.
 func (tr *Reader) Read(b []byte) (n int, err error) {
-	if tr.nb == 0 {
+	n, err = tr.curr.Read(b)
+	if err != nil && err != io.EOF {
+		tr.err = err
+	}
+	return
+}
+
+func (rfr *regFileReader) Read(b []byte) (n int, err error) {
+	if rfr.nb == 0 {
 		// file consumed
 		return 0, io.EOF
 	}
-
-	if int64(len(b)) > tr.nb {
-		b = b[0:tr.nb]
-	}
-	n, err = tr.r.Read(b)
-	tr.nb -= int64(n)
-
-	if err == io.EOF && tr.nb > 0 {
+	if int64(len(b)) > rfr.nb {
+		b = b[0:rfr.nb]
+	}
+	n, err = rfr.r.Read(b)
+	rfr.nb -= int64(n)
+
+	if err == io.EOF && rfr.nb > 0 {
 		err = io.ErrUnexpectedEOF
 	}
-	tr.err = err
 	return
 }
+
+// numBytes returns the number of bytes left to read in the file's data in the tar archive.
+func (rfr *regFileReader) numBytes() int64 {
+	return rfr.nb
+}
+
+// readHole reads a sparse file hole ending at offset toOffset
+func (sfr *sparseFileReader) readHole(b []byte, toOffset int64) int {
+	n64 := toOffset - sfr.pos
+	if n64 > int64(len(b)) {
+		n64 = int64(len(b))
+	}
+	n := int(n64)
+	for i := 0; i < n; i++ {
+		b[i] = 0
+	}
+	sfr.pos += n64
+	return n
+}
+
+// Read reads the sparse file data in expanded form.
+func (sfr *sparseFileReader) Read(b []byte) (n int, err error) {
+	if len(sfr.sp) == 0 {
+		// No more data fragments to read from.
+		if sfr.pos < sfr.tot {
+			// We're in the last hole
+			n = sfr.readHole(b, sfr.tot)
+			return
+		}
+		// Otherwise, we're at the end of the file
+		return 0, io.EOF
+	}
+	if sfr.pos < sfr.sp[0].offset {
+		// We're in a hole
+		n = sfr.readHole(b, sfr.sp[0].offset)
+		return
+	}
+
+	// We're not in a hole, so we'll read from the next data fragment
+	posInFragment := sfr.pos - sfr.sp[0].offset
+	bytesLeft := sfr.sp[0].numBytes - posInFragment
+	if int64(len(b)) > bytesLeft {
+		b = b[0:bytesLeft]
+	}
+
+	n, err = sfr.rfr.Read(b)
+	sfr.pos += int64(n)
+
+	if int64(n) == bytesLeft {
+		// We're done with this fragment
+		sfr.sp = sfr.sp[1:]
+	}
+
+	if err == io.EOF && sfr.pos < sfr.tot {
+		// We reached the end of the last fragment's data, but there's a final hole
+		err = nil
+	}
+	return
+}
+
+// numBytes returns the number of bytes left to read in the sparse file's
+// sparse-encoded data in the tar archive.
+func (sfr *sparseFileReader) numBytes() int64 {
+	return sfr.rfr.nb
+}

src/pkg/archive/tar/reader_test.go

--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -9,6 +9,7 @@ import (
 	"crypto/md5"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"reflect"
 	"strings"
@@ -54,8 +55,92 @@ var gnuTarTest = &untarTest{
 	},
 }
 
+var sparseTarTest = &untarTest{
+	file: "testdata/sparse-formats.tar",
+	headers: []*Header{
+		{
+			Name:     "sparse-gnu",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392395740, 0),
+			Typeflag: 0x53,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "sparse-posix-0.0",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392342187, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "sparse-posix-0.1",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392340456, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "sparse-posix-1.0",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     200,
+			ModTime:  time.Unix(1392337404, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+		{
+			Name:     "end",
+			Mode:     420,
+			Uid:      1000,
+			Gid:      1000,
+			Size:     4,
+			ModTime:  time.Unix(1392398319, 0),
+			Typeflag: 0x30,
+			Linkname: "",
+			Uname:    "david",
+			Gname:    "david",
+			Devmajor: 0,
+			Devminor: 0,
+		},
+	},
+	cksums: []string{
+		"6f53234398c2449fe67c1812d993012f",
+		"6f53234398c2449fe67c1812d993012f",
+		"6f53234398c2449fe67c1812d993012f",
+		"6f53234398c2449fe67c1812d993012f",
+		"b0061974914468de549a2af8ced10316",
+	},
+}
+
 var untarTests = []*untarTest{
 	gnuTarTest,
+	sparseTarTest,
 	{
 		file: "testdata/star.tar",
 		headers: []*Header{
@@ -423,3 +508,220 @@ func TestMergePAX(t *testing.T) {
 	\tt.Errorf("incorrect merge: got %+v, want %+v", hdr, want)\n \t}\n }\n+\n+func TestSparseEndToEnd(t *testing.T) {\n+\ttest := sparseTarTest\n+\tf, err := os.Open(test.file)\n+\tif err != nil {\n+\t\tt.Fatalf("Unexpected error: %v", err)\n+\t}\n+\tdefer f.Close()\n+\n+\ttr := NewReader(f)\n+\n+\theaders := test.headers\n+\tcksums := test.cksums\n+\tnread := 0\n+\n+\t// loop over all files\n+\tfor ; ; nread++ {\n+\t\thdr, err := tr.Next()\n+\t\tif hdr == nil || err == io.EOF {\n+\t\t\tbreak\n+\t\t}\n+\n+\t\t// check the header\n+\t\tif !reflect.DeepEqual(*hdr, *headers[nread]) {\n+\t\t\tt.Errorf("Incorrect header:\\nhave %+v\\nwant %+v",\n+\t\t\t\t*hdr, headers[nread])\n+\t\t}\n+\n+\t\t// read and checksum the file data\n+\t\th := md5.New()\n+\t\t_, err = io.Copy(h, tr)\n+\t\tif err != nil {\n+\t\t\tt.Fatalf("Unexpected error: %v", err)\n+\t\t}\n+\n+\t\t// verify checksum\n+\t\thave := fmt.Sprintf("%x", h.Sum(nil))\n+\t\twant := cksums[nread]\n+\t\tif want != have {\n+\t\t\tt.Errorf("Bad checksum on file %s:\\nhave %+v\\nwant %+v", hdr.Name, have, want)\n+\t\t}\n+\t}\n+\tif nread != len(headers) {\n+\t\tt.Errorf("Didn't process all files\\nexpected: %d\\nprocessed %d\\n", len(headers), nread)\n+\t}\n+}\n+\n+type sparseFileReadTest struct {\n+\tsparseData []byte\n+\tsparseMap  []sparseEntry\n+\trealSize   int64\n+\texpected   []byte\n+}\n+\n+var sparseFileReadTests = []sparseFileReadTest{\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 0, numBytes: 2},\n+\t\t\t{offset: 5, numBytes: 3},\n+\t\t},\n+\t\trealSize: 8,\n+\t\texpected: []byte("ab\\x00\\x00\\x00cde"),\n+\t},\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 0, numBytes: 2},\n+\t\t\t{offset: 5, numBytes: 3},\n+\t\t},\n+\t\trealSize: 10,\n+\t\texpected: []byte("ab\\x00\\x00\\x00cde\\x00\\x00"),\n+\t},\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 1, numBytes: 3},\n+\t\t\t{offset: 6, numBytes: 2},\n+\t\t},\n+\t\trealSize: 8,\n+\t\texpected: []byte("\\x00abc\\x00\\x00de"),\n+\t},\n+\t{\n+\t\tsparseData: []byte("abcde"),\n+\t\tsparseMap: []sparseEntry{\n+\t\t\t{offset: 1, numBytes: 3},\n+\t\t\t{offset: 6, numBytes: 2},\n+\t\t},\n+\t\trealSize: 10,\n+\t\texpected: []byte("\\x00abc\\x00\\x00de\\x00\\x00"),\n+\t},\n+\t{\n+\t\tsparseData: []byte(""),\n+\t\tsparseMap:  nil,\n+\t\trealSize:   2,\n+\t\texpected:   []byte("\\x00\\x00"),\n+\t},\n+}\n+\n+func TestSparseFileReader(t *testing.T) {\n+\tfor i, test := range sparseFileReadTests {\n+\t\tr := bytes.NewReader(test.sparseData)\n+\t\tnb := int64(r.Len())\n+\t\tsfr := &sparseFileReader{\n+\t\t\trfr: &regFileReader{r: r, nb: nb},\n+\t\t\tsp:  test.sparseMap,\n+\t\t\tpos: 0,\n+\t\t\ttot: test.realSize,\n+\t\t}\n+\t\tif sfr.numBytes() != nb {\n+\t\t\tt.Errorf("test %d: Before reading, sfr.numBytes() = %d, want %d", i, sfr.numBytes, nb)\n+\t\t}\n+\t\tbuf, err := ioutil.ReadAll(sfr)\n+\t\tif err != nil {\n+\t\t\tt.Errorf("test %d: Unexpected error: %v", i, err)\n+\t\t}\n+\t\tif e := test.expected; !bytes.Equal(buf, e) {\n+\t\t\tt.Errorf("test %d: Contents = %v, want %v", i, buf, e)\n+\t\t}\n+\t\tif sfr.numBytes() != 0 {\n+\t\t\tt.Errorf("test %d: After draining the reader, numBytes() was nonzero", i)\n+\t\t}\n+\t}\n+}\n+\n+func TestSparseIncrementalRead(t *testing.T) {\n+\tsparseMap := []sparseEntry{{10, 2}}\n+\tsparseData := []byte("Go")\n+\texpected := "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00Go\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"\n+\n+\tr := bytes.NewReader(sparseData)\n+\tnb := int64(r.Len())\n+\tsfr := &sparseFileReader{\n+\t\trfr: &regFileReader{r: r, nb: nb},\n+\t\tsp:  sparseMap,\n+\t\tpos: 0,\n+\t\ttot: int64(len(expected)),\n+\t}\n+\n+\t// We'll read the data 6 bytes at a time, with a hole of size 10 at\n+\t// the beginning and one of size 8 at the end.\n+\tvar outputBuf bytes.Buffer\n+\tbuf := make([]byte, 6)\n+\tfor {\n+\t\tn, err := sfr.Read(buf)\n+\t\tif err == io.EOF {\n+\t\t\tbreak\n+\t\t}\n+\t\tif err != nil {\n+\t\t\tt.Errorf("Read: unexpected error %v\\n", err)\n+\t\t}\n+\t\tif n > 0 {\n+\t\t\t_, err := outputBuf.Write(buf[:n])\n+\t\t\tif err != nil {\n+\t\t\t\tt.Errorf("Write: unexpected error %v\\n", err)\n+\t\t\t}\n+\t\t}\n+\t}\n+\tgot := outputBuf.String()\n+\tif got != expected {\n+\t\tt.Errorf("Contents = %v, want %v", got, expected)\n+\t}\n+}\n+\n+func TestReadGNUSparseMap0x1(t *testing.T) {\n+\theaders := map[string]string{\n+\t\tpaxGNUSparseNumBlocks: "4",\n+\t\tpaxGNUSparseMap:       "0,5,10,5,20,5,30,5",\n+\t}\n+\texpected := []sparseEntry{\n+\t\t{offset: 0, numBytes: 5},\n+\t\t{offset: 10, numBytes: 5},\n+\t\t{offset: 20, numBytes: 5},\n+\t\t{offset: 30, numBytes: 5},\n+\t}\n+\n+\tsp, err := readGNUSparseMap0x1(headers)\n+\tif err != nil {\n+\t\tt.Errorf("Unexpected error: %v", err)\n+\t}\n+\tif !reflect.DeepEqual(sp, expected) {\n+\t\tt.Errorf("Incorrect sparse map: got %v, wanted %v", sp, expected)\n+\t}\n+}\n+\n+func TestReadGNUSparseMap1x0(t *testing.T) {\n+\t// This test uses lots of holes so the sparse header takes up more than two blocks\n+\tnumEntries := 100\n+\texpected := make([]sparseEntry, 0, numEntries)\n+\tsparseMap := new(bytes.Buffer)\n+\n+\tfmt.Fprintf(sparseMap, "%d\\n", numEntries)\n+\tfor i := 0; i < numEntries; i++ {\n+\t\toffset := int64(2048 * i)\n+\t\tnumBytes := int64(1024)\n+\t\texpected = append(expected, sparseEntry{offset: offset, numBytes: numBytes})\n+\t\tfmt.Fprintf(sparseMap, "%d\\n%d\\n", offset, numBytes)\n+\t}\n+\n+\t// Make the header the smallest multiple of blockSize that fits the sparseMap\n+\theaderBlocks := (sparseMap.Len() + blockSize - 1) / blockSize\n+\tbufLen := blockSize * headerBlocks\n+\tbuf := make([]byte, bufLen)\n+\tcopy(buf, sparseMap.Bytes())\n+\n+\t// Get an reader to read the sparse map\n+\tr := bytes.NewReader(buf)\n+\n+\t// Read the sparse map\n+\tsp, err := readGNUSparseMap1x0(r)\n+\tif err != nil {\n+\t\tt.Errorf("Unexpected error: %v", err)\n+\t}\n+\tif !reflect.DeepEqual(sp, expected) {\n+\t\tt.Errorf("Incorrect sparse map: got %v, wanted %v", sp, expected)\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/pkg/archive/tar/common.go`

-   **`TypeGNUSparse = 'S'`の追加**:
    この定数は、Tarアーカイブ内のエントリが古いGNU sparseファイルであることを示す新しいタイプフラグを定義します。Tarヘッダの`Typeflag`フィールドがこの値を持つ場合、`archive/tar`パッケージは、そのファイルが疎ファイルとして特別に処理される必要があることを認識します。

### `src/pkg/archive/tar/reader.go`

-   **`Reader`構造体の変更**:
    -   `r io.Reader`, `err error`, `pad int64`は既存のフィールドですが、`nb int64`(未読バイト数)が削除され、代わりに`curr numBytesReader`が追加されました。これは、ファイルデータの読み取りロジックが、通常のファイルと疎ファイルで異なる実装を持つ`numBytesReader`インターフェースに抽象化されたことを意味します。

-   **`numBytesReader`インターフェース**:
    -   `io.Reader`と`numBytes() int64`メソッドを持つ新しいインターフェースです。`numBytes()`は、現在のファイルエントリに残っている未読バイト数を返します。これにより、`Reader`は具体的なファイルタイプ(通常ファイルか疎ファイルか)を意識せずに、統一された方法で残りのバイト数を取得し、データを読み取ることができます。

-   **`regFileReader`構造体**:
    -   `numBytesReader`インターフェースの基本的な実装で、通常のファイルデータを読み取ります。`r`は基になる`io.Reader`(Tarアーカイブ全体)、`nb`は現在のファイルエントリの未読バイト数です。

-   **`sparseFileReader`構造体**:
    -   `numBytesReader`インターフェースの疎ファイル特化実装です。
    -   `rfr *regFileReader`: 疎ファイル内の実際のデータブロックを読み取るために使用されます。
    -   `sp []sparseEntry`: 疎ファイルのオフセットとバイト数のマップ(疎マップ)を保持します。
    -   `pos int64`: 疎ファイル内の現在の読み取り位置を追跡します。
    -   `tot int64`: 疎ファイルの論理的な合計サイズ(穴を含む)を保持します。
    -   この構造体は、`Read`メソッドが呼び出されたときに、疎マップに基づいて「穴」の部分をゼロで埋め、データブロックの部分を内部の`regFileReader`から読み取ることで、疎ファイルを展開された形式で提供します。

-   **PAX GNU SparseヘッダおよびOld GNU Sparseヘッダのキーワード定数**:
    -   これらの定数は、PAX拡張ヘッダや古いGNU Tarヘッダ内で疎ファイル関連の情報を識別するために使用されます。例えば、`paxGNUSparseMap`はPAX 0.1フォーマットで疎マップが格納されるキーを示し、`oldGNUSparseMainHeaderOffset`は古いGNUフォーマットで疎マップが始まるオフセットを示します。

-   **`Reader.Next()`メソッドの変更**:
    -   `Next()`メソッドは、Tarヘッダを読み込んだ後、まず`checkForGNUSparsePAXHeaders`を呼び出してPAX形式の疎ファイルを検出します。検出された場合、`tr.curr`を`sparseFileReader`のインスタンスに設定し、疎ファイルとして処理を開始します。
    -   古いGNU sparseフォーマット(`TypeGNUSparse`)の場合も、`readHeader()`内で同様の処理が行われ、`tr.curr`が`sparseFileReader`に設定されます。これにより、`Reader`は次の`Read`呼び出しから、疎ファイルの特性を考慮した読み取りを行うことができます。

-   **`checkForGNUSparsePAXHeaders()`関数**:
    -   この関数は、PAXヘッダ内の特定のキー(`GNU.sparse.major`, `GNU.sparse.minor`, `GNU.sparse.map`など)の存在をチェックすることで、どのGNU PAX sparseフォーマット(0.0, 0.1, 1.0)が使用されているかを識別します。
    -   識別されたフォーマットに基づいて、適切な疎マップ読み取り関数(`readGNUSparseMap0x1`または`readGNUSparseMap1x0`)を呼び出し、疎マップを解析して返します。
    -   これにより、`archive/tar`パッケージは、異なるPAX sparseフォーマットのTarアーカイブを透過的に処理できます。

-   **`parsePAX()`でのGNU sparse format 0.0の変換ロジック**:
    -   `parsePAX()`関数は、PAX拡張ヘッダを解析する際に、`paxGNUSparseOffset`と`paxGNUSparseNumBytes`というキーを特別に扱います。これらはGNU sparse format 0.0で使用されるキーで、オフセットとバイト数が個別のエントリとして格納されます。
    -   このコミットでは、これらの個別のエントリを読み取り、カンマで区切られた文字列として`sparseMap bytes.Buffer`に結合します。最終的に、この結合された文字列が`paxGNUSparseMap`キーの値として`headers`マップに追加されます。
    -   この変換により、内部的にはGNU sparse format 0.0のデータがバージョン0.1の形式に統一され、後続の`readGNUSparseMap0x1`関数で一貫して処理できるようになります。

-   **`readOldGNUSparseMap()`関数**:
    -   古いGNU sparseフォーマットの疎マップを読み取るための関数です。このフォーマットでは、疎マップがTarヘッダの固定オフセットに格納されるか、または追加の拡張ヘッダブロックに格納されます。
    -   関数は、まずメインヘッダから疎マップエントリを読み取り、必要に応じて追加の拡張ヘッダブロックを読み込んで残りのエントリを解析します。

-   **`readGNUSparseMap1x0()`関数**:
    -   GNU PAX sparseフォーマット1.0の疎マップを読み取るための関数です。このフォーマットでは、疎マップがファイルデータ本体の直前に、改行区切りの数値(エントリ数、オフセット、バイト数)のリストとして格納されます。
    -   関数は、基になる`io.Reader`からブロック単位でデータを読み込み、改行を区切りとして数値を解析し、疎マップを構築します。

-   **`readGNUSparseMap0x1()`関数**:
    -   GNU PAX sparseフォーマット0.1の疎マップを読み取るための関数です。このフォーマットでは、疎マップがPAXヘッダの`GNU.sparse.map`キーに、カンマ区切りのオフセットとバイト数のペアのリストとして格納されています。
    -   関数は、`headers`マップから`paxGNUSparseMap`の値を取得し、カンマで分割して数値を解析し、疎マップを構築します。

-   **`Reader.Read()`、`regFileReader.Read()`、`sparseFileReader.Read()`メソッド**:
    -   `Reader.Read()`は、実際の読み取り処理を`tr.curr`(`numBytesReader`インターフェース)に委譲します。
    -   `regFileReader.Read()`は、通常のファイルデータを基になる`io.Reader`から読み込み、未読バイト数を更新します。
    -   `sparseFileReader.Read()`は、疎ファイルの読み取りの中核をなす部分です。
        -   現在の読み取り位置`sfr.pos`と疎マップ`sfr.sp`を比較し、次に読み取るべき領域が「穴」であるか「データブロック」であるかを判断します。
        -   「穴」の場合、要求されたバイト数または次のデータブロックまでのバイト数分、出力バッファをゼロで埋めます。
        -   「データブロック」の場合、内部の`regFileReader`(`sfr.rfr`)から実際のデータを読み込みます。
        -   データブロックの読み取りが完了すると、疎マップからそのエントリを削除し、次のデータブロックまたは穴の処理に備えます。
        -   これにより、疎ファイルはアプリケーションに対して、あたかもすべてのゼロバイトが実際に存在するかのように透過的に提供されます。

### `src/pkg/archive/tar/reader_test.go`

-   **`sparseTarTest`の追加**:
    -   様々なGNU sparseフォーマット(Old GNU, PAX 0.0, 0.1, 1.0)を含む`sparse-formats.tar`ファイルに対するテストデータセットが追加されました。これには、各エントリの期待されるヘッダ情報とMD5チェックサムが含まれます。

-   **`TestSparseEndToEnd()`関数**:
    -   `sparseTarTest`データセットを使用して、`archive/tar`パッケージが`sparse-formats.tar`ファイルを正しく読み取り、各エントリのヘッダとデータが期待通りであることを検証するエンドツーエンドテストです。ファイルデータのMD5チェックサムを計算し、期待値と比較することで、疎ファイルが正しく展開されていることを確認します。

-   **`sparseFileReadTest`構造体と`sparseFileReadTests`**:
    -   `sparseFileReader`の単体テストのためのデータ構造とテストケースの配列です。疎データ、疎マップ、実際のファイルサイズ、そして期待される展開後のバイト列を定義します。これにより、様々な疎マップのパターンと読み取りシナリオがカバーされます。

-   **`TestSparseFileReader()`関数**:
    -   `sparseFileReadTests`を使用して、`sparseFileReader`の`Read`メソッドが正しく動作するかを検証する単体テストです。疎ファイルが正しく展開され、穴がゼロで埋められていることを確認します。

-   **`TestSparseIncrementalRead()`関数**:
    -   `sparseFileReader`が、小さなバッファで段階的に読み取られた場合でも正しく動作するかを検証するテストです。これにより、`Read`メソッドが部分的な読み取り要求にも対応できることが保証されます。

-   **`TestReadGNUSparseMap0x1()`および`TestReadGNUSparseMap1x0()`関数**:
    -   それぞれGNU PAX sparseフォーマット0.1と1.0の疎マップ読み取り関数(`readGNUSparseMap0x1`と`readGNUSparseMap1x0`)の単体テストです。これらのテストは、特定の入力ヘッダまたはリーダーから、期待される疎マップが正しく解析されることを確認します。

これらのテストは、GNU sparseファイルの複雑なフォーマットと読み取りロジックが、Goの`archive/tar`パッケージによって正確に実装されていることを保証します。

## 関連リンク

-   Go Issue #3864: [archive/tar: add support for GNU sparse files](https://github.com/golang/go/issues/3864)
-   Gerrit Code Review: [https://golang.org/cl/64740043](https://golang.org/cl/64740043)

## 参考にした情報源リンク

-   GNU Tar Manual: [https://www.gnu.org/software/tar/manual/](https://www.gnu.org/software/tar/manual/) (特に"Sparse Files"のセクション)
-   POSIX.1-2001 (PAX): [https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html](https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html)
-   Sparse file - Wikipedia: [https://en.wikipedia.org/wiki/Sparse_file](https://en.wikipedia.org/wiki/Sparse_file)
-   Tar (file format) - Wikipedia: [https://en.wikipedia.org/wiki/Tar_(file_format)](https://en.wikipedia.org/wiki/Tar_(file_format))