use dactyl::NiceU32;
use serde::Deserialize;
use std::{
collections::HashMap,
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";
pub 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>> {
use std::io::Read;
let res = ureq::get(DATA_URL)
.set("user-agent", "Mozilla/5.0")
.call()
.ok()?;
let mut out: Vec<u8> = Vec::new();
res.into_reader().read_to_end(&mut out).ok()?;
if out.is_empty() { None }
else {
let _res = File::create(cache)
.and_then(|mut f| f.write_all(&out).and_then(|_| f.flush()));
Some(out)
}
}
#[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 {
let all: HashMap<Agent, Vec<(u32, u32)>> = 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<(u32, u32)> = v.version_list.into_iter()
.filter_map(|v2| {
v2.release_date?;
let (parcel, major) = parse_version(&v2.version)?;
Some((parcel, major))
})
.collect();
Some((agent, releases))
})
.collect();
let mut all: Vec<String> = all.into_iter()
.map(|(k, v)| {
format!(
"const {}: [(u32, u32); {}] = [{}];",
k.as_str(),
v.len(),
v.into_iter()
.map(|(a, b)| format!(
"({}, {})",
NiceU32::with_separator(a, b'_'),
NiceU32::with_separator(b, b'_'),
))
.collect::<Vec<String>>()
.join(", ")
)
})
.collect();
all.sort_unstable();
all.join("\n")
}
#[derive(Deserialize)]
struct Raw {
agents: HashMap<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(Debug, Clone, Copy, Eq, Hash, PartialEq)]
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(()),
}
}
}
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())
}