mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
164 lines
4 KiB
Go
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
|
|
}
|