mirror of
https://github.com/vbatts/tar-split.git
synced 2024-11-26 09:45:39 +00:00
Vincent Batts
d97b8009bb
NOTE: I'm not sure this is really the route I want to go here, but it would need benchmarking to show if it's actually beneficial. It would still be nicer to get something like this upstreamed instead. trim down anything not used directly by tar-split. Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
747 lines
24 KiB
Go
747 lines
24 KiB
Go
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package tar implements access to tar archives.
|
|
//
|
|
// Tape archives (tar) are a file format for storing a sequence of files that
|
|
// can be read and written in a streaming manner.
|
|
// This package aims to cover most variations of the format,
|
|
// including those produced by GNU and BSD tar tools.
|
|
package tar
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// BUG: Use of the Uid and Gid fields in Header could overflow on 32-bit
|
|
// architectures. If a large value is encountered when decoding, the result
|
|
// stored in Header will be the truncated version.
|
|
|
|
var (
|
|
ErrHeader = errors.New("archive/tar: invalid tar header")
|
|
ErrWriteTooLong = errors.New("archive/tar: write too long")
|
|
ErrFieldTooLong = errors.New("archive/tar: header field too long")
|
|
ErrWriteAfterClose = errors.New("archive/tar: write after close")
|
|
errMissData = errors.New("archive/tar: sparse file references non-existent data")
|
|
errUnrefData = errors.New("archive/tar: sparse file contains unreferenced data")
|
|
errWriteHole = errors.New("archive/tar: write non-NUL byte in sparse hole")
|
|
)
|
|
|
|
type headerError []string
|
|
|
|
func (he headerError) Error() string {
|
|
const prefix = "archive/tar: cannot encode header"
|
|
var ss []string
|
|
for _, s := range he {
|
|
if s != "" {
|
|
ss = append(ss, s)
|
|
}
|
|
}
|
|
if len(ss) == 0 {
|
|
return prefix
|
|
}
|
|
return fmt.Sprintf("%s: %v", prefix, strings.Join(ss, "; and "))
|
|
}
|
|
|
|
// Type flags for Header.Typeflag.
|
|
const (
|
|
// Type '0' indicates a regular file.
|
|
TypeReg = '0'
|
|
TypeRegA = '\x00' // Deprecated: Use TypeReg instead.
|
|
|
|
// Type '1' to '6' are header-only flags and may not have a data body.
|
|
TypeLink = '1' // Hard link
|
|
TypeSymlink = '2' // Symbolic link
|
|
TypeChar = '3' // Character device node
|
|
TypeBlock = '4' // Block device node
|
|
TypeDir = '5' // Directory
|
|
TypeFifo = '6' // FIFO node
|
|
|
|
// Type '7' is reserved.
|
|
TypeCont = '7'
|
|
|
|
// Type 'x' is used by the PAX format to store key-value records that
|
|
// are only relevant to the next file.
|
|
// This package transparently handles these types.
|
|
TypeXHeader = 'x'
|
|
|
|
// Type 'g' is used by the PAX format to store key-value records that
|
|
// are relevant to all subsequent files.
|
|
// This package only supports parsing and composing such headers,
|
|
// but does not currently support persisting the global state across files.
|
|
TypeXGlobalHeader = 'g'
|
|
|
|
// Type 'S' indicates a sparse file in the GNU format.
|
|
TypeGNUSparse = 'S'
|
|
|
|
// Types 'L' and 'K' are used by the GNU format for a meta file
|
|
// used to store the path or link name for the next file.
|
|
// This package transparently handles these types.
|
|
TypeGNULongName = 'L'
|
|
TypeGNULongLink = 'K'
|
|
)
|
|
|
|
// 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 {
|
|
// Typeflag is the type of header entry.
|
|
// The zero value is automatically promoted to either TypeReg or TypeDir
|
|
// depending on the presence of a trailing slash in Name.
|
|
Typeflag byte
|
|
|
|
Name string // Name of file entry
|
|
Linkname string // Target name of link (valid for TypeLink or TypeSymlink)
|
|
|
|
Size int64 // Logical file size in bytes
|
|
Mode int64 // Permission and mode bits
|
|
Uid int // User ID of owner
|
|
Gid int // Group ID of owner
|
|
Uname string // User name of owner
|
|
Gname string // Group name of owner
|
|
|
|
// If the Format is unspecified, then Writer.WriteHeader rounds ModTime
|
|
// to the nearest second and ignores the AccessTime and ChangeTime fields.
|
|
//
|
|
// To use AccessTime or ChangeTime, specify the Format as PAX or GNU.
|
|
// To use sub-second resolution, specify the Format as PAX.
|
|
ModTime time.Time // Modification time
|
|
AccessTime time.Time // Access time (requires either PAX or GNU support)
|
|
ChangeTime time.Time // Change time (requires either PAX or GNU support)
|
|
|
|
Devmajor int64 // Major device number (valid for TypeChar or TypeBlock)
|
|
Devminor int64 // Minor device number (valid for TypeChar or TypeBlock)
|
|
|
|
// 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
|
|
// 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.
|
|
// Since the Reader liberally reads some non-compliant files,
|
|
// it is possible for this to be FormatUnknown.
|
|
//
|
|
// If the format is unspecified when Writer.WriteHeader is called,
|
|
// then it uses the first format (in the order of USTAR, PAX, GNU)
|
|
// capable of encoding this Header (see Format).
|
|
Format Format
|
|
}
|
|
|
|
// sparseEntry represents a Length-sized fragment at Offset in the file.
|
|
type sparseEntry struct{ Offset, Length int64 }
|
|
|
|
func (s sparseEntry) endOffset() int64 { return s.Offset + s.Length }
|
|
|
|
// A sparse file can be represented as either a sparseDatas or a sparseHoles.
|
|
// As long as the total size is known, they are equivalent and one can be
|
|
// converted to the other form and back. The various tar formats with sparse
|
|
// file support represent sparse files in the sparseDatas form. That is, they
|
|
// specify the fragments in the file that has data, and treat everything else as
|
|
// having zero bytes. As such, the encoding and decoding logic in this package
|
|
// deals with sparseDatas.
|
|
//
|
|
// However, the external API uses sparseHoles instead of sparseDatas because the
|
|
// zero value of sparseHoles logically represents a normal file (i.e., there are
|
|
// no holes in it). On the other hand, the zero value of sparseDatas implies
|
|
// that the file has no data in it, which is rather odd.
|
|
//
|
|
// As an example, if the underlying raw file contains the 10-byte data:
|
|
//
|
|
// var compactFile = "abcdefgh"
|
|
//
|
|
// And the sparse map has the following entries:
|
|
//
|
|
// var spd sparseDatas = []sparseEntry{
|
|
// {Offset: 2, Length: 5}, // Data fragment for 2..6
|
|
// {Offset: 18, Length: 3}, // Data fragment for 18..20
|
|
// }
|
|
// var sph sparseHoles = []sparseEntry{
|
|
// {Offset: 0, Length: 2}, // Hole fragment for 0..1
|
|
// {Offset: 7, Length: 11}, // Hole fragment for 7..17
|
|
// {Offset: 21, Length: 4}, // Hole fragment for 21..24
|
|
// }
|
|
//
|
|
// Then the content of the resulting sparse file with a Header.Size of 25 is:
|
|
//
|
|
// var sparseFile = "\x00"*2 + "abcde" + "\x00"*11 + "fgh" + "\x00"*4
|
|
type (
|
|
sparseDatas []sparseEntry
|
|
sparseHoles []sparseEntry
|
|
)
|
|
|
|
// validateSparseEntries reports whether sp is a valid sparse map.
|
|
// It does not matter whether sp represents data fragments or hole fragments.
|
|
func validateSparseEntries(sp []sparseEntry, size int64) bool {
|
|
// Validate all sparse entries. These are the same checks as performed by
|
|
// the BSD tar utility.
|
|
if size < 0 {
|
|
return false
|
|
}
|
|
var pre sparseEntry
|
|
for _, cur := range sp {
|
|
switch {
|
|
case cur.Offset < 0 || cur.Length < 0:
|
|
return false // Negative values are never okay
|
|
case cur.Offset > math.MaxInt64-cur.Length:
|
|
return false // Integer overflow with large length
|
|
case cur.endOffset() > size:
|
|
return false // Region extends beyond the actual size
|
|
case pre.endOffset() > cur.Offset:
|
|
return false // Regions cannot overlap and must be in order
|
|
}
|
|
pre = cur
|
|
}
|
|
return true
|
|
}
|
|
|
|
// alignSparseEntries mutates src and returns dst where each fragment's
|
|
// starting offset is aligned up to the nearest block edge, and each
|
|
// ending offset is aligned down to the nearest block edge.
|
|
//
|
|
// Even though the Go tar Reader and the BSD tar utility can handle entries
|
|
// with arbitrary offsets and lengths, the GNU tar utility can only handle
|
|
// offsets and lengths that are multiples of blockSize.
|
|
func alignSparseEntries(src []sparseEntry, size int64) []sparseEntry {
|
|
dst := src[:0]
|
|
for _, s := range src {
|
|
pos, end := s.Offset, s.endOffset()
|
|
pos += blockPadding(+pos) // Round-up to nearest blockSize
|
|
if end != size {
|
|
end -= blockPadding(-end) // Round-down to nearest blockSize
|
|
}
|
|
if pos < end {
|
|
dst = append(dst, sparseEntry{Offset: pos, Length: end - pos})
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// invertSparseEntries converts a sparse map from one form to the other.
|
|
// If the input is sparseHoles, then it will output sparseDatas and vice-versa.
|
|
// The input must have been already validated.
|
|
//
|
|
// This function mutates src and returns a normalized map where:
|
|
// - adjacent fragments are coalesced together
|
|
// - only the last fragment may be empty
|
|
// - the endOffset of the last fragment is the total size
|
|
func invertSparseEntries(src []sparseEntry, size int64) []sparseEntry {
|
|
dst := src[:0]
|
|
var pre sparseEntry
|
|
for _, cur := range src {
|
|
if cur.Length == 0 {
|
|
continue // Skip empty fragments
|
|
}
|
|
pre.Length = cur.Offset - pre.Offset
|
|
if pre.Length > 0 {
|
|
dst = append(dst, pre) // Only add non-empty fragments
|
|
}
|
|
pre.Offset = cur.endOffset()
|
|
}
|
|
pre.Length = size - pre.Offset // Possibly the only empty fragment
|
|
return append(dst, pre)
|
|
}
|
|
|
|
// fileState tracks the number of logical (includes sparse holes) and physical
|
|
// (actual in tar archive) bytes remaining for the current file.
|
|
//
|
|
// Invariant: LogicalRemaining >= PhysicalRemaining
|
|
type fileState interface {
|
|
LogicalRemaining() int64
|
|
PhysicalRemaining() int64
|
|
}
|
|
|
|
// allowedFormats determines which formats can be used.
|
|
// The value returned is the logical OR of multiple possible formats.
|
|
// If the value is FormatUnknown, then the input Header cannot be encoded
|
|
// and an error is returned explaining why.
|
|
//
|
|
// As a by-product of checking the fields, this function returns paxHdrs, which
|
|
// contain all fields that could not be directly encoded.
|
|
// A value receiver ensures that this method does not mutate the source Header.
|
|
func (h Header) allowedFormats() (format Format, paxHdrs map[string]string, err error) {
|
|
format = FormatUSTAR | FormatPAX | FormatGNU
|
|
paxHdrs = make(map[string]string)
|
|
|
|
var whyNoUSTAR, whyNoPAX, whyNoGNU string
|
|
var preferPAX bool // Prefer PAX over USTAR
|
|
verifyString := func(s string, size int, name, paxKey string) {
|
|
// NUL-terminator is optional for path and linkpath.
|
|
// Technically, it is required for uname and gname,
|
|
// but neither GNU nor BSD tar checks for it.
|
|
tooLong := len(s) > size
|
|
allowLongGNU := paxKey == paxPath || paxKey == paxLinkpath
|
|
if hasNUL(s) || (tooLong && !allowLongGNU) {
|
|
whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%q", name, s)
|
|
format.mustNotBe(FormatGNU)
|
|
}
|
|
if !isASCII(s) || tooLong {
|
|
canSplitUSTAR := paxKey == paxPath
|
|
if _, _, ok := splitUSTARPath(s); !canSplitUSTAR || !ok {
|
|
whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%q", name, s)
|
|
format.mustNotBe(FormatUSTAR)
|
|
}
|
|
if paxKey == paxNone {
|
|
whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%q", name, s)
|
|
format.mustNotBe(FormatPAX)
|
|
} else {
|
|
paxHdrs[paxKey] = s
|
|
}
|
|
}
|
|
if v, ok := h.PAXRecords[paxKey]; ok && v == s {
|
|
paxHdrs[paxKey] = v
|
|
}
|
|
}
|
|
verifyNumeric := func(n int64, size int, name, paxKey string) {
|
|
if !fitsInBase256(size, n) {
|
|
whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%d", name, n)
|
|
format.mustNotBe(FormatGNU)
|
|
}
|
|
if !fitsInOctal(size, n) {
|
|
whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%d", name, n)
|
|
format.mustNotBe(FormatUSTAR)
|
|
if paxKey == paxNone {
|
|
whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%d", name, n)
|
|
format.mustNotBe(FormatPAX)
|
|
} else {
|
|
paxHdrs[paxKey] = strconv.FormatInt(n, 10)
|
|
}
|
|
}
|
|
if v, ok := h.PAXRecords[paxKey]; ok && v == strconv.FormatInt(n, 10) {
|
|
paxHdrs[paxKey] = v
|
|
}
|
|
}
|
|
verifyTime := func(ts time.Time, size int, name, paxKey string) {
|
|
if ts.IsZero() {
|
|
return // Always okay
|
|
}
|
|
if !fitsInBase256(size, ts.Unix()) {
|
|
whyNoGNU = fmt.Sprintf("GNU cannot encode %s=%v", name, ts)
|
|
format.mustNotBe(FormatGNU)
|
|
}
|
|
isMtime := paxKey == paxMtime
|
|
fitsOctal := fitsInOctal(size, ts.Unix())
|
|
if (isMtime && !fitsOctal) || !isMtime {
|
|
whyNoUSTAR = fmt.Sprintf("USTAR cannot encode %s=%v", name, ts)
|
|
format.mustNotBe(FormatUSTAR)
|
|
}
|
|
needsNano := ts.Nanosecond() != 0
|
|
if !isMtime || !fitsOctal || needsNano {
|
|
preferPAX = true // USTAR may truncate sub-second measurements
|
|
if paxKey == paxNone {
|
|
whyNoPAX = fmt.Sprintf("PAX cannot encode %s=%v", name, ts)
|
|
format.mustNotBe(FormatPAX)
|
|
} else {
|
|
paxHdrs[paxKey] = formatPAXTime(ts)
|
|
}
|
|
}
|
|
if v, ok := h.PAXRecords[paxKey]; ok && v == formatPAXTime(ts) {
|
|
paxHdrs[paxKey] = v
|
|
}
|
|
}
|
|
|
|
// Check basic fields.
|
|
var blk block
|
|
v7 := blk.V7()
|
|
ustar := blk.USTAR()
|
|
gnu := blk.GNU()
|
|
verifyString(h.Name, len(v7.Name()), "Name", paxPath)
|
|
verifyString(h.Linkname, len(v7.LinkName()), "Linkname", paxLinkpath)
|
|
verifyString(h.Uname, len(ustar.UserName()), "Uname", paxUname)
|
|
verifyString(h.Gname, len(ustar.GroupName()), "Gname", paxGname)
|
|
verifyNumeric(h.Mode, len(v7.Mode()), "Mode", paxNone)
|
|
verifyNumeric(int64(h.Uid), len(v7.UID()), "Uid", paxUid)
|
|
verifyNumeric(int64(h.Gid), len(v7.GID()), "Gid", paxGid)
|
|
verifyNumeric(h.Size, len(v7.Size()), "Size", paxSize)
|
|
verifyNumeric(h.Devmajor, len(ustar.DevMajor()), "Devmajor", paxNone)
|
|
verifyNumeric(h.Devminor, len(ustar.DevMinor()), "Devminor", paxNone)
|
|
verifyTime(h.ModTime, len(v7.ModTime()), "ModTime", paxMtime)
|
|
verifyTime(h.AccessTime, len(gnu.AccessTime()), "AccessTime", paxAtime)
|
|
verifyTime(h.ChangeTime, len(gnu.ChangeTime()), "ChangeTime", paxCtime)
|
|
|
|
// Check for header-only types.
|
|
var whyOnlyPAX, whyOnlyGNU string
|
|
switch h.Typeflag {
|
|
case TypeReg, TypeChar, TypeBlock, TypeFifo, TypeGNUSparse:
|
|
// Exclude TypeLink and TypeSymlink, since they may reference directories.
|
|
if strings.HasSuffix(h.Name, "/") {
|
|
return FormatUnknown, nil, headerError{"filename may not have trailing slash"}
|
|
}
|
|
case TypeXHeader, TypeGNULongName, TypeGNULongLink:
|
|
return FormatUnknown, nil, headerError{"cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers"}
|
|
case TypeXGlobalHeader:
|
|
h2 := Header{Name: h.Name, Typeflag: h.Typeflag, Xattrs: h.Xattrs, PAXRecords: h.PAXRecords, Format: h.Format}
|
|
if !reflect.DeepEqual(h, h2) {
|
|
return FormatUnknown, nil, headerError{"only PAXRecords should be set for TypeXGlobalHeader"}
|
|
}
|
|
whyOnlyPAX = "only PAX supports TypeXGlobalHeader"
|
|
format.mayOnlyBe(FormatPAX)
|
|
}
|
|
if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 {
|
|
return FormatUnknown, nil, headerError{"negative size on header-only type"}
|
|
}
|
|
|
|
// Check PAX records.
|
|
if len(h.Xattrs) > 0 {
|
|
for k, v := range h.Xattrs {
|
|
paxHdrs[paxSchilyXattr+k] = v
|
|
}
|
|
whyOnlyPAX = "only PAX supports Xattrs"
|
|
format.mayOnlyBe(FormatPAX)
|
|
}
|
|
if len(h.PAXRecords) > 0 {
|
|
for k, v := range h.PAXRecords {
|
|
switch _, exists := paxHdrs[k]; {
|
|
case exists:
|
|
continue // Do not overwrite existing records
|
|
case h.Typeflag == TypeXGlobalHeader:
|
|
paxHdrs[k] = v // Copy all records
|
|
case !basicKeys[k] && !strings.HasPrefix(k, paxGNUSparse):
|
|
paxHdrs[k] = v // Ignore local records that may conflict
|
|
}
|
|
}
|
|
whyOnlyPAX = "only PAX supports PAXRecords"
|
|
format.mayOnlyBe(FormatPAX)
|
|
}
|
|
for k, v := range paxHdrs {
|
|
if !validPAXRecord(k, v) {
|
|
return FormatUnknown, nil, headerError{fmt.Sprintf("invalid PAX record: %q", k+" = "+v)}
|
|
}
|
|
}
|
|
|
|
// TODO(dsnet): Re-enable this when adding sparse support.
|
|
// See https://golang.org/issue/22735
|
|
/*
|
|
// Check sparse files.
|
|
if len(h.SparseHoles) > 0 || h.Typeflag == TypeGNUSparse {
|
|
if isHeaderOnlyType(h.Typeflag) {
|
|
return FormatUnknown, nil, headerError{"header-only type cannot be sparse"}
|
|
}
|
|
if !validateSparseEntries(h.SparseHoles, h.Size) {
|
|
return FormatUnknown, nil, headerError{"invalid sparse holes"}
|
|
}
|
|
if h.Typeflag == TypeGNUSparse {
|
|
whyOnlyGNU = "only GNU supports TypeGNUSparse"
|
|
format.mayOnlyBe(FormatGNU)
|
|
} else {
|
|
whyNoGNU = "GNU supports sparse files only with TypeGNUSparse"
|
|
format.mustNotBe(FormatGNU)
|
|
}
|
|
whyNoUSTAR = "USTAR does not support sparse files"
|
|
format.mustNotBe(FormatUSTAR)
|
|
}
|
|
*/
|
|
|
|
// Check desired format.
|
|
if wantFormat := h.Format; wantFormat != FormatUnknown {
|
|
if wantFormat.has(FormatPAX) && !preferPAX {
|
|
wantFormat.mayBe(FormatUSTAR) // PAX implies USTAR allowed too
|
|
}
|
|
format.mayOnlyBe(wantFormat) // Set union of formats allowed and format wanted
|
|
}
|
|
if format == FormatUnknown {
|
|
switch h.Format {
|
|
case FormatUSTAR:
|
|
err = headerError{"Format specifies USTAR", whyNoUSTAR, whyOnlyPAX, whyOnlyGNU}
|
|
case FormatPAX:
|
|
err = headerError{"Format specifies PAX", whyNoPAX, whyOnlyGNU}
|
|
case FormatGNU:
|
|
err = headerError{"Format specifies GNU", whyNoGNU, whyOnlyPAX}
|
|
default:
|
|
err = headerError{whyNoUSTAR, whyNoPAX, whyNoGNU, whyOnlyPAX, whyOnlyGNU}
|
|
}
|
|
}
|
|
return format, paxHdrs, err
|
|
}
|
|
|
|
// FileInfo returns an os.FileInfo for the Header.
|
|
func (h *Header) FileInfo() os.FileInfo {
|
|
return headerFileInfo{h}
|
|
}
|
|
|
|
// headerFileInfo implements os.FileInfo.
|
|
type headerFileInfo struct {
|
|
h *Header
|
|
}
|
|
|
|
func (fi headerFileInfo) Size() int64 { return fi.h.Size }
|
|
func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
|
|
func (fi headerFileInfo) ModTime() time.Time { return fi.h.ModTime }
|
|
func (fi headerFileInfo) Sys() interface{} { return fi.h }
|
|
|
|
// Name returns the base name of the file.
|
|
func (fi headerFileInfo) Name() string {
|
|
if fi.IsDir() {
|
|
return path.Base(path.Clean(fi.h.Name))
|
|
}
|
|
return path.Base(fi.h.Name)
|
|
}
|
|
|
|
// Mode returns the permission and mode bits for the headerFileInfo.
|
|
func (fi headerFileInfo) Mode() (mode os.FileMode) {
|
|
// Set file permission bits.
|
|
mode = os.FileMode(fi.h.Mode).Perm()
|
|
|
|
// Set setuid, setgid and sticky bits.
|
|
if fi.h.Mode&c_ISUID != 0 {
|
|
mode |= os.ModeSetuid
|
|
}
|
|
if fi.h.Mode&c_ISGID != 0 {
|
|
mode |= os.ModeSetgid
|
|
}
|
|
if fi.h.Mode&c_ISVTX != 0 {
|
|
mode |= os.ModeSticky
|
|
}
|
|
|
|
// Set file mode bits; clear perm, setuid, setgid, and sticky bits.
|
|
switch m := os.FileMode(fi.h.Mode) &^ 07777; m {
|
|
case c_ISDIR:
|
|
mode |= os.ModeDir
|
|
case c_ISFIFO:
|
|
mode |= os.ModeNamedPipe
|
|
case c_ISLNK:
|
|
mode |= os.ModeSymlink
|
|
case c_ISBLK:
|
|
mode |= os.ModeDevice
|
|
case c_ISCHR:
|
|
mode |= os.ModeDevice
|
|
mode |= os.ModeCharDevice
|
|
case c_ISSOCK:
|
|
mode |= os.ModeSocket
|
|
}
|
|
|
|
switch fi.h.Typeflag {
|
|
case TypeSymlink:
|
|
mode |= os.ModeSymlink
|
|
case TypeChar:
|
|
mode |= os.ModeDevice
|
|
mode |= os.ModeCharDevice
|
|
case TypeBlock:
|
|
mode |= os.ModeDevice
|
|
case TypeDir:
|
|
mode |= os.ModeDir
|
|
case TypeFifo:
|
|
mode |= os.ModeNamedPipe
|
|
}
|
|
|
|
return mode
|
|
}
|
|
|
|
// sysStat, if non-nil, populates h from system-dependent fields of fi.
|
|
var sysStat func(fi os.FileInfo, h *Header) error
|
|
|
|
const (
|
|
// Mode constants from the USTAR spec:
|
|
// See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06
|
|
c_ISUID = 04000 // Set uid
|
|
c_ISGID = 02000 // Set gid
|
|
c_ISVTX = 01000 // Save text (sticky bit)
|
|
|
|
// Common Unix mode constants; these are not defined in any common tar standard.
|
|
// Header.FileInfo understands these, but FileInfoHeader will never produce these.
|
|
c_ISDIR = 040000 // Directory
|
|
c_ISFIFO = 010000 // FIFO
|
|
c_ISREG = 0100000 // Regular file
|
|
c_ISLNK = 0120000 // Symbolic link
|
|
c_ISBLK = 060000 // Block special file
|
|
c_ISCHR = 020000 // Character special file
|
|
c_ISSOCK = 0140000 // Socket
|
|
)
|
|
|
|
// 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.
|
|
//
|
|
// Since os.FileInfo's Name method only returns the base name of
|
|
// the file it describes, it may be necessary to modify Header.Name
|
|
// to provide the full path name of the file.
|
|
func FileInfoHeader(fi os.FileInfo, link string) (*Header, error) {
|
|
if fi == nil {
|
|
return nil, errors.New("archive/tar: FileInfo is nil")
|
|
}
|
|
fm := fi.Mode()
|
|
h := &Header{
|
|
Name: fi.Name(),
|
|
ModTime: fi.ModTime(),
|
|
Mode: int64(fm.Perm()), // or'd with c_IS* constants later
|
|
}
|
|
switch {
|
|
case fm.IsRegular():
|
|
h.Typeflag = TypeReg
|
|
h.Size = fi.Size()
|
|
case fi.IsDir():
|
|
h.Typeflag = TypeDir
|
|
h.Name += "/"
|
|
case fm&os.ModeSymlink != 0:
|
|
h.Typeflag = TypeSymlink
|
|
h.Linkname = link
|
|
case fm&os.ModeDevice != 0:
|
|
if fm&os.ModeCharDevice != 0 {
|
|
h.Typeflag = TypeChar
|
|
} else {
|
|
h.Typeflag = TypeBlock
|
|
}
|
|
case fm&os.ModeNamedPipe != 0:
|
|
h.Typeflag = TypeFifo
|
|
case fm&os.ModeSocket != 0:
|
|
return nil, fmt.Errorf("archive/tar: sockets not supported")
|
|
default:
|
|
return nil, fmt.Errorf("archive/tar: unknown file mode %v", fm)
|
|
}
|
|
if fm&os.ModeSetuid != 0 {
|
|
h.Mode |= c_ISUID
|
|
}
|
|
if fm&os.ModeSetgid != 0 {
|
|
h.Mode |= c_ISGID
|
|
}
|
|
if fm&os.ModeSticky != 0 {
|
|
h.Mode |= c_ISVTX
|
|
}
|
|
// If possible, populate additional fields from OS-specific
|
|
// FileInfo fields.
|
|
if sys, ok := fi.Sys().(*Header); ok {
|
|
// This FileInfo came from a Header (not the OS). Use the
|
|
// original Header to populate all remaining fields.
|
|
h.Uid = sys.Uid
|
|
h.Gid = sys.Gid
|
|
h.Uname = sys.Uname
|
|
h.Gname = sys.Gname
|
|
h.AccessTime = sys.AccessTime
|
|
h.ChangeTime = sys.ChangeTime
|
|
if sys.Xattrs != nil {
|
|
h.Xattrs = make(map[string]string)
|
|
for k, v := range sys.Xattrs {
|
|
h.Xattrs[k] = v
|
|
}
|
|
}
|
|
if sys.Typeflag == TypeLink {
|
|
// hard link
|
|
h.Typeflag = TypeLink
|
|
h.Size = 0
|
|
h.Linkname = sys.Linkname
|
|
}
|
|
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)
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// isHeaderOnlyType checks if the given type flag is of the type that has no
|
|
// data section even if a size is specified.
|
|
func isHeaderOnlyType(flag byte) bool {
|
|
switch flag {
|
|
case TypeLink, TypeSymlink, TypeChar, TypeBlock, TypeDir, TypeFifo:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func min(a, b int64) int64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// splitUSTARPath splits a path according to USTAR prefix and suffix rules.
|
|
// If the path is not splittable, then it will return ("", "", false).
|
|
func splitUSTARPath(name string) (prefix, suffix string, ok bool) {
|
|
length := len(name)
|
|
if length <= nameSize || !isASCII(name) {
|
|
return "", "", false
|
|
} else if length > prefixSize+1 {
|
|
length = prefixSize + 1
|
|
} else if name[length-1] == '/' {
|
|
length--
|
|
}
|
|
|
|
i := strings.LastIndex(name[:length], "/")
|
|
nlen := len(name) - i - 1 // nlen is length of suffix
|
|
plen := i // plen is length of prefix
|
|
if i <= 0 || nlen > nameSize || nlen == 0 || plen > prefixSize {
|
|
return "", "", false
|
|
}
|
|
return name[:i], name[i+1:], true
|
|
}
|