liburlx 0.2.2

A memory-safe URL transfer library — idiomatic Rust reimplementation of libcurl
Documentation
//! Throughput benchmarks for liburlx parsers.

#![allow(missing_docs, unused_results)]

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn bench_url_parsing(c: &mut Criterion) {
    let mut group = c.benchmark_group("url_parsing");

    group.bench_function("simple_http", |b| {
        b.iter(|| liburlx::Url::parse(black_box("http://example.com/")));
    });

    group.bench_function("https_with_path", |b| {
        b.iter(|| {
            liburlx::Url::parse(black_box("https://example.com/api/v1/users?page=1&limit=50"))
        });
    });

    group.bench_function("with_credentials", |b| {
        b.iter(|| liburlx::Url::parse(black_box("http://admin:secret@example.com:8080/path")));
    });

    group.bench_function("complex_url", |b| {
        b.iter(|| {
            liburlx::Url::parse(black_box(
                "https://user:pass@sub.example.com:8443/api/v2/resource?key=value&foo=bar#section",
            ))
        });
    });

    group.bench_function("no_scheme_default", |b| {
        b.iter(|| liburlx::Url::parse(black_box("example.com/path")));
    });

    group.finish();
}

fn bench_cookie_jar(c: &mut Criterion) {
    let mut group = c.benchmark_group("cookie_jar");

    group.bench_function("store_simple", |b| {
        b.iter(|| {
            let mut jar = liburlx::CookieJar::new();
            jar.store_cookies(
                black_box(&["session=abc123"]),
                black_box("example.com"),
                black_box("/"),
                true,
            );
        });
    });

    group.bench_function("store_with_attributes", |b| {
        b.iter(|| {
            let mut jar = liburlx::CookieJar::new();
            jar.store_cookies(
                black_box(&[
                    "session=abc123; Path=/api; Domain=example.com; Secure; HttpOnly; Max-Age=3600",
                ]),
                black_box("example.com"),
                black_box("/"),
                true,
            );
        });
    });

    group.bench_function("lookup_10_cookies", |b| {
        let mut jar = liburlx::CookieJar::new();
        for i in 0..10 {
            jar.store_cookies(&[&format!("key{i}=value{i}")], "example.com", "/", true);
        }
        b.iter(|| jar.cookie_header(black_box("example.com"), black_box("/api"), black_box(false)));
    });

    group.bench_function("lookup_100_cookies", |b| {
        let mut jar = liburlx::CookieJar::new();
        for i in 0..100 {
            jar.store_cookies(&[&format!("key{i}=value{i}")], "example.com", "/", true);
        }
        b.iter(|| jar.cookie_header(black_box("example.com"), black_box("/api"), black_box(false)));
    });

    group.finish();
}

fn bench_hsts_cache(c: &mut Criterion) {
    let mut group = c.benchmark_group("hsts_cache");

    group.bench_function("store_and_lookup", |b| {
        b.iter(|| {
            let mut cache = liburlx::HstsCache::new();
            cache.store(black_box("example.com"), black_box("max-age=31536000; includeSubDomains"));
            cache.should_upgrade(black_box("sub.example.com"))
        });
    });

    group.bench_function("lookup_miss_10_entries", |b| {
        let mut cache = liburlx::HstsCache::new();
        for i in 0..10 {
            cache.store(&format!("host{i}.com"), "max-age=31536000");
        }
        b.iter(|| cache.should_upgrade(black_box("unknown.com")));
    });

    group.bench_function("lookup_hit_subdomain", |b| {
        let mut cache = liburlx::HstsCache::new();
        cache.store("example.com", "max-age=31536000; includeSubDomains");
        b.iter(|| cache.should_upgrade(black_box("deep.sub.example.com")));
    });

    group.finish();
}

fn bench_dns_cache(c: &mut Criterion) {
    use std::net::{IpAddr, Ipv4Addr, SocketAddr};

    let mut group = c.benchmark_group("dns_cache");

    group.bench_function("put_and_get", |b| {
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 443);
        b.iter(|| {
            let mut cache = liburlx::DnsCache::new();
            cache.put(black_box("example.com"), black_box(443), vec![addr]);
            let _ = cache.get(black_box("example.com"), black_box(443)).is_some();
        });
    });

    group.bench_function("get_hit_10_entries", |b| {
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 443);
        let mut cache = liburlx::DnsCache::new();
        for i in 0..10 {
            cache.put(&format!("host{i}.example.com"), 443, vec![addr]);
        }
        cache.put("target.example.com", 443, vec![addr]);
        b.iter(|| cache.get(black_box("target.example.com"), black_box(443)));
    });

    group.bench_function("get_miss", |b| {
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 443);
        let mut cache = liburlx::DnsCache::new();
        for i in 0..10 {
            cache.put(&format!("host{i}.example.com"), 443, vec![addr]);
        }
        b.iter(|| cache.get(black_box("unknown.com"), black_box(443)));
    });

    group.finish();
}

