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

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

このコミットは、Go言語の archive/tar パッケージにおける、TARアーカイブのPAX拡張ヘッダーのサポート改善と、長いファイル名やリンク名の処理に関するバグ修正を目的としています。特に、リンク名が100文字を超える場合の不具合と、PAXヘッダーが不完全にしか書き込まれていなかった問題に対処しています。

コミット

commit a07c95a53c906e2d30762d76cd3e36c93d2c83f4
Author: Marco Hennings <marco.hennings@freiheit.com>
Date:   Mon Aug 19 10:45:44 2013 +1000

    archive/tar: Fix support for long links and improve PAX support.
    
    The tar/archive code from golang has a problem with linknames with length >
    100. A pax header is added but the original header still written with a too
    long field length.
    
    As it is clear that pax support is incomplete I have added missing
    implementation parts.
    
    This commit contains code from the golang project in the folder tar/archiv.
    
    The following pax header records are now automatically written:
    
    - gname)
    - linkpath
    - path
    - uname
    
    The following fields can be written with PAX, but the default is to use the
    star binary extension.
    
    - gid  (value > 2097151)
    - size (value > 8589934591)
    - uid (value > 2097151)
    
    The string fields are written when the value is longer as the field or if the
    string contains a char that is not encodable as 7-bit ASCII value.
    
    The change was tested against a current ubuntu-cloud image tarball comparing
    the compressed result.
    
    + added some automated tests for the new functionality.
    
    Fixes #6056.
    
    R=dsymonds
    CC=golang-dev
    https://golang.org/cl/12561043

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

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

元コミット内容

archive/tar: Fix support for long links and improve PAX support.

このコミットは、Go言語の archive/tar パッケージにおいて、長いリンク名(100文字以上)のサポートを修正し、PAX(Portable Archive eXchange)ヘッダーのサポートを改善するものです。既存の実装では、長いリンク名に対してPAXヘッダーが追加されるにもかかわらず、元のヘッダーフィールドが長すぎるまま書き込まれる問題がありました。このコミットでは、PAXサポートの不完全な部分を補完し、gname, linkpath, path, uname といったPAXヘッダーレコードが自動的に書き込まれるように変更されています。また、gid, size, uid のような数値フィールドについても、値が大きすぎる場合にPAXヘッダーを使用するオプションが追加されています。さらに、文字列フィールドは、フィールド長を超える場合や7ビットASCIIでエンコードできない文字を含む場合にPAXヘッダーとして書き込まれるようになっています。変更はUbuntuクラウドイメージのtarballでテストされ、新しい機能のための自動テストも追加されています。

変更の背景

TARアーカイブフォーマットは、UNIX系システムで広く使われているファイルアーカイブ形式です。しかし、オリジナルのTARフォーマットには、ファイル名やリンク名、ユーザー/グループ名、ファイルサイズなどのフィールド長に制限がありました。特に、ファイル名やリンク名が100文字を超える場合、標準的なTARヘッダーでは表現できませんでした。

この問題を解決するために、POSIX.1-2001標準でPAX(Portable Archive eXchange)フォーマットが導入されました。PAXは、TARフォーマットの拡張であり、長いファイル名や大きなファイルサイズ、非ASCII文字を含むファイル名などを扱うためのメカニズムを提供します。PAXは、通常のTARヘッダーの前に「拡張ヘッダー」として追加のメタデータを格納することで、これらの制限を克服します。

Go言語の archive/tar パッケージは、TARアーカイブの読み書きをサポートしていますが、このコミット以前はPAXサポートが不完全でした。具体的には、リンク名が100文字を超えると、PAXヘッダーが追加されるにもかかわらず、元のTARヘッダーのリンク名フィールドが切り詰められたり、不正な値が書き込まれたりする問題がありました。これにより、作成されたTARアーカイブが他のTARツールで正しく解釈されない可能性がありました。

このコミットは、このような既存のバグを修正し、archive/tar パッケージのPAXサポートをより堅牢で包括的なものにすることを目的としています。これにより、Goで作成されたTARアーカイブが、より広範なファイルシステム特性(長いパス、大きなファイル、非ASCII文字など)を持つファイルを正確に表現できるようになります。

前提知識の解説

TARアーカイブフォーマット

TAR(Tape ARchive)は、複数のファイルを一つのアーカイブファイルにまとめるためのフォーマットです。元々は磁気テープにデータを保存するために設計されましたが、現在ではファイルシステム上のアーカイブとしても広く利用されています。TARアーカイブは、一連の「ヘッダーブロック」と「データブロック」で構成されます。各ファイルやディレクトリは、そのメタデータ(ファイル名、サイズ、パーミッション、所有者など)を含むヘッダーブロックと、実際のファイルデータを含むデータブロックによって表現されます。

USTAR (POSIX.1-1988)

USTAR(Unix Standard TAR)は、POSIX.1-1988で標準化されたTARフォーマットの拡張です。USTARは、元のTARフォーマットの制限(例えば、ファイル名が100文字まで)を緩和するために、追加のフィールドをヘッダーに導入しました。これにより、ファイル名が100文字を超える場合でも、prefix フィールドと name フィールドを組み合わせて長いパスを表現できるようになりました。しかし、prefixname を合わせても255文字程度の制限があり、また、特定のフィールド(例えば、リンク名)には依然として100文字の制限がありました。

PAX (Portable Archive eXchange) (POSIX.1-2001)

PAXは、POSIX.1-2001で標準化されたTARフォーマットのさらなる拡張です。PAXは、USTARの制限をさらに克服するために設計されました。PAXの主要な特徴は、「拡張ヘッダー(Extended Header)」の導入です。拡張ヘッダーは、通常のTARヘッダーの前に特別なタイプのファイルとして追加され、キーと値のペアの形式で追加のメタデータを格納します。これにより、以下のことが可能になります。

  • 長いパス名とリンク名: 任意の長さのパス名やリンク名を pathlinkpath キーで指定できます。
  • 大きなファイルサイズ: 8GBを超えるファイルサイズを size キーで指定できます。
  • 大きなUID/GID: ユーザーIDやグループIDが2097151を超える場合でも uidgid キーで指定できます。
  • 非ASCII文字: ファイル名やユーザー/グループ名にUTF-8エンコードされた非ASCII文字を含めることができます。
  • 高精度なタイムスタンプ: atime, mtime, ctime などのタイムスタンプを秒以下の精度で記録できます。

PAX拡張ヘッダーは、TypeXHeader または TypeGNULongLink などの特別なタイプフラグを持つTARエントリとしてアーカイブ内に存在します。これらのヘッダーは、後続のデータエントリに適用されるメタデータを定義します。

GNU Tar拡張

GNU Tarは、TARフォーマットの独自の拡張をいくつか導入しています。その中には、長いファイル名やリンク名を扱うためのメカニズムも含まれます。GNU Tarは、PAXとは異なる方法で長い名前を処理することがあり、例えば、././@LongLink././@LongFileName といった特別なエントリを使用して長い名前を格納します。このコミットでは、PAXヘッダーを優先しつつも、必要に応じてGNU Tarのバイナリ拡張(特に大きな数値フィールドの場合)も考慮に入れています。

Go言語の archive/tar パッケージ

Go言語の標準ライブラリには、archive/tar パッケージが含まれており、TARアーカイブの作成と読み取りのためのAPIを提供します。このパッケージは、TARフォーマットの様々なバリエーション(V7、USTAR、PAXなど)をサポートすることを目指しています。このコミットは、特にPAXフォーマットの書き込み機能の堅牢性を向上させるものです。

