feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tarExtractor struct{ *BaseExtractor }
|
||||
|
||||
func NewTarExtractor() *tarExtractor {
|
||||
return &tarExtractor{NewBaseExtractor("tar", []string{".tar"})}
|
||||
}
|
||||
|
||||
// Extract extracts a tar archive into destDir (streaming-friendly)
|
||||
func (t *tarExtractor) Extract(r io.Reader, destDir string) error {
|
||||
tr := tar.NewReader(r)
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read tar header: %w", err)
|
||||
}
|
||||
|
||||
if err := t.extractEntry(header, tr, destDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractEntry extracts a single entry from a tar archive
|
||||
func (t *tarExtractor) extractEntry(header *tar.Header, r io.Reader, destDir string) error {
|
||||
// Sanitize path to prevent tar-slip attacks
|
||||
cleanName := filepath.Clean(header.Name)
|
||||
if strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
|
||||
return fmt.Errorf("unsafe path in tar: %s", header.Name)
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(destDir, cleanName)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
// Create directory with original permissions
|
||||
return os.MkdirAll(targetPath, os.FileMode(header.Mode))
|
||||
|
||||
case tar.TypeReg:
|
||||
// Create parent directories
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create file with original permissions (mask setuid/setgid/sticky)
|
||||
mode := os.FileMode(header.Mode) & 0777
|
||||
file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy contents
|
||||
if _, err := io.Copy(file, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore timestamps
|
||||
atime := header.AccessTime
|
||||
if atime.IsZero() {
|
||||
atime = time.Now()
|
||||
}
|
||||
mtime := header.ModTime
|
||||
if mtime.IsZero() {
|
||||
mtime = time.Now()
|
||||
}
|
||||
if err := os.Chtimes(targetPath, atime, mtime); err != nil {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case tar.TypeSymlink:
|
||||
// Validate symlink target against path traversal
|
||||
linkTarget := filepath.Clean(header.Linkname)
|
||||
if filepath.IsAbs(linkTarget) || strings.HasPrefix(linkTarget, "..") {
|
||||
return fmt.Errorf("symlink target outside extraction directory: %s", header.Linkname)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Remove(targetPath)
|
||||
return os.Symlink(linkTarget, targetPath)
|
||||
|
||||
case tar.TypeLink:
|
||||
// Hard link - validate target stays within destDir to prevent path traversal
|
||||
cleanTarget := filepath.Clean(header.Linkname)
|
||||
if filepath.IsAbs(cleanTarget) || strings.HasPrefix(cleanTarget, "..") {
|
||||
return fmt.Errorf("hard link target outside extraction directory: %s", header.Linkname)
|
||||
}
|
||||
linkTarget := filepath.Join(destDir, cleanTarget)
|
||||
// Ensure resolved path does not escape destDir
|
||||
if !strings.HasPrefix(linkTarget, destDir+string(filepath.Separator)) && linkTarget != destDir {
|
||||
return fmt.Errorf("hard link target outside extraction directory: %s", header.Linkname)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Link(linkTarget, targetPath)
|
||||
|
||||
case tar.TypeChar, tar.TypeBlock:
|
||||
// Device files - skip with warning (requires root)
|
||||
return nil
|
||||
|
||||
case tar.TypeFifo:
|
||||
// FIFO - skip (platform-specific)
|
||||
return nil
|
||||
|
||||
default:
|
||||
// Unknown type - skip
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() { Register(NewTarExtractor()) }
|
||||
Reference in New Issue
Block a user