mirror of
https://github.com/c4milo/unpackit.git
synced 2025-09-26 19:01:11 +08:00
362 lines
7.8 KiB
Go
362 lines
7.8 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
// Package unpackit allows you to easily unpack *.tar.gz, *.tar.bzip2, *.tar.xz, *.zip and *.tar files.
|
|
// There are not CGO involved nor hard dependencies of any type.
|
|
package unpackit
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dsnet/compress/bzip2"
|
|
gzip "github.com/klauspost/pgzip"
|
|
"github.com/pkg/errors"
|
|
"github.com/ulikunitz/xz"
|
|
)
|
|
|
|
var (
|
|
magicZIP = []byte{0x50, 0x4b, 0x03, 0x04}
|
|
magicGZ = []byte{0x1f, 0x8b}
|
|
magicBZIP = []byte{0x42, 0x5a}
|
|
magicTAR = []byte{0x75, 0x73, 0x74, 0x61, 0x72} // at offset 257
|
|
magicXZ = []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}
|
|
)
|
|
|
|
// Check whether a file has the magic number for tar, gzip, bzip2 or zip files
|
|
//
|
|
// Note that this function does not advance the Reader.
|
|
//
|
|
// 50 4b 03 04 for pkzip format
|
|
// 1f 8b for .gz format
|
|
// 42 5a for .bzip format
|
|
// 75 73 74 61 72 at offset 257 for tar files
|
|
// fd 37 7a 58 5a 00 for .xz format
|
|
func magicNumber(reader *bufio.Reader, offset int) (string, error) {
|
|
headerBytes, err := reader.Peek(offset + 6)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
magic := headerBytes[offset : offset+6]
|
|
|
|
if bytes.Equal(magicTAR, magic[0:5]) {
|
|
return "tar", nil
|
|
}
|
|
|
|
if bytes.Equal(magicZIP, magic[0:4]) {
|
|
return "zip", nil
|
|
}
|
|
|
|
if bytes.Equal(magicGZ, magic[0:2]) {
|
|
return "gzip", nil
|
|
} else if bytes.Equal(magicBZIP, magic[0:2]) {
|
|
return "bzip", nil
|
|
}
|
|
|
|
if bytes.Equal(magicXZ, magic) {
|
|
return "xz", nil
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
// Unpack unpacks a compressed stream. Magic numbers are used to determine what
|
|
// decompressor and/or unarchiver to use.
|
|
func Unpack(reader io.Reader, destPath string) error {
|
|
var err error
|
|
|
|
// Makes sure destPath exists
|
|
if err := os.MkdirAll(destPath, 0o740); err != nil {
|
|
return err
|
|
}
|
|
|
|
r := bufio.NewReader(reader)
|
|
|
|
// Reads magic number from the stream so we can better determine how to proceed
|
|
ftype, err := magicNumber(r, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var decompressingReader *bufio.Reader
|
|
switch ftype {
|
|
case "gzip":
|
|
gzr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err := gzr.Close(); err != nil {
|
|
fmt.Printf("%+v", errors.Wrapf(err, "unpackit: failed closing gzip reader"))
|
|
}
|
|
}()
|
|
|
|
decompressingReader = bufio.NewReader(gzr)
|
|
case "xz":
|
|
xzr, err := xz.NewReader(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
decompressingReader = bufio.NewReader(xzr)
|
|
case "bzip":
|
|
br, err := bzip2.NewReader(r, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err := br.Close(); err != nil {
|
|
fmt.Printf("%+v", errors.Wrapf(err, "unpackit: failed closing bzip2 reader"))
|
|
}
|
|
}()
|
|
|
|
decompressingReader = bufio.NewReader(br)
|
|
case "zip":
|
|
// Like TAR, ZIP is also an archiving format, therefore we can just return
|
|
// after it finishes
|
|
return Unzip(r, destPath)
|
|
default:
|
|
// maybe it is a tarball file
|
|
decompressingReader = r
|
|
}
|
|
|
|
// Check magic number in offset 257 too see if this is also a TAR file
|
|
ftype, err = magicNumber(decompressingReader, 257)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ftype == "tar" {
|
|
return Untar(decompressingReader, destPath)
|
|
}
|
|
|
|
// If it's not a TAR archive then save it to disk as is.
|
|
destRawFile := filepath.Join(destPath, sanitize(path.Base("unknown-pack")))
|
|
|
|
// Creates destination file
|
|
destFile, err := os.Create(destRawFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := destFile.Close(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
// Copies data to destination file
|
|
if _, err := io.Copy(destFile, decompressingReader); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unzip unpacks a ZIP stream. When given a os.File reader it will get its size without
|
|
// reading the entire zip file in memory.
|
|
func Unzip(r io.Reader, destPath string) error {
|
|
var (
|
|
zr *zip.Reader
|
|
readerErr error
|
|
)
|
|
|
|
if f, ok := r.(*os.File); ok {
|
|
fstat, err := f.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
zr, readerErr = zip.NewReader(f, fstat.Size())
|
|
} else {
|
|
data, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
memReader := bytes.NewReader(data)
|
|
zr, readerErr = zip.NewReader(memReader, memReader.Size())
|
|
}
|
|
|
|
if readerErr != nil {
|
|
return readerErr
|
|
}
|
|
|
|
return unpackZip(zr, destPath)
|
|
}
|
|
|
|
func unpackZip(zr *zip.Reader, destPath string) error {
|
|
for _, f := range zr.File {
|
|
err := unzipFile(f, destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func unzipFile(f *zip.File, destPath string) error {
|
|
if f.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(filepath.Join(destPath, f.Name), f.Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := rc.Close(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
filePath := sanitize(f.Name)
|
|
destPath = filepath.Join(destPath, filePath)
|
|
|
|
// If directories were not included in the archive but are part of the file name,
|
|
// we create them relative to the destination path.
|
|
fileDir := filepath.Dir(destPath)
|
|
_, err = os.Lstat(fileDir)
|
|
if err != nil {
|
|
if err := os.MkdirAll(fileDir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
file, err := os.Create(destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err := file.Close(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
if err := file.Chmod(f.Mode()); err != nil {
|
|
log.Printf("warn: failed setting file permissions for %q: %#v", file.Name(), err)
|
|
}
|
|
|
|
if err := os.Chtimes(file.Name(), time.Now(), f.ModTime()); err != nil {
|
|
log.Printf("warn: failed setting file atime and mtime for %q: %#v", file.Name(), err)
|
|
}
|
|
|
|
if _, err := io.CopyN(file, rc, int64(f.UncompressedSize64)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Untar unarchives a TAR archive and returns the final destination path or an error
|
|
func Untar(data io.Reader, destPath string) error {
|
|
// Makes sure destPath exists
|
|
if err := os.MkdirAll(destPath, 0o740); err != nil {
|
|
return err
|
|
}
|
|
|
|
tr := tar.NewReader(data)
|
|
|
|
// Iterate through the files in the archive.
|
|
rootdir := destPath
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
// end of tar archive
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip pax_global_header with the commit ID this archive was created from
|
|
if hdr.Name == "pax_global_header" {
|
|
continue
|
|
}
|
|
|
|
fp := filepath.Join(destPath, sanitize(hdr.Name))
|
|
if hdr.FileInfo().IsDir() {
|
|
if rootdir == destPath {
|
|
rootdir = fp
|
|
}
|
|
|
|
if err := os.MkdirAll(fp, os.FileMode(hdr.Mode)); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
untarErr := untarFile(hdr, tr, fp, rootdir)
|
|
if untarErr != nil {
|
|
return untarErr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func untarFile(hdr *tar.Header, tr *tar.Reader, fp, rootdir string) error {
|
|
parentDir, _ := filepath.Split(fp)
|
|
|
|
if err := os.MkdirAll(parentDir, 0o740); err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := os.Create(fp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err := file.Close(); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}()
|
|
|
|
if err := file.Chmod(os.FileMode(hdr.Mode)); err != nil {
|
|
log.Printf("warn: failed setting file permissions for %q: %#v", file.Name(), err)
|
|
}
|
|
|
|
if err := os.Chtimes(file.Name(), time.Now(), hdr.ModTime); err != nil {
|
|
log.Printf("warn: failed setting file atime and mtime for %q: %#v", file.Name(), err)
|
|
}
|
|
|
|
if _, err := io.Copy(file, tr); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sanitizes name to avoid overwriting sensitive system files when unarchiving
|
|
func sanitize(name string) string {
|
|
// Gets rid of volume drive label in Windows
|
|
if len(name) > 1 && name[1] == ':' && runtime.GOOS == "windows" {
|
|
name = name[2:]
|
|
}
|
|
|
|
name = filepath.Clean(name)
|
|
name = filepath.ToSlash(name)
|
|
for strings.HasPrefix(name, "../") {
|
|
name = name[3:]
|
|
}
|
|
|
|
return name
|
|
}
|