|
5 | 5 | archive_tar "archive/tar" |
6 | 6 | "bytes" |
7 | 7 | "context" |
| 8 | + stderrors "errors" |
8 | 9 | "fmt" |
9 | 10 | "io" |
10 | 11 | "net/http" |
@@ -487,6 +488,72 @@ func TestExtractZstdDrainsBufferedReaderToEOF(t *testing.T) { |
487 | 488 | }) |
488 | 489 | } |
489 | 490 |
|
| 491 | +// ─── Truncated archive test ────────────────────────────────────────────────── |
| 492 | + |
| 493 | +// TestExtractBundleTruncatedArchive verifies that extractBundleZstd returns an |
| 494 | +// io.ErrUnexpectedEOF when the archive is truncated, and that no partially |
| 495 | +// written files are left on disk. |
| 496 | +func TestExtractBundleTruncatedArchive(t *testing.T) { |
| 497 | + ctx := context.Background() |
| 498 | + |
| 499 | + t.Run("small files", func(t *testing.T) { |
| 500 | + srcDir := t.TempDir() |
| 501 | + must(t, os.MkdirAll(filepath.Join(srcDir, "caches"), 0o755)) |
| 502 | + must(t, os.WriteFile(filepath.Join(srcDir, "caches", "first.jar"), bytes.Repeat([]byte("A"), 4096), 0o644)) |
| 503 | + must(t, os.WriteFile(filepath.Join(srcDir, "caches", "second.jar"), bytes.Repeat([]byte("B"), 4096), 0o644)) |
| 504 | + |
| 505 | + var archive bytes.Buffer |
| 506 | + must(t, CreateDeltaTarZstd(ctx, &archive, srcDir, []string{"caches/first.jar", "caches/second.jar"})) |
| 507 | + |
| 508 | + truncated := archive.Bytes()[:archive.Len()*60/100] |
| 509 | + gradleHome := t.TempDir() |
| 510 | + projectDir := t.TempDir() |
| 511 | + |
| 512 | + _, err := extractBundleZstd(ctx, bytes.NewReader(truncated), []extractRule{ |
| 513 | + {prefix: "caches/", baseDir: gradleHome}, |
| 514 | + }, projectDir, false) |
| 515 | + |
| 516 | + if err == nil { |
| 517 | + t.Fatal("expected an error from truncated archive, got nil") |
| 518 | + } |
| 519 | + if !stderrors.Is(err, io.ErrUnexpectedEOF) { |
| 520 | + t.Fatalf("expected io.ErrUnexpectedEOF in error chain, got: %v", err) |
| 521 | + } |
| 522 | + }) |
| 523 | + |
| 524 | + t.Run("large file no partial artifact", func(t *testing.T) { |
| 525 | + srcDir := t.TempDir() |
| 526 | + must(t, os.MkdirAll(filepath.Join(srcDir, "caches"), 0o755)) |
| 527 | + // 5 MB file — larger than maxBufferedFileSize so it takes the |
| 528 | + // streaming large-file path. |
| 529 | + must(t, os.WriteFile(filepath.Join(srcDir, "caches", "big.jar"), bytes.Repeat([]byte("X"), 5<<20), 0o644)) |
| 530 | + |
| 531 | + var archive bytes.Buffer |
| 532 | + must(t, CreateDeltaTarZstd(ctx, &archive, srcDir, []string{"caches/big.jar"})) |
| 533 | + |
| 534 | + // Truncate at ~60% so the large-file read fails mid-stream. |
| 535 | + truncated := archive.Bytes()[:archive.Len()*60/100] |
| 536 | + gradleHome := t.TempDir() |
| 537 | + projectDir := t.TempDir() |
| 538 | + |
| 539 | + _, err := extractBundleZstd(ctx, bytes.NewReader(truncated), []extractRule{ |
| 540 | + {prefix: "caches/", baseDir: gradleHome}, |
| 541 | + }, projectDir, false) |
| 542 | + |
| 543 | + if err == nil { |
| 544 | + t.Fatal("expected an error from truncated archive, got nil") |
| 545 | + } |
| 546 | + if !stderrors.Is(err, io.ErrUnexpectedEOF) { |
| 547 | + t.Fatalf("expected io.ErrUnexpectedEOF in error chain, got: %v", err) |
| 548 | + } |
| 549 | + |
| 550 | + // The partially-written large file must not remain on disk. |
| 551 | + if _, statErr := os.Stat(filepath.Join(gradleHome, "caches", "big.jar")); statErr == nil { |
| 552 | + t.Fatal("partially-written big.jar should have been removed") |
| 553 | + } |
| 554 | + }) |
| 555 | +} |
| 556 | + |
490 | 557 | // ─── Round-trip archive test ───────────────────────────────────────────────── |
491 | 558 |
|
492 | 559 | // TestTarZstdRoundTrip verifies that CreateTarZstd → extractTarZstd preserves |
|
0 commit comments