147 lines
3.5 KiB
Go
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,
|
|
}
|
|
}
|