mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 10:00:27 +00:00
1321 lines
32 KiB
Go
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")
|
|
}
|
|
}
|