Files
unpackit/unpackit.go
Camilo Aguilar af9d87f400 Fix error reporting for reader
since it was never making out of the local scope.
2022-10-04 11:48:52 -04:00

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
}