feat(tarball): pure-Go ./-rooted tar.gz writer with sha256
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package tarball
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// writeFile creates path inside srcDir with the given content and mode.
|
||||
func writeFile(t *testing.T, srcDir, rel, content string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
p := filepath.Join(srcDir, rel)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatalf("mkdir for %s: %v", rel, err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(content), mode); err != nil {
|
||||
t.Fatalf("write %s: %v", rel, err)
|
||||
}
|
||||
if err := os.Chmod(p, mode); err != nil {
|
||||
t.Fatalf("chmod %s: %v", rel, err)
|
||||
}
|
||||
}
|
||||
|
||||
// readEntries opens a gzip+tar archive and returns all headers, names, and
|
||||
// file contents keyed by header name.
|
||||
func readEntries(t *testing.T, dest string) (map[string]*tar.Header, map[string]string, []string) {
|
||||
t.Helper()
|
||||
f, err := os.Open(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", dest, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
t.Fatalf("gzip open: %v", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
hdrs := map[string]*tar.Header{}
|
||||
contents := map[string]string{}
|
||||
var names []string
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("tar next: %v", err)
|
||||
}
|
||||
names = append(names, hdr.Name)
|
||||
hdrs[hdr.Name] = hdr
|
||||
if hdr.Typeflag == tar.TypeReg {
|
||||
b, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", hdr.Name, err)
|
||||
}
|
||||
contents[hdr.Name] = string(b)
|
||||
}
|
||||
}
|
||||
return hdrs, contents, names
|
||||
}
|
||||
|
||||
func TestWriteRoundtrip(t *testing.T) {
|
||||
src := t.TempDir()
|
||||
dest := filepath.Join(t.TempDir(), "pkg.tar.gz")
|
||||
|
||||
writeFile(t, src, "usr/bin/tool", "#!/bin/sh\necho hi\n", 0o755)
|
||||
writeFile(t, src, "usr/lib/libtool.so", "ELFDATA", 0o644)
|
||||
if err := os.Symlink("../lib/libtool.so", filepath.Join(src, "usr/bin/link")); err != nil {
|
||||
t.Fatalf("symlink: %v", err)
|
||||
}
|
||||
writeFile(t, src, ".PKGINFO", "pkgname = hi\n", 0o644)
|
||||
|
||||
sum, err := Write(src, dest)
|
||||
if err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
|
||||
// Returned sha256 must equal the sha256 of the written file.
|
||||
raw, err := os.ReadFile(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("read dest: %v", err)
|
||||
}
|
||||
want := sha256.Sum256(raw)
|
||||
if got := hex.EncodeToString(want[:]); got != sum {
|
||||
t.Fatalf("sha256 mismatch:\n got %s\n want %s", sum, got)
|
||||
}
|
||||
if sum != strings.ToLower(sum) {
|
||||
t.Fatalf("sha256 not lowercase hex: %q", sum)
|
||||
}
|
||||
|
||||
hdrs, contents, names := readEntries(t, dest)
|
||||
|
||||
// Every entry must be rooted at "./" (Zeta archive={strip=1} convention).
|
||||
if len(names) == 0 {
|
||||
t.Fatal("archive is empty")
|
||||
}
|
||||
for _, n := range names {
|
||||
if !strings.HasPrefix(n, "./") {
|
||||
t.Errorf("entry %q does not start with ./", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Root directory entry present.
|
||||
if h := hdrs["./"]; h == nil || h.Typeflag != tar.TypeDir {
|
||||
t.Errorf("root ./ dir entry missing or wrong type: %+v", h)
|
||||
}
|
||||
|
||||
// Directory entries included (GNU tar style: trailing slash).
|
||||
for _, want := range []string{"./usr/", "./usr/bin/", "./usr/lib/"} {
|
||||
if h := hdrs[want]; h == nil || h.Typeflag != tar.TypeDir {
|
||||
t.Errorf("dir entry %q missing or wrong type: %+v", want, h)
|
||||
}
|
||||
}
|
||||
|
||||
// Root dot-metadata must never be included.
|
||||
for n := range hdrs {
|
||||
if strings.HasSuffix(n, ".PKGINFO") {
|
||||
t.Errorf("root metadata %q must be excluded", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Executable keeps mode 0755 and its content round-trips.
|
||||
h := hdrs["./usr/bin/tool"]
|
||||
if h == nil {
|
||||
t.Fatal("./usr/bin/tool missing")
|
||||
}
|
||||
if h.Typeflag != tar.TypeReg {
|
||||
t.Errorf("./usr/bin/tool typeflag = %c, want %c", h.Typeflag, tar.TypeReg)
|
||||
}
|
||||
if got := h.Mode & 0o777; got != 0o755 {
|
||||
t.Errorf("./usr/bin/tool mode = %#o, want 0755", got)
|
||||
}
|
||||
if contents["./usr/bin/tool"] != "#!/bin/sh\necho hi\n" {
|
||||
t.Errorf("./usr/bin/tool content = %q", contents["./usr/bin/tool"])
|
||||
}
|
||||
|
||||
// Plain file keeps 0644.
|
||||
if h := hdrs["./usr/lib/libtool.so"]; h != nil {
|
||||
if got := h.Mode & 0o777; got != 0o644 {
|
||||
t.Errorf("./usr/lib/libtool.so mode = %#o, want 0644", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Symlink preserved with its target.
|
||||
h = hdrs["./usr/bin/link"]
|
||||
if h == nil {
|
||||
t.Fatal("./usr/bin/link missing")
|
||||
}
|
||||
if h.Typeflag != tar.TypeSymlink {
|
||||
t.Errorf("./usr/bin/link typeflag = %c, want %c", h.Typeflag, tar.TypeSymlink)
|
||||
}
|
||||
if h.Linkname != "../lib/libtool.so" {
|
||||
t.Errorf("./usr/bin/link linkname = %q, want %q", h.Linkname, "../lib/libtool.so")
|
||||
}
|
||||
|
||||
// Determinism: a second write yields the same sha256.
|
||||
sum2, err := Write(src, dest)
|
||||
if err != nil {
|
||||
t.Fatalf("second Write: %v", err)
|
||||
}
|
||||
if sum2 != sum {
|
||||
t.Fatalf("nondeterministic output: %s != %s", sum, sum2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRejectsEscapingSymlink(t *testing.T) {
|
||||
src := t.TempDir()
|
||||
dest := filepath.Join(t.TempDir(), "pkg.tar.gz")
|
||||
|
||||
if err := os.Symlink("../../etc/passwd", filepath.Join(src, "evil")); err != nil {
|
||||
t.Fatalf("symlink: %v", err)
|
||||
}
|
||||
|
||||
_, err := Write(src, dest)
|
||||
if err == nil {
|
||||
t.Fatal("Write accepted a symlink whose target escapes the root")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "escape") {
|
||||
t.Errorf("error should mention escape, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteKeepsNestedDotFiles(t *testing.T) {
|
||||
src := t.TempDir()
|
||||
dest := filepath.Join(t.TempDir(), "pkg.tar.gz")
|
||||
|
||||
// Dot-files below the root are real content and must NOT be skipped.
|
||||
writeFile(t, src, "usr/share/.hidden", "dotfile below root\n", 0o644)
|
||||
writeFile(t, src, "usr/share/visible", "visible\n", 0o644)
|
||||
|
||||
if _, err := Write(src, dest); err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
|
||||
hdrs, contents, _ := readEntries(t, dest)
|
||||
if hdrs["./usr/share/.hidden"] == nil {
|
||||
t.Fatal("nested dot-file ./usr/share/.hidden was wrongly skipped")
|
||||
}
|
||||
if got := contents["./usr/share/.hidden"]; got != "dotfile below root\n" {
|
||||
t.Errorf("./usr/share/.hidden content = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user