diff --git a/README.md b/README.md index 04fda32..67d59ad 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Contents: the time to expiry and checking for revocations. clustersh/ Run commands or transfer files across multiple servers via SSH. + cruntar/ Untar an archive with hard links, copying instead of + linking. csrpubdump/ Dump the public key from an X.509 certificate request. fragment/ Print a fragment of a file. jlp/ JSON linter/prettifier. @@ -47,7 +49,7 @@ Contents: testio/ Various I/O utilities useful during testing. testutil/ Various utility functions useful during testing. - + Each program should have a small README in the directory with more information. diff --git a/cmd/cruntar/README b/cmd/cruntar/README new file mode 100644 index 0000000..aad18d9 --- /dev/null +++ b/cmd/cruntar/README @@ -0,0 +1,20 @@ +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. + +I wrote this after running into problems with untarring the +gcc-arm-eabi-none toolchain. The shared storage in Termux under +ChromeOS doesn't support hard links, so I opted to just make a copy +rather than dealing with links and whatnot. diff --git a/cmd/cruntar/main.go b/cmd/cruntar/main.go new file mode 100644 index 0000000..8b9ffd2 --- /dev/null +++ b/cmd/cruntar/main.go @@ -0,0 +1,265 @@ +package main + +import ( + "archive/tar" + "compress/bzip2" + "compress/gzip" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/kisom/goutils/die" +) + +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, tar.TypeRegA: + 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: + 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() +}