212 lines
5.6 KiB
Go
212 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|