leyline-http 0.1.5

HTTP client that impersonates Chrome, Firefox, and Safari on the wire: TLS (JA3/JA4), HTTP/2, and HTTP/3 fingerprints
Documentation
use std::collections::BTreeMap;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};

#[path = "build/brand.rs"]
mod brand;
#[path = "build/browser.rs"]
mod browser;
#[path = "build/entities.rs"]
mod entities;
#[path = "build/header_style.rs"]
mod header_style;
#[path = "build/ids.rs"]
mod ids;
#[path = "src/profile/preset/kind.rs"]
mod preset_kind;

type BuildResult<T> = Result<T, Box<dyn Error>>;

fn is_ident(name: &str) -> bool {
    name.starts_with(|c: char| c.is_ascii_uppercase())
        && name.chars().all(|c| c.is_ascii_alphanumeric())
}

type Shapes = BTreeMap<String, header_style::Shape>;

fn main() -> BuildResult<()> {
    let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
    let root = manifest.join("profiles");
    println!("cargo:rerun-if-changed={}", root.display());
    println!("cargo:rerun-if-changed=build");
    println!("cargo:rerun-if-changed=src/profile/preset/kind.rs");
    let entity_data = manifest.join("data").join("entities.tsv");
    println!("cargo:rerun-if-changed={}", entity_data.display());
    let out = PathBuf::from(std::env::var("OUT_DIR")?);
    let rows = write_browsers(&root, &out)?;
    let shapes = write_header_styles(&root, &out, &rows)?;
    write_brands(&root, &out, &shapes)?;
    write_entities(&entity_data, &out)
}

fn write_browsers(root: &Path, out: &Path) -> BuildResult<Vec<browser::Row>> {
    let families: browser::Families =
        toml::from_str(&fs::read_to_string(root.join("families.toml"))?)?;
    let mut rows = browser::load_rows(root, &families)?;
    browser::check_unique(&rows, &families)?;
    let browser_ids: BTreeMap<String, u32> =
        toml::from_str(&fs::read_to_string(root.join("browser_ids.toml"))?)?;
    ids::assign(&mut rows, &browser_ids)?;
    fs::write(out.join("browser.rs"), browser::render(&rows, &families)?)?;
    Ok(rows)
}

fn write_header_styles(root: &Path, out: &Path, rows: &[browser::Row]) -> BuildResult<Shapes> {
    let shapes: Shapes = toml::from_str(&fs::read_to_string(root.join("headers.toml"))?)?;
    header_style::check_shapes(&shapes)?;
    check_row_styles(rows, &shapes)?;
    fs::write(
        out.join("header_style.rs"),
        header_style::render_shapes(&shapes)?,
    )?;
    Ok(shapes)
}

fn check_row_styles(rows: &[browser::Row], shapes: &Shapes) -> BuildResult<()> {
    let missing = rows.iter().find_map(|row| {
        row.meta
            .header_style
            .as_deref()
            .filter(|s| !shapes.contains_key(*s))
            .map(|style| (row, style))
    });
    match missing {
        Some((row, style)) => Err(format!(
            "{}: header_style {style:?} is not in headers.toml",
            row.path
        )
        .into()),
        None => Ok(()),
    }
}

fn write_brands(root: &Path, out: &Path, shapes: &Shapes) -> BuildResult<()> {
    let brands: BTreeMap<String, brand::Brand> =
        toml::from_str(&fs::read_to_string(root.join("brands.toml"))?)?;
    brand::check_brands(&brands, shapes)?;
    fs::write(
        out.join("chromium_brand.rs"),
        brand::render_brands(&brands)?,
    )?;
    Ok(())
}

fn write_entities(data: &Path, out: &Path) -> BuildResult<()> {
    fs::write(
        out.join("entities.rs"),
        entities::render(&fs::read_to_string(data)?)?,
    )?;
    Ok(())
}