From 0ddca754a481cdc47db772dd10b8f0b3847172fd Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 11 Jun 2026 19:08:19 +0300 Subject: [PATCH] `URL`: move resolver functions to Rust Implements `url_resolve_with_encoding` and `url_resolve_without_encoding` in Rust; that way, we don't pay the cost of extra `Box`es we allocate during resolving. --- src/html5ever/url.rs | 172 +++++++++++++++++++++++++++++++++++++++++++ src/sys/url.zig | 6 ++ 2 files changed, 178 insertions(+) diff --git a/src/html5ever/url.rs b/src/html5ever/url.rs index 55d14c2ad..804b6037b 100644 --- a/src/html5ever/url.rs +++ b/src/html5ever/url.rs @@ -19,6 +19,8 @@ // host string becomes its punycode form, or an error. use ::url::Url; +use encoding_rs::{EncoderResult, Encoding}; +use std::borrow::Cow; use std::os::raw::c_uchar; use std::slice; @@ -280,6 +282,11 @@ pub struct OwnedString { pub len: usize, } +const EMPTY_OWNED_STRING: OwnedString = OwnedString { + ptr: std::ptr::null_mut(), + len: 0, +}; + #[no_mangle] pub unsafe extern "C" fn free_owned_string(owned: OwnedString) { if owned.ptr.is_null() || owned.len == 0 { @@ -568,3 +575,168 @@ pub unsafe extern "C" fn url_get_href( *out_ptr = href.as_ptr(); *out_len = href.len(); } + +fn encode_query_ncr(encoding: &'static Encoding, s: &str) -> Cow<'static, [u8]> { + // fast path: fully mappable + let (out, _, had_errors) = encoding.encode(s); + if !had_errors { + return Cow::Owned(out.into_owned()); + } + + let mut encoder = encoding.new_encoder(); + let mut result = Vec::with_capacity(s.len() * 2); + let mut input = s; + loop { + let needed = encoder + .max_buffer_length_from_utf8_without_replacement(input.len()) + .unwrap(); + let start = result.len(); + result.resize(start + needed, 0); + let (r, read, written) = + encoder.encode_from_utf8_without_replacement(input, &mut result[start..], true); + result.truncate(start + written); + input = &input[read..]; + match r { + EncoderResult::InputEmpty => break, + EncoderResult::Unmappable(c) => { + result.extend_from_slice(format!("%26%23{}%3B", c as u32).as_bytes()); + } + // Output was sized with max_buffer_length, so it cannot run out. + EncoderResult::OutputFull => unreachable!(), + } + } + Cow::Owned(result) +} + +#[no_mangle] +pub unsafe extern "C" fn url_resolve_with_encoding( + base_ptr: *const c_uchar, + base_len: usize, + input_ptr: *const c_uchar, + input_len: usize, + enc_ptr: *const c_uchar, + enc_len: usize, + err: *mut i32, +) -> OwnedString { + let base_slice = match str_from(base_ptr, base_len) { + Some(s) => s, + None => { + *err = -1; + return EMPTY_OWNED_STRING; + } + }; + // An empty base means the input must be an absolute URL. + let base = if base_slice.is_empty() { + None + } else { + match Url::parse(base_slice) { + Ok(u) => Some(u), + Err(_) => { + *err = -1; + return EMPTY_OWNED_STRING; + } + } + }; + + let slice = match str_from(input_ptr, input_len) { + Some(s) => s, + None => { + *err = -1; + return EMPTY_OWNED_STRING; + } + }; + + let encoding_slice = match str_from(enc_ptr, enc_len) { + Some(s) => s, + None => { + *err = -1; + return EMPTY_OWNED_STRING; + } + }; + // Per the URL spec, queries use the document encoding's *output encoding*. + let encoding = Encoding::for_label(encoding_slice.as_bytes()) + .map(|encoding| encoding.output_encoding()) + .filter(|&encoding| encoding != encoding_rs::UTF_8); + + let result = match encoding { + Some(encoding) => Url::options() + .base_url(base.as_ref()) + .encoding_override(Some(&move |s| encode_query_ncr(encoding, s))) + .parse(slice), + // Fallback to default. + None => match &base { + Some(base) => base.join(slice), + None => Url::parse(slice), + }, + }; + + match result { + Ok(url) => { + *err = 0; + let s = String::from(url); // Moves the serialization, no copy. + let len = s.len(); + let ptr = Box::into_raw(s.into_bytes().into_boxed_slice()) as *mut c_uchar; + OwnedString { ptr, len } + } + Err(_) => { + *err = -1; + EMPTY_OWNED_STRING + } + } +} + +/// Similar to url_parse_with_base; returns a href instead. +#[no_mangle] +pub unsafe extern "C" fn url_resolve_without_encoding( + base_ptr: *const c_uchar, + base_len: usize, + input_ptr: *const c_uchar, + input_len: usize, + err: *mut i32, +) -> OwnedString { + let base_slice = match str_from(base_ptr, base_len) { + Some(s) => s, + None => { + *err = -1; + return EMPTY_OWNED_STRING; + } + }; + // An empty base means the input must be an absolute URL. + let base = if base_slice.is_empty() { + None + } else { + match Url::parse(base_slice) { + Ok(u) => Some(u), + Err(_) => { + *err = -1; + return EMPTY_OWNED_STRING; + } + } + }; + + let input = match str_from(input_ptr, input_len) { + Some(s) => s, + None => { + *err = -1; + return EMPTY_OWNED_STRING; + } + }; + + let result = match &base { + Some(base) => base.join(input), + None => Url::parse(input), + }; + match result { + Ok(url) => { + *err = 0; + let s = String::from(url); // Moves the serialization, no copy. + let len = s.len(); + let ptr = Box::into_raw(s.into_bytes().into_boxed_slice()) as *mut c_uchar; + OwnedString { ptr, len } + } + Err(_) => { + *err = -1; + EMPTY_OWNED_STRING + } + } +} diff --git a/src/sys/url.zig b/src/sys/url.zig index dd0d46928..506225b27 100644 --- a/src/sys/url.zig +++ b/src/sys/url.zig @@ -83,6 +83,12 @@ pub extern "c" fn url_get_fragment(url: *const Url, out_ptr: *[*]const u8, out_l pub extern "c" fn url_set_query(url: *Url, ptr: [*]const u8, len: usize) i32; pub extern "c" fn url_set_query_to_null(url: *Url) void; pub extern "c" fn url_get_query(url: *const Url, out_ptr: *[*]const u8, out_len: *usize) i32; +/// `err` is `0` if there's no error. +/// Returned `OwnedString` doesn't have a sentinel; callers must be aware of that. +pub extern "c" fn url_resolve_with_encoding(base_ptr: [*]const u8, base_len: usize, input_ptr: [*]const u8, input_len: usize, enc_ptr: [*]const u8, enc_len: usize, err: *i32) OwnedString; +/// `err` is `0` if there's no error. +/// Returned `OwnedString` doesn't have a sentinel; callers must be aware of that. +pub extern "c" fn url_resolve_without_encoding(base_ptr: [*]const u8, base_len: usize, input_ptr: [*]const u8, input_len: usize, err: *i32) OwnedString; extern "c" fn url_get_port(url: *const Url) i32; pub inline fn urlGetPort(url: *const Url) ?u16 {