clonepack/internal/clone/apt/packages.go
GnomeZworc c4776d81dd
add apt gestion
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-04-25 19:03:26 +02:00

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
}
}