mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-20 09:57:31 +00:00
packages/deb: add package to extract metadata from .deb files.
Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:

committed by
Dave Anderson

parent
1c6946f971
commit
8236464252
184
packages/deb/deb.go
Normal file
184
packages/deb/deb.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// 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"
|
||||
"io/ioutil"
|
||||
"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(ioutil.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(ioutil.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 := ioutil.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 {
|
||||
l, err := b.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if bytes.HasPrefix(l, archKey) {
|
||||
arch = string(bytes.TrimSpace(l[len(archKey):]))
|
||||
} else if bytes.HasPrefix(l, versionKey) {
|
||||
version = string(bytes.TrimSpace(l[len(versionKey):]))
|
||||
}
|
||||
if arch != "" && version != "" {
|
||||
return arch, version, nil
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user