fn bench_response_header(c: &mut Criterion) {
    use std::collections::HashMap;

    let mut group = c.benchmark_group("response_header");

    group.bench_function("lookup_lowercase", |b| {
        let mut headers = HashMap::new();
        headers.insert("content-type".to_string(), "text/html".to_string());
        headers.insert("content-length".to_string(), "1234".to_string());
        headers.insert("x-request-id".to_string(), "abc-123".to_string());
        let resp =
            liburlx::Response::new(200, headers, Vec::new(), "http://example.com".to_string());
        b.iter(|| resp.header(black_box("content-type")));
    });

    group.bench_function("lookup_mixed_case", |b| {
        let mut headers = HashMap::new();
        headers.insert("content-type".to_string(), "text/html".to_string());
        headers.insert("content-length".to_string(), "1234".to_string());
        let resp =
            liburlx::Response::new(200, headers, Vec::new(), "http://example.com".to_string());
        b.iter(|| resp.header(black_box("Content-Type")));
    });

    group.finish();
}

fn bench_cookie_domain_matching(c: &mut Criterion) {
    let mut group = c.benchmark_group("cookie_domain_match");

    group.bench_function("exact_match", |b| {
        let mut jar = liburlx::CookieJar::new();
        jar.store_cookies(&["key=val"], "example.com", "/", true);
        b.iter(|| jar.cookie_header(black_box("example.com"), black_box("/"), black_box(false)));
    });

    group.bench_function("subdomain_match", |b| {
        let mut jar = liburlx::CookieJar::new();
        jar.store_cookies(&["key=val; Domain=example.com"], "www.example.com", "/", true);
        b.iter(|| {
            jar.cookie_header(
                black_box("sub.deep.example.com"),
                black_box("/path"),
                black_box(false),
            )
        });
    });

    group.bench_function("no_match_1000_cookies", |b| {
        let mut jar = liburlx::CookieJar::new();
        for i in 0..1000 {
            jar.store_cookies(&[&format!("k{i}=v{i}")], &format!("host{i}.com"), "/", true);
        }
        b.iter(|| jar.cookie_header(black_box("other.com"), black_box("/"), black_box(false)));
    });

    group.finish();
}

fn bench_http_response_parsing(c: &mut Criterion) {
    let mut group = c.benchmark_group("http_response_parsing");

    let simple_response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";

    group.bench_function("simple_200", |b| {
        b.iter(|| {
            liburlx::protocol::http::h1::parse_response(
                black_box(simple_response),
                black_box("http://example.com"),
                black_box(false),
            )
        });
    });

    let response_with_headers = b"HTTP/1.1 200 OK\r\n\
        Content-Type: text/html; charset=utf-8\r\n\
        Content-Length: 13\r\n\
        Server: nginx/1.24\r\n\
        X-Request-Id: abc-123-def\r\n\
        Cache-Control: max-age=3600, public\r\n\
        Set-Cookie: session=abc; Path=/; HttpOnly; Secure\r\n\
        \r\n\
        Hello, World!";

    group.bench_function("with_many_headers", |b| {
        b.iter(|| {
            liburlx::protocol::http::h1::parse_response(
                black_box(response_with_headers),
                black_box("http://example.com"),
                black_box(false),
            )
        });
    });

    let redirect_response = b"HTTP/1.1 301 Moved Permanently\r\n\
        Location: https://www.example.com/\r\n\
        Content-Length: 0\r\n\
        \r\n";

    group.bench_function("redirect_301", |b| {
        b.iter(|| {
            liburlx::protocol::http::h1::parse_response(
                black_box(redirect_response),
                black_box("http://example.com"),
                black_box(false),
            )
        });
    });

    group.finish();
}

fn bench_multipart_form(c: &mut Criterion) {
    let mut group = c.benchmark_group("multipart_form");

    group.bench_function("encode_text_fields", |b| {
        b.iter(|| {
            let mut form = liburlx::MultipartForm::new();
            form.field(black_box("name"), black_box("John Doe"));
            form.field(black_box("email"), black_box("john@example.com"));
            form.field(black_box("message"), black_box("Hello, World!"));
            form.encode()
        });
    });

    group.bench_function("encode_file_data", |b| {
        let data = vec![0u8; 1024];
        b.iter(|| {
            let mut form = liburlx::MultipartForm::new();
            form.file_data(black_box("file"), black_box("test.bin"), black_box(&data));
            form.encode()
        });
    });

    group.finish();
}

criterion_group!(
    benches,
    bench_url_parsing,
    bench_cookie_jar,
    bench_hsts_cache,
    bench_dns_cache,
    bench_response_header,
    bench_cookie_domain_matching,
    bench_http_response_parsing,
    bench_multipart_form,
);
criterion_main!(benches);