fleet/pkg/file/deb.go
2024-11-11 14:13:01 -05:00

164 lines
4 KiB
Go

package file
import (
"archive/tar"
"bufio"
"bytes"
"compress/bzip2"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"path"
"path/filepath"
"strings"
"github.com/blakesmith/ar"
"github.com/klauspost/compress/zstd"
"github.com/xi2/xz"
)
// ExtractDebMetadata extracts the name and version metadata from a .deb file ,
// a debian installer package which is in archive format.
func ExtractDebMetadata(r io.Reader) (*InstallerMetadata, error) {
h := sha256.New()
r = io.TeeReader(r, h)
rr := ar.NewReader(r)
for {
hdr, err := rr.Next()
if err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to advance to next file in archive: %w", err)
}
filename := path.Clean(hdr.Name)
if strings.HasPrefix(filename, "control.tar") {
ext := filepath.Ext(filename)
if ext == ".tar" {
ext = ""
}
name, version, err := parseControl(rr, ext)
if err != nil {
return nil, err
}
// ensure the whole file is read to get the correct hash
if _, err := io.Copy(io.Discard, r); err != nil {
return nil, fmt.Errorf("failed to read all content: %w", err)
}
return &InstallerMetadata{
Name: name,
Version: version,
PackageIDs: []string{name},
SHASum: h.Sum(nil),
}, nil
}
}
// ensure the whole file is read to get the correct hash
if _, err := io.Copy(io.Discard, r); err != nil {
return nil, fmt.Errorf("failed to read all content: %w", err)
}
// no control.tar file found, return empty information
return &InstallerMetadata{SHASum: h.Sum(nil)}, nil
}
// parseControl adapted from
// https://github.com/sassoftware/relic/blob/6c510a666832163a5d02587bda8be970d5e29b8c/lib/signdeb/control.go#L38-L39
//
// Copyright (c) SAS Institute Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Parse basic package info from a control.tar.* stream.
func parseControl(r io.Reader, ext string) (name, version string, err error) {
switch ext {
case ".gz":
gz, err := gzip.NewReader(r)
if err != nil {
return "", "", fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gz.Close()
r = gz
case ".bz2":
r = bzip2.NewReader(r)
case ".xz":
r, err = xz.NewReader(r, 0)
if err != nil {
return "", "", fmt.Errorf("failed to create xz reader: %w", err)
}
case ".zst":
zr, err := zstd.NewReader(r)
if err != nil {
return "", "", fmt.Errorf("failed to create zstd reader: %w", err)
}
defer zr.Close()
r = zr
case "":
// uncompressed
default:
return "", "", errors.New("unrecognized compression on control.tar: " + ext)
}
tr := tar.NewReader(r)
found := false
for {
hdr, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return "", "", err
}
if path.Clean(hdr.Name) == "control" {
found = true
break
}
}
if !found {
return "", "", errors.New("control.tar has no control file")
}
blob, err := io.ReadAll(tr)
if err != nil {
return "", "", fmt.Errorf("failed to read tar file: %w", err)
}
scanner := bufio.NewScanner(bytes.NewReader(blob))
for scanner.Scan() {
line := scanner.Text()
i := strings.IndexAny(line, " \t\r\n")
j := strings.Index(line, ":")
if j < 0 || i < j {
continue
}
key := line[:j]
value := strings.Trim(line[j+1:], " \t\r\n")
switch strings.ToLower(key) {
case "package":
name = value
case "version":
version = value
}
}
if err := scanner.Err(); err != nil {
return name, version, fmt.Errorf("failed to scan control file: %w", err)
}
return name, version, nil
}