Skip to content

Commit ead6255

Browse files
committedAug 11, 2017
archive/tar: check for permissible output formats first
The current logic in writeHeader attempts to encode the Header in one format and if it discovered that it could not it would attempt to switch to a different format mid-way through. This makes it very hard to reason about what format will be used in the end and whether it will even be a valid format. Instead, we should verify from the start what formats are allowed to encode the given input Header. If no formats are possible, then we can return immediately, rejecting the Header. For now, we continue on to the hairy logic in writeHeader, but a future CL can split that logic up and specialize them for each format now that we know what is possible. Update #9683 Update #12594 Change-Id: I8406ea855dfcb8b478a03a7058ddf8b2b09d46dc Reviewed-on: https://go-review.googlesource.com/54433 Run-TryBot: Joe Tsai <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent 49ab0db commit ead6255

File tree

3 files changed

+245
-1
lines changed

3 files changed

+245
-1
lines changed
 

‎src/archive/tar/common.go

+101
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"os"
1919
"path"
20+
"strconv"
2021
"time"
2122
)
2223

@@ -67,6 +68,106 @@ func (h *Header) FileInfo() os.FileInfo {
6768
return headerFileInfo{h}
6869
}
6970

71+
// allowedFormats determines which formats can be used. The value returned
72+
// is the logical OR of multiple possible formats. If the value is
73+
// formatUnknown, then the input Header cannot be encoded.
74+
//
75+
// As a by-product of checking the fields, this function returns paxHdrs, which
76+
// contain all fields that could not be directly encoded.
77+
func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) {
78+
format = formatUSTAR | formatPAX | formatGNU
79+
paxHdrs = make(map[string]string)
80+
81+
verifyString := func(s string, size int, gnuLong bool, paxKey string) {
82+
// NUL-terminator is optional for path and linkpath.
83+
// Technically, it is required for uname and gname,
84+
// but neither GNU nor BSD tar checks for it.
85+
tooLong := len(s) > size
86+
if !isASCII(s) || (tooLong && !gnuLong) {
87+
// TODO(dsnet): GNU supports UTF-8 (without NUL) for strings.
88+
format &^= formatGNU // No GNU
89+
}
90+
if !isASCII(s) || tooLong {
91+
// TODO(dsnet): If the path is splittable, it is possible to still
92+
// use the USTAR format.
93+
format &^= formatUSTAR // No USTAR
94+
if paxKey == paxNone {
95+
format &^= formatPAX // No PAX
96+
} else {
97+
paxHdrs[paxKey] = s
98+
}
99+
}
100+
}
101+
verifyNumeric := func(n int64, size int, paxKey string) {
102+
if !fitsInBase256(size, n) {
103+
format &^= formatGNU // No GNU
104+
}
105+
if !fitsInOctal(size, n) {
106+
format &^= formatUSTAR // No USTAR
107+
if paxKey == paxNone {
108+
format &^= formatPAX // No PAX
109+
} else {
110+
paxHdrs[paxKey] = strconv.FormatInt(n, 10)
111+
}
112+
}
113+
}
114+
verifyTime := func(ts time.Time, size int, ustarField bool, paxKey string) {
115+
if ts.IsZero() {
116+
return // Always okay
117+
}
118+
needsNano := ts.Nanosecond() != 0
119+
if !fitsInBase256(size, ts.Unix()) || needsNano {
120+
format &^= formatGNU // No GNU
121+
}
122+
if !fitsInOctal(size, ts.Unix()) || needsNano || !ustarField {
123+
format &^= formatUSTAR // No USTAR
124+
if paxKey == paxNone {
125+
format &^= formatPAX // No PAX
126+
} else {
127+
// TODO(dsnet): Support PAX time here.
128+
// paxHdrs[paxKey] = formatPAXTime(ts)
129+
}
130+
}
131+
}
132+
133+
// TODO(dsnet): Add GNU long name support.
134+
const supportGNULong = false
135+
136+
var blk block
137+
var v7 = blk.V7()
138+
var ustar = blk.USTAR()
139+
verifyString(h.Name, len(v7.Name()), supportGNULong, paxPath)
140+
verifyString(h.Linkname, len(v7.LinkName()), supportGNULong, paxLinkpath)
141+
verifyString(h.Uname, len(ustar.UserName()), false, paxUname)
142+
verifyString(h.Gname, len(ustar.GroupName()), false, paxGname)
143+
verifyNumeric(h.Mode, len(v7.Mode()), paxNone)
144+
verifyNumeric(int64(h.Uid), len(v7.UID()), paxUid)
145+
verifyNumeric(int64(h.Gid), len(v7.GID()), paxGid)
146+
verifyNumeric(h.Size, len(v7.Size()), paxSize)
147+
verifyNumeric(h.Devmajor, len(ustar.DevMajor()), paxNone)
148+
verifyNumeric(h.Devminor, len(ustar.DevMinor()), paxNone)
149+
verifyTime(h.ModTime, len(v7.ModTime()), true, paxMtime)
150+
// TODO(dsnet): Support atime and ctime fields.
151+
// verifyTime(h.AccessTime, len(gnu.AccessTime()), false, paxAtime)
152+
// verifyTime(h.ChangeTime, len(gnu.ChangeTime()), false, paxCtime)
153+
154+
if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 {
155+
return formatUnknown, nil
156+
}
157+
if len(h.Xattrs) > 0 {
158+
for k, v := range h.Xattrs {
159+
paxHdrs[paxXattr+k] = v
160+
}
161+
format &= formatPAX // PAX only
162+
}
163+
for k, v := range paxHdrs {
164+
if !validPAXRecord(k, v) {
165+
return formatUnknown, nil // Invalid PAX key
166+
}
167+
}
168+
return format, paxHdrs
169+
}
170+
70171
// headerFileInfo implements os.FileInfo.
71172
type headerFileInfo struct {
72173
h *Header

‎src/archive/tar/tar_test.go

+127
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"internal/testenv"
1010
"io/ioutil"
11+
"math"
1112
"os"
1213
"path"
1314
"path/filepath"
@@ -329,3 +330,129 @@ func TestHeaderRoundTrip(t *testing.T) {
329330
}
330331
}
331332
}
333+
334+
func TestHeaderAllowedFormats(t *testing.T) {
335+
prettyFormat := func(f int) string {
336+
if f == formatUnknown {
337+
return "(formatUnknown)"
338+
}
339+
var fs []string
340+
if f&formatUSTAR > 0 {
341+
fs = append(fs, "formatUSTAR")
342+
}
343+
if f&formatPAX > 0 {
344+
fs = append(fs, "formatPAX")
345+
}
346+
if f&formatGNU > 0 {
347+
fs = append(fs, "formatGNU")
348+
}
349+
return "(" + strings.Join(fs, " | ") + ")"
350+
}
351+
352+
vectors := []struct {
353+
header *Header // Input header
354+
paxHdrs map[string]string // Expected PAX headers that may be needed
355+
formats int // Expected formats that can encode the header
356+
}{{
357+
header: &Header{},
358+
formats: formatUSTAR | formatPAX | formatGNU,
359+
}, {
360+
header: &Header{Size: 077777777777},
361+
formats: formatUSTAR | formatPAX | formatGNU,
362+
}, {
363+
header: &Header{Size: 077777777777 + 1},
364+
paxHdrs: map[string]string{paxSize: "8589934592"},
365+
formats: formatPAX | formatGNU,
366+
}, {
367+
header: &Header{Mode: 07777777},
368+
formats: formatUSTAR | formatPAX | formatGNU,
369+
}, {
370+
header: &Header{Mode: 07777777 + 1},
371+
formats: formatGNU,
372+
}, {
373+
header: &Header{Devmajor: -123},
374+
formats: formatGNU,
375+
}, {
376+
header: &Header{Devmajor: 1<<56 - 1},
377+
formats: formatGNU,
378+
}, {
379+
header: &Header{Devmajor: 1 << 56},
380+
formats: formatUnknown,
381+
}, {
382+
header: &Header{Devmajor: -1 << 56},
383+
formats: formatGNU,
384+
}, {
385+
header: &Header{Devmajor: -1<<56 - 1},
386+
formats: formatUnknown,
387+
}, {
388+
header: &Header{Name: "用戶名", Devmajor: -1 << 56},
389+
formats: formatUnknown,
390+
}, {
391+
header: &Header{Size: math.MaxInt64},
392+
paxHdrs: map[string]string{paxSize: "9223372036854775807"},
393+
formats: formatPAX | formatGNU,
394+
}, {
395+
header: &Header{Size: math.MinInt64},
396+
paxHdrs: map[string]string{paxSize: "-9223372036854775808"},
397+
formats: formatUnknown,
398+
}, {
399+
header: &Header{Uname: "0123456789abcdef0123456789abcdef"},
400+
formats: formatUSTAR | formatPAX | formatGNU,
401+
}, {
402+
header: &Header{Uname: "0123456789abcdef0123456789abcdefx"},
403+
paxHdrs: map[string]string{paxUname: "0123456789abcdef0123456789abcdefx"},
404+
formats: formatPAX,
405+
}, {
406+
header: &Header{Name: "foobar"},
407+
formats: formatUSTAR | formatPAX | formatGNU,
408+
}, {
409+
header: &Header{Name: strings.Repeat("a", nameSize)},
410+
formats: formatUSTAR | formatPAX | formatGNU,
411+
}, {
412+
header: &Header{Linkname: "用戶名"},
413+
paxHdrs: map[string]string{paxLinkpath: "用戶名"},
414+
formats: formatPAX,
415+
}, {
416+
header: &Header{Linkname: strings.Repeat("用戶名\x00", nameSize)},
417+
paxHdrs: map[string]string{paxLinkpath: strings.Repeat("用戶名\x00", nameSize)},
418+
formats: formatUnknown,
419+
}, {
420+
header: &Header{Linkname: "\x00hello"},
421+
paxHdrs: map[string]string{paxLinkpath: "\x00hello"},
422+
formats: formatUnknown,
423+
}, {
424+
header: &Header{Uid: 07777777},
425+
formats: formatUSTAR | formatPAX | formatGNU,
426+
}, {
427+
header: &Header{Uid: 07777777 + 1},
428+
paxHdrs: map[string]string{paxUid: "2097152"},
429+
formats: formatPAX | formatGNU,
430+
}, {
431+
header: &Header{Xattrs: nil},
432+
formats: formatUSTAR | formatPAX | formatGNU,
433+
}, {
434+
header: &Header{Xattrs: map[string]string{"foo": "bar"}},
435+
paxHdrs: map[string]string{paxXattr + "foo": "bar"},
436+
formats: formatPAX,
437+
}, {
438+
header: &Header{Xattrs: map[string]string{"用戶名": "\x00hello"}},
439+
paxHdrs: map[string]string{paxXattr + "用戶名": "\x00hello"},
440+
formats: formatPAX,
441+
}, {
442+
header: &Header{ModTime: time.Unix(0, 0)},
443+
formats: formatUSTAR | formatPAX | formatGNU,
444+
}, {
445+
header: &Header{ModTime: time.Unix(077777777777, 0)},
446+
formats: formatUSTAR | formatPAX | formatGNU,
447+
}}
448+
449+
for i, v := range vectors {
450+
formats, paxHdrs := v.header.allowedFormats()
451+
if formats != v.formats {
452+
t.Errorf("test %d, allowedFormats(...): got %v, want %v", i, prettyFormat(formats), prettyFormat(v.formats))
453+
}
454+
if formats&formatPAX > 0 && !reflect.DeepEqual(paxHdrs, v.paxHdrs) && !(len(paxHdrs) == 0 && len(v.paxHdrs) == 0) {
455+
t.Errorf("test %d, allowedFormats(...):\ngot %v\nwant %s", i, paxHdrs, v.paxHdrs)
456+
}
457+
}
458+
}

