1// Copyright 2009 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package tar
6
7import (
8	"bytes"
9	"encoding/hex"
10	"errors"
11	"io"
12	"os"
13	"path"
14	"reflect"
15	"sort"
16	"strings"
17	"testing"
18	"testing/iotest"
19	"time"
20)
21
22func bytediff(a, b []byte) string {
23	const (
24		uniqueA  = "-  "
25		uniqueB  = "+  "
26		identity = "   "
27	)
28	var ss []string
29	sa := strings.Split(strings.TrimSpace(hex.Dump(a)), "\n")
30	sb := strings.Split(strings.TrimSpace(hex.Dump(b)), "\n")
31	for len(sa) > 0 && len(sb) > 0 {
32		if sa[0] == sb[0] {
33			ss = append(ss, identity+sa[0])
34		} else {
35			ss = append(ss, uniqueA+sa[0])
36			ss = append(ss, uniqueB+sb[0])
37		}
38		sa, sb = sa[1:], sb[1:]
39	}
40	for len(sa) > 0 {
41		ss = append(ss, uniqueA+sa[0])
42		sa = sa[1:]
43	}
44	for len(sb) > 0 {
45		ss = append(ss, uniqueB+sb[0])
46		sb = sb[1:]
47	}
48	return strings.Join(ss, "\n")
49}
50
51func TestWriter(t *testing.T) {
52	type (
53		testHeader struct { // WriteHeader(hdr) == wantErr
54			hdr     Header
55			wantErr error
56		}
57		testWrite struct { // Write(str) == (wantCnt, wantErr)
58			str     string
59			wantCnt int
60			wantErr error
61		}
62		testReadFrom struct { // ReadFrom(testFile{ops}) == (wantCnt, wantErr)
63			ops     fileOps
64			wantCnt int64
65			wantErr error
66		}
67		testClose struct { // Close() == wantErr
68			wantErr error
69		}
70		testFnc interface{} // testHeader | testWrite | testReadFrom | testClose
71	)
72
73	vectors := []struct {
74		file  string // Optional filename of expected output
75		tests []testFnc
76	}{{
77		// The writer test file was produced with this command:
78		// tar (GNU tar) 1.26
79		//   ln -s small.txt link.txt
80		//   tar -b 1 --format=ustar -c -f writer.tar small.txt small2.txt link.txt
81		file: "testdata/writer.tar",
82		tests: []testFnc{
83			testHeader{Header{
84				Typeflag: TypeReg,
85				Name:     "small.txt",
86				Size:     5,
87				Mode:     0640,
88				Uid:      73025,
89				Gid:      5000,
90				Uname:    "dsymonds",
91				Gname:    "eng",
92				ModTime:  time.Unix(1246508266, 0),
93			}, nil},
94			testWrite{"Kilts", 5, nil},
95
96			testHeader{Header{
97				Typeflag: TypeReg,
98				Name:     "small2.txt",
99				Size:     11,
100				Mode:     0640,
101				Uid:      73025,
102				Uname:    "dsymonds",
103				Gname:    "eng",
104				Gid:      5000,
105				ModTime:  time.Unix(1245217492, 0),
106			}, nil},
107			testWrite{"Google.com\n", 11, nil},
108
109			testHeader{Header{
110				Typeflag: TypeSymlink,
111				Name:     "link.txt",
112				Linkname: "small.txt",
113				Mode:     0777,
114				Uid:      1000,
115				Gid:      1000,
116				Uname:    "strings",
117				Gname:    "strings",
118				ModTime:  time.Unix(1314603082, 0),
119			}, nil},
120			testWrite{"", 0, nil},
121
122			testClose{nil},
123		},
124	}, {
125		// The truncated test file was produced using these commands:
126		//   dd if=/dev/zero bs=1048576 count=16384 > /tmp/16gig.txt
127		//   tar -b 1 -c -f- /tmp/16gig.txt | dd bs=512 count=8 > writer-big.tar
128		file: "testdata/writer-big.tar",
129		tests: []testFnc{
130			testHeader{Header{
131				Typeflag: TypeReg,
132				Name:     "tmp/16gig.txt",
133				Size:     16 << 30,
134				Mode:     0640,
135				Uid:      73025,
136				Gid:      5000,
137				Uname:    "dsymonds",
138				Gname:    "eng",
139				ModTime:  time.Unix(1254699560, 0),
140				Format:   FormatGNU,
141			}, nil},
142		},
143	}, {
144		// This truncated file was produced using this library.
145		// It was verified to work with GNU tar 1.27.1 and BSD tar 3.1.2.
146		//  dd if=/dev/zero bs=1G count=16 >> writer-big-long.tar
147		//  gnutar -xvf writer-big-long.tar
148		//  bsdtar -xvf writer-big-long.tar
149		//
150		// This file is in PAX format.
151		file: "testdata/writer-big-long.tar",
152		tests: []testFnc{
153			testHeader{Header{
154				Typeflag: TypeReg,
155				Name:     strings.Repeat("longname/", 15) + "16gig.txt",
156				Size:     16 << 30,
157				Mode:     0644,
158				Uid:      1000,
159				Gid:      1000,
160				Uname:    "guillaume",
161				Gname:    "guillaume",
162				ModTime:  time.Unix(1399583047, 0),
163			}, nil},
164		},
165	}, {
166		// This file was produced using GNU tar v1.17.
167		//	gnutar -b 4 --format=ustar (longname/)*15 + file.txt
168		file: "testdata/ustar.tar",
169		tests: []testFnc{
170			testHeader{Header{
171				Typeflag: TypeReg,
172				Name:     strings.Repeat("longname/", 15) + "file.txt",
173				Size:     6,
174				Mode:     0644,
175				Uid:      501,
176				Gid:      20,
177				Uname:    "shane",
178				Gname:    "staff",
179				ModTime:  time.Unix(1360135598, 0),
180			}, nil},
181			testWrite{"hello\n", 6, nil},
182			testClose{nil},
183		},
184	}, {
185		// This file was produced using GNU tar v1.26:
186		//	echo "Slartibartfast" > file.txt
187		//	ln file.txt hard.txt
188		//	tar -b 1 --format=ustar -c -f hardlink.tar file.txt hard.txt
189		file: "testdata/hardlink.tar",
190		tests: []testFnc{
191			testHeader{Header{
192				Typeflag: TypeReg,
193				Name:     "file.txt",
194				Size:     15,
195				Mode:     0644,
196				Uid:      1000,
197				Gid:      100,
198				Uname:    "vbatts",
199				Gname:    "users",
200				ModTime:  time.Unix(1425484303, 0),
201			}, nil},
202			testWrite{"Slartibartfast\n", 15, nil},
203
204			testHeader{Header{
205				Typeflag: TypeLink,
206				Name:     "hard.txt",
207				Linkname: "file.txt",
208				Mode:     0644,
209				Uid:      1000,
210				Gid:      100,
211				Uname:    "vbatts",
212				Gname:    "users",
213				ModTime:  time.Unix(1425484303, 0),
214			}, nil},
215			testWrite{"", 0, nil},
216
217			testClose{nil},
218		},
219	}, {
220		tests: []testFnc{
221			testHeader{Header{
222				Typeflag: TypeReg,
223				Name:     "bad-null.txt",
224				Xattrs:   map[string]string{"null\x00null\x00": "fizzbuzz"},
225			}, headerError{}},
226		},
227	}, {
228		tests: []testFnc{
229			testHeader{Header{
230				Typeflag: TypeReg,
231				Name:     "null\x00.txt",
232			}, headerError{}},
233		},
234	}, {
235		file: "testdata/pax-records.tar",
236		tests: []testFnc{
237			testHeader{Header{
238				Typeflag: TypeReg,
239				Name:     "file",
240				Uname:    strings.Repeat("long", 10),
241				PAXRecords: map[string]string{
242					"path":           "FILE", // Should be ignored
243					"GNU.sparse.map": "0,0",  // Should be ignored
244					"comment":        "Hello, 世界",
245					"GOLANG.pkg":     "tar",
246				},
247			}, nil},
248			testClose{nil},
249		},
250	}, {
251		// Craft a theoretically valid PAX archive with global headers.
252		// The GNU and BSD tar tools do not parse these the same way.
253		//
254		// BSD tar v3.1.2 parses and ignores all global headers;
255		// the behavior is verified by researching the source code.
256		//
257		//	$ bsdtar -tvf pax-global-records.tar
258		//	----------  0 0      0           0 Dec 31  1969 file1
259		//	----------  0 0      0           0 Dec 31  1969 file2
260		//	----------  0 0      0           0 Dec 31  1969 file3
261		//	----------  0 0      0           0 May 13  2014 file4
262		//
263		// GNU tar v1.27.1 applies global headers to subsequent records,
264		// but does not do the following properly:
265		//	* It does not treat an empty record as deletion.
266		//	* It does not use subsequent global headers to update previous ones.
267		//
268		//	$ gnutar -tvf pax-global-records.tar
269		//	---------- 0/0               0 2017-07-13 19:40 global1
270		//	---------- 0/0               0 2017-07-13 19:40 file2
271		//	gnutar: Substituting `.' for empty member name
272		//	---------- 0/0               0 1969-12-31 16:00
273		//	gnutar: Substituting `.' for empty member name
274		//	---------- 0/0               0 2014-05-13 09:53
275		//
276		// According to the PAX specification, this should have been the result:
277		//	---------- 0/0               0 2017-07-13 19:40 global1
278		//	---------- 0/0               0 2017-07-13 19:40 file2
279		//	---------- 0/0               0 2017-07-13 19:40 file3
280		//	---------- 0/0               0 2014-05-13 09:53 file4
281		file: "testdata/pax-global-records.tar",
282		tests: []testFnc{
283			testHeader{Header{
284				Typeflag:   TypeXGlobalHeader,
285				PAXRecords: map[string]string{"path": "global1", "mtime": "1500000000.0"},
286			}, nil},
287			testHeader{Header{
288				Typeflag: TypeReg, Name: "file1",
289			}, nil},
290			testHeader{Header{
291				Typeflag:   TypeReg,
292				Name:       "file2",
293				PAXRecords: map[string]string{"path": "file2"},
294			}, nil},
295			testHeader{Header{
296				Typeflag:   TypeXGlobalHeader,
297				PAXRecords: map[string]string{"path": ""}, // Should delete "path", but keep "mtime"
298			}, nil},
299			testHeader{Header{
300				Typeflag: TypeReg, Name: "file3",
301			}, nil},
302			testHeader{Header{
303				Typeflag:   TypeReg,
304				Name:       "file4",
305				ModTime:    time.Unix(1400000000, 0),
306				PAXRecords: map[string]string{"mtime": "1400000000"},
307			}, nil},
308			testClose{nil},
309		},
310	}, {
311		file: "testdata/gnu-utf8.tar",
312		tests: []testFnc{
313			testHeader{Header{
314				Typeflag: TypeReg,
315				Name:     "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
316				Mode:     0644,
317				Uid:      1000, Gid: 1000,
318				Uname:   "☺",
319				Gname:   "⚹",
320				ModTime: time.Unix(0, 0),
321				Format:  FormatGNU,
322			}, nil},
323			testClose{nil},
324		},
325	}, {
326		file: "testdata/gnu-not-utf8.tar",
327		tests: []testFnc{
328			testHeader{Header{
329				Typeflag: TypeReg,
330				Name:     "hi\x80\x81\x82\x83bye",
331				Mode:     0644,
332				Uid:      1000,
333				Gid:      1000,
334				Uname:    "rawr",
335				Gname:    "dsnet",
336				ModTime:  time.Unix(0, 0),
337				Format:   FormatGNU,
338			}, nil},
339			testClose{nil},
340		},
341		// TODO(dsnet): Re-enable this test when adding sparse support.
342		// See https://golang.org/issue/22735
343		/*
344			}, {
345				file: "testdata/gnu-nil-sparse-data.tar",
346				tests: []testFnc{
347					testHeader{Header{
348						Typeflag:    TypeGNUSparse,
349						Name:        "sparse.db",
350						Size:        1000,
351						SparseHoles: []sparseEntry{{Offset: 1000, Length: 0}},
352					}, nil},
353					testWrite{strings.Repeat("0123456789", 100), 1000, nil},
354					testClose{},
355				},
356			}, {
357				file: "testdata/gnu-nil-sparse-hole.tar",
358				tests: []testFnc{
359					testHeader{Header{
360						Typeflag:    TypeGNUSparse,
361						Name:        "sparse.db",
362						Size:        1000,
363						SparseHoles: []sparseEntry{{Offset: 0, Length: 1000}},
364					}, nil},
365					testWrite{strings.Repeat("\x00", 1000), 1000, nil},
366					testClose{},
367				},
368			}, {
369				file: "testdata/pax-nil-sparse-data.tar",
370				tests: []testFnc{
371					testHeader{Header{
372						Typeflag:    TypeReg,
373						Name:        "sparse.db",
374						Size:        1000,
375						SparseHoles: []sparseEntry{{Offset: 1000, Length: 0}},
376					}, nil},
377					testWrite{strings.Repeat("0123456789", 100), 1000, nil},
378					testClose{},
379				},
380			}, {
381				file: "testdata/pax-nil-sparse-hole.tar",
382				tests: []testFnc{
383					testHeader{Header{
384						Typeflag:    TypeReg,
385						Name:        "sparse.db",
386						Size:        1000,
387						SparseHoles: []sparseEntry{{Offset: 0, Length: 1000}},
388					}, nil},
389					testWrite{strings.Repeat("\x00", 1000), 1000, nil},
390					testClose{},
391				},
392			}, {
393				file: "testdata/gnu-sparse-big.tar",
394				tests: []testFnc{
395					testHeader{Header{
396						Typeflag: TypeGNUSparse,
397						Name:     "gnu-sparse",
398						Size:     6e10,
399						SparseHoles: []sparseEntry{
400							{Offset: 0e10, Length: 1e10 - 100},
401							{Offset: 1e10, Length: 1e10 - 100},
402							{Offset: 2e10, Length: 1e10 - 100},
403							{Offset: 3e10, Length: 1e10 - 100},
404							{Offset: 4e10, Length: 1e10 - 100},
405							{Offset: 5e10, Length: 1e10 - 100},
406						},
407					}, nil},
408					testReadFrom{fileOps{
409						int64(1e10 - blockSize),
410						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
411						int64(1e10 - blockSize),
412						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
413						int64(1e10 - blockSize),
414						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
415						int64(1e10 - blockSize),
416						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
417						int64(1e10 - blockSize),
418						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
419						int64(1e10 - blockSize),
420						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
421					}, 6e10, nil},
422					testClose{nil},
423				},
424			}, {
425				file: "testdata/pax-sparse-big.tar",
426				tests: []testFnc{
427					testHeader{Header{
428						Typeflag: TypeReg,
429						Name:     "pax-sparse",
430						Size:     6e10,
431						SparseHoles: []sparseEntry{
432							{Offset: 0e10, Length: 1e10 - 100},
433							{Offset: 1e10, Length: 1e10 - 100},
434							{Offset: 2e10, Length: 1e10 - 100},
435							{Offset: 3e10, Length: 1e10 - 100},
436							{Offset: 4e10, Length: 1e10 - 100},
437							{Offset: 5e10, Length: 1e10 - 100},
438						},
439					}, nil},
440					testReadFrom{fileOps{
441						int64(1e10 - blockSize),
442						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
443						int64(1e10 - blockSize),
444						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
445						int64(1e10 - blockSize),
446						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
447						int64(1e10 - blockSize),
448						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
449						int64(1e10 - blockSize),
450						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
451						int64(1e10 - blockSize),
452						strings.Repeat("\x00", blockSize-100) + strings.Repeat("0123456789", 10),
453					}, 6e10, nil},
454					testClose{nil},
455				},
456		*/
457	}, {
458		file: "testdata/trailing-slash.tar",
459		tests: []testFnc{
460			testHeader{Header{Name: strings.Repeat("123456789/", 30)}, nil},
461			testClose{nil},
462		},
463	}, {
464		// Automatically promote zero value of Typeflag depending on the name.
465		file: "testdata/file-and-dir.tar",
466		tests: []testFnc{
467			testHeader{Header{Name: "small.txt", Size: 5}, nil},
468			testWrite{"Kilts", 5, nil},
469			testHeader{Header{Name: "dir/"}, nil},
470			testClose{nil},
471		},
472	}}
473
474	equalError := func(x, y error) bool {
475		_, ok1 := x.(headerError)
476		_, ok2 := y.(headerError)
477		if ok1 || ok2 {
478			return ok1 && ok2
479		}
480		return x == y
481	}
482	for _, v := range vectors {
483		t.Run(path.Base(v.file), func(t *testing.T) {
484			const maxSize = 10 << 10 // 10KiB
485			buf := new(bytes.Buffer)
486			tw := NewWriter(iotest.TruncateWriter(buf, maxSize))
487
488			for i, tf := range v.tests {
489				switch tf := tf.(type) {
490				case testHeader:
491					err := tw.WriteHeader(&tf.hdr)
492					if !equalError(err, tf.wantErr) {
493						t.Fatalf("test %d, WriteHeader() = %v, want %v", i, err, tf.wantErr)
494					}
495				case testWrite:
496					got, err := tw.Write([]byte(tf.str))
497					if got != tf.wantCnt || !equalError(err, tf.wantErr) {
498						t.Fatalf("test %d, Write() = (%d, %v), want (%d, %v)", i, got, err, tf.wantCnt, tf.wantErr)
499					}
500				case testReadFrom:
501					f := &testFile{ops: tf.ops}
502					got, err := tw.readFrom(f)
503					if _, ok := err.(testError); ok {
504						t.Errorf("test %d, ReadFrom(): %v", i, err)
505					} else if got != tf.wantCnt || !equalError(err, tf.wantErr) {
506						t.Errorf("test %d, ReadFrom() = (%d, %v), want (%d, %v)", i, got, err, tf.wantCnt, tf.wantErr)
507					}
508					if len(f.ops) > 0 {
509						t.Errorf("test %d, expected %d more operations", i, len(f.ops))
510					}
511				case testClose:
512					err := tw.Close()
513					if !equalError(err, tf.wantErr) {
514						t.Fatalf("test %d, Close() = %v, want %v", i, err, tf.wantErr)
515					}
516				default:
517					t.Fatalf("test %d, unknown test operation: %T", i, tf)
518				}
519			}
520
521			if v.file != "" {
522				want, err := os.ReadFile(v.file)
523				if err != nil {
524					t.Fatalf("ReadFile() = %v, want nil", err)
525				}
526				got := buf.Bytes()
527				if !bytes.Equal(want, got) {
528					t.Fatalf("incorrect result: (-got +want)\n%v", bytediff(got, want))
529				}
530			}
531		})
532	}
533}
534
535func TestPax(t *testing.T) {
536	// Create an archive with a large name
537	fileinfo, err := os.Stat("testdata/small.txt")
538	if err != nil {
539		t.Fatal(err)
540	}
541	hdr, err := FileInfoHeader(fileinfo, "")
542	if err != nil {
543		t.Fatalf("os.Stat: %v", err)
544	}
545	// Force a PAX long name to be written
546	longName := strings.Repeat("ab", 100)
547	contents := strings.Repeat(" ", int(hdr.Size))
548	hdr.Name = longName
549	var buf bytes.Buffer
550	writer := NewWriter(&buf)
551	if err := writer.WriteHeader(hdr); err != nil {
552		t.Fatal(err)
553	}
554	if _, err = writer.Write([]byte(contents)); err != nil {
555		t.Fatal(err)
556	}
557	if err := writer.Close(); err != nil {
558		t.Fatal(err)
559	}
560	// Simple test to make sure PAX extensions are in effect
561	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
562		t.Fatal("Expected at least one PAX header to be written.")
563	}
564	// Test that we can get a long name back out of the archive.
565	reader := NewReader(&buf)
566	hdr, err = reader.Next()
567	if err != nil {
568		t.Fatal(err)
569	}
570	if hdr.Name != longName {
571		t.Fatal("Couldn't recover long file name")
572	}
573}
574
575func TestPaxSymlink(t *testing.T) {
576	// Create an archive with a large linkname
577	fileinfo, err := os.Stat("testdata/small.txt")
578	if err != nil {
579		t.Fatal(err)
580	}
581	hdr, err := FileInfoHeader(fileinfo, "")
582	hdr.Typeflag = TypeSymlink
583	if err != nil {
584		t.Fatalf("os.Stat:1 %v", err)
585	}
586	// Force a PAX long linkname to be written
587	longLinkname := strings.Repeat("1234567890/1234567890", 10)
588	hdr.Linkname = longLinkname
589
590	hdr.Size = 0
591	var buf bytes.Buffer
592	writer := NewWriter(&buf)
593	if err := writer.WriteHeader(hdr); err != nil {
594		t.Fatal(err)
595	}
596	if err := writer.Close(); err != nil {
597		t.Fatal(err)
598	}
599	// Simple test to make sure PAX extensions are in effect
600	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
601		t.Fatal("Expected at least one PAX header to be written.")
602	}
603	// Test that we can get a long name back out of the archive.
604	reader := NewReader(&buf)
605	hdr, err = reader.Next()
606	if err != nil {
607		t.Fatal(err)
608	}
609	if hdr.Linkname != longLinkname {
610		t.Fatal("Couldn't recover long link name")
611	}
612}
613
614func TestPaxNonAscii(t *testing.T) {
615	// Create an archive with non ascii. These should trigger a pax header
616	// because pax headers have a defined utf-8 encoding.
617	fileinfo, err := os.Stat("testdata/small.txt")
618	if err != nil {
619		t.Fatal(err)
620	}
621
622	hdr, err := FileInfoHeader(fileinfo, "")
623	if err != nil {
624		t.Fatalf("os.Stat:1 %v", err)
625	}
626
627	// some sample data
628	chineseFilename := "文件名"
629	chineseGroupname := "組"
630	chineseUsername := "用戶名"
631
632	hdr.Name = chineseFilename
633	hdr.Gname = chineseGroupname
634	hdr.Uname = chineseUsername
635
636	contents := strings.Repeat(" ", int(hdr.Size))
637
638	var buf bytes.Buffer
639	writer := NewWriter(&buf)
640	if err := writer.WriteHeader(hdr); err != nil {
641		t.Fatal(err)
642	}
643	if _, err = writer.Write([]byte(contents)); err != nil {
644		t.Fatal(err)
645	}
646	if err := writer.Close(); err != nil {
647		t.Fatal(err)
648	}
649	// Simple test to make sure PAX extensions are in effect
650	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
651		t.Fatal("Expected at least one PAX header to be written.")
652	}
653	// Test that we can get a long name back out of the archive.
654	reader := NewReader(&buf)
655	hdr, err = reader.Next()
656	if err != nil {
657		t.Fatal(err)
658	}
659	if hdr.Name != chineseFilename {
660		t.Fatal("Couldn't recover unicode name")
661	}
662	if hdr.Gname != chineseGroupname {
663		t.Fatal("Couldn't recover unicode group")
664	}
665	if hdr.Uname != chineseUsername {
666		t.Fatal("Couldn't recover unicode user")
667	}
668}
669
670func TestPaxXattrs(t *testing.T) {
671	xattrs := map[string]string{
672		"user.key": "value",
673	}
674
675	// Create an archive with an xattr
676	fileinfo, err := os.Stat("testdata/small.txt")
677	if err != nil {
678		t.Fatal(err)
679	}
680	hdr, err := FileInfoHeader(fileinfo, "")
681	if err != nil {
682		t.Fatalf("os.Stat: %v", err)
683	}
684	contents := "Kilts"
685	hdr.Xattrs = xattrs
686	var buf bytes.Buffer
687	writer := NewWriter(&buf)
688	if err := writer.WriteHeader(hdr); err != nil {
689		t.Fatal(err)
690	}
691	if _, err = writer.Write([]byte(contents)); err != nil {
692		t.Fatal(err)
693	}
694	if err := writer.Close(); err != nil {
695		t.Fatal(err)
696	}
697	// Test that we can get the xattrs back out of the archive.
698	reader := NewReader(&buf)
699	hdr, err = reader.Next()
700	if err != nil {
701		t.Fatal(err)
702	}
703	if !reflect.DeepEqual(hdr.Xattrs, xattrs) {
704		t.Fatalf("xattrs did not survive round trip: got %+v, want %+v",
705			hdr.Xattrs, xattrs)
706	}
707}
708
709func TestPaxHeadersSorted(t *testing.T) {
710	fileinfo, err := os.Stat("testdata/small.txt")
711	if err != nil {
712		t.Fatal(err)
713	}
714	hdr, err := FileInfoHeader(fileinfo, "")
715	if err != nil {
716		t.Fatalf("os.Stat: %v", err)
717	}
718	contents := strings.Repeat(" ", int(hdr.Size))
719
720	hdr.Xattrs = map[string]string{
721		"foo": "foo",
722		"bar": "bar",
723		"baz": "baz",
724		"qux": "qux",
725	}
726
727	var buf bytes.Buffer
728	writer := NewWriter(&buf)
729	if err := writer.WriteHeader(hdr); err != nil {
730		t.Fatal(err)
731	}
732	if _, err = writer.Write([]byte(contents)); err != nil {
733		t.Fatal(err)
734	}
735	if err := writer.Close(); err != nil {
736		t.Fatal(err)
737	}
738	// Simple test to make sure PAX extensions are in effect
739	if !bytes.Contains(buf.Bytes(), []byte("PaxHeaders.0")) {
740		t.Fatal("Expected at least one PAX header to be written.")
741	}
742
743	// xattr bar should always appear before others
744	indices := []int{
745		bytes.Index(buf.Bytes(), []byte("bar=bar")),
746		bytes.Index(buf.Bytes(), []byte("baz=baz")),
747		bytes.Index(buf.Bytes(), []byte("foo=foo")),
748		bytes.Index(buf.Bytes(), []byte("qux=qux")),
749	}
750	if !sort.IntsAreSorted(indices) {
751		t.Fatal("PAX headers are not sorted")
752	}
753}
754
755func TestUSTARLongName(t *testing.T) {
756	// Create an archive with a path that failed to split with USTAR extension in previous versions.
757	fileinfo, err := os.Stat("testdata/small.txt")
758	if err != nil {
759		t.Fatal(err)
760	}
761	hdr, err := FileInfoHeader(fileinfo, "")
762	hdr.Typeflag = TypeDir
763	if err != nil {
764		t.Fatalf("os.Stat:1 %v", err)
765	}
766	// Force a PAX long name to be written. The name was taken from a practical example
767	// that fails and replaced ever char through numbers to anonymize the sample.
768	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/"
769	hdr.Name = longName
770
771	hdr.Size = 0
772	var buf bytes.Buffer
773	writer := NewWriter(&buf)
774	if err := writer.WriteHeader(hdr); err != nil {
775		t.Fatal(err)
776	}
777	if err := writer.Close(); err != nil {
778		t.Fatal(err)
779	}
780	// Test that we can get a long name back out of the archive.
781	reader := NewReader(&buf)
782	hdr, err = reader.Next()
783	if err != nil {
784		t.Fatal(err)
785	}
786	if hdr.Name != longName {
787		t.Fatal("Couldn't recover long name")
788	}
789}
790
791func TestValidTypeflagWithPAXHeader(t *testing.T) {
792	var buffer bytes.Buffer
793	tw := NewWriter(&buffer)
794
795	fileName := strings.Repeat("ab", 100)
796
797	hdr := &Header{
798		Name:     fileName,
799		Size:     4,
800		Typeflag: 0,
801	}
802	if err := tw.WriteHeader(hdr); err != nil {
803		t.Fatalf("Failed to write header: %s", err)
804	}
805	if _, err := tw.Write([]byte("fooo")); err != nil {
806		t.Fatalf("Failed to write the file's data: %s", err)
807	}
808	tw.Close()
809
810	tr := NewReader(&buffer)
811
812	for {
813		header, err := tr.Next()
814		if err == io.EOF {
815			break
816		}
817		if err != nil {
818			t.Fatalf("Failed to read header: %s", err)
819		}
820		if header.Typeflag != TypeReg {
821			t.Fatalf("Typeflag should've been %d, found %d", TypeReg, header.Typeflag)
822		}
823	}
824}
825
826// failOnceWriter fails exactly once and then always reports success.
827type failOnceWriter bool
828
829func (w *failOnceWriter) Write(b []byte) (int, error) {
830	if !*w {
831		return 0, io.ErrShortWrite
832	}
833	*w = true
834	return len(b), nil
835}
836
837func TestWriterErrors(t *testing.T) {
838	t.Run("HeaderOnly", func(t *testing.T) {
839		tw := NewWriter(new(bytes.Buffer))
840		hdr := &Header{Name: "dir/", Typeflag: TypeDir}
841		if err := tw.WriteHeader(hdr); err != nil {
842			t.Fatalf("WriteHeader() = %v, want nil", err)
843		}
844		if _, err := tw.Write([]byte{0x00}); err != ErrWriteTooLong {
845			t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
846		}
847	})
848
849	t.Run("NegativeSize", func(t *testing.T) {
850		tw := NewWriter(new(bytes.Buffer))
851		hdr := &Header{Name: "small.txt", Size: -1}
852		if err := tw.WriteHeader(hdr); err == nil {
853			t.Fatalf("WriteHeader() = nil, want non-nil error")
854		}
855	})
856
857	t.Run("BeforeHeader", func(t *testing.T) {
858		tw := NewWriter(new(bytes.Buffer))
859		if _, err := tw.Write([]byte("Kilts")); err != ErrWriteTooLong {
860			t.Fatalf("Write() = %v, want %v", err, ErrWriteTooLong)
861		}
862	})
863
864	t.Run("AfterClose", func(t *testing.T) {
865		tw := NewWriter(new(bytes.Buffer))
866		hdr := &Header{Name: "small.txt"}
867		if err := tw.WriteHeader(hdr); err != nil {
868			t.Fatalf("WriteHeader() = %v, want nil", err)
869		}
870		if err := tw.Close(); err != nil {
871			t.Fatalf("Close() = %v, want nil", err)
872		}
873		if _, err := tw.Write([]byte("Kilts")); err != ErrWriteAfterClose {
874			t.Fatalf("Write() = %v, want %v", err, ErrWriteAfterClose)
875		}
876		if err := tw.Flush(); err != ErrWriteAfterClose {
877			t.Fatalf("Flush() = %v, want %v", err, ErrWriteAfterClose)
878		}
879		if err := tw.Close(); err != nil {
880			t.Fatalf("Close() = %v, want nil", err)
881		}
882	})
883
884	t.Run("PrematureFlush", func(t *testing.T) {
885		tw := NewWriter(new(bytes.Buffer))
886		hdr := &Header{Name: "small.txt", Size: 5}
887		if err := tw.WriteHeader(hdr); err != nil {
888			t.Fatalf("WriteHeader() = %v, want nil", err)
889		}
890		if err := tw.Flush(); err == nil {
891			t.Fatalf("Flush() = %v, want non-nil error", err)
892		}
893	})
894
895	t.Run("PrematureClose", func(t *testing.T) {
896		tw := NewWriter(new(bytes.Buffer))
897		hdr := &Header{Name: "small.txt", Size: 5}
898		if err := tw.WriteHeader(hdr); err != nil {
899			t.Fatalf("WriteHeader() = %v, want nil", err)
900		}
901		if err := tw.Close(); err == nil {
902			t.Fatalf("Close() = %v, want non-nil error", err)
903		}
904	})
905
906	t.Run("Persistence", func(t *testing.T) {
907		tw := NewWriter(new(failOnceWriter))
908		if err := tw.WriteHeader(&Header{}); err != io.ErrShortWrite {
909			t.Fatalf("WriteHeader() = %v, want %v", err, io.ErrShortWrite)
910		}
911		if err := tw.WriteHeader(&Header{Name: "small.txt"}); err == nil {
912			t.Errorf("WriteHeader() = got %v, want non-nil error", err)
913		}
914		if _, err := tw.Write(nil); err == nil {
915			t.Errorf("Write() = %v, want non-nil error", err)
916		}
917		if err := tw.Flush(); err == nil {
918			t.Errorf("Flush() = %v, want non-nil error", err)
919		}
920		if err := tw.Close(); err == nil {
921			t.Errorf("Close() = %v, want non-nil error", err)
922		}
923	})
924}
925
926func TestSplitUSTARPath(t *testing.T) {
927	sr := strings.Repeat
928
929	vectors := []struct {
930		input  string // Input path
931		prefix string // Expected output prefix
932		suffix string // Expected output suffix
933		ok     bool   // Split success?
934	}{
935		{"", "", "", false},
936		{"abc", "", "", false},
937		{"用戶名", "", "", false},
938		{sr("a", nameSize), "", "", false},
939		{sr("a", nameSize) + "/", "", "", false},
940		{sr("a", nameSize) + "/a", sr("a", nameSize), "a", true},
941		{sr("a", prefixSize) + "/", "", "", false},
942		{sr("a", prefixSize) + "/a", sr("a", prefixSize), "a", true},
943		{sr("a", nameSize+1), "", "", false},
944		{sr("/", nameSize+1), sr("/", nameSize-1), "/", true},
945		{sr("a", prefixSize) + "/" + sr("b", nameSize),
946			sr("a", prefixSize), sr("b", nameSize), true},
947		{sr("a", prefixSize) + "//" + sr("b", nameSize), "", "", false},
948		{sr("a/", nameSize), sr("a/", 77) + "a", sr("a/", 22), true},
949	}
950
951	for _, v := range vectors {
952		prefix, suffix, ok := splitUSTARPath(v.input)
953		if prefix != v.prefix || suffix != v.suffix || ok != v.ok {
954			t.Errorf("splitUSTARPath(%q):\ngot  (%q, %q, %v)\nwant (%q, %q, %v)",
955				v.input, prefix, suffix, ok, v.prefix, v.suffix, v.ok)
956		}
957	}
958}
959
960// TestIssue12594 tests that the Writer does not attempt to populate the prefix
961// field when encoding a header in the GNU format. The prefix field is valid
962// in USTAR and PAX, but not GNU.
963func TestIssue12594(t *testing.T) {
964	names := []string{
965		"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",
966		"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",
967		"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",
968		"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",
969		"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000/file.txt",
970		"/home/support/.openoffice.org/3/user/uno_packages/cache/registry/com.sun.star.comp.deployment.executable.PackageRegistryBackend",
971	}
972
973	for i, name := range names {
974		var b bytes.Buffer
975
976		tw := NewWriter(&b)
977		if err := tw.WriteHeader(&Header{
978			Name: name,
979			Uid:  1 << 25, // Prevent USTAR format
980		}); err != nil {
981			t.Errorf("test %d, unexpected WriteHeader error: %v", i, err)
982		}
983		if err := tw.Close(); err != nil {
984			t.Errorf("test %d, unexpected Close error: %v", i, err)
985		}
986
987		// The prefix field should never appear in the GNU format.
988		var blk block
989		copy(blk[:], b.Bytes())
990		prefix := string(blk.USTAR().Prefix())
991		if i := strings.IndexByte(prefix, 0); i >= 0 {
992			prefix = prefix[:i] // Truncate at the NUL terminator
993		}
994		if blk.GetFormat() == FormatGNU && len(prefix) > 0 && strings.HasPrefix(name, prefix) {
995			t.Errorf("test %d, found prefix in GNU format: %s", i, prefix)
996		}
997
998		tr := NewReader(&b)
999		hdr, err := tr.Next()
1000		if err != nil {
1001			t.Errorf("test %d, unexpected Next error: %v", i, err)
1002		}
1003		if hdr.Name != name {
1004			t.Errorf("test %d, hdr.Name = %s, want %s", i, hdr.Name, name)
1005		}
1006	}
1007}
1008
1009// testNonEmptyWriter wraps an io.Writer and ensures that
1010// Write is never called with an empty buffer.
1011type testNonEmptyWriter struct{ io.Writer }
1012
1013func (w testNonEmptyWriter) Write(b []byte) (int, error) {
1014	if len(b) == 0 {
1015		return 0, errors.New("unexpected empty Write call")
1016	}
1017	return w.Writer.Write(b)
1018}
1019
1020func TestFileWriter(t *testing.T) {
1021	type (
1022		testWrite struct { // Write(str) == (wantCnt, wantErr)
1023			str     string
1024			wantCnt int
1025			wantErr error
1026		}
1027		testReadFrom struct { // ReadFrom(testFile{ops}) == (wantCnt, wantErr)
1028			ops     fileOps
1029			wantCnt int64
1030			wantErr error
1031		}
1032		testRemaining struct { // LogicalRemaining() == wantLCnt, PhysicalRemaining() == wantPCnt
1033			wantLCnt int64
1034			wantPCnt int64
1035		}
1036		testFnc interface{} // testWrite | testReadFrom | testRemaining
1037	)
1038
1039	type (
1040		makeReg struct {
1041			size    int64
1042			wantStr string
1043		}
1044		makeSparse struct {
1045			makeReg makeReg
1046			sph     sparseHoles
1047			size    int64
1048		}
1049		fileMaker interface{} // makeReg | makeSparse
1050	)
1051
1052	vectors := []struct {
1053		maker fileMaker
1054		tests []testFnc
1055	}{{
1056		maker: makeReg{0, ""},
1057		tests: []testFnc{
1058			testRemaining{0, 0},
1059			testWrite{"", 0, nil},
1060			testWrite{"a", 0, ErrWriteTooLong},
1061			testReadFrom{fileOps{""}, 0, nil},
1062			testReadFrom{fileOps{"a"}, 0, ErrWriteTooLong},
1063			testRemaining{0, 0},
1064		},
1065	}, {
1066		maker: makeReg{1, "a"},
1067		tests: []testFnc{
1068			testRemaining{1, 1},
1069			testWrite{"", 0, nil},
1070			testWrite{"a", 1, nil},
1071			testWrite{"bcde", 0, ErrWriteTooLong},
1072			testWrite{"", 0, nil},
1073			testReadFrom{fileOps{""}, 0, nil},
1074			testReadFrom{fileOps{"a"}, 0, ErrWriteTooLong},
1075			testRemaining{0, 0},
1076		},
1077	}, {
1078		maker: makeReg{5, "hello"},
1079		tests: []testFnc{
1080			testRemaining{5, 5},
1081			testWrite{"hello", 5, nil},
1082			testRemaining{0, 0},
1083		},
1084	}, {
1085		maker: makeReg{5, "\x00\x00\x00\x00\x00"},
1086		tests: []testFnc{
1087			testRemaining{5, 5},
1088			testReadFrom{fileOps{"\x00\x00\x00\x00\x00"}, 5, nil},
1089			testRemaining{0, 0},
1090		},
1091	}, {
1092		maker: makeReg{5, "\x00\x00\x00\x00\x00"},
1093		tests: []testFnc{
1094			testRemaining{5, 5},
1095			testReadFrom{fileOps{"\x00\x00\x00\x00\x00extra"}, 5, ErrWriteTooLong},
1096			testRemaining{0, 0},
1097		},
1098	}, {
1099		maker: makeReg{5, "abc\x00\x00"},
1100		tests: []testFnc{
1101			testRemaining{5, 5},
1102			testWrite{"abc", 3, nil},
1103			testRemaining{2, 2},
1104			testReadFrom{fileOps{"\x00\x00"}, 2, nil},
1105			testRemaining{0, 0},
1106		},
1107	}, {
1108		maker: makeReg{5, "\x00\x00abc"},
1109		tests: []testFnc{
1110			testRemaining{5, 5},
1111			testWrite{"\x00\x00", 2, nil},
1112			testRemaining{3, 3},
1113			testWrite{"abc", 3, nil},
1114			testReadFrom{fileOps{"z"}, 0, ErrWriteTooLong},
1115			testWrite{"z", 0, ErrWriteTooLong},
1116			testRemaining{0, 0},
1117		},
1118	}, {
1119		maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
1120		tests: []testFnc{
1121			testRemaining{8, 5},
1122			testWrite{"ab\x00\x00\x00cde", 8, nil},
1123			testWrite{"a", 0, ErrWriteTooLong},
1124			testRemaining{0, 0},
1125		},
1126	}, {
1127		maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
1128		tests: []testFnc{
1129			testWrite{"ab\x00\x00\x00cdez", 8, ErrWriteTooLong},
1130			testRemaining{0, 0},
1131		},
1132	}, {
1133		maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
1134		tests: []testFnc{
1135			testWrite{"ab\x00", 3, nil},
1136			testRemaining{5, 3},
1137			testWrite{"\x00\x00cde", 5, nil},
1138			testWrite{"a", 0, ErrWriteTooLong},
1139			testRemaining{0, 0},
1140		},
1141	}, {
1142		maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
1143		tests: []testFnc{
1144			testWrite{"ab", 2, nil},
1145			testRemaining{6, 3},
1146			testReadFrom{fileOps{int64(3), "cde"}, 6, nil},
1147			testRemaining{0, 0},
1148		},
1149	}, {
1150		maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
1151		tests: []testFnc{
1152			testReadFrom{fileOps{"ab", int64(3), "cde"}, 8, nil},
1153			testRemaining{0, 0},
1154		},
1155	}, {
1156		maker: makeSparse{makeReg{5, "abcde"}, sparseHoles{{2, 3}}, 8},
1157		tests: []testFnc{
1158			testReadFrom{fileOps{"ab", int64(3), "cdeX"}, 8, ErrWriteTooLong},
1159			testRemaining{0, 0},
1160		},
1161	}, {
1162		maker: makeSparse{makeReg{4, "abcd"}, sparseHoles{{2, 3}}, 8},
1163		tests: []testFnc{
1164			testReadFrom{fileOps{"ab", int64(3), "cd"}, 7, io.ErrUnexpectedEOF},
1165			testRemaining{1, 0},
1166		},
1167	}, {
1168		maker: makeSparse{makeReg{4, "abcd"}, sparseHoles{{2, 3}}, 8},
1169		tests: []testFnc{
1170			testReadFrom{fileOps{"ab", int64(3), "cde"}, 7, errMissData},
1171			testRemaining{1, 0},
1172		},
1173	}, {
1174		maker: makeSparse{makeReg{6, "abcde"}, sparseHoles{{2, 3}}, 8},
1175		tests: []testFnc{
1176			testReadFrom{fileOps{"ab", int64(3), "cde"}, 8, errUnrefData},
1177			testRemaining{0, 1},
1178		},
1179	}, {
1180		maker: makeSparse{makeReg{4, "abcd"}, sparseHoles{{2, 3}}, 8},
1181		tests: []testFnc{
1182			testWrite{"ab", 2, nil},
1183			testRemaining{6, 2},
1184			testWrite{"\x00\x00\x00", 3, nil},
1185			testRemaining{3, 2},
1186			testWrite{"cde", 2, errMissData},
1187			testRemaining{1, 0},
1188		},
1189	}, {
1190		maker: makeSparse{makeReg{6, "abcde"}, sparseHoles{{2, 3}}, 8},
1191		tests: []testFnc{
1192			testWrite{"ab", 2, nil},
1193			testRemaining{6, 4},
1194			testWrite{"\x00\x00\x00", 3, nil},
1195			testRemaining{3, 4},
1196			testWrite{"cde", 3, errUnrefData},
1197			testRemaining{0, 1},
1198		},
1199	}, {
1200		maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1201		tests: []testFnc{
1202			testRemaining{7, 3},
1203			testWrite{"\x00\x00abc\x00\x00", 7, nil},
1204			testRemaining{0, 0},
1205		},
1206	}, {
1207		maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1208		tests: []testFnc{
1209			testRemaining{7, 3},
1210			testReadFrom{fileOps{int64(2), "abc", int64(1), "\x00"}, 7, nil},
1211			testRemaining{0, 0},
1212		},
1213	}, {
1214		maker: makeSparse{makeReg{3, ""}, sparseHoles{{0, 2}, {5, 2}}, 7},
1215		tests: []testFnc{
1216			testWrite{"abcdefg", 0, errWriteHole},
1217		},
1218	}, {
1219		maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1220		tests: []testFnc{
1221			testWrite{"\x00\x00abcde", 5, errWriteHole},
1222		},
1223	}, {
1224		maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1225		tests: []testFnc{
1226			testWrite{"\x00\x00abc\x00\x00z", 7, ErrWriteTooLong},
1227			testRemaining{0, 0},
1228		},
1229	}, {
1230		maker: makeSparse{makeReg{3, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1231		tests: []testFnc{
1232			testWrite{"\x00\x00", 2, nil},
1233			testRemaining{5, 3},
1234			testWrite{"abc", 3, nil},
1235			testRemaining{2, 0},
1236			testWrite{"\x00\x00", 2, nil},
1237			testRemaining{0, 0},
1238		},
1239	}, {
1240		maker: makeSparse{makeReg{2, "ab"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1241		tests: []testFnc{
1242			testWrite{"\x00\x00", 2, nil},
1243			testWrite{"abc", 2, errMissData},
1244			testWrite{"\x00\x00", 0, errMissData},
1245		},
1246	}, {
1247		maker: makeSparse{makeReg{4, "abc"}, sparseHoles{{0, 2}, {5, 2}}, 7},
1248		tests: []testFnc{
1249			testWrite{"\x00\x00", 2, nil},
1250			testWrite{"abc", 3, nil},
1251			testWrite{"\x00\x00", 2, errUnrefData},
1252		},
1253	}}
1254
1255	for i, v := range vectors {
1256		var wantStr string
1257		bb := new(bytes.Buffer)
1258		w := testNonEmptyWriter{bb}
1259		var fw fileWriter
1260		switch maker := v.maker.(type) {
1261		case makeReg:
1262			fw = &regFileWriter{w, maker.size}
1263			wantStr = maker.wantStr
1264		case makeSparse:
1265			if !validateSparseEntries(maker.sph, maker.size) {
1266				t.Fatalf("invalid sparse map: %v", maker.sph)
1267			}
1268			spd := invertSparseEntries(maker.sph, maker.size)
1269			fw = &regFileWriter{w, maker.makeReg.size}
1270			fw = &sparseFileWriter{fw, spd, 0}
1271			wantStr = maker.makeReg.wantStr
1272		default:
1273			t.Fatalf("test %d, unknown make operation: %T", i, maker)
1274		}
1275
1276		for j, tf := range v.tests {
1277			switch tf := tf.(type) {
1278			case testWrite:
1279				got, err := fw.Write([]byte(tf.str))
1280				if got != tf.wantCnt || err != tf.wantErr {
1281					t.Errorf("test %d.%d, Write(%s):\ngot  (%d, %v)\nwant (%d, %v)", i, j, tf.str, got, err, tf.wantCnt, tf.wantErr)
1282				}
1283			case testReadFrom:
1284				f := &testFile{ops: tf.ops}
1285				got, err := fw.ReadFrom(f)
1286				if _, ok := err.(testError); ok {
1287					t.Errorf("test %d.%d, ReadFrom(): %v", i, j, err)
1288				} else if got != tf.wantCnt || err != tf.wantErr {
1289					t.Errorf("test %d.%d, ReadFrom() = (%d, %v), want (%d, %v)", i, j, got, err, tf.wantCnt, tf.wantErr)
1290				}
1291				if len(f.ops) > 0 {
1292					t.Errorf("test %d.%d, expected %d more operations", i, j, len(f.ops))
1293				}
1294			case testRemaining:
1295				if got := fw.LogicalRemaining(); got != tf.wantLCnt {
1296					t.Errorf("test %d.%d, LogicalRemaining() = %d, want %d", i, j, got, tf.wantLCnt)
1297				}
1298				if got := fw.PhysicalRemaining(); got != tf.wantPCnt {
1299					t.Errorf("test %d.%d, PhysicalRemaining() = %d, want %d", i, j, got, tf.wantPCnt)
1300				}
1301			default:
1302				t.Fatalf("test %d.%d, unknown test operation: %T", i, j, tf)
1303			}
1304		}
1305
1306		if got := bb.String(); got != wantStr {
1307			t.Fatalf("test %d, String() = %q, want %q", i, got, wantStr)
1308		}
1309	}
1310}
1311