"fmt"
"os"
"path"
+ "strconv"
"time"
)
return headerFileInfo{h}
}
+// 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.
+//
+// As a by-product of checking the fields, this function returns paxHdrs, which
+// contain all fields that could not be directly encoded.
+func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) {
+ format = formatUSTAR | formatPAX | formatGNU
+ paxHdrs = make(map[string]string)
+
+ verifyString := func(s string, size int, gnuLong bool, 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
+ if !isASCII(s) || (tooLong && !gnuLong) {
+ // TODO(dsnet): GNU supports UTF-8 (without NUL) for strings.
+ format &^= formatGNU // No GNU
+ }
+ if !isASCII(s) || tooLong {
+ // TODO(dsnet): If the path is splittable, it is possible to still
+ // use the USTAR format.
+ format &^= formatUSTAR // No USTAR
+ if paxKey == paxNone {
+ format &^= formatPAX // No PAX
+ } else {
+ paxHdrs[paxKey] = s
+ }
+ }
+ }
+ verifyNumeric := func(n int64, size int, paxKey string) {
+ if !fitsInBase256(size, n) {
+ format &^= formatGNU // No GNU
+ }
+ if !fitsInOctal(size, n) {
+ format &^= formatUSTAR // No USTAR
+ if paxKey == paxNone {
+ format &^= formatPAX // No PAX
+ } else {
+ paxHdrs[paxKey] = strconv.FormatInt(n, 10)
+ }
+ }
+ }
+ verifyTime := func(ts time.Time, size int, ustarField bool, paxKey string) {
+ if ts.IsZero() {
+ return // Always okay
+ }
+ needsNano := ts.Nanosecond() != 0
+ if !fitsInBase256(size, ts.Unix()) || needsNano {
+ format &^= formatGNU // No GNU
+ }
+ if !fitsInOctal(size, ts.Unix()) || needsNano || !ustarField {
+ format &^= formatUSTAR // No USTAR
+ if paxKey == paxNone {
+ format &^= formatPAX // No PAX
+ } else {
+ // TODO(dsnet): Support PAX time here.
+ // paxHdrs[paxKey] = formatPAXTime(ts)
+ }
+ }
+ }
+
+ // TODO(dsnet): Add GNU long name support.
+ const supportGNULong = false
+
+ var blk block
+ var v7 = blk.V7()
+ var ustar = blk.USTAR()
+ verifyString(h.Name, len(v7.Name()), supportGNULong, paxPath)
+ verifyString(h.Linkname, len(v7.LinkName()), supportGNULong, paxLinkpath)
+ verifyString(h.Uname, len(ustar.UserName()), false, paxUname)
+ verifyString(h.Gname, len(ustar.GroupName()), false, paxGname)
+ verifyNumeric(h.Mode, len(v7.Mode()), paxNone)
+ verifyNumeric(int64(h.Uid), len(v7.UID()), paxUid)
+ verifyNumeric(int64(h.Gid), len(v7.GID()), paxGid)
+ verifyNumeric(h.Size, len(v7.Size()), paxSize)
+ verifyNumeric(h.Devmajor, len(ustar.DevMajor()), paxNone)
+ verifyNumeric(h.Devminor, len(ustar.DevMinor()), paxNone)
+ verifyTime(h.ModTime, len(v7.ModTime()), true, paxMtime)
+ // TODO(dsnet): Support atime and ctime fields.
+ // verifyTime(h.AccessTime, len(gnu.AccessTime()), false, paxAtime)
+ // verifyTime(h.ChangeTime, len(gnu.ChangeTime()), false, paxCtime)
+
+ if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 {
+ return formatUnknown, nil
+ }
+ if len(h.Xattrs) > 0 {
+ for k, v := range h.Xattrs {
+ paxHdrs[paxXattr+k] = v
+ }
+ format &= formatPAX // PAX only
+ }
+ for k, v := range paxHdrs {
+ if !validPAXRecord(k, v) {
+ return formatUnknown, nil // Invalid PAX key
+ }
+ }
+ return format, paxHdrs
+}
+
// headerFileInfo implements os.FileInfo.
type headerFileInfo struct {
h *Header
"bytes"
"internal/testenv"
"io/ioutil"
+ "math"
"os"
"path"
"path/filepath"
}
}
}
+
+func TestHeaderAllowedFormats(t *testing.T) {
+ prettyFormat := func(f int) string {
+ if f == formatUnknown {
+ return "(formatUnknown)"
+ }
+ var fs []string
+ if f&formatUSTAR > 0 {
+ fs = append(fs, "formatUSTAR")
+ }
+ if f&formatPAX > 0 {
+ fs = append(fs, "formatPAX")
+ }
+ if f&formatGNU > 0 {
+ fs = append(fs, "formatGNU")
+ }
+ return "(" + strings.Join(fs, " | ") + ")"
+ }
+
+ vectors := []struct {
+ header *Header // Input header
+ paxHdrs map[string]string // Expected PAX headers that may be needed
+ formats int // Expected formats that can encode the header
+ }{{
+ header: &Header{},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Size: 077777777777},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Size: 077777777777 + 1},
+ paxHdrs: map[string]string{paxSize: "8589934592"},
+ formats: formatPAX | formatGNU,
+ }, {
+ header: &Header{Mode: 07777777},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Mode: 07777777 + 1},
+ formats: formatGNU,
+ }, {
+ header: &Header{Devmajor: -123},
+ formats: formatGNU,
+ }, {
+ header: &Header{Devmajor: 1<<56 - 1},
+ formats: formatGNU,
+ }, {
+ header: &Header{Devmajor: 1 << 56},
+ formats: formatUnknown,
+ }, {
+ header: &Header{Devmajor: -1 << 56},
+ formats: formatGNU,
+ }, {
+ header: &Header{Devmajor: -1<<56 - 1},
+ formats: formatUnknown,
+ }, {
+ header: &Header{Name: "用戶名", Devmajor: -1 << 56},
+ formats: formatUnknown,
+ }, {
+ header: &Header{Size: math.MaxInt64},
+ paxHdrs: map[string]string{paxSize: "9223372036854775807"},
+ formats: formatPAX | formatGNU,
+ }, {
+ header: &Header{Size: math.MinInt64},
+ paxHdrs: map[string]string{paxSize: "-9223372036854775808"},
+ formats: formatUnknown,
+ }, {
+ header: &Header{Uname: "0123456789abcdef0123456789abcdef"},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Uname: "0123456789abcdef0123456789abcdefx"},
+ paxHdrs: map[string]string{paxUname: "0123456789abcdef0123456789abcdefx"},
+ formats: formatPAX,
+ }, {
+ header: &Header{Name: "foobar"},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Name: strings.Repeat("a", nameSize)},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Linkname: "用戶名"},
+ paxHdrs: map[string]string{paxLinkpath: "用戶名"},
+ formats: formatPAX,
+ }, {
+ header: &Header{Linkname: strings.Repeat("用戶名\x00", nameSize)},
+ paxHdrs: map[string]string{paxLinkpath: strings.Repeat("用戶名\x00", nameSize)},
+ formats: formatUnknown,
+ }, {
+ header: &Header{Linkname: "\x00hello"},
+ paxHdrs: map[string]string{paxLinkpath: "\x00hello"},
+ formats: formatUnknown,
+ }, {
+ header: &Header{Uid: 07777777},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Uid: 07777777 + 1},
+ paxHdrs: map[string]string{paxUid: "2097152"},
+ formats: formatPAX | formatGNU,
+ }, {
+ header: &Header{Xattrs: nil},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{Xattrs: map[string]string{"foo": "bar"}},
+ paxHdrs: map[string]string{paxXattr + "foo": "bar"},
+ formats: formatPAX,
+ }, {
+ header: &Header{Xattrs: map[string]string{"用戶名": "\x00hello"}},
+ paxHdrs: map[string]string{paxXattr + "用戶名": "\x00hello"},
+ formats: formatPAX,
+ }, {
+ header: &Header{ModTime: time.Unix(0, 0)},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }, {
+ header: &Header{ModTime: time.Unix(077777777777, 0)},
+ formats: formatUSTAR | formatPAX | formatGNU,
+ }}
+
+ for i, v := range vectors {
+ formats, paxHdrs := v.header.allowedFormats()
+ if formats != v.formats {
+ t.Errorf("test %d, allowedFormats(...): got %v, want %v", i, prettyFormat(formats), prettyFormat(v.formats))
+ }
+ if formats&formatPAX > 0 && !reflect.DeepEqual(paxHdrs, v.paxHdrs) && !(len(paxHdrs) == 0 && len(v.paxHdrs) == 0) {
+ t.Errorf("test %d, allowedFormats(...):\ngot %v\nwant %s", i, paxHdrs, v.paxHdrs)
+ }
+ }
+}
// WriteHeader calls Flush if it is not the first header.
// Calling after a Close will return ErrWriteAfterClose.
func (tw *Writer) WriteHeader(hdr *Header) error {
- return tw.writeHeader(hdr, true)
+ // TODO(dsnet): Add PAX timestamps with nanosecond support.
+ hdrCpy := *hdr
+ hdrCpy.ModTime = hdrCpy.ModTime.Truncate(time.Second)
+
+ switch allowedFormats, _ := hdrCpy.allowedFormats(); {
+ case allowedFormats&formatUSTAR > 0:
+ // TODO(dsnet): Implement and call specialized writeUSTARHeader.
+ return tw.writeHeader(&hdrCpy, true)
+ case allowedFormats&formatPAX > 0:
+ // TODO(dsnet): Implement and call specialized writePAXHeader.
+ return tw.writeHeader(&hdrCpy, true)
+ case allowedFormats&formatGNU > 0:
+ // TODO(dsnet): Implement and call specialized writeGNUHeader.
+ return tw.writeHeader(&hdrCpy, true)
+ default:
+ return ErrHeader
+ }
}
// WriteHeader writes hdr and prepares to accept the file's contents.