技術的詳細

このコミットの主要な変更点は、archive/tar パッケージがTARヘッダーを書き込む際に、PAX拡張ヘッダーをより適切に利用するように改善されたことです。

  1. PAXキーワードの定義: src/pkg/archive/tar/common.go に、PAX拡張ヘッダーで使用される新しいキーワード定数(paxAtime, paxCharset, paxComment, paxCtime, paxGid, paxGname, paxLinkpath, paxMtime, paxPath, paxSize, paxUid, paxUname)が追加されました。これにより、コード内でこれらのキーワードを安全かつ一貫して参照できるようになります。

  2. ASCIIチェックと変換: isASCIItoASCII 関数が src/pkg/archive/tar/common.go に追加されました。

    • isASCII(s string) bool: 文字列 s が7ビットASCII文字のみで構成されているかをチェックします。
    • toASCII(s string) string: 文字列 s から7ビットASCII文字のみを抽出し、それ以外の文字を破棄します。これは、TARヘッダーの特定のフィールドがASCII文字のみを期待する場合に、非ASCII文字を含む文字列を処理するために使用されます。
  3. Reader のPAXマージの改善: src/pkg/archive/tar/reader.gomergePAX 関数が、新しいPAXキーワード定数を使用するように更新されました。これにより、PAX拡張ヘッダーから読み取られた値が、対応する Header フィールドに正しくマッピングされるようになります。

  4. Writer のPAX書き込みロジックの強化: src/pkg/archive/tar/writer.goWriter 構造体と関連メソッドが大幅に修正されました。

    • preferPax フィールドの追加: Writer 構造体に preferPax という新しいブール型フィールドが追加されました。これは、数値フィールドが大きすぎる場合に、GNU Tarのバイナリ拡張ではなくPAXヘッダーを優先するかどうかを制御します。
    • cString 関数の変更: cString 関数(文字列を固定長のバイト配列に書き込む)が変更され、allowPax, paxKeyword, paxHeaders の引数を受け取るようになりました。これにより、文字列がフィールド長を超える場合や非ASCII文字を含む場合に、自動的にPAXヘッダーレコードとして追加されるようになりました。
    • numeric 関数の変更: numeric 関数(数値を固定長のバイト配列に書き込む)も同様に、allowPax, paxKeyword, paxHeaders の引数を受け取るようになりました。これにより、数値がフィールドの最大値を超える場合に、PAXヘッダーレコードとして追加されるようになりました。特に、preferPaxtrue の場合、バイナリ拡張よりもPAXが優先されます。
    • WriteHeaderwriteHeader の分離: WriteHeader メソッドが writeHeader という内部メソッドを呼び出すように変更されました。writeHeaderallowPax 引数を受け取り、PAXヘッダーの書き込みを抑制できるため、PAX拡張ヘッダー自体を書き込む際に無限ループに陥るのを防ぎます。
    • PAXヘッダーの自動生成: writeHeader メソッド内で、paxHeaders というマップが導入され、ファイル名、リンク名、ユーザー名、グループ名、UID、GID、サイズなどのフィールドが、標準のTARヘッダーフィールドの制限を超える場合や非ASCII文字を含む場合に、自動的にこのマップにPAXレコードとして追加されるようになりました。
    • USTARロングネームの優先: preferPaxfalse の場合、かつファイル名のみが長すぎる場合に、PAXヘッダーではなくUSTARのロングネーム拡張(prefix フィールド)を使用するロジックが追加されました。これは、PAXヘッダーが不要な場合に、より互換性の高いUSTAR形式を優先するためのものです。
    • writePAXHeader の変更: writePAXHeader 関数が paxHeaders マップを受け取るようになり、このマップに含まれるすべてのPAXレコードを拡張ヘッダーとして書き込むようになりました。また、PAXヘッダーのファイル名(PaxHeaders.PID/filename)を生成する際に、toASCII を使用してASCII文字のみを保証し、100文字に切り詰める処理が追加されました。
  5. テストの追加: src/pkg/archive/tar/writer_test.go に、長いシンボリックリンク名と非ASCII文字を含むファイル名、グループ名、ユーザー名をテストするための新しいテストケース(TestPaxSymlink, TestPaxNonAscii)が追加されました。これにより、PAXサポートの改善が正しく機能していることが検証されます。

これらの変更により、archive/tar パッケージは、より堅牢なPAXサポートを提供し、長いファイル名、リンク名、大きな数値、非ASCII文字を含むTARアーカイブをより正確に作成できるようになりました。

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

src/pkg/archive/tar/common.go

 // Keywords for the PAX Extended Header
 const (
 	paxAtime    = "atime"
 	paxCharset  = "charset"
 	paxComment  = "comment"
 	paxCtime    = "ctime" // please note that ctime is not a valid pax header.
 	paxGid      = "gid"
 	paxGname    = "gname"
 	paxLinkpath = "linkpath"
 	paxMtime    = "mtime"
 	paxPath     = "path"
 	paxSize     = "size"
 	paxUid      = "uid"
 	paxUname    = "uname"
 	paxNone     = ""
 )

 func isASCII(s string) bool {
 	for _, c := range s {
 		if c >= 0x80 {
 			return false
 		}
 	}
 	return true
 }

 func toASCII(s string) string {
 	if isASCII(s) {
 		return s
 	}
 	var buf bytes.Buffer
 	for _, c := range s {
 		if c < 0x80 {
 			buf.WriteByte(byte(c))
 		}
 	}
 	return buf.String()
 }

