use super::{lookup, Browser, BrowserOs, TlsFingerprint};
pub fn era_for(browser: Browser, major: u16, os: BrowserOs) -> Option<&'static TlsFingerprint> {
if let Some(fp) = exact_match(browser, major, os) {
return Some(fp);
}
let representative = match browser {
Browser::Chrome | Browser::Chromium => chrome_era_representative(major, os),
Browser::Edge => edge_era_representative(major),
Browser::Firefox => firefox_era_representative(major),
Browser::Safari => safari_era_representative(major),
Browser::Brave | Browser::Opera => {
chrome_era_representative(major, os)
}
Browser::Other => None,
};
if let Some(name) = representative {
if let Some(fp) = lookup(name) {
tracing::warn!(
target: "crawlex::impersonate::catalog",
browser = ?browser,
major,
os = ?os,
fallback = name,
"exact TLS fingerprint not in catalog; using era representative"
);
return Some(fp);
}
}
None
}
fn exact_match(browser: Browser, major: u16, os: BrowserOs) -> Option<&'static TlsFingerprint> {
let browser_token = match browser {
Browser::Chrome => "chrome",
Browser::Chromium => "chromium",
Browser::Firefox => "firefox",
Browser::Edge => "edge",
Browser::Safari => "safari",
_ => return None,
};
let os_token = match os {
BrowserOs::Windows => "win10",
BrowserOs::MacOs => "macos",
BrowserOs::Linux => "linux",
BrowserOs::Android => "android",
BrowserOs::Other => return None,
};
let prefix = format!("{}_{}", browser_token, major);
let suffix = format!("_{}", os_token);
super::all().find(|fp| {
fp.name.starts_with(&prefix)
&& fp.name.contains(&suffix)
&& (fp.name.as_bytes().get(prefix.len()) == Some(&b'.')
|| fp.name.as_bytes().get(prefix.len()) == Some(&b'_'))
})
}
fn chrome_era_representative(major: u16, os: BrowserOs) -> Option<&'static str> {
let _ = os;
let (name, era_label) = match major {
0..=98 => ("chrome_98.0.4758.102_win10", "E1"),
99 => ("chrome_99.0.4844.51_win10", "E1"),
100 => ("chrome_100.0.4896.127_win10", "E2"),
101..=103 => ("chrome_101.0.4951.67_win10", "E2"),
104..=106 => ("chrome_104.0.5112.81_win10", "E3a"),
107..=109 => ("chrome_107.0.5304.107_win10", "E3a"),
110 => ("chrome_110.0.5481.177_win10", "E3a"),
111..=116 => ("chrome_116.0.5845.180_win10", "E3b"),
117..=123 => ("chrome_116.0.5845.180_win10", "E4"),
124..=131 => ("chrome_116.0.5845.180_win10", "E5"),
132..=141 => ("chrome_116.0.5845.180_win10", "E6"),
142..=u16::MAX => ("chrome_116.0.5845.180_win10", "E7"),
};
if matches!(major, 117..=u16::MAX) {
tracing::trace!(
target: "crawlex::impersonate::catalog::era",
major,
era = era_label,
representative = name,
"chrome era fallback active — capture this version to collapse"
);
}
Some(name)
}
fn edge_era_representative(major: u16) -> Option<&'static str> {
Some(match major {
0..=98 => "edge_98.0.1108.62_win10",
99..=100 => "edge_99.0.1150.30_win10",
101..=u16::MAX => "edge_101.0.1210.47_win10",
})
}
fn firefox_era_representative(major: u16) -> Option<&'static str> {
let (name, era_label) = match major {
0..=91 => ("firefox_91.6.0esr_win10", "ESR"),
92..=95 => ("firefox_95.0.2_win10", "F-A"),
96..=100 => ("firefox_100.0_win10", "F-B"),
101..=108 => ("firefox_102.0_win10", "F-C"),
109..=116 => ("firefox_109.0_win10", "F-D"),
117..=119 => ("firefox_117.0.1_win10", "FF-A"),
120..=126 => ("firefox_117.0.1_win10", "FF-B"),
127..=u16::MAX => ("firefox_117.0.1_win10", "FF-C"),
};
if matches!(major, 118..=u16::MAX) {
tracing::trace!(
target: "crawlex::impersonate::catalog::era",
major,
era = era_label,
representative = name,
"firefox era fallback active — capture this version to collapse"
);
}
Some(name)
}
fn safari_era_representative(major: u16) -> Option<&'static str> {
Some(match major {
0..=15 => "safari_15.5_macos12.4",
_ => "safari_15.5_macos12.4",
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chrome_149_resolves_via_era_fallback() {
let fp = era_for(Browser::Chrome, 149, BrowserOs::Linux).expect("era resolves");
assert!(fp.name.starts_with("chrome_"), "name = {}", fp.name);
}
#[test]
fn firefox_130_resolves_via_era_fallback() {
let fp = era_for(Browser::Firefox, 130, BrowserOs::Linux).expect("era resolves");
assert!(fp.name.starts_with("firefox_"), "name = {}", fp.name);
}
#[test]
fn chromium_122_uses_chrome_era() {
let fp = era_for(Browser::Chromium, 122, BrowserOs::Linux).expect("era resolves");
assert!(fp.name.starts_with("chrome_"), "name = {}", fp.name);
}
#[test]
fn safari_18_resolves_via_fallback() {
let fp = era_for(Browser::Safari, 18, BrowserOs::MacOs).expect("era resolves");
assert!(fp.name.starts_with("safari_"), "name = {}", fp.name);
}
}