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:
parent
4d6da86469
commit
a795ca51db
@ -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)
|
||||||
|
@ -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]
|
||||||
}
|
}
|
||||||
|
@ -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: ®FileReader{r, int64(r.Len())}}
|
tr := Reader{curr: ®FileReader{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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
BIN
src/archive/tar/testdata/pax-records.tar
vendored
Normal file
Binary file not shown.
@ -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{
|
||||||
|
Loading…
Reference in New Issue
Block a user