cuelang.org/go@v0.10.1/mod/modzip/zip_test.go (about)

     1  // Copyright 2019 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 modzip_test
     6  
     7  import (
     8  	"archive/zip"
     9  	"bytes"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"runtime"
    16  	"strconv"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  
    23  	"cuelang.org/go/internal/cuetest"
    24  	"cuelang.org/go/mod/module"
    25  	"cuelang.org/go/mod/modzip"
    26  	"golang.org/x/mod/sumdb/dirhash"
    27  	"golang.org/x/tools/txtar"
    28  )
    29  
    30  type testParams struct {
    31  	path, version, wantErr, hash string
    32  	want                         string
    33  	archive                      *txtar.Archive
    34  }
    35  
    36  // readTest loads a test from a txtar file. The comment section of the file
    37  // should contain lines with key=value pairs. Valid keys are the field names
    38  // from testParams.
    39  func readTest(file string) (testParams, error) {
    40  	var test testParams
    41  	var err error
    42  	test.archive, err = txtar.ParseFile(file)
    43  	if err != nil {
    44  		return testParams{}, err
    45  	}
    46  	for i, f := range test.archive.Files {
    47  		if f.Name == "want" {
    48  			test.want = string(f.Data)
    49  			test.archive.Files = append(test.archive.Files[:i], test.archive.Files[i+1:]...)
    50  			break
    51  		}
    52  	}
    53  
    54  	lines := strings.Split(string(test.archive.Comment), "\n")
    55  	for n, line := range lines {
    56  		n++ // report line numbers starting with 1
    57  		line = strings.TrimSpace(line)
    58  		if line == "" || line[0] == '#' {
    59  			continue
    60  		}
    61  		eq := strings.IndexByte(line, '=')
    62  		if eq < 0 {
    63  			return testParams{}, fmt.Errorf("%s:%d: missing = separator", file, n)
    64  		}
    65  		key, value := strings.TrimSpace(line[:eq]), strings.TrimSpace(line[eq+1:])
    66  		if strings.HasPrefix(value, "\"") {
    67  			unq, err := strconv.Unquote(value)
    68  			if err != nil {
    69  				return testParams{}, fmt.Errorf("%s:%d: %v", file, n, err)
    70  			}
    71  			value = unq
    72  		}
    73  		switch key {
    74  		case "path":
    75  			test.path = value
    76  		case "version":
    77  			test.version = value
    78  		case "wantErr":
    79  			test.wantErr = value
    80  		case "hash":
    81  			test.hash = value
    82  		default:
    83  			return testParams{}, fmt.Errorf("%s:%d: unknown key %q", file, n, key)
    84  		}
    85  	}
    86  
    87  	return test, nil
    88  }
    89  
    90  func extractTxtarToTempDir(t testing.TB, arc *txtar.Archive) (dir string, err error) {
    91  	dir = t.TempDir()
    92  	for _, f := range arc.Files {
    93  		filePath := filepath.Join(dir, f.Name)
    94  		if err := os.MkdirAll(filepath.Dir(filePath), 0777); err != nil {
    95  			return "", err
    96  		}
    97  		if err := os.WriteFile(filePath, f.Data, 0666); err != nil {
    98  			return "", err
    99  		}
   100  	}
   101  	return dir, nil
   102  }
   103  
   104  func extractTxtarToTempZip(t *testing.T, arc *txtar.Archive) (zipPath string, err error) {
   105  	zipPath = filepath.Join(t.TempDir(), "txtar.zip")
   106  
   107  	zipFile, err := os.Create(zipPath)
   108  	if err != nil {
   109  		return "", err
   110  	}
   111  	defer func() {
   112  		if cerr := zipFile.Close(); err == nil && cerr != nil {
   113  			err = cerr
   114  		}
   115  	}()
   116  
   117  	zw := zip.NewWriter(zipFile)
   118  	for _, f := range arc.Files {
   119  		zf, err := zw.Create(f.Name)
   120  		if err != nil {
   121  			return "", err
   122  		}
   123  		if _, err := zf.Write(f.Data); err != nil {
   124  			return "", err
   125  		}
   126  	}
   127  	if err := zw.Close(); err != nil {
   128  		return "", err
   129  	}
   130  	return zipFile.Name(), nil
   131  }
   132  
   133  type fakeFileIO struct{}
   134  
   135  func (fakeFileIO) Path(f fakeFile) string                { return f.name }
   136  func (fakeFileIO) Lstat(f fakeFile) (os.FileInfo, error) { return fakeFileInfo{f}, nil }
   137  func (fakeFileIO) Open(f fakeFile) (io.ReadCloser, error) {
   138  	if f.data != nil {
   139  		return io.NopCloser(bytes.NewReader(f.data)), nil
   140  	}
   141  	if f.size >= uint64(modzip.MaxZipFile<<1) {
   142  		return nil, fmt.Errorf("cannot open fakeFile of size %d", f.size)
   143  	}
   144  	return io.NopCloser(io.LimitReader(zeroReader{}, int64(f.size))), nil
   145  }
   146  
   147  type fakeFile struct {
   148  	name  string
   149  	isDir bool
   150  	size  uint64
   151  	data  []byte // if nil, Open will access a sequence of 0-bytes
   152  }
   153  
   154  type fakeFileInfo struct {
   155  	f fakeFile
   156  }
   157  
   158  func (fi fakeFileInfo) Name() string {
   159  	return path.Base(fi.f.name)
   160  }
   161  
   162  func (fi fakeFileInfo) Size() int64 {
   163  	if fi.f.size == 0 {
   164  		return int64(len(fi.f.data))
   165  	}
   166  	return int64(fi.f.size)
   167  }
   168  func (fi fakeFileInfo) Mode() os.FileMode {
   169  	if fi.f.isDir {
   170  		return os.ModeDir | 0o755
   171  	}
   172  	return 0o644
   173  }
   174  
   175  func (fi fakeFileInfo) ModTime() time.Time { return time.Time{} }
   176  func (fi fakeFileInfo) IsDir() bool        { return fi.f.isDir }
   177  func (fi fakeFileInfo) Sys() interface{}   { return nil }
   178  
   179  type zeroReader struct{}
   180  
   181  func (r zeroReader) Read(b []byte) (int, error) {
   182  	clear(b)
   183  	return len(b), nil
   184  }
   185  
   186  func formatCheckedFiles(cf modzip.CheckedFiles) string {
   187  	buf := &bytes.Buffer{}
   188  	fmt.Fprintf(buf, "valid:\n")
   189  	for _, f := range cf.Valid {
   190  		fmt.Fprintln(buf, f)
   191  	}
   192  	fmt.Fprintf(buf, "\nomitted:\n")
   193  	for _, f := range cf.Omitted {
   194  		fmt.Fprintf(buf, "%s: %v\n", f.Path, f.Err)
   195  	}
   196  	fmt.Fprintf(buf, "\ninvalid:\n")
   197  	for _, f := range cf.Invalid {
   198  		fmt.Fprintf(buf, "%s: %v\n", f.Path, f.Err)
   199  	}
   200  	return buf.String()
   201  }
   202  
   203  func TestCheckFilesWithDirWithTrailingSlash(t *testing.T) {
   204  	t.Parallel()
   205  	// When checking a zip file,
   206  	files := []fakeFile{{
   207  		name:  "cue.mod/",
   208  		isDir: true,
   209  	}, {
   210  		name: "cue.mod/module.cue",
   211  		data: []byte(`module: "example.com/m"`),
   212  	}}
   213  	_, err := modzip.CheckFiles[fakeFile](files, fakeFileIO{})
   214  	if err != nil {
   215  		t.Fatal(err)
   216  	}
   217  }
   218  
   219  // TestCheckFiles verifies behavior of CheckFiles. Note that CheckFiles is also
   220  // covered by TestCreate, TestCreateDir, and TestCreateSizeLimits, so this test
   221  // focuses on how multiple errors and omissions are reported, rather than trying
   222  // to cover every case.
   223  func TestCheckFiles(t *testing.T) {
   224  	t.Parallel()
   225  	testPaths, err := filepath.Glob(filepath.FromSlash("testdata/check_files/*.txt"))
   226  	if err != nil {
   227  		t.Fatal(err)
   228  	}
   229  	for _, testPath := range testPaths {
   230  		name := strings.TrimSuffix(filepath.Base(testPath), ".txt")
   231  		t.Run(name, func(t *testing.T) {
   232  			t.Parallel()
   233  
   234  			// Load the test.
   235  			test, err := readTest(testPath)
   236  			if err != nil {
   237  				t.Fatal(err)
   238  			}
   239  			t.Logf("test file %s", testPath)
   240  			files := make([]fakeFile, 0, len(test.archive.Files))
   241  			for _, tf := range test.archive.Files {
   242  				files = append(files, fakeFile{
   243  					name: tf.Name,
   244  					size: uint64(len(tf.Data)),
   245  					data: tf.Data,
   246  				})
   247  			}
   248  
   249  			// Check the files.
   250  			cf, _ := modzip.CheckFiles[fakeFile](files, fakeFileIO{})
   251  			got := formatCheckedFiles(cf)
   252  			if diff := cmp.Diff(test.want, got); diff != "" {
   253  				t.Errorf("unexpected result; (-want +got):\n%s", diff)
   254  			}
   255  			// Check that the error (if any) is just a list of invalid files.
   256  			// SizeError is not covered in this test.
   257  			var gotErr string
   258  			wantErr := test.wantErr
   259  			if wantErr == "" && len(cf.Invalid) > 0 {
   260  				wantErr = modzip.FileErrorList(cf.Invalid).Error()
   261  			}
   262  			if err := cf.Err(); err != nil {
   263  				gotErr = err.Error()
   264  			}
   265  			if gotErr != wantErr {
   266  				t.Errorf("got error:\n%s\n\nwant error:\n%s", gotErr, wantErr)
   267  			}
   268  		})
   269  	}
   270  }
   271  
   272  // TestCheckDir verifies behavior of the CheckDir function. Note that CheckDir
   273  // relies on CheckFiles and listFilesInDir (called by CreateFromDir), so this
   274  // test focuses on how multiple errors and omissions are reported, rather than
   275  // trying to cover every case.
   276  func TestCheckDir(t *testing.T) {
   277  	t.Parallel()
   278  	testPaths, err := filepath.Glob(filepath.FromSlash("testdata/check_dir/*.txt"))
   279  	if err != nil {
   280  		t.Fatal(err)
   281  	}
   282  	for _, testPath := range testPaths {
   283  		name := strings.TrimSuffix(filepath.Base(testPath), ".txt")
   284  		t.Run(name, func(t *testing.T) {
   285  			t.Parallel()
   286  
   287  			// Load the test and extract the files to a temporary directory.
   288  			test, err := readTest(testPath)
   289  			if err != nil {
   290  				t.Fatal(err)
   291  			}
   292  			t.Logf("test file %s", testPath)
   293  			tmpDir, err := extractTxtarToTempDir(t, test.archive)
   294  			if err != nil {
   295  				t.Fatal(err)
   296  			}
   297  
   298  			// Check the directory.
   299  			cf, _ := modzip.CheckDir(tmpDir)
   300  			rep := strings.NewReplacer(tmpDir, "$work", `'\''`, `'\''`, string(os.PathSeparator), "/")
   301  			got := rep.Replace(formatCheckedFiles(cf))
   302  			if diff := cmp.Diff(test.want, got); diff != "" {
   303  				t.Errorf("unexpected result; (-want +got):\n%s", diff)
   304  			}
   305  
   306  			// Check that the error (if any) is just a list of invalid files.
   307  			// SizeError is not covered in this test.
   308  			var gotErr string
   309  			wantErr := test.wantErr
   310  			if wantErr == "" && len(cf.Invalid) > 0 {
   311  				wantErr = modzip.FileErrorList(cf.Invalid).Error()
   312  			}
   313  			if err := cf.Err(); err != nil {
   314  				gotErr = err.Error()
   315  			}
   316  			if gotErr != wantErr {
   317  				t.Errorf("got error:\n%s\n\nwant error:\n%s", gotErr, wantErr)
   318  			}
   319  		})
   320  	}
   321  }
   322  
   323  // TestCheckZip verifies behavior of CheckZip. Note that CheckZip is also
   324  // covered by TestUnzip, so this test focuses on how multiple errors are
   325  // reported, rather than trying to cover every case.
   326  func TestCheckZip(t *testing.T) {
   327  	t.Parallel()
   328  	testPaths, err := filepath.Glob(filepath.FromSlash("testdata/check_zip/*.txt"))
   329  	if err != nil {
   330  		t.Fatal(err)
   331  	}
   332  	for _, testPath := range testPaths {
   333  		name := strings.TrimSuffix(filepath.Base(testPath), ".txt")
   334  		t.Run(name, func(t *testing.T) {
   335  			t.Parallel()
   336  
   337  			// Load the test and extract the files to a temporary zip file.
   338  			test, err := readTest(testPath)
   339  			if err != nil {
   340  				t.Fatal(err)
   341  			}
   342  			t.Logf("test file %s", testPath)
   343  			tmpZipPath, err := extractTxtarToTempZip(t, test.archive)
   344  			if err != nil {
   345  				t.Fatal(err)
   346  			}
   347  
   348  			// Check the zip.
   349  			m := module.MustNewVersion(test.path, test.version)
   350  			cf, checkZipErr := modzip.CheckZipFile(m, tmpZipPath)
   351  			got := formatCheckedFiles(cf)
   352  			if diff := cmp.Diff(test.want, got); diff != "" {
   353  				t.Errorf("unexpected result; (-want +got):\n%s", diff)
   354  			}
   355  
   356  			// Check that the error (if any) is just a list of invalid files.
   357  			// SizeError is not covered in this test.
   358  			var gotErr string
   359  			wantErr := test.wantErr
   360  			if wantErr == "" && len(cf.Invalid) > 0 {
   361  				wantErr = modzip.FileErrorList(cf.Invalid).Error()
   362  			}
   363  			if checkZipErr != nil {
   364  				gotErr = checkZipErr.Error()
   365  			} else if err := cf.Err(); err != nil {
   366  				gotErr = err.Error()
   367  			}
   368  			if gotErr != wantErr {
   369  				t.Errorf("got error:\n%s\n\nwant error:\n%s", gotErr, wantErr)
   370  			}
   371  		})
   372  	}
   373  }
   374  
   375  func TestCreate(t *testing.T) {
   376  	t.Parallel()
   377  	testDir := filepath.FromSlash("testdata/create")
   378  	testInfos, err := os.ReadDir(testDir)
   379  	if err != nil {
   380  		t.Fatal(err)
   381  	}
   382  	for _, testInfo := range testInfos {
   383  		base := filepath.Base(testInfo.Name())
   384  		if filepath.Ext(base) != ".txt" {
   385  			continue
   386  		}
   387  		t.Run(base[:len(base)-len(".txt")], func(t *testing.T) {
   388  			t.Parallel()
   389  
   390  			// Load the test.
   391  			testPath := filepath.Join(testDir, testInfo.Name())
   392  			test, err := readTest(testPath)
   393  			if err != nil {
   394  				t.Fatal(err)
   395  			}
   396  			t.Logf("test file: %s", testPath)
   397  
   398  			// Write zip to temporary file.
   399  			tmpZipFile := tempFile(t, "tmp.zip")
   400  			m := module.MustNewVersion(test.path, test.version)
   401  			files := make([]fakeFile, len(test.archive.Files))
   402  			for i, tf := range test.archive.Files {
   403  				files[i] = fakeFile{
   404  					name: tf.Name,
   405  					size: uint64(len(tf.Data)),
   406  					data: tf.Data,
   407  				}
   408  			}
   409  			if err := modzip.Create[fakeFile](tmpZipFile, m, files, fakeFileIO{}); err != nil {
   410  				if test.wantErr == "" {
   411  					t.Fatalf("unexpected error: %v", err)
   412  				} else if !strings.Contains(err.Error(), test.wantErr) {
   413  					t.Fatalf("got error %q; want error containing %q", err.Error(), test.wantErr)
   414  				} else {
   415  					return
   416  				}
   417  			} else if test.wantErr != "" {
   418  				t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
   419  			}
   420  			if err := tmpZipFile.Close(); err != nil {
   421  				t.Fatal(err)
   422  			}
   423  
   424  			// Hash zip file, compare with known value.
   425  			if hash, err := dirhash.HashZip(tmpZipFile.Name(), dirhash.Hash1); err != nil {
   426  				t.Fatal(err)
   427  			} else if hash != test.hash {
   428  				t.Errorf("got hash: %q\nwant: %q", hash, test.hash)
   429  			}
   430  			assertNoExcludedFiles(t, tmpZipFile.Name())
   431  		})
   432  	}
   433  }
   434  
   435  func assertNoExcludedFiles(t *testing.T, zf string) {
   436  	z, err := zip.OpenReader(zf)
   437  	if err != nil {
   438  		t.Fatal(err)
   439  	}
   440  	defer z.Close()
   441  	for _, f := range z.File {
   442  		if shouldExclude(f) {
   443  			t.Errorf("file %s should have been excluded but was not", f.Name)
   444  		}
   445  	}
   446  }
   447  
   448  func shouldExclude(f *zip.File) bool {
   449  	r, err := f.Open()
   450  	if err != nil {
   451  		panic(err)
   452  	}
   453  	defer r.Close()
   454  	data, err := io.ReadAll(r)
   455  	if err != nil {
   456  		panic(err)
   457  	}
   458  	return bytes.Contains(data, []byte("excluded"))
   459  }
   460  
   461  func TestCreateFromDir(t *testing.T) {
   462  	t.Parallel()
   463  	testDir := filepath.FromSlash("testdata/create_from_dir")
   464  	testInfos, err := os.ReadDir(testDir)
   465  	if err != nil {
   466  		t.Fatal(err)
   467  	}
   468  	for _, testInfo := range testInfos {
   469  		base := filepath.Base(testInfo.Name())
   470  		if filepath.Ext(base) != ".txt" {
   471  			continue
   472  		}
   473  		t.Run(base[:len(base)-len(".txt")], func(t *testing.T) {
   474  			t.Parallel()
   475  
   476  			// Load the test.
   477  			testPath := filepath.Join(testDir, testInfo.Name())
   478  			test, err := readTest(testPath)
   479  			if err != nil {
   480  				t.Fatal(err)
   481  			}
   482  			t.Logf("test file %s", testPath)
   483  
   484  			// Write files to a temporary directory.
   485  			tmpDir, err := extractTxtarToTempDir(t, test.archive)
   486  			if err != nil {
   487  				t.Fatal(err)
   488  			}
   489  
   490  			// Create zip from the directory.
   491  			tmpZipFile := tempFile(t, "tmp.zip")
   492  			m := module.MustNewVersion(test.path, test.version)
   493  			if err := modzip.CreateFromDir(tmpZipFile, m, tmpDir); err != nil {
   494  				if test.wantErr == "" {
   495  					t.Fatalf("unexpected error: %v", err)
   496  				} else if !strings.Contains(err.Error(), test.wantErr) {
   497  					t.Fatalf("got error %q; want error containing %q", err, test.wantErr)
   498  				} else {
   499  					return
   500  				}
   501  			} else if test.wantErr != "" {
   502  				t.Fatalf("unexpected success; want error containing %q", test.wantErr)
   503  			}
   504  
   505  			// Hash zip file, compare with known value.
   506  			if hash, err := dirhash.HashZip(tmpZipFile.Name(), dirhash.Hash1); err != nil {
   507  				t.Fatal(err)
   508  			} else if hash != test.hash {
   509  				t.Fatalf("got hash: %q\nwant: %q", hash, test.hash)
   510  			}
   511  			assertNoExcludedFiles(t, tmpZipFile.Name())
   512  		})
   513  	}
   514  }
   515  
   516  func TestCreateFromDirSpecial(t *testing.T) {
   517  	t.Parallel()
   518  	for _, test := range []struct {
   519  		desc     string
   520  		setup    func(t *testing.T, tmpDir string) string
   521  		wantHash string
   522  	}{
   523  		{
   524  			desc: "ignore_empty_dir",
   525  			setup: func(t *testing.T, tmpDir string) string {
   526  				if err := os.Mkdir(filepath.Join(tmpDir, "empty"), 0777); err != nil {
   527  					t.Fatal(err)
   528  				}
   529  				mustWriteFile(
   530  					filepath.Join(tmpDir, "cue.mod/module.cue"),
   531  					`module: "example.com/m"`,
   532  				)
   533  				return tmpDir
   534  			},
   535  			wantHash: "h1:vEUjl4tTsFcZJC/Ed/Rph2nVDCMG7OFC4wrQDfxF3n0=",
   536  		}, {
   537  			desc: "ignore_symlink",
   538  			setup: func(t *testing.T, tmpDir string) string {
   539  				if err := os.Symlink(tmpDir, filepath.Join(tmpDir, "link")); err != nil {
   540  					switch runtime.GOOS {
   541  					case "plan9", "windows":
   542  						t.Skipf("could not create symlink: %v", err)
   543  					default:
   544  						t.Fatal(err)
   545  					}
   546  				}
   547  				mustWriteFile(
   548  					filepath.Join(tmpDir, "cue.mod/module.cue"),
   549  					`module: "example.com/m"`,
   550  				)
   551  				return tmpDir
   552  			},
   553  			wantHash: "h1:vEUjl4tTsFcZJC/Ed/Rph2nVDCMG7OFC4wrQDfxF3n0=",
   554  		}, {
   555  			desc: "dir_is_vendor",
   556  			setup: func(t *testing.T, tmpDir string) string {
   557  				vendorDir := filepath.Join(tmpDir, "vendor")
   558  				if err := os.Mkdir(vendorDir, 0777); err != nil {
   559  					t.Fatal(err)
   560  				}
   561  				mustWriteFile(
   562  					filepath.Join(vendorDir, "cue.mod/module.cue"),
   563  					`module: "example.com/m"`,
   564  				)
   565  				return vendorDir
   566  			},
   567  			wantHash: "h1:vEUjl4tTsFcZJC/Ed/Rph2nVDCMG7OFC4wrQDfxF3n0=",
   568  		},
   569  	} {
   570  		t.Run(test.desc, func(t *testing.T) {
   571  			tmpDir := t.TempDir()
   572  			dir := test.setup(t, tmpDir)
   573  
   574  			tmpZipFile := tempFile(t, "tmp.zip")
   575  			m := module.MustNewVersion("example.com/m@v1", "v1.0.0")
   576  
   577  			if err := modzip.CreateFromDir(tmpZipFile, m, dir); err != nil {
   578  				t.Fatal(err)
   579  			}
   580  			if err := tmpZipFile.Close(); err != nil {
   581  				t.Fatal(err)
   582  			}
   583  
   584  			if hash, err := dirhash.HashZip(tmpZipFile.Name(), dirhash.Hash1); err != nil {
   585  				t.Fatal(err)
   586  			} else if hash != test.wantHash {
   587  				t.Fatalf("got hash %q; want %q", hash, test.wantHash)
   588  			}
   589  		})
   590  	}
   591  }
   592  
   593  func TestUnzip(t *testing.T) {
   594  	t.Parallel()
   595  	testDir := filepath.FromSlash("testdata/unzip")
   596  	testInfos, err := os.ReadDir(testDir)
   597  	if err != nil {
   598  		t.Fatal(err)
   599  	}
   600  	for _, testInfo := range testInfos {
   601  		base := filepath.Base(testInfo.Name())
   602  		if filepath.Ext(base) != ".txt" {
   603  			continue
   604  		}
   605  		t.Run(base[:len(base)-len(".txt")], func(t *testing.T) {
   606  			// Load the test.
   607  			testPath := filepath.Join(testDir, testInfo.Name())
   608  			test, err := readTest(testPath)
   609  			if err != nil {
   610  				t.Fatal(err)
   611  			}
   612  			t.Logf("test file %s", testPath)
   613  
   614  			// Convert txtar to temporary zip file.
   615  			tmpZipPath, err := extractTxtarToTempZip(t, test.archive)
   616  			if err != nil {
   617  				t.Fatal(err)
   618  			}
   619  
   620  			// Extract to a temporary directory.
   621  			tmpDir := t.TempDir()
   622  			m := module.MustNewVersion(test.path, test.version)
   623  			if err := modzip.Unzip(tmpDir, m, tmpZipPath); err != nil {
   624  				if test.wantErr == "" {
   625  					t.Fatalf("unexpected error: %v", err)
   626  				} else if !strings.Contains(err.Error(), test.wantErr) {
   627  					t.Fatalf("got error %q; want error containing %q", err.Error(), test.wantErr)
   628  				} else {
   629  					return
   630  				}
   631  			} else if test.wantErr != "" {
   632  				t.Fatalf("unexpected success; wanted error containing %q", test.wantErr)
   633  			}
   634  
   635  			// Hash the directory, compare to known value.
   636  			if hash, err := dirhash.HashDir(tmpDir, "", dirhash.Hash1); err != nil {
   637  				t.Fatal(err)
   638  			} else if hash != test.hash {
   639  				t.Fatalf("got hash %q\nwant: %q", hash, test.hash)
   640  			}
   641  		})
   642  	}
   643  }
   644  
   645  type sizeLimitTest struct {
   646  	desc              string
   647  	files             []fakeFile
   648  	wantErr           string
   649  	wantCheckFilesErr string
   650  	wantCreateErr     string
   651  	wantCheckZipErr   string
   652  	wantUnzipErr      string
   653  }
   654  
   655  // sizeLimitTests is shared by TestCreateSizeLimits and TestUnzipSizeLimits.
   656  var sizeLimitTests = [...]sizeLimitTest{
   657  	{
   658  		desc: "total_large",
   659  		files: []fakeFile{{
   660  			name: "large.go",
   661  			size: modzip.MaxZipFile - uint64(len(`module: "example.com/m@v1"`)),
   662  		}, {
   663  			name: "cue.mod/module.cue",
   664  			data: []byte(`module: "example.com/m@v1"`),
   665  		}},
   666  	}, {
   667  		desc: "total_too_large",
   668  		files: []fakeFile{{
   669  			name: "large.go",
   670  			size: modzip.MaxZipFile - uint64(len(`module: "example.com/m@v1"`)) + 1,
   671  		}, {
   672  			name: "cue.mod/module.cue",
   673  			data: []byte(`module: "example.com/m@v1"`),
   674  		}},
   675  		wantCheckFilesErr: "module source tree too large",
   676  		wantCreateErr:     "module source tree too large",
   677  		wantCheckZipErr:   "total uncompressed size of module contents too large",
   678  		wantUnzipErr:      "total uncompressed size of module contents too large",
   679  	}, {
   680  		desc: "large_cuemod",
   681  		files: []fakeFile{{
   682  			name: "cue.mod/module.cue",
   683  			size: modzip.MaxCUEMod,
   684  		}},
   685  	}, {
   686  		desc: "too_large_cuemod",
   687  		files: []fakeFile{{
   688  			name: "cue.mod/module.cue",
   689  			size: modzip.MaxCUEMod + 1,
   690  		}},
   691  		wantErr: "cue.mod/module.cue file too large",
   692  	}, {
   693  		desc: "large_license",
   694  		files: []fakeFile{{
   695  			name: "LICENSE",
   696  			size: modzip.MaxLICENSE,
   697  		}, {
   698  			name: "cue.mod/module.cue",
   699  			data: []byte(`module: "example.com/m@v1"`),
   700  		}},
   701  	}, {
   702  		desc: "too_large_license",
   703  		files: []fakeFile{{
   704  			name: "LICENSE",
   705  			size: modzip.MaxLICENSE + 1,
   706  		}, {
   707  			name: "cue.mod/module.cue",
   708  			data: []byte(`module: "example.com/m@v1"`),
   709  		}},
   710  		wantErr: "LICENSE file too large",
   711  	},
   712  }
   713  
   714  var sizeLimitVersion = module.MustNewVersion("example.com/large@v1", "v1.0.0")
   715  
   716  func TestCreateSizeLimits(t *testing.T) {
   717  	if testing.Short() || cuetest.RaceEnabled {
   718  		t.Skip("creating large files takes time")
   719  	}
   720  	t.Parallel()
   721  	tests := append(sizeLimitTests[:], sizeLimitTest{
   722  		// negative file size may happen when size is represented as uint64
   723  		// but is cast to int64, as is the case in zip files.
   724  		desc: "negative",
   725  		files: []fakeFile{{
   726  			name: "neg.go",
   727  			size: 0x8000000000000000,
   728  		}, {
   729  			name: "cue.mod/module.cue",
   730  			data: []byte(`module: "example.com/m@v1"`),
   731  		}},
   732  		wantErr: "module source tree too large",
   733  	}, sizeLimitTest{
   734  		desc: "size_is_a_lie",
   735  		files: []fakeFile{{
   736  			name: "lie.go",
   737  			size: 1,
   738  			data: []byte(`package large`),
   739  		}, {
   740  			name: "cue.mod/module.cue",
   741  			data: []byte(`module: "example.com/m@v1"`),
   742  		}},
   743  		wantCreateErr: "larger than declared size",
   744  	})
   745  
   746  	for _, test := range tests {
   747  		t.Run(test.desc, func(t *testing.T) {
   748  			t.Parallel()
   749  
   750  			wantCheckFilesErr := test.wantCheckFilesErr
   751  			if wantCheckFilesErr == "" {
   752  				wantCheckFilesErr = test.wantErr
   753  			}
   754  			if _, err := modzip.CheckFiles[fakeFile](test.files, fakeFileIO{}); err == nil && wantCheckFilesErr != "" {
   755  				t.Fatalf("CheckFiles: unexpected success; want error containing %q", wantCheckFilesErr)
   756  			} else if err != nil && wantCheckFilesErr == "" {
   757  				t.Fatalf("CheckFiles: got error %q; want success", err)
   758  			} else if err != nil && !strings.Contains(err.Error(), wantCheckFilesErr) {
   759  				t.Fatalf("CheckFiles: got error %q; want error containing %q", err, wantCheckFilesErr)
   760  			}
   761  
   762  			wantCreateErr := test.wantCreateErr
   763  			if wantCreateErr == "" {
   764  				wantCreateErr = test.wantErr
   765  			}
   766  			if err := modzip.Create[fakeFile](io.Discard, sizeLimitVersion, test.files, fakeFileIO{}); err == nil && wantCreateErr != "" {
   767  				t.Fatalf("Create: unexpected success; want error containing %q", wantCreateErr)
   768  			} else if err != nil && wantCreateErr == "" {
   769  				t.Fatalf("Create: got error %q; want success", err)
   770  			} else if err != nil && !strings.Contains(err.Error(), wantCreateErr) {
   771  				t.Fatalf("Create: got error %q; want error containing %q", err, wantCreateErr)
   772  			}
   773  		})
   774  	}
   775  }
   776  
   777  func TestUnzipSizeLimits(t *testing.T) {
   778  	if testing.Short() || cuetest.RaceEnabled {
   779  		t.Skip("creating large files takes time")
   780  	}
   781  	t.Parallel()
   782  	for _, test := range sizeLimitTests {
   783  		t.Run(test.desc, func(t *testing.T) {
   784  			t.Parallel()
   785  			tmpZipFile := tempFile(t, "tmp.zip")
   786  
   787  			zw := zip.NewWriter(tmpZipFile)
   788  			for _, tf := range test.files {
   789  				zf, err := zw.Create(fakeFileIO{}.Path(tf))
   790  				if err != nil {
   791  					t.Fatal(err)
   792  				}
   793  				rc, err := fakeFileIO{}.Open(tf)
   794  				if err != nil {
   795  					t.Fatal(err)
   796  				}
   797  				_, err = io.Copy(zf, rc)
   798  				rc.Close()
   799  				if err != nil {
   800  					t.Fatal(err)
   801  				}
   802  			}
   803  			if err := zw.Close(); err != nil {
   804  				t.Fatal(err)
   805  			}
   806  			if err := tmpZipFile.Close(); err != nil {
   807  				t.Fatal(err)
   808  			}
   809  
   810  			tmpDir := t.TempDir()
   811  
   812  			wantCheckZipErr := test.wantCheckZipErr
   813  			if wantCheckZipErr == "" {
   814  				wantCheckZipErr = test.wantErr
   815  			}
   816  			cf, err := modzip.CheckZipFile(sizeLimitVersion, tmpZipFile.Name())
   817  			if err == nil {
   818  				err = cf.Err()
   819  			}
   820  			if err == nil && wantCheckZipErr != "" {
   821  				t.Fatalf("CheckZip: unexpected success; want error containing %q", wantCheckZipErr)
   822  			} else if err != nil && wantCheckZipErr == "" {
   823  				t.Fatalf("CheckZip: got error %q; want success", err)
   824  			} else if err != nil && !strings.Contains(err.Error(), wantCheckZipErr) {
   825  				t.Fatalf("CheckZip: got error %q; want error containing %q", err, wantCheckZipErr)
   826  			}
   827  
   828  			wantUnzipErr := test.wantUnzipErr
   829  			if wantUnzipErr == "" {
   830  				wantUnzipErr = test.wantErr
   831  			}
   832  			if err := modzip.Unzip(tmpDir, sizeLimitVersion, tmpZipFile.Name()); err == nil && wantUnzipErr != "" {
   833  				t.Fatalf("Unzip: unexpected success; want error containing %q", wantUnzipErr)
   834  			} else if err != nil && wantUnzipErr == "" {
   835  				t.Fatalf("Unzip: got error %q; want success", err)
   836  			} else if err != nil && !strings.Contains(err.Error(), wantUnzipErr) {
   837  				t.Fatalf("Unzip: got error %q; want error containing %q", err, wantUnzipErr)
   838  			}
   839  		})
   840  	}
   841  }
   842  
   843  func TestUnzipSizeLimitsSpecial(t *testing.T) {
   844  	if testing.Short() || cuetest.RaceEnabled {
   845  		t.Skip("skipping test; creating large files takes time")
   846  	}
   847  
   848  	t.Parallel()
   849  	for _, test := range []struct {
   850  		desc     string
   851  		wantErr  string
   852  		m        module.Version
   853  		writeZip func(t *testing.T, zipFile *os.File)
   854  	}{
   855  		{
   856  			desc: "large_zip",
   857  			m:    module.MustNewVersion("example.com/m@v1", "v1.0.0"),
   858  			writeZip: func(t *testing.T, zipFile *os.File) {
   859  				if err := zipFile.Truncate(modzip.MaxZipFile); err != nil {
   860  					t.Fatal(err)
   861  				}
   862  			},
   863  			// this is not an error we care about; we're just testing whether
   864  			// Unzip checks the size of the file before opening.
   865  			// It's harder to create a valid zip file of exactly the right size.
   866  			wantErr: "not a valid zip file",
   867  		}, {
   868  			desc: "too_large_zip",
   869  			m:    module.MustNewVersion("example.com/m@v1", "v1.0.0"),
   870  			writeZip: func(t *testing.T, zipFile *os.File) {
   871  				if err := zipFile.Truncate(modzip.MaxZipFile + 1); err != nil {
   872  					t.Fatal(err)
   873  				}
   874  			},
   875  			wantErr: "module zip file is too large",
   876  		}, {
   877  			desc: "size_is_a_lie",
   878  			m:    module.MustNewVersion("example.com/m@v1", "v1.0.0"),
   879  			writeZip: func(t *testing.T, zipFile *os.File) {
   880  				// Create a normal zip file in memory containing one file full of zero
   881  				// bytes. Use a distinctive size so we can find it later.
   882  				zipBuf := &bytes.Buffer{}
   883  				zw := zip.NewWriter(zipBuf)
   884  				f, err := zw.Create("cue.mod/module.cue")
   885  				if err != nil {
   886  					t.Fatal(err)
   887  				}
   888  				realSize := 0x0BAD
   889  				buf := make([]byte, realSize)
   890  				if _, err := f.Write(buf); err != nil {
   891  					t.Fatal(err)
   892  				}
   893  				if err := zw.Close(); err != nil {
   894  					t.Fatal(err)
   895  				}
   896  
   897  				// Replace the uncompressed size of the file. As a shortcut, we just
   898  				// search-and-replace the byte sequence. It should occur twice because
   899  				// the 32- and 64-byte sizes are stored separately. All multi-byte
   900  				// values are little-endian.
   901  				zipData := zipBuf.Bytes()
   902  				realSizeData := []byte{0xAD, 0x0B}
   903  				fakeSizeData := []byte{0xAC, 0x00}
   904  				s := zipData
   905  				n := 0
   906  				for {
   907  					if i := bytes.Index(s, realSizeData); i < 0 {
   908  						break
   909  					} else {
   910  						s = s[i:]
   911  					}
   912  					copy(s[:len(fakeSizeData)], fakeSizeData)
   913  					n++
   914  				}
   915  				if n != 2 {
   916  					t.Fatalf("replaced size %d times; expected 2", n)
   917  				}
   918  
   919  				// Write the modified zip to the actual file.
   920  				if _, err := zipFile.Write(zipData); err != nil {
   921  					t.Fatal(err)
   922  				}
   923  			},
   924  			wantErr: "not a valid zip file",
   925  		},
   926  	} {
   927  		t.Run(test.desc, func(t *testing.T) {
   928  			t.Parallel()
   929  
   930  			tmpZipFile := tempFile(t, "tmp.zip")
   931  			test.writeZip(t, tmpZipFile)
   932  			if err := tmpZipFile.Close(); err != nil {
   933  				t.Fatal(err)
   934  			}
   935  
   936  			tmpDir := t.TempDir()
   937  
   938  			if err := modzip.Unzip(tmpDir, test.m, tmpZipFile.Name()); err == nil && test.wantErr != "" {
   939  				t.Fatalf("unexpected success; want error containing %q", test.wantErr)
   940  			} else if err != nil && test.wantErr == "" {
   941  				t.Fatalf("got error %q; want success", err)
   942  			} else if err != nil && !strings.Contains(err.Error(), test.wantErr) {
   943  				t.Fatalf("got error %q; want error containing %q", err, test.wantErr)
   944  			}
   945  		})
   946  	}
   947  }
   948  
   949  func mustWriteFile(name string, content string) {
   950  	if err := os.MkdirAll(filepath.Dir(name), 0o777); err != nil {
   951  		panic(err)
   952  	}
   953  	if err := os.WriteFile(name, []byte(content), 0o666); err != nil {
   954  		panic(err)
   955  	}
   956  }
   957  
   958  func tempFile(t *testing.T, name string) *os.File {
   959  	f, err := os.Create(filepath.Join(t.TempDir(), name))
   960  	if err != nil {
   961  		t.Fatal(err)
   962  	}
   963  	t.Cleanup(func() { f.Close() })
   964  	return f
   965  }