‎src/archive/tar/writer.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,23 @@ var (
7777
// WriteHeader calls Flush if it is not the first header.
7878
// Calling after a Close will return ErrWriteAfterClose.
7979
func (tw *Writer) WriteHeader(hdr *Header) error {
80-
return tw.writeHeader(hdr, true)
80+
// TODO(dsnet): Add PAX timestamps with nanosecond support.
81+
hdrCpy := *hdr
82+
hdrCpy.ModTime = hdrCpy.ModTime.Truncate(time.Second)
83+
84+
switch allowedFormats, _ := hdrCpy.allowedFormats(); {
85+
case allowedFormats&formatUSTAR > 0:
86+
// TODO(dsnet): Implement and call specialized writeUSTARHeader.
87+
return tw.writeHeader(&hdrCpy, true)
88+
case allowedFormats&formatPAX > 0:
89+
// TODO(dsnet): Implement and call specialized writePAXHeader.
90+
return tw.writeHeader(&hdrCpy, true)
91+
case allowedFormats&formatGNU > 0:
92+
// TODO(dsnet): Implement and call specialized writeGNUHeader.
93+
return tw.writeHeader(&hdrCpy, true)
94+
default:
95+
return ErrHeader
96+
}
8197
}
8298

8399
// WriteHeader writes hdr and prepares to accept the file's contents.

0 commit comments

Comments
 (0)
Please sign in to comment.