1
0
mirror of https://github.com/golang/go synced 2024-11-19 14:34:42 -07:00

archive/tar: support arbitrary PAX records

This CL adds the following new publicly visible API:
	type Header struct { ...; PAXRecords map[string]string }

The new Header.PAXRecords field is a map of all PAX extended header records.

We suggest (but do not enforce) that users use VENDOR-prefixed keys
according to the following in the PAX specification:
<<<
The standard developers have reserved keyword name space for vendor extensions.
It is suggested that the format to be used is:
	VENDOR.keyword
where VENDOR is the name of the vendor or organization in all uppercase letters.
>>>

When reading, the Header.PAXRecords is populated with all PAX records
encountered so far, including basic ones (e.g., "path", "mtime", etc).
When writing, the fields of Header will be merged into PAXRecords,
overwriting any records that may conflict.

Since PAXRecords is a more expressive feature than Xattrs and
is entirely a superset of Xattrs, we mark Xattrs as deprecated,
and steer users towards the new PAXRecords API.

The issue has a discussion about adding a Header.SetPAXRecord method
to help validate records and keep the Header fields in sync.
However, we do not include that in this CL since that helper
method can always be added in the future.

There is no support for global records.

Fixes #14472

Change-Id: If285a52749acc733476cf75a2c7ad15bc1542071
Reviewed-on: https://go-review.googlesource.com/58390
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
Joe Tsai 2017-08-23 23:44:33 -07:00 committed by Joe Tsai
parent 4d6da86469
commit a795ca51db
7 changed files with 234 additions and 87 deletions

View File

