mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
2494 lines
63 KiB
Go
2494 lines
63 KiB
Go
package download
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/text/encoding/simplifiedchinese"
|
|
)
|
|
|
|
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++ {
|
|
// Use Windows-safe names. The previous rune-based approach could generate
|
|
// invalid Windows filename characters (e.g. '\\', ':', '|') when numFiles is large.
|
|
nameInZip := fmt.Sprintf("dir/file%03d.txt", i+1)
|
|
f, err := w.Create(nameInZip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f.Write([]byte(fmt.Sprintf("Content of file %03d", i+1)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return w.Close()
|
|
}
|
|
|
|
// createTestZipWithChineseFilenames creates a test ZIP file with Chinese filenames encoded in GBK
|
|
func createTestZipWithChineseFilenames(path string) error {
|
|
zipFile, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer zipFile.Close()
|
|
|
|
w := zip.NewWriter(zipFile)
|
|
|
|
// Encode Chinese filenames in GBK (as some legacy Windows applications do)
|
|
encoder := simplifiedchinese.GBK.NewEncoder()
|
|
|
|
// Add a file with Chinese filename
|
|
chineseFilename := "测试文件.txt"
|
|
gbkFilename, err := encoder.String(chineseFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a FileHeader and manually set the Name with GBK encoding
|
|
// We need to mark it as non-UTF8 by not setting the UTF-8 flag
|
|
header := &zip.FileHeader{
|
|
Name: gbkFilename,
|
|
Method: zip.Deflate,
|
|
}
|
|
// Clear the UTF-8 bit (bit 11) to indicate non-UTF8 encoding
|
|
header.Flags = 0
|
|
|
|
f, err := w.CreateHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f.Write([]byte("这是测试内容"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add a file in a subdirectory with Chinese name
|
|
chineseDirAndFile := "文件夹/中文内容.txt"
|
|
gbkDirAndFile, err := encoder.String(chineseDirAndFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
header2 := &zip.FileHeader{
|
|
Name: gbkDirAndFile,
|
|
Method: zip.Deflate,
|
|
}
|
|
header2.Flags = 0
|
|
|
|
f2, err := w.CreateHeader(header2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f2.Write([]byte("中文子文件内容"))
|
|
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 func() { _ = os.Chmod(destDir, 0755) }()
|
|
|
|
// Windows ACL semantics mean chmod often does not prevent writes.
|
|
// If the filesystem doesn't enforce the permission change, skip.
|
|
probe := filepath.Join(destDir, "__write_probe.tmp")
|
|
if err := os.WriteFile(probe, []byte("x"), 0644); err == nil {
|
|
_ = os.Remove(probe)
|
|
t.Skip("filesystem does not enforce read-only directory permissions")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|
|
|
|
// Tests for multi-part archive detection
|
|
func TestIsMultiPartArchive(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
expected bool
|
|
}{
|
|
// 7z multi-part
|
|
{"archive.7z.001", true},
|
|
{"archive.7z.002", true},
|
|
{"archive.7z.100", true},
|
|
{"ARCHIVE.7Z.001", true},
|
|
|
|
// RAR new style
|
|
{"archive.part01.rar", true},
|
|
{"archive.part1.rar", true},
|
|
{"archive.part99.rar", true},
|
|
{"ARCHIVE.PART01.RAR", true},
|
|
|
|
// RAR old style (extension parts)
|
|
{"archive.r00", true},
|
|
{"archive.r01", true},
|
|
{"archive.r99", true},
|
|
|
|
// ZIP multi-part
|
|
{"archive.zip.001", true},
|
|
{"archive.zip.002", true},
|
|
|
|
// ZIP split
|
|
{"archive.z01", true},
|
|
{"archive.z02", true},
|
|
|
|
// Regular (non-multi-part) archives
|
|
{"archive.zip", false},
|
|
{"archive.rar", false},
|
|
{"archive.7z", false},
|
|
{"archive.tar.gz", false},
|
|
|
|
// Non-archive files
|
|
{"file.txt", false},
|
|
{"file.001", false}, // No .7z or .zip prefix
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
result := isMultiPartArchive(tt.filename)
|
|
if result != tt.expected {
|
|
t.Errorf("isMultiPartArchive(%q) = %v, expected %v", tt.filename, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetArchivePartInfo_7z(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
baseName string
|
|
partNumber int
|
|
isMultiPart bool
|
|
}{
|
|
{"archive.7z.001", "archive.7z", 1, true},
|
|
{"archive.7z.002", "archive.7z", 2, true},
|
|
{"archive.7z.010", "archive.7z", 10, true},
|
|
{"my.file.7z.005", "my.file.7z", 5, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if info.IsMultiPart != tt.isMultiPart {
|
|
t.Errorf("IsMultiPart: expected %v, got %v", tt.isMultiPart, info.IsMultiPart)
|
|
}
|
|
if info.BaseName != tt.baseName {
|
|
t.Errorf("BaseName: expected %q, got %q", tt.baseName, info.BaseName)
|
|
}
|
|
if info.PartNumber != tt.partNumber {
|
|
t.Errorf("PartNumber: expected %d, got %d", tt.partNumber, info.PartNumber)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetArchivePartInfo_RarNewStyle(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
baseName string
|
|
partNumber int
|
|
isMultiPart bool
|
|
}{
|
|
{"archive.part01.rar", "archive", 1, true},
|
|
{"archive.part02.rar", "archive", 2, true},
|
|
{"archive.part1.rar", "archive", 1, true},
|
|
{"my.archive.part10.rar", "my.archive", 10, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if info.IsMultiPart != tt.isMultiPart {
|
|
t.Errorf("IsMultiPart: expected %v, got %v", tt.isMultiPart, info.IsMultiPart)
|
|
}
|
|
if info.BaseName != tt.baseName {
|
|
t.Errorf("BaseName: expected %q, got %q", tt.baseName, info.BaseName)
|
|
}
|
|
if info.PartNumber != tt.partNumber {
|
|
t.Errorf("PartNumber: expected %d, got %d", tt.partNumber, info.PartNumber)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetArchivePartInfo_RarOldStyle(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
baseName string
|
|
partNumber int
|
|
isMultiPart bool
|
|
}{
|
|
{"archive.r00", "archive", 1, true}, // r00 is treated as first extension part (after .rar)
|
|
{"archive.r01", "archive", 1, true},
|
|
{"archive.r99", "archive", 99, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if info.IsMultiPart != tt.isMultiPart {
|
|
t.Errorf("IsMultiPart: expected %v, got %v", tt.isMultiPart, info.IsMultiPart)
|
|
}
|
|
if info.BaseName != tt.baseName {
|
|
t.Errorf("BaseName: expected %q, got %q", tt.baseName, info.BaseName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetArchivePartInfo_ZipMultiPart(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
baseName string
|
|
partNumber int
|
|
isMultiPart bool
|
|
}{
|
|
{"archive.zip.001", "archive.zip", 1, true},
|
|
{"archive.zip.002", "archive.zip", 2, true},
|
|
{"my.file.zip.010", "my.file.zip", 10, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if info.IsMultiPart != tt.isMultiPart {
|
|
t.Errorf("IsMultiPart: expected %v, got %v", tt.isMultiPart, info.IsMultiPart)
|
|
}
|
|
if info.BaseName != tt.baseName {
|
|
t.Errorf("BaseName: expected %q, got %q", tt.baseName, info.BaseName)
|
|
}
|
|
if info.PartNumber != tt.partNumber {
|
|
t.Errorf("PartNumber: expected %d, got %d", tt.partNumber, info.PartNumber)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetArchivePartInfo_ZipSplit(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
baseName string
|
|
partNumber int
|
|
isMultiPart bool
|
|
}{
|
|
{"archive.z01", "archive", 1, true},
|
|
{"archive.z02", "archive", 2, true},
|
|
{"archive.z99", "archive", 99, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if info.IsMultiPart != tt.isMultiPart {
|
|
t.Errorf("IsMultiPart: expected %v, got %v", tt.isMultiPart, info.IsMultiPart)
|
|
}
|
|
if info.BaseName != tt.baseName {
|
|
t.Errorf("BaseName: expected %q, got %q", tt.baseName, info.BaseName)
|
|
}
|
|
if info.PartNumber != tt.partNumber {
|
|
t.Errorf("PartNumber: expected %d, got %d", tt.partNumber, info.PartNumber)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetArchivePartInfo_NonMultiPart(t *testing.T) {
|
|
tests := []string{
|
|
"archive.zip",
|
|
"archive.rar",
|
|
"archive.7z",
|
|
"file.txt",
|
|
"file.001",
|
|
}
|
|
|
|
for _, filename := range tests {
|
|
t.Run(filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(filename)
|
|
if info.IsMultiPart {
|
|
t.Errorf("Expected non-multi-part for %q, but got IsMultiPart=true", filename)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsFirstPart(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
expected bool
|
|
}{
|
|
// First parts
|
|
{"archive.7z.001", true},
|
|
{"archive.part01.rar", true},
|
|
{"archive.part1.rar", true},
|
|
{"archive.zip.001", true},
|
|
{"archive.z01", true},
|
|
|
|
// Non-first parts
|
|
{"archive.7z.002", false},
|
|
{"archive.part02.rar", false},
|
|
{"archive.zip.002", false},
|
|
{"archive.z02", false},
|
|
|
|
// For RAR old style (.r00, .r01), these are NOT the first part
|
|
// The first part is the .rar file, but these extension files
|
|
// have partNumber=1 due to parsePartNumber treating 00 as 1
|
|
// So isFirstPart returns true for these (which is technically correct
|
|
// in terms of part numbering, even though .rar is the "real" first file)
|
|
{"archive.r00", true},
|
|
{"archive.r01", true},
|
|
|
|
// Non-multi-part (should return false)
|
|
{"archive.zip", false},
|
|
{"archive.rar", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
result := isFirstPart(tt.filename)
|
|
if result != tt.expected {
|
|
t.Errorf("isFirstPart(%q) = %v, expected %v", tt.filename, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetMultiPartArchiveBaseName(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
expected string
|
|
}{
|
|
{"/path/to/archive.7z.001", "/path/to/archive.7z"},
|
|
{"/path/to/archive.part01.rar", "/path/to/archive"},
|
|
{"/path/to/archive.zip.001", "/path/to/archive.zip"},
|
|
{"/path/to/archive.z01", "/path/to/archive"},
|
|
// Non-multi-part should return empty
|
|
{"/path/to/archive.zip", ""},
|
|
{"/path/to/file.txt", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
filename := filepath.FromSlash(tt.filename)
|
|
expected := tt.expected
|
|
if expected != "" {
|
|
expected = filepath.FromSlash(expected)
|
|
}
|
|
result := GetMultiPartArchiveBaseName(filename)
|
|
if result != expected {
|
|
t.Errorf("GetMultiPartArchiveBaseName(%q) = %q, expected %q", filename, result, expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsArchiveFile_IncludesMultiPart(t *testing.T) {
|
|
// Test that isArchiveFile returns true for multi-part archives
|
|
tests := []struct {
|
|
filename string
|
|
expected bool
|
|
}{
|
|
{"archive.7z.001", true},
|
|
{"archive.7z.002", true},
|
|
{"archive.part01.rar", true},
|
|
{"archive.zip.001", true},
|
|
{"archive.z01", true},
|
|
{"archive.r00", true},
|
|
}
|
|
|
|
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 TestArchivePartInfo_PatternField(t *testing.T) {
|
|
// Verify that the Pattern field is set correctly for different formats
|
|
tests := []struct {
|
|
filename string
|
|
patternContains string
|
|
}{
|
|
{"archive.7z.001", ".7z)"},
|
|
{"archive.part01.rar", ".part"},
|
|
{"archive.r00", ".r("},
|
|
{"archive.zip.001", ".zip)"},
|
|
{"archive.z01", ".z("},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if !info.IsMultiPart {
|
|
t.Fatalf("Expected multi-part archive for %q", tt.filename)
|
|
}
|
|
if info.Pattern == "" {
|
|
t.Errorf("Pattern should not be empty for %q", tt.filename)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestArchivePartInfo_FirstPartPath(t *testing.T) {
|
|
// Verify that FirstPartPath is set correctly for different formats
|
|
tests := []struct {
|
|
filename string
|
|
expectedSuffix string // Expected suffix of the FirstPartPath
|
|
}{
|
|
{"archive.7z.001", "archive.7z.001"},
|
|
{"archive.7z.002", "archive.7z.001"},
|
|
{"archive.7z.005", "archive.7z.001"},
|
|
{"archive.part01.rar", "archive.part01.rar"},
|
|
{"archive.part02.rar", "archive.part01.rar"},
|
|
{"archive.zip.001", "archive.zip.001"},
|
|
{"archive.zip.003", "archive.zip.001"},
|
|
{"archive.z01", "archive.z01"},
|
|
{"archive.z05", "archive.z01"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
info := getArchivePartInfo(tt.filename)
|
|
if !info.IsMultiPart {
|
|
t.Fatalf("Expected multi-part archive for %q", tt.filename)
|
|
}
|
|
if info.FirstPartPath == "" {
|
|
t.Errorf("FirstPartPath should not be empty for %q", tt.filename)
|
|
}
|
|
if !strings.HasSuffix(info.FirstPartPath, tt.expectedSuffix) {
|
|
t.Errorf("FirstPartPath for %q = %q, expected suffix %q", tt.filename, info.FirstPartPath, tt.expectedSuffix)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Tests for multiPartFileReader
|
|
func TestMultiPartFileReader_Basic(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "multipart_reader_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create test parts with known content
|
|
part1Content := []byte("Hello")
|
|
part2Content := []byte("World")
|
|
part3Content := []byte("!")
|
|
|
|
part1Path := filepath.Join(tempDir, "test.001")
|
|
part2Path := filepath.Join(tempDir, "test.002")
|
|
part3Path := filepath.Join(tempDir, "test.003")
|
|
|
|
if err := os.WriteFile(part1Path, part1Content, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(part2Path, part2Content, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(part3Path, part3Content, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader := newMultiPartFileReader([]string{part1Path, part2Path, part3Path})
|
|
defer reader.Close()
|
|
|
|
// Test Size()
|
|
expectedSize := int64(len(part1Content) + len(part2Content) + len(part3Content))
|
|
if reader.Size() != expectedSize {
|
|
t.Errorf("Size() = %d, expected %d", reader.Size(), expectedSize)
|
|
}
|
|
}
|
|
|
|
func TestMultiPartFileReader_ReadAt(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "multipart_readat_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create test parts with known content
|
|
part1Path := filepath.Join(tempDir, "test.001")
|
|
part2Path := filepath.Join(tempDir, "test.002")
|
|
|
|
if err := os.WriteFile(part1Path, []byte("AAAA"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(part2Path, []byte("BBBB"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader := newMultiPartFileReader([]string{part1Path, part2Path})
|
|
defer reader.Close()
|
|
|
|
// Test reading from first part
|
|
buf := make([]byte, 2)
|
|
n, err := reader.ReadAt(buf, 0)
|
|
if err != nil {
|
|
t.Errorf("ReadAt(0) error: %v", err)
|
|
}
|
|
if n != 2 || string(buf) != "AA" {
|
|
t.Errorf("ReadAt(0) = %q, expected %q", string(buf[:n]), "AA")
|
|
}
|
|
|
|
// Test reading across parts
|
|
buf = make([]byte, 4)
|
|
n, err = reader.ReadAt(buf, 2)
|
|
if err != nil {
|
|
t.Errorf("ReadAt(2) error: %v", err)
|
|
}
|
|
if n != 4 || string(buf) != "AABB" {
|
|
t.Errorf("ReadAt(2) = %q, expected %q", string(buf[:n]), "AABB")
|
|
}
|
|
|
|
// Test reading from second part only
|
|
buf = make([]byte, 2)
|
|
n, err = reader.ReadAt(buf, 6)
|
|
if err != nil {
|
|
t.Errorf("ReadAt(6) error: %v", err)
|
|
}
|
|
if n != 2 || string(buf) != "BB" {
|
|
t.Errorf("ReadAt(6) = %q, expected %q", string(buf[:n]), "BB")
|
|
}
|
|
}
|
|
|
|
func TestMultiPartFileReader_ReadAtEOF(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "multipart_eof_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
part1Path := filepath.Join(tempDir, "test.001")
|
|
if err := os.WriteFile(part1Path, []byte("ABC"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader := newMultiPartFileReader([]string{part1Path})
|
|
defer reader.Close()
|
|
|
|
// Reading beyond EOF should return io.EOF
|
|
buf := make([]byte, 10)
|
|
n, err := reader.ReadAt(buf, 100)
|
|
if err != io.EOF {
|
|
t.Errorf("ReadAt beyond EOF: expected io.EOF, got %v", err)
|
|
}
|
|
if n != 0 {
|
|
t.Errorf("ReadAt beyond EOF: expected 0 bytes, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestMultiPartFileReader_Close(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "multipart_close_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
part1Path := filepath.Join(tempDir, "test.001")
|
|
if err := os.WriteFile(part1Path, []byte("test"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reader := newMultiPartFileReader([]string{part1Path})
|
|
|
|
// Initialize by calling Size
|
|
_ = reader.Size()
|
|
|
|
// Close should work
|
|
err = reader.Close()
|
|
if err != nil {
|
|
t.Errorf("Close() error: %v", err)
|
|
}
|
|
|
|
// After close, files should be nil
|
|
if reader.files != nil {
|
|
t.Error("files should be nil after Close()")
|
|
}
|
|
}
|
|
|
|
func TestMultiPartFileReader_InitError(t *testing.T) {
|
|
// Test with non-existent file
|
|
reader := newMultiPartFileReader([]string{"/nonexistent/path/file.001"})
|
|
defer reader.Close()
|
|
|
|
// Size should return 0 on error
|
|
size := reader.Size()
|
|
if size != 0 {
|
|
t.Errorf("Size() with non-existent file should return 0, got %d", size)
|
|
}
|
|
|
|
// ReadAt should return error
|
|
buf := make([]byte, 10)
|
|
_, err := reader.ReadAt(buf, 0)
|
|
if err == nil {
|
|
t.Error("ReadAt with non-existent file should return error")
|
|
}
|
|
}
|
|
|
|
// Tests for findZipMultiParts
|
|
func TestFindZipMultiParts(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "find_zip_parts_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create test parts
|
|
for i := 1; i <= 5; i++ {
|
|
partPath := filepath.Join(tempDir, fmt.Sprintf("archive.zip.%03d", i))
|
|
if err := os.WriteFile(partPath, []byte("test"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
firstPartPath := filepath.Join(tempDir, "archive.zip.001")
|
|
parts, err := findZipMultiParts(firstPartPath)
|
|
if err != nil {
|
|
t.Fatalf("findZipMultiParts error: %v", err)
|
|
}
|
|
|
|
if len(parts) != 5 {
|
|
t.Errorf("Expected 5 parts, got %d", len(parts))
|
|
}
|
|
|
|
// Verify order
|
|
for i, part := range parts {
|
|
expected := filepath.Join(tempDir, fmt.Sprintf("archive.zip.%03d", i+1))
|
|
if part != expected {
|
|
t.Errorf("Part %d: expected %q, got %q", i, expected, part)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindZipMultiParts_NoParts(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "find_zip_no_parts_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Don't create any parts
|
|
firstPartPath := filepath.Join(tempDir, "archive.zip.001")
|
|
_, err = findZipMultiParts(firstPartPath)
|
|
if err == nil {
|
|
t.Error("Expected error when no parts found")
|
|
}
|
|
}
|
|
|
|
func TestFindZipMultiParts_SinglePart(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "find_zip_single_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create only the first part
|
|
firstPartPath := filepath.Join(tempDir, "archive.zip.001")
|
|
if err := os.WriteFile(firstPartPath, []byte("test"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
parts, err := findZipMultiParts(firstPartPath)
|
|
if err != nil {
|
|
t.Fatalf("findZipMultiParts error: %v", err)
|
|
}
|
|
|
|
if len(parts) != 1 {
|
|
t.Errorf("Expected 1 part, got %d", len(parts))
|
|
}
|
|
}
|
|
|
|
// Tests for extractMultiPartArchive error paths
|
|
func TestExtractMultiPartArchive_NonExistentFile(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_multipart_err_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
|
|
// Test with non-existent 7z multi-part
|
|
err = extractMultiPartArchive("/nonexistent/archive.7z.001", destDir, "", nil)
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent 7z file")
|
|
}
|
|
|
|
// Test with non-existent zip multi-part
|
|
err = extractMultiPartArchive("/nonexistent/archive.zip.001", destDir, "", nil)
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent zip file")
|
|
}
|
|
}
|
|
|
|
// Test extractZipMultiPart with invalid archive
|
|
func TestExtractZipMultiPart_InvalidArchive(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_zip_invalid_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create invalid "zip" parts (just text files)
|
|
part1Path := filepath.Join(tempDir, "invalid.zip.001")
|
|
if err := os.WriteFile(part1Path, []byte("not a zip file"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
err = extractZipMultiPart(part1Path, destDir, "", nil)
|
|
// Should return an error because the file is not a valid zip
|
|
if err == nil {
|
|
t.Error("Expected error for invalid zip archive")
|
|
}
|
|
}
|
|
|
|
// Test extractRarMultiPart with non-existent file
|
|
func TestExtractRarMultiPart_NonExistent(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_rar_err_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
err = extractRarMultiPart("/nonexistent/archive.part01.rar", destDir, "", nil)
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent RAR file")
|
|
}
|
|
}
|
|
|
|
// Test extractSevenZipMultiPart with non-existent file
|
|
func TestExtractSevenZipMultiPart_NonExistent(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_7z_err_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
err = extractSevenZipMultiPart("/nonexistent/archive.7z.001", destDir, "", nil)
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent 7z file")
|
|
}
|
|
}
|
|
|
|
// Test extractSevenZipMultiPart with invalid archive
|
|
func TestExtractSevenZipMultiPart_Invalid(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_7z_invalid_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create an invalid 7z file
|
|
part1Path := filepath.Join(tempDir, "invalid.7z.001")
|
|
if err := os.WriteFile(part1Path, []byte("not a 7z file"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
err = extractSevenZipMultiPart(part1Path, destDir, "", nil)
|
|
if err == nil {
|
|
t.Error("Expected error for invalid 7z archive")
|
|
}
|
|
}
|
|
|
|
// Test determineFirstPartPath with all patterns
|
|
func TestDetermineFirstPartPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dir string
|
|
baseName string
|
|
pattern string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "7z pattern",
|
|
dir: "/path/to",
|
|
baseName: "archive.7z",
|
|
pattern: `(?i)^(.+\.7z)\.(\d{3})$`,
|
|
expected: "/path/to/archive.7z.001",
|
|
},
|
|
{
|
|
name: "RAR new style pattern",
|
|
dir: "/path/to",
|
|
baseName: "archive",
|
|
pattern: `(?i)^(.+)\.part(\d+)\.rar$`,
|
|
expected: "/path/to/archive.part01.rar",
|
|
},
|
|
{
|
|
name: "RAR old style pattern",
|
|
dir: "/path/to",
|
|
baseName: "archive",
|
|
pattern: `(?i)^(.+)\.r(\d{2})$`,
|
|
expected: "/path/to/archive.rar",
|
|
},
|
|
{
|
|
name: "ZIP multi-part pattern",
|
|
dir: "/path/to",
|
|
baseName: "archive.zip",
|
|
pattern: `(?i)^(.+\.zip)\.(\d{3})$`,
|
|
expected: "/path/to/archive.zip.001",
|
|
},
|
|
{
|
|
name: "ZIP split pattern",
|
|
dir: "/path/to",
|
|
baseName: "archive",
|
|
pattern: `(?i)^(.+)\.z(\d{2})$`,
|
|
expected: "/path/to/archive.z01",
|
|
},
|
|
{
|
|
name: "Unknown pattern",
|
|
dir: "/path/to",
|
|
baseName: "archive",
|
|
pattern: "unknown",
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := filepath.FromSlash(tt.dir)
|
|
expected := tt.expected
|
|
if expected != "" {
|
|
expected = filepath.FromSlash(expected)
|
|
}
|
|
|
|
result := determineFirstPartPath(dir, tt.baseName, tt.pattern)
|
|
if result != expected {
|
|
t.Errorf("determineFirstPartPath(%q, %q, %q) = %q, expected %q",
|
|
dir, tt.baseName, tt.pattern, result, expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test parsePartNumber
|
|
func TestParsePartNumber(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected int
|
|
}{
|
|
{"001", 1},
|
|
{"01", 1},
|
|
{"1", 1},
|
|
{"00", 1}, // 00 is treated as 1
|
|
{"10", 10},
|
|
{"99", 99},
|
|
{"100", 100},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
var result int
|
|
_, err := parsePartNumber(tt.input, &result)
|
|
if err != nil {
|
|
t.Errorf("parsePartNumber(%q) error: %v", tt.input, err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("parsePartNumber(%q) = %d, expected %d", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test multiPartArchivePatterns directly
|
|
func TestMultiPartArchivePatterns(t *testing.T) {
|
|
// Verify patterns are valid and match expected formats
|
|
testCases := []struct {
|
|
filename string
|
|
matches bool
|
|
}{
|
|
// 7z
|
|
{"archive.7z.001", true},
|
|
{"archive.7z.999", true},
|
|
{"Archive.7Z.001", true},
|
|
{"archive.7z.01", false}, // Only 3 digits
|
|
{"archive.7z.0001", false}, // 4 digits not matched
|
|
|
|
// RAR new style
|
|
{"archive.part01.rar", true},
|
|
{"archive.part1.rar", true},
|
|
{"archive.part999.rar", true},
|
|
{"archive.PART01.RAR", true},
|
|
|
|
// RAR old style
|
|
{"archive.r00", true},
|
|
{"archive.r01", true},
|
|
{"archive.r99", true},
|
|
{"archive.R00", true},
|
|
|
|
// ZIP multi-part
|
|
{"archive.zip.001", true},
|
|
{"archive.zip.999", true},
|
|
{"Archive.ZIP.001", true},
|
|
|
|
// ZIP split
|
|
{"archive.z01", true},
|
|
{"archive.z99", true},
|
|
{"Archive.Z01", true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.filename, func(t *testing.T) {
|
|
matched := false
|
|
for _, pattern := range multiPartArchivePatterns {
|
|
if pattern.MatchString(tc.filename) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if matched != tc.matches {
|
|
t.Errorf("Pattern match for %q: got %v, expected %v", tc.filename, matched, tc.matches)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test extractRarMultiPart - test that destDir creation works
|
|
func TestExtractRarMultiPart_DestDirCreation(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_rar_destdir_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create a simple RAR-like file (will fail extraction but destDir should be created)
|
|
rarPath := filepath.Join(tempDir, "test.part01.rar")
|
|
if err := os.WriteFile(rarPath, []byte("Rar!\x1a\x07\x00"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
destDir := filepath.Join(tempDir, "level1", "level2", "extracted")
|
|
// We expect an error since the file is not a complete valid RAR
|
|
_ = extractRarMultiPart(rarPath, destDir, "", nil)
|
|
|
|
// The destDir should have been created before the extraction error
|
|
if _, err := os.Stat(destDir); os.IsNotExist(err) {
|
|
t.Log("Note: destDir was not created, extraction failed early")
|
|
}
|
|
}
|
|
|
|
// Test the old-style RAR detection with .rar + .r00 files
|
|
func TestGetArchivePartInfo_RarOldStyleWithRar(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "rar_old_style_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create a .rar file and .r00 file to simulate old-style multi-part
|
|
rarPath := filepath.Join(tempDir, "archive.rar")
|
|
r00Path := filepath.Join(tempDir, "archive.r00")
|
|
|
|
if err := os.WriteFile(rarPath, []byte("fake rar"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(r00Path, []byte("fake r00"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test that .rar file is detected as first part of old-style multi-part
|
|
info := getArchivePartInfo(rarPath)
|
|
if !info.IsMultiPart {
|
|
t.Error("Expected .rar with .r00 to be detected as multi-part")
|
|
}
|
|
if info.Pattern != "rar-old-style" {
|
|
t.Errorf("Expected pattern 'rar-old-style', got %q", info.Pattern)
|
|
}
|
|
if info.FirstPartPath != rarPath {
|
|
t.Errorf("Expected FirstPartPath to be %q, got %q", rarPath, info.FirstPartPath)
|
|
}
|
|
}
|
|
|
|
// Test extractSevenZipFile function indirectly through mock
|
|
func TestExtractSevenZipMultiPart_DestDirCreation(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "7z_destdir_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create an invalid 7z file - the test is for error handling, not extraction
|
|
part1Path := filepath.Join(tempDir, "test.7z.001")
|
|
if err := os.WriteFile(part1Path, []byte("invalid"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Use a nested dest directory that doesn't exist
|
|
destDir := filepath.Join(tempDir, "level1", "level2", "extracted")
|
|
err = extractSevenZipMultiPart(part1Path, destDir, "", nil)
|
|
// Error is expected because the file is invalid, but the dest directory should be created
|
|
// Actually, error happens before directory creation in this case
|
|
if err == nil {
|
|
t.Error("Expected error for invalid 7z")
|
|
}
|
|
}
|
|
|
|
// Test multiPartFileReader with multiple files spanning reads
|
|
func TestMultiPartFileReader_SpanningRead(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "multipart_spanning_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create 3 parts with known sizes
|
|
parts := []string{
|
|
filepath.Join(tempDir, "test.001"),
|
|
filepath.Join(tempDir, "test.002"),
|
|
filepath.Join(tempDir, "test.003"),
|
|
}
|
|
|
|
contents := []string{"12345", "67890", "ABCDE"}
|
|
for i, content := range contents {
|
|
if err := os.WriteFile(parts[i], []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
reader := newMultiPartFileReader(parts)
|
|
defer reader.Close()
|
|
|
|
// Read all content at once
|
|
buf := make([]byte, 15)
|
|
n, err := reader.ReadAt(buf, 0)
|
|
if err != nil {
|
|
t.Errorf("ReadAt error: %v", err)
|
|
}
|
|
if n != 15 {
|
|
t.Errorf("Expected to read 15 bytes, got %d", n)
|
|
}
|
|
if string(buf) != "1234567890ABCDE" {
|
|
t.Errorf("Expected '1234567890ABCDE', got %q", string(buf))
|
|
}
|
|
}
|
|
|
|
// Test extractZipMultiPart with destDir creation
|
|
func TestExtractZipMultiPart_DestDirCreation(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "zip_multipart_destdir_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create a valid single-part "multi-part" zip (just one .001 file with valid zip content)
|
|
// First create a valid zip
|
|
zipPath := filepath.Join(tempDir, "temp.zip")
|
|
if err := createTestZip(zipPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read the zip content and write as .001
|
|
zipContent, err := os.ReadFile(zipPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
part1Path := filepath.Join(tempDir, "archive.zip.001")
|
|
if err := os.WriteFile(part1Path, zipContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Extract to nested directory
|
|
destDir := filepath.Join(tempDir, "level1", "level2", "extracted")
|
|
err = extractZipMultiPart(part1Path, destDir, "", nil)
|
|
if err != nil {
|
|
t.Fatalf("extractZipMultiPart error: %v", err)
|
|
}
|
|
|
|
// Verify destDir was created and files extracted
|
|
if _, err := os.Stat(destDir); os.IsNotExist(err) {
|
|
t.Error("destDir was not created")
|
|
}
|
|
|
|
// Verify extracted file
|
|
extractedFile := filepath.Join(destDir, "test.txt")
|
|
if _, err := os.Stat(extractedFile); os.IsNotExist(err) {
|
|
t.Error("Expected file not found after extraction")
|
|
}
|
|
}
|
|
|
|
// Test extractZipMultiPart with progress callback
|
|
func TestExtractZipMultiPart_Progress(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "zip_multipart_progress_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create a valid zip with multiple files
|
|
zipPath := filepath.Join(tempDir, "temp.zip")
|
|
if err := createTestZipWithMultipleFiles(zipPath, 4); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
zipContent, err := os.ReadFile(zipPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
part1Path := filepath.Join(tempDir, "archive.zip.001")
|
|
if err := os.WriteFile(part1Path, zipContent, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
var progressCalls int
|
|
err = extractZipMultiPart(part1Path, destDir, "", func(extracted int, total int, progress int) {
|
|
progressCalls++
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("extractZipMultiPart error: %v", err)
|
|
}
|
|
|
|
// Should have progress calls
|
|
if progressCalls == 0 {
|
|
t.Error("Expected progress callbacks")
|
|
}
|
|
}
|
|
|
|
// Test extracting ZIP files with Chinese filenames encoded in GBK/GB18030
|
|
func TestExtractArchive_ChineseFilenames(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "extract_chinese_test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create a test ZIP file with Chinese filenames encoded in GBK
|
|
zipPath := filepath.Join(tempDir, "chinese.zip")
|
|
destDir := filepath.Join(tempDir, "extracted")
|
|
|
|
if err := createTestZipWithChineseFilenames(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 with proper Chinese filenames
|
|
expectedFiles := []string{
|
|
filepath.Join(destDir, "测试文件.txt"),
|
|
filepath.Join(destDir, "文件夹", "中文内容.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 of the Chinese file
|
|
content, err := os.ReadFile(filepath.Join(destDir, "测试文件.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(content) != "这是测试内容" {
|
|
t.Errorf("unexpected content: %q", string(content))
|
|
}
|
|
}
|