Commit 5c20ffbb authored by Joe Tsai's avatar Joe Tsai Committed by Joe Tsai

archive/tar: add support for long binary strings in GNU format

The GNU tar format defines the following type flags:
	TypeGNULongName = 'L' // Next file has a long name
	TypeGNULongLink = 'K' // Next file symlinks to a file w/ a long name

Anytime a string exceeds the field dedicated to store it, the GNU format
permits a fake "file" to be prepended where that file entry has a Typeflag
of 'L' or 'K' and the contents of the file is a NUL-terminated string.

Contrary to previous TODO comments,
the GNU format supports arbitrary strings (without NUL) rather UTF-8 strings.
The manual says the following:
<<<
The name, linkname, magic, uname, and gname are
null-terminated character strings

> <<<
> All characters in header blocks are represented
> by using 8-bit characters in the local variant of ASCII.


From this description, we gather the following:
* We must forbid NULs in any GNU strings
* Any 8-bit value (other than NUL) is permitted

Since the modern world has moved to UTF-8, it is really difficult to
determine what a "local variant of ASCII" means. For this reason,
we treat strings as just an arbitrary binary string (without NUL)
and leave it to the user to determine the encoding of this string.
(Practically, it seems that UTF-8 is the typical encoding used
in GNU archives seen in the wild).

The implementation of GNU tar seems to confirm this interpretation
of the manual where it permits any arbitrary binary string to exist
within these fields so long as they do not contain the NUL character.

 $ touch `echo -e "not\x80\x81\x82\x83utf8"`
 $ gnutar -H gnu --tar -cvf gnu-not-utf8.tar $(echo -e "not\x80\x81\x82\x83utf8")

The fact that we permit arbitrary binary in GNU strings goes
hand-in-hand with the fact that GNU also permits a "base-256" encoding
of numeric fields, which is effectively two-complement binary.

