]> Cypherpunks repositories - gostls13.git/commitdiff
archive/tar: support arbitrary PAX records
authorJoe Tsai <joetsai@digital-static.net>
Thu, 24 Aug 2017 06:44:33 +0000 (23:44 -0700)
committerJoe Tsai <thebrokentoaster@gmail.com>
Fri, 25 Aug 2017 21:57:32 +0000 (21:57 +0000)
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>
src/archive/tar/common.go
src/archive/tar/reader.go
src/archive/tar/reader_test.go
src/archive/tar/strconv_test.go
src/archive/tar/tar_test.go
src/archive/tar/testdata/pax-records.tar [new file with mode: 0644]
src/archive/tar/writer_test.go

index e9a3499a64d0c46e6805d1dcd24a255c01bc6852..dfea49bd899e07de30b6ef88d512d186db0bdb64 100644 (file)
@@ -66,8 +66,53 @@ const (
        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.
 // 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 {
        Name       string    // name of header file entry
        Mode       int64     // permission and mode bits
@@ -83,7 +128,6 @@ type Header struct {
        Devminor   int64     // minor number of character or block device
        AccessTime time.Time // access time
        ChangeTime time.Time // status change time
-       Xattrs     map[string]string
 
        // 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.
        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.
        //
        // 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.
        if len(h.Xattrs) > 0 {
                for k, v := range h.Xattrs {
-                       paxHdrs[paxXattr+k] = v
+                       paxHdrs[paxSchilyXattr+k] = v
                }
                whyOnlyPAX = "only PAX supports Xattrs"
                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 {
                // Forbid empty values (which represent deletion) since usage of
                // them are non-sensible without global PAX record support.
@@ -497,35 +577,6 @@ const (
        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.
 // If fi describes a symlink, FileInfoHeader records link as the link target.
 // 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 {
                        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 {
                return h, sysStat(fi, h)
index 87732eca6d4633354906d04653aa759cd52dc3e9..e4e694e0b8dfb581e66b61b65358bbf9380c244f 100644 (file)
@@ -4,9 +4,6 @@
 
 package tar
 
-// TODO(dsymonds):
-//   - pax extensions
-
 import (
        "bytes"
        "io"
@@ -57,7 +54,8 @@ 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
        // files. Internally, the tar format often uses fake "files" to add meta
@@ -89,7 +87,7 @@ loop:
                switch hdr.Typeflag {
                case TypeXHeader:
                        format.mayOnlyBe(FormatPAX)
-                       extHdrs, err = parsePAX(tr)
+                       paxHdrs, err = parsePAX(tr)
                        if err != nil {
                                return nil, err
                        }
@@ -101,28 +99,27 @@ loop:
                                return nil, err
                        }
 
-                       // Convert GNU extensions to use PAX headers.
-                       if extHdrs == nil {
-                               extHdrs = make(map[string]string)
-                       }
                        var p parser
                        switch hdr.Typeflag {
                        case TypeGNULongName:
-                               extHdrs[paxPath] = p.parseString(realname)
+                               gnuLongName = p.parseString(realname)
                        case TypeGNULongLink:
-                               extHdrs[paxLinkpath] = p.parseString(realname)
-                       }
-                       if p.err != nil {
-                               return nil, p.err
+                               gnuLongLink = p.parseString(realname)
                        }
                        continue loop // This is a meta header affecting the next header
                default:
                        // The old GNU sparse format is handled here since it is technically
                        // just a regular file with additional attributes.
 
-                       if err := mergePAX(hdr, extHdrs); err != nil {
+                       if err := mergePAX(hdr, paxHdrs); err != nil {
                                return nil, err
                        }
+                       if gnuLongName != "" {
+                               hdr.Name = gnuLongName
+                       }
+                       if gnuLongLink != "" {
+                               hdr.Linkname = gnuLongLink
+                       }
 
                        // The extended headers may have updated the size.
                        // 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
                        // 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
                        }
 
@@ -165,13 +162,13 @@ func (tr *Reader) handleRegularFile(hdr *Header) error {
 
 // handleSparseFile checks if the current file is a sparse format of any type
 // 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 err error
        if hdr.Typeflag == TypeGNUSparse {
                spd, err = tr.readOldGNUSparseMap(hdr, rawHdr)
        } else {
-               spd, err = tr.readGNUSparsePAXHeaders(hdr, extHdrs)
+               spd, err = tr.readGNUSparsePAXHeaders(hdr)
        }
 
        // 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.
 // This assumes that 0.0 headers have already been converted to 0.1 headers
 // 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.
        var is1x0 bool
-       major, minor := extHdrs[paxGNUSparseMajor], extHdrs[paxGNUSparseMinor]
+       major, minor := hdr.PAXRecords[paxGNUSparseMajor], hdr.PAXRecords[paxGNUSparseMinor]
        switch {
        case major == "0" && (minor == "0" || minor == "1"):
                is1x0 = false
@@ -202,7 +199,7 @@ func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header, extHdrs map[string]string
                is1x0 = true
        case major != "" || minor != "":
                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
        default:
                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)
 
        // Update hdr from GNU sparse PAX headers.
-       if name := extHdrs[paxGNUSparseName]; name != "" {
+       if name := hdr.PAXRecords[paxGNUSparseName]; name != "" {
                hdr.Name = name
        }
-       size := extHdrs[paxGNUSparseSize]
+       size := hdr.PAXRecords[paxGNUSparseSize]
        if size == "" {
-               size = extHdrs[paxGNUSparseRealSize]
+               size = hdr.PAXRecords[paxGNUSparseRealSize]
        }
        if size != "" {
                n, err := strconv.ParseInt(size, 10, 64)
@@ -229,7 +226,7 @@ func (tr *Reader) readGNUSparsePAXHeaders(hdr *Header, extHdrs map[string]string
        if is1x0 {
                return readGNUSparseMap1x0(tr.curr)
        } else {
-               return readGNUSparseMap0x1(extHdrs)
+               return readGNUSparseMap0x1(hdr.PAXRecords)
        }
 }
 
@@ -265,17 +262,18 @@ func mergePAX(hdr *Header, headers map[string]string) (err error) {
                case paxSize:
                        hdr.Size, err = strconv.ParseInt(v, 10, 64)
                default:
-                       if strings.HasPrefix(k, paxXattr) {
+                       if strings.HasPrefix(k, paxSchilyXattr) {
                                if hdr.Xattrs == nil {
                                        hdr.Xattrs = make(map[string]string)
                                }
-                               hdr.Xattrs[k[len(paxXattr):]] = v
+                               hdr.Xattrs[k[len(paxSchilyXattr):]] = v
                        }
                }
                if err != nil {
                        return ErrHeader
                }
        }
+       hdr.PAXRecords = headers
        return nil
 }
 
@@ -293,7 +291,7 @@ func parsePAX(r io.Reader) (map[string]string, error) {
        // headers since 0.0 headers were not PAX compliant.
        var sparseMap []string
 
-       extHdrs := make(map[string]string)
+       paxHdrs := make(map[string]string)
        for len(sbuf) > 0 {
                key, value, residual, err := parsePAXRecord(sbuf)
                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
                        // non-empty. Otherwise, the key is deleted.
                        if len(value) > 0 {
-                               extHdrs[key] = value
+                               paxHdrs[key] = value
                        } else {
-                               delete(extHdrs, key)
+                               delete(paxHdrs, key)
                        }
                }
        }
        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
@@ -570,17 +568,17 @@ func readGNUSparseMap1x0(r io.Reader) (sparseDatas, error) {
 
 // readGNUSparseMap0x1 reads the sparse map as stored in GNU's PAX sparse format
 // version 0.1. The sparse map is stored in the PAX headers.
-func readGNUSparseMap0x1(extHdrs map[string]string) (sparseDatas, error) {
+func readGNUSparseMap0x1(paxHdrs map[string]string) (sparseDatas, error) {
        // Get number of entries.
        // 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
        if err != nil || numEntries < 0 || int(2*numEntries) < int(numEntries) {
                return nil, ErrHeader
        }
 
        // 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] == "" {
                sparseMap = sparseMap[:0]
        }
index c764baf39e422a396bf35fc0161306dbff050f74..bb0b2b112a6fca726ed05c7f3f46e09b09cd7f1a 100644 (file)
@@ -118,6 +118,11 @@ func TestReader(t *testing.T) {
                                {172, 1}, {174, 1}, {176, 1}, {178, 1}, {180, 1}, {182, 1},
                                {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,
                }, {
                        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},
                                {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,
                }, {
                        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},
                                {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,
                }, {
                        Name:     "end",
@@ -263,7 +280,13 @@ func TestReader(t *testing.T) {
                        ChangeTime: time.Unix(1350244992, 23960108),
                        AccessTime: time.Unix(1350244992, 23960108),
                        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",
                        Mode:       0777,
@@ -277,7 +300,13 @@ func TestReader(t *testing.T) {
                        AccessTime: time.Unix(1350266320, 910238425),
                        Typeflag:   TypeSymlink,
                        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",
@@ -297,11 +326,28 @@ func TestReader(t *testing.T) {
                        Typeflag: '0',
                        Uname:    "joetsai",
                        Gname:    "eng",
-                       Format:   FormatPAX,
+                       PAXRecords: map[string]string{
+                               "size": "000000000000000000000999",
+                       },
+                       Format: FormatPAX,
                }},
                chksums: []string{
                        "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
                headers: []*Header{{
@@ -339,6 +385,14 @@ func TestReader(t *testing.T) {
                                // Interestingly, selinux encodes the terminating null inside the xattr
                                "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,
                }, {
                        Name:       "small2.txt",
@@ -355,6 +409,12 @@ func TestReader(t *testing.T) {
                        Xattrs: map[string]string{
                                "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,
                }},
        }, {
@@ -421,7 +481,10 @@ func TestReader(t *testing.T) {
                        Linkname: "PAX4/PAX4/long-linkpath-name",
                        ModTime:  time.Unix(0, 0),
                        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
@@ -551,7 +614,14 @@ func TestReader(t *testing.T) {
                        Size:        1000,
                        ModTime:     time.Unix(0, 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.
@@ -562,7 +632,14 @@ func TestReader(t *testing.T) {
                        Size:        1000,
                        ModTime:     time.Unix(0, 0),
                        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 {
                got := new(Header)
                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) {
                        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 {
                var hdr Header
+               hdr.PAXRecords = v.inputHdrs
                r := strings.NewReader(v.inputData + "#") // Add canary byte
                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) {
                        t.Errorf("test %d, readGNUSparsePAXHeaders(): got %v, want %v", i, got, v.wantMap)
                }
index 7156368ede5884b7b9f5e9b51acf7e8a8b00b23d..4cc388cb0f269b88268c3f283f09d86a966dc626 100644 (file)
@@ -413,7 +413,7 @@ func TestFormatPAXRecord(t *testing.T) {
                {"xhello", "\x00world", "17 xhello=\x00world\n", true},
                {"path", "null\x00", "", 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 {
index abbf9615e3e1aaa0fba729acf6bf12229c08aa1d..93196e9126faad884cfad42470fa944a56343a89 100644 (file)
@@ -221,16 +221,12 @@ func TestRoundTrip(t *testing.T) {
        var b bytes.Buffer
        tw := NewWriter(&b)
        hdr := &Header{
-               Name: "file.txt",
-               Uid:  1 << 21, // too big for 8 octal digits
-               Size: int64(len(data)),
-               // AddDate to strip monotonic clock reading,
-               // and Round to discard sub-second precision,
-               // both of which are not included in the tar header
-               // and would otherwise break the round-trip check
-               // below.
-               ModTime: time.Now().AddDate(0, 0, 0).Round(1 * time.Second),
-               Format:  FormatPAX,
+               Name:       "file.txt",
+               Uid:        1 << 21, // Too big for 8 octal digits
+               Size:       int64(len(data)),
+               ModTime:    time.Now().Round(time.Second),
+               PAXRecords: map[string]string{"uid": "2097152"},
+               Format:     FormatPAX,
        }
        if err := tw.WriteHeader(hdr); err != nil {
                t.Fatalf("tw.WriteHeader: %v", err)
@@ -548,15 +544,15 @@ func TestHeaderAllowedFormats(t *testing.T) {
                formats: FormatUSTAR | FormatPAX | FormatGNU,
        }, {
                header:  &Header{Xattrs: map[string]string{"foo": "bar"}},
-               paxHdrs: map[string]string{paxXattr + "foo": "bar"},
+               paxHdrs: map[string]string{paxSchilyXattr + "foo": "bar"},
                formats: FormatPAX,
        }, {
                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,
        }, {
                header:  &Header{Xattrs: map[string]string{"用戶名": "\x00hello"}},
-               paxHdrs: map[string]string{paxXattr + "用戶名": "\x00hello"},
+               paxHdrs: map[string]string{paxSchilyXattr + "用戶名": "\x00hello"},
                formats: FormatPAX,
        }, {
                header:  &Header{Xattrs: map[string]string{"foo=bar": "baz"}},
diff --git a/src/archive/tar/testdata/pax-records.tar b/src/archive/tar/testdata/pax-records.tar
new file mode 100644 (file)
index 0000000..276c211
Binary files /dev/null and b/src/archive/tar/testdata/pax-records.tar differ
index 1d62055391d861d569b0406da8b1246b3e393660..36308f25101ff7dd60b0a74a1b63a777f81ccf51 100644 (file)
@@ -231,6 +231,22 @@ func TestWriter(t *testing.T) {
                                Name:     "null\x00.txt",
                        }, 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",
                tests: []testFnc{