crfs/stargz/stargzify: add tool to convert a tar.gz to stargz

And in testing converting the Debian base layer I found a hard link,
so add enough hardlink support (mostly in TODO form) for the tool to
run for now. Proper hardlink support later.

Size stats:

-rw-r--r-- 1 bradfitz bradfitz 51354364 Mar  3 03:32 debian.tar.gz
-rw-r--r-- 1 bradfitz bradfitz 55061714 Mar 21 20:37 debian.stargz

About 7.6% bigger. (Acceptable)

Updates golang/go#30829

Change-Id: I4d76850be68d32ea6e8c2bd81c4233df1b5fc7af
Reviewed-on: https://go-review.googlesource.com/c/build/+/168737
Reviewed-by: Jon Johnson <jonjohnson@google.com>
diff --git a/crfs/stargz/stargz.go b/crfs/stargz/stargz.go
index d2983cf..1dd1863 100644
--- a/crfs/stargz/stargz.go
+++ b/crfs/stargz/stargz.go
@@ -107,7 +107,7 @@
 	// stored in the tar file, not just the base name.
 	Name string `json:"name"`
 
-	// Type is one of "dir", "reg", "symlink", or "chunk".
+	// Type is one of "dir", "reg", "symlink", "hardlink", or "chunk".
 	// The "chunk" type is used for regular file data chunks past the first
 	// TOCEntry; the 2nd chunk and on have only Type ("chunk"), Offset,
 	// ChunkOffset, and ChunkSize populated.
@@ -122,7 +122,7 @@
 	ModTime3339 string `json:"modtime,omitempty"`
 	modTime     time.Time
 
-	// LinkName, for symlinks, is the link target.
+	// LinkName, for symlinks and hardlinks, is the link target.
 	LinkName string `json:"linkName,omitempty"`
 
 	// Mode is the permission and mode bits.
@@ -239,6 +239,9 @@
 	if r == nil {
 		return
 	}
+	// TODO: decide at which stage to handle hard links. Probably
+	// here? And it probably needs a link count field stored in
+	// the TOCEntry.
 	e, ok = r.m[path]
 	return
 }
@@ -488,7 +491,8 @@
 		}
 		switch h.Typeflag {
 		case tar.TypeLink:
-			return fmt.Errorf("TODO: unsupported hardlink %q => %q", h.Name, h.Linkname)
+			ent.Type = "hardlink"
+			ent.LinkName = h.Linkname
 		case tar.TypeSymlink:
 			ent.Type = "symlink"
 			ent.LinkName = h.Linkname
diff --git a/crfs/stargz/stargzify/stargzify.go b/crfs/stargz/stargzify/stargzify.go
new file mode 100644
index 0000000..115bb9b
--- /dev/null
+++ b/crfs/stargz/stargzify/stargzify.go
@@ -0,0 +1,70 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The stargzify command converts a tarball into a seekable stargz
+// tarball. The output is still a valid tarball, but has new gzip
+// streams throughout the file and and an Table of Contents (TOC)
+// index at the end pointing into those streams.
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"strings"
+
+	"golang.org/x/build/crfs/stargz"
+)
+
+var (
+	in  = flag.String("in", "", "input file in tar or tar.gz format. Use \"-\" for stdin.")
+	out = flag.String("out", "", "output file. If empty, it's the input base + \".stargz\", or stdout if the input is stdin. Use \"-\" for stdout.")
+)
+
+func main() {
+	flag.Parse()
+	var f, fo *os.File // file in, file out
+	var err error
+	switch *in {
+	case "":
+		log.Fatal("missing required --in flag")
+	case "-":
+		f = os.Stdin
+	default:
+		f, err = os.Open(*in)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	defer f.Close()
+
+	if *out == "" {
+		if *in == "-" {
+			*out = "-"
+		} else {
+			base := strings.TrimSuffix(*in, ".gz")
+			base = strings.TrimSuffix(base, ".tgz")
+			base = strings.TrimSuffix(base, ".tar")
+			*out = base + ".stargz"
+		}
+	}
+	if *out == "-" {
+		fo = os.Stdout
+	} else {
+		fo, err = os.Create(*out)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	w := stargz.NewWriter(fo)
+	if err := w.AppendTar(f); err != nil {
+		log.Fatal(err)
+	}
+	if err := w.Close(); err != nil {
+		log.Fatal(err)
+	}
+	if err := fo.Close(); err != nil {
+		log.Fatal(err)
+	}
+}