diff --git a/src/html5ever/Cargo.lock b/src/html5ever/Cargo.lock
index d1646acb1..72445a7c7 100644
--- a/src/html5ever/Cargo.lock
+++ b/src/html5ever/Cargo.lock
@@ -62,6 +62,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "html5ever"
version = "0.39.0"
@@ -192,6 +201,7 @@ dependencies = [
"tikv-jemalloc-ctl",
"tikv-jemallocator",
"typed-arena",
+ "url",
"xml5ever",
]
@@ -263,6 +273,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
[[package]]
name = "phf"
version = "0.13.1"
@@ -504,6 +520,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
[[package]]
name = "utf-8"
version = "0.7.6"
diff --git a/src/html5ever/Cargo.toml b/src/html5ever/Cargo.toml
index 2cc913cf1..216f21098 100644
--- a/src/html5ever/Cargo.toml
+++ b/src/html5ever/Cargo.toml
@@ -17,6 +17,7 @@ tikv-jemalloc-ctl = {version = "0.6.1", features = ["stats"]}
xml5ever = "0.39.0"
encoding_rs = "0.8"
idna = "1.1.0"
+url = "2.5.8"
[profile.release]
lto = true
diff --git a/src/html5ever/url.rs b/src/html5ever/url.rs
index 98a83b606..6c2699e83 100644
--- a/src/html5ever/url.rs
+++ b/src/html5ever/url.rs
@@ -18,6 +18,7 @@
// IDNA-2008 behavior diverged from the spec. Value-in / value-out: a UTF-8
// host string becomes its punycode form, or an error.
+use ::url::Url;
use std::os::raw::c_uchar;
use std::slice;
@@ -81,3 +82,357 @@ pub extern "C" fn lpurl_free(ptr: *mut c_uchar, len: usize) {
drop(Box::from_raw(slice));
}
}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_parse(ptr: *const c_uchar, len: usize, err: *mut i32) -> *mut Url {
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => {
+ *err = url::ParseError::EmptyHost as i32;
+ return std::ptr::null_mut();
+ }
+ };
+
+ match Url::parse(slice) {
+ Ok(url) => Box::into_raw(Box::new(url)),
+ Err(e) => {
+ *err = e as i32;
+ std::ptr::null_mut()
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_join(
+ base: *const Url,
+ ptr: *const c_uchar,
+ len: usize,
+ err: *mut i32,
+) -> *mut Url {
+ let base = unsafe { &*base };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => {
+ *err = url::ParseError::EmptyHost as i32;
+ return std::ptr::null_mut();
+ }
+ };
+
+ match base.join(slice) {
+ Ok(url) => Box::into_raw(Box::new(url)),
+ Err(e) => {
+ *err = e as i32;
+ std::ptr::null_mut()
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_can_parse(ptr: *const c_uchar, len: usize) -> bool {
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return false,
+ };
+
+ Url::parse(slice).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_can_parse_with_base(
+ base_ptr: *const c_uchar,
+ base_len: usize,
+ ptr: *const c_uchar,
+ len: usize,
+) -> bool {
+ let base_slice = match str_from(base_ptr, base_len) {
+ Some(s) => s,
+ None => return false,
+ };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return false,
+ };
+
+ match Url::parse(base_slice) {
+ Ok(url) => url.join(slice).is_ok(),
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_free(url: *mut Url) {
+ if url.is_null() {
+ return;
+ }
+ drop(Box::from_raw(url));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_to_string(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) {
+ let url = unsafe { &*url };
+ let s = url.as_str();
+ *out_ptr = s.as_ptr();
+ *out_len = s.len();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_username(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ match url.set_username(slice) {
+ Ok(()) => 0,
+ Err(()) => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_username(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) {
+ let url = unsafe { &*url };
+ let username = url.username();
+ *out_ptr = username.as_ptr();
+ *out_len = username.len();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_password(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ match url.set_password(Some(slice)) {
+ Ok(()) => 0,
+ Err(()) => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_password(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) -> i32 {
+ let url = unsafe { &*url };
+ match url.password() {
+ Some(password) => {
+ *out_ptr = password.as_ptr();
+ *out_len = password.len();
+ 0
+ }
+ None => -1,
+ }
+}
+
+#[repr(C)]
+pub struct OwnedString {
+ pub ptr: *mut c_uchar,
+ pub len: usize,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn free_owned_string(owned: OwnedString) {
+ if owned.ptr.is_null() || owned.len == 0 {
+ return;
+ }
+
+ unsafe {
+ let slice = std::ptr::slice_from_raw_parts_mut(owned.ptr, owned.len);
+ drop(Box::from_raw(slice));
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_origin(url: *const Url) -> OwnedString {
+ let url = unsafe { &*url };
+ let origin = url.origin().ascii_serialization();
+ let len = origin.len();
+ let ptr = Box::into_raw(origin.into_bytes().into_boxed_slice()) as *mut c_uchar;
+ OwnedString { ptr, len }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_port(url: *mut Url, port: u16) -> i32 {
+ let url = unsafe { &mut *url };
+ match url.set_port(Some(port)) {
+ Ok(()) => 0,
+ Err(()) => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_port(url: *const Url) -> i32 {
+ let url = unsafe { &*url };
+ match url.port() {
+ Some(port) => port as i32,
+ None => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_host(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ match url.set_host(Some(slice)) {
+ Ok(()) => 0,
+ Err(_) => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_host(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) -> i32 {
+ let url = unsafe { &*url };
+
+ match url.host_str() {
+ Some(h) => unsafe {
+ *out_ptr = h.as_ptr();
+ *out_len = h.len();
+ 0
+ },
+ None => unsafe {
+ *out_len = 0;
+ -1
+ },
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_scheme(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ match url.set_scheme(slice) {
+ Ok(()) => 0,
+ Err(()) => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_scheme(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) {
+ let url = unsafe { &*url };
+ let scheme = url.scheme();
+ *out_ptr = scheme.as_ptr();
+ *out_len = scheme.len();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_path(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ url.set_path(slice);
+ 0
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_path(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) {
+ let url = unsafe { &*url };
+ let path = url.path();
+ *out_ptr = path.as_ptr();
+ *out_len = path.len();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_query(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ url.set_query(Some(slice));
+ 0
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_query(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) -> i32 {
+ let url = unsafe { &*url };
+ match url.query() {
+ Some(query) => {
+ *out_ptr = query.as_ptr();
+ *out_len = query.len();
+ 0
+ }
+ None => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_set_fragment(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
+ let url = unsafe { &mut *url };
+ let slice = match str_from(ptr, len) {
+ Some(s) => s,
+ None => return -1,
+ };
+
+ url.set_fragment(Some(slice));
+ 0
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_fragment(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) -> i32 {
+ let url = unsafe { &*url };
+ match url.fragment() {
+ Some(fragment) => {
+ *out_ptr = fragment.as_ptr();
+ *out_len = fragment.len();
+ 0
+ }
+ None => -1,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn url_get_href(
+ url: *const Url,
+ out_ptr: *mut *const c_uchar,
+ out_len: *mut usize,
+) {
+ let url = unsafe { &*url };
+ let href = url.as_str();
+ *out_ptr = href.as_ptr();
+ *out_len = href.len();
+}
diff --git a/src/sys/url.zig b/src/sys/url.zig
new file mode 100644
index 000000000..e74a511d5
--- /dev/null
+++ b/src/sys/url.zig
@@ -0,0 +1,71 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//! Bindings for Servo's rust-url (https://github.com/servo/rust-url).
+//! Check @src/html5ever/url.rs for Rust-side of the bindings.
+
+const std = @import("std");
+const mem = std.mem;
+
+pub const Url = anyopaque;
+
+pub const OwnedString = extern struct {
+ ptr: [*]const u8,
+ len: usize,
+
+ pub inline fn deinit(self: OwnedString) void {
+ return free_owned_string(self);
+ }
+
+ pub inline fn slice(self: OwnedString) []const u8 {
+ return self.ptr[0..self.len];
+ }
+};
+
+/// https://docs.rs/url/latest/url/enum.ParseError.html
+pub const ParseError = enum(i32) {
+ EmptyHost,
+ IdnaError,
+ InvalidPort,
+ InvalidIpv4Address,
+ InvalidIpv6Address,
+ InvalidDomainCharacter,
+ RelativeUrlWithoutBase,
+ RelativeUrlWithCannotBeABaseBase,
+ SetHostOnCannotBeABaseUrl,
+ Overflow,
+};
+
+/// If return value is null, `err` indicates `ParseError`.
+pub extern "c" fn url_parse(ptr: [*]const u8, len: usize, err: *i32) ?*Url;
+/// If return value is null, `err` indicates `ParseError`.
+pub extern "c" fn url_join(base: *const Url, ptr: [*]const u8, len: usize, err: *i32) ?*Url;
+pub extern "c" fn url_free(url: *Url) void;
+pub extern "c" fn url_can_parse(ptr: [*]const u8, len: usize) bool;
+pub extern "c" fn url_can_parse_with_base(base_ptr: [*]const u8, base_len: usize, ptr: [*]const u8, len: usize) bool;
+pub extern "c" fn url_to_string(url: *const Url, out_ptr: *[*]const u8, out_len: *usize) void;
+pub extern "c" fn url_set_host(url: *Url, ptr: [*]const u8, len: usize) i32;
+pub extern "c" fn url_get_host(url: *const Url, out_ptr: *[*]const u8, out_len: *usize) void;
+/// This function allocates on Rust-side.
+pub extern "c" fn url_get_origin(url: *const Url) OwnedString;
+pub extern "c" fn free_owned_string(owned: OwnedString) void;
+pub extern "c" fn url_set_port(url: *Url, port: u16) i32;
+
+extern "c" fn url_get_port(url: *const Url) i32;
+pub inline fn urlGetPort(url: *const Url) ?u16 {
+ const result = url_get_port(url);
+ if (result < 0) return null;
+ return @intCast(result);
+}