Files
device_agent/capability_pkg.go
2026-05-30 17:28:38 +02:00

147 lines
3.5 KiB
Go

package main
import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
const pkgMaxResults = 50
// detectDistro lee /etc/os-release y devuelve ID (ubuntu, debian, fedora, arch, ...).
func detectDistro() string {
f, err := os.Open("/etc/os-release")
if err != nil {
return ""
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
line := sc.Text()
if strings.HasPrefix(line, "ID=") {
return strings.Trim(strings.TrimPrefix(line, "ID="), `"`)
}
}
return ""
}
// runPkgSearch busca paquetes. Detect distro y delega a apt/dnf/pacman.
// Permite override via env PKG_FAKE_OUTPUT (for tests).
func runPkgSearch(cap *Capability, args map[string]any) (any, int, error) {
_ = cap
query := mapStringField(args, "query")
if query == "" {
return nil, -1, fmt.Errorf("query required")
}
// Validar query: solo alfanumerico + . + _ + -
for _, r := range query {
if !(r >= 'a' && r <= 'z') && !(r >= 'A' && r <= 'Z') &&
!(r >= '0' && r <= '9') && r != '.' && r != '_' && r != '-' && r != '+' {
return nil, -1, fmt.Errorf("invalid char in query: %q", query)
}
}
// Test fake output via env
if fake := os.Getenv("PKG_FAKE_OUTPUT"); fake != "" {
return parsePkgOutput(fake, "apt", query), 0, nil
}
distro := detectDistro()
var bin string
var argv []string
switch distro {
case "ubuntu", "debian":
bin = "apt-cache"
argv = []string{"search", query}
case "fedora", "rhel", "centos":
bin = "dnf"
argv = []string{"search", "--quiet", query}
case "arch", "manjaro":
bin = "pacman"
argv = []string{"-Ss", query}
default:
// fallback try apt-cache
if _, err := exec.LookPath("apt-cache"); err == nil {
bin = "apt-cache"
argv = []string{"search", query}
distro = "ubuntu"
} else {
return nil, -1, fmt.Errorf("unsupported distro %q (no pkg manager found)", distro)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
c := exec.CommandContext(ctx, bin, argv...) // #nosec G204 — query validated
out, err := c.Output()
if err != nil {
return nil, -1, fmt.Errorf("%s search: %w", bin, err)
}
return parsePkgOutput(string(out), distro, query), 0, nil
}
// parsePkgOutput parsea salida apt-cache search / dnf search / pacman -Ss.
func parsePkgOutput(out, distro, query string) map[string]any {
packages := []map[string]any{}
lines := strings.Split(out, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var name, desc string
switch distro {
case "ubuntu", "debian", "apt":
// "name - description"
idx := strings.Index(line, " - ")
if idx > 0 {
name = line[:idx]
desc = line[idx+3:]
} else {
name = line
}
case "arch", "manjaro":
// "repo/name version" then next line " desc"
if strings.HasPrefix(line, " ") {
if len(packages) > 0 {
packages[len(packages)-1]["description"] = strings.TrimSpace(line)
}
continue
}
parts := strings.Fields(line)
if len(parts) > 0 {
name = parts[0]
}
default:
// dnf: "name.arch : description"
if idx := strings.Index(line, " : "); idx > 0 {
name = strings.TrimSpace(line[:idx])
desc = strings.TrimSpace(line[idx+3:])
} else {
name = line
}
}
if name == "" {
continue
}
packages = append(packages, map[string]any{
"name": name,
"description": desc,
})
if len(packages) >= pkgMaxResults {
break
}
}
return map[string]any{
"query": query,
"distro": distro,
"packages": packages,
"count": len(packages),
"truncated": len(packages) >= pkgMaxResults,
}
}