captchaforge 0.2.39

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! Solver selection + ordering: provider-routing (name-keyed, then method-keyed
//! for `Custom` kinds) with a legacy `supports()`-scan + PatternStore reorder
//! fallback. Split out of `chain.rs` (Law 5); reaches the chain's fields and the
//! prelude through `use super::*`.

use super::*;

impl CaptchaSolverChain {
    /// Return solvers ordered by routing intelligence:
    ///
    /// 1. If a [`crate::provider::ProviderRegistry`] is installed AND
    ///    the kind has a registered provider, **provider routing** wins:
    ///    - Provider's `recommended_solver_names()` (when non-empty)
    ///      gives an exact, name-keyed list, best for vendors with
    ///      dedicated solvers.
    ///    - Else the provider's `recommended_solver_methods()` is
    ///      consulted, method-keyed, fragile when multiple solvers
    ///      share a method but kept for backwards compat.
    /// 2. Otherwise, fall back to the legacy `supports()`-scan path
    ///    with PatternStore-based reordering.
    ///
    /// Provider routing applies to **all** kinds (built-in AND
    /// `Custom(_)`), adding a dedicated `TurnstileInteractiveSolver`
    /// no longer collides with `BehavioralCaptchaSolver` because the
    /// provider names which solver to prefer.
    pub(crate) fn ordered_solvers(
        &self,
        domain: &str,
        captcha_type: &CaptchaType,
        kind: &crate::captcha_detect::DetectedCaptcha,
    ) -> Vec<&dyn CaptchaSolver> {
        // Provider-routing path. Three cases:
        //
        // - Provider declares `recommended_solver_names()` (any kind)
        //   → strict name-keyed routing.
        // - Custom kind with a registered provider (no names, just
        //   methods) → method-keyed routing (legacy behaviour).
        // - Custom kind WITHOUT a registered provider → empty list,
        //   so the chain reports "no applicable solvers".
        //
        // For built-in kinds we INTENTIONALLY don't route via
        // method-only providers, the existing macro-emitted
        // `recommended_solver_methods` lists were shipped before name
        // routing existed, and method routing would over-narrow to a
        // single solver per method (vs the supports-scan path which
        // returns every eligible solver). Built-ins fall through to
        // supports-scan unless a provider opts in via names.
        if let Some(reg) = &self.providers {
            if let Some(provider) = reg.find_by_kind(kind) {
                let names = provider.recommended_solver_names();
                if !names.is_empty() {
                    return self.solvers_by_name(names, kind);
                }
                if matches!(kind, DetectedCaptcha::Custom(_)) {
                    let methods = provider.recommended_solver_methods();
                    return self.solvers_by_method(methods, kind);
                }
                // Built-in kind, no names declared → fall through to
                // supports-scan below.
            } else if matches!(kind, DetectedCaptcha::Custom(_)) {
                return Vec::new();
            }
        } else if matches!(kind, DetectedCaptcha::Custom(_)) {
            return Vec::new();
        }

        // Legacy supports-scan + PatternStore reorder.
        let preferred = self.patterns.best_method(domain, captcha_type);
        let mut ordered: Vec<&dyn CaptchaSolver> = self
            .solvers
            .iter()
            .filter(|s| s.supports(kind))
            .map(|s| s.as_ref())
            .collect();

        if let Some(pref) = preferred {
            if let Some(pos) = ordered.iter().position(|s| s.method() == pref) {
                if pos > 0 {
                    let item = ordered.remove(pos);
                    ordered.insert(0, item);
                }
            }
        }
        ordered
    }

    /// Resolve a name-keyed recommendation list to actual solvers.
    /// Each name is matched against `solver.name()` exactly. Solvers
    /// must additionally pass `supports(kind)` so an unkeyed
    /// third-party solver (which says `supports = false` without an
    /// API key) doesn't get picked.
    fn solvers_by_name(
        &self,
        names: &[&'static str],
        kind: &crate::captcha_detect::DetectedCaptcha,
    ) -> Vec<&dyn CaptchaSolver> {
        let mut ordered: Vec<&dyn CaptchaSolver> = Vec::with_capacity(names.len());
        for name in names {
            if let Some(s) = self
                .solvers
                .iter()
                .find(|s| s.name() == *name && s.supports(kind))
            {
                let s_ref: &dyn CaptchaSolver = s.as_ref();
                if !ordered
                    .iter()
                    .any(|existing| std::ptr::eq(*existing, s_ref))
                {
                    ordered.push(s_ref);
                }
            }
        }
        ordered
    }

    /// Resolve a method-keyed recommendation list to actual solvers.
    /// Method-based routing collides when multiple solvers share a
    /// `SolveMethod`; the FIRST chain entry wins. Prefer
    /// [`Self::solvers_by_name`] for vendors with dedicated solvers.
    fn solvers_by_method(
        &self,
        methods: &[SolveMethod],
        kind: &crate::captcha_detect::DetectedCaptcha,
    ) -> Vec<&dyn CaptchaSolver> {
        let mut ordered: Vec<&dyn CaptchaSolver> = Vec::with_capacity(methods.len());
        for method in methods {
            if let Some(s) = self
                .solvers
                .iter()
                .find(|s| s.method() == *method && s.supports(kind))
            {
                let s_ref: &dyn CaptchaSolver = s.as_ref();
                if !ordered
                    .iter()
                    .any(|existing| std::ptr::eq(*existing, s_ref))
                {
                    ordered.push(s_ref);
                }
            }
        }
        ordered
    }
}