1308 lines
36 KiB
Go
1308 lines
36 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"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"testing/iotest"
|
|
"time"
|
|
)
|
|
|
|
func bytediff(a, b []byte) string {
|
|
const (
|
|
uniqueA = "- "
|
|
uniqueB = "+ "
|
|
identity = " "
|
|
)
|
|
var ss []string
|
|
sa := strings.Split(strings.TrimSpace(hex.Dump(a)), "\n")
|
|
sb := strings.Split(strings.TrimSpace(hex.Dump(b)), "\n")
|
|
for len(sa) > 0 && len(sb) > 0 {
|
|
if sa[0] == sb[0] {
|
|
ss = append(ss, identity+sa[0])
|
|
} else {
|
|
ss = append(ss, uniqueA+sa[0])
|
|
ss = append(ss, uniqueB+sb[0])
|
|
}
|
|
sa, sb = sa[1:], sb[1:]
|
|
}
|
|
for len(sa) > 0 {
|
|
ss = append(ss, uniqueA+sa[0])
|
|
sa = sa[1:]
|
|
}
|
|
for len(sb) > 0 {
|
|
ss = append(ss, uniqueB+sb[0])
|
|
sb = sb[1:]
|
|
}
|
|
return strings.Join(ss, "\n")
|
|
}
|
|
|
|
func TestWriter(t *testing.T) {
|
|
type (
|
|
testHeader struct { // WriteHeader(hdr) == wantErr
|
|
hdr Header
|
|
wantErr error
|
|
}
|
|
testWrite struct { // Write(str) == (wantCnt, wantErr)
|
|
str string
|
|
wantCnt int
|
|
wantErr error
|
|
}
|
|
testReadFrom struct { // ReadFrom(testFile{ops}) == (wantCnt, wantErr)
|
|
ops fileOps
|
|
wantCnt int64
|
|
wantErr error
|
|
}
|
|
testClose struct { // Close() == wantErr
|
|
wantErr error
|
|
}
|
|
testFnc any // testHeader | testWrite | testReadFrom | testClose
|
|
)
|
|
|
|
vectors := []struct {
|
|
file string // Optional filename of expected output
|
|
tests []testFnc
|
|
}{{
|
|
// The writer test file was produced with this command:
|
|
// tar (GNU tar) 1.26
|
|
// ln -s small.txt link.txt
|
|
// tar -b 1 --format=ustar -c -f writer.tar small.txt small2.txt link.txt
|
|
file: "testdata/writer.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "small.txt",
|
|
Size: 5,
|
|
Mode: 0640,
|
|
Uid: 73025,
|
|
Gid: 5000,
|
|
Uname: "dsymonds",
|
|
Gname: "eng",
|
|
ModTime: time.Unix(1246508266, 0),
|
|
}, nil},
|
|
testWrite{"Kilts", 5, nil},
|
|
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "small2.txt",
|
|
Size: 11,
|
|
Mode: 0640,
|
|
Uid: 73025,
|
|
Uname: "dsymonds",
|
|
Gname: "eng",
|
|
Gid: 5000,
|
|
ModTime: time.Unix(1245217492, 0),
|
|
}, nil},
|
|
testWrite{"Google.com\n", 11, nil},
|
|
|
|
testHeader{Header{
|
|
Typeflag: TypeSymlink,
|
|
Name: "link.txt",
|
|
Linkname: "small.txt",
|
|
Mode: 0777,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Uname: "strings",
|
|
Gname: "strings",
|
|
ModTime: time.Unix(1314603082, 0),
|
|
}, nil},
|
|
testWrite{"", 0, nil},
|
|
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
// The truncated test file was produced using these commands:
|
|
// dd if=/dev/zero bs=1048576 count=16384 > /tmp/16gig.txt
|
|
// tar -b 1 -c -f- /tmp/16gig.txt | dd bs=512 count=8 > writer-big.tar
|
|
file: "testdata/writer-big.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "tmp/16gig.txt",
|
|
Size: 16 << 30,
|
|
Mode: 0640,
|
|
Uid: 73025,
|
|
Gid: 5000,
|
|
Uname: "dsymonds",
|
|
Gname: "eng",
|
|
ModTime: time.Unix(1254699560, 0),
|
|
Format: FormatGNU,
|
|
}, nil},
|
|
},
|
|
}, {
|
|
// This truncated file was produced using this library.
|
|
// It was verified to work with GNU tar 1.27.1 and BSD tar 3.1.2.
|
|
// dd if=/dev/zero bs=1G count=16 >> writer-big-long.tar
|
|
// gnutar -xvf writer-big-long.tar
|
|
// bsdtar -xvf writer-big-long.tar
|
|
//
|
|
// This file is in PAX format.
|
|
file: "testdata/writer-big-long.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: strings.Repeat("longname/", 15) + "16gig.txt",
|
|
Size: 16 << 30,
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Uname: "guillaume",
|
|
Gname: "guillaume",
|
|
ModTime: time.Unix(1399583047, 0),
|
|
}, nil},
|
|
},
|
|
}, {
|
|
// This file was produced using GNU tar v1.17.
|
|
// gnutar -b 4 --format=ustar (longname/)*15 + file.txt
|
|
file: "testdata/ustar.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: strings.Repeat("longname/", 15) + "file.txt",
|
|
Size: 6,
|
|
Mode: 0644,
|
|
Uid: 501,
|
|
Gid: 20,
|
|
Uname: "shane",
|
|
Gname: "staff",
|
|
ModTime: time.Unix(1360135598, 0),
|
|
}, nil},
|
|
testWrite{"hello\n", 6, nil},
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
// This file was produced using GNU tar v1.26:
|
|
// echo "Slartibartfast" > file.txt
|
|
// ln file.txt hard.txt
|
|
// tar -b 1 --format=ustar -c -f hardlink.tar file.txt hard.txt
|
|
file: "testdata/hardlink.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "file.txt",
|
|
Size: 15,
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 100,
|
|
Uname: "vbatts",
|
|
Gname: "users",
|
|
ModTime: time.Unix(1425484303, 0),
|
|
}, nil},
|
|
testWrite{"Slartibartfast\n", 15, nil},
|
|
|
|
testHeader{Header{
|
|
Typeflag: TypeLink,
|
|
Name: "hard.txt",
|
|
Linkname: "file.txt",
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 100,
|
|
Uname: "vbatts",
|
|
Gname: "users",
|
|
ModTime: time.Unix(1425484303, 0),
|
|
}, nil},
|
|
testWrite{"", 0, nil},
|
|
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "bad-null.txt",
|
|
Xattrs: map[string]string{"null\x00null\x00": "fizzbuzz"},
|
|
}, headerError{}},
|
|
},
|
|
}, {
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "null\x00.txt",
|
|
}, headerError{}},
|
|
},
|
|
}, {
|
|
file: "testdata/pax-records.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "file",
|
|
Uname: strings.Repeat("long", 10),
|
|
PAXRecords: map[string]string{
|
|
"path": "FILE", // Should be ignored
|
|
"GNU.sparse.map": "0,0", // Should be ignored
|
|
"comment": "Hello, 世界",
|
|
"GOLANG.pkg": "tar",
|
|
},
|
|
}, nil},
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
// Craft a theoretically valid PAX archive with global headers.
|
|
// The GNU and BSD tar tools do not parse these the same way.
|
|
//
|
|
// BSD tar v3.1.2 parses and ignores all global headers;
|
|
// the behavior is verified by researching the source code.
|
|
//
|
|
// $ bsdtar -tvf pax-global-records.tar
|
|
// ---------- 0 0 0 0 Dec 31 1969 file1
|
|
// ---------- 0 0 0 0 Dec 31 1969 file2
|
|
// ---------- 0 0 0 0 Dec 31 1969 file3
|
|
// ---------- 0 0 0 0 May 13 2014 file4
|
|
//
|
|
// GNU tar v1.27.1 applies global headers to subsequent records,
|
|
// but does not do the following properly:
|
|
// * It does not treat an empty record as deletion.
|
|
// * It does not use subsequent global headers to update previous ones.
|
|
//
|
|
// $ gnutar -tvf pax-global-records.tar
|
|
// ---------- 0/0 0 2017-07-13 19:40 global1
|
|
// ---------- 0/0 0 2017-07-13 19:40 file2
|
|
// gnutar: Substituting `.' for empty member name
|
|
// ---------- 0/0 0 1969-12-31 16:00
|
|
// gnutar: Substituting `.' for empty member name
|
|
// ---------- 0/0 0 2014-05-13 09:53
|
|
//
|
|
// According to the PAX specification, this should have been the result:
|
|
// ---------- 0/0 0 2017-07-13 19:40 global1
|
|
// ---------- 0/0 0 2017-07-13 19:40 file2
|
|
// ---------- 0/0 0 2017-07-13 19:40 file3
|
|
// ---------- 0/0 0 2014-05-13 09:53 file4
|
|
file: "testdata/pax-global-records.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeXGlobalHeader,
|
|
PAXRecords: map[string]string{"path": "global1", "mtime": "1500000000.0"},
|
|
}, nil},
|
|
testHeader{Header{
|
|
Typeflag: TypeReg, Name: "file1",
|
|
}, nil},
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "file2",
|
|
PAXRecords: map[string]string{"path": "file2"},
|
|
}, nil},
|
|
testHeader{Header{
|
|
Typeflag: TypeXGlobalHeader,
|
|
PAXRecords: map[string]string{"path": ""}, // Should delete "path", but keep "mtime"
|
|
}, nil},
|
|
testHeader{Header{
|
|
Typeflag: TypeReg, Name: "file3",
|
|
}, nil},
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "file4",
|
|
ModTime: time.Unix(1400000000, 0),
|
|
PAXRecords: map[string]string{"mtime": "1400000000"},
|
|
}, nil},
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
file: "testdata/gnu-utf8.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
|
|
Mode: 0644,
|
|
Uid: 1000, Gid: 1000,
|
|
Uname: "☺",
|
|
Gname: "⚹",
|
|
ModTime: time.Unix(0, 0),
|
|
Format: FormatGNU,
|
|
}, nil},
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
file: "testdata/gnu-not-utf8.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "hi\x80\x81\x82\x83bye",
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Uname: "rawr",
|
|
Gname: "dsnet",
|
|
ModTime: time.Unix(0, 0),
|
|
Format: FormatGNU,
|
|
}, nil},
|
|
testClose{nil},
|
|
},
|
|
// TODO(dsnet): Re-enable this test when adding sparse support.
|
|
// See https://golang.org/issue/22735
|
|
/*
|
|
}, {
|
|
file: "testdata/gnu-nil-sparse-data.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeGNUSparse,
|
|
Name: "sparse.db",
|
|
Size: 1000,
|
|
SparseHoles: []sparseEntry{{Offset: 1000, Length: 0}},
|
|
}, nil},
|
|
testWrite{strings.Repeat("0123456789", 100), 1000, nil},
|
|
testClose{},
|
|
},
|
|
}, {
|
|
file: "testdata/gnu-nil-sparse-hole.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeGNUSparse,
|
|
Name: "sparse.db",
|
|
Size: 1000,
|
|
SparseHoles: []sparseEntry{{Offset: 0, Length: 1000}},
|
|
}, nil},
|
|
testWrite{strings.Repeat("\x00", 1000), 1000, nil},
|
|
testClose{},
|
|
},
|
|
}, {
|
|
file: "testdata/pax-nil-sparse-data.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "sparse.db",
|
|
Size: 1000,
|
|
SparseHoles: []sparseEntry{{Offset: 1000, Length: 0}},
|
|
}, nil},
|
|
testWrite{strings.Repeat("0123456789", 100), 1000, nil},
|
|
testClose{},
|
|
},
|
|
}, {
|
|
file: "testdata/pax-nil-sparse-hole.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "sparse.db",
|
|
Size: 1000,
|
|
SparseHoles: []sparseEntry{{Offset: 0, Length: 1000}},
|
|
}, nil},
|
|
testWrite{strings.Repeat("\x00", 1000), 1000, nil},
|
|
testClose{},
|
|
},
|
|
}, {
|
|
file: "testdata/gnu-sparse-big.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeGNUSparse,
|
|
Name: "gnu-sparse",
|
|
Size: 6e10,
|
|
SparseHoles: []sparseEntry{
|
|
{Offset: 0e10, Length: 1e10 - 100},
|
|
{Offset: 1e10, Length: 1e10 - 100},
|
|
{Offset: 2e10, Length: 1e10 - 100},
|
|
{Offset: 3e10, Length: 1e10 - 100},
|
|
{Offset: 4e10, Length: 1e10 - 100},
|
|
{Offset: 5e10, Length: 1e10 - 100},
|
|
},
|
|
}, nil},
|
|
testReadFrom{fileOps{
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
}, 6e10, nil},
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
file: "testdata/pax-sparse-big.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{
|
|
Typeflag: TypeReg,
|
|
Name: "pax-sparse",
|
|
Size: 6e10,
|
|
SparseHoles: []sparseEntry{
|
|
{Offset: 0e10, Length: 1e10 - 100},
|
|
{Offset: 1e10, Length: 1e10 - 100},
|
|
{Offset: 2e10, Length: 1e10 - 100},
|
|
{Offset: 3e10, Length: 1e10 - 100},
|
|
{Offset: 4e10, Length: 1e10 - 100},
|
|
{Offset: 5e10, Length: 1e10 - 100},
|
|
},
|
|
}, nil},
|
|
testReadFrom{fileOps{
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
int64(1e10 - blockSize),
|
|
strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
|
|
}, 6e10, nil},
|
|
testClose{nil},
|
|
},
|
|
*/
|
|
}, {
|
|
file: "testdata/trailing-slash.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{Name: strings.Repeat("123456789/", 30)}, nil},
|
|
testClose{nil},
|
|
},
|
|
}, {
|
|
// Automatically promote zero value of Typeflag depending on the name.
|
|
file: "testdata/file-and-dir.tar",
|
|
tests: []testFnc{
|
|
testHeader{Header{Name: "small.txt", Size: 5}, nil},
|
|
testWrite{"Kilts", 5, nil},
|
|
testHeader{Header{Name: "dir/"}, nil},
|
|
testClose{nil},
|
|
},
|
|
}}
|
|
|
|
equalError := func(x, y error) bool {
|
|
_, ok1 := x.(headerError)
|
|
_, ok2 := y.(headerError)
|
|
if ok1 || ok2 {
|
|
return ok1 && ok2
|
|
}
|
|
return x == y
|
|
}
|
|
for _, v := range vectors {
|
|
t.Run(path.Base(v.file), func(t *testing.T) {
|
|
const maxSize = 10 << 10 // 10KiB
|
|
buf := new(bytes.Buffer)
|
|
tw := NewWriter(iotest.TruncateWriter(buf, maxSize))
|
|
|
|
for i, tf := range v.tests {
|
|
switch tf := tf.(type) {
|
|
case testHeader:
|
|
err := tw.WriteHeader(&tf.hdr)
|
|
if !equalError(err, tf.wantErr) {
|
|
t.Fatalf("test %d, WriteHeader() = %v, want %v", i, err, tf.wantErr)
|
|
}
|
|
case testWrite:
|
|
got, err := tw.Write([]byte(tf.str))
|
|
if got != tf.wantCnt || !equalError(err, tf.wantErr) {
|
|
t.Fatalf("test %d, Write() = (%d, %v), want (%d, %v)", i, got, err, tf.wantCnt, tf.wantErr)
|
|
}
|
|
case testReadFrom:
|
|
f := &testFile{ops: tf.ops}
|
|
got, err := tw.readFrom(f)
|
|
if _, ok := err.(testError); ok {
|
|
t.Errorf("test %d, ReadFrom(): %v", i, err)
|
|
} else if got != tf.wantCnt || !equalError(err, tf.wantErr) {
|
|
t.Errorf("test %d, ReadFrom() = (%d, %v), want (%d, %v)", i, got, err, tf.wantCnt, tf.wantErr)
|
|
}
|
|
if len(f.ops) > 0 {
|
|
t.Errorf("test %d, expected %d more operations", i, len(f.ops))
|
|
}
|
|
case testClose:
|
|
err := tw.Close()
|
|
if !equalError(err, tf.wantErr) {
|
|
t.Fatalf("test %d, Close() = %v, want %v", i, err, tf.wantErr)
|
|
}
|
|
default:
|
|
t.Fatalf("test %d, unknown test operation: %T", i, tf)
|
|
}
|
|
}
|
|
|
|
if v.file != "" {
|
|
want, err := os.ReadFile(v.file)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile() = %v, want nil", err)
|
|
}
|
|
got := buf.Bytes()
|
|
if !bytes.Equal(want, got) {
|
|
t.Fatalf("incorrect result: (-got +want)\n%v", bytediff(got, want))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPax(t *testing.T) {
|
|
// Create an archive with a large name
|
|
fileinfo, err := os.Stat("testdata/small.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hdr, err := FileInfoHeader(fileinfo, "")
|
|
if err != nil {
|
|
t.Fatalf("os.Stat: %v", err)
|
|
}
|
|
// Force a PAX long name to be written
|
|
longName := strings.Repeat("ab", 100)
|
|
contents := strings.Repeat(" ", int(hdr.Size))
|
|
hdr.Name = longName
|
|
var buf bytes.Buffer
|
|
writer := NewWriter(&buf)
|
|
if err := writer.WriteHeader(hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = writer.Write([]byte(contents)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Simple test to make sure PAX extensions are in effect
|
|
if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
|
|
t.Fatal("Expected at least one PAX header to be written.")
|
|
}
|
|
// Test that we can get a long name back out of the archive.
|
|
reader := NewReader(&buf)
|
|
hdr, err = reader.Next()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hdr.Name != longName {
|
|
t.Fatal("Couldn't recover long file name")
|
|
}
|
|
}
|
|
|
|
func TestPaxSymlink(t *testing.T) {
|
|
// Create an archive with a large linkname
|
|
fileinfo, err := os.Stat("testdata/small.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hdr, err := FileInfoHeader(fileinfo, "")
|
|
hdr.Typeflag = TypeSymlink
|
|
if err != nil {
|
|
t.Fatalf("os.Stat:1 %v", err)
|
|
}
|
|
// Force a PAX long linkname to be written
|
|
longLinkname := strings.Repeat("1234567890/1234567890", 10)
|
|
hdr.Linkname = longLinkname
|
|
|
|
hdr.Size = 0
|
|
var buf bytes.Buffer
|
|
writer := NewWriter(&buf)
|
|
if err := writer.WriteHeader(hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Simple test to make sure PAX extensions are in effect
|
|
if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
|
|
t.Fatal("Expected at least one PAX header to be written.")
|
|
}
|
|
// Test that we can get a long name back out of the archive.
|
|
reader := NewReader(&buf)
|
|
hdr, err = reader.Next()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hdr.Linkname != longLinkname {
|
|
t.Fatal("Couldn't recover long link name")
|
|
}
|
|
}
|
|
|
|
func TestPaxNonAscii(t *testing.T) {
|
|
// Create an archive with non ascii. These should trigger a pax header
|
|
// because pax headers have a defined utf-8 encoding.
|
|
fileinfo, err := os.Stat("testdata/small.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
hdr, err := FileInfoHeader(fileinfo, "")
|
|
if err != nil {
|
|
t.Fatalf("os.Stat:1 %v", err)
|
|
}
|
|
|
|
// some sample data
|
|
chineseFilename := "文件名"
|
|
chineseGroupname := "組"
|
|
chineseUsername := "用戶名"
|
|
|
|
hdr.Name = chineseFilename
|
|
hdr.Gname = chineseGroupname
|
|
hdr.Uname = chineseUsername
|
|
|
|
contents := strings.Repeat(" ", int(hdr.Size))
|
|
|
|
var buf bytes.Buffer
|
|
writer := NewWriter(&buf)
|
|
if err := writer.WriteHeader(hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = writer.Write([]byte(contents)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Simple test to make sure PAX extensions are in effect
|
|
if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
|
|
t.Fatal("Expected at least one PAX header to be written.")
|
|
}
|
|
// Test that we can get a long name back out of the archive.
|
|
reader := NewReader(&buf)
|
|
hdr, err = reader.Next()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hdr.Name != chineseFilename {
|
|
t.Fatal("Couldn't recover unicode name")
|
|
}
|
|
if hdr.Gname != chineseGroupname {
|
|
t.Fatal("Couldn't recover unicode group")
|
|
}
|
|
if hdr.Uname != chineseUsername {
|
|
t.Fatal("Couldn't recover unicode user")
|
|
}
|
|
}
|
|
|
|
func TestPaxXattrs(t *testing.T) {
|
|
xattrs := map[string]string{
|
|
"user.key": "value",
|
|
}
|
|
|
|
// Create an archive with an xattr
|
|
fileinfo, err := os.Stat("testdata/small.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hdr, err := FileInfoHeader(fileinfo, "")
|
|
if err != nil {
|
|
t.Fatalf("os.Stat: %v", err)
|
|
}
|
|
contents := "Kilts"
|
|
hdr.Xattrs = xattrs
|
|
var buf bytes.Buffer
|
|
writer := NewWriter(&buf)
|
|
if err := writer.WriteHeader(hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = writer.Write([]byte(contents)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Test that we can get the xattrs back out of the archive.
|
|
reader := NewReader(&buf)
|
|
hdr, err = reader.Next()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(hdr.Xattrs, xattrs) {
|
|
t.Fatalf("xattrs did not survive round trip: got %+v, want %+v",
|
|
hdr.Xattrs, xattrs)
|
|
}
|
|
}
|
|
|
|
func TestPaxHeadersSorted(t *testing.T) {
|
|
fileinfo, err := os.Stat("testdata/small.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hdr, err := FileInfoHeader(fileinfo, "")
|
|
if err != nil {
|
|
t.Fatalf("os.Stat: %v", err)
|
|
}
|
|
contents := strings.Repeat(" ", int(hdr.Size))
|
|
|
|
hdr.Xattrs = map[string]string{
|
|
"foo": "foo",
|
|
"bar": "bar",
|
|
"baz": "baz",
|
|
"qux": "qux",
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
writer := NewWriter(&buf)
|
|
if err := writer.WriteHeader(hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = writer.Write([]byte(contents)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Simple test to make sure PAX extensions are in effect
|
|
if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
|
|
t.Fatal("Expected at least one PAX header to be written.")
|
|
}
|
|
|
|
// xattr bar should always appear before others
|
|
indices := []int{
|
|
bytes.Index(buf.Bytes(), []byte("bar=bar")),
|
|
bytes.Index(buf.Bytes(), []byte("baz=baz")),
|
|
bytes.Index(buf.Bytes(), []byte("foo=foo")),
|
|
bytes.Index(buf.Bytes(), []byte("qux=qux")),
|
|
}
|
|
if !sort.IntsAreSorted(indices) {
|
|
t.Fatal("PAX headers are not sorted")
|
|
}
|
|
}
|
|
|
|
func TestUSTARLongName(t *testing.T) {
|
|
// Create an archive with a path that failed to split with USTAR extension in previous versions.
|
|
fileinfo, err := os.Stat("testdata/small.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hdr, err := FileInfoHeader(fileinfo, "")
|
|
hdr.Typeflag = TypeDir
|
|
if err != nil {
|
|
t.Fatalf("os.Stat:1 %v", err)
|
|
}
|
|
// Force a PAX long name to be written. The name was taken from a practical example
|
|
// that fails and replaced ever char through numbers to anonymize the sample.
|
|
longName := "/0000_0000000/00000-000000000/0000_0000000/00000-0000000000000/0000_0000000/00000-0000000-00000000/0000_0000000/00000000/0000_0000000/000/0000_0000000/00000000v00/0000_0000000/000000/0000_0000000/0000000/0000_0000000/00000y-00/0000/0000/00000000/0x000000/"
|
|
hdr.Name = longName
|
|
|
|
hdr.Size = 0
|
|
var buf bytes.Buffer
|
|
writer := NewWriter(&buf)
|
|
if err := writer.WriteHeader(hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Test that we can get a long name back out of the archive.
|
|
reader := NewReader(&buf)
|
|
hdr, err = reader.Next()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hdr.Name != longName {
|
|
t.Fatal("Couldn't recover long name")
|
|
}
|
|
}
|
|
|
|
func TestValidTypeflagWithPAXHeader(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
tw := NewWriter(&buffer)
|
|
|
|
fileName := strings.Repeat("ab", 100)
|
|
|
|
hdr := &Header{
|
|
Name: fileName,
|
|
Size: 4,
|
|
Typeflag: 0,
|
|
}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
t.Fatalf("Failed to write header: %s", err)
|
|
}
|
|
if _, err := tw.Write([]byte("fooo")); err != nil {
|
|
t.Fatalf("Failed to write the file's data: %s", err)
|
|
}
|
|
tw.Close()
|
|
|
|
tr := NewReader(&buffer)
|
|
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Failed to read header: %s", err)
|
|
}
|
|
if header.Typeflag != TypeReg {
|
|
t.Fatalf("Typeflag should've been %d, found %d", TypeReg, header.Typeflag)
|
|
}
|
|
}
|
|
}
|
|
|
|
// failOnceWriter fails exactly once and then always reports success.
|
|
type failOnceWriter bool
|
|
|
|
func (w *failOnceWriter) Write(b []byte) (int, error) {
|
|
if !*w {
|
|
return 0, io.ErrShortWrite
|
|
}
|
|
*w = true
|
|
return len(b), nil
|
|
}
|
|
|
|
func TestWriterErrors(t *testing.T) {
|
|
t.Run("HeaderOnly", func(t *testing.T) {
|
|
tw := NewWriter(new(bytes.Buffer))
|
|
hdr := &Header{Name: "dir/", Typeflag: TypeDir}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
t.Fatalf("WriteHeader() = %v, want nil", err)
|
|
}
|
|
if _, err := tw.Write([]byte{0x00}); err != ErrWriteTooLong {
|
|
t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
|
|
}
|
|
})
|
|
|
|
t.Run("NegativeSize", func(t *testing.T) {
|
|
tw := NewWriter(new(bytes.Buffer))
|
|
hdr := &Header{Name: "small.txt", Size: -1}
|
|
if err := tw.WriteHeader(hdr); err == nil {
|
|
t.Fatalf("WriteHeader() = nil, want non-nil error")
|
|
}
|
|
})
|
|
|
|
t.Run("BeforeHeader", func(t *testing.T) {
|
|
tw := NewWriter(new(bytes.Buffer))
|
|
if _, err := tw.Write([]byte("Kilts")); err != ErrWriteTooLong {
|
|
t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
|
|
}
|
|
})
|
|
|
|
t.Run("AfterClose", func(t *testing.T) {
|
|
tw := NewWriter(new(bytes.Buffer))
|
|
hdr := &Header{Name: "small.txt"}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
t.Fatalf("WriteHeader() = %v, want nil", err)
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Fatalf("Close() = %v, want nil", err)
|
|
}
|
|
if _, err := tw.Write([]byte("Kilts")); err != ErrWriteAfterClose {
|
|
t.Fatalf("Write() = %v, want %v", err, ErrWriteAfterClose)
|
|
}
|
|
if err := tw.Flush(); err != ErrWriteAfterClose {
|
|
t.Fatalf("Flush() = %v, want %v", err, ErrWriteAfterClose)
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Fatalf("Close() = %v, want nil", err)
|
|
}
|
|
})
|
|
|
|
t.Run("PrematureFlush", func(t *testing.T) {
|
|
tw := NewWriter(new(bytes.Buffer))
|
|
hdr := &Header{Name: "small.txt", Size: 5}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
t.Fatalf("WriteHeader() = %v, want nil", err)
|
|
}
|
|
if err := tw.Flush(); err == nil {
|
|
t.Fatalf("Flush() = %v, want non-nil error", err)
|
|
}
|
|
})
|
|
|
|
t.Run("PrematureClose", func(t *testing.T) {
|
|
tw := NewWriter(new(bytes.Buffer))
|
|
hdr := &Header{Name: "small.txt", Size: 5}
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
t.Fatalf("WriteHeader() = %v, want nil", err)
|
|
}
|
|
if err := tw.Close(); err == nil {
|
|
t.Fatalf("Close() = %v, want non-nil error", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Persistence", func(t *testing.T) {
|
|
tw := NewWriter(new(failOnceWriter))
|
|
if err := tw.WriteHeader(&Header{}); err != io.ErrShortWrite {
|
|
t.Fatalf("WriteHeader() = %v, want %v", err, io.ErrShortWrite)
|
|
}
|
|
if err := tw.WriteHeader(&Header{Name: "small.txt"}); err == nil {
|
|
t.Errorf("WriteHeader() = got %v, want non-nil error", err)
|
|
}
|
|
if _, err := tw.Write(nil); err == nil {
|
|
t.Errorf("Write() = %v, want non-nil error", err)
|
|
}
|
|
if err := tw.Flush(); err == nil {
|
|
t.Errorf("Flush() = %v, want non-nil error", err)
|
|
}
|
|
if err := tw.Close(); err == nil {
|
|
t.Errorf("Close() = %v, want non-nil error", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSplitUSTARPath(t *testing.T) {
|
|
sr := strings.Repeat
|
|
|
|
vectors := []struct {
|
|
input string // Input path
|
|
prefix string // Expected output prefix
|
|
suffix string // Expected output suffix
|
|
ok bool // Split success?
|
|
}{
|
|
{"", "", "", false},
|
|
{"abc", "", "", false},
|
|
{"用戶名", "", "", false},
|
|
{sr("a", nameSize), "", "", false},
|
|
{sr("a", nameSize) + "/", "", "", false},
|
|
{sr("a", nameSize) + "/a", sr("a", nameSize), "a", true},
|
|
{sr("a", prefixSize) + "/", "", "", false},
|
|
{sr("a", prefixSize) + "/a", sr("a", prefixSize), "a", true},
|
|
{sr("a", nameSize+1), "", "", false},
|
|
{sr("/", nameSize+1), sr("/", nameSize-1), "/", true},
|
|
{sr("a", prefixSize) + "/" + sr("b", nameSize),
|
|
sr("a", prefixSize), sr("b", nameSize), true},
|
|
{sr("a", prefixSize) + "//" + sr("b", nameSize), "", "", false},
|
|
{sr("a/", nameSize), sr("a/", 77) + "a", sr("a/", 22), true},
|
|
}
|
|
|
|
for _, v := range vectors {
|
|
prefix, suffix, ok := splitUSTARPath(v.input)
|
|
if prefix != v.prefix || suffix != v.suffix || ok != v.ok {
|
|
t.Errorf("splitUSTARPath(%q):\ngot (%q, %q, %v)\nwant (%q, %q, %v)",
|
|
v.input, prefix, suffix, ok, v.prefix, v.suffix, v.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIssue12594 tests that the Writer does not attempt to populate the prefix
|
|
// field when encoding a header in the GNU format. The prefix field is valid
|
|
// in USTAR and PAX, but not GNU.
|
|
func TestIssue12594(t *testing.T) {
|
|
names := []string{
|
|
"0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/file.txt",
|
|
"0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/33/file.txt",
|
|
"0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/333/file.txt",
|
|
"0/1/2/3/4/5/6/7/8/9/10/11/12/13/14/15/16/17/18/19/20/21/22/23/24/25/26/27/28/29/30/31/32/33/34/35/36/37/38/39/40/file.txt",
|
|
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/file.txt",
|
|
"/home/support/.openoffice.org/3/user/uno_packages/cache/registry/com.sun.star.comp.deployment.executable.PackageRegistryBackend",
|
|
}
|
|
|
|
for i, name := range names {
|
|
var b bytes.Buffer
|
|
|
|
tw := NewWriter(&b)
|
|
if err := tw.WriteHeader(&Header{
|
|
Name: name,
|
|
Uid: 1 << 25, // Prevent USTAR format
|
|
}); err != nil {
|
|
t.Errorf("test %d, unexpected WriteHeader error: %v", i, err)
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Errorf("test %d, unexpected Close error: %v", i, err)
|
|
}
|
|
|
|
// The prefix field should never appear in the GNU format.
|
|
var blk block
|
|
copy(blk[:], b.Bytes())
|
|
prefix := string(blk.toUSTAR().prefix())
|
|
prefix, _, _ = strings.Cut(prefix, "\x00") // Truncate at the NUL terminator
|
|
if blk.getFormat() == FormatGNU && len(prefix) > 0 && strings.HasPrefix(name, prefix) {
|
|
t.Errorf("test %d, found prefix in GNU format: %s", i, prefix)
|
|
}
|
|
|
|
tr := NewReader(&b)
|
|
hdr, err := tr.Next()
|
|
if err != nil {
|
|
t.Errorf("test %d, unexpected Next error: %v", i, err)
|
|
}
|
|
if hdr.Name != name {
|
|
t.Errorf("test %d, hdr.Name = %s, want %s", i, hdr.Name, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testNonEmptyWriter wraps an io.Writer and ensures that
|
|
// Write is never called with an empty buffer.
|
|
type testNonEmptyWriter struct{ io.Writer }
|
|
|
|
func (w testNonEmptyWriter) Write(b []byte) (int, error) {
|
|
if len(b) == 0 {
|
|
return 0, errors.New("unexpected empty Write call")
|
|
}
|
|
return w.Writer.Write(b)
|
|
}
|
|
|
|
func TestFileWriter(t *testing.T) {
|
|
type (
|
|
testWrite struct { // Write(str) == (wantCnt, wantErr)
|
|
str string
|
|
wantCnt int
|
|
wantErr error
|
|
}
|
|
testReadFrom struct { // ReadFrom(testFile{ops}) == (wantCnt, wantErr)
|
|
ops fileOps
|
|
wantCnt int64
|
|
wantErr error
|
|
}
|
|
testRemaining struct { // logicalRemaining() == wantLCnt, physicalRemaining() == wantPCnt
|
|
wantLCnt int64
|
|
wantPCnt int64
|
|
}
|
|
testFnc any // testWrite | testReadFrom | testRemaining
|
|
)
|
|
|
|
type (
|
|
makeReg struct {
|
|
size int64
|
|
wantStr string
|
|
}
|
|
makeSparse struct {
|
|
makeReg makeReg
|
|
sph sparseHoles
|
|
size int64
|
|
}
|
|
fileMaker any // makeReg | makeSparse
|
|
)
|
|
|
|
vectors := []struct {
|
|
maker fileMaker
|
|
tests []testFnc
|
|
}{{
|
|
maker: makeReg{0, ""},
|
|
tests: []testFnc{
|
|
testRemaining{0, 0},
|
|
testWrite{"", 0, nil},
|
|
testWrite{"a", 0, ErrWriteTooLong},
|
|
testReadFrom{fileOps{""}, 0, nil},
|
|
testReadFrom{fileOps{"a"}, 0, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeReg{1, "a"},
|
|
tests: []testFnc{
|
|
testRemaining{1, 1},
|
|
testWrite{"", 0, nil},
|
|
testWrite{"a", 1, nil},
|
|
testWrite{"bcde", 0, ErrWriteTooLong},
|
|
testWrite{"", 0, nil},
|
|
testReadFrom{fileOps{""}, 0, nil},
|
|
testReadFrom{fileOps{"a"}, 0, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeReg{5, "hello"},
|
|
tests: []testFnc{
|
|
testRemaining{5, 5},
|
|
testWrite{"hello", 5, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeReg{5, "\x00\x00\x00\x00\x00"},
|
|
tests: []testFnc{
|
|
testRemaining{5, 5},
|
|
testReadFrom{fileOps{"\x00\x00\x00\x00\x00"}, 5, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeReg{5, "\x00\x00\x00\x00\x00"},
|
|
tests: []testFnc{
|
|
testRemaining{5, 5},
|
|
testReadFrom{fileOps{"\x00\x00\x00\x00\x00extra"}, 5, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeReg{5, "abc\x00\x00"},
|
|
tests: []testFnc{
|
|
testRemaining{5, 5},
|
|
testWrite{"abc", 3, nil},
|
|
testRemaining{2, 2},
|
|
testReadFrom{fileOps{"\x00\x00"}, 2, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeReg{5, "\x00\x00abc"},
|
|
tests: []testFnc{
|
|
testRemaining{5, 5},
|
|
testWrite{"\x00\x00", 2, nil},
|
|
testRemaining{3, 3},
|
|
testWrite{"abc", 3, nil},
|
|
testReadFrom{fileOps{"z"}, 0, ErrWriteTooLong},
|
|
testWrite{"z", 0, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testRemaining{8, 5},
|
|
testWrite{"ab\x00\x00\x00cde", 8, nil},
|
|
testWrite{"a", 0, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testWrite{"ab\x00\x00\x00cdez", 8, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testWrite{"ab\x00", 3, nil},
|
|
testRemaining{5, 3},
|
|
testWrite{"\x00\x00cde", 5, nil},
|
|
testWrite{"a", 0, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testWrite{"ab", 2, nil},
|
|
testRemaining{6, 3},
|
|
testReadFrom{fileOps{int64(3), "cde"}, 6, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testReadFrom{fileOps{"ab", int64(3), "cde"}, 8, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testReadFrom{fileOps{"ab", int64(3), "cdeX"}, 8, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{4, "abcd"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testReadFrom{fileOps{"ab", int64(3), "cd"}, 7, io.ErrUnexpectedEOF},
|
|
testRemaining{1, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{4, "abcd"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testReadFrom{fileOps{"ab", int64(3), "cde"}, 7, errMissData},
|
|
testRemaining{1, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{6, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testReadFrom{fileOps{"ab", int64(3), "cde"}, 8, errUnrefData},
|
|
testRemaining{0, 1},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{4, "abcd"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testWrite{"ab", 2, nil},
|
|
testRemaining{6, 2},
|
|
testWrite{"\x00\x00\x00", 3, nil},
|
|
testRemaining{3, 2},
|
|
testWrite{"cde", 2, errMissData},
|
|
testRemaining{1, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{6, "abcde"}, sparseHoles{{2, 3}}, 8},
|
|
tests: []testFnc{
|
|
testWrite{"ab", 2, nil},
|
|
testRemaining{6, 4},
|
|
testWrite{"\x00\x00\x00", 3, nil},
|
|
testRemaining{3, 4},
|
|
testWrite{"cde", 3, errUnrefData},
|
|
testRemaining{0, 1},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testRemaining{7, 3},
|
|
testWrite{"\x00\x00abc\x00\x00", 7, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testRemaining{7, 3},
|
|
testReadFrom{fileOps{int64(2), "abc", int64(1), "\x00"}, 7, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{3, ""}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testWrite{"abcdefg", 0, errWriteHole},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testWrite{"\x00\x00abcde", 5, errWriteHole},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testWrite{"\x00\x00abc\x00\x00z", 7, ErrWriteTooLong},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testWrite{"\x00\x00", 2, nil},
|
|
testRemaining{5, 3},
|
|
testWrite{"abc", 3, nil},
|
|
testRemaining{2, 0},
|
|
testWrite{"\x00\x00", 2, nil},
|
|
testRemaining{0, 0},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{2, "ab"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testWrite{"\x00\x00", 2, nil},
|
|
testWrite{"abc", 2, errMissData},
|
|
testWrite{"\x00\x00", 0, errMissData},
|
|
},
|
|
}, {
|
|
maker: makeSparse{makeReg{4, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
|
|
tests: []testFnc{
|
|
testWrite{"\x00\x00", 2, nil},
|
|
testWrite{"abc", 3, nil},
|
|
testWrite{"\x00\x00", 2, errUnrefData},
|
|
},
|
|
}}
|
|
|
|
for i, v := range vectors {
|
|
var wantStr string
|
|
bb := new(bytes.Buffer)
|
|
w := testNonEmptyWriter{bb}
|
|
var fw fileWriter
|
|
switch maker := v.maker.(type) {
|
|
case makeReg:
|
|
fw = ®FileWriter{w, maker.size}
|
|
wantStr = maker.wantStr
|
|
case makeSparse:
|
|
if !validateSparseEntries(maker.sph, maker.size) {
|
|
t.Fatalf("invalid sparse map: %v", maker.sph)
|
|
}
|
|
spd := invertSparseEntries(maker.sph, maker.size)
|
|
fw = ®FileWriter{w, maker.makeReg.size}
|
|
fw = &sparseFileWriter{fw, spd, 0}
|
|
wantStr = maker.makeReg.wantStr
|
|
default:
|
|
t.Fatalf("test %d, unknown make operation: %T", i, maker)
|
|
}
|
|
|
|
for j, tf := range v.tests {
|
|
switch tf := tf.(type) {
|
|
case testWrite:
|
|
got, err := fw.Write([]byte(tf.str))
|
|
if got != tf.wantCnt || err != tf.wantErr {
|
|
t.Errorf("test %d.%d, Write(%s):\ngot (%d, %v)\nwant (%d, %v)", i, j, tf.str, got, err, tf.wantCnt, tf.wantErr)
|
|
}
|
|
case testReadFrom:
|
|
f := &testFile{ops: tf.ops}
|
|
got, err := fw.ReadFrom(f)
|
|
if _, ok := err.(testError); ok {
|
|
t.Errorf("test %d.%d, ReadFrom(): %v", i, j, err)
|
|
} else if got != tf.wantCnt || err != tf.wantErr {
|
|
t.Errorf("test %d.%d, ReadFrom() = (%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 := fw.logicalRemaining(); got != tf.wantLCnt {
|
|
t.Errorf("test %d.%d, logicalRemaining() = %d, want %d", i, j, got, tf.wantLCnt)
|
|
}
|
|
if got := fw.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)
|
|
}
|
|
}
|
|
|
|
if got := bb.String(); got != wantStr {
|
|
t.Fatalf("test %d, String() = %q, want %q", i, got, wantStr)
|
|
}
|
|
}
|
|
}
|