128 lines
3.2 KiB
Go
128 lines
3.2 KiB
Go
package apt
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
)
|
|
|
|
// DebPackage represents a single stanza from a Debian Packages file.
|
|
type DebPackage struct {
|
|
Package string
|
|
Version string
|
|
Architecture string
|
|
Filename string // relative path, e.g. pool/main/c/curl/curl_7.88_amd64.deb
|
|
SHA256 string
|
|
Size int64
|
|
Raw []byte // original stanza bytes for faithful re-emission
|
|
}
|
|
|
|
// ParsePackages parses all stanzas from a Debian Packages control file.
|
|
// Stanzas are separated by blank lines.
|
|
func ParsePackages(data []byte) ([]DebPackage, error) {
|
|
// Normalise line endings.
|
|
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
|
|
|
|
var pkgs []DebPackage
|
|
stanzas := bytes.Split(data, []byte("\n\n"))
|
|
|
|
for _, stanza := range stanzas {
|
|
stanza = bytes.TrimSpace(stanza)
|
|
if len(stanza) == 0 {
|
|
continue
|
|
}
|
|
pkg, err := parseStanza(stanza)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse stanza: %w", err)
|
|
}
|
|
pkgs = append(pkgs, pkg)
|
|
}
|
|
return pkgs, nil
|
|
}
|
|
|
|
func parseStanza(stanza []byte) (DebPackage, error) {
|
|
var pkg DebPackage
|
|
pkg.Raw = stanza
|
|
|
|
lines := bytes.Split(stanza, []byte("\n"))
|
|
for _, rawLine := range lines {
|
|
line := bytes.TrimRight(rawLine, " \t")
|
|
// Continuation lines (description body etc.) start with a space — skip.
|
|
if len(line) == 0 || line[0] == ' ' || line[0] == '\t' {
|
|
continue
|
|
}
|
|
|
|
colonIdx := bytes.IndexByte(line, ':')
|
|
if colonIdx < 0 {
|
|
continue
|
|
}
|
|
key := string(bytes.TrimSpace(line[:colonIdx]))
|
|
value := string(bytes.TrimSpace(line[colonIdx+1:]))
|
|
|
|
switch key {
|
|
case "Package":
|
|
pkg.Package = value
|
|
case "Version":
|
|
pkg.Version = value
|
|
case "Architecture":
|
|
pkg.Architecture = value
|
|
case "Filename":
|
|
pkg.Filename = value
|
|
case "SHA256":
|
|
pkg.SHA256 = value
|
|
case "Size":
|
|
n, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return DebPackage{}, fmt.Errorf("bad Size value %q: %w", value, err)
|
|
}
|
|
pkg.Size = n
|
|
}
|
|
}
|
|
|
|
if pkg.Package == "" {
|
|
return DebPackage{}, fmt.Errorf("stanza missing Package field")
|
|
}
|
|
return pkg, nil
|
|
}
|
|
|
|
// FilterPackages returns only the packages for which presentOnDisk returns true.
|
|
func FilterPackages(pkgs []DebPackage, presentOnDisk func(filename string) bool) []DebPackage {
|
|
var out []DebPackage
|
|
for _, p := range pkgs {
|
|
if presentOnDisk(p.Filename) {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// emitPackages serialises a slice of DebPackage back to Packages-file format,
|
|
// using the original Raw bytes for each stanza.
|
|
func emitPackages(pkgs []DebPackage) []byte {
|
|
var buf bytes.Buffer
|
|
for i, pkg := range pkgs {
|
|
// Strip trailing whitespace from each line within the stanza.
|
|
lines := bytes.Split(pkg.Raw, []byte("\n"))
|
|
for j, l := range lines {
|
|
lines[j] = bytes.TrimRight(l, " \t")
|
|
}
|
|
buf.Write(bytes.Join(lines, []byte("\n")))
|
|
if i < len(pkgs)-1 {
|
|
buf.WriteString("\n\n")
|
|
} else {
|
|
buf.WriteString("\n")
|
|
}
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// diskPresenceChecker returns a presentOnDisk func rooted at localDir.
|
|
func diskPresenceChecker(localDir string) func(string) bool {
|
|
return func(filename string) bool {
|
|
p := filepath.Join(localDir, filepath.FromSlash(filename))
|
|
_, err := os.Stat(p)
|
|
return err == nil
|
|
}
|
|
}
|