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