package main import ( "context" "errors" "flag" "fmt" "io" "net/http" "net/url" "os" "regexp" "sort" "strings" "time" ) const ( kindCPU = "cpu" kindGPU = "gpu" ) type options struct { kind string query string } type result struct { Kind string Name string URL string Stats []stat } type stat struct { Name string Value string } type client struct { http *http.Client } var ( letterNumberRe = regexp.MustCompile(`([a-z])([0-9])`) numberLetterRe = regexp.MustCompile(`([0-9])([a-z])`) nonWordRe = regexp.MustCompile(`[^a-z0-9]+`) scriptRe = regexp.MustCompile(`(?is)]*>.*?`) styleRe = regexp.MustCompile(`(?is)]*>.*?`) tagRe = regexp.MustCompile(`(?s)<[^>]+>`) tokenReplacer = strings.NewReplacer("+", " plus ", "-", " ", "_", " ", "(", " ", ")", " ", "/", " ") ) func main() { opts, err := parseArgs(os.Args[1:]) if err != nil { printUsage(os.Stderr) exitErr(err) } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() c := &client{http: &http.Client{Timeout: 15 * time.Second}} res, err := c.passmark(ctx, opts.kind, opts.query) if err != nil { exitErr(err) } printResult(os.Stdout, res) } func parseArgs(args []string) (options, error) { fs := flag.NewFlagSet("passctl", flag.ContinueOnError) fs.SetOutput(io.Discard) cpu := fs.Bool("cpu", false, "search PassMark CPUs") gpu := fs.Bool("gpu", false, "search PassMark GPUs") longHelp := fs.Bool("help", false, "show help") shortHelp := fs.Bool("h", false, "show help") if err := fs.Parse(args); err != nil { return options{}, err } if *shortHelp || *longHelp { printUsage(os.Stdout) os.Exit(0) } rest := fs.Args() if len(rest) != 1 { return options{}, errors.New("expected exactly one CPU or GPU search query") } var opts options if *cpu && *gpu { return options{}, errors.New("choose only one target: --cpu or --gpu") } if *cpu { opts.kind = kindCPU } if *gpu { opts.kind = kindGPU } if opts.kind == "" { return options{}, errors.New("missing target: use --cpu or --gpu") } opts.query = strings.TrimSpace(rest[0]) if opts.query == "" { return options{}, errors.New("query cannot be empty") } return opts, nil } func printUsage(w io.Writer) { fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, ` passctl --cpu "Ryzen 3600x"`) fmt.Fprintln(w, ` passctl --gpu "Radeon RX 6650 XT"`) fmt.Fprintln(w) fmt.Fprintln(w, "Options:") fmt.Fprintln(w, " --cpu Search PassMark CPU benchmarks") fmt.Fprintln(w, " --gpu Search PassMark GPU benchmarks") } func exitErr(err error) { fmt.Fprintf(os.Stderr, "passctl: %v\n", err) os.Exit(1) } func printResult(w io.Writer, res result) { fmt.Fprintf(w, "PassMark %s: %s\n", strings.ToUpper(res.Kind), res.Name) if res.URL != "" { fmt.Fprintf(w, "URL: %s\n", res.URL) } fmt.Fprintln(w) width := 0 for _, s := range res.Stats { if len(s.Name) > width { width = len(s.Name) } } for _, s := range res.Stats { fmt.Fprintf(w, "%-*s %s\n", width, s.Name+":", s.Value) } } func (c *client) passmark(ctx context.Context, kind, query string) (result, error) { baseHost := "www.cpubenchmark.net" listPath := "/cpu-list/all" detailPaths := []string{"/cpu.php", "/cpu_lookup.php"} if kind == kindGPU { baseHost = "www.videocardbenchmark.net" listPath = "/gpu_list.php" detailPaths = []string{"/gpu.php", "/gpu_lookup.php", "video_lookup.php", "/video_lookup.php"} } if kind == kindCPU { directURL := (&url.URL{Scheme: "https", Host: baseHost, Path: "/cpu.php", RawQuery: "cpu=" + url.QueryEscape(query)}).String() if direct, err := c.get(ctx, directURL); err == nil { name, stats := parsePassMarkDetail(direct) if name != "" && len(stats) > 0 { return result{Kind: kind, Name: name, URL: directURL, Stats: stats}, nil } } } listURL := (&url.URL{Scheme: "https", Host: baseHost, Path: listPath}).String() body, err := c.get(ctx, listURL) if err != nil { return result{}, err } candidates := passmarkCandidates(body, baseHost, detailPaths) best, ok := bestCandidate(query, candidates) if !ok { return result{}, fmt.Errorf("no PassMark %s match found for %q", kind, query) } if len(best.Stats) > 0 { return result{Kind: kind, Name: best.Name, URL: best.URL, Stats: best.Stats}, nil } detail, err := c.get(ctx, best.URL) if err != nil { return result{}, err } name, stats := parsePassMarkDetail(detail) if name == "" { name = best.Name } if len(stats) == 0 { return result{}, fmt.Errorf("PassMark page was fetched but no benchmark stats could be parsed: %s", best.URL) } return result{Kind: kind, Name: name, URL: best.URL, Stats: stats}, nil } func (c *client) get(ctx context.Context, rawURL string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) if err != nil { return "", err } req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 passctl/0.1") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") req.Header.Set("Accept-Language", "en-US,en;q=0.9") resp, err := c.http.Do(req) if err != nil { return "", err } defer resp.Body.Close() data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) if err != nil { return "", err } body := string(data) if resp.StatusCode >= 400 { return "", fmt.Errorf("%s returned %s", rawURL, resp.Status) } if isBlocked(body) { return "", fmt.Errorf("%s returned an anti-bot/challenge page; try again later or from a network/browser session the site accepts", rawURL) } return body, nil } func isBlocked(body string) bool { lower := strings.ToLower(body) return strings.Contains(lower, "403 forbidden") || strings.TrimSpace(lower) == "forbidden" || strings.Contains(lower, "just a moment") || strings.Contains(lower, "enable javascript and cookies") || strings.Contains(lower, "cf-chl") } type candidate struct { Name string URL string Stats []stat } func passmarkCandidates(body, host string, detailPaths []string) []candidate { var pathAlternates []string for _, p := range detailPaths { pathAlternates = append(pathAlternates, regexp.QuoteMeta(p)) } pattern := fmt.Sprintf(`(?is)]*>\s*]*>\s*]+href=["']([^"']*(?:%s)\?[^"']*(?:cpu|gpu)=[^"']*(?:&|&)id=\d+[^"']*)["'][^>]*>(.*?)\s*\s*]*>(.*?)\s*]*>(.*?)\s*]*>(.*?)\s*]*>(.*?)\s*`, strings.Join(pathAlternates, "|")) re := regexp.MustCompile(pattern) seen := make(map[string]bool) var out []candidate for _, m := range re.FindAllStringSubmatch(body, -1) { href := htmlUnescape(m[1]) name := cleanText(stripTags(m[2])) if name == "" { if u, err := url.Parse(href); err == nil { name = u.Query().Get("cpu") if name == "" { name = u.Query().Get("gpu") } } } if strings.HasPrefix(href, "/") { href = "https://" + host + href } if !strings.HasPrefix(href, "http") { href = "https://" + host + "/" + strings.TrimPrefix(href, "/") } key := strings.ToLower(name + "|" + href) if name != "" && !seen[key] { seen[key] = true stats := []stat{ {Name: "PassMark Score", Value: cleanText(stripTags(m[3]))}, {Name: "Rank", Value: cleanText(stripTags(m[4]))}, {Name: "Value", Value: cleanText(stripTags(m[5]))}, {Name: "Price", Value: cleanText(stripTags(m[6]))}, } out = append(out, candidate{Name: name, URL: href, Stats: compactStats(stats)}) } } return out } func compactStats(stats []stat) []stat { var out []stat for _, s := range stats { s.Name = strings.TrimSpace(s.Name) s.Value = strings.TrimSpace(s.Value) if s.Name == "" || s.Value == "" { continue } out = append(out, s) } return out } func bestCandidate(query string, candidates []candidate) (candidate, bool) { if len(candidates) == 0 { return candidate{}, false } normalizedQuery := normalizeTokens(query) type scored struct { candidate score int } var scoredList []scored for _, c := range candidates { score := matchScoreNormalized(normalizedQuery, c.Name) if score > 0 { scoredList = append(scoredList, scored{candidate: c, score: score}) } } if len(scoredList) == 0 { return candidates[0], true } sort.SliceStable(scoredList, func(i, j int) bool { return scoredList[i].score > scoredList[j].score }) return scoredList[0].candidate, true } func matchScore(query, name string) int { return matchScoreNormalized(normalizeTokens(query), name) } func matchScoreNormalized(q, name string) int { n := normalizeTokens(name) if q == "" || n == "" { return 0 } if q == n { return 1000 } score := 0 if strings.Contains(n, q) { score += 500 } for _, tok := range strings.Fields(q) { if strings.Contains(n, tok) { score += 50 + len(tok) } } return score } func normalizeTokens(s string) string { s = strings.ToLower(s) s = tokenReplacer.Replace(s) s = letterNumberRe.ReplaceAllString(s, "$1 $2") s = numberLetterRe.ReplaceAllString(s, "$1 $2") return strings.Join(strings.Fields(nonWordRe.ReplaceAllString(s, " ")), " ") } func parsePassMarkDetail(body string) (string, []stat) { name := firstMatch(body, `(?is)]*>(.*?)`) if name == "" { name = firstMatch(body, `(?is)]*>(.*?)`) } name = strings.TrimSuffix(cleanText(stripTags(name)), " PassMark CPU Mark") name = strings.TrimSuffix(name, " Videocard Benchmarks") var stats []stat add := func(k, v string) { k, v = cleanText(stripTags(k)), cleanText(stripTags(v)) if k == "" || v == "" || len(k) > 80 || len(v) > 200 { return } if skipPassMarkStat(k, v) { return } stats = appendUnique(stats, stat{Name: k, Value: v}) } for _, s := range passmarkSelectedStats(body) { add(s.Name, s.Value) } directScoreRe := regexp.MustCompile(`(?is)Average\s+(CPU|G3D|G2D)\s+Mark.*?]*>\s*([^<]*Rating)\s*\s*]*>\s*([0-9,]+)\s*`) for _, m := range directScoreRe.FindAllStringSubmatch(body, -1) { add("Average "+strings.ToUpper(m[1])+" Mark", m[3]) } singleRe := regexp.MustCompile(`(?is)Single\s+Thread\s+Rating\s*\s*]*>\s*([0-9,]+)\s*`) if m := singleRe.FindStringSubmatch(body); len(m) == 2 { add("Single Thread Rating", m[1]) } rowRe := regexp.MustCompile(`(?is)]*>\s*]*>(.*?)\s*]*>(.*?)`) for _, m := range rowRe.FindAllStringSubmatch(body, -1) { add(m[1], m[2]) } strongRe := regexp.MustCompile(`(?is)<(?:strong|b)[^>]*>([^:<]{2,80}:?)\s*([^<\n]{1,200})`) for _, m := range strongRe.FindAllStringSubmatch(body, -1) { add(strings.TrimSuffix(m[1], ":"), m[2]) } return name, stats } func skipPassMarkStat(k, v string) bool { lk := strings.ToLower(k) lv := strings.ToLower(v) switch lk { case "merchant", "processor", "baseline": return true } if strings.Contains(k, `"`) || strings.Contains(v, `" +`) { return true } if strings.Contains(lk, "detected and enabled") || strings.Contains(lk, "view current prices") { return true } if strings.Contains(lv, "for our website") || strings.Contains(lv, "selected cpu") { return true } return false } func passmarkSelectedStats(body string) []stat { id := firstMatch(body, `(?is)var\s+(?:cpu|gpu|videocard)DetailsId\s*=\s*["']?(\d+)`) if id == "" { id = firstMatch(body, `(?is)["'](?:cpu|gpu|videocard)DetailsId["']\s*:\s*["']?(\d+)`) } if id == "" { return nil } var stats []stat row := firstMatch(body, `(?is)]+id=["']pk`+regexp.QuoteMeta(id)+`["'][^>]*>(.*?)`) if row != "" { if product := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*prdname[^"']*["'][^>]*>(.*?)`))); product != "" { stats = appendUnique(stats, stat{Name: "Matched Product", Value: product}) } if score := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*mark-neww[^"']*["'][^>]*>(.*?)`))); score != "" { stats = appendUnique(stats, stat{Name: "PassMark Score", Value: score}) } if value := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*count[^"']*["'][^>]*>(.*?)`))); value != "" { stats = appendUnique(stats, stat{Name: "Value", Value: value}) } if price := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*price-neww[^"']*["'][^>]*>(.*?)`))); price != "" { stats = appendUnique(stats, stat{Name: "Price", Value: price}) } score := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*mark-neww[^"']*["'][^>]*>(.*?)`))) nums := onclickNumbers(row) if len(nums) >= 3 { if sameNumber(nums[0], score) { stats = appendUnique(stats, stat{Name: "Rank", Value: nums[1]}) stats = appendUnique(stats, stat{Name: "Samples", Value: nums[2]}) } else { stats = appendUnique(stats, stat{Name: "Rank", Value: nums[0]}) stats = appendUnique(stats, stat{Name: "Samples", Value: nums[2]}) } } } rankRow := firstMatch(body, `(?is)]+id=["']rk`+regexp.QuoteMeta(id)+`["'][^>]*>(.*?)`) if rankRow != "" { if score := cleanText(stripTags(firstMatch(rankRow, `(?is)]+class=["'][^"']*count[^"']*["'][^>]*>(.*?)`))); score != "" { stats = appendUnique(stats, stat{Name: "PassMark Score", Value: score}) } } return stats } func onclickNumbers(row string) []string { payload := firstMatch(row, `(?is)onclick=["'][^(]*\((.*?)\)`) if payload == "" { return nil } re := regexp.MustCompile(`['"]?(-?(?:\d{1,3},)*\d+(?:\.\d+)?)['"]?`) var nums []string for _, m := range re.FindAllStringSubmatch(payload, -1) { nums = append(nums, m[1]) } return nums } func sameNumber(a, b string) bool { clean := func(s string) string { s = strings.ReplaceAll(s, ",", "") s = strings.TrimSpace(s) return strings.TrimSuffix(s, ".0") } return clean(a) == clean(b) } func firstMatch(s, pattern string) string { re := regexp.MustCompile(pattern) m := re.FindStringSubmatch(s) if len(m) < 2 { return "" } return m[1] } func stripScripts(s string) string { s = scriptRe.ReplaceAllString(s, " ") return styleRe.ReplaceAllString(s, " ") } func stripTags(s string) string { s = stripScripts(s) return tagRe.ReplaceAllString(s, " ") } func cleanText(s string) string { s = htmlUnescape(s) s = strings.ReplaceAll(s, "\u00a0", " ") return strings.Join(strings.Fields(s), " ") } func htmlUnescape(s string) string { repl := strings.NewReplacer( "&", "&", "<", "<", ">", ">", """, `"`, "'", "'", "'", "'", " ", " ", ) return repl.Replace(s) } func appendUnique(stats []stat, next stat) []stat { key := strings.ToLower(next.Name) for _, s := range stats { if strings.ToLower(s.Name) == key { return stats } } return append(stats, next) }