use dactyl::{
NiceSeparator,
NiceU32,
};
use oxford_join::JoinFmt;
use serde::Deserialize;
use std::{
collections::BTreeMap,
fmt,
fs::File,
io::Write,
num::NonZeroU32,
path::PathBuf,
};
#[cfg(not(docsrs))]
use std::{
fs::Metadata,
path::Path,
};
#[cfg(not(docsrs))]
const DATA_URL: &str = "https://github.com/Fyrd/caniuse/raw/main/fulldata-json/data-2.0.json";
const DATA_FALLBACK: &str = "skel/data-2.0.json";
fn main() {
println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
let raw: Raw = serde_json::from_slice(&fetch()).expect("Unable to parse raw.");
let out: String = process(raw);
let cache = out_path("guff-browsers.rs");
File::create(cache)
.and_then(|mut f| f.write_all(out.as_bytes()).and_then(|_| f.flush()))
.expect("Unable to save browser data.");
}
#[cfg(docsrs)]
fn fetch() -> Vec<u8> {
std::fs::read(DATA_FALLBACK).expect("Unable to load browser data.")
}
#[cfg(not(docsrs))]
fn fetch() -> Vec<u8> {
let cache = out_path("guff-browsers.json");
if let Some(x) = try_cache(&cache) {
return x;
}
fetch_remote(&cache).unwrap_or_else(fetch_local)
}
#[cfg(not(docsrs))]
fn fetch_remote(cache: &Path) -> Option<Vec<u8>> {
let res = minreq::get(DATA_URL)
.with_header("user-agent", "Mozilla/5.0")
.with_timeout(30)
.send()
.ok()?;
if (200..=399).contains(&res.status_code) {
let out = res.into_bytes();
if ! out.is_empty() {
let _res = File::create(cache)
.and_then(|mut f| f.write_all(&out).and_then(|_| f.flush()));
return Some(out);
}
}
None
}
#[cfg(not(docsrs))]
fn fetch_local() -> Vec<u8> {
let out = std::fs::read(DATA_FALLBACK).expect("Unable to load browser data.");
println!("cargo:warning=Unable to download current caniuse data; building with bundled copy instead.");
out
}
fn process(raw: Raw) -> String {
use fmt::Write;
let all: BTreeMap<Agent, Vec<Versions>> = raw.agents.into_iter()
.filter_map(|(k, mut v)| {
let agent = Agent::try_from(k.as_str()).ok()?;
v.version_list.sort_by(|a, b| b.era.cmp(&a.era));
let releases: Vec<Versions> = v.version_list.into_iter()
.filter_map(|v2| {
v2.release_date?;
let (parcel, major) = parse_version(&v2.version)?;
Some(Versions(parcel, major))
})
.collect();
Some((agent, releases))
})
.collect();
let mut out = String::with_capacity(256 * all.len()); for (k, v) in all {
writeln!(
&mut out,
"#[expect(clippy::missing_docs_in_private_items, reason = \"List is auto-generated.\")]\nconst {}: [(u32, u32); {}] = [{}];",
k.as_str(),
v.len(),
JoinFmt::new(v.into_iter(), ", "),
).unwrap();
}
out
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
enum Agent {
Android,
Chrome,
Edge,
Firefox,
Ie,
Ios,
Opera,
Safari,
Samsung,
}
impl Agent {
const fn as_str(self) -> &'static str {
match self {
Self::Android => "ANDROID",
Self::Chrome => "CHROME",
Self::Edge => "EDGE",
Self::Firefox => "FIREFOX",
Self::Ie => "IE",
Self::Ios => "IOS",
Self::Opera => "OPERA",
Self::Safari => "SAFARI",
Self::Samsung => "SAMSUNG",
}
}
}
impl TryFrom<&str> for Agent {
type Error = ();
fn try_from(src: &str) -> Result<Self, Self::Error> {
match src.trim() {
"android" => Ok(Self::Android),
"chrome" => Ok(Self::Chrome),
"edge" => Ok(Self::Edge),
"firefox" => Ok(Self::Firefox),
"ie" => Ok(Self::Ie),
"ios_saf" => Ok(Self::Ios),
"opera" => Ok(Self::Opera),
"safari" => Ok(Self::Safari),
"samsung" => Ok(Self::Samsung),
_ => Err(()),
}
}
}
#[derive(Deserialize)]
struct Raw {
agents: BTreeMap<String, RawAgent>
}
#[derive(Deserialize)]
struct RawAgent {
version_list: Vec<RawAgentVersions>,
}
#[derive(Default, Deserialize)]
#[serde(default)]
struct RawAgentVersions {
version: String,
release_date: Option<u32>,
era: i32,
}
#[derive(Clone, Copy)]
struct Versions(u32, u32);
impl fmt::Display for Versions {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"({}, {})",
NiceU32::with_separator(self.0, NiceSeparator::Underscore),
NiceU32::with_separator(self.1, NiceSeparator::Underscore),
)
}
}
fn out_path(name: &str) -> PathBuf {
let dir = std::env::var("OUT_DIR").expect("Missing OUT_DIR.");
let mut out = std::fs::canonicalize(dir).expect("Missing OUT_DIR.");
out.push(name);
out
}
fn parse_version(src: &str) -> Option<(u32, u32)> {
use dactyl::traits::BytesToUnsigned;
let mut version = src.split('-')
.next()?
.split('.');
let major = version.next().and_then(|v| u32::btou(v.as_bytes()))?;
let minor = version.next().and_then(|v| u32::btou(v.as_bytes())).unwrap_or(0);
let patch = version.next().and_then(|v| u32::btou(v.as_bytes())).unwrap_or(0);
let v: u32 = ((major & 0xff) << 16) | ((minor & 0xff) << 8) | (patch & 0xff);
let v = NonZeroU32::new(v)?;
Some((v.get(), major))
}
#[cfg(not(docsrs))]
fn try_cache(path: &Path) -> Option<Vec<u8>> {
std::fs::metadata(path)
.ok()
.filter(Metadata::is_file)
.and_then(|meta| meta.modified().ok())
.and_then(|time| time.elapsed().ok().filter(|secs| secs.as_secs() < 3600))
.and_then(|_| std::fs::read(path).ok())
}