go-package-manager/apt_common.go
2026-07-01 17:47:49 -05:00

163 lines
5.4 KiB
Go

package pkgmgr
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const (
// APT_SOURCES_DIR is where apt repo files are written.
APT_SOURCES_DIR = "/etc/apt/sources.list.d"
// APT_KEYRINGS_DIR is where apt repo signing keys are written when apt-key
// is unavailable. It is only used as a fallback on modern systems (apt has
// removed apt-key), so the armored ".asc" form written here is always read
// by the apt that is present.
APT_KEYRINGS_DIR = "/etc/apt/trusted.gpg.d"
)
// aptBase provides the repo and signing-key behavior shared by the apt and
// apt-get managers. Each concrete manager embeds it and supplies its own Name,
// Format, Path, and action subcommands.
type aptBase struct {
baseManager
}
func (p *aptBase) repoPath(name string) string {
return filepath.Join(APT_SOURCES_DIR, name+".list")
}
// AddRepo adds a repo from a configuration string.
func (p *aptBase) AddRepo(name, config string) error {
return writeRepoFile(p.repoPath(name), config)
}
// AddRepoURL adds a repo whose configuration is downloaded from repoURL.
func (p *aptBase) AddRepoURL(ctx context.Context, name, repoURL string) error {
return downloadRepoFile(ctx, p.repoPath(name), repoURL)
}
// RemoveRepo removes a repo.
func (p *aptBase) RemoveRepo(name string) error {
return removeRepoFile(p.repoPath(name))
}
// GetRepo returns a repo's configuration, or "" if it does not exist.
func (p *aptBase) GetRepo(name string) string {
return readRepoFile(p.repoPath(name))
}
// ListRepos returns every repo file in APT_SOURCES_DIR keyed by file stem. The
// value matches what GetRepo returns for that name. Only the .list drop-in files
// this manager reads and writes are enumerated; the main sources.list and
// deb822 .sources files are not included.
func (p *aptBase) ListRepos(ctx context.Context, args []string) (map[string]string, error) {
return listRepoFiles(APT_SOURCES_DIR, ".list")
}
// AddRepoKey adds a key for repo package verification. apt-key is used when it
// is present, which keeps older releases (where it is the only supported
// method) working. On modern systems that have removed apt-key, a drop-in
// keyring file is written instead.
func (p *aptBase) AddRepoKey(ctx context.Context, key string) error {
if aptKey := findBinary("apt-key"); aptKey != "" {
return p.aptKeyAdd(ctx, aptKey, strings.NewReader(key))
}
return aptWriteKey([]byte(key))
}
// AddRepoKeyFile adds a key for repo package verification from a file.
func (p *aptBase) AddRepoKeyFile(ctx context.Context, keyFile string) error {
if aptKey := findBinary("apt-key"); aptKey != "" {
return p.Command(ctx, aptKey, "add", keyFile).Run()
}
data, err := os.ReadFile(keyFile)
if err != nil {
return err
}
return aptWriteKey(data)
}
// AddRepoKeyURL adds a key for repo package verification from a URL.
func (p *aptBase) AddRepoKeyURL(ctx context.Context, keyURL string) error {
var buf bytes.Buffer
if err := download(ctx, keyURL, &buf); err != nil {
return err
}
if aptKey := findBinary("apt-key"); aptKey != "" {
return p.aptKeyAdd(ctx, aptKey, &buf)
}
return aptWriteKey(buf.Bytes())
}
// aptKeyAdd runs `apt-key add -`, feeding the key in through stdin.
func (p *aptBase) aptKeyAdd(ctx context.Context, aptKey string, key io.Reader) error {
cmd := p.Command(ctx, aptKey, "add", "-")
cmd.Stdin = key
return cmd.Run()
}
// aptKeyDest returns the keyring file path for a key. The fallback path only
// runs on modern apt, which reads both armored (.asc) and binary (.gpg) keys,
// so the suffix is chosen from the key's contents. The name is derived from the
// key so re-adding the same key is idempotent.
func aptKeyDest(key []byte) string {
sum := sha256.Sum256(key)
name := "pkgmgr-" + hex.EncodeToString(sum[:8])
ext := ".gpg"
if bytes.Contains(key, []byte("-----BEGIN PGP")) {
ext = ".asc"
}
return filepath.Join(APT_KEYRINGS_DIR, name+ext)
}
// aptWriteKey installs a repo signing key into APT_KEYRINGS_DIR.
func aptWriteKey(key []byte) error {
if err := os.MkdirAll(APT_KEYRINGS_DIR, 0755); err != nil {
return err
}
return os.WriteFile(aptKeyDest(key), key, 0644)
}
// ListUpgradable returns packages with an available update mapped to the
// candidate version. It simulates an upgrade with apt-get, which both apt and
// apt-get ship, so the listing is consistent and never prompts.
func (p *aptBase) ListUpgradable(ctx context.Context, args []string) (map[string]string, error) {
aptGet := findBinary("apt-get")
if aptGet == "" {
return nil, fmt.Errorf("unable to find apt-get")
}
// `apt-get -s upgrade` prints an "Inst <name> [old] (<new> ...)" line for
// each package that would be upgraded, without changing the system.
args = joinArgs(args, "-s", "upgrade")
return runParseList(p.Command(ctx, aptGet, args...), parseAptUpgradable)
}
// parseAptUpgradable parses the "Inst <name> [old] (<new> ...)" lines from a
// simulated apt-get upgrade into a name->candidate-version map.
func parseAptUpgradable(r io.Reader) (map[string]string, error) {
out := make(map[string]string)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 3 || fields[0] != "Inst" {
continue
}
// The candidate version is the parenthesized token after the name.
for _, f := range fields[2:] {
if strings.HasPrefix(f, "(") {
out[fields[1]] = strings.TrimPrefix(f, "(")
break
}
}
}
return out, scanner.Err()
}