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) } }