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 }