diff --git a/src/internal/file_operations_extract.go b/src/internal/file_operations_extract.go index 6cb75177..1349eda2 100644 --- a/src/internal/file_operations_extract.go +++ b/src/internal/file_operations_extract.go @@ -1,14 +1,7 @@ package internal import ( - "archive/zip" - "fmt" - "io" "log/slog" - "os" - "path/filepath" - "runtime" - "strings" "time" "github.com/charmbracelet/bubbles/progress" @@ -17,20 +10,6 @@ import ( "golift.io/xtractr" ) -func getDefaultFileMode() os.FileMode { - if runtime.GOOS == "windows" { - return 0666 - } - return 0644 -} - -func shouldSkipFile(name string) bool { - // Skip system files across platforms - return strings.HasPrefix(name, "__MACOSX/") || - strings.EqualFold(name, "Thumbs.db") || - strings.EqualFold(name, "desktop.ini") -} - func extractCompressFile(src, dest string) error { id := shortuuid.New() @@ -58,6 +37,8 @@ func extractCompressFile(src, dest string) error { x := &xtractr.XFile{ FilePath: src, OutputDir: dest, + FileMode: 0644, + DirMode: 0755, } _, _, _, err := xtractr.ExtractFile(x) @@ -83,135 +64,3 @@ func extractCompressFile(src, dest string) error { return nil } - -// Extract zip file -func unzip(src, dest string) error { - id := shortuuid.New() - r, err := zip.OpenReader(src) - if err != nil { - return fmt.Errorf("failed to open zip: %w", err) - } - defer func() { - if err := r.Close(); err != nil { - slog.Error("Error closing zip reader", "error", err) - } - }() - - totalFiles := len(r.File) - // progressbar - prog := progress.New(generateGradientColor()) - prog.PercentageStyle = footerStyle - // channel message - p := process{ - name: icon.ExtractFile + icon.Space + "unzip file", - progress: prog, - state: inOperation, - total: totalFiles, - done: 0, - doneTime: time.Time{}, - } - - message := channelMessage{ - messageId: id, - messageType: sendProcess, - processNewState: p, - } - - // Closure to address file descriptors issue with all the deferred .Close() methods - extractAndWriteFile := func(f *zip.File) error { - - rc, err := f.Open() - if err != nil { - return fmt.Errorf("failed to open file in zip: %w", err) - } - defer func() { - if err := rc.Close(); err != nil { - slog.Error("Error closing file reader", "error", err) - } - }() - - path := filepath.Join(dest, f.Name) - - // Cross-platform path security check - if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(dest)+string(os.PathSeparator)) { - return fmt.Errorf("illegal file path: %s", path) - } - - fileMode := f.Mode() - if f.FileInfo().IsDir() { - err := os.MkdirAll(path, fileMode) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - return nil - } - - // Create directory structure - if err := os.MkdirAll(filepath.Dir(path), fileMode); err != nil { - return fmt.Errorf("failed to create parent directory: %w", err) - } - - // Try default permissions first - outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, getDefaultFileMode()) - if err != nil { - // Fall back to original file permissions - outFile, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) - } - } - defer func() { - if err := outFile.Close(); err != nil { - slog.Error("Error closing output file", "path", path, "error", err) - } - }() - - if _, err := io.Copy(outFile, rc); err != nil { - return fmt.Errorf("failed to write file content: %w", err) - } - - return nil - } - - for _, f := range r.File { - p.name = icon.ExtractFile + icon.Space + f.Name - if len(channel) < 5 { - message.processNewState = p - channel <- message - } - - if shouldSkipFile(f.Name) { - p.done++ - continue - } - - err := extractAndWriteFile(f) - if err != nil { - p.state = failure - message.processNewState = p - channel <- message - slog.Error("Error extracting", "path", f.Name, "error", err) - p.done++ - continue - } - p.done++ - if len(channel) < 5 { - message.processNewState = p - channel <- message - } - } - - p.total = totalFiles - p.doneTime = time.Now() - if p.done == totalFiles { - p.state = successful - } else { - p.state = failure - } - message.processNewState = p - if len(channel) < 5 { - channel <- message - } - - return nil -} diff --git a/src/internal/handle_file_operations.go b/src/internal/handle_file_operations.go index 98f47800..2366898d 100644 --- a/src/internal/handle_file_operations.go +++ b/src/internal/handle_file_operations.go @@ -1,6 +1,8 @@ package internal import ( + "errors" + "fmt" "log/slog" "os" "os/exec" @@ -519,6 +521,11 @@ func (m *model) extractFile() { var err error panel := &m.fileModel.filePanels[m.filePanelFocusIndex] ext := strings.ToLower(filepath.Ext(panel.element[panel.cursor].location)) + if !isExensionExtractable(ext) { + slog.Error(fmt.Sprintf("Error unexpected file extension type: %s", ext), "error", errors.ErrUnsupported) + return + } + outputDir := fileNameWithoutExtension(panel.element[panel.cursor].location) outputDir, err = renameIfDuplicate(outputDir) if err != nil { @@ -531,19 +538,10 @@ func (m *model) extractFile() { slog.Error("Error while making directory for extracting files", "error", err) return } - switch ext { - case ".zip": - err = unzip(panel.element[panel.cursor].location, outputDir) - if err != nil { - slog.Error("Error extract file", "error", err) - return - } - default: - err = extractCompressFile(panel.element[panel.cursor].location, outputDir) - if err != nil { - slog.Error("Error extract file", "error", err) - return - } + err = extractCompressFile(panel.element[panel.cursor].location, outputDir) + if err != nil { + slog.Error("Error extract file", "error", err) + return } } diff --git a/src/internal/string_function.go b/src/internal/string_function.go index 50f7a93e..82c495ce 100644 --- a/src/internal/string_function.go +++ b/src/internal/string_function.go @@ -154,6 +154,24 @@ func isBufferPrintable(buffer []byte) bool { return true } +// isExensionExtractable checks if a string is a valid compressed archive file extension. +func isExensionExtractable(ext string) bool { + // Extensions based on the types that package: `xtractr` `ExtractFile` function handles. + validExtensions := map[string]struct{}{ + ".zip": {}, + ".bz": {}, + ".gz": {}, + ".iso": {}, + ".rar": {}, + ".7z": {}, + ".tar": {}, + ".tar.gz": {}, + ".tar.bz2": {}, + } + _, exists := validExtensions[strings.ToLower(ext)] + return exists +} + // Check file is text file or not func isTextFile(filename string) (bool, error) { file, err := os.Open(filename) diff --git a/src/internal/string_function_test.go b/src/internal/string_function_test.go index 2693979d..c4302f45 100644 --- a/src/internal/string_function_test.go +++ b/src/internal/string_function_test.go @@ -87,6 +87,37 @@ func TestIsBufferPrintable(t *testing.T) { } } +func TestIsExtensionExtractable(t *testing.T) { + inputs := []struct { + ext string + expected bool + }{ + {".zip", true}, + {".rar", true}, + {".7z", true}, + {".tar.gz", true}, + {".tar.bz2", true}, + {".exe", false}, + {".txt", false}, + {".tar", true}, + {"", false}, // Empty string case + {".ZIP", true}, // Case sensitivity check + {".Zip", true}, // Case sensitivity check + {".bz", true}, + {".gz", true}, + {".iso", true}, + } + + for _, tt := range inputs { + t.Run(tt.ext, func(t *testing.T) { + result := isExensionExtractable(tt.ext) + if result != tt.expected { + t.Errorf("isExensionExtractable (%q) = %v; want %v", tt.ext, result, tt.expected) + } + }) + } +} + func TestMakePrintable(t *testing.T) { var inputs = []struct { input string