From ffbe98b8636e306d13765ba5281cd4c384713bf7 Mon Sep 17 00:00:00 2001 From: Noel Cower Date: Fri, 9 Mar 2018 22:37:37 -0800 Subject: [PATCH] Initial commit This program is a horrifying mess but I really wanted it for.. some reason. Yeah. It should surprise nobody. Change-Id: I5a2498a09c5dbbd379b37b71404d7982753b7f8a --- .gitignore | 4 + LICENSE.txt | 23 ++++ README.md | 20 +++ mtar.go | 361 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 408 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 mtar.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3ce7d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.*.sw[op] +*.tar +mtar +.DS_Store diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e07ce6d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright 2018 Noel Cower + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..292e2d2 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +mtar +==== + +[![GoDoc](https://godoc.org/go.spiff.io/mtar?status.svg)](https://godoc.org/go.spiff.io/mtar) + +mtar is a simple-ish tool for creating tar files with arbitrary src-to-archive +path mappings and regexp filters. + +You can download and install mtar using the go tool by running + + $ go get -u go.spiff.io/mtar + +Usage text can be read at or by running +`mtar -h`. + +LICENSE +------- + +mtar is licensed under the BSD two-clause license. This is readable at the top +of its source file(s) and in the LICENSE.txt file. diff --git a/mtar.go b/mtar.go new file mode 100644 index 0000000..9ed163f --- /dev/null +++ b/mtar.go @@ -0,0 +1,361 @@ +// Copyright 2018 Noel Cower +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +// Mtar is a simple tar program to create tar files with arbitrary path mappings from the source +// filesystem to tar paths. It only supports regular files, directories, and symlinks. +// +// Download and install with +// +// go get -u go.spiff.io/mtar +// +// Usage: +// +// mtar [-h|--help] [FILE|OPTION] +// +// Writes a tar file to standard output. +// +// FILE may be a regular filepath for a file, symlink, or directory. If +// FILE contains a ':', the text after the colon is the path to write to +// the tar file. For example, the following paths behave differently: +// +// SRC +// Add file SRC to the tar file as-is. +// SRC: +// Add file SRC to the tar file as-is. +// SRC:DEST +// Add file SRC as DEST to the tar file. +// +// There is currently no SRC to accept standard input as a file (other +// than, for example, using /dev/stdin). +// +// In addition, options may be passed in the middle of file arguments to +// control archive creation: +// +// -h | --help +// When passed as the first argument, print this usage text. +// -Cdir | -C dir +// Change to directory (relative to PWD at all times; -C. will reset +// the current directory) for subsequent file additions. +// -OREGEX | -O REGEX +// Add a filter to reject output paths, after mapping, that do not +// match the REGEX. +// -oREGEX | -o REGEX +// Select only output paths, after mapping, that match the REGEX. +// -IREGEX | -I REGEX +// Add a filter to reject input paths (as passed) that match the REGEX. +// -iREGEX | -i REGEX +// Add a filter to select only input paths that match the REGEX. +// -Ri, -Ro, -R +// Reset input, output, or all filters, respectively. +// +package main // import "go.spiff.io/mtar" + +import ( + "archive/tar" + "io" + "log" + "os" + "os/user" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" +) + +type Args struct{ args []string } + +type Matcher struct { + rx *regexp.Regexp + want bool +} + +func (m Matcher) matches(s string) bool { + return m.rx.MatchString(s) == m.want +} + +var startupDir string +var skipSrcGlobs []Matcher +var skipDestGlobs []Matcher + +func (p *Args) Shift() (s string, ok bool) { + if ok = len(p.args) > 0; ok { + s, p.args = p.args[0], p.args[1:] + } + return +} + +func usage() { + io.WriteString(os.Stderr, + `Usage: mtar [-h|--help] [FILE|OPTION] + +Writes a tar file to standard output. + +FILE may be a regular filepath for a file, symlink, or directory. If +FILE contains a ':', the text after the colon is the path to write to +the tar file. For example, the following paths behave differently: + + SRC + Add file SRC to the tar file as-is. + SRC: + Add file SRC to the tar file as-is. + SRC:DEST + Add file SRC as DEST to the tar file. + +There is currently no SRC to accept standard input as a file (other +than, for example, using /dev/stdin). + +In addition, options may be passed in the middle of file arguments to +control archive creation: + + -h | --help + When passed as the first argument, print this usage text. + -Cdir | -C dir + Change to directory (relative to PWD at all times; -C. will reset + the current directory) for subsequent file additions. + -OREGEX | -O REGEX + Add a filter to reject output paths, after mapping, that do not + match the REGEX. + -oREGEX | -o REGEX + Select only output paths, after mapping, that match the REGEX. + -IREGEX | -I REGEX + Add a filter to reject input paths (as passed) that match the REGEX. + -iREGEX | -i REGEX + Add a filter to select only input paths that match the REGEX. + -Ri, -Ro, -R + Reset input, output, or all filters, respectively.`+"\n") +} + +func main() { + log.SetFlags(0) + log.SetPrefix("mtar: ") + + var err error + startupDir, err = os.Getwd() + failOnError("getwd", err) + + // Using some pretty weird CLI arguments here so incoming weird as hell arg loop ahead + if len(os.Args) <= 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + usage() + os.Exit(2) + } + + w := tar.NewWriter(os.Stdout) + defer func() { failOnError("error writing output", w.Close()) }() + argv := Args{args: os.Args[1:]} + for s, ok := argv.Shift(); ok; s, ok = argv.Shift() { + switch { + // Filter flags + case s == "-Ro": // reset output filters + skipSrcGlobs = nil + case s == "-Ri": // reset input filters + skipSrcGlobs = nil + case s == "-R": // reset all filters + skipSrcGlobs, skipDestGlobs = nil, nil + case s == "-i" || s == "-I": // filter input by regexp + want := s[1] == 'i' + if s, ok = argv.Shift(); !ok { + log.Fatal("-i: missing regexp") + } + skipSrcGlobs = append(skipSrcGlobs, Matcher{rx: regexp.MustCompile(s), want: want}) + case strings.HasPrefix(s, "-I") || strings.HasPrefix(s, "-i"): + want := s[1] == 'i' + skipSrcGlobs = append(skipSrcGlobs, Matcher{rx: regexp.MustCompile(s[2:]), want: want}) + case s == "-o" || s == "-O": // filter output by regexp (after mapping) + want := s[1] == 'o' + if s, ok = argv.Shift(); !ok { + log.Fatal("-O: missing regexp") + } + skipDestGlobs = append(skipDestGlobs, Matcher{rx: regexp.MustCompile(s), want: want}) + case strings.HasPrefix(s, "-O") || strings.HasPrefix(s, "-o"): + want := s[1] == 'o' + skipDestGlobs = append(skipDestGlobs, Matcher{rx: regexp.MustCompile(s[2:]), want: want}) + + // Change dir + case s == "-C": // cd + if s, ok = argv.Shift(); !ok { + log.Fatal("-C: missing directory") + } + failOnError("cd", changeDir(s)) + continue + + case strings.HasPrefix(s, "-C"): // cd + failOnError("cd", changeDir(s[2:])) + + // Add files + default: + src, dest := s, "" + switch idx := strings.IndexByte(src, ':'); idx { + case -1: // no mapping -- use src as path + case 0: // no src + log.Fatalf("no source: %q", s) + case len(src) - 1: // no dest -- use src path + src = s[:idx] + default: // path given + src, dest = s[:idx], s[idx+1:] + } + + addFile(w, src, dest, true) + } + } +} + +func addFile(w *tar.Writer, src, dest string, allowRecursive bool) { + if shouldSkip(skipSrcGlobs, src) { + return + } + + st, err := os.Lstat(src) + failOnError("add file: stat error", err) + if dest == "" { + dest = src + } + + dest = path.Clean(filepath.ToSlash(dest)) + if strings.HasPrefix(dest, "/") { + dest = "." + dest + } else if !strings.HasPrefix("./", dest) { + dest = "./" + dest + } + if dest == ".." || strings.HasPrefix(dest, "../") { + log.Fatal("add file: destination may not contain .. (", dest, ")") + } + + hdr := &tar.Header{ + Name: dest, + Typeflag: tar.TypeReg, + ModTime: st.ModTime(), + Mode: int64(st.Mode().Perm()), + Format: tar.FormatPAX, + } + + if uid, gid, ok := getUidGid(st); ok { + hdr.Uid, err = strconv.Atoi(uid.Uid) + hdr.Uname = uid.Username + if err != nil { + log.Fatalf("cannot parse uid (%q) for %s: %v", uid.Uid, src, err) + } + hdr.Gid, err = strconv.Atoi(gid.Gid) + hdr.Gname = gid.Name + if err != nil { + log.Fatalf("cannot parse gid (%q) for %s: %v", gid.Gid, src, err) + } + } + + switch { + case st.Mode().IsRegular(): + hdr.Size = st.Size() + case st.IsDir(): + hdr.Typeflag = tar.TypeDir + hdr.Name = dest + "/" + case st.Mode()&os.ModeSymlink == os.ModeSymlink: + hdr.Typeflag = tar.TypeSymlink + hdr.Name = dest + link, err := os.Readlink(src) + failOnError("cannot resolve symlink", err) + hdr.Linkname = link + default: + log.Print("skipping file: ", src, ": cannot add file with mode ", st.Mode().Perm()) + return + } + + if shouldSkip(skipDestGlobs, hdr.Name) { + return + } + + failOnError("write header: "+hdr.Name, w.WriteHeader(hdr)) + + if st.Mode().IsDir() { + if allowRecursive { + addRecursive(w, src, dest) + } + return + } + + if !st.Mode().IsRegular() { + return + } + + file, err := os.Open(src) + failOnError("read error: "+src, err) + defer file.Close() + n, err := io.Copy(w, file) + failOnError("copy error: "+src, err) + if n != hdr.Size { + log.Fatalf("copy error: size mismatch for %s: wrote %d, want %d", src, n, hdr.Size) + } +} + +func addRecursive(w *tar.Writer, src, prefix string) { + src = strings.TrimRight(src, "/") + filepath.Walk(src, func(p string, info os.FileInfo, err error) error { + if filepath.Clean(p) == filepath.Clean(src) || shouldSkip(skipSrcGlobs, p) { + return nil + } + dest := path.Join(prefix, strings.TrimPrefix(p, src)) + addFile(w, p, dest, false) + return nil + }) +} + +func changeDir(dir string) error { + const pathsep = string(filepath.Separator) + if !filepath.IsAbs(dir) { + dir = filepath.Join(startupDir, dir) + } + return os.Chdir(dir) +} + +func failOnError(prefix string, err error) { + if err != nil { + log.Fatalf("%s: %v", prefix, err) + } +} + +func shouldSkip(set []Matcher, s string) bool { + for _, m := range set { + if !m.matches(s) { + return true + } + } + return false +} + +func getUidGid(f os.FileInfo) (*user.User, *user.Group, bool) { + stat, ok := f.Sys().(*syscall.Stat_t) + if !ok { + return nil, nil, false + } + uid, gid := strconv.FormatUint(uint64(stat.Uid), 10), strconv.FormatUint(uint64(stat.Gid), 10) + u, err := user.LookupId(uid) + if err != nil { + return nil, nil, false + } + g, err := user.LookupGroupId(gid) + if err != nil { + return nil, nil, false + } + return u, g, true +} -- 2.45.2