goutils/cmd/cruntar/main.go

278 lines
5.0 KiB
Go

package main
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/fileutil"
)
var (
preserveOwners bool
preserveMode bool
verbose bool
)
func setupFile(hdr *tar.Header, file *os.File) error {
if preserveMode {
if verbose {
fmt.Printf("\tchmod %0#o\n", hdr.Mode)
}
err := file.Chmod(os.FileMode(hdr.Mode))
if err != nil {
return err
}
}
if preserveOwners {
fmt.Printf("\tchown %d:%d\n", hdr.Uid, hdr.Gid)
err := file.Chown(hdr.Uid, hdr.Gid)
if err != nil {
return err
}
}
return nil
}
func linkTarget(target, top string) string {
if filepath.IsAbs(target) {
return target
}
return filepath.Clean(filepath.Join(target, top))
}
func processFile(tfr *tar.Reader, hdr *tar.Header, top string) error {
if verbose {
fmt.Println(hdr.Name)
}
filePath := filepath.Clean(filepath.Join(top, hdr.Name))
switch hdr.Typeflag {
case tar.TypeReg:
file, err := os.Create(filePath)
if err != nil {
return err
}
_, err = io.Copy(file, tfr)
if err != nil {
return err
}
err = setupFile(hdr, file)
if err != nil {
return err
}
case tar.TypeLink:
file, err := os.Create(filePath)
if err != nil {
return err
}
source, err := os.Open(hdr.Linkname)
if err != nil {
return err
}
_, err = io.Copy(file, source)
if err != nil {
return err
}
err = setupFile(hdr, file)
if err != nil {
return err
}
case tar.TypeSymlink:
if !fileutil.ValidateSymlink(hdr.Linkname, top) {
return fmt.Errorf("symlink %s is outside the top-level %s",
hdr.Linkname, top)
}
path := linkTarget(hdr.Linkname, top)
if ok, err := filepath.Match(top+"/*", filepath.Clean(path)); !ok {
return fmt.Errorf("symlink %s isn't in %s", hdr.Linkname, top)
} else if err != nil {
return err
}
err := os.Symlink(linkTarget(hdr.Linkname, top), filePath)
if err != nil {
return err
}
case tar.TypeDir:
err := os.MkdirAll(filePath, os.FileMode(hdr.Mode))
if err != nil {
return err
}
}
return nil
}
var compression = map[string]bool{
"gzip": false,
"bzip2": false,
}
type bzipCloser struct {
r io.Reader
}
func (brc *bzipCloser) Read(p []byte) (int, error) {
return brc.r.Read(p)
}
func (brc *bzipCloser) Close() error {
return nil
}
func newBzipCloser(r io.ReadCloser) (io.ReadCloser, error) {
br := bzip2.NewReader(r)
return &bzipCloser{r: br}, nil
}
var compressFuncs = map[string]func(io.ReadCloser) (io.ReadCloser, error){
"gzip": func(r io.ReadCloser) (io.ReadCloser, error) { return gzip.NewReader(r) },
"bzip2": newBzipCloser,
}
func verifyCompression() bool {
var compressed bool
for _, v := range compression {
if compressed && v {
return false
}
compressed = compressed || v
}
return true
}
func getReader(r io.ReadCloser) (io.ReadCloser, error) {
for c, v := range compression {
if v {
return compressFuncs[c](r)
}
}
return r, nil
}
func openArchive(path string) (io.ReadCloser, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
r, err := getReader(file)
if err != nil {
return nil, err
}
return r, nil
}
var compressFlags struct {
z bool
j bool
}
func parseCompressFlags() error {
if compressFlags.z {
compression["gzip"] = true
}
if compressFlags.j {
compression["bzip2"] = true
}
if !verifyCompression() {
return errors.New("multiple compression formats specified")
}
return nil
}
func usage(w io.Writer) {
fmt.Fprintf(w, `ChromeOS untar
This is a tool that is intended to support untarring on SquashFS file
systems. In particular, every time it encounters a hard link, it
will just create a copy of the file.
Usage: cruntar [-jmvpz] archive [dest]
Flags:
-a Shortcut for -m -p: preserve owners and file mode.
-j The archive is compressed with bzip2.
-m Preserve file modes.
-p Preserve ownership.
-v Print the name of each file as it is being processed.
-z The archive is compressed with gzip.
`)
}
func init() {
flag.Usage = func() { usage(os.Stderr) }
}
func main() {
var archive, help bool
flag.BoolVar(&archive, "a", false, "Shortcut for -m -p: preserve owners and file mode.")
flag.BoolVar(&help, "h", false, "print a help message")
flag.BoolVar(&compressFlags.j, "j", false, "bzip2 compression")
flag.BoolVar(&preserveMode, "m", false, "preserve file modes")
flag.BoolVar(&preserveOwners, "p", false, "preserve ownership")
flag.BoolVar(&verbose, "v", false, "verbose mode")
flag.BoolVar(&compressFlags.z, "z", false, "gzip compression")
flag.Parse()
if help {
usage(os.Stdout)
os.Exit(0)
}
if archive {
preserveMode = true
preserveOwners = true
}
err := parseCompressFlags()
die.If(err)
if flag.NArg() == 0 {
return
}
top := "./"
if flag.NArg() > 1 {
top = flag.Arg(1)
}
r, err := openArchive(flag.Arg(0))
die.If(err)
tfr := tar.NewReader(r)
for {
hdr, err := tfr.Next()
if err == io.EOF {
break
}
die.If(err)
err = processFile(tfr, hdr, top)
die.If(err)
}
r.Close()
}