Source file src/archive/tar/writer_test.go

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

View as plain text