summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/deb/deb.go182
-rw-r--r--packages/deb/deb_test.go205
2 files changed, 0 insertions, 387 deletions
diff --git a/packages/deb/deb.go b/packages/deb/deb.go
deleted file mode 100644
index 63f30fc9d..000000000
--- a/packages/deb/deb.go
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright (c) Tailscale Inc & contributors
-// SPDX-License-Identifier: BSD-3-Clause
-
-// Package deb extracts metadata from Debian packages.
-package deb
-
-import (
- "archive/tar"
- "bufio"
- "bytes"
- "compress/gzip"
- "crypto/md5"
- "crypto/sha1"
- "crypto/sha256"
- "errors"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-)
-
-// Info is the Debian package metadata needed to integrate the package
-// into a repository.
-type Info struct {
- // Version is the version of the package, as reported by dpkg.
- Version string
- // Arch is the Debian CPU architecture the package is for.
- Arch string
- // Control is the entire contents of the package's control file,
- // with leading and trailing whitespace removed.
- Control []byte
- // MD5 is the MD5 hash of the package file.
- MD5 []byte
- // SHA1 is the SHA1 hash of the package file.
- SHA1 []byte
- // SHA256 is the SHA256 hash of the package file.
- SHA256 []byte
-}
-
-// ReadFile returns Debian package metadata from the .deb file at path.
-func ReadFile(path string) (*Info, error) {
- f, err := os.Open(path)
- if err != nil {
- return nil, err
- }
- return Read(f)
-}
-
-// Read returns Debian package metadata from the .deb file in r.
-func Read(r io.Reader) (*Info, error) {
- b := bufio.NewReader(r)
-
- m5, s1, s256 := md5.New(), sha1.New(), sha256.New()
- summers := io.MultiWriter(m5, s1, s256)
- r = io.TeeReader(b, summers)
-
- t, err := findControlTar(r)
- if err != nil {
- return nil, fmt.Errorf("searching for control.tar.gz: %w", err)
- }
-
- control, err := findControlFile(t)
- if err != nil {
- return nil, fmt.Errorf("searching for control file in control.tar.gz: %w", err)
- }
-
- arch, version, err := findArchAndVersion(control)
- if err != nil {
- return nil, fmt.Errorf("extracting version and architecture from control file: %w", err)
- }
-
- // Exhaust the remainder of r, so that the summers see the entire file.
- if _, err := io.Copy(io.Discard, r); err != nil {
- return nil, fmt.Errorf("hashing file: %w", err)
- }
-
- return &Info{
- Version: version,
- Arch: arch,
- Control: control,
- MD5: m5.Sum(nil),
- SHA1: s1.Sum(nil),
- SHA256: s256.Sum(nil),
- }, nil
-}
-
-// findControlTar reads r as an `ar` archive, finds a tarball named
-// `control.tar.gz` within, and returns a reader for that file.
-func findControlTar(r io.Reader) (tarReader io.Reader, err error) {
- var magic [8]byte
- if _, err := io.ReadFull(r, magic[:]); err != nil {
- return nil, fmt.Errorf("reading ar magic: %w", err)
- }
- if string(magic[:]) != "!<arch>\n" {
- return nil, fmt.Errorf("not an ar file (bad magic %q)", magic)
- }
-
- for {
- var hdr [60]byte
- if _, err := io.ReadFull(r, hdr[:]); err != nil {
- return nil, fmt.Errorf("reading file header: %w", err)
- }
- filename := strings.TrimSpace(string(hdr[:16]))
- size, err := strconv.ParseInt(strings.TrimSpace(string(hdr[48:58])), 10, 64)
- if err != nil {
- return nil, fmt.Errorf("reading size of file %q: %w", filename, err)
- }
- if filename == "control.tar.gz" {
- return io.LimitReader(r, size), nil
- }
-
- // files in ar are padded out to 2 bytes.
- if size%2 == 1 {
- size++
- }
- if _, err := io.CopyN(io.Discard, r, size); err != nil {
- return nil, fmt.Errorf("seeking past file %q: %w", filename, err)
- }
- }
-}
-
-// findControlFile reads r as a tar.gz archive, finds a file named
-// `control` within, and returns its contents.
-func findControlFile(r io.Reader) (control []byte, err error) {
- gz, err := gzip.NewReader(r)
- if err != nil {
- return nil, fmt.Errorf("decompressing control.tar.gz: %w", err)
- }
- defer gz.Close()
-
- tr := tar.NewReader(gz)
- for {
- hdr, err := tr.Next()
- if err != nil {
- if errors.Is(err, io.EOF) {
- return nil, errors.New("EOF while looking for control file in control.tar.gz")
- }
- return nil, fmt.Errorf("reading tar header: %w", err)
- }
-
- if filepath.Clean(hdr.Name) != "control" {
- continue
- }
-
- // Found control file
- break
- }
-
- bs, err := io.ReadAll(tr)
- if err != nil {
- return nil, fmt.Errorf("reading control file: %w", err)
- }
-
- return bytes.TrimSpace(bs), nil
-}
-
-var (
- archKey = []byte("Architecture:")
- versionKey = []byte("Version:")
-)
-
-// findArchAndVersion extracts the architecture and version strings
-// from the given control file.
-func findArchAndVersion(control []byte) (arch string, version string, err error) {
- b := bytes.NewBuffer(control)
- for {
- ln, err := b.ReadBytes('\n')
- if err != nil {
- return "", "", err
- }
- if bytes.HasPrefix(ln, archKey) {
- arch = string(bytes.TrimSpace(ln[len(archKey):]))
- } else if bytes.HasPrefix(ln, versionKey) {
- version = string(bytes.TrimSpace(ln[len(versionKey):]))
- }
- if arch != "" && version != "" {
- return arch, version, nil
- }
- }
-}
diff --git a/packages/deb/deb_test.go b/packages/deb/deb_test.go
deleted file mode 100644
index fb8a6454c..000000000
--- a/packages/deb/deb_test.go
+++ /dev/null
@@ -1,205 +0,0 @@
-// Copyright (c) Tailscale Inc & contributors
-// SPDX-License-Identifier: BSD-3-Clause
-
-package deb
-
-import (
- "bytes"
- "crypto/md5"
- "crypto/sha1"
- "crypto/sha256"
- "encoding/hex"
- "fmt"
- "hash"
- "strings"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "github.com/goreleaser/nfpm/v2"
- _ "github.com/goreleaser/nfpm/v2/deb"
-)
-
-func TestDebInfo(t *testing.T) {
- tests := []struct {
- name string
- in []byte
- want *Info
- wantErr bool
- }{
- {
- name: "simple",
- in: mkTestDeb("1.2.3", "amd64"),
- want: &Info{
- Version: "1.2.3",
- Arch: "amd64",
- Control: mkControl(
- "Package", "tailscale",
- "Version", "1.2.3",
- "Section", "net",
- "Priority", "extra",
- "Architecture", "amd64",
- "Maintainer", "Tail Scalar",
- "Installed-Size", "0",
- "Description", "test package"),
- },
- },
- {
- name: "arm64",
- in: mkTestDeb("1.2.3", "arm64"),
- want: &Info{
- Version: "1.2.3",
- Arch: "arm64",
- Control: mkControl(
- "Package", "tailscale",
- "Version", "1.2.3",
- "Section", "net",
- "Priority", "extra",
- "Architecture", "arm64",
- "Maintainer", "Tail Scalar",
- "Installed-Size", "0",
- "Description", "test package"),
- },
- },
- {
- name: "unstable",
- in: mkTestDeb("1.7.25", "amd64"),
- want: &Info{
- Version: "1.7.25",
- Arch: "amd64",
- Control: mkControl(
- "Package", "tailscale",
- "Version", "1.7.25",
- "Section", "net",
- "Priority", "extra",
- "Architecture", "amd64",
- "Maintainer", "Tail Scalar",
- "Installed-Size", "0",
- "Description", "test package"),
- },
- },
-
- // These truncation tests assume the structure of a .deb
- // package, which is as follows:
- // magic: 8 bytes
- // file header: 60 bytes, before each file blob
- //
- // The first file in a .deb ar is "debian-binary", which is 4
- // bytes long and consists of "2.0\n".
- // The second file is control.tar.gz, which is what we care
- // about introspecting for metadata.
- // The final file is data.tar.gz, which we don't care about.
- //
- // The first file in control.tar.gz is the "control" file we
- // want to read for metadata.
- {
- name: "truncated_ar_magic",
- in: mkTestDeb("1.7.25", "amd64")[:4],
- wantErr: true,
- },
- {
- name: "truncated_ar_header",
- in: mkTestDeb("1.7.25", "amd64")[:30],
- wantErr: true,
- },
- {
- name: "missing_control_tgz",
- // Truncate right after the "debian-binary" file, which
- // makes the file a valid 1-file archive that's missing
- // control.tar.gz.
- in: mkTestDeb("1.7.25", "amd64")[:72],
- wantErr: true,
- },
- {
- name: "truncated_tgz",
- in: mkTestDeb("1.7.25", "amd64")[:172],
- wantErr: true,
- },
- }
-
- for _, test := range tests {
- // mkTestDeb returns non-deterministic output due to
- // timestamps embedded in the package file, so compute the
- // wanted hashes on the fly here.
- if test.want != nil {
- test.want.MD5 = mkHash(test.in, md5.New)
- test.want.SHA1 = mkHash(test.in, sha1.New)
- test.want.SHA256 = mkHash(test.in, sha256.New)
- }
-
- t.Run(test.name, func(t *testing.T) {
- b := bytes.NewBuffer(test.in)
- got, err := Read(b)
- if err != nil {
- if test.wantErr {
- t.Logf("got expected error: %v", err)
- return
- }
- t.Fatalf("reading deb info: %v", err)
- }
- if diff := diff(got, test.want); diff != "" {
- t.Fatalf("parsed info diff (-got+want):\n%s", diff)
- }
- })
- }
-}
-
-func diff(got, want any) string {
- matchField := func(name string) func(p cmp.Path) bool {
- return func(p cmp.Path) bool {
- if len(p) != 3 {
- return false
- }
- return p[2].String() == "."+name
- }
- }
- toLines := cmp.Transformer("lines", func(b []byte) []string { return strings.Split(string(b), "\n") })
- toHex := cmp.Transformer("hex", func(b []byte) string { return hex.EncodeToString(b) })
- return cmp.Diff(got, want,
- cmp.FilterPath(matchField("Control"), toLines),
- cmp.FilterPath(matchField("MD5"), toHex),
- cmp.FilterPath(matchField("SHA1"), toHex),
- cmp.FilterPath(matchField("SHA256"), toHex))
-}
-
-func mkTestDeb(version, arch string) []byte {
- info := nfpm.WithDefaults(&nfpm.Info{
- Name: "tailscale",
- Description: "test package",
- Arch: arch,
- Platform: "linux",
- Version: version,
- Section: "net",
- Priority: "extra",
- Maintainer: "Tail Scalar",
- })
-
- pkg, err := nfpm.Get("deb")
- if err != nil {
- panic(fmt.Sprintf("getting deb packager: %v", err))
- }
-
- var b bytes.Buffer
- if err := pkg.Package(info, &b); err != nil {
- panic(fmt.Sprintf("creating deb package: %v", err))
- }
-
- return b.Bytes()
-}
-
-func mkControl(fs ...string) []byte {
- if len(fs)%2 != 0 {
- panic("odd number of control file fields")
- }
- var b bytes.Buffer
- for i := 0; i < len(fs); i = i + 2 {
- k, v := fs[i], fs[i+1]
- fmt.Fprintf(&b, "%s: %s\n", k, v)
- }
- return bytes.TrimSpace(b.Bytes())
-}
-
-func mkHash(b []byte, hasher func() hash.Hash) []byte {
- h := hasher()
- h.Write(b)
- return h.Sum(nil)
-}