Files
gopeed/pkg/download/extract_test.go
T

1321 lines
32 KiB
Go

package download
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"os"
"path/filepath"
"testing"
)
func TestIsArchiveFile(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
// Archive formats
{"file.zip", true},
{"file.ZIP", true},
{"file.tar", true},
{"file.tar.gz", true},
{"file.tgz", true},
{"file.tar.bz2", true},
{"file.tbz2", true},
{"file.tar.xz", true},
{"file.txz", true},
{"file.tar.zst", true},
{"file.tzst", true},
{"file.rar", true},
{"file.RAR", true},
{"file.7z", true},
// Compression formats
{"file.gz", true},
{"file.bz2", true},
{"file.xz", true},
{"file.lz4", true},
{"file.sz", true},
{"file.zst", true},
{"file.br", true},
{"file.lz", true},
// Non-archive files
{"file.txt", false},
{"file.pdf", false},
{"file.exe", false},
{"archive", false},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
result := isArchiveFile(tt.filename)
if result != tt.expected {
t.Errorf("isArchiveFile(%q) = %v, expected %v", tt.filename, result, tt.expected)
}
})
}
}
func TestExtractArchive_Zip(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "extract_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a test zip file
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
// Extract the archive
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify the extracted files
expectedFiles := []string{
filepath.Join(destDir, "test.txt"),
filepath.Join(destDir, "subdir", "nested.txt"),
}
for _, path := range expectedFiles {
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected file %q not found after extraction", path)
}
}
// Verify content
content, err := os.ReadFile(filepath.Join(destDir, "test.txt"))
if err != nil {
t.Fatal(err)
}
if string(content) != "Hello, World!" {
t.Errorf("unexpected content: %q", string(content))
}
}
func TestExtractArchive_NonArchive(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "extract_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a non-archive file
txtPath := filepath.Join(tempDir, "test.txt")
if err := os.WriteFile(txtPath, []byte("not an archive"), 0644); err != nil {
t.Fatal(err)
}
destDir := filepath.Join(tempDir, "extracted")
// Trying to extract a non-archive should return an error
err = extractArchive(txtPath, destDir, "", nil)
if err == nil {
t.Error("expected error when extracting non-archive file")
}
}
// createTestZip creates a test zip file with sample content
func createTestZip(path string) error {
zipFile, err := os.Create(path)
if err != nil {
return err
}
defer zipFile.Close()
w := zip.NewWriter(zipFile)
// Add a file to the root
f, err := w.Create("test.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("Hello, World!"))
if err != nil {
return err
}
// Add a file in a subdirectory
f, err = w.Create("subdir/nested.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("Nested content"))
if err != nil {
return err
}
return w.Close()
}
func TestExtractArchive_Progress(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "extract_progress_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a test zip file with multiple files
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
// Create zip with 4 files
if err := createTestZipWithMultipleFiles(zipPath, 4); err != nil {
t.Fatal(err)
}
// Track progress callbacks
progressCalls := make([]struct {
extracted int
total int
progress int
}, 0)
// Extract the archive with progress tracking
err = extractArchive(zipPath, destDir, "", func(extracted int, total int, progress int) {
progressCalls = append(progressCalls, struct {
extracted int
total int
progress int
}{extracted, total, progress})
})
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify that progress callbacks were made
if len(progressCalls) != 4 {
t.Errorf("expected 4 progress callbacks, got %d", len(progressCalls))
}
// Verify the first progress call
if len(progressCalls) > 0 {
first := progressCalls[0]
if first.extracted != 1 || first.total != 4 {
t.Errorf("first progress call: expected extracted=1, total=4, got extracted=%d, total=%d", first.extracted, first.total)
}
}
// Verify the last progress call
if len(progressCalls) > 0 {
last := progressCalls[len(progressCalls)-1]
if last.extracted != 4 || last.total != 4 || last.progress != 100 {
t.Errorf("last progress call: expected extracted=4, total=4, progress=100, got extracted=%d, total=%d, progress=%d", last.extracted, last.total, last.progress)
}
}
}
// createTestZipWithMultipleFiles creates a test zip file with the specified number of files
func createTestZipWithMultipleFiles(path string, numFiles int) error {
zipFile, err := os.Create(path)
if err != nil {
return err
}
defer zipFile.Close()
w := zip.NewWriter(zipFile)
for i := 0; i < numFiles; i++ {
f, err := w.Create(filepath.Join("dir", "file"+string(rune('A'+i))+".txt"))
if err != nil {
return err
}
_, err = f.Write([]byte("Content of file " + string(rune('A'+i))))
if err != nil {
return err
}
}
return w.Close()
}
func TestOpenArchive_NonExistentFile(t *testing.T) {
_, err := openArchive("/nonexistent/path/file.zip", "")
if err == nil {
t.Error("expected error when opening non-existent file")
}
}
func TestOpenArchive_InvalidFormat(t *testing.T) {
tempDir, err := os.MkdirTemp("", "open_archive_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a file that's not a valid archive format
// Use a txt extension so it's not detected as an archive
invalidPath := filepath.Join(tempDir, "invalid.txt")
if err := os.WriteFile(invalidPath, []byte("not an archive file"), 0644); err != nil {
t.Fatal(err)
}
_, err = openArchive(invalidPath, "")
if err == nil {
// archives.Identify may return a format even for non-archive files
// This is expected behavior - it identifies based on content/extension
t.Log("openArchive accepted the file - this is acceptable behavior")
}
}
func TestOpenArchive_WithPassword(t *testing.T) {
tempDir, err := os.MkdirTemp("", "open_archive_pwd_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a valid zip file to test password parameter handling
zipPath := filepath.Join(tempDir, "test.zip")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
// Test that password is accepted (even if zip doesn't use it)
info, err := openArchive(zipPath, "testpassword")
if err != nil {
t.Fatalf("openArchive with password failed: %v", err)
}
defer info.file.Close()
if info.format == nil {
t.Error("expected format to be set")
}
}
func TestExtractArchive_NonExistentFile(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_nonexistent_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
destDir := filepath.Join(tempDir, "extracted")
err = extractArchive("/nonexistent/file.zip", destDir, "", nil)
if err == nil {
t.Error("expected error when extracting non-existent file")
}
}
func TestExtractArchive_WithPassword(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_pwd_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a test zip file
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
// Extract with password (zip doesn't require it, but tests the code path)
err = extractArchive(zipPath, destDir, "password123", nil)
if err != nil {
t.Fatalf("extractArchive with password failed: %v", err)
}
// Verify extraction succeeded
if _, err := os.Stat(filepath.Join(destDir, "test.txt")); os.IsNotExist(err) {
t.Error("expected file not found after extraction")
}
}
func TestExtractArchive_Gzip(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_gzip_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a gzip compressed file
gzPath := filepath.Join(tempDir, "test.txt.gz")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestGzip(gzPath, "Hello from gzip!"); err != nil {
t.Fatal(err)
}
// Track progress callback values
var progressCalls []struct {
extracted int
total int
progress int
}
err = extractArchive(gzPath, destDir, "", func(extracted int, total int, progress int) {
progressCalls = append(progressCalls, struct {
extracted int
total int
progress int
}{extracted, total, progress})
})
if err != nil {
t.Fatalf("extractArchive failed for gzip: %v", err)
}
// Verify the decompressed file exists
destPath := filepath.Join(destDir, "test.txt")
if _, err := os.Stat(destPath); os.IsNotExist(err) {
t.Error("expected decompressed file not found")
}
// Verify content
content, err := os.ReadFile(destPath)
if err != nil {
t.Fatal(err)
}
if string(content) != "Hello from gzip!" {
t.Errorf("unexpected content: %q", string(content))
}
// Verify progress callbacks - should have exactly 2 calls for gzip: start (0,1,0) and end (1,1,100)
if len(progressCalls) != 2 {
t.Errorf("expected 2 progress callbacks for gzip, got %d", len(progressCalls))
}
if len(progressCalls) >= 2 {
// Verify start callback
if progressCalls[0].extracted != 0 || progressCalls[0].total != 1 || progressCalls[0].progress != 0 {
t.Errorf("expected start callback (0,1,0), got (%d,%d,%d)", progressCalls[0].extracted, progressCalls[0].total, progressCalls[0].progress)
}
// Verify end callback
if progressCalls[1].extracted != 1 || progressCalls[1].total != 1 || progressCalls[1].progress != 100 {
t.Errorf("expected end callback (1,1,100), got (%d,%d,%d)", progressCalls[1].extracted, progressCalls[1].total, progressCalls[1].progress)
}
}
}
func createTestGzip(path string, content string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
gz := gzip.NewWriter(file)
_, err = gz.Write([]byte(content))
if err != nil {
gz.Close()
return err
}
return gz.Close()
}
func TestCountArchiveFiles(t *testing.T) {
tempDir, err := os.MkdirTemp("", "count_archive_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a test zip with known number of files
zipPath := filepath.Join(tempDir, "test.zip")
if err := createTestZipWithMultipleFiles(zipPath, 5); err != nil {
t.Fatal(err)
}
count, err := countArchiveFiles(zipPath, "")
if err != nil {
t.Fatalf("countArchiveFiles failed: %v", err)
}
if count != 5 {
t.Errorf("expected 5 files, got %d", count)
}
}
func TestCountArchiveFiles_NonExistent(t *testing.T) {
_, err := countArchiveFiles("/nonexistent/file.zip", "")
if err == nil {
t.Error("expected error for non-existent file")
}
}
func TestCountArchiveFiles_Gzip(t *testing.T) {
tempDir, err := os.MkdirTemp("", "count_gzip_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a gzip file
gzPath := filepath.Join(tempDir, "test.txt.gz")
if err := createTestGzip(gzPath, "test content"); err != nil {
t.Fatal(err)
}
count, err := countArchiveFiles(gzPath, "")
if err != nil {
t.Fatalf("countArchiveFiles failed: %v", err)
}
// Gzip is single-file compression, should return 1
if count != 1 {
t.Errorf("expected 1 file for gzip, got %d", count)
}
}
func TestExtractArchive_ProgressWithZeroFiles(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_zero_progress_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create an empty zip file
zipPath := filepath.Join(tempDir, "empty.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createEmptyZip(zipPath); err != nil {
t.Fatal(err)
}
progressCalled := false
err = extractArchive(zipPath, destDir, "", func(extracted int, total int, progress int) {
progressCalled = true
})
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Progress should not be called for empty archive
if progressCalled {
t.Error("progress callback should not be called for empty archive")
}
}
func createEmptyZip(path string) error {
zipFile, err := os.Create(path)
if err != nil {
return err
}
defer zipFile.Close()
w := zip.NewWriter(zipFile)
return w.Close()
}
func TestExtractArchive_WithDirectories(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_dirs_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestZipWithDirectories(zipPath); err != nil {
t.Fatal(err)
}
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify files were created (directories are created implicitly)
expectedPaths := []string{
filepath.Join(destDir, "dir1", "file1.txt"),
filepath.Join(destDir, "dir2", "subdir", "file2.txt"),
}
for _, path := range expectedPaths {
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected path %q not found", path)
}
}
}
func createTestZipWithDirectories(path string) error {
zipFile, err := os.Create(path)
if err != nil {
return err
}
defer zipFile.Close()
w := zip.NewWriter(zipFile)
// Create file in directory (directory will be created automatically)
f, err := w.Create("dir1/file1.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("content1"))
if err != nil {
return err
}
// Create nested directory structure
f, err = w.Create("dir2/subdir/file2.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("content2"))
if err != nil {
return err
}
return w.Close()
}
func TestExtractArchive_NilProgressCallback(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_nil_progress_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestZipWithMultipleFiles(zipPath, 3); err != nil {
t.Fatal(err)
}
// Should not panic with nil callback
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
}
func TestExtractArchive_GzipUppercase(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_gzip_upper_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a gzip compressed file with uppercase extension
gzPath := filepath.Join(tempDir, "test.txt.GZ")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestGzip(gzPath, "Hello from gzip!"); err != nil {
t.Fatal(err)
}
err = extractArchive(gzPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed for gzip with uppercase: %v", err)
}
// The base name should have the extension stripped
destPath := filepath.Join(destDir, "test.txt")
if _, err := os.Stat(destPath); os.IsNotExist(err) {
t.Error("expected decompressed file not found")
}
}
func TestExtractArchive_PathTraversalPrevention(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_traversal_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a zip with a path traversal attempt
zipPath := filepath.Join(tempDir, "malicious.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createMaliciousZip(zipPath); err != nil {
t.Fatal(err)
}
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify that the file was not created outside destDir
// The malicious path should be sanitized
dangerousPath := filepath.Join(tempDir, "evil.txt")
if _, err := os.Stat(dangerousPath); err == nil {
t.Error("path traversal attack succeeded - file created outside destDir")
}
}
func createMaliciousZip(path string) error {
zipFile, err := os.Create(path)
if err != nil {
return err
}
defer zipFile.Close()
w := zip.NewWriter(zipFile)
// Create a file with path traversal attempt
// Note: Go's zip library may sanitize this, but we test it anyway
f, err := w.Create("../evil.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("malicious content"))
if err != nil {
return err
}
// Also add a normal file
f, err = w.Create("safe.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("safe content"))
if err != nil {
return err
}
return w.Close()
}
func TestIsArchiveFile_AdditionalFormats(t *testing.T) {
// Test additional archive formats
tests := []struct {
filename string
expected bool
}{
// More compression formats
{"file.tar.lz4", true},
{"file.tlz4", true},
{"file.tar.sz", true},
{"file.tsz", true},
// Edge cases
{"file.tar.gz.backup", false},
{".gz", true},
{"archive.ZIP.bak", false},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
result := isArchiveFile(tt.filename)
if result != tt.expected {
t.Errorf("isArchiveFile(%q) = %v, expected %v", tt.filename, result, tt.expected)
}
})
}
}
func TestExtractArchive_DestDirCreation(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_destdir_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
// Use a nested path that doesn't exist
destDir := filepath.Join(tempDir, "level1", "level2", "extracted")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify destDir was created
if _, err := os.Stat(destDir); os.IsNotExist(err) {
t.Error("destDir was not created")
}
// Verify files were extracted
if _, err := os.Stat(filepath.Join(destDir, "test.txt")); os.IsNotExist(err) {
t.Error("file not extracted")
}
}
func TestExtractArchive_ProgressCallbackValues(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_progress_values_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
// Create zip with 10 files to test progress calculation
if err := createTestZipWithMultipleFiles(zipPath, 10); err != nil {
t.Fatal(err)
}
var progressValues []int
err = extractArchive(zipPath, destDir, "", func(extracted int, total int, progress int) {
progressValues = append(progressValues, progress)
})
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify progress increases monotonically
for i := 1; i < len(progressValues); i++ {
if progressValues[i] < progressValues[i-1] {
t.Errorf("progress decreased from %d to %d", progressValues[i-1], progressValues[i])
}
}
// Verify last progress is 100
if len(progressValues) > 0 && progressValues[len(progressValues)-1] != 100 {
t.Errorf("final progress should be 100, got %d", progressValues[len(progressValues)-1])
}
}
func TestCountArchiveFiles_WithDirectories(t *testing.T) {
tempDir, err := os.MkdirTemp("", "count_dirs_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
if err := createTestZipWithDirectories(zipPath); err != nil {
t.Fatal(err)
}
count, err := countArchiveFiles(zipPath, "")
if err != nil {
t.Fatalf("countArchiveFiles failed: %v", err)
}
// Should only count files, not directories
// createTestZipWithDirectories creates 2 files
if count != 2 {
t.Errorf("expected 2 files, got %d", count)
}
}
func TestOpenArchive_FileStatError(t *testing.T) {
// Test error path when file can't be stat'd
// This is hard to test without mocking, so we just ensure
// the function handles basic error cases
tempDir, err := os.MkdirTemp("", "open_archive_stat_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a valid zip first
zipPath := filepath.Join(tempDir, "test.zip")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
// Test normal opening
info, err := openArchive(zipPath, "")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer info.file.Close()
if info.stat == nil {
t.Error("stat should not be nil")
}
if info.format == nil {
t.Error("format should not be nil")
}
if info.input == nil {
t.Error("input should not be nil")
}
}
func TestExtractArchive_FilePermissions(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_perms_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Check that files are readable
content, err := os.ReadFile(filepath.Join(destDir, "test.txt"))
if err != nil {
t.Fatalf("couldn't read extracted file: %v", err)
}
if string(content) != "Hello, World!" {
t.Errorf("unexpected content: %q", string(content))
}
}
func TestExtractArchive_TarGz(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_targz_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
tarGzPath := filepath.Join(tempDir, "test.tar.gz")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestTarGz(tarGzPath); err != nil {
t.Fatal(err)
}
var progressCalls int
err = extractArchive(tarGzPath, destDir, "", func(extracted int, total int, progress int) {
progressCalls++
})
if err != nil {
t.Fatalf("extractArchive failed for tar.gz: %v", err)
}
// Verify the extracted file exists
destPath := filepath.Join(destDir, "test.txt")
content, err := os.ReadFile(destPath)
if err != nil {
t.Fatalf("couldn't read extracted file: %v", err)
}
if string(content) != "Hello from tar.gz!" {
t.Errorf("unexpected content: %q", string(content))
}
// Verify nested file exists
nestedPath := filepath.Join(destDir, "subdir", "nested.txt")
content, err = os.ReadFile(nestedPath)
if err != nil {
t.Fatalf("couldn't read nested file: %v", err)
}
if string(content) != "Nested content" {
t.Errorf("unexpected nested content: %q", string(content))
}
}
func createTestTarGz(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
gzWriter := gzip.NewWriter(file)
defer gzWriter.Close()
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
// Add a file
content := []byte("Hello from tar.gz!")
header := &tar.Header{
Name: "test.txt",
Mode: 0644,
Size: int64(len(content)),
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
if _, err := tarWriter.Write(content); err != nil {
return err
}
// Add a nested file
nestedContent := []byte("Nested content")
nestedHeader := &tar.Header{
Name: "subdir/nested.txt",
Mode: 0644,
Size: int64(len(nestedContent)),
}
if err := tarWriter.WriteHeader(nestedHeader); err != nil {
return err
}
if _, err := tarWriter.Write(nestedContent); err != nil {
return err
}
return nil
}
func TestCountArchiveFiles_TarGz(t *testing.T) {
tempDir, err := os.MkdirTemp("", "count_targz_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
tarGzPath := filepath.Join(tempDir, "test.tar.gz")
if err := createTestTarGz(tarGzPath); err != nil {
t.Fatal(err)
}
count, err := countArchiveFiles(tarGzPath, "")
if err != nil {
t.Fatalf("countArchiveFiles failed: %v", err)
}
// Should have 2 files
if count != 2 {
t.Errorf("expected 2 files, got %d", count)
}
}
func TestExtractArchive_LargeFileCount(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_large_count_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
// Create zip with many files to test progress capping at 100
numFiles := 100
if err := createTestZipWithMultipleFiles(zipPath, numFiles); err != nil {
t.Fatal(err)
}
var maxProgress int
err = extractArchive(zipPath, destDir, "", func(extracted int, total int, progress int) {
if progress > maxProgress {
maxProgress = progress
}
// Progress should never exceed 100
if progress > 100 {
t.Errorf("progress exceeded 100: %d", progress)
}
})
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Max progress should be 100
if maxProgress != 100 {
t.Errorf("expected max progress 100, got %d", maxProgress)
}
}
func TestExtractArchive_GzipNoExtension(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_gzip_noext_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a gzip with just .gz (no original extension)
gzPath := filepath.Join(tempDir, "compressed.gz")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestGzip(gzPath, "Compressed content"); err != nil {
t.Fatal(err)
}
err = extractArchive(gzPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Should create file named "compressed" (without .gz)
destPath := filepath.Join(destDir, "compressed")
if _, err := os.Stat(destPath); os.IsNotExist(err) {
t.Error("expected decompressed file not found")
}
}
func TestExtractArchive_ReadOnlyDestDir(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_readonly_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "readonly")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
// Create a read-only directory
if err := os.MkdirAll(destDir, 0555); err != nil {
t.Fatal(err)
}
// Make it writable for cleanup
defer os.Chmod(destDir, 0755)
// Extraction should fail due to read-only destination
err = extractArchive(zipPath, destDir, "", nil)
if err == nil {
t.Error("expected error when extracting to read-only directory")
}
}
func TestSupportedArchiveExtensions(t *testing.T) {
// Test that all listed extensions are recognized
for _, ext := range supportedArchiveExtensions {
filename := "test" + ext
if !isArchiveFile(filename) {
t.Errorf("extension %q should be recognized as archive", ext)
}
}
}
func TestExtractArchive_Tar(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_tar_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
tarPath := filepath.Join(tempDir, "test.tar")
destDir := filepath.Join(tempDir, "extracted")
if err := createTestTar(tarPath); err != nil {
t.Fatal(err)
}
err = extractArchive(tarPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed for tar: %v", err)
}
// Verify files were extracted
destPath := filepath.Join(destDir, "test.txt")
if _, err := os.Stat(destPath); os.IsNotExist(err) {
t.Error("expected file not found")
}
}
func createTestTar(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
tarWriter := tar.NewWriter(file)
defer tarWriter.Close()
content := []byte("Hello from tar!")
header := &tar.Header{
Name: "test.txt",
Mode: 0644,
Size: int64(len(content)),
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
if _, err := tarWriter.Write(content); err != nil {
return err
}
return nil
}
func TestCountArchiveFiles_Tar(t *testing.T) {
tempDir, err := os.MkdirTemp("", "count_tar_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
tarPath := filepath.Join(tempDir, "test.tar")
if err := createTestTar(tarPath); err != nil {
t.Fatal(err)
}
count, err := countArchiveFiles(tarPath, "")
if err != nil {
t.Fatalf("countArchiveFiles failed: %v", err)
}
if count != 1 {
t.Errorf("expected 1 file, got %d", count)
}
}
func TestOpenArchive_ValidArchive(t *testing.T) {
tempDir, err := os.MkdirTemp("", "open_valid_archive_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
info, err := openArchive(zipPath, "")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer info.file.Close()
// Verify all fields are set
if info.file == nil {
t.Error("file should not be nil")
}
if info.stat == nil {
t.Error("stat should not be nil")
}
if info.format == nil {
t.Error("format should not be nil")
}
if info.input == nil {
t.Error("input should not be nil")
}
if info.stat.Size() == 0 {
t.Error("file size should be > 0")
}
}
func TestExtractArchive_NestedDirectoryStructure(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_nested_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "nested.zip")
destDir := filepath.Join(tempDir, "extracted")
if err := createDeeplyNestedZip(zipPath); err != nil {
t.Fatal(err)
}
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify deeply nested file
deepPath := filepath.Join(destDir, "a", "b", "c", "d", "deep.txt")
if _, err := os.Stat(deepPath); os.IsNotExist(err) {
t.Error("deeply nested file not found")
}
}
func createDeeplyNestedZip(path string) error {
zipFile, err := os.Create(path)
if err != nil {
return err
}
defer zipFile.Close()
w := zip.NewWriter(zipFile)
f, err := w.Create("a/b/c/d/deep.txt")
if err != nil {
return err
}
_, err = f.Write([]byte("Deep content"))
if err != nil {
return err
}
return w.Close()
}
func TestExtractArchive_EmptyFileName(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_empty_name_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
// Create a normal zip - the archive library handles empty names gracefully
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
err = extractArchive(zipPath, destDir, "", nil)
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
}
func TestExtractArchive_ProgressTracking(t *testing.T) {
tempDir, err := os.MkdirTemp("", "extract_progress_track_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
destDir := filepath.Join(tempDir, "extracted")
numFiles := 5
if err := createTestZipWithMultipleFiles(zipPath, numFiles); err != nil {
t.Fatal(err)
}
var extractedValues []int
var totalValues []int
err = extractArchive(zipPath, destDir, "", func(extracted int, total int, progress int) {
extractedValues = append(extractedValues, extracted)
totalValues = append(totalValues, total)
})
if err != nil {
t.Fatalf("extractArchive failed: %v", err)
}
// Verify total is always the same
for _, total := range totalValues {
if total != numFiles {
t.Errorf("total should be %d, got %d", numFiles, total)
}
}
// Verify extracted increases sequentially
for i, extracted := range extractedValues {
if extracted != i+1 {
t.Errorf("extracted should be %d, got %d", i+1, extracted)
}
}
}
func TestArchiveInfo_Fields(t *testing.T) {
tempDir, err := os.MkdirTemp("", "archive_info_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
zipPath := filepath.Join(tempDir, "test.zip")
if err := createTestZip(zipPath); err != nil {
t.Fatal(err)
}
info, err := openArchive(zipPath, "")
if err != nil {
t.Fatalf("openArchive failed: %v", err)
}
defer info.file.Close()
// Verify file name
if info.file.Name() != zipPath {
t.Errorf("expected file name %q, got %q", zipPath, info.file.Name())
}
// Verify stat mode
if info.stat.Mode().IsDir() {
t.Error("expected file, not directory")
}
}