LibURL: Expose rust host parsing over FFI

To be used instead of the C++ implementation.
This commit is contained in:
Shannon Booth 2026-04-05 16:55:02 +02:00 committed by Shannon Booth
parent b624276e72
commit 57cc7b04ee
6 changed files with 93 additions and 23 deletions

View file

@ -534,6 +534,9 @@ static ErrorOr<String> domain_to_ascii(StringView domain, bool be_strict)
// https://url.spec.whatwg.org/#concept-host-parser
Optional<Host> Parser::parse_host(StringView input, bool is_opaque)
{
#ifdef ENABLE_RUST
return RustIntegration::parse_host(input, is_opaque);
#endif
// 1. If input starts with U+005B ([), then:
if (input.starts_with('[')) {
// 1. If input does not end with U+005D (]), IPv6-unclosed validation error, return failure.

View file

@ -17,6 +17,7 @@ use crate::url::Url;
use crate::url::basic_parse;
use crate::url::basic_parse_into;
use crate::url::is_special_scheme;
use crate::url::parse_host;
#[repr(C)]
#[derive(Clone, Copy)]
@ -91,6 +92,7 @@ pub struct RustBasicParseOptions {
}
pub type FfiUrlResultFn = unsafe extern "C" fn(*mut c_void, *const RustFfiUrl);
pub type FfiHostResultFn = unsafe extern "C" fn(*mut c_void, *const FfiUrlHost);
fn abort_on_panic<F: FnOnce() -> R, R>(f: F) -> R {
match catch_unwind(AssertUnwindSafe(f)) {
@ -287,3 +289,33 @@ pub unsafe extern "C" fn rust_url_basic_parse(
did_succeed
})
}
/// # Safety
/// `input` must be valid for `input_length` bytes.
/// `on_complete` is called exactly once with either a host result or null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_url_parse_host(
input: *const u8,
input_length: usize,
is_opaque: bool,
ctx: *mut c_void,
on_complete: FfiHostResultFn,
) -> bool {
abort_on_panic(|| {
// SAFETY: caller guarantees input is valid.
let input_bytes = unsafe { std::slice::from_raw_parts(input, input_length) };
let input_str = String::from_utf8_lossy(input_bytes);
let Some(host) = parse_host(&input_str, is_opaque) else {
// SAFETY: on_complete is a valid function pointer; ctx is caller-provided.
unsafe { on_complete(ctx, std::ptr::null()) };
return false;
};
let ffi_result = host_to_ffi(Some(&host));
// SAFETY: ffi_result borrows from host, which lives until the callback returns.
unsafe { on_complete(ctx, &raw const ffi_result) };
true
})
}

View file

@ -521,7 +521,7 @@ fn ends_in_a_number_checker(input: &str) -> bool {
}
// https://url.spec.whatwg.org/#concept-host-parser
pub(super) fn parse_host(input: &str, is_opaque: bool) -> Option<Host> {
pub(crate) fn parse_host(input: &str, is_opaque: bool) -> Option<Host> {
// 1. If input starts with U+005B ([), then:
if input.starts_with('[') {
// 1. If input does not end with U+005D (]), IPv6-unclosed validation error, return failure.

View file

@ -12,6 +12,7 @@ mod scheme;
mod serialize;
mod types;
pub(crate) use self::host::parse_host;
pub(crate) use self::scheme::default_port_for_scheme;
pub(crate) use self::scheme::is_special_scheme;
pub(crate) use self::scheme::special_schemes;

View file

@ -174,6 +174,32 @@ static FFI::FfiUrlHost host_to_ffi(Optional<Host> const& host)
return result;
}
static Optional<Host> host_from_ffi(FFI::FfiUrlHost const& ffi)
{
if (!ffi.has_host)
return {};
switch (ffi.kind) {
case FFI::RustUrlHostKind::String:
return Host(string_from_ffi({ ffi.string_data, ffi.string_length }));
case FFI::RustUrlHostKind::Ipv4: {
u32 const n = (static_cast<u32>(ffi.ipv4[0]) << 24)
| (static_cast<u32>(ffi.ipv4[1]) << 16)
| (static_cast<u32>(ffi.ipv4[2]) << 8)
| static_cast<u32>(ffi.ipv4[3]);
return Host(IPv4Address(NetworkOrdered<u32>(n)));
}
case FFI::RustUrlHostKind::Ipv6: {
Array<u16, 8> pieces;
for (size_t i = 0; i < 8; i++)
pieces[i] = (static_cast<u16>(ffi.ipv6[i * 2]) << 8) | ffi.ipv6[(i * 2) + 1];
return Host(IPv6Address(pieces));
}
}
VERIFY_NOT_REACHED();
}
static Optional<URL> url_from_ffi(FFI::RustFfiUrl const& ffi)
{
URL url;
@ -181,28 +207,8 @@ static Optional<URL> url_from_ffi(FFI::RustFfiUrl const& ffi)
url.set_username(string_from_ffi(ffi.username));
url.set_password(string_from_ffi(ffi.password));
if (ffi.host.has_host) {
switch (ffi.host.kind) {
case FFI::RustUrlHostKind::String:
url.set_host(Host(string_from_ffi({ ffi.host.string_data, ffi.host.string_length })));
break;
case FFI::RustUrlHostKind::Ipv4: {
u32 const n = (static_cast<u32>(ffi.host.ipv4[0]) << 24)
| (static_cast<u32>(ffi.host.ipv4[1]) << 16)
| (static_cast<u32>(ffi.host.ipv4[2]) << 8)
| static_cast<u32>(ffi.host.ipv4[3]);
url.set_host(Host(IPv4Address(NetworkOrdered<u32>(n))));
break;
}
case FFI::RustUrlHostKind::Ipv6: {
Array<u16, 8> pieces;
for (size_t i = 0; i < 8; i++)
pieces[i] = (static_cast<u16>(ffi.host.ipv6[i * 2]) << 8) | ffi.host.ipv6[(i * 2) + 1];
url.set_host(Host(IPv6Address(pieces)));
break;
}
}
}
if (auto host = host_from_ffi(ffi.host); host.has_value())
url.set_host(host.release_value());
if (ffi.has_port)
url.set_port(ffi.port);
@ -388,6 +394,10 @@ struct ParseCallbackCtx {
URL* url_inout;
};
struct HostParseCallbackCtx {
Optional<Host>* result;
};
static void on_basic_parse_complete(void* ctx_ptr, FFI::RustFfiUrl const* ffi_result)
{
auto* ctx = static_cast<ParseCallbackCtx*>(ctx_ptr);
@ -398,6 +408,29 @@ static void on_basic_parse_complete(void* ctx_ptr, FFI::RustFfiUrl const* ffi_re
*ctx->url_inout = **ctx->result;
}
static void on_parse_host_complete(void* ctx_ptr, FFI::FfiUrlHost const* ffi_result)
{
auto* ctx = static_cast<HostParseCallbackCtx*>(ctx_ptr);
if (!ffi_result)
return;
*ctx->result = host_from_ffi(*ffi_result);
}
Optional<Host> parse_host(StringView input, bool is_opaque)
{
Optional<Host> result;
HostParseCallbackCtx ctx { .result = &result };
bool const did_succeed = FFI::rust_url_parse_host(
reinterpret_cast<uint8_t const*>(input.characters_without_null_termination()),
input.length(),
is_opaque,
&ctx,
on_parse_host_complete);
if (!did_succeed)
return {};
return result;
}
Optional<URL> parse_basic_url(StringView input, Optional<URL const&> base_url, URL* url, Optional<Parser::State> state_override, Optional<StringView> encoding)
{
auto const state_override_from_cpp = [](Parser::State state) {

View file

@ -19,6 +19,7 @@
namespace URL::RustIntegration {
Optional<URL> parse_basic_url(StringView input, Optional<URL const&> base_url = {}, URL* url = nullptr, Optional<Parser::State> state_override = {}, Optional<StringView> encoding = {});
Optional<Host> parse_host(StringView input, bool is_opaque = false);
class URLPattern {
public: