go/src/archive/tar/reader_test.go
Damien Neil 613e746327 [release-branch.go1.24] archive/tar: set a limit on the size of GNU sparse file 1.0 regions
Sparse files in tar archives contain only the non-zero components
of the file. There are several different encodings for sparse
files. When reading GNU tar pax 1.0 sparse files, archive/tar did
not set a limit on the size of the sparse region data. A malicious
archive containing a large number of sparse blocks could cause
archive/tar to read an unbounded amount of data from the archive
into memory.

Since a malicious input can be highly compressable, a small
compressed input could cause very large allocations.

Cap the size of the sparse block data to the same limit used
for PAX headers (1 MiB).

Thanks to Harshit Gupta (Mr HAX) (https://www.linkedin.com/in/iam-harshit-gupta/)
for reporting this issue.

Fixes CVE-2025-58183
For #75677
Fixes #75710

Change-Id: I70b907b584a7b8676df8a149a1db728ae681a770
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2800
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2967
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/709843
Reviewed-by: Carlos Amedee <carlos@golang.org>
TryBot-Bypass: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
2025-10-07 11:01:00 -07:00

1681 lines
47 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
import (
"bytes"
"compress/bzip2"
"errors"
"fmt"
"hash/crc32"
"io"
"maps"
"math"
"os"
"path"
"reflect"
"slices"
"strconv"
"strings"
"testing"
"time"
)
func TestReader(t *testing.T) {
vectors := []struct {
file string // Test input file
headers []*Header // Expected output headers
chksums []string // CRC32 checksum of files, leave as nil if not checked
err error // Expected error to occur
}{{
file: "testdata/gnu.tar",
headers: []*Header{{
Name: "small.txt",
Mode: 0640,
Uid: 73025,
Gid: 5000,
Size: 5,
ModTime: time.Unix(1244428340, 0),
Typeflag: '0',
Uname: "dsymonds",
Gname: "eng",
Format: FormatGNU,
}, {
Name: "small2.txt",
Mode: 0640,
Uid: 73025,
Gid: 5000,
Size: 11,
ModTime: time.Unix(1244436044, 0),
Typeflag: '0',
Uname: "dsymonds",
Gname: "eng",
Format: FormatGNU,
}},
chksums: []string{
"6cbd88fc",
"ddac04b3",
},
}, {
file: "testdata/sparse-formats.tar",
headers: []*Header{{
Name: "sparse-gnu",
Mode: 420,
Uid: 1000,
Gid: 1000,
Size: 200,
ModTime: time.Unix(1392395740, 0),
Typeflag: 0x53,
Linkname: "",
Uname: "david",
Gname: "david",
Devmajor: 0,
Devminor: 0,
Format: FormatGNU,
}, {
Name: "sparse-posix-0.0",
Mode: 420,
Uid: 1000,
Gid: 1000,
Size: 200,
ModTime: time.Unix(1392342187, 0),
Typeflag: 0x30,
Linkname: "",
Uname: "david",
Gname: "david",
Devmajor: 0,
Devminor: 0,
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,
}, {
Name: "sparse-posix-0.1",
Mode: 420,
Uid: 1000,
Gid: 1000,
Size: 200,
ModTime: time.Unix(1392340456, 0),
Typeflag: 0x30,
Linkname: "",
Uname: "david",
Gname: "david",
Devmajor: 0,
Devminor: 0,
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,
}, {
Name: "sparse-posix-1.0",
Mode: 420,
Uid: 1000,
Gid: 1000,
Size: 200,
ModTime: time.Unix(1392337404, 0),
Typeflag: 0x30,
Linkname: "",
Uname: "david",
Gname: "david",
Devmajor: 0,
Devminor: 0,
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,
}, {
Name: "end",
Mode: 420,
Uid: 1000,
Gid: 1000,
Size: 4,
ModTime: time.Unix(1392398319, 0),
Typeflag: 0x30,
Linkname: "",
Uname: "david",
Gname: "david",
Devmajor: 0,
Devminor: 0,
Format: FormatGNU,
}},
chksums: []string{
"5375e1d2",
"5375e1d2",
"5375e1d2",
"5375e1d2",
"8eb179ba",
},
}, {
file: "testdata/star.tar",
headers: []*Header{{
Name: "small.txt",
Mode: 0640,
Uid: 73025,
Gid: 5000,
Size: 5,
ModTime: time.Unix(1244592783, 0),
Typeflag: '0',
Uname: "dsymonds",
Gname: "eng",
AccessTime: time.Unix(1244592783, 0),
ChangeTime: time.Unix(1244592783, 0),
}, {
Name: "small2.txt",
Mode: 0640,
Uid: 73025,
Gid: 5000,
Size: 11,
ModTime: time.Unix(1244592783, 0),
Typeflag: '0',
Uname: "dsymonds",
Gname: "eng",
AccessTime: time.Unix(1244592783, 0),
ChangeTime: time.Unix(1244592783, 0),
}},
}, {
file: "testdata/v7.tar",
headers: []*Header{{
Name: "small.txt",
Mode: 0444,
Uid: 73025,
Gid: 5000,
Size: 5,
ModTime: time.Unix(1244593104, 0),
Typeflag: '0',
}, {
Name: "small2.txt",
Mode: 0444,
Uid: 73025,
Gid: 5000,
Size: 11,
ModTime: time.Unix(1244593104, 0),
Typeflag: '0',
}},
}, {
file: "testdata/pax.tar",
headers: []*Header{{
Name: "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
Mode: 0664,
Uid: 1000,
Gid: 1000,
Uname: "shane",
Gname: "shane",
Size: 7,
ModTime: time.Unix(1350244992, 23960108),
ChangeTime: time.Unix(1350244992, 23960108),
AccessTime: time.Unix(1350244992, 23960108),
Typeflag: TypeReg,
PAXRecords: map[string]string{
"path": "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
"mtime": "1350244992.023960108",
"atime": "1350244992.023960108",
"ctime": "1350244992.023960108",
},
Format: FormatPAX,
}, {
Name: "a/b",
Mode: 0777,
Uid: 1000,
Gid: 1000,
Uname: "shane",
Gname: "shane",
Size: 0,
ModTime: time.Unix(1350266320, 910238425),
ChangeTime: time.Unix(1350266320, 910238425),
AccessTime: time.Unix(1350266320, 910238425),
Typeflag: TypeSymlink,
Linkname: "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
PAXRecords: map[string]string{
"linkpath": "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
"mtime": "1350266320.910238425",
"atime": "1350266320.910238425",
"ctime": "1350266320.910238425",
},
Format: FormatPAX,
}},
}, {
file: "testdata/pax-bad-hdr-file.tar",
err: ErrHeader,
}, {
file: "testdata/pax-bad-hdr-large.tar.bz2",
err: ErrFieldTooLong,
}, {
file: "testdata/pax-bad-mtime-file.tar",
err: ErrHeader,
}, {
file: "testdata/pax-pos-size-file.tar",
headers: []*Header{{
Name: "foo",
Mode: 0640,
Uid: 319973,
Gid: 5000,
Size: 999,
ModTime: time.Unix(1442282516, 0),
Typeflag: '0',
Uname: "joetsai",
Gname: "eng",
PAXRecords: map[string]string{
"size": "000000000000000000000999",
},
Format: FormatPAX,
}},
chksums: []string{
"5fd7e86a",
},
}, {
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/pax-global-records.tar",
headers: []*Header{{
Typeflag: TypeXGlobalHeader,
Name: "global1",
PAXRecords: map[string]string{"path": "global1", "mtime": "1500000000.0"},
Format: FormatPAX,
}, {
Typeflag: TypeReg,
Name: "file1",
ModTime: time.Unix(0, 0),
Format: FormatUSTAR,
}, {
Typeflag: TypeReg,
Name: "file2",
PAXRecords: map[string]string{"path": "file2"},
ModTime: time.Unix(0, 0),
Format: FormatPAX,
}, {
Typeflag: TypeXGlobalHeader,
Name: "GlobalHead.0.0",
PAXRecords: map[string]string{"path": ""},
Format: FormatPAX,
}, {
Typeflag: TypeReg,
Name: "file3",
ModTime: time.Unix(0, 0),
Format: FormatUSTAR,
}, {
Typeflag: TypeReg,
Name: "file4",
ModTime: time.Unix(1400000000, 0),
PAXRecords: map[string]string{"mtime": "1400000000"},
Format: FormatPAX,
}},
}, {
file: "testdata/nil-uid.tar", // golang.org/issue/5290
headers: []*Header{{
Name: "P1050238.JPG.log",
Mode: 0664,
Uid: 0,
Gid: 0,
Size: 14,
ModTime: time.Unix(1365454838, 0),
Typeflag: TypeReg,
Linkname: "",
Uname: "eyefi",
Gname: "eyefi",
Devmajor: 0,
Devminor: 0,
Format: FormatGNU,
}},
}, {
file: "testdata/xattrs.tar",
headers: []*Header{{
Name: "small.txt",
Mode: 0644,
Uid: 1000,
Gid: 10,
Size: 5,
ModTime: time.Unix(1386065770, 448252320),
Typeflag: '0',
Uname: "alex",
Gname: "wheel",
AccessTime: time.Unix(1389782991, 419875220),
ChangeTime: time.Unix(1389782956, 794414986),
Xattrs: map[string]string{
"user.key": "value",
"user.key2": "value2",
// Interestingly, selinux encodes the terminating null inside the xattr
"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,
}, {
Name: "small2.txt",
Mode: 0644,
Uid: 1000,
Gid: 10,
Size: 11,
ModTime: time.Unix(1386065770, 449252304),
Typeflag: '0',
Uname: "alex",
Gname: "wheel",
AccessTime: time.Unix(1389782991, 419875220),
ChangeTime: time.Unix(1386065770, 449252304),
Xattrs: map[string]string{
"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,
}},
}, {
// Matches the behavior of GNU, BSD, and STAR tar utilities.
file: "testdata/gnu-multi-hdrs.tar",
headers: []*Header{{
Name: "GNU2/GNU2/long-path-name",
Linkname: "GNU4/GNU4/long-linkpath-name",
ModTime: time.Unix(0, 0),
Typeflag: '2',
Format: FormatGNU,
}},
}, {
// GNU tar file with atime and ctime fields set.
// Created with the GNU tar v1.27.1.
// tar --incremental -S -cvf gnu-incremental.tar test2
file: "testdata/gnu-incremental.tar",
headers: []*Header{{
Name: "test2/",
Mode: 16877,
Uid: 1000,
Gid: 1000,
Size: 14,
ModTime: time.Unix(1441973427, 0),
Typeflag: 'D',
Uname: "rawr",
Gname: "dsnet",
AccessTime: time.Unix(1441974501, 0),
ChangeTime: time.Unix(1441973436, 0),
Format: FormatGNU,
}, {
Name: "test2/foo",
Mode: 33188,
Uid: 1000,
Gid: 1000,
Size: 64,
ModTime: time.Unix(1441973363, 0),
Typeflag: '0',
Uname: "rawr",
Gname: "dsnet",
AccessTime: time.Unix(1441974501, 0),
ChangeTime: time.Unix(1441973436, 0),
Format: FormatGNU,
}, {
Name: "test2/sparse",
Mode: 33188,
Uid: 1000,
Gid: 1000,
Size: 536870912,
ModTime: time.Unix(1441973427, 0),
Typeflag: 'S',
Uname: "rawr",
Gname: "dsnet",
AccessTime: time.Unix(1441991948, 0),
ChangeTime: time.Unix(1441973436, 0),
Format: FormatGNU,
}},
}, {
// Matches the behavior of GNU and BSD tar utilities.
file: "testdata/pax-multi-hdrs.tar",
headers: []*Header{{
Name: "bar",
Linkname: "PAX4/PAX4/long-linkpath-name",
ModTime: time.Unix(0, 0),
Typeflag: '2',
PAXRecords: map[string]string{
"linkpath": "PAX4/PAX4/long-linkpath-name",
},
Format: FormatPAX,
}},
}, {
// Both BSD and GNU tar truncate long names at first NUL even
// if there is data following that NUL character.
// This is reasonable as GNU long names are C-strings.
file: "testdata/gnu-long-nul.tar",
headers: []*Header{{
Name: "0123456789",
Mode: 0644,
Uid: 1000,
Gid: 1000,
ModTime: time.Unix(1486082191, 0),
Typeflag: '0',
Uname: "rawr",
Gname: "dsnet",
Format: FormatGNU,
}},
}, {
// 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: "âš¹",
Format: FormatGNU,
}},
}, {
// 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",
Format: FormatGNU,
}},
}, {
// BSD tar v3.1.2 and GNU tar v1.27.1 both rejects PAX records
// with NULs in the key.
file: "testdata/pax-nul-xattrs.tar",
err: ErrHeader,
}, {
// BSD tar v3.1.2 rejects a PAX path with NUL in the value, while
// GNU tar v1.27.1 simply truncates at first NUL.
// We emulate the behavior of BSD since it is strange doing NUL
// truncations since PAX records are length-prefix strings instead
// of NUL-terminated C-strings.
file: "testdata/pax-nul-path.tar",
err: ErrHeader,
}, {
file: "testdata/neg-size.tar",
err: ErrHeader,
}, {
file: "testdata/issue10968.tar",
err: ErrHeader,
}, {
file: "testdata/issue11169.tar",
err: ErrHeader,
}, {
file: "testdata/issue12435.tar",
err: ErrHeader,
}, {
// Ensure that we can read back the original Header as written with
// a buggy pre-Go1.8 tar.Writer.
file: "testdata/invalid-go17.tar",
headers: []*Header{{
Name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
Uid: 010000000,
ModTime: time.Unix(0, 0),
Typeflag: '0',
}},
}, {
// USTAR archive with a regular entry with non-zero device numbers.
file: "testdata/ustar-file-devs.tar",
headers: []*Header{{
Name: "file",
Mode: 0644,
Typeflag: '0',
ModTime: time.Unix(0, 0),
Devmajor: 1,
Devminor: 1,
Format: FormatUSTAR,
}},
}, {
// Generated by Go, works on BSD tar v3.1.2 and GNU tar v.1.27.1.
file: "testdata/gnu-nil-sparse-data.tar",
headers: []*Header{{
Name: "sparse.db",
Typeflag: TypeGNUSparse,
Size: 1000,
ModTime: time.Unix(0, 0),
Format: FormatGNU,
}},
}, {
// Generated by Go, works on BSD tar v3.1.2 and GNU tar v.1.27.1.
file: "testdata/gnu-nil-sparse-hole.tar",
headers: []*Header{{
Name: "sparse.db",
Typeflag: TypeGNUSparse,
Size: 1000,
ModTime: time.Unix(0, 0),
Format: FormatGNU,
}},
}, {
// Generated by Go, works on BSD tar v3.1.2 and GNU tar v.1.27.1.
file: "testdata/pax-nil-sparse-data.tar",
headers: []*Header{{
Name: "sparse.db",
Typeflag: TypeReg,
Size: 1000,
ModTime: time.Unix(0, 0),
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.
file: "testdata/pax-nil-sparse-hole.tar",
headers: []*Header{{
Name: "sparse.db",
Typeflag: TypeReg,
Size: 1000,
ModTime: time.Unix(0, 0),
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,
}},
}, {
file: "testdata/trailing-slash.tar",
headers: []*Header{{
Typeflag: TypeDir,
Name: strings.Repeat("123456789/", 30),
ModTime: time.Unix(0, 0),
PAXRecords: map[string]string{
"path": strings.Repeat("123456789/", 30),
},
Format: FormatPAX,
}},
}, {
// Small compressed file that uncompresses to
// a file with a very large GNU 1.0 sparse map.
file: "testdata/gnu-sparse-many-zeros.tar.bz2",
err: errSparseTooLong,
}}
for _, v := range vectors {
t.Run(path.Base(v.file), func(t *testing.T) {
f, err := os.Open(v.file)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer f.Close()
var fr io.Reader = f
if strings.HasSuffix(v.file, ".bz2") {
fr = bzip2.NewReader(fr)
}
// Capture all headers and checksums.
var (
tr = NewReader(fr)
hdrs []*Header
chksums []string
rdbuf = make([]byte, 8)
)
for {
var hdr *Header
hdr, err = tr.Next()
if err != nil {
if err == io.EOF {
err = nil // Expected error
}
break
}
hdrs = append(hdrs, hdr)
if v.chksums == nil {
continue
}
h := crc32.NewIEEE()
_, err = io.CopyBuffer(h, tr, rdbuf) // Effectively an incremental read
if err != nil {
break
}
chksums = append(chksums, fmt.Sprintf("%x", h.Sum(nil)))
}
for i, hdr := range hdrs {
if i >= len(v.headers) {
t.Fatalf("entry %d: unexpected header:\ngot %+v", i, *hdr)
}
if !reflect.DeepEqual(*hdr, *v.headers[i]) {
t.Fatalf("entry %d: incorrect header:\ngot %+v\nwant %+v", i, *hdr, *v.headers[i])
}
}
if len(hdrs) != len(v.headers) {
t.Fatalf("got %d headers, want %d headers", len(hdrs), len(v.headers))
}
for i, sum := range chksums {
if i >= len(v.chksums) {
t.Fatalf("entry %d: unexpected sum: got %s", i, sum)
}
if sum != v.chksums[i] {
t.Fatalf("entry %d: incorrect checksum: got %s, want %s", i, sum, v.chksums[i])
}
}
if err != v.err {
t.Fatalf("unexpected error: got %v, want %v", err, v.err)
}
f.Close()
})
}
}
func TestPartialRead(t *testing.T) {
type testCase struct {
cnt int // Number of bytes to read
output string // Expected value of string read
}
vectors := []struct {
file string
cases []testCase
}{{
file: "testdata/gnu.tar",
cases: []testCase{
{4, "Kilt"},
{6, "Google"},
},
}, {
file: "testdata/sparse-formats.tar",
cases: []testCase{
{2, "\x00G"},
{4, "\x00G\x00o"},
{6, "\x00G\x00o\x00G"},
{8, "\x00G\x00o\x00G\x00o"},
{4, "end\n"},
},
}}
for _, v := range vectors {
t.Run(path.Base(v.file), func(t *testing.T) {
f, err := os.Open(v.file)
if err != nil {
t.Fatalf("Open() error: %v", err)
}
defer f.Close()
tr := NewReader(f)
for i, tc := range v.cases {
hdr, err := tr.Next()
if err != nil || hdr == nil {
t.Fatalf("entry %d, Next(): got %v, want %v", i, err, nil)
}
buf := make([]byte, tc.cnt)
if _, err := io.ReadFull(tr, buf); err != nil {
t.Fatalf("entry %d, ReadFull(): got %v, want %v", i, err, nil)
}
if string(buf) != tc.output {
t.Fatalf("entry %d, ReadFull(): got %q, want %q", i, string(buf), tc.output)
}
}
if _, err := tr.Next(); err != io.EOF {
t.Fatalf("Next(): got %v, want EOF", err)
}
})
}
}
func TestUninitializedRead(t *testing.T) {
f, err := os.Open("testdata/gnu.tar")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defer f.Close()
tr := NewReader(f)
_, err = tr.Read([]byte{})
if err == nil || err != io.EOF {
t.Errorf("Unexpected error: %v, wanted %v", err, io.EOF)
}
}
type reader struct{ io.Reader }
type readSeeker struct{ io.ReadSeeker }
type readBadSeeker struct{ io.ReadSeeker }
func (rbs *readBadSeeker) Seek(int64, int) (int64, error) { return 0, fmt.Errorf("illegal seek") }
// TestReadTruncation test the ending condition on various truncated files and
// that truncated files are still detected even if the underlying io.Reader
// satisfies io.Seeker.
func TestReadTruncation(t *testing.T) {
var ss []string
for _, p := range []string{
"testdata/gnu.tar",
"testdata/ustar-file-reg.tar",
"testdata/pax-path-hdr.tar",
"testdata/sparse-formats.tar",
} {
buf, err := os.ReadFile(p)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ss = append(ss, string(buf))
}
data1, data2, pax, sparse := ss[0], ss[1], ss[2], ss[3]
data2 += strings.Repeat("\x00", 10*512)
trash := strings.Repeat("garbage ", 64) // Exactly 512 bytes
vectors := []struct {
input string // Input stream
cnt int // Expected number of headers read
err error // Expected error outcome
}{
{"", 0, io.EOF}, // Empty file is a "valid" tar file
{data1[:511], 0, io.ErrUnexpectedEOF},
{data1[:512], 1, io.ErrUnexpectedEOF},
{data1[:1024], 1, io.EOF},
{data1[:1536], 2, io.ErrUnexpectedEOF},
{data1[:2048], 2, io.EOF},
{data1, 2, io.EOF},
{data1[:2048] + data2[:1536], 3, io.EOF},
{data2[:511], 0, io.ErrUnexpectedEOF},
{data2[:512], 1, io.ErrUnexpectedEOF},
{data2[:1195], 1, io.ErrUnexpectedEOF},
{data2[:1196], 1, io.EOF}, // Exact end of data and start of padding
{data2[:1200], 1, io.EOF},
{data2[:1535], 1, io.EOF},
{data2[:1536], 1, io.EOF}, // Exact end of padding
{data2[:1536] + trash[:1], 1, io.ErrUnexpectedEOF},
{data2[:1536] + trash[:511], 1, io.ErrUnexpectedEOF},
{data2[:1536] + trash, 1, ErrHeader},
{data2[:2048], 1, io.EOF}, // Exactly 1 empty block
{data2[:2048] + trash[:1], 1, io.ErrUnexpectedEOF},
{data2[:2048] + trash[:511], 1, io.ErrUnexpectedEOF},
{data2[:2048] + trash, 1, ErrHeader},
{data2[:2560], 1, io.EOF}, // Exactly 2 empty blocks (normal end-of-stream)
{data2[:2560] + trash[:1], 1, io.EOF},
{data2[:2560] + trash[:511], 1, io.EOF},
{data2[:2560] + trash, 1, io.EOF},
{data2[:3072], 1, io.EOF},
{pax, 0, io.EOF}, // PAX header without data is a "valid" tar file
{pax + trash[:1], 0, io.ErrUnexpectedEOF},
{pax + trash[:511], 0, io.ErrUnexpectedEOF},
{sparse[:511], 0, io.ErrUnexpectedEOF},
{sparse[:512], 0, io.ErrUnexpectedEOF},
{sparse[:3584], 1, io.EOF},
{sparse[:9200], 1, io.EOF}, // Terminate in padding of sparse header
{sparse[:9216], 1, io.EOF},
{sparse[:9728], 2, io.ErrUnexpectedEOF},
{sparse[:10240], 2, io.EOF},
{sparse[:11264], 2, io.ErrUnexpectedEOF},
{sparse, 5, io.EOF},
{sparse + trash, 5, io.EOF},
}
for i, v := range vectors {
for j := 0; j < 6; j++ {
var tr *Reader
var s1, s2 string
switch j {
case 0:
tr = NewReader(&reader{strings.NewReader(v.input)})
s1, s2 = "io.Reader", "auto"
case 1:
tr = NewReader(&reader{strings.NewReader(v.input)})
s1, s2 = "io.Reader", "manual"
case 2:
tr = NewReader(&readSeeker{strings.NewReader(v.input)})
s1, s2 = "io.ReadSeeker", "auto"
case 3:
tr = NewReader(&readSeeker{strings.NewReader(v.input)})
s1, s2 = "io.ReadSeeker", "manual"
case 4:
tr = NewReader(&readBadSeeker{strings.NewReader(v.input)})
s1, s2 = "ReadBadSeeker", "auto"
case 5:
tr = NewReader(&readBadSeeker{strings.NewReader(v.input)})
s1, s2 = "ReadBadSeeker", "manual"
}
var cnt int
var err error
for {
if _, err = tr.Next(); err != nil {
break
}
cnt++
if s2 == "manual" {
if _, err = tr.writeTo(io.Discard); err != nil {
break
}
}
}
if err != v.err {
t.Errorf("test %d, NewReader(%s) with %s discard: got %v, want %v",
i, s1, s2, err, v.err)
}
if cnt != v.cnt {
t.Errorf("test %d, NewReader(%s) with %s discard: got %d headers, want %d headers",
i, s1, s2, cnt, v.cnt)
}
}
}
}
// TestReadHeaderOnly tests that Reader does not attempt to read special
// header-only files.
func TestReadHeaderOnly(t *testing.T) {
f, err := os.Open("testdata/hdr-only.tar")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer f.Close()
var hdrs []*Header
tr := NewReader(f)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
t.Errorf("Next(): got %v, want %v", err, nil)
continue
}
hdrs = append(hdrs, hdr)
// If a special flag, we should read nothing.
cnt, _ := io.ReadFull(tr, []byte{0})
if cnt > 0 && hdr.Typeflag != TypeReg {
t.Errorf("ReadFull(...): got %d bytes, want 0 bytes", cnt)
}
}
// File is crafted with 16 entries. The later 8 are identical to the first
// 8 except that the size is set.
if len(hdrs) != 16 {
t.Fatalf("len(hdrs): got %d, want %d", len(hdrs), 16)
}
for i := 0; i < 8; i++ {
hdr1, hdr2 := hdrs[i+0], hdrs[i+8]
hdr1.Size, hdr2.Size = 0, 0
if !reflect.DeepEqual(*hdr1, *hdr2) {
t.Errorf("incorrect header:\ngot %+v\nwant %+v", *hdr1, *hdr2)
}
}
}
func TestMergePAX(t *testing.T) {
vectors := []struct {
in map[string]string
want *Header
ok bool
}{{
in: map[string]string{
"path": "a/b/c",
"uid": "1000",
"mtime": "1350244992.023960108",
},
want: &Header{
Name: "a/b/c",
Uid: 1000,
ModTime: time.Unix(1350244992, 23960108),
PAXRecords: map[string]string{
"path": "a/b/c",
"uid": "1000",
"mtime": "1350244992.023960108",
},
},
ok: true,
}, {
in: map[string]string{
"gid": "gtgergergersagersgers",
},
ok: false,
}, {
in: map[string]string{
"missing": "missing",
"SCHILY.xattr.key": "value",
},
want: &Header{
Xattrs: map[string]string{"key": "value"},
PAXRecords: map[string]string{
"missing": "missing",
"SCHILY.xattr.key": "value",
},
},
ok: true,
}}
for i, v := range vectors {
got := new(Header)
err := mergePAX(got, v.in)
if v.ok && !reflect.DeepEqual(*got, *v.want) {
t.Errorf("test %d, mergePAX(...):\ngot %+v\nwant %+v", i, *got, *v.want)
}
if ok := err == nil; ok != v.ok {
t.Errorf("test %d, mergePAX(...): got %v, want %v", i, ok, v.ok)
}
}
}
func TestParsePAX(t *testing.T) {
vectors := []struct {
in string
want map[string]string
ok bool
}{
{"", nil, true},
{"6 k=1\n", map[string]string{"k": "1"}, true},
{"10 a=name\n", map[string]string{"a": "name"}, true},
{"9 a=name\n", map[string]string{"a": "name"}, true},
{"30 mtime=1350244992.023960108\n", map[string]string{"mtime": "1350244992.023960108"}, true},
{"3 somelongkey=\n", nil, false},
{"50 tooshort=\n", nil, false},
{"13 key1=haha\n13 key2=nana\n13 key3=kaka\n",
map[string]string{"key1": "haha", "key2": "nana", "key3": "kaka"}, true},
{"13 key1=val1\n13 key2=val2\n8 key1=\n",
map[string]string{"key1": "", "key2": "val2"}, true},
{"22 GNU.sparse.size=10\n26 GNU.sparse.numblocks=2\n" +
"23 GNU.sparse.offset=1\n25 GNU.sparse.numbytes=2\n" +
"23 GNU.sparse.offset=3\n25 GNU.sparse.numbytes=4\n",
map[string]string{paxGNUSparseSize: "10", paxGNUSparseNumBlocks: "2", paxGNUSparseMap: "1,2,3,4"}, true},
{"22 GNU.sparse.size=10\n26 GNU.sparse.numblocks=1\n" +
"25 GNU.sparse.numbytes=2\n23 GNU.sparse.offset=1\n",
nil, false},
{"22 GNU.sparse.size=10\n26 GNU.sparse.numblocks=1\n" +
"25 GNU.sparse.offset=1,2\n25 GNU.sparse.numbytes=2\n",
nil, false},
}
for i, v := range vectors {
r := strings.NewReader(v.in)
got, err := parsePAX(r)
if !maps.Equal(got, v.want) && !(len(got) == 0 && len(v.want) == 0) {
t.Errorf("test %d, parsePAX():\ngot %v\nwant %v", i, got, v.want)
}
if ok := err == nil; ok != v.ok {
t.Errorf("test %d, parsePAX(): got %v, want %v", i, ok, v.ok)
}
}
}
func TestReadOldGNUSparseMap(t *testing.T) {
populateSparseMap := func(sa sparseArray, sps []string) []string {
for i := 0; len(sps) > 0 && i < sa.maxEntries(); i++ {
copy(sa.entry(i), sps[0])
sps = sps[1:]
}
if len(sps) > 0 {
copy(sa.isExtended(), "\x80")
}
return sps
}
makeInput := func(format Format, size string, sps ...string) (out []byte) {
// Write the initial GNU header.
var blk block
gnu := blk.toGNU()
sparse := gnu.sparse()
copy(gnu.realSize(), size)
sps = populateSparseMap(sparse, sps)
if format != FormatUnknown {
blk.setFormat(format)
}
out = append(out, blk[:]...)
// Write extended sparse blocks.
for len(sps) > 0 {
var blk block
sps = populateSparseMap(blk.toSparse(), sps)
out = append(out, blk[:]...)
}
return out
}
makeSparseStrings := func(sp []sparseEntry) (out []string) {
var f formatter
for _, s := range sp {
var b [24]byte
f.formatNumeric(b[:12], s.Offset)
f.formatNumeric(b[12:], s.Length)
out = append(out, string(b[:]))
}
return out
}
vectors := []struct {
input []byte
wantMap sparseDatas
wantSize int64
wantErr error
}{{
input: makeInput(FormatUnknown, ""),
wantErr: ErrHeader,
}, {
input: makeInput(FormatGNU, "1234", "fewa"),
wantSize: 01234,
wantErr: ErrHeader,
}, {
input: makeInput(FormatGNU, "0031"),
wantSize: 031,
}, {
input: makeInput(FormatGNU, "80"),
wantErr: ErrHeader,
}, {
input: makeInput(FormatGNU, "1234",
makeSparseStrings(sparseDatas{{0, 0}, {1, 1}})...),
wantMap: sparseDatas{{0, 0}, {1, 1}},
wantSize: 01234,
}, {
input: makeInput(FormatGNU, "1234",
append(makeSparseStrings(sparseDatas{{0, 0}, {1, 1}}), []string{"", "blah"}...)...),
wantMap: sparseDatas{{0, 0}, {1, 1}},
wantSize: 01234,
}, {
input: makeInput(FormatGNU, "3333",
makeSparseStrings(sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}})...),
wantMap: sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}},
wantSize: 03333,
}, {
input: makeInput(FormatGNU, "",
append(append(
makeSparseStrings(sparseDatas{{0, 1}, {2, 1}}),
[]string{"", ""}...),
makeSparseStrings(sparseDatas{{4, 1}, {6, 1}})...)...),
wantMap: sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}},
}, {
input: makeInput(FormatGNU, "",
makeSparseStrings(sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}, {8, 1}, {10, 1}})...)[:blockSize],
wantErr: io.ErrUnexpectedEOF,
}, {
input: makeInput(FormatGNU, "",
makeSparseStrings(sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}, {8, 1}, {10, 1}})...)[:3*blockSize/2],
wantErr: io.ErrUnexpectedEOF,
}, {
input: makeInput(FormatGNU, "",
makeSparseStrings(sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}, {8, 1}, {10, 1}})...),
wantMap: sparseDatas{{0, 1}, {2, 1}, {4, 1}, {6, 1}, {8, 1}, {10, 1}},
}, {
input: makeInput(FormatGNU, "",
makeSparseStrings(sparseDatas{{10 << 30, 512}, {20 << 30, 512}})...),
wantMap: sparseDatas{{10 << 30, 512}, {20 << 30, 512}},
}}
for i, v := range vectors {
var blk block
var hdr Header
v.input = v.input[copy(blk[:], v.input):]
tr := Reader{r: bytes.NewReader(v.input)}
got, err := tr.readOldGNUSparseMap(&hdr, &blk)
if !slices.Equal(got, v.wantMap) {
t.Errorf("test %d, readOldGNUSparseMap(): got %v, want %v", i, got, v.wantMap)
}
if err != v.wantErr {
t.Errorf("test %d, readOldGNUSparseMap() = %v, want %v", i, err, v.wantErr)
}
if hdr.Size != v.wantSize {
t.Errorf("test %d, Header.Size = %d, want %d", i, hdr.Size, v.wantSize)
}
}
}
func TestReadGNUSparsePAXHeaders(t *testing.T) {
padInput := func(s string) string {
return s + string(zeroBlock[:blockPadding(int64(len(s)))])
}
vectors := []struct {
inputData string
inputHdrs map[string]string
wantMap sparseDatas
wantSize int64
wantName string
wantErr error
}{{
inputHdrs: nil,
wantErr: nil,
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: strconv.FormatInt(math.MaxInt64, 10),
paxGNUSparseMap: "0,1,2,3",
},
wantErr: ErrHeader,
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: "4\x00",
paxGNUSparseMap: "0,1,2,3",
},
wantErr: ErrHeader,
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: "4",
paxGNUSparseMap: "0,1,2,3",
},
wantErr: ErrHeader,
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: "2",
paxGNUSparseMap: "0,1,2,3",
},
wantMap: sparseDatas{{0, 1}, {2, 3}},
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: "2",
paxGNUSparseMap: "0, 1,2,3",
},
wantErr: ErrHeader,
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: "2",
paxGNUSparseMap: "0,1,02,3",
paxGNUSparseRealSize: "4321",
},
wantMap: sparseDatas{{0, 1}, {2, 3}},
wantSize: 4321,
}, {
inputHdrs: map[string]string{
paxGNUSparseNumBlocks: "2",
paxGNUSparseMap: "0,one1,2,3",
},
wantErr: ErrHeader,
}, {
inputHdrs: map[string]string{
paxGNUSparseMajor: "0",
paxGNUSparseMinor: "0",
paxGNUSparseNumBlocks: "2",
paxGNUSparseMap: "0,1,2,3",
paxGNUSparseSize: "1234",
paxGNUSparseRealSize: "4321",
paxGNUSparseName: "realname",
},
wantMap: sparseDatas{{0, 1}, {2, 3}},
wantSize: 1234,
wantName: "realname",
}, {
inputHdrs: map[string]string{
paxGNUSparseMajor: "0",
paxGNUSparseMinor: "0",
paxGNUSparseNumBlocks: "1",
paxGNUSparseMap: "10737418240,512",
paxGNUSparseSize: "10737418240",
paxGNUSparseName: "realname",
},
wantMap: sparseDatas{{10737418240, 512}},
wantSize: 10737418240,
wantName: "realname",
}, {
inputHdrs: map[string]string{
paxGNUSparseMajor: "0",
paxGNUSparseMinor: "0",
paxGNUSparseNumBlocks: "0",
paxGNUSparseMap: "",
},
wantMap: sparseDatas{},
}, {
inputHdrs: map[string]string{
paxGNUSparseMajor: "0",
paxGNUSparseMinor: "1",
paxGNUSparseNumBlocks: "4",
paxGNUSparseMap: "0,5,10,5,20,5,30,5",
},
wantMap: sparseDatas{{0, 5}, {10, 5}, {20, 5}, {30, 5}},
}, {
inputHdrs: map[string]string{
paxGNUSparseMajor: "1",
paxGNUSparseMinor: "0",
paxGNUSparseNumBlocks: "4",
paxGNUSparseMap: "0,5,10,5,20,5,30,5",
},
wantErr: io.ErrUnexpectedEOF,
}, {
inputData: padInput("0\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: sparseDatas{},
}, {
inputData: padInput("0\n")[:blockSize-1] + "#",
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: sparseDatas{},
}, {
inputData: padInput("0"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantErr: io.ErrUnexpectedEOF,
}, {
inputData: padInput("ab\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantErr: ErrHeader,
}, {
inputData: padInput("1\n2\n3\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: sparseDatas{{2, 3}},
}, {
inputData: padInput("1\n2\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantErr: io.ErrUnexpectedEOF,
}, {
inputData: padInput("1\n2\n\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantErr: ErrHeader,
}, {
inputData: string(zeroBlock[:]) + padInput("0\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantErr: ErrHeader,
}, {
inputData: strings.Repeat("0", blockSize) + padInput("1\n5\n1\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: sparseDatas{{5, 1}},
}, {
inputData: padInput(fmt.Sprintf("%d\n", int64(math.MaxInt64))),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantErr: ErrHeader,
}, {
inputData: padInput(strings.Repeat("0", 300) + "1\n" + strings.Repeat("0", 1000) + "5\n" + strings.Repeat("0", 800) + "2\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: sparseDatas{{5, 2}},
}, {
inputData: padInput("2\n10737418240\n512\n21474836480\n512\n"),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: sparseDatas{{10737418240, 512}, {21474836480, 512}},
}, {
inputData: padInput("100\n" + func() string {
var ss []string
for i := 0; i < 100; i++ {
ss = append(ss, fmt.Sprintf("%d\n%d\n", int64(i)<<30, 512))
}
return strings.Join(ss, "")
}()),
inputHdrs: map[string]string{paxGNUSparseMajor: "1", paxGNUSparseMinor: "0"},
wantMap: func() (spd sparseDatas) {
for i := 0; i < 100; i++ {
spd = append(spd, sparseEntry{int64(i) << 30, 512})
}
return spd
}(),
}}
for i, v := range vectors {
var hdr Header
hdr.PAXRecords = v.inputHdrs
r := strings.NewReader(v.inputData + "#") // Add canary byte
tr := Reader{curr: &regFileReader{r, int64(r.Len())}}
got, err := tr.readGNUSparsePAXHeaders(&hdr)
if !slices.Equal(got, v.wantMap) {
t.Errorf("test %d, readGNUSparsePAXHeaders(): got %v, want %v", i, got, v.wantMap)
}
if err != v.wantErr {
t.Errorf("test %d, readGNUSparsePAXHeaders() = %v, want %v", i, err, v.wantErr)
}
if hdr.Size != v.wantSize {
t.Errorf("test %d, Header.Size = %d, want %d", i, hdr.Size, v.wantSize)
}
if hdr.Name != v.wantName {
t.Errorf("test %d, Header.Name = %s, want %s", i, hdr.Name, v.wantName)
}
if v.wantErr == nil && r.Len() == 0 {
t.Errorf("test %d, canary byte unexpectedly consumed", i)
}
}
}
// testNonEmptyReader wraps an io.Reader and ensures that
// Read is never called with an empty buffer.
type testNonEmptyReader struct{ io.Reader }
func (r testNonEmptyReader) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, errors.New("unexpected empty Read call")
}
return r.Reader.Read(b)
}
func TestFileReader(t *testing.T) {
type (
testRead struct { // Read(cnt) == (wantStr, wantErr)
cnt int
wantStr string
wantErr error
}
testWriteTo struct { // WriteTo(testFile{ops}) == (wantCnt, wantErr)
ops fileOps
wantCnt int64
wantErr error
}
testRemaining struct { // logicalRemaining() == wantLCnt, physicalRemaining() == wantPCnt
wantLCnt int64
wantPCnt int64
}
testFnc any // testRead | testWriteTo | testRemaining
)
type (
makeReg struct {
str string
size int64
}
makeSparse struct {
makeReg makeReg
spd sparseDatas
size int64
}
fileMaker any // makeReg | makeSparse
)
vectors := []struct {
maker fileMaker
tests []testFnc
}{{
maker: makeReg{"", 0},
tests: []testFnc{
testRemaining{0, 0},
testRead{0, "", io.EOF},
testRead{1, "", io.EOF},
testWriteTo{nil, 0, nil},
testRemaining{0, 0},
},
}, {
maker: makeReg{"", 1},
tests: []testFnc{
testRemaining{1, 1},
testRead{5, "", io.ErrUnexpectedEOF},
testWriteTo{nil, 0, io.ErrUnexpectedEOF},
testRemaining{1, 1},
},
}, {
maker: makeReg{"hello", 5},
tests: []testFnc{
testRemaining{5, 5},
testRead{5, "hello", io.EOF},
testRemaining{0, 0},
},
}, {
maker: makeReg{"hello, world", 50},
tests: []testFnc{
testRemaining{50, 50},
testRead{7, "hello, ", nil},
testRemaining{43, 43},
testRead{5, "world", nil},
testRemaining{38, 38},
testWriteTo{nil, 0, io.ErrUnexpectedEOF},
testRead{1, "", io.ErrUnexpectedEOF},
testRemaining{38, 38},
},
}, {
maker: makeReg{"hello, world", 5},
tests: []testFnc{
testRemaining{5, 5},
testRead{0, "", nil},
testRead{4, "hell", nil},
testRemaining{1, 1},
testWriteTo{fileOps{"o"}, 1, nil},
testRemaining{0, 0},
testWriteTo{nil, 0, nil},
testRead{0, "", io.EOF},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{0, 2}, {5, 3}}, 8},
tests: []testFnc{
testRemaining{8, 5},
testRead{3, "ab\x00", nil},
testRead{10, "\x00\x00cde", io.EOF},
testRemaining{0, 0},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{0, 2}, {5, 3}}, 8},
tests: []testFnc{
testRemaining{8, 5},
testWriteTo{fileOps{"ab", int64(3), "cde"}, 8, nil},
testRemaining{0, 0},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{0, 2}, {5, 3}}, 10},
tests: []testFnc{
testRemaining{10, 5},
testRead{100, "ab\x00\x00\x00cde\x00\x00", io.EOF},
testRemaining{0, 0},
},
}, {
maker: makeSparse{makeReg{"abc", 5}, sparseDatas{{0, 2}, {5, 3}}, 10},
tests: []testFnc{
testRemaining{10, 5},
testRead{100, "ab\x00\x00\x00c", io.ErrUnexpectedEOF},
testRemaining{4, 2},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 2}}, 8},
tests: []testFnc{
testRemaining{8, 5},
testRead{8, "\x00abc\x00\x00de", io.EOF},
testRemaining{0, 0},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 0}, {6, 0}, {6, 2}}, 8},
tests: []testFnc{
testRemaining{8, 5},
testRead{8, "\x00abc\x00\x00de", io.EOF},
testRemaining{0, 0},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 0}, {6, 0}, {6, 2}}, 8},
tests: []testFnc{
testRemaining{8, 5},
testWriteTo{fileOps{int64(1), "abc", int64(2), "de"}, 8, nil},
testRemaining{0, 0},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 2}}, 10},
tests: []testFnc{
testRead{100, "\x00abc\x00\x00de\x00\x00", io.EOF},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 2}}, 10},
tests: []testFnc{
testWriteTo{fileOps{int64(1), "abc", int64(2), "de", int64(1), "\x00"}, 10, nil},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 2}, {8, 0}, {8, 0}, {8, 0}, {8, 0}}, 10},
tests: []testFnc{
testRead{100, "\x00abc\x00\x00de\x00\x00", io.EOF},
},
}, {
maker: makeSparse{makeReg{"", 0}, sparseDatas{}, 2},
tests: []testFnc{
testRead{100, "\x00\x00", io.EOF},
},
}, {
maker: makeSparse{makeReg{"", 8}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00", io.ErrUnexpectedEOF},
},
}, {
maker: makeSparse{makeReg{"ab", 2}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00ab", errMissData},
},
}, {
maker: makeSparse{makeReg{"ab", 8}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00ab", io.ErrUnexpectedEOF},
},
}, {
maker: makeSparse{makeReg{"abc", 3}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00abc\x00\x00", errMissData},
},
}, {
maker: makeSparse{makeReg{"abc", 8}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00abc\x00\x00", io.ErrUnexpectedEOF},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00abc\x00\x00de", errMissData},
},
}, {
maker: makeSparse{makeReg{"abcde", 5}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testWriteTo{fileOps{int64(1), "abc", int64(2), "de"}, 8, errMissData},
},
}, {
maker: makeSparse{makeReg{"abcde", 8}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRead{100, "\x00abc\x00\x00de", io.ErrUnexpectedEOF},
},
}, {
maker: makeSparse{makeReg{"abcdefghEXTRA", 13}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRemaining{15, 13},
testRead{100, "\x00abc\x00\x00defgh\x00\x00\x00\x00", errUnrefData},
testWriteTo{nil, 0, errUnrefData},
testRemaining{0, 5},
},
}, {
maker: makeSparse{makeReg{"abcdefghEXTRA", 13}, sparseDatas{{1, 3}, {6, 5}}, 15},
tests: []testFnc{
testRemaining{15, 13},
testWriteTo{fileOps{int64(1), "abc", int64(2), "defgh", int64(4)}, 15, errUnrefData},
testRead{100, "", errUnrefData},
testRemaining{0, 5},
},
}}
for i, v := range vectors {
var fr fileReader
switch maker := v.maker.(type) {
case makeReg:
r := testNonEmptyReader{strings.NewReader(maker.str)}
fr = &regFileReader{r, maker.size}
case makeSparse:
if !validateSparseEntries(maker.spd, maker.size) {
t.Fatalf("invalid sparse map: %v", maker.spd)
}
sph := invertSparseEntries(maker.spd, maker.size)
r := testNonEmptyReader{strings.NewReader(maker.makeReg.str)}
fr = &regFileReader{r, maker.makeReg.size}
fr = &sparseFileReader{fr, sph, 0}
default:
t.Fatalf("test %d, unknown make operation: %T", i, maker)
}
for j, tf := range v.tests {
switch tf := tf.(type) {
case testRead:
b := make([]byte, tf.cnt)
n, err := fr.Read(b)
if got := string(b[:n]); got != tf.wantStr || err != tf.wantErr {
t.Errorf("test %d.%d, Read(%d):\ngot (%q, %v)\nwant (%q, %v)", i, j, tf.cnt, got, err, tf.wantStr, tf.wantErr)
}
case testWriteTo:
f := &testFile{ops: tf.ops}
got, err := fr.WriteTo(f)
if _, ok := err.(testError); ok {
t.Errorf("test %d.%d, WriteTo(): %v", i, j, err)
} else if got != tf.wantCnt || err != tf.wantErr {
t.Errorf("test %d.%d, WriteTo() = (%d, %v), want (%d, %v)", i, j, got, err, tf.wantCnt, tf.wantErr)
}
if len(f.ops) > 0 {
t.Errorf("test %d.%d, expected %d more operations", i, j, len(f.ops))
}
case testRemaining:
if got := fr.logicalRemaining(); got != tf.wantLCnt {
t.Errorf("test %d.%d, logicalRemaining() = %d, want %d", i, j, got, tf.wantLCnt)
}
if got := fr.physicalRemaining(); got != tf.wantPCnt {
t.Errorf("test %d.%d, physicalRemaining() = %d, want %d", i, j, got, tf.wantPCnt)
}
default:
t.Fatalf("test %d.%d, unknown test operation: %T", i, j, tf)
}
}
}
}
func TestInsecurePaths(t *testing.T) {
t.Setenv("GODEBUG", "tarinsecurepath=0")
for _, path := range []string{
"../foo",
"/foo",
"a/b/../../../c",
} {
var buf bytes.Buffer
tw := NewWriter(&buf)
tw.WriteHeader(&Header{
Name: path,
})
const securePath = "secure"
tw.WriteHeader(&Header{
Name: securePath,
})
tw.Close()
tr := NewReader(&buf)
h, err := tr.Next()
if err != ErrInsecurePath {
t.Errorf("tr.Next for file %q: got err %v, want ErrInsecurePath", path, err)
continue
}
if h.Name != path {
t.Errorf("tr.Next for file %q: got name %q, want %q", path, h.Name, path)
}
// Error should not be sticky.
h, err = tr.Next()
if err != nil {
t.Errorf("tr.Next for file %q: got err %v, want nil", securePath, err)
}
if h.Name != securePath {
t.Errorf("tr.Next for file %q: got name %q, want %q", securePath, h.Name, securePath)
}
}
}
func TestDisableInsecurePathCheck(t *testing.T) {
t.Setenv("GODEBUG", "tarinsecurepath=1")
var buf bytes.Buffer
tw := NewWriter(&buf)
const name = "/foo"
tw.WriteHeader(&Header{
Name: name,
})
tw.Close()
tr := NewReader(&buf)
h, err := tr.Next()
if err != nil {
t.Fatalf("tr.Next with tarinsecurepath=1: got err %v, want nil", err)
}
if h.Name != name {
t.Fatalf("tr.Next with tarinsecurepath=1: got name %q, want %q", h.Name, name)
}
}