src/pkg/archive/tar/reader.go

 func mergePAX(hdr *Header, headers map[string]string) error {
 	for k, v := range headers {
 		switch k {
-		case "path":
+		case paxPath:
 			hdr.Name = v
-		case "linkpath":
+		case paxLinkpath:
 			hdr.Linkname = v
-		case "gname":
+		case paxGname:
 			hdr.Gname = v
-		case "uname":
+		case paxUname:
 			hdr.Uname = v
-		case "uid":
+		case paxUid:
 			uid, err := strconv.ParseInt(v, 10, 0)
 			if err != nil {
 				return err
 			}
 			hdr.Uid = int(uid)
-		case "gid":
+		case paxGid:
 			gid, err := strconv.ParseInt(v, 10, 0)
 			if err != nil {
 				return err
 			}
 			hdr.Gid = int(gid)
-		case "atime":
+		case paxAtime:
 			t, err := parsePAXTime(v)
 			if err != nil {
 				return err
 			}
 			hdr.AccessTime = t
-		case "mtime":
+		case paxMtime:
 			t, err := parsePAXTime(v)
 			if err != nil {
 				return err
 			}
 			hdr.ModTime = t
-		case "ctime":
+		case paxCtime:
 			t, err := parsePAXTime(v)
 			if err != nil {
 				return err
 			}
 			hdr.ChangeTime = t
-		case "size":
+		case paxSize:
 			size, err := strconv.ParseInt(v, 10, 0)
 			if err != nil {
 				return err

src/pkg/archive/tar/writer.go

 type Writer struct {
 	w          io.Writer
 	err        error
 	nb         int64 // number of unwritten bytes for current file entry
 	pad        int64 // amount of padding to write after current file entry
 	closed     bool
 	usedBinary bool // whether the binary numeric field extension was used
+	preferPax  bool // use pax header instead of binary numeric header
 }

 // Write s into b, terminating it with a NUL if there is room.
-// If the value is too long for the field and allowPax is true add a paxheader record instead
-func (tw *Writer) cString(b []byte, s string, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
+func (tw *Writer) cString(b []byte, s string, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
+	needsPaxHeader := allowPax && len(s) > len(b) || !isASCII(s)
+	if needsPaxHeader {
+		paxHeaders[paxKeyword] = s
+		return
+	}
 	if len(s) > len(b) {
 		if tw.err == nil {
 			tw.err = ErrFieldTooLong
 		}
 		return
 	}
-	copy(b, s)
-	if len(s) < len(b) {
-		b[len(s)] = 0
+	ascii := toASCII(s)
+	copy(b, ascii)
+	if len(ascii) < len(b) {
+		b[len(ascii)] = 0
 	}
 }

 // Write x into b, either as octal or as binary (GNUtar/star extension).
-// If the value is too long for the field and writingPax is enabled both for the field and the add a paxheader record instead
-func (tw *Writer) numeric(b []byte, x int64, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
+func (tw *Writer) numeric(b []byte, x int64, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
 	// Try octal first.
 	s := strconv.FormatInt(x, 8)
 	if len(s) < len(b) {
 		tw.octal(b, x)
 		return
 	}
+
+	// If it is too long for octal, and pax is preferred, use a pax header
+	if allowPax && tw.preferPax {
+		tw.octal(b, 0)
+		s := strconv.FormatInt(x, 10)
+		paxHeaders[paxKeyword] = s
+		return
+	}
+
 	// Too big: use binary (big-endian).
 	tw.usedBinary = true
 	for i := len(b) - 1; x > 0 && i >= 0; i-- {
@@ -115,6 +134,15 @@ var (
 // WriteHeader calls Flush if it is not the first header.
 // Calling after a Close will return ErrWriteAfterClose.
 func (tw *Writer) WriteHeader(hdr *Header) error {
+	return tw.writeHeader(hdr, true)
+}
+
+// WriteHeader writes hdr and prepares to accept the file's contents.
+// WriteHeader calls Flush if it is not the first header.
+// Calling after a Close will return ErrWriteAfterClose.
+// As this method is called internally by writePax header to allow it to
+// suppress writing the pax header.
+func (tw *Writer) writeHeader(hdr *Header, allowPax bool) error {
 	if tw.closed {
 		return ErrWriteAfterClose
 	}
@@ -124,31 +152,21 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
 	if tw.err != nil {
 		return tw.err
 	}
-	// Decide whether or not to use PAX extensions
+
+	// a map to hold pax header records, if any are needed
+	paxHeaders := make(map[string]string)
+
 	// TODO(shanemhansen): we might want to use PAX headers for
 	// subsecond time resolution, but for now let's just capture
-	// the long name/long symlink use case.
-	suffix := hdr.Name
-	prefix := ""
-	if len(hdr.Name) > fileNameSize || len(hdr.Linkname) > fileNameSize {
-		var err error
-		prefix, suffix, err = tw.splitUSTARLongName(hdr.Name)
-		// Either we were unable to pack the long name into ustar format
-		// or the link name is too long; use PAX headers.
-		if err == errNameTooLong || len(hdr.Linkname) > fileNameSize {
-			if err := tw.writePAXHeader(hdr); err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
-	}
-	tw.nb = int64(hdr.Size)
-	tw.pad = -tw.nb & (blockSize - 1) // blockSize is a power of two
+	// too long fields or non ascii characters
 
 	header := make([]byte, blockSize)
 	s := slicer(header)
-	tw.cString(s.next(fileNameSize), suffix)
+
+	// keep a reference to the filename to allow to overwrite it later if we detect that we can use ustar longnames instead of pax
+	pathHeaderBytes := s.next(fileNameSize)
+
+	tw.cString(pathHeaderBytes, hdr.Name, true, paxPath, paxHeaders)
 
 	// Handle out of range ModTime carefully.
 	var modTime int64
@@ -156,27 +164,55 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
 		modTime = hdr.ModTime.Unix()
 	}
 
-	tw.octal(s.next(8), hdr.Mode)          // 100:108
-	tw.numeric(s.next(8), int64(hdr.Uid))  // 108:116
-	tw.numeric(s.next(8), int64(hdr.Gid))  // 116:124
-	tw.numeric(s.next(12), hdr.Size)       // 124:136
-	tw.numeric(s.next(12), modTime)        // 136:148
-	s.next(8)                              // chksum (148:156)
-	s.next(1)[0] = hdr.Typeflag            // 156:157
-	tw.cString(s.next(100), hdr.Linkname)  // linkname (157:257)
-	copy(s.next(8), []byte("ustar\x0000")) // 257:265
-	tw.cString(s.next(32), hdr.Uname)      // 265:297
-	tw.cString(s.next(32), hdr.Gname)      // 297:329
-	tw.numeric(s.next(8), hdr.Devmajor)    // 329:337
-	tw.numeric(s.next(8), hdr.Devminor)    // 337:345
-	tw.cString(s.next(155), prefix)        // 345:500
+	tw.octal(s.next(8), hdr.Mode)                                   // 100:108
+	tw.numeric(s.next(8), int64(hdr.Uid), true, paxUid, paxHeaders) // 108:116
+	tw.numeric(s.next(8), int64(hdr.Gid), true, paxGid, paxHeaders) // 116:124
+	tw.numeric(s.next(12), hdr.Size, true, paxSize, paxHeaders)     // 124:136
+	tw.numeric(s.next(12), modTime, false, paxNone, nil)            // 136:148 --- consider using pax for finer granularity
+	s.next(8)                                                       // chksum (148:156)
+	s.next(1)[0] = hdr.Typeflag                                     // 156:157
+
+	tw.cString(s.next(100), hdr.Linkname, true, paxLinkpath, paxHeaders)
+
+	copy(s.next(8), []byte("ustar\x0000"))                        // 257:265
+	tw.cString(s.next(32), hdr.Uname, true, paxUname, paxHeaders) // 265:297
+	tw.cString(s.next(32), hdr.Gname, true, paxGname, paxHeaders) // 297:329
+	tw.numeric(s.next(8), hdr.Devmajor, false, paxNone, nil)      // 329:337
+	tw.numeric(s.next(8), hdr.Devminor, false, paxNone, nil)      // 337:345
+
+	// keep a reference to the prefix to allow to overwrite it later if we detect that we can use ustar longnames instead of pax
+	prefixHeaderBytes := s.next(155)
+	tw.cString(prefixHeaderBytes, "", false, paxNone, nil) // 345:500  prefix
+
 	// Use the GNU magic instead of POSIX magic if we used any GNU extensions.
 	if tw.usedBinary {
 		copy(header[257:265], []byte("ustar  \x00"))
 	}
-	// Use the ustar magic if we used ustar long names.
-	if len(prefix) > 0 {
-		copy(header[257:265], []byte("ustar\000"))
+
+	_, paxPathUsed := paxHeaders[paxPath]
+	// try to use a ustar header when only the name is too long
+	if !tw.preferPax && len(paxHeaders) == 1 && paxPathUsed {
+		suffix := hdr.Name
+		prefix := ""
+		if len(hdr.Name) > fileNameSize && isASCII(hdr.Name) {
+			var err error
+			prefix, suffix, err = tw.splitUSTARLongName(hdr.Name)
+			if err == nil {
+				// ok we can use a ustar long name instead of pax, now correct the fields
+
+				// remove the path field from the pax header. this will suppress the pax header
+				delete(paxHeaders, paxPath)
+
+				// update the path fields
+				tw.cString(pathHeaderBytes, suffix, false, paxNone, nil)
+				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"))
+				}
+			}
+		}
 	}
 
 	// The chksum field is terminated by a NUL and a space.
@@ -190,8 +226,18 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
 		return tw.err
 	}
 
-	_, tw.err = tw.w.Write(header)
+	if len(paxHeaders) > 0 {
+		if !allowPax {
+			return errInvalidHeader
+		}
+		if err := tw.writePAXHeader(hdr, paxHeaders); err != nil {
+			return err
+		}
+	}
+	tw.nb = int64(hdr.Size)
+	tw.pad = (blockSize - (tw.nb % blockSize)) % blockSize
 
+	_, tw.err = tw.w.Write(header)
 	return tw.err
 }
 
@@ -218,7 +264,7 @@ func (tw *Writer) splitUSTARLongName(name string) (prefix, suffix string, err er
 
 // writePaxHeader writes an extended pax header to the
 // archive.
-func (tw *Writer) writePAXHeader(hdr *Header) error {
+func (tw *Writer) writePAXHeader(hdr *Header, paxHeaders map[string]string) error {
 	// Prepare extended header
 	ext := new(Header)
 	ext.Typeflag = TypeXHeader
@@ -229,18 +275,23 @@ func (tw *Writer) writePAXHeader(hdr *Header) error {
 	// with the current pid.
 	pid := os.Getpid()
 	dir, file := path.Split(hdr.Name)
-	ext.Name = path.Join(dir,
-		fmt.Sprintf("PaxHeaders.%d", pid), file)[0:100]
+	fullName := path.Join(dir,
+		fmt.Sprintf("PaxHeaders.%d", pid), file)
+
+	ascii := toASCII(fullName)
+	if len(ascii) > 100 {
+		ascii = ascii[:100]
+	}
+	ext.Name = ascii
 	// Construct the body
 	var buf bytes.Buffer
-	if len(hdr.Name) > fileNameSize {
-		fmt.Fprint(&buf, paxHeader("path="+hdr.Name))
-	}
-	if len(hdr.Linkname) > fileNameSize {
-		fmt.Fprint(&buf, paxHeader("linkpath="+hdr.Linkname))
+
+	for k, v := range paxHeaders {
+		fmt.Fprint(&buf, paxHeader(k+"="+v))
 	}
+
 	ext.Size = int64(len(buf.Bytes()))
-	if err := tw.WriteHeader(ext); err != nil {
+	if err := tw.writeHeader(ext, false); err != nil {
 		return err
 	}
 	if _, err := tw.Write(buf.Bytes()); err != nil {

コアとなるコードの解説

このコミットの核となる変更は、archive/tar パッケージがTARヘッダーを書き込む際のロジックにあります。

  1. PAXキーワードとASCIIユーティリティ (common.go):

    • paxAtime などの定数は、PAX拡張ヘッダーのキーとして使用される文字列を定義しています。これにより、コードの可読性と保守性が向上します。
    • isASCII 関数は、文字列が純粋なASCIIであるかを効率的にチェックします。これは、TARヘッダーの特定のフィールドがASCIIのみを許容するため、非ASCII文字を含む文字列をPAXヘッダーにオフロードする必要があるかどうかを判断するために重要です。
    • toASCII 関数は、非ASCII文字を削除して文字列をASCIIに変換します。これは、PAXヘッダーのファイル名(PaxHeaders.PID/filename)のように、特定のコンテキストでASCIIのみが許可される場合に、安全な文字列を生成するために使用されます。
  2. PAXヘッダーのマージ (reader.go):

    • mergePAX 関数は、TARアーカイブを読み取る際にPAX拡張ヘッダーから取得した情報を、Goの Header 構造体の対応するフィールドにマッピングします。このコミットでは、新しいPAXキーワード定数を使用するように更新され、より正確なマッピングを保証します。例えば、"path" の代わりに paxPath を使用することで、コードがより堅牢になります。
  3. PAX書き込みロジックの強化 (writer.go):

    • Writer.preferPax: この新しいフィールドは、数値フィールド(UID、GID、サイズなど)が大きすぎる場合に、GNU Tarのバイナリ拡張とPAXヘッダーのどちらを優先するかを制御します。これにより、ユーザーは生成されるTARアーカイブの互換性についてより細かく制御できるようになります。
    • cStringnumeric の変更: これらの関数は、TARヘッダーの文字列および数値フィールドを書き込む際に使用されます。変更後、これらの関数は allowPax, paxKeyword, paxHeaders の引数を受け取るようになりました。
      • cString は、書き込もうとしている文字列がターゲットフィールドの長さを超える場合、または非ASCII文字を含む場合に、その文字列を paxHeaders マップに追加し、PAXヘッダーとして書き込まれるようにマークします。これにより、長いファイル名や非ASCII文字を含むファイル名が正しく処理されます。
      • numeric も同様に、数値がフィールドの最大値を超える場合にPAXヘッダーにオフロードするロジックが追加されました。特に preferPaxtrue の場合、バイナリ拡張よりもPAXが優先されます。
    • WriteHeaderwriteHeader: WriteHeader は外部から呼び出される主要なメソッドですが、内部的には writeHeader を呼び出します。writeHeaderallowPax 引数を受け取り、PAXヘッダー自体を書き込む際にPAXヘッダーの再帰的な生成を防ぐために使用されます。
    • PAXヘッダーの自動生成ロジック: writeHeader 内で、paxHeaders マップが初期化され、ファイル名、リンク名、ユーザー名、グループ名、UID、GID、サイズなどのフィールドが、標準のTARヘッダーフィールドの制限を超えるか、非ASCII文字を含む場合に、自動的にこのマップにPAXレコードとして追加されます。
    • USTARロングネームの優先: preferPaxfalse で、かつファイル名のみが長すぎる場合に、PAXヘッダーではなくUSTARのロングネーム拡張(prefix フィールド)を使用するロジックが追加されました。これは、PAXヘッダーが不要な場合に、より互換性の高いUSTAR形式を優先するための最適化です。
    • writePAXHeader の変更: この関数は、paxHeaders マップを受け取り、その中のすべてのキーと値のペアをPAX拡張ヘッダーとして書き込みます。また、PAXヘッダーのファイル名(PaxHeaders.PID/filename)を生成する際に、toASCII を使用してASCII文字のみを保証し、100文字に切り詰める処理が追加されました。これは、PAXヘッダー自体もTARエントリとして扱われるため、そのヘッダーフィールドもTARの制限に従う必要があるためです。

これらの変更により、archive/tar パッケージは、TARアーカイブを書き込む際に、フィールドの長さ制限や文字エンコーディングの問題を自動的に検出し、必要に応じてPAX拡張ヘッダーを生成することで、より堅牢で互換性の高いアーカイブを作成できるようになりました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語の `archive/tar` パッケージにおける、TARアーカイブのPAX拡張ヘッダーのサポート改善と、長いファイル名やリンク名の処理に関するバグ修正を目的としています。特に、リンク名が100文字を超える場合の不具合と、PAXヘッダーが不完全にしか書き込まれていなかった問題に対処しています。

## コミット

commit a07c95a53c906e2d30762d76cd3e36c93d2c83f4 Author: Marco Hennings marco.hennings@freiheit.com Date: Mon Aug 19 10:45:44 2013 +1000

archive/tar: Fix support for long links and improve PAX support.

The tar/archive code from golang has a problem with linknames with length >
100. A pax header is added but the original header still written with a too
long field length.

As it is clear that pax support is incomplete I have added missing
implementation parts.

This commit contains code from the golang project in the folder tar/archiv.

The following pax header records are now automatically written:

- gname)
- linkpath
- path
- uname

The following fields can be written with PAX, but the default is to use the
star binary extension.

- gid  (value > 2097151)
- size (value > 8589934591)
- uid (value > 2097151)

The string fields are written when the value is longer as the field or if the
string contains a char that is not encodable as 7-bit ASCII value.

The change was tested against a current ubuntu-cloud image tarball comparing
the compressed result.

+ added some automated tests for the new functionality.

Fixes #6056.

R=dsymonds
CC=golang-dev
https://golang.org/cl/12561043

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

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

## 元コミット内容

`archive/tar: Fix support for long links and improve PAX support.`

このコミットは、Go言語の `archive/tar` パッケージにおいて、長いリンク名(100文字以上)のサポートを修正し、PAX(Portable Archive eXchange)ヘッダーのサポートを改善するものです。既存の実装では、長いリンク名に対してPAXヘッダーが追加されるにもかかわらず、元のヘッダーフィールドが長すぎるまま書き込まれる問題がありました。このコミットでは、PAXサポートの不完全な部分を補完し、`gname`, `linkpath`, `path`, `uname` といったPAXヘッダーレコードが自動的に書き込まれるように変更されています。また、`gid`, `size`, `uid` のような数値フィールドについても、値が大きすぎる場合にPAXヘッダーを使用するオプションが追加されています。さらに、文字列フィールドは、フィールド長を超える場合や7ビットASCIIでエンコードできない文字を含む場合にPAXヘッダーとして書き込まれるようになっています。変更はUbuntuクラウドイメージのtarballでテストされ、新しい機能のための自動テストも追加されています。

## 変更の背景

TARアーカイブフォーマットは、UNIX系システムで広く使われているファイルアーカイブ形式です。しかし、オリジナルのTARフォーマットには、ファイル名やリンク名、ユーザー/グループ名、ファイルサイズなどのフィールド長に制限がありました。特に、ファイル名やリンク名が100文字を超える場合、標準的なTARヘッダーでは表現できませんでした。

この問題を解決するために、POSIX.1-2001標準でPAX(Portable Archive eXchange)フォーマットが導入されました。PAXは、TARフォーマットの拡張であり、長いファイル名や大きなファイルサイズ、非ASCII文字を含むファイル名などを扱うためのメカニズムを提供します。PAXは、通常のTARヘッダーの前に「拡張ヘッダー」として追加のメタデータを格納することで、これらの制限を克服します。

Go言語の `archive/tar` パッケージは、TARアーカイブの読み書きをサポートしていますが、このコミット以前はPAXサポートが不完全でした。具体的には、リンク名が100文字を超えると、PAXヘッダーが追加されるにもかかわらず、元のTARヘッダーのリンク名フィールドが切り詰められたり、不正な値が書き込まれたりする問題がありました。これにより、作成されたTARアーカイブが他のTARツールで正しく解釈されない可能性がありました。

このコミットは、このような既存のバグを修正し、`archive/tar` パッケージのPAXサポートをより堅牢で包括的なものにすることを目的としています。これにより、Goで作成されたTARアーカイブが、より広範なファイルシステム特性(長いパス、大きなファイル、非ASCII文字など)を持つファイルを正確に表現できるようになります。

## 前提知識の解説

### TARアーカイブフォーマット

TAR(Tape ARchive)は、複数のファイルを一つのアーカイブファイルにまとめるためのフォーマットです。元々は磁気テープにデータを保存するために設計されましたが、現在ではファイルシステム上のアーカイブとしても広く利用されています。TARアーカイブは、一連の「ヘッダーブロック」と「データブロック」で構成されます。各ファイルやディレクトリは、そのメタデータ(ファイル名、サイズ、パーミッション、所有者など)を含むヘッダーブロックと、実際のファイルデータを含むデータブロックによって表現されます。

### USTAR (POSIX.1-1988)

USTAR(Unix Standard TAR)は、POSIX.1-1988で標準化されたTARフォーマットの拡張です。USTARは、元のTARフォーマットの制限(例えば、ファイル名が100文字まで)を緩和するために、追加のフィールドをヘッダーに導入しました。これにより、ファイル名が100文字を超える場合でも、`prefix` フィールドと `name` フィールドを組み合わせて長いパスを表現できるようになりました。しかし、`prefix` と `name` を合わせても255文字程度の制限があり、また、特定のフィールド(例えば、リンク名)には依然として100文字の制限がありました。

### PAX (Portable Archive eXchange) (POSIX.1-2001)

PAXは、POSIX.1-2001で標準化されたTARフォーマットのさらなる拡張です。PAXは、USTARの制限をさらに克服するために設計されました。PAXの主要な特徴は、「拡張ヘッダー(Extended Header)」の導入です。拡張ヘッダーは、通常のTARヘッダーの前に特別なタイプのファイルとして追加され、キーと値のペアの形式で追加のメタデータを格納します。これにより、以下のことが可能になります。

*   **長いパス名とリンク名**: 任意の長さのパス名やリンク名を `path` や `linkpath` キーで指定できます。
*   **大きなファイルサイズ**: 8GBを超えるファイルサイズを `size` キーで指定できます。
*   **大きなUID/GID**: ユーザーIDやグループIDが2097151を超える場合でも `uid` や `gid` キーで指定できます。
*   **非ASCII文字**: ファイル名やユーザー/グループ名にUTF-8エンコードされた非ASCII文字を含めることができます。
*   **高精度なタイムスタンプ**: `atime`, `mtime`, `ctime` などのタイムスタンプを秒以下の精度で記録できます。

PAX拡張ヘッダーは、`TypeXHeader` または `TypeGNULongLink` などの特別なタイプフラグを持つTARエントリとしてアーカイブ内に存在します。これらのヘッダーは、後続のデータエントリに適用されるメタデータを定義します。

### GNU Tar拡張

GNU Tarは、TARフォーマットの独自の拡張をいくつか導入しています。その中には、長いファイル名やリンク名を扱うためのメカニズムも含まれます。GNU Tarは、PAXとは異なる方法で長い名前を処理することがあり、例えば、`././@LongLink` や `././@LongFileName` といった特別なエントリを使用して長い名前を格納します。このコミットでは、PAXヘッダーを優先しつつも、必要に応じてGNU Tarのバイナリ拡張(特に大きな数値フィールドの場合)も考慮に入れています。

### Go言語の `archive/tar` パッケージ

Go言語の標準ライブラリには、`archive/tar` パッケージが含まれており、TARアーカイブの作成と読み取りのためのAPIを提供します。このパッケージは、TARフォーマットの様々なバリエーション(V7、USTAR、PAXなど)をサポートすることを目指しています。このコミットは、特にPAXフォーマットの書き込み機能の堅牢性を向上させるものです。

## 技術的詳細

このコミットの主要な変更点は、`archive/tar` パッケージがTARヘッダーを書き込む際に、PAX拡張ヘッダーをより適切に利用するように改善されたことです。

1.  **PAXキーワードの定義**: `src/pkg/archive/tar/common.go` に、PAX拡張ヘッダーで使用される新しいキーワード定数(`paxAtime`, `paxCharset`, `paxComment`, `paxCtime`, `paxGid`, `paxGname`, `paxLinkpath`, `paxMtime`, `paxPath`, `paxSize`, `paxUid`, `paxUname`)が追加されました。これにより、コード内でこれらのキーワードを安全かつ一貫して参照できるようになります。

2.  **ASCIIチェックと変換**: `isASCII` と `toASCII` 関数が `src/pkg/archive/tar/common.go` に追加されました。
    *   `isASCII(s string) bool`: 文字列 `s` が7ビットASCII文字のみで構成されているかをチェックします。
    *   `toASCII(s string) string`: 文字列 `s` から7ビットASCII文字のみを抽出し、それ以外の文字を破棄します。これは、TARヘッダーの特定のフィールドがASCII文字のみを期待する場合に、非ASCII文字を含む文字列を処理するために使用されます。

3.  **`Reader` のPAXマージの改善**: `src/pkg/archive/tar/reader.go` の `mergePAX` 関数が、新しいPAXキーワード定数を使用するように更新されました。これにより、PAX拡張ヘッダーから読み取られた値が、対応する `Header` フィールドに正しくマッピングされるようになります。

4.  **`Writer` のPAX書き込みロジックの強化**: `src/pkg/archive/tar/writer.go` の `Writer` 構造体と関連メソッドが大幅に修正されました。
    *   **`preferPax` フィールドの追加**: `Writer` 構造体に `preferPax` という新しいブール型フィールドが追加されました。これは、数値フィールドが大きすぎる場合に、GNU Tarのバイナリ拡張ではなくPAXヘッダーを優先するかどうかを制御します。
    *   **`cString` 関数の変更**: `cString` 関数(文字列を固定長のバイト配列に書き込む)が変更され、`allowPax`, `paxKeyword`, `paxHeaders` の引数を受け取るようになりました。これにより、文字列がフィールド長を超える場合や非ASCII文字を含む場合に、自動的にPAXヘッダーレコードとして追加されるようになりました。
    *   **`numeric` 関数の変更**: `numeric` 関数(数値を固定長のバイト配列に書き込む)も同様に、`allowPax`, `paxKeyword`, `paxHeaders` の引数を受け取るようになりました。これにより、数値がフィールドの最大値を超える場合に、PAXヘッダーレコードとして追加されるようになりました。特に、`preferPax` が `true` の場合、バイナリ拡張よりもPAXが優先されます。
    *   **`WriteHeader` と `writeHeader` の分離**: `WriteHeader` メソッドが `writeHeader` という内部メソッドを呼び出すように変更されました。`writeHeader` は `allowPax` 引数を受け取り、PAXヘッダーの書き込みを抑制できるため、PAX拡張ヘッダー自体を書き込む際に無限ループに陥るのを防ぎます。
    *   **PAXヘッダーの自動生成**: `writeHeader` メソッド内で、`paxHeaders` というマップが導入され、ファイル名、リンク名、ユーザー名、グループ名、UID、GID、サイズなどのフィールドが、標準のTARヘッダーフィールドの制限を超える場合や非ASCII文字を含む場合に、自動的にこのマップにPAXレコードとして追加されるようになりました。
    *   **USTARロングネームの優先**: `preferPax` が `false` の場合、かつファイル名のみが長すぎる場合に、PAXヘッダーではなくUSTARのロングネーム拡張(`prefix` フィールド)を使用するロジックが追加されました。これは、PAXヘッダーが不要な場合に、より互換性の高いUSTAR形式を優先するためのものです。
    *   **`writePAXHeader` の変更**: `writePAXHeader` 関数が `paxHeaders` マップを受け取るようになり、このマップに含まれるすべてのPAXレコードを拡張ヘッダーとして書き込むようになりました。また、PAXヘッダーのファイル名(`PaxHeaders.PID/filename`)を生成する際に、`toASCII` を使用してASCII文字のみを保証し、100文字に切り詰める処理が追加されました。

5.  **テストの追加**: `src/pkg/archive/tar/writer_test.go` に、長いシンボリックリンク名と非ASCII文字を含むファイル名、グループ名、ユーザー名をテストするための新しいテストケース(`TestPaxSymlink`, `TestPaxNonAscii`)が追加されました。これにより、PAXサポートの改善が正しく機能していることが検証されます。

これらの変更により、`archive/tar` パッケージは、より堅牢なPAXサポートを提供し、長いファイル名、リンク名、大きな数値、非ASCII文字を含むTARアーカイブをより正確に作成できるようになりました。

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

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

```go
 // Keywords for the PAX Extended Header
 const (
 	paxAtime    = "atime"
 	paxCharset  = "charset"
 	paxComment  = "comment"
 	paxCtime    = "ctime" // please note that ctime is not a valid pax header.
 	paxGid      = "gid"
 	paxGname    = "gname"
 	paxLinkpath = "linkpath"
 	paxMtime    = "mtime"
 	paxPath     = "path"
 	paxSize     = "size"
 	paxUid      = "uid"
 	paxUname    = "uname"
 	paxNone     = ""
 )

 func isASCII(s string) bool {
 	for _, c := range s {
 		if c >= 0x80 {
 			return false
 		}
 	}
 	return true
 }

 func toASCII(s string) string {
 	if isASCII(s) {
 		return s
 	}
 	var buf bytes.Buffer
 	for _, c := range s {
 		if c < 0x80 {
 			buf.WriteByte(byte(c))
 		}
 	}
 	return buf.String()
 }

src/pkg/archive/tar/reader.go

 func mergePAX(hdr *Header, headers map[string]string) error {
 	for k, v := range headers {
 		switch k {
-		case "path":
+		case paxPath:
 			hdr.Name = v
-		case "linkpath":
+		case paxLinkpath:
 			hdr.Linkname = v
-		case "gname":
+		case paxGname:
 			hdr.Gname = v
-		case "uname":
+		case paxUname:
 			hdr.Uname = v
-		case "uid":
+		case paxUid:
 			uid, err := strconv.ParseInt(v, 10, 0)
 			if err != nil {
 				return err
 			}
 			hdr.Uid = int(uid)
-		case "gid":
+		case paxGid:
 			gid, err := strconv.ParseInt(v, 10, 0)
 			if err != nil {
 				return err
 			}
 			hdr.Gid = int(gid)
-		case "atime":
+		case paxAtime:
 			t, err := parsePAXTime(v)
 			if err != nil {
 				return err
 			}
 			hdr.AccessTime = t
-		case "mtime":
+		case paxMtime:
 			t, err := parsePAXTime(v)
 			if err != nil {
 				return err
 			}
 			hdr.ModTime = t
-		case "ctime":
+		case paxCtime:
 			t, err := parsePAXTime(v)
 			if err != nil {
 				return err
 			}
 			hdr.ChangeTime = t
-		case "size":
+		case paxSize:
 			size, err := strconv.ParseInt(v, 10, 0)
 			if err != nil {
 				return err

src/pkg/archive/tar/writer.go

 type Writer struct {
 	w          io.Writer
 	err        error
 	nb         int64 // number of unwritten bytes for current file entry
 	pad        int64 // amount of padding to write after current file entry
 	closed     bool
 	usedBinary bool // whether the binary numeric field extension was used
+	preferPax  bool // use pax header instead of binary numeric header
 }

 // Write s into b, terminating it with a NUL if there is room.
-// If the value is too long for the field and allowPax is true add a paxheader record instead
-func (tw *Writer) cString(b []byte, s string, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
+func (tw *Writer) cString(b []byte, s string, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
+	needsPaxHeader := allowPax && len(s) > len(b) || !isASCII(s)
+	if needsPaxHeader {
+		paxHeaders[paxKeyword] = s
+		return
+	}
 	if len(s) > len(b) {
 		if tw.err == nil {
 			tw.err = ErrFieldTooLong
 		}
 		return
 	}
-	copy(b, s)
-	if len(s) < len(b) {
-		b[len(s)] = 0
+	ascii := toASCII(s)
+	copy(b, ascii)
+	if len(ascii) < len(b) {
+		b[len(ascii)] = 0
 	}
 }

 // Write x into b, either as octal or as binary (GNUtar/star extension).
-// If the value is too long for the field and writingPax is enabled both for the field and the add a paxheader record instead
-func (tw *Writer) numeric(b []byte, x int64, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
+func (tw *Writer) numeric(b []byte, x int64, allowPax bool, paxKeyword string, paxHeaders map[string]string) {
 	// Try octal first.
 	s := strconv.FormatInt(x, 8)
 	if len(s) < len(b) {
 		tw.octal(b, x)
 		return
 	}
+
+	// If it is too long for octal, and pax is preferred, use a pax header
+	if allowPax && tw.preferPax {
+		tw.octal(b, 0)
+		s := strconv.FormatInt(x, 10)
+		paxHeaders[paxKeyword] = s
+		return
+	}
+
 	// Too big: use binary (big-endian).
 	tw.usedBinary = true
 	for i := len(b) - 1; x > 0 && i >= 0; i-- {
@@ -115,6 +134,15 @@ var (
 // WriteHeader calls Flush if it is not the first header.
 // Calling after a Close will return ErrWriteAfterClose.
 func (tw *Writer) WriteHeader(hdr *Header) error {
+	return tw.writeHeader(hdr, true)
+}
+
+// WriteHeader writes hdr and prepares to accept the file's contents.
+// WriteHeader calls Flush if it is not the first header.
+// Calling after a Close will return ErrWriteAfterClose.
+// As this method is called internally by writePax header to allow it to
+// suppress writing the pax header.
+func (tw *Writer) writeHeader(hdr *Header, allowPax bool) error {
 	if tw.closed {
 		return ErrWriteAfterClose
 	}
@@ -124,31 +152,21 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
 	if tw.err != nil {
 		return tw.err
 	}
-	// Decide whether or not to use PAX extensions
+
+	// a map to hold pax header records, if any are needed
+	paxHeaders := make(map[string]string)
+
 	// TODO(shanemhansen): we might want to use PAX headers for
 	// subsecond time resolution, but for now let's just capture
-	// the long name/long symlink use case.
-	suffix := hdr.Name
-	prefix := ""
-	if len(hdr.Name) > fileNameSize || len(hdr.Linkname) > fileNameSize {
-		var err error
-		prefix, suffix, err = tw.splitUSTARLongName(hdr.Name)
-		// Either we were unable to pack the long name into ustar format
-		// or the link name is too long; use PAX headers.
-		if err == errNameTooLong || len(hdr.Linkname) > fileNameSize {
-			if err := tw.writePAXHeader(hdr); err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
-	}
-	tw.nb = int64(hdr.Size)
-	tw.pad = -tw.nb & (blockSize - 1) // blockSize is a power of two
+	// too long fields or non ascii characters
 
 	header := make([]byte, blockSize)
 	s := slicer(header)
-	tw.cString(s.next(fileNameSize), suffix)
+
+	// keep a reference to the filename to allow to overwrite it later if we detect that we can use ustar longnames instead of pax
+	pathHeaderBytes := s.next(fileNameSize)
+
+	tw.cString(pathHeaderBytes, hdr.Name, true, paxPath, paxHeaders)
 
 	// Handle out of range ModTime carefully.
 	var modTime int64
@@ -156,27 +164,55 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
 		modTime = hdr.ModTime.Unix()
 	}
 
-	tw.octal(s.next(8), hdr.Mode)          // 100:108
-	tw.numeric(s.next(8), int64(hdr.Uid))  // 108:116
-	tw.numeric(s.next(8), int64(hdr.Gid))  // 116:124
-	tw.numeric(s.next(12), hdr.Size)       // 124:136
-	tw.numeric(s.next(12), modTime)        // 136:148
-	s.next(8)                              // chksum (148:156)
-	s.next(1)[0] = hdr.Typeflag            // 156:157
-	tw.cString(s.next(100), hdr.Linkname)  // linkname (157:257)
-	copy(s.next(8), []byte("ustar\x0000")) // 257:265
-	tw.cString(s.next(32), hdr.Uname)      // 265:297
-	tw.cString(s.next(32), hdr.Gname)      // 297:329
-	tw.numeric(s.next(8), hdr.Devmajor)    // 329:337
-	tw.numeric(s.next(8), hdr.Devminor)    // 337:345
-	tw.cString(s.next(155), prefix)        // 345:500
+	tw.octal(s.next(8), hdr.Mode)                                   // 100:108
+	tw.numeric(s.next(8), int64(hdr.Uid), true, paxUid, paxHeaders) // 108:116
+	tw.numeric(s.next(8), int64(hdr.Gid), true, paxGid, paxHeaders) // 116:124
+	tw.numeric(s.next(12), hdr.Size, true, paxSize, paxHeaders)     // 124:136
+	tw.numeric(s.next(12), modTime, false, paxNone, nil)            // 136:148 --- consider using pax for finer granularity
+	s.next(8)                                                       // chksum (148:156)
+	s.next(1)[0] = hdr.Typeflag                                     // 156:157
+
+	tw.cString(s.next(100), hdr.Linkname, true, paxLinkpath, paxHeaders)
+
+	copy(s.next(8), []byte("ustar\x0000"))                        // 257:265
+	tw.cString(s.next(32), hdr.Uname, true, paxUname, paxHeaders) // 265:297
+	tw.cString(s.next(32), hdr.Gname, true, paxGname, paxHeaders) // 297:329
+	tw.numeric(s.next(8), hdr.Devmajor, false, paxNone, nil)      // 329:337
+	tw.numeric(s.next(8), hdr.Devminor, false, paxNone, nil)      // 337:345
+
+	// keep a reference to the prefix to allow to overwrite it later if we detect that we can use ustar longnames instead of pax
+	prefixHeaderBytes := s.next(155)
+	tw.cString(prefixHeaderBytes, "", false, paxNone, nil) // 345:500  prefix
+
 	// Use the GNU magic instead of POSIX magic if we used any GNU extensions.
 	if tw.usedBinary {
 		copy(header[257:265], []byte("ustar  \x00"))
 	}
-	// Use the ustar magic if we used ustar long names.
-	if len(prefix) > 0 {
-		copy(header[257:265], []byte("ustar\000"))
+
+	_, paxPathUsed := paxHeaders[paxPath]
+	// try to use a ustar header when only the name is too long
+	if !tw.preferPax && len(paxHeaders) == 1 && paxPathUsed {
+		suffix := hdr.Name
+		prefix := ""
+		if len(hdr.Name) > fileNameSize && isASCII(hdr.Name) {
+			var err error
+			prefix, suffix, err = tw.splitUSTARLongName(hdr.Name)
+			if err == nil {
+				// ok we can use a ustar long name instead of pax, now correct the fields
+
+				// remove the path field from the pax header. this will suppress the pax header
+				delete(paxHeaders, paxPath)
+
+				// update the path fields
+				tw.cString(pathHeaderBytes, suffix, false, paxNone, nil)
+				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"))
+				}
+			}
+		}
 	}
 
 	// The chksum field is terminated by a NUL and a space.
@@ -190,8 +236,18 @@ func (tw *Writer) WriteHeader(hdr *Header) error {
 		return tw.err
 	}
 
-	_, tw.err = tw.w.Write(header)
+	if len(paxHeaders) > 0 {
+		if !allowPax {
+			return errInvalidHeader
+		}
+		if err := tw.writePAXHeader(hdr, paxHeaders); err != nil {
+			return err
+		}
+	}
+	tw.nb = int64(hdr.Size)
+	tw.pad = (blockSize - (tw.nb % blockSize)) % blockSize
 
+	_, tw.err = tw.w.Write(header)
 	return tw.err
 }
 
@@ -218,7 +274,7 @@ func (tw *Writer) splitUSTARLongName(name string) (prefix, suffix string, err er
 
 // writePaxHeader writes an extended pax header to the
 // archive.
-func (tw *Writer) writePAXHeader(hdr *Header) error {
+func (tw *Writer) writePAXHeader(hdr *Header, paxHeaders map[string]string) error {
 	// Prepare extended header
 	ext := new(Header)
 	ext.Typeflag = TypeXHeader
@@ -229,18 +285,23 @@ func (tw *Writer) writePAXHeader(hdr *Header) error {
 	// with the current pid.
 	pid := os.Getpid()
 	dir := path.Dir(hdr.Name)
-	ext.Name = path.Join(dir,
-		fmt.Sprintf("PaxHeaders.%d", pid), path.Base(hdr.Name))[0:100]
+	fullName := path.Join(dir,
+		fmt.Sprintf("PaxHeaders.%d", pid), path.Base(hdr.Name))
+
+	ascii := toASCII(fullName)
+	if len(ascii) > 100 {
+		ascii = ascii[:100]
+	}
+	ext.Name = ascii
 	// Construct the body
 	var buf bytes.Buffer
-	if len(hdr.Name) > fileNameSize {
-		fmt.Fprint(&buf, paxHeader("path="+hdr.Name))
-	}
-	if len(hdr.Linkname) > fileNameSize {
-		fmt.Fprint(&buf, paxHeader("linkpath="+hdr.Linkname))
+
+	for k, v := range paxHeaders {
+		fmt.Fprint(&buf, paxHeader(k+"="+v))
 	}
+
 	ext.Size = int64(len(buf.Bytes()))
-	if err := tw.WriteHeader(ext); err != nil {
+	if err := tw.writeHeader(ext, false); err != nil {
 		return err
 	}
 	if _, err := tw.Write(buf.Bytes()); err != nil {

コアとなるコードの解説

このコミットの核となる変更は、archive/tar パッケージがTARヘッダーを書き込む際のロジックにあります。

  1. PAXキーワードとASCIIユーティリティ (common.go):

    • paxAtime などの定数は、PAX拡張ヘッダーのキーとして使用される文字列を定義しています。これにより、コードの可読性と保守性が向上します。
    • isASCII 関数は、文字列が純粋なASCIIであるかを効率的にチェックします。これは、TARヘッダーの特定のフィールドがASCIIのみを許容するため、非ASCII文字を含む文字列をPAXヘッダーにオフロードする必要があるかどうかを判断するために重要です。
    • toASCII 関数は、非ASCII文字を削除して文字列をASCIIに変換します。これは、PAXヘッダーのファイル名(PaxHeaders.PID/filename)のように、特定のコンテキストでASCIIのみが許可される場合に、安全な文字列を生成するために使用されます。
  2. PAXヘッダーのマージ (reader.go):

    • mergePAX 関数は、TARアーカイブを読み取る際にPAX拡張ヘッダーから取得した情報を、Goの Header 構造体の対応するフィールドにマッピングします。このコミットでは、新しいPAXキーワード定数を使用するように更新され、より正確なマッピングを保証します。例えば、"path" の代わりに paxPath を使用することで、コードがより堅牢になります。
  3. PAX書き込みロジックの強化 (writer.go):

    • Writer.preferPax: この新しいフィールドは、数値フィールド(UID、GID、サイズなど)が大きすぎる場合に、GNU Tarのバイナリ拡張とPAXヘッダーのどちらを優先するかを制御します。これにより、ユーザーは生成されるTARアーカイブの互換性についてより細かく制御できるようになります。
    • cStringnumeric の変更: これらの関数は、TARヘッダーの文字列および数値フィールドを書き込む際に使用されます。変更後、これらの関数は allowPax, paxKeyword, paxHeaders の引数を受け取るようになりました。
      • cString は、書き込もうとしている文字列がターゲットフィールドの長さを超える場合、または非ASCII文字を含む場合に、その文字列を paxHeaders マップに追加し、PAXヘッダーとして書き込まれるようにマークします。これにより、長いファイル名や非ASCII文字を含むファイル名が正しく処理されます。
      • numeric も同様に、数値がフィールドの最大値を超える場合にPAXヘッダーにオフロードするロジックが追加されました。特に preferPaxtrue の場合、バイナリ拡張よりもPAXが優先されます。
    • WriteHeaderwriteHeader: WriteHeader は外部から呼び出される主要なメソッドですが、内部的には writeHeader を呼び出します。writeHeaderallowPax 引数を受け取り、PAXヘッダー自体を書き込む際にPAXヘッダーの再帰的な生成を防ぐために使用されます。
    • PAXヘッダーの自動生成ロジック: writeHeader 内で、paxHeaders マップが初期化され、ファイル名、リンク名、ユーザー名、グループ名、UID、GID、サイズなどのフィールドが、標準のTARヘッダーフィールドの制限を超えるか、非ASCII文字を含む場合に、自動的にこのマップにPAXレコードとして追加されます。
    • USTARロングネームの優先: preferPaxfalse で、かつファイル名のみが長すぎる場合に、PAXヘッダーではなくUSTARのロングネーム拡張(prefix フィールド)を使用するロジックが追加されました。これは、PAXヘッダーが不要な場合に、より互換性の高いUSTAR形式を優先するための最適化です。
    • writePAXHeader の変更: この関数は、paxHeaders マップを受け取り、その中のすべてのキーと値のペアをPAX拡張ヘッダーとして書き込みます。また、PAXヘッダーのファイル名(PaxHeaders.PID/filename)を生成する際に、toASCII を使用してASCII文字のみを保証し、100文字に切り詰める処理が追加されました。これは、PAXヘッダー自体もTARエントリとして扱われるため、そのヘッダーフィールドもTARの制限に従う必要があるためです。

これらの変更により、archive/tar パッケージは、TARアーカイブを書き込む際に、フィールドの長さ制限や文字エンコーディングの問題を自動的に検出し、必要に応じてPAX拡張ヘッダーを生成することで、より堅牢で互換性の高いアーカイブを作成できるようになりました。

関連リンク

参考にした情報源リンク