@ -66,8 +66,53 @@ const (
TypeGNUSparse = 'S' // sparse file TypeGNUSparse = 'S' // sparse file
) )
// Keywords for PAX extended header records.
const (
paxNone = "" // Indicates that no PAX key is suitable
paxPath = "path"
paxLinkpath = "linkpath"
paxSize = "size"
paxUid = "uid"
paxGid = "gid"
paxUname = "uname"
paxGname = "gname"
paxMtime = "mtime"
paxAtime = "atime"
paxCtime = "ctime" // Removed from later revision of PAX spec, but was valid
paxCharset = "charset" // Currently unused
paxComment = "comment" // Currently unused
paxSchilyXattr = "SCHILY.xattr."
// Keywords for GNU sparse files in a PAX extended header.
paxGNUSparse = "GNU.sparse."
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"
)
// basicKeys is a set of the PAX keys for which we have built-in support.
// This does not contain "charset" or "comment", which are both PAX-specific,
// so adding them as first-class features of Header is unlikely.
// Users can use the PAXRecords field to set it themselves.
var basicKeys = map[string]bool{
paxPath: true, paxLinkpath: true, paxSize: true, paxUid: true, paxGid: true,
paxUname: true, paxGname: true, paxMtime: true, paxAtime: true, paxCtime: true,
}
// A Header represents a single header in a tar archive. // A Header represents a single header in a tar archive.
// Some fields may not be populated. // Some fields may not be populated.
//
// For forward compatibility, users that retrieve a Header from Reader.Next,
// mutate it in some ways, and then pass it back to Writer.WriteHeader
// should do so by creating a new Header and copying the fields
// that they are interested in preserving.
type Header struct { type Header struct {
Name string // name of header file entry Name string // name of header file entry
Mode int64 // permission and mode bits Mode int64 // permission and mode bits
@ -83,7 +128,6 @@ type Header struct {
Devminor int64 // minor number of character or block device Devminor int64 // minor number of character or block device
AccessTime time.Time // access time AccessTime time.Time // access time
ChangeTime time.Time // status change time ChangeTime time.Time // status change time
Xattrs map[string]string
// SparseHoles represents a sequence of holes in a sparse file. // SparseHoles represents a sequence of holes in a sparse file.
// //
@ -99,6 +143,31 @@ type Header struct {
// not overlap with each other, and not extend past the specified Size. // not overlap with each other, and not extend past the specified Size.
SparseHoles []SparseEntry SparseHoles []SparseEntry
// Xattrs stores extended attributes as PAX records under the
// "SCHILY.xattr." namespace.
//
// The following are semantically equivalent:
// h.Xattrs[key] = value
// h.PAXRecords["SCHILY.xattr."+key] = value
//
// When Writer.WriteHeader is called, the contents of Xattrs will take
// precedence over those in PAXRecords.
//
// Deprecated: Use PAXRecords instead.
Xattrs map[string]string
// PAXRecords is a map of PAX extended header records.
//
// User-defined records should have keys of the following form:
// VENDOR.keyword
// Where VENDOR is some namespace in all uppercase, and keyword may
// not contain the '=' character (e.g., "GOLANG.pkg.version").
// The key and value should be non-empty UTF-8 strings.
//
// When Writer.WriteHeader is called, PAX records derived from the
// the other fields in Header take precedence over PAXRecords.
PAXRecords map[string]string
// Format specifies the format of the tar header. // Format specifies the format of the tar header.
// //
// This is set by Reader.Next as a best-effort guess at the format. // This is set by Reader.Next as a best-effort guess at the format.
@ -334,11 +403,22 @@ func (h *Header) allowedFormats() (format Format, paxHdrs map[string]string, err
// Check PAX records. // Check PAX records.
if len(h.Xattrs) > 0 { if len(h.Xattrs) > 0 {
for k, v := range h.Xattrs { for k, v := range h.Xattrs {
paxHdrs[paxXattr+k] = v paxHdrs[paxSchilyXattr+k] = v
} }
whyOnlyPAX = "only PAX supports Xattrs" whyOnlyPAX = "only PAX supports Xattrs"
format.mayOnlyBe(FormatPAX) format.mayOnlyBe(FormatPAX)
} }
if len(h.PAXRecords) > 0 {
for k, v := range h.PAXRecords {
_, exists := paxHdrs[k]
ignore := exists || basicKeys[k] || strings.HasPrefix(k, paxGNUSparse)
if !ignore {
paxHdrs[k] = v
}
}
whyOnlyPAX = "only PAX supports PAXRecords"
format.mayOnlyBe(FormatPAX)
}
for k, v := range paxHdrs { for k, v := range paxHdrs {
// Forbid empty values (which represent deletion) since usage of // Forbid empty values (which represent deletion) since usage of
// them are non-sensible without global PAX record support. // them are non-sensible without global PAX record support.
@ -497,35 +577,6 @@ const (
c_ISSOCK = 0140000 // Socket c_ISSOCK = 0140000 // Socket
) )
// 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"
paxXattr = "SCHILY.xattr."
paxNone = ""
// Keywords for GNU sparse files in a PAX extended header.
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"
)
// FileInfoHeader creates a partially-populated Header from fi. // FileInfoHeader creates a partially-populated Header from fi.
// If fi describes a symlink, FileInfoHeader records link as the link target. // If fi describes a symlink, FileInfoHeader records link as the link target.
// If fi describes a directory, a slash is appended to the name. // If fi describes a directory, a slash is appended to the name.
@ -600,6 +651,12 @@ func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
if sys.SparseHoles != nil { if sys.SparseHoles != nil {
h.SparseHoles = append([]SparseEntry{}, sys.SparseHoles...) h.SparseHoles = append([]SparseEntry{}, sys.SparseHoles...)
} }
if sys.PAXRecords != nil {
h.PAXRecords = make(map[string]string)
for k, v := range sys.PAXRecords {
h.PAXRecords[k] = v
}
}
} }
if sysStat != nil { if sysStat != nil {
return h, sysStat(fi, h) return h, sysStat(fi, h)

View File

@ -4,9 +4,6 @@
package tar package tar
// TODO(dsymonds):
// - pax extensions
import ( import (
"bytes" "bytes"
"io" "io"
@ -57,7 +54,8 @@ func (tr *Reader) Next() (*Header, error) {
} }
func (tr *Reader) next() (*Header, error) { func (tr *Reader) next() (*Header, error) {
var extHdrs map[string]string var paxHdrs map[string]string
var gnuLongName, gnuLongLink string
// Externally, Next iterates through the tar archive as if it is a series of // Externally, Next iterates through the tar archive as if it is a series of
// files. Internally, the tar format often uses fake "files" to add meta // files. Internally, the tar format often uses fake "files" to add meta
@ -89,7 +87,7 @@ loop:
switch hdr.Typeflag { switch hdr.Typeflag {
case TypeXHeader: case TypeXHeader:
format.mayOnlyBe(FormatPAX) format.mayOnlyBe(FormatPAX)
extHdrs, err = parsePAX(tr) paxHdrs, err = parsePAX(tr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -101,28 +99,27 @@ loop:
return nil, err return nil, err
} }
// Convert GNU extensions to use PAX headers.
if extHdrs == nil {
extHdrs = make(map[string]string)
}
var p parser var p parser
switch hdr.Typeflag { switch hdr.Typeflag {
case TypeGNULongName: case TypeGNULongName:
extHdrs[paxPath] = p.parseString(realname) gnuLongName = p.parseString(realname)
case TypeGNULongLink: case TypeGNULongLink:
extHdrs[paxLinkpath] = p.parseString(realname) gnuLongLink = p.parseString(realname)
}
if p.err != nil {
return nil, p.err
} }
continue loop // This is a meta header affecting the next header continue loop // This is a meta header affecting the next header
default: default:
// The old GNU sparse format is handled here since it is technically // The old GNU sparse format is handled here since it is technically
// just a regular file with additional attributes. // just a regular file with additional attributes.
if err := mergePAX(hdr, extHdrs); err != nil { if err := mergePAX(hdr, paxHdrs); err != nil {
return nil, err return nil, err
} }
if gnuLongName != "" {
hdr.Name = gnuLongName
}
if gnuLongLink != "" {
hdr.Linkname = gnuLongLink
}
// The extended headers may have updated the size. // The extended headers may have updated the size.
// Thus, setup the regFileReader again after merging PAX headers. // Thus, setup the regFileReader again after merging PAX headers.
@ -132,7 +129,7 @@ loop:
// Sparse formats rely on being able to read from the logical data // Sparse formats rely on being able to read from the logical data
// section; there must be a preceding call to handleRegularFile. // section; there must be a preceding call to handleRegularFile.
if err := tr.handleSparseFile(hdr, rawHdr, extHdrs); err != nil { if err := tr.handleSparseFile(hdr, rawHdr); err != nil {
return nil, err return nil, err
} }
@ -165,13 +162,13 @@ func (tr *Reader) handleRegularFile(hdr *Header) error {
// handleSparseFile checks if the current file is a sparse format of any type // handleSparseFile checks if the current file is a sparse format of any type
// and sets the curr reader appropriately. // and sets the curr reader appropriately.
func (tr *Reader) handleSparseFile(hdr *Header, rawHdr *block, extHdrs map[string]string) error { func (tr *Reader) handleSparseFile(hdr *Header, rawHdr *block) error {
var spd sparseDatas var spd sparseDatas
var err error var err error
if hdr.Typeflag == TypeGNUSparse { if hdr.Typeflag == TypeGNUSparse {
spd, err = tr.readOldGNUSparseMap(hdr, rawHdr) spd, err = tr.readOldGNUSparseMap(hdr, rawHdr)
} else { } else {
spd, err = tr.readGNUSparsePAXHeaders(hdr, extHdrs) spd, err = tr.readGNUSparsePAXHeaders(hdr)
} }
// If sp is non-nil, then this is a sparse file. // If sp is non-nil, then this is a sparse file.
@ -191,10 +188,10 @@ func (tr *Reader) handleSparseFile(hdr *Header, rawHdr *block, extHdrs map[strin
// If they are found, then this function reads the sparse map and returns it. // If they are found, then this function reads the sparse map and returns it.
// This assumes that 0.0 headers have already been converted to 0.1 headers // This assumes that 0.0 headers have already been converted to 0.1 headers
// by the the PAX header parsing logic. // by the the PAX header parsing logic.
func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header, extHdrs map[string]string) (sparseDatas, error) { func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header) (sparseDatas, error) {
// Identify the version of GNU headers. // Identify the version of GNU headers.
var is1x0 bool var is1x0 bool
major, minor := extHdrs[paxGNUSparseMajor], extHdrs[paxGNUSparseMinor] major, minor := hdr.PAXRecords[paxGNUSparseMajor], hdr.PAXRecords[paxGNUSparseMinor]
switch { switch {
case major == "0" && (minor == "0" || minor == "1"): case major == "0" && (minor == "0" || minor == "1"):
is1x0 = false is1x0 = false
@ -202,7 +199,7 @@ func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header, extHdrs map[string]string
is1x0 = true is1x0 = true
case major != "" || minor != "": case major != "" || minor != "":
return nil, nil // Unknown GNU sparse PAX version return nil, nil // Unknown GNU sparse PAX version
case extHdrs[paxGNUSparseMap] != "": case hdr.PAXRecords[paxGNUSparseMap] != "":
is1x0 = false // 0.0 and 0.1 did not have explicit version records, so guess is1x0 = false // 0.0 and 0.1 did not have explicit version records, so guess
default: default:
return nil, nil // Not a PAX format GNU sparse file. return nil, nil // Not a PAX format GNU sparse file.
@ -210,12 +207,12 @@ func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header, extHdrs map[string]string
hdr.Format.mayOnlyBe(FormatPAX) hdr.Format.mayOnlyBe(FormatPAX)
// Update hdr from GNU sparse PAX headers. // Update hdr from GNU sparse PAX headers.
if name := extHdrs[paxGNUSparseName]; name != "" { if name := hdr.PAXRecords[paxGNUSparseName]; name != "" {
hdr.Name = name hdr.Name = name
} }
size := extHdrs[paxGNUSparseSize] size := hdr.PAXRecords[paxGNUSparseSize]
if size == "" { if size == "" {
size = extHdrs[paxGNUSparseRealSize] size = hdr.PAXRecords[paxGNUSparseRealSize]
} }
if size != "" { if size != "" {
n, err := strconv.ParseInt(size, 10, 64) n, err := strconv.ParseInt(size, 10, 64)
@ -229,7 +226,7 @@ func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header, extHdrs map[string]string
if is1x0 { if is1x0 {
return readGNUSparseMap1x0(tr.curr) return readGNUSparseMap1x0(tr.curr)
} else { } else {
return readGNUSparseMap0x1(extHdrs) return readGNUSparseMap0x1(hdr.PAXRecords)
} }
} }
@ -265,17 +262,18 @@ func mergePAX(hdr *Header, headers map[string]string) (err error) {
case paxSize: case paxSize:
hdr.Size, err = strconv.ParseInt(v, 10, 64) hdr.Size, err = strconv.ParseInt(v, 10, 64)
default: default:
if strings.HasPrefix(k, paxXattr) { if strings.HasPrefix(k, paxSchilyXattr) {
if hdr.Xattrs == nil { if hdr.Xattrs == nil {
hdr.Xattrs = make(map[string]string) hdr.Xattrs = make(map[string]string)
} }
hdr.Xattrs[k[len(paxXattr):]] = v hdr.Xattrs[k[len(paxSchilyXattr):]] = v
} }
} }
if err != nil { if err != nil {
return ErrHeader return ErrHeader
} }
} }
hdr.PAXRecords = headers
return nil return nil
} }
@ -293,7 +291,7 @@ func parsePAX(r io.Reader) (map[string]string, error) {
// headers since 0.0 headers were not PAX compliant. // headers since 0.0 headers were not PAX compliant.
var sparseMap []string var sparseMap []string
extHdrs := make(map[string]string) paxHdrs := make(map[string]string)
for len(sbuf) > 0 { for len(sbuf) > 0 {
key, value, residual, err := parsePAXRecord(sbuf) key, value, residual, err := parsePAXRecord(sbuf)
if err != nil { if err != nil {
@ -314,16 +312,16 @@ func parsePAX(r io.Reader) (map[string]string, error) {
// According to PAX specification, a value is stored only if it is // According to PAX specification, a value is stored only if it is
// non-empty. Otherwise, the key is deleted. // non-empty. Otherwise, the key is deleted.
if len(value) > 0 { if len(value) > 0 {
extHdrs[key] = value paxHdrs[key] = value
} else { } else {
delete(extHdrs, key) delete(paxHdrs, key)
} }
} }
} }
if len(sparseMap) > 0 { if len(sparseMap) > 0 {
extHdrs[paxGNUSparseMap] = strings.Join(sparseMap, ",") paxHdrs[paxGNUSparseMap] = strings.Join(sparseMap, ",")
} }
return extHdrs, nil return paxHdrs, nil
} }
// readHeader reads the next block header and assumes that the underlying reader // readHeader reads the next block header and assumes that the underlying reader
@ -570,17 +568,17 @@ func readGNUSparseMap1x0(r io.Reader) (sparseDatas, error) {
// readGNUSparseMap0x1 reads the sparse map as stored in GNU's PAX sparse format // 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. // version 0.1. The sparse map is stored in the PAX headers.
func readGNUSparseMap0x1(extHdrs map[string]string) (sparseDatas, error) { func readGNUSparseMap0x1(paxHdrs map[string]string) (sparseDatas, error) {
// Get number of entries. // Get number of entries.
// Use integer overflow resistant math to check this. // Use integer overflow resistant math to check this.
numEntriesStr := extHdrs[paxGNUSparseNumBlocks] numEntriesStr := paxHdrs[paxGNUSparseNumBlocks]
numEntries, err := strconv.ParseInt(numEntriesStr, 10, 0) // Intentionally parse as native int numEntries, err := strconv.ParseInt(numEntriesStr, 10, 0) // Intentionally parse as native int
if err != nil || numEntries < 0 || int(2*numEntries) < int(numEntries) { if err != nil || numEntries < 0 || int(2*numEntries) < int(numEntries) {
return nil, ErrHeader return nil, ErrHeader
} }
// There should be two numbers in sparseMap for each entry. // There should be two numbers in sparseMap for each entry.
sparseMap := strings.Split(extHdrs[paxGNUSparseMap], ",") sparseMap := strings.Split(paxHdrs[paxGNUSparseMap], ",")
if len(sparseMap) == 1 && sparseMap[0] == "" { if len(sparseMap) == 1 && sparseMap[0] == "" {
sparseMap = sparseMap[:0] sparseMap = sparseMap[:0]
} }

View File

@ -118,6 +118,11 @@ func TestReader(t *testing.T) {
{172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1}, {172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1},
{184, 1}, {186, 1}, {188, 1}, {190, 10}, {184, 1}, {186, 1}, {188, 1}, {190, 10},
}, },
PAXRecords: map[string]string{
"GNU.sparse.size": "200",
"GNU.sparse.numblocks": "95",
"GNU.sparse.map": "1,1,3,1,5,1,7,1,9,1,11,1,13,1,15,1,17,1,19,1,21,1,23,1,25,1,27,1,29,1,31,1,33,1,35,1,37,1,39,1,41,1,43,1,45,1,47,1,49,1,51,1,53,1,55,1,57,1,59,1,61,1,63,1,65,1,67,1,69,1,71,1,73,1,75,1,77,1,79,1,81,1,83,1,85,1,87,1,89,1,91,1,93,1,95,1,97,1,99,1,101,1,103,1,105,1,107,1,109,1,111,1,113,1,115,1,117,1,119,1,121,1,123,1,125,1,127,1,129,1,131,1,133,1,135,1,137,1,139,1,141,1,143,1,145,1,147,1,149,1,151,1,153,1,155,1,157,1,159,1,161,1,163,1,165,1,167,1,169,1,171,1,173,1,175,1,177,1,179,1,181,1,183,1,185,1,187,1,189,1",
},
Format: FormatPAX, Format: FormatPAX,
}, { }, {
Name: "sparse-posix-0.1", Name: "sparse-posix-0.1",
@ -149,6 +154,12 @@ func TestReader(t *testing.T) {
{172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1}, {172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1},
{184, 1}, {186, 1}, {188, 1}, {190, 10}, {184, 1}, {186, 1}, {188, 1}, {190, 10},
}, },
PAXRecords: map[string]string{
"GNU.sparse.size": "200",
"GNU.sparse.numblocks": "95",
"GNU.sparse.map": "1,1,3,1,5,1,7,1,9,1,11,1,13,1,15,1,17,1,19,1,21,1,23,1,25,1,27,1,29,1,31,1,33,1,35,1,37,1,39,1,41,1,43,1,45,1,47,1,49,1,51,1,53,1,55,1,57,1,59,1,61,1,63,1,65,1,67,1,69,1,71,1,73,1,75,1,77,1,79,1,81,1,83,1,85,1,87,1,89,1,91,1,93,1,95,1,97,1,99,1,101,1,103,1,105,1,107,1,109,1,111,1,113,1,115,1,117,1,119,1,121,1,123,1,125,1,127,1,129,1,131,1,133,1,135,1,137,1,139,1,141,1,143,1,145,1,147,1,149,1,151,1,153,1,155,1,157,1,159,1,161,1,163,1,165,1,167,1,169,1,171,1,173,1,175,1,177,1,179,1,181,1,183,1,185,1,187,1,189,1",
"GNU.sparse.name": "sparse-posix-0.1",
},
Format: FormatPAX, Format: FormatPAX,
}, { }, {
Name: "sparse-posix-1.0", Name: "sparse-posix-1.0",
@ -180,6 +191,12 @@ func TestReader(t *testing.T) {
{172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1}, {172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1},
{184, 1}, {186, 1}, {188, 1}, {190, 10}, {184, 1}, {186, 1}, {188, 1}, {190, 10},
}, },
PAXRecords: map[string]string{
"GNU.sparse.major": "1",
"GNU.sparse.minor": "0",
"GNU.sparse.realsize": "200",
"GNU.sparse.name": "sparse-posix-1.0",
},
Format: FormatPAX, Format: FormatPAX,
}, { }, {
Name: "end", Name: "end",
@ -263,7 +280,13 @@ func TestReader(t *testing.T) {
ChangeTime: time.Unix(1350244992, 23960108), ChangeTime: time.Unix(1350244992, 23960108),
AccessTime: time.Unix(1350244992, 23960108), AccessTime: time.Unix(1350244992, 23960108),
Typeflag: TypeReg, Typeflag: TypeReg,
Format: FormatPAX, PAXRecords: map[string]string{
"path": "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
"mtime": "1350244992.023960108",
"atime": "1350244992.023960108",
"ctime": "1350244992.023960108",
},
Format: FormatPAX,
}, { }, {
Name: "a/b", Name: "a/b",
Mode: 0777, Mode: 0777,
@ -277,7 +300,13 @@ func TestReader(t *testing.T) {
AccessTime: time.Unix(1350266320, 910238425), AccessTime: time.Unix(1350266320, 910238425),
Typeflag: TypeSymlink, Typeflag: TypeSymlink,
Linkname: "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100", Linkname: "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
Format: FormatPAX, PAXRecords: map[string]string{
"linkpath": "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
"mtime": "1350266320.910238425",
"atime": "1350266320.910238425",
"ctime": "1350266320.910238425",
},
Format: FormatPAX,
}}, }},
}, { }, {
file: "testdata/pax-bad-hdr-file.tar", file: "testdata/pax-bad-hdr-file.tar",
@ -297,11 +326,28 @@ func TestReader(t *testing.T) {
Typeflag: '0', Typeflag: '0',
Uname: "joetsai", Uname: "joetsai",
Gname: "eng", Gname: "eng",
Format: FormatPAX, PAXRecords: map[string]string{
"size": "000000000000000000000999",
},
Format: FormatPAX,
}}, }},
chksums: []string{ chksums: []string{
"0afb597b283fe61b5d4879669a350556", "0afb597b283fe61b5d4879669a350556",
}, },
}, {
file: "testdata/pax-records.tar",
headers: []*Header{{
Typeflag: TypeReg,
Name: "file",
Uname: strings.Repeat("long", 10),
ModTime: time.Unix(0, 0),
PAXRecords: map[string]string{
"GOLANG.pkg": "tar",
"comment": "Hello, 世界",
"uname": strings.Repeat("long", 10),
},
Format: FormatPAX,
}},
}, { }, {
file: "testdata/nil-uid.tar", // golang.org/issue/5290 file: "testdata/nil-uid.tar", // golang.org/issue/5290
headers: []*Header{{ headers: []*Header{{
@ -339,6 +385,14 @@ func TestReader(t *testing.T) {
// Interestingly, selinux encodes the terminating null inside the xattr // Interestingly, selinux encodes the terminating null inside the xattr
"security.selinux": "unconfined_u:object_r:default_t:s0\x00", "security.selinux": "unconfined_u:object_r:default_t:s0\x00",
}, },
PAXRecords: map[string]string{
"mtime": "1386065770.44825232",
"atime": "1389782991.41987522",
"ctime": "1389782956.794414986",
"SCHILY.xattr.user.key": "value",
"SCHILY.xattr.user.key2": "value2",
"SCHILY.xattr.security.selinux": "unconfined_u:object_r:default_t:s0\x00",
},
Format: FormatPAX, Format: FormatPAX,
}, { }, {
Name: "small2.txt", Name: "small2.txt",
@ -355,6 +409,12 @@ func TestReader(t *testing.T) {
Xattrs: map[string]string{ Xattrs: map[string]string{
"security.selinux": "unconfined_u:object_r:default_t:s0\x00", "security.selinux": "unconfined_u:object_r:default_t:s0\x00",
}, },
PAXRecords: map[string]string{
"mtime": "1386065770.449252304",
"atime": "1389782991.41987522",
"ctime": "1386065770.449252304",
"SCHILY.xattr.security.selinux": "unconfined_u:object_r:default_t:s0\x00",
},
Format: FormatPAX, Format: FormatPAX,
}}, }},
}, { }, {
@ -421,7 +481,10 @@ func TestReader(t *testing.T) {
Linkname: "PAX4/PAX4/long-linkpath-name", Linkname: "PAX4/PAX4/long-linkpath-name",
ModTime: time.Unix(0, 0), ModTime: time.Unix(0, 0),
Typeflag: '2', Typeflag: '2',
Format: FormatPAX, PAXRecords: map[string]string{
"linkpath": "PAX4/PAX4/long-linkpath-name",
},
Format: FormatPAX,
}}, }},
}, { }, {
// Both BSD and GNU tar truncate long names at first NUL even // Both BSD and GNU tar truncate long names at first NUL even
@ -551,7 +614,14 @@ func TestReader(t *testing.T) {
Size: 1000, Size: 1000,
ModTime: time.Unix(0, 0), ModTime: time.Unix(0, 0),
SparseHoles: []SparseEntry{{Offset: 1000, Length: 0}}, SparseHoles: []SparseEntry{{Offset: 1000, Length: 0}},
Format: FormatPAX, PAXRecords: map[string]string{
"size": "1512",
"GNU.sparse.major": "1",
"GNU.sparse.minor": "0",
"GNU.sparse.realsize": "1000",
"GNU.sparse.name": "sparse.db",
},
Format: FormatPAX,
}}, }},
}, { }, {
// Generated by Go, works on BSD tar v3.1.2 and GNU tar v.1.27.1. // Generated by Go, works on BSD tar v3.1.2 and GNU tar v.1.27.1.
@ -562,7 +632,14 @@ func TestReader(t *testing.T) {
Size: 1000, Size: 1000,
ModTime: time.Unix(0, 0), ModTime: time.Unix(0, 0),
SparseHoles: []SparseEntry{{Offset: 0, Length: 1000}}, SparseHoles: []SparseEntry{{Offset: 0, Length: 1000}},
Format: FormatPAX, PAXRecords: map[string]string{
"size": "512",
"GNU.sparse.major": "1",
"GNU.sparse.minor": "0",
"GNU.sparse.realsize": "1000",
"GNU.sparse.name": "sparse.db",
},
Format: FormatPAX,
}}, }},
}} }}
@ -908,6 +985,8 @@ func TestMergePAX(t *testing.T) {
for i, v := range vectors { for i, v := range vectors {
got := new(Header) got := new(Header)
err := mergePAX(got, v.in) err := mergePAX(got, v.in)
// TODO(dsnet): Test more combinations with global record support.
got.PAXRecords = nil
if v.ok && !reflect.DeepEqual(*got, *v.want) { if v.ok && !reflect.DeepEqual(*got, *v.want) {
t.Errorf("test %d, mergePAX(...):\ngot %+v\nwant %+v", i, *got, *v.want) t.Errorf("test %d, mergePAX(...):\ngot %+v\nwant %+v", i, *got, *v.want)
} }
@ -1253,9 +1332,10 @@ func TestReadGNUSparsePAXHeaders(t *testing.T) {
for i, v := range vectors { for i, v := range vectors {
var hdr Header var hdr Header
hdr.PAXRecords = v.inputHdrs
r := strings.NewReader(v.inputData + "#") // Add canary byte r := strings.NewReader(v.inputData + "#") // Add canary byte
tr := Reader{curr: &regFileReader{r, int64(r.Len())}} tr := Reader{curr: &regFileReader{r, int64(r.Len())}}
got, err := tr.readGNUSparsePAXHeaders(&hdr, v.inputHdrs) got, err := tr.readGNUSparsePAXHeaders(&hdr)
if !equalSparseEntries(got, v.wantMap) { if !equalSparseEntries(got, v.wantMap) {
t.Errorf("test %d, readGNUSparsePAXHeaders(): got %v, want %v", i, got, v.wantMap) t.Errorf("test %d, readGNUSparsePAXHeaders(): got %v, want %v", i, got, v.wantMap)
} }

View File

@ -413,7 +413,7 @@ func TestFormatPAXRecord(t *testing.T) {
{"xhello", "\x00world", "17 xhello=\x00world\n", true}, {"xhello", "\x00world", "17 xhello=\x00world\n", true},
{"path", "null\x00", "", false}, {"path", "null\x00", "", false},
{"null\x00", "value", "", false}, {"null\x00", "value", "", false},
{paxXattr + "key", "null\x00", "26 SCHILY.xattr.key=null\x00\n", true}, {paxSchilyXattr + "key", "null\x00", "26 SCHILY.xattr.key=null\x00\n", true},
} }
for _, v := range vectors { for _, v := range vectors {

View File

@ -221,16 +221,12 @@ func TestRoundTrip(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
tw := NewWriter(&b) tw := NewWriter(&b)
hdr := &Header{ hdr := &Header{
Name: "file.txt", Name: "file.txt",
Uid: 1 << 21, // too big for 8 octal digits Uid: 1 << 21, // Too big for 8 octal digits
Size: int64(len(data)), Size: int64(len(data)),
// AddDate to strip monotonic clock reading, ModTime: time.Now().Round(time.Second),
// and Round to discard sub-second precision, PAXRecords: map[string]string{"uid": "2097152"},
// both of which are not included in the tar header Format: FormatPAX,
// and would otherwise break the round-trip check
// below.
ModTime: time.Now().AddDate(0, 0, 0).Round(1 * time.Second),
Format: FormatPAX,
} }
if err := tw.WriteHeader(hdr); err != nil { if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("tw.WriteHeader: %v", err) t.Fatalf("tw.WriteHeader: %v", err)
@ -548,15 +544,15 @@ func TestHeaderAllowedFormats(t *testing.T) {
formats: FormatUSTAR | FormatPAX | FormatGNU, formats: FormatUSTAR | FormatPAX | FormatGNU,
}, { }, {
header: &Header{Xattrs: map[string]string{"foo": "bar"}}, header: &Header{Xattrs: map[string]string{"foo": "bar"}},
paxHdrs: map[string]string{paxXattr + "foo": "bar"}, paxHdrs: map[string]string{paxSchilyXattr + "foo": "bar"},
formats: FormatPAX, formats: FormatPAX,
}, { }, {
header: &Header{Xattrs: map[string]string{"foo": "bar"}, Format: FormatGNU}, header: &Header{Xattrs: map[string]string{"foo": "bar"}, Format: FormatGNU},
paxHdrs: map[string]string{paxXattr + "foo": "bar"}, paxHdrs: map[string]string{paxSchilyXattr + "foo": "bar"},
formats: FormatUnknown, formats: FormatUnknown,
}, { }, {
header: &Header{Xattrs: map[string]string{"用戶名": "\x00hello"}}, header: &Header{Xattrs: map[string]string{"用戶名": "\x00hello"}},
paxHdrs: map[string]string{paxXattr + "用戶名": "\x00hello"}, paxHdrs: map[string]string{paxSchilyXattr + "用戶名": "\x00hello"},
formats: FormatPAX, formats: FormatPAX,
}, { }, {
header: &Header{Xattrs: map[string]string{"foo=bar": "baz"}}, header: &Header{Xattrs: map[string]string{"foo=bar": "baz"}},

BIN
src/archive/tar/testdata/pax-records.tar vendored Normal file

Binary file not shown.

View File

@ -231,6 +231,22 @@ func TestWriter(t *testing.T) {
Name: "null\x00.txt", Name: "null\x00.txt",
}, headerError{}}, }, headerError{}},
}, },
}, {
file: "testdata/pax-records.tar",
tests: []testFnc{
testHeader{Header{
Typeflag: TypeReg,
Name: "file",
Uname: strings.Repeat("long", 10),
PAXRecords: map[string]string{
"path": "FILE", // Should be ignored
"GNU.sparse.map": "0,0", // Should be ignored
"comment": "Hello, 世界",
"GOLANG.pkg": "tar",
},
}, nil},
testClose{nil},
},
}, { }, {
file: "testdata/gnu-utf8.tar", file: "testdata/gnu-utf8.tar",
tests: []testFnc{ tests: []testFnc{