[インデックス 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
フィールドを組み合わせて長いパスを表現できるようになりました。しかし、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拡張ヘッダーをより適切に利用するように改善されたことです。
-
PAXキーワードの定義:
src/pkg/archive/tar/common.go
に、PAX拡張ヘッダーで使用される新しいキーワード定数(paxAtime
,paxCharset
,paxComment
,paxCtime
,paxGid
,paxGname
,paxLinkpath
,paxMtime
,paxPath
,paxSize
,paxUid
,paxUname
)が追加されました。これにより、コード内でこれらのキーワードを安全かつ一貫して参照できるようになります。 -
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文字を含む文字列を処理するために使用されます。
-
Reader
のPAXマージの改善:src/pkg/archive/tar/reader.go
のmergePAX
関数が、新しいPAXキーワード定数を使用するように更新されました。これにより、PAX拡張ヘッダーから読み取られた値が、対応するHeader
フィールドに正しくマッピングされるようになります。 -
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文字に切り詰める処理が追加されました。
-
テストの追加:
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ヘッダーを書き込む際のロジックにあります。
-
PAXキーワードとASCIIユーティリティ (
common.go
):paxAtime
などの定数は、PAX拡張ヘッダーのキーとして使用される文字列を定義しています。これにより、コードの可読性と保守性が向上します。isASCII
関数は、文字列が純粋なASCIIであるかを効率的にチェックします。これは、TARヘッダーの特定のフィールドがASCIIのみを許容するため、非ASCII文字を含む文字列をPAXヘッダーにオフロードする必要があるかどうかを判断するために重要です。toASCII
関数は、非ASCII文字を削除して文字列をASCIIに変換します。これは、PAXヘッダーのファイル名(PaxHeaders.PID/filename
)のように、特定のコンテキストでASCIIのみが許可される場合に、安全な文字列を生成するために使用されます。
-
PAXヘッダーのマージ (
reader.go
):mergePAX
関数は、TARアーカイブを読み取る際にPAX拡張ヘッダーから取得した情報を、GoのHeader
構造体の対応するフィールドにマッピングします。このコミットでは、新しいPAXキーワード定数を使用するように更新され、より正確なマッピングを保証します。例えば、"path"
の代わりにpaxPath
を使用することで、コードがより堅牢になります。
-
PAX書き込みロジックの強化 (
writer.go
):Writer.preferPax
: この新しいフィールドは、数値フィールド(UID、GID、サイズなど)が大きすぎる場合に、GNU Tarのバイナリ拡張とPAXヘッダーのどちらを優先するかを制御します。これにより、ユーザーは生成されるTARアーカイブの互換性についてより細かく制御できるようになります。cString
とnumeric
の変更: これらの関数は、TARヘッダーの文字列および数値フィールドを書き込む際に使用されます。変更後、これらの関数はallowPax
,paxKeyword
,paxHeaders
の引数を受け取るようになりました。cString
は、書き込もうとしている文字列がターゲットフィールドの長さを超える場合、または非ASCII文字を含む場合に、その文字列をpaxHeaders
マップに追加し、PAXヘッダーとして書き込まれるようにマークします。これにより、長いファイル名や非ASCII文字を含むファイル名が正しく処理されます。numeric
も同様に、数値がフィールドの最大値を超える場合に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
の変更: この関数は、paxHeaders
マップを受け取り、その中のすべてのキーと値のペアをPAX拡張ヘッダーとして書き込みます。また、PAXヘッダーのファイル名(PaxHeaders.PID/filename
)を生成する際に、toASCII
を使用してASCII文字のみを保証し、100文字に切り詰める処理が追加されました。これは、PAXヘッダー自体もTARエントリとして扱われるため、そのヘッダーフィールドもTARの制限に従う必要があるためです。
これらの変更により、archive/tar
パッケージは、TARアーカイブを書き込む際に、フィールドの長さ制限や文字エンコーディングの問題を自動的に検出し、必要に応じてPAX拡張ヘッダーを生成することで、より堅牢で互換性の高いアーカイブを作成できるようになりました。
関連リンク
- Go Issue #6056: archive/tar: long linkname causes error
- Go CL 12561043: archive/tar: Fix support for long links and improve PAX support.
参考にした情報源リンク
- POSIX.1-2001 (IEEE Std 1003.1-2001) - Portable Archive Interchange Format (PAX)
- TAR (file format) - Wikipedia
- GNU Tar Manual
- Go archive/tar package documentation
- USTAR formatI have provided the detailed technical explanation of the commit in Markdown format, following all the specified instructions and chapter structure. I have used the commit information and my knowledge of TAR, USTAR, and PAX formats to explain the background, prerequisites, and technical details. I have also highlighted the core code changes and their explanations. I have included relevant links to the GitHub commit, Go issue, and documentation.
# [インデックス 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ヘッダーを書き込む際のロジックにあります。
-
PAXキーワードとASCIIユーティリティ (
common.go
):paxAtime
などの定数は、PAX拡張ヘッダーのキーとして使用される文字列を定義しています。これにより、コードの可読性と保守性が向上します。isASCII
関数は、文字列が純粋なASCIIであるかを効率的にチェックします。これは、TARヘッダーの特定のフィールドがASCIIのみを許容するため、非ASCII文字を含む文字列をPAXヘッダーにオフロードする必要があるかどうかを判断するために重要です。toASCII
関数は、非ASCII文字を削除して文字列をASCIIに変換します。これは、PAXヘッダーのファイル名(PaxHeaders.PID/filename
)のように、特定のコンテキストでASCIIのみが許可される場合に、安全な文字列を生成するために使用されます。
-
PAXヘッダーのマージ (
reader.go
):mergePAX
関数は、TARアーカイブを読み取る際にPAX拡張ヘッダーから取得した情報を、GoのHeader
構造体の対応するフィールドにマッピングします。このコミットでは、新しいPAXキーワード定数を使用するように更新され、より正確なマッピングを保証します。例えば、"path"
の代わりにpaxPath
を使用することで、コードがより堅牢になります。
-
PAX書き込みロジックの強化 (
writer.go
):Writer.preferPax
: この新しいフィールドは、数値フィールド(UID、GID、サイズなど)が大きすぎる場合に、GNU Tarのバイナリ拡張とPAXヘッダーのどちらを優先するかを制御します。これにより、ユーザーは生成されるTARアーカイブの互換性についてより細かく制御できるようになります。cString
とnumeric
の変更: これらの関数は、TARヘッダーの文字列および数値フィールドを書き込む際に使用されます。変更後、これらの関数はallowPax
,paxKeyword
,paxHeaders
の引数を受け取るようになりました。cString
は、書き込もうとしている文字列がターゲットフィールドの長さを超える場合、または非ASCII文字を含む場合に、その文字列をpaxHeaders
マップに追加し、PAXヘッダーとして書き込まれるようにマークします。これにより、長いファイル名や非ASCII文字を含むファイル名が正しく処理されます。numeric
も同様に、数値がフィールドの最大値を超える場合に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
の変更: この関数は、paxHeaders
マップを受け取り、その中のすべてのキーと値のペアをPAX拡張ヘッダーとして書き込みます。また、PAXヘッダーのファイル名(PaxHeaders.PID/filename
)を生成する際に、toASCII
を使用してASCII文字のみを保証し、100文字に切り詰める処理が追加されました。これは、PAXヘッダー自体もTARエントリとして扱われるため、そのヘッダーフィールドもTARの制限に従う必要があるためです。
これらの変更により、archive/tar
パッケージは、TARアーカイブを書き込む際に、フィールドの長さ制限や文字エンコーディングの問題を自動的に検出し、必要に応じてPAX拡張ヘッダーを生成することで、より堅牢で互換性の高いアーカイブを作成できるようになりました。
関連リンク
- Go Issue #6056: archive/tar: long linkname causes error
- Go CL 12561043: archive/tar: Fix support for long links and improve PAX support.
参考にした情報源リンク
- POSIX.1-2001 (IEEE Std 1003.1-2001) - Portable Archive Interchange Format (PAX)
- TAR (file format) - Wikipedia
- GNU Tar Manual
- Go archive/tar package documentation
- USTAR format