Change-Id: Ic037ec6bed306d07d1312f0058594bd9b64d9880
Reviewed-on: https://go-review.googlesource.com/55573Reviewed-by: default avatarIan Lance Taylor <iant@golang.org>
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent d6cada7f
...@@ -85,14 +85,13 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) { ...@@ -85,14 +85,13 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) {
format = formatUSTAR | formatPAX | formatGNU format = formatUSTAR | formatPAX | formatGNU
paxHdrs = make(map[string]string) paxHdrs = make(map[string]string)
verifyString := func(s string, size int, gnuLong bool, paxKey string) { verifyString := func(s string, size int, paxKey string) {
// NUL-terminator is optional for path and linkpath. // NUL-terminator is optional for path and linkpath.
// Technically, it is required for uname and gname, // Technically, it is required for uname and gname,
// but neither GNU nor BSD tar checks for it. // but neither GNU nor BSD tar checks for it.
tooLong := len(s) > size tooLong := len(s) > size
if !isASCII(s) || (tooLong && !gnuLong) { allowLongGNU := paxKey == paxPath || paxKey == paxLinkpath
// TODO(dsnet): GNU supports UTF-8 (without NUL) for strings. if hasNUL(s) || (tooLong && !allowLongGNU) {
format &^= formatGNU // No GNU format &^= formatGNU // No GNU
} }
if !isASCII(s) || tooLong { if !isASCII(s) || tooLong {
...@@ -120,15 +119,16 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) { ...@@ -120,15 +119,16 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) {
} }
} }
} }
verifyTime := func(ts time.Time, size int, ustarField bool, paxKey string) { verifyTime := func(ts time.Time, size int, paxKey string) {
if ts.IsZero() { if ts.IsZero() {
return // Always okay return // Always okay
} }
needsNano := ts.Nanosecond() != 0 needsNano := ts.Nanosecond() != 0
hasFieldUSTAR := paxKey == paxMtime
if !fitsInBase256(size, ts.Unix()) || needsNano { if !fitsInBase256(size, ts.Unix()) || needsNano {
format &^= formatGNU // No GNU format &^= formatGNU // No GNU
} }
if !fitsInOctal(size, ts.Unix()) || needsNano || !ustarField { if !fitsInOctal(size, ts.Unix()) || needsNano || !hasFieldUSTAR {
format &^= formatUSTAR // No USTAR format &^= formatUSTAR // No USTAR
if paxKey == paxNone { if paxKey == paxNone {
format &^= formatPAX // No PAX format &^= formatPAX // No PAX
...@@ -138,26 +138,23 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) { ...@@ -138,26 +138,23 @@ func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) {
} }
} }
// TODO(dsnet): Add GNU long name support.
const supportGNULong = false
var blk block var blk block
v7 := blk.V7() v7 := blk.V7()
ustar := blk.USTAR() ustar := blk.USTAR()
gnu := blk.GNU() gnu := blk.GNU()
verifyString(h.Name, len(v7.Name()), supportGNULong, paxPath) verifyString(h.Name, len(v7.Name()), paxPath)
verifyString(h.Linkname, len(v7.LinkName()), supportGNULong, paxLinkpath) verifyString(h.Linkname, len(v7.LinkName()), paxLinkpath)
verifyString(h.Uname, len(ustar.UserName()), false, paxUname) verifyString(h.Uname, len(ustar.UserName()), paxUname)
verifyString(h.Gname, len(ustar.GroupName()), false, paxGname) verifyString(h.Gname, len(ustar.GroupName()), paxGname)
verifyNumeric(h.Mode, len(v7.Mode()), paxNone) verifyNumeric(h.Mode, len(v7.Mode()), paxNone)
verifyNumeric(int64(h.Uid), len(v7.UID()), paxUid) verifyNumeric(int64(h.Uid), len(v7.UID()), paxUid)
verifyNumeric(int64(h.Gid), len(v7.GID()), paxGid) verifyNumeric(int64(h.Gid), len(v7.GID()), paxGid)
verifyNumeric(h.Size, len(v7.Size()), paxSize) verifyNumeric(h.Size, len(v7.Size()), paxSize)
verifyNumeric(h.Devmajor, len(ustar.DevMajor()), paxNone) verifyNumeric(h.Devmajor, len(ustar.DevMajor()), paxNone)
verifyNumeric(h.Devminor, len(ustar.DevMinor()), paxNone) verifyNumeric(h.Devminor, len(ustar.DevMinor()), paxNone)
verifyTime(h.ModTime, len(v7.ModTime()), true, paxMtime) verifyTime(h.ModTime, len(v7.ModTime()), paxMtime)
verifyTime(h.AccessTime, len(gnu.AccessTime()), false, paxAtime) verifyTime(h.AccessTime, len(gnu.AccessTime()), paxAtime)
verifyTime(h.ChangeTime, len(gnu.ChangeTime()), false, paxCtime) verifyTime(h.ChangeTime, len(gnu.ChangeTime()), paxCtime)
if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 { if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 {
return formatUnknown, nil return formatUnknown, nil
......
...@@ -350,6 +350,41 @@ func TestReader(t *testing.T) { ...@@ -350,6 +350,41 @@ func TestReader(t *testing.T) {
Uname: "rawr", Uname: "rawr",
Gname: "dsnet", Gname: "dsnet",
}}, }},
}, {
// This archive was generated by Writer but is readable by both
// GNU and BSD tar utilities.
// The archive generated by GNU is nearly byte-for-byte identical
// to the Go version except the Go version sets a negative Devminor
// just to force the GNU format.
file: "testdata/gnu-utf8.tar",
headers: []*Header{{
Name: "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
Mode: 0644,
Uid: 1000, Gid: 1000,
ModTime: time.Unix(0, 0),
Typeflag: '0',
Uname: "☺",
Gname: "⚹",
Devminor: -1,
}},
}, {
// This archive was generated by Writer but is readable by both
// GNU and BSD tar utilities.
// The archive generated by GNU is nearly byte-for-byte identical
// to the Go version except the Go version sets a negative Devminor
// just to force the GNU format.
file: "testdata/gnu-not-utf8.tar",
headers: []*Header{{
Name: "hi\x80\x81\x82\x83bye",
Mode: 0644,
Uid: 1000,
Gid: 1000,
ModTime: time.Unix(0, 0),
Typeflag: '0',
Uname: "rawr",
Gname: "dsnet",
Devminor: -1,
}},
}, { }, {
// BSD tar v3.1.2 and GNU tar v1.27.1 both rejects PAX records // BSD tar v3.1.2 and GNU tar v1.27.1 both rejects PAX records
// with NULs in the key. // with NULs in the key.
......
...@@ -12,6 +12,11 @@ import ( ...@@ -12,6 +12,11 @@ import (
"time" "time"
) )
// hasNUL reports whether the NUL character exists within s.
func hasNUL(s string) bool {
return strings.IndexByte(s, 0) >= 0
}
// isASCII reports whether the input is an ASCII C-style string. // isASCII reports whether the input is an ASCII C-style string.
func isASCII(s string) bool { func isASCII(s string) bool {
for _, c := range s { for _, c := range s {
...@@ -59,7 +64,6 @@ func (f *formatter) formatString(b []byte, s string) { ...@@ -59,7 +64,6 @@ func (f *formatter) formatString(b []byte, s string) {
if len(s) > len(b) { if len(s) > len(b) {
f.err = ErrFieldTooLong f.err = ErrFieldTooLong
} }
s = toASCII(s) // TODO(dsnet): Remove this for UTF-8 support in GNU format
copy(b, s) copy(b, s)
if len(s) < len(b) { if len(s) < len(b) {
b[len(s)] = 0 b[len(s)] = 0
...@@ -307,8 +311,8 @@ func validPAXRecord(k, v string) bool { ...@@ -307,8 +311,8 @@ func validPAXRecord(k, v string) bool {
} }
switch k { switch k {
case paxPath, paxLinkpath, paxUname, paxGname: case paxPath, paxLinkpath, paxUname, paxGname:
return strings.IndexByte(v, 0) < 0 return !hasNUL(v)
default: default:
return strings.IndexByte(k, 0) < 0 return !hasNUL(k)
} }
} }
...@@ -386,7 +386,7 @@ func TestHeaderAllowedFormats(t *testing.T) { ...@@ -386,7 +386,7 @@ func TestHeaderAllowedFormats(t *testing.T) {
formats: formatUnknown, formats: formatUnknown,
}, { }, {
header: &Header{Name: "用戶名", Devmajor: -1 << 56}, header: &Header{Name: "用戶名", Devmajor: -1 << 56},
formats: formatUnknown, formats: formatGNU,
}, { }, {
header: &Header{Size: math.MaxInt64}, header: &Header{Size: math.MaxInt64},
paxHdrs: map[string]string{paxSize: "9223372036854775807"}, paxHdrs: map[string]string{paxSize: "9223372036854775807"},
...@@ -408,10 +408,14 @@ func TestHeaderAllowedFormats(t *testing.T) { ...@@ -408,10 +408,14 @@ func TestHeaderAllowedFormats(t *testing.T) {
}, { }, {
header: &Header{Name: strings.Repeat("a", nameSize)}, header: &Header{Name: strings.Repeat("a", nameSize)},
formats: formatUSTAR | formatPAX | formatGNU, formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Name: strings.Repeat("a", nameSize+1)},
paxHdrs: map[string]string{paxPath: strings.Repeat("a", nameSize+1)},
formats: formatPAX | formatGNU,
}, { }, {
header: &Header{Linkname: "用戶名"}, header: &Header{Linkname: "用戶名"},
paxHdrs: map[string]string{paxLinkpath: "用戶名"}, paxHdrs: map[string]string{paxLinkpath: "用戶名"},
formats: formatPAX, formats: formatPAX | formatGNU,
}, { }, {
header: &Header{Linkname: strings.Repeat("用戶名\x00", nameSize)}, header: &Header{Linkname: strings.Repeat("用戶名\x00", nameSize)},
paxHdrs: map[string]string{paxLinkpath: strings.Repeat("用戶名\x00", nameSize)}, paxHdrs: map[string]string{paxLinkpath: strings.Repeat("用戶名\x00", nameSize)},
......
...@@ -121,12 +121,10 @@ func (tw *Writer) writePAXHeader(hdr *Header, paxHdrs map[string]string) error { ...@@ -121,12 +121,10 @@ func (tw *Writer) writePAXHeader(hdr *Header, paxHdrs map[string]string) error {
} }
// Pack the main header. // Pack the main header.
var f formatter var f formatter // Ignore errors since they are expected
blk := tw.templateV7Plus(hdr, f.formatString, f.formatOctal) fmtStr := func(b []byte, s string) { f.formatString(b, toASCII(s)) }
blk := tw.templateV7Plus(hdr, fmtStr, f.formatOctal)
blk.SetFormat(formatPAX) blk.SetFormat(formatPAX)
if f.err != nil && len(paxHdrs) == 0 {
return f.err // Should never happen, otherwise PAX headers would be used
}
return tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag) return tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag)
} }
...@@ -134,10 +132,23 @@ func (tw *Writer) writeGNUHeader(hdr *Header) error { ...@@ -134,10 +132,23 @@ func (tw *Writer) writeGNUHeader(hdr *Header) error {
// TODO(dsnet): Support writing sparse files. // TODO(dsnet): Support writing sparse files.
// See https://golang.org/issue/13548 // See https://golang.org/issue/13548
// TODO(dsnet): Support long filenames (with UTF-8) support. // Use long-link files if Name or Linkname exceeds the field size.
const longName = "././@LongLink"
if len(hdr.Name) > nameSize {
data := hdr.Name + "\x00"
if err := tw.writeRawFile(longName, data, TypeGNULongName, formatGNU); err != nil {
return err
}
}
if len(hdr.Linkname) > nameSize {
data := hdr.Linkname + "\x00"
if err := tw.writeRawFile(longName, data, TypeGNULongLink, formatGNU); err != nil {
return err
}
}
// Pack the main header. // Pack the main header.
var f formatter var f formatter // Ignore errors since they are expected
blk := tw.templateV7Plus(hdr, f.formatString, f.formatNumeric) blk := tw.templateV7Plus(hdr, f.formatString, f.formatNumeric)
if !hdr.AccessTime.IsZero() { if !hdr.AccessTime.IsZero() {
f.formatNumeric(blk.GNU().AccessTime(), hdr.AccessTime.Unix()) f.formatNumeric(blk.GNU().AccessTime(), hdr.AccessTime.Unix())
...@@ -146,9 +157,6 @@ func (tw *Writer) writeGNUHeader(hdr *Header) error { ...@@ -146,9 +157,6 @@ func (tw *Writer) writeGNUHeader(hdr *Header) error {
f.formatNumeric(blk.GNU().ChangeTime(), hdr.ChangeTime.Unix()) f.formatNumeric(blk.GNU().ChangeTime(), hdr.ChangeTime.Unix())
} }
blk.SetFormat(formatGNU) blk.SetFormat(formatGNU)
if f.err != nil {
return f.err // Should never happen since header is validated
}
return tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag) return tw.writeRawHeader(blk, hdr.Size, hdr.Typeflag)
} }
......
...@@ -219,6 +219,35 @@ func TestWriter(t *testing.T) { ...@@ -219,6 +219,35 @@ func TestWriter(t *testing.T) {
}, },
}}, }},
err: ErrHeader, err: ErrHeader,
}, {
file: "testdata/gnu-utf8.tar",
entries: []*entry{{
header: &Header{
Name: "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
Mode: 0644,
Uid: 1000, Gid: 1000,
ModTime: time.Unix(0, 0),
Typeflag: '0',
Uname: "☺",
Gname: "⚹",
Devminor: -1, // Force use of GNU format
},
}},
}, {
file: "testdata/gnu-not-utf8.tar",
entries: []*entry{{
header: &Header{
Name: "hi\x80\x81\x82\x83bye",
Mode: 0644,
Uid: 1000,
Gid: 1000,
ModTime: time.Unix(0, 0),
Typeflag: '0',
Uname: "rawr",
Gname: "dsnet",
Devminor: -1, // Force use of GNU format
},
}},
}} }}
for _, v := range vectors { for _, v := range vectors {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment