feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type ExtractConfig struct {
|
||||
DestDir string
|
||||
StripComponents int
|
||||
IncludePatterns []string
|
||||
ExcludePatterns []string
|
||||
PreservePermissions bool
|
||||
Overwrite bool
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func DefaultExtractConfig() *ExtractConfig {
|
||||
return &ExtractConfig{
|
||||
DestDir: ".", StripComponents: 0, IncludePatterns: []string{},
|
||||
ExcludePatterns: []string{}, PreservePermissions: true, Overwrite: true, Verbose: false,
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractFile(srcPath, destDir string, cfg *ExtractConfig) error {
|
||||
if cfg == nil {
|
||||
cfg = DefaultExtractConfig()
|
||||
}
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
return AutoExtract(filepath.Base(srcPath), srcFile, destDir)
|
||||
}
|
||||
|
||||
func ShouldExtractFile(filename string) bool { return IsArchiveFormat(filename) }
|
||||
func ListSupportedFormats() []string { return ListSupported() }
|
||||
func SupportsFormat(format string) bool { return Supports(format) }
|
||||
@@ -0,0 +1,80 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzTarExtract tests tar extraction with fuzzed input to catch path traversal
|
||||
// and other security issues in header parsing.
|
||||
func FuzzTarExtract(f *testing.F) {
|
||||
// Seed corpus with valid tar data
|
||||
f.Add([]byte{})
|
||||
|
||||
// Minimal valid tar header (512 bytes of zeros)
|
||||
emptyHeader := make([]byte, 512)
|
||||
f.Add(emptyHeader)
|
||||
|
||||
// Tar file with a ".." path traversal attempt
|
||||
traversalTar := makeTarWithName("../../../etc/passwd")
|
||||
f.Add(traversalTar)
|
||||
|
||||
// Tar file with absolute path
|
||||
absTar := makeTarWithName("/etc/passwd")
|
||||
f.Add(absTar)
|
||||
|
||||
// Tar file with symlink pointing outside
|
||||
symlinkTar := makeSymlinkTar("link", "../../../etc/passwd")
|
||||
f.Add(symlinkTar)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
// Fuzz the tar extraction — it must never panic or escape the temp dir
|
||||
dir := t.TempDir()
|
||||
ext := NewTarExtractor()
|
||||
reader := bytes.NewReader(data)
|
||||
// Ignore errors — we only care about panics and path escapes
|
||||
_ = ext.Extract(reader, dir)
|
||||
})
|
||||
}
|
||||
|
||||
func makeTarWithName(name string) []byte {
|
||||
// Create a minimal tar file with a regular file at the given path
|
||||
var buf bytes.Buffer
|
||||
nameBytes := []byte(name)
|
||||
if len(nameBytes) > 99 {
|
||||
nameBytes = nameBytes[:99]
|
||||
}
|
||||
header := make([]byte, 512)
|
||||
copy(header[0:99], nameBytes)
|
||||
// Set type flag to regular file
|
||||
header[156] = '0'
|
||||
// Set size to zero
|
||||
buf.Write(header)
|
||||
// Two zero blocks to mark end of archive
|
||||
buf.Write(make([]byte, 1024))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func makeSymlinkTar(name, target string) []byte {
|
||||
var buf bytes.Buffer
|
||||
nameBytes := []byte(name)
|
||||
if len(nameBytes) > 99 {
|
||||
nameBytes = nameBytes[:99]
|
||||
}
|
||||
header := make([]byte, 512)
|
||||
copy(header[0:99], nameBytes)
|
||||
// Set type flag to symlink
|
||||
header[156] = '2'
|
||||
// Write link target
|
||||
targetBytes := []byte(target)
|
||||
if len(targetBytes) > 99 {
|
||||
targetBytes = targetBytes[:99]
|
||||
}
|
||||
copy(header[157:256], targetBytes)
|
||||
buf.Write(header)
|
||||
buf.Write(make([]byte, 1024))
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Extractor is the interface for archive extraction
|
||||
type Extractor interface {
|
||||
Name() string
|
||||
Extensions() []string
|
||||
Extract(r io.Reader, destDir string) error
|
||||
CanHandle(filename string) bool
|
||||
}
|
||||
|
||||
// BaseExtractor provides a basic implementation
|
||||
type BaseExtractor struct {
|
||||
name string
|
||||
extensions []string
|
||||
}
|
||||
|
||||
func NewBaseExtractor(name string, extensions []string) *BaseExtractor {
|
||||
return &BaseExtractor{name: name, extensions: extensions}
|
||||
}
|
||||
|
||||
func (b *BaseExtractor) Name() string { return b.name }
|
||||
func (b *BaseExtractor) Extensions() []string { return b.extensions }
|
||||
func (b *BaseExtractor) CanHandle(filename string) bool {
|
||||
filename = strings.ToLower(filename)
|
||||
for _, ext := range b.extensions {
|
||||
if strings.HasSuffix(filename, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DetectArchiveFormat detects the archive format by file extension
|
||||
func DetectArchiveFormat(filename string) string {
|
||||
filename = strings.ToLower(filename)
|
||||
if strings.HasSuffix(filename, ".tar.gz") || strings.HasSuffix(filename, ".tgz") {
|
||||
return "tar.gz"
|
||||
}
|
||||
if strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".tbz2") {
|
||||
return "tar.bz2"
|
||||
}
|
||||
if strings.HasSuffix(filename, ".zip") {
|
||||
return "zip"
|
||||
}
|
||||
if strings.HasSuffix(filename, ".tar") {
|
||||
return "tar"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsArchiveFormat checks whether the filename looks like an archive
|
||||
func IsArchiveFormat(filename string) bool { return DetectArchiveFormat(filename) != "" }
|
||||
|
||||
// GetArchiveAndCompression splits filename into archive + compression parts
|
||||
func GetArchiveAndCompression(filename string) (archive, compression string) {
|
||||
filename = strings.ToLower(filename)
|
||||
if strings.HasSuffix(filename, ".tar.gz") || strings.HasSuffix(filename, ".tgz") {
|
||||
return "tar", "gzip"
|
||||
}
|
||||
if strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".tbz2") {
|
||||
return "tar", "bzip2"
|
||||
}
|
||||
if strings.HasSuffix(filename, ".zip") {
|
||||
return "zip", ""
|
||||
}
|
||||
if strings.HasSuffix(filename, ".tar") {
|
||||
return "tar", ""
|
||||
}
|
||||
if strings.HasSuffix(filename, ".gz") {
|
||||
return "", "gzip"
|
||||
}
|
||||
if strings.HasSuffix(filename, ".bz2") {
|
||||
return "", "bzip2"
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type registry struct {
|
||||
mu sync.RWMutex
|
||||
extractors map[string]Extractor
|
||||
}
|
||||
|
||||
var globalRegistry = ®istry{extractors: make(map[string]Extractor)}
|
||||
|
||||
func Register(e Extractor) error {
|
||||
globalRegistry.mu.Lock()
|
||||
defer globalRegistry.mu.Unlock()
|
||||
name := strings.ToLower(e.Name())
|
||||
if name == "" {
|
||||
return fmt.Errorf("extractor name cannot be empty")
|
||||
}
|
||||
if _, exists := globalRegistry.extractors[name]; exists {
|
||||
return fmt.Errorf("extractor already registered: %s", name)
|
||||
}
|
||||
globalRegistry.extractors[name] = e
|
||||
return nil
|
||||
}
|
||||
|
||||
func Get(format string) (Extractor, error) {
|
||||
globalRegistry.mu.RLock()
|
||||
defer globalRegistry.mu.RUnlock()
|
||||
e, exists := globalRegistry.extractors[strings.ToLower(format)]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("unsupported archive format: %s", format)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func GetByFilename(filename string) (Extractor, error) {
|
||||
format := DetectArchiveFormat(filename)
|
||||
if format == "" {
|
||||
return nil, fmt.Errorf("unknown archive format for: %s", filename)
|
||||
}
|
||||
return Get(format)
|
||||
}
|
||||
|
||||
func Extract(format string, src io.Reader, destDir string) error {
|
||||
e, err := Get(format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Extract(src, destDir)
|
||||
}
|
||||
|
||||
func ExtractByFilename(filename string, src io.Reader, destDir string) error {
|
||||
e, err := GetByFilename(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.Extract(src, destDir)
|
||||
}
|
||||
|
||||
func AutoExtract(filename string, src io.Reader, destDir string) error {
|
||||
return ExtractByFilename(filename, src, destDir)
|
||||
}
|
||||
|
||||
func Supports(format string) bool {
|
||||
globalRegistry.mu.RLock()
|
||||
defer globalRegistry.mu.RUnlock()
|
||||
_, exists := globalRegistry.extractors[strings.ToLower(format)]
|
||||
return exists
|
||||
}
|
||||
|
||||
func ListSupported() []string {
|
||||
globalRegistry.mu.RLock()
|
||||
defer globalRegistry.mu.RUnlock()
|
||||
names := make([]string, 0, len(globalRegistry.extractors))
|
||||
for name := range globalRegistry.extractors {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -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()) }
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"compress/bzip2"
|
||||
"io"
|
||||
)
|
||||
|
||||
type tarBz2Extractor struct {
|
||||
*BaseExtractor
|
||||
tar *tarExtractor
|
||||
}
|
||||
|
||||
func NewTarBz2Extractor() *tarBz2Extractor {
|
||||
return &tarBz2Extractor{NewBaseExtractor("tar.bz2", []string{".tar.bz2", ".tbz2"}), NewTarExtractor()}
|
||||
}
|
||||
|
||||
func (t *tarBz2Extractor) Extract(r io.Reader, destDir string) error {
|
||||
bz2r := bzip2.NewReader(r)
|
||||
return t.tar.Extract(bz2r, destDir)
|
||||
}
|
||||
|
||||
func init() { Register(NewTarBz2Extractor()) }
|
||||
@@ -0,0 +1,42 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type tarGzExtractor struct {
|
||||
*BaseExtractor
|
||||
tar *tarExtractor
|
||||
}
|
||||
|
||||
func NewTarGzExtractor() *tarGzExtractor {
|
||||
return &tarGzExtractor{
|
||||
BaseExtractor: NewBaseExtractor("tar.gz", []string{".tar.gz", ".tgz"}),
|
||||
tar: NewTarExtractor(),
|
||||
}
|
||||
}
|
||||
|
||||
// Extract extracts a tar.gz archive into destDir
|
||||
func (t *tarGzExtractor) Extract(r io.Reader, destDir string) error {
|
||||
// Step 1: Create gzip reader for decompression
|
||||
gzr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if cerr := gzr.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
// Step 2: Extract tar from decompressed stream
|
||||
// gzr implements io.Reader that returns DECOMPRESSED data
|
||||
return t.tar.Extract(gzr, destDir)
|
||||
}
|
||||
|
||||
func init() { Register(NewTarGzExtractor()) }
|
||||
@@ -0,0 +1,141 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type zipExtractor struct{ *BaseExtractor }
|
||||
|
||||
// MaxZipInputSize is the maximum size in bytes of a single file extracted
|
||||
// from a zip archive. Files whose declared uncompressed size exceeds this
|
||||
// limit are rejected outright; the limit is also enforced at copy time as
|
||||
// a safety net in case the central directory lies (a known zip-bomb
|
||||
// technique). Defaults to 1 GiB.
|
||||
var MaxZipInputSize int64 = 1 << 30
|
||||
|
||||
// MaxZipBufferSize is the maximum size in bytes of the buffered
|
||||
// (compressed) zip archive before it is opened. Archives larger than this
|
||||
// are rejected to prevent the on-disk temp file from growing without
|
||||
// bound. Defaults to 4 GiB.
|
||||
var MaxZipBufferSize int64 = 4 << 30
|
||||
|
||||
func NewZipExtractor() *zipExtractor {
|
||||
return &zipExtractor{NewBaseExtractor("zip", []string{".zip"})}
|
||||
}
|
||||
|
||||
// Extract extracts a zip archive into destDir
|
||||
func (z *zipExtractor) Extract(r io.Reader, destDir string) error {
|
||||
// Zip.NewReader requires io.ReaderAt + size
|
||||
// Buffer input into a temp file, bounded by MaxZipBufferSize to
|
||||
// prevent a zip bomb from filling the disk with the (still
|
||||
// compressed) archive while we try to open it.
|
||||
tmpFile, err := os.CreateTemp("", "goget-zip-*.tmp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath) // Cleanup temp file after completion
|
||||
|
||||
// LimitReader returns EOF after MaxZipBufferSize+1 bytes, which lets
|
||||
// us detect "input exceeded the limit" by stat'ing the temp file.
|
||||
if _, err := io.Copy(tmpFile, io.LimitReader(r, MaxZipBufferSize+1)); err != nil {
|
||||
tmpFile.Close()
|
||||
return fmt.Errorf("failed to buffer zip: %w", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat temp file: %w", err)
|
||||
}
|
||||
if info.Size() > MaxZipBufferSize {
|
||||
return fmt.Errorf("zip archive exceeds maximum size of %d bytes (possible zip bomb)", MaxZipBufferSize)
|
||||
}
|
||||
|
||||
// Open zip reader (zip.OpenReader detects size automatically)
|
||||
zipReader, err := zip.OpenReader(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open zip archive: %w", err)
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
// Extract each file from archive
|
||||
for _, file := range zipReader.File {
|
||||
if err := z.extractFile(file, destDir); err != nil {
|
||||
return fmt.Errorf("failed to extract %s: %w", file.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFile extracts a single file from zip archive
|
||||
func (z *zipExtractor) extractFile(file *zip.File, destDir string) error {
|
||||
// Sanitize path to prevent zip-slip attacks
|
||||
cleanName := filepath.Clean(file.Name)
|
||||
if strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
|
||||
return fmt.Errorf("unsafe path in zip: %s", file.Name)
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(destDir, cleanName)
|
||||
|
||||
// Handle directories
|
||||
if file.FileInfo().IsDir() {
|
||||
return os.MkdirAll(targetPath, 0755)
|
||||
}
|
||||
|
||||
// Reject files whose declared uncompressed size exceeds the limit.
|
||||
// The central directory declares this for every file; a value larger
|
||||
// than MaxZipInputSize is a strong zip-bomb signal and we refuse the
|
||||
// whole archive (caller can decide whether to retry with a different
|
||||
// policy).
|
||||
if file.UncompressedSize64 > uint64(MaxZipInputSize) {
|
||||
return fmt.Errorf("file %s too large: %d bytes uncompressed (max %d)", file.Name, file.UncompressedSize64, MaxZipInputSize)
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open file in archive (automatically decompresses if compressed)
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Create output file with original permissions (mask setuid/setgid/sticky)
|
||||
mode := file.Mode() & 0777
|
||||
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// io.LimitReader is a safety net for archives whose central directory
|
||||
// lies about UncompressedSize64 (or leaves it at 0). The primary
|
||||
// defence is the UncompressedSize64 check above.
|
||||
if _, err := io.Copy(outFile, io.LimitReader(rc, MaxZipInputSize)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore timestamps
|
||||
if err := os.Chtimes(targetPath, file.Modified, file.Modified); err != nil {
|
||||
// Non-fatal, ignore error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() { Register(NewZipExtractor()) }
|
||||
Reference in New Issue
Block a user