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