use std::{borrow::Cow, num::NonZero, sync::OnceLock};
use rustc_hash::FxHashMap;
use crate::data::{BrowserName, decode_browser_name};
use compression::{LazyData, read_varint, read_zigzag};
pub mod compression;
pub mod features;
pub mod region;
pub const ANDROID_EVERGREEN_FIRST: f32 = 37.0;
pub const OP_MOB_BLINK_FIRST: u16 = 14;
#[derive(Clone, Debug)]
pub struct BrowserStat {
pub name: Cow<'static, str>,
pub version_list: Vec<VersionDetail>,
}
#[derive(Debug, Clone)]
pub struct VersionDetail(
pub Cow<'static, str>,
pub f32,
pub Option<NonZero<i64>>,
);
impl VersionDetail {
pub fn version(&self) -> &str {
&self.0
}
pub fn global_usage(&self) -> f32 {
self.1
}
pub fn release_date(&self) -> Option<NonZero<i64>> {
self.2
}
}
pub type CaniuseData = FxHashMap<BrowserName, BrowserStat>;
static VERSION_TABLE: LazyData<Vec<String>> =
LazyData::new(include_bytes!("../generated/caniuse_version_table.bin.deflate"));
pub(crate) fn version_table() -> &'static [String] {
VERSION_TABLE.get()
}
struct CaniuseCore {
browsers: CaniuseData,
global_usage: Vec<(u8, u16, f32)>,
version_orders: Vec<Vec<u16>>,
}
fn core() -> &'static CaniuseCore {
static CORE: OnceLock<CaniuseCore> = OnceLock::new();
CORE.get_or_init(|| {
let data = compression::decompress_blob(include_bytes!(
"../generated/caniuse_browsers.bin.deflate"
));
decode_browsers(&data, version_table())
})
}
fn decode_browsers(data: &[u8], table: &'static [String]) -> CaniuseCore {
let mut pos = 0;
assert_eq!(data[pos], 2, "unsupported caniuse browsers data format");
pos += 1;
let date_unit = i64::from(u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()));
pos += 4;
let usage_count = read_varint(data, &mut pos);
let mut usage_table = Vec::with_capacity(usage_count);
for _ in 0..usage_count {
usage_table
.push(f32::from_bits(u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap())));
pos += 4;
}
let browser_count = usize::from(data[pos]);
pos += 1;
let ids = &data[pos..pos + browser_count];
pos += browser_count;
let version_counts: Vec<usize> =
(0..browser_count).map(|_| read_varint(data, &mut pos)).collect();
let released_counts: Vec<usize> =
(0..browser_count).map(|_| read_varint(data, &mut pos)).collect();
let total_versions: usize = version_counts.iter().sum();
let max_id = usize::from(ids.iter().copied().max().unwrap_or(0));
let mut version_orders: Vec<Vec<u16>> = vec![Vec::new(); max_id + 1];
for (&id, &count) in ids.iter().zip(&version_counts) {
version_orders[usize::from(id)] =
(0..count).map(|_| u16::try_from(read_varint(data, &mut pos)).unwrap()).collect();
}
let mut usages = Vec::with_capacity(total_versions);
for _ in 0..total_versions {
let index = read_varint(data, &mut pos);
usages.push(if index == 0 { 0.0 } else { usage_table[index - 1] });
}
let mut browsers = CaniuseData::default();
let mut flat_pairs = Vec::with_capacity(total_versions);
for (i, &id) in ids.iter().enumerate() {
let order = &version_orders[usize::from(id)];
let mut version_list = Vec::with_capacity(order.len());
let mut day = 0i64;
for (j, &index) in order.iter().enumerate() {
let date = if j < released_counts[i] {
day += read_zigzag(data, &mut pos);
NonZero::new(day * date_unit)
} else {
None
};
let version = Cow::Borrowed(table[usize::from(index)].as_str());
version_list.push(VersionDetail(version, usages[flat_pairs.len()], date));
flat_pairs.push((id, index));
}
let name = decode_browser_name(id);
browsers.insert(name.clone(), BrowserStat { name, version_list });
}
let global_usage_count = read_varint(data, &mut pos);
let mut global_usage = Vec::with_capacity(global_usage_count);
for _ in 0..global_usage_count {
let position = read_varint(data, &mut pos);
let (id, index) = flat_pairs[position];
global_usage.push((id, index, usages[position]));
}
assert_eq!(pos, data.len(), "trailing caniuse browsers data");
CaniuseCore { browsers, global_usage, version_orders }
}
pub fn caniuse_browsers() -> &'static CaniuseData {
&core().browsers
}
pub(crate) fn global_usage() -> &'static [(u8, u16, f32)] {
&core().global_usage
}
pub(crate) fn version_orders() -> &'static [Vec<u16>] {
&core().version_orders
}
pub fn browser_version_aliases()
-> &'static FxHashMap<Cow<'static, str>, FxHashMap<&'static str, &'static str>> {
static BROWSER_VERSION_ALIASES: OnceLock<
FxHashMap<Cow<'static, str>, FxHashMap<&'static str, &'static str>>,
> = OnceLock::new();
BROWSER_VERSION_ALIASES.get_or_init(|| {
let mut aliases = caniuse_browsers()
.iter()
.filter_map(|(name, stat)| {
let mut aliases = FxHashMap::<&str, &str>::default();
for version in &stat.version_list {
if let Some((bottom, top)) = version.version().split_once('-') {
aliases.insert(bottom, version.version());
aliases.insert(top, version.version());
}
}
if aliases.is_empty() { None } else { Some((name.clone(), aliases)) }
})
.collect::<FxHashMap<Cow<'static, str>, _>>();
aliases.insert(Cow::Borrowed("op_mob"), FxHashMap::from_iter([("59", "58")]));
aliases
})
}
fn android_to_desktop() -> &'static BrowserStat {
static ANDROID_TO_DESKTOP: OnceLock<BrowserStat> = OnceLock::new();
ANDROID_TO_DESKTOP.get_or_init(|| {
let chrome = &caniuse_browsers()["chrome"];
let android = &caniuse_browsers()["android"];
let chrome_skip_index = find_chrome_evergreen_start(chrome);
let mut version_list = Vec::new();
version_list.extend(
android
.version_list
.iter()
.filter(|version| is_legacy_android_version(version.version()))
.cloned(),
);
version_list.extend(chrome.version_list.iter().skip(chrome_skip_index).cloned());
BrowserStat { name: android.name.clone(), version_list }
})
}
#[inline]
fn is_legacy_android_version(version: &str) -> bool {
version.starts_with("2.")
|| version.starts_with("3.")
|| version.starts_with("4.")
|| version == "3"
|| version == "4"
}
fn find_chrome_evergreen_start(chrome: &BrowserStat) -> usize {
chrome
.version_list
.iter()
.position(|version| {
version.version().parse::<usize>().is_ok_and(|v| v == ANDROID_EVERGREEN_FIRST as usize)
})
.unwrap_or(0)
}
pub fn get_browser_stat(
name: &str,
mobile_to_desktop: bool,
) -> Option<(&'static str, &'static BrowserStat)> {
let normalized_name = if name.bytes().all(|b| b.is_ascii_lowercase()) {
get_browser_alias(name)
} else {
get_browser_alias_lowercase(name)
};
if mobile_to_desktop {
get_browser_stat_mobile_to_desktop(normalized_name.as_ref())
} else {
caniuse_browsers().get(&normalized_name).map(|stat| (stat.name.as_ref(), stat))
}
}
fn get_browser_stat_mobile_to_desktop(name: &str) -> Option<(&'static str, &'static BrowserStat)> {
match name {
"and_chr" => caniuse_browsers().get("chrome").map(|stat| ("and_chr", stat)),
"android" => Some(("android", android_to_desktop())), "and_ff" => caniuse_browsers().get("firefox").map(|stat| ("and_ff", stat)),
"ie_mob" => caniuse_browsers().get("ie").map(|stat| ("ie_mob", stat)),
_ => caniuse_browsers().get(name).map(|stat| (stat.name.as_ref(), stat)),
}
}
fn resolve_alias(name: &str) -> Option<&'static str> {
match name {
"fx" | "ff" => Some("firefox"),
"ios" => Some("ios_saf"),
"explorer" => Some("ie"),
"blackberry" => Some("bb"),
"explorermobile" => Some("ie_mob"),
"operamini" => Some("op_mini"),
"operamobile" => Some("op_mob"),
"chromeandroid" => Some("and_chr"),
"firefoxandroid" => Some("and_ff"),
"ucandroid" => Some("and_uc"),
"qqandroid" => Some("and_qq"),
_ => None,
}
}
#[cold]
fn get_browser_alias_lowercase(name: &str) -> Cow<'static, str> {
let lowercase = name.to_ascii_lowercase();
if let Some(alias) = resolve_alias(&lowercase) {
return Cow::Borrowed(alias);
}
if caniuse_browsers().contains_key(lowercase.as_str()) {
Cow::Owned(lowercase)
} else {
Cow::Owned(name.to_string())
}
}
fn get_browser_alias(name: &str) -> Cow<'static, str> {
if let Some(alias) = resolve_alias(name) {
return Cow::Borrowed(alias);
}
Cow::Owned(name.to_string())
}
pub fn to_desktop_name(name: &str) -> Option<&'static str> {
match name {
"and_chr" | "android" => Some("chrome"),
"and_ff" => Some("firefox"),
"ie_mob" => Some("ie"),
_ => None,
}
}
pub fn normalize_version<'a>(stat: &'static BrowserStat, version: &'a str) -> Option<Cow<'a, str>> {
if stat.version_list.iter().any(|v| v.version() == version) {
Some(Cow::Borrowed(version))
} else if let Some(version) =
browser_version_aliases().get(&stat.name).and_then(|aliases| aliases.get(version))
{
Some(Cow::Borrowed(version))
} else if stat.version_list.len() == 1 {
stat.version_list.first().map(|s| Cow::Owned(s.version().to_string()))
} else {
None
}
}