#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::process::Command;
const DEFAULT_SEARCH_PATHS: &[&str] = &[
"/usr/local/bin/captchaforge-chromium",
"/opt/captchaforge-chromium/bin/chromium",
"/opt/captchaforge/chromium",
"/Applications/captchaforge-chromium.app/Contents/MacOS/Chromium",
"/snap/captchaforge-chromium/current/usr/bin/chromium",
];
const PATCHED_VERSION_PREFIX: &str = "captchaforge-chromium/";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DetectOutcome {
Found { path: PathBuf, version: String },
NotFound,
UnverifiedCandidate { path: PathBuf, version: String },
}
impl DetectOutcome {
pub fn is_found(&self) -> bool {
matches!(self, DetectOutcome::Found { .. })
}
pub fn path(&self) -> Option<&Path> {
match self {
DetectOutcome::Found { path, .. } | DetectOutcome::UnverifiedCandidate { path, .. } => {
Some(path.as_path())
}
DetectOutcome::NotFound => None,
}
}
}
pub struct PatchedChromiumDetector {
extra_paths: Vec<PathBuf>,
}
impl PatchedChromiumDetector {
pub fn new() -> Self {
Self { extra_paths: Vec::new() }
}
pub fn with_extra_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.extra_paths = paths;
self
}
pub fn detect(&self) -> DetectOutcome {
let mut last_unverified: Option<DetectOutcome> = None;
for raw in self.candidate_paths() {
let path = raw.clone();
if !path.exists() {
continue;
}
let Some(banner) = read_version(&path) else {
continue;
};
if banner.contains(PATCHED_VERSION_PREFIX) {
tracing::info!(
path = %path.display(),
version = %banner,
"captchaforge: patched chromium sidecar detected"
);
return DetectOutcome::Found {
path,
version: banner,
};
}
last_unverified = Some(DetectOutcome::UnverifiedCandidate {
path,
version: banner,
});
}
if let Some(outcome) = last_unverified {
tracing::debug!(
path = ?outcome.path(),
"captchaforge: candidate chromium binary present but version banner \
does not carry the patched-fork prefix; skipping"
);
return outcome;
}
tracing::debug!(
"captchaforge: no patched chromium sidecar detected — \
falling back to system chromium (true-stealth tier unavailable)"
);
DetectOutcome::NotFound
}
pub fn recommended_browser_config(&self) -> Option<chromiumoxide::BrowserConfig> {
let outcome = self.detect();
let DetectOutcome::Found { path, .. } = outcome else {
return None;
};
let mut builder = chromiumoxide::BrowserConfig::builder().chrome_executable(path);
for arg in crate::sdk::recommended_chromium_args() {
builder = builder.arg(arg);
}
builder.build().ok()
}
fn candidate_paths(&self) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = self.extra_paths.clone();
for s in DEFAULT_SEARCH_PATHS {
out.push(PathBuf::from(s));
}
out
}
}
impl Default for PatchedChromiumDetector {
fn default() -> Self {
Self::new()
}
}
fn read_version(path: &Path) -> Option<String> {
let output = Command::new(path).arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
let s = String::from_utf8(output.stdout).ok()?;
Some(s.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn detect_returns_not_found_when_search_paths_empty_and_extras_unset() {
let detector = PatchedChromiumDetector::new();
let outcome = detector.detect();
match outcome {
DetectOutcome::Found { .. } => {
println!("test host has patched fork installed (rare; benign)");
}
DetectOutcome::NotFound | DetectOutcome::UnverifiedCandidate { .. } => {}
}
}
#[test]
fn extra_paths_are_searched_before_defaults() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("fake-patched-chromium");
std::fs::write(
&path,
format!("#!/bin/sh\necho '{PATCHED_VERSION_PREFIX}1.2.3 (fake)'\n"),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&path).unwrap().permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&path, perm).unwrap();
}
let detector = PatchedChromiumDetector::new().with_extra_paths(vec![path.clone()]);
let outcome = detector.detect();
match outcome {
DetectOutcome::Found {
path: found_path,
version,
} => {
assert_eq!(found_path, path);
assert!(version.contains(PATCHED_VERSION_PREFIX));
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn unverified_candidate_returned_when_version_banner_wrong() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("system-chromium");
std::fs::write(
&path,
"#!/bin/sh\necho 'Chromium 130.0.6723.116'\n",
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(&path).unwrap().permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&path, perm).unwrap();
}
let detector = PatchedChromiumDetector::new().with_extra_paths(vec![path.clone()]);
let outcome = detector.detect();
match outcome {
DetectOutcome::UnverifiedCandidate {
path: found_path,
version,
} => {
assert_eq!(found_path, path);
assert!(version.contains("Chromium 130"));
assert!(!version.contains(PATCHED_VERSION_PREFIX));
}
DetectOutcome::Found { .. } => {
panic!("system chromium must NOT be classified as Found");
}
DetectOutcome::NotFound => {
}
}
}
#[test]
fn detect_outcome_helpers() {
let f = DetectOutcome::Found {
path: PathBuf::from("/x"),
version: "captchaforge-chromium/1.0".into(),
};
assert!(f.is_found());
assert_eq!(f.path(), Some(Path::new("/x")));
let nf = DetectOutcome::NotFound;
assert!(!nf.is_found());
assert_eq!(nf.path(), None);
let u = DetectOutcome::UnverifiedCandidate {
path: PathBuf::from("/y"),
version: "Chromium 130".into(),
};
assert!(!u.is_found());
assert_eq!(u.path(), Some(Path::new("/y")));
}
#[test]
fn candidate_paths_include_defaults_after_extras() {
let detector = PatchedChromiumDetector::new().with_extra_paths(vec![
PathBuf::from("/custom/path1"),
PathBuf::from("/custom/path2"),
]);
let paths = detector.candidate_paths();
assert_eq!(paths[0], PathBuf::from("/custom/path1"));
assert_eq!(paths[1], PathBuf::from("/custom/path2"));
assert!(paths.len() > 2);
assert!(paths.iter().any(|p| p == Path::new("/usr/local/bin/captchaforge-chromium")));
}
#[test]
fn read_version_returns_none_for_missing_binary() {
assert_eq!(
read_version(Path::new("/path/that/definitely/does/not/exist/abc123")),
None
);
}
#[test]
fn patched_version_prefix_is_stable_for_dashboards() {
assert_eq!(PATCHED_VERSION_PREFIX, "captchaforge-chromium/");
}
}