LibURL: Implement a URL parser in rust
This commit is contained in:
parent
8cc458e73a
commit
d44ff6628f
20 changed files with 3175 additions and 1 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -736,6 +736,14 @@ dependencies = [
|
|||
"icu_calendar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "liburl_rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cbindgen",
|
||||
"libunicode_rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libwasm_cranelift"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ members = [
|
|||
"Libraries/LibJS/Rust",
|
||||
"Libraries/LibRegex/Rust",
|
||||
"Libraries/LibUnicode/Rust",
|
||||
"Libraries/LibURL/Rust",
|
||||
"Libraries/LibWasm/Rust",
|
||||
"Libraries/LibWeb/ContentBlocker/Rust",
|
||||
"Libraries/LibWeb/Rust",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ set(SOURCES
|
|||
Host.cpp
|
||||
Origin.cpp
|
||||
Parser.cpp
|
||||
RustIntegration.cpp
|
||||
Site.cpp
|
||||
URL.cpp
|
||||
${PUBLIC_SUFFIX_SOURCES}
|
||||
|
|
@ -20,4 +21,7 @@ set(SOURCES
|
|||
)
|
||||
|
||||
ladybird_lib(LibURL url)
|
||||
target_link_libraries(LibURL PRIVATE LibUnicode LibTextCodec LibRegex)
|
||||
target_link_libraries(LibURL PRIVATE liburl_rust LibUnicode LibTextCodec LibRegex)
|
||||
|
||||
import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME liburl_rust FEATURES allocator FFI_HEADER RustFFI.h)
|
||||
target_compile_definitions(LibURL PRIVATE ENABLE_RUST)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <LibTextCodec/Decoder.h>
|
||||
#include <LibTextCodec/Encoder.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibURL/RustIntegration.h>
|
||||
#include <LibUnicode/IDNA.h>
|
||||
|
||||
namespace URL {
|
||||
|
|
@ -708,6 +709,10 @@ static String remove_ascii_tab_or_newline(StringView input)
|
|||
// https://url.spec.whatwg.org/#concept-basic-url-parser
|
||||
Optional<URL> Parser::basic_parse(StringView raw_input, Optional<URL const&> base_url, URL* url, Optional<State> state_override, Optional<StringView> encoding)
|
||||
{
|
||||
#ifdef ENABLE_RUST
|
||||
return RustIntegration::parse_basic_url(raw_input, base_url, url, state_override, encoding);
|
||||
#endif
|
||||
|
||||
dbgln_if(URL_PARSER_DEBUG, "URL::Parser::basic_parse: Parsing '{}'", raw_input);
|
||||
|
||||
size_t start_index = 0;
|
||||
|
|
|
|||
18
Libraries/LibURL/Rust/Cargo.toml
Normal file
18
Libraries/LibURL/Rust/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "liburl_rust"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
libunicode_rust = { path = "../../LibUnicode/Rust" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
allocator = []
|
||||
debug-validation-errors = []
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.29"
|
||||
40
Libraries/LibURL/Rust/build.rs
Normal file
40
Libraries/LibURL/Rust/build.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=cbindgen.toml");
|
||||
println!("cargo:rerun-if-env-changed=FFI_OUTPUT_DIR");
|
||||
println!("cargo:rerun-if-changed=src");
|
||||
|
||||
let ffi_out_dir = env::var("FFI_OUTPUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| out_dir.clone());
|
||||
|
||||
cbindgen::generate(manifest_dir).map_or_else(
|
||||
|error| match error {
|
||||
cbindgen::Error::ParseSyntaxError { .. } => {}
|
||||
e => panic!("{e:?}"),
|
||||
},
|
||||
|bindings| {
|
||||
let header_path = out_dir.join("RustFFI.h");
|
||||
bindings.write_to_file(&header_path);
|
||||
|
||||
if ffi_out_dir != out_dir {
|
||||
bindings.write_to_file(ffi_out_dir.join("RustFFI.h"));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
17
Libraries/LibURL/Rust/cbindgen.toml
Normal file
17
Libraries/LibURL/Rust/cbindgen.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
language = "C++"
|
||||
header = """/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/"""
|
||||
pragma_once = true
|
||||
include_version = true
|
||||
namespaces = ["URL", "FFI"]
|
||||
line_length = 120
|
||||
tab_width = 4
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stddef.h"]
|
||||
usize_is_size_t = true
|
||||
|
||||
[export.mangle]
|
||||
rename_types = "PascalCase"
|
||||
7
Libraries/LibURL/Rust/src/ffi.rs
Normal file
7
Libraries/LibURL/Rust/src/ffi.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
pub mod url;
|
||||
289
Libraries/LibURL/Rust/src/ffi/url.rs
Normal file
289
Libraries/LibURL/Rust/src/ffi/url.rs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::Ipv6Addr;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::panic::catch_unwind;
|
||||
|
||||
use crate::url::BasicParseOptions;
|
||||
use crate::url::Host;
|
||||
use crate::url::State;
|
||||
use crate::url::Url;
|
||||
use crate::url::basic_parse;
|
||||
use crate::url::basic_parse_into;
|
||||
use crate::url::is_special_scheme;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RustUrlByteSlice {
|
||||
pub data: *const u8,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RustUrlHostKind {
|
||||
String,
|
||||
Ipv4,
|
||||
Ipv6,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct FfiUrlHost {
|
||||
pub has_host: bool,
|
||||
pub kind: RustUrlHostKind,
|
||||
pub ipv4: [u8; 4],
|
||||
pub ipv6: [u8; 16],
|
||||
pub string_data: *const u8,
|
||||
pub string_length: usize,
|
||||
}
|
||||
|
||||
impl Default for FfiUrlHost {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
has_host: false,
|
||||
kind: RustUrlHostKind::String,
|
||||
ipv4: [0; 4],
|
||||
ipv6: [0; 16],
|
||||
string_data: std::ptr::null(),
|
||||
string_length: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct RustFfiUrl {
|
||||
pub scheme: RustUrlByteSlice,
|
||||
pub username: RustUrlByteSlice,
|
||||
pub password: RustUrlByteSlice,
|
||||
pub host: FfiUrlHost,
|
||||
pub has_port: bool,
|
||||
pub port: u16,
|
||||
pub path_segments: *const RustUrlByteSlice,
|
||||
pub path_segment_count: usize,
|
||||
pub has_opaque_path: bool,
|
||||
pub has_query: bool,
|
||||
pub query: RustUrlByteSlice,
|
||||
pub has_fragment: bool,
|
||||
pub fragment: RustUrlByteSlice,
|
||||
}
|
||||
|
||||
/// FFI parse options borrowed from C++ storage.
|
||||
///
|
||||
/// The embedded `RustFfiUrl` values contain raw pointers into `UrlFfiStorage`
|
||||
/// objects on the C++ side. Those storage objects must outlive the call to
|
||||
/// `rust_url_basic_parse`.
|
||||
#[repr(C)]
|
||||
pub struct RustBasicParseOptions {
|
||||
pub has_base_url: bool,
|
||||
pub has_url: bool,
|
||||
pub base_url: RustFfiUrl,
|
||||
pub url: RustFfiUrl,
|
||||
pub has_state_override: bool,
|
||||
pub state_override: State,
|
||||
pub encoding: RustUrlByteSlice,
|
||||
}
|
||||
|
||||
pub type FfiUrlResultFn = unsafe extern "C" fn(*mut c_void, *const RustFfiUrl);
|
||||
|
||||
fn abort_on_panic<F: FnOnce() -> R, R>(f: F) -> R {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(result) => result,
|
||||
Err(_) => std::process::abort(),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_utf8(slice: RustUrlByteSlice) -> String {
|
||||
if slice.data.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
// SAFETY: slice.data is valid for slice.length bytes (contract with C++ caller).
|
||||
let bytes = unsafe { std::slice::from_raw_parts(slice.data, slice.length) };
|
||||
std::str::from_utf8(bytes)
|
||||
.expect("URL fields are valid UTF-8")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn host_from_ffi(ffi: &FfiUrlHost, scheme: &str) -> Option<Host> {
|
||||
if !ffi.has_host {
|
||||
return None;
|
||||
}
|
||||
Some(match ffi.kind {
|
||||
RustUrlHostKind::String => {
|
||||
let s = decode_utf8(RustUrlByteSlice {
|
||||
data: ffi.string_data,
|
||||
length: ffi.string_length,
|
||||
});
|
||||
if is_special_scheme(scheme.as_bytes()) {
|
||||
Host::Domain(s)
|
||||
} else {
|
||||
Host::Opaque(s)
|
||||
}
|
||||
}
|
||||
RustUrlHostKind::Ipv4 => Host::Ipv4(Ipv4Addr::new(ffi.ipv4[0], ffi.ipv4[1], ffi.ipv4[2], ffi.ipv4[3])),
|
||||
RustUrlHostKind::Ipv6 => Host::Ipv6(Ipv6Addr::from(ffi.ipv6)),
|
||||
})
|
||||
}
|
||||
|
||||
fn host_to_ffi(host: Option<&Host>) -> FfiUrlHost {
|
||||
let Some(host) = host else {
|
||||
return FfiUrlHost::default();
|
||||
};
|
||||
match host {
|
||||
Host::Domain(s) | Host::Opaque(s) => FfiUrlHost {
|
||||
has_host: true,
|
||||
kind: RustUrlHostKind::String,
|
||||
string_data: s.as_ptr(),
|
||||
string_length: s.len(),
|
||||
..FfiUrlHost::default()
|
||||
},
|
||||
Host::Ipv4(addr) => FfiUrlHost {
|
||||
has_host: true,
|
||||
kind: RustUrlHostKind::Ipv4,
|
||||
ipv4: addr.octets(),
|
||||
..FfiUrlHost::default()
|
||||
},
|
||||
Host::Ipv6(addr) => FfiUrlHost {
|
||||
has_host: true,
|
||||
kind: RustUrlHostKind::Ipv6,
|
||||
ipv6: addr.octets(),
|
||||
..FfiUrlHost::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn url_from_ffi(ffi: &RustFfiUrl) -> Url {
|
||||
let scheme = decode_utf8(ffi.scheme);
|
||||
let path = if !ffi.path_segments.is_null() {
|
||||
// SAFETY: path_segments is valid for path_segment_count elements.
|
||||
let segments = unsafe { std::slice::from_raw_parts(ffi.path_segments, ffi.path_segment_count) };
|
||||
segments.iter().map(|s| decode_utf8(*s)).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
Url {
|
||||
scheme: scheme.clone(),
|
||||
username: decode_utf8(ffi.username),
|
||||
password: decode_utf8(ffi.password),
|
||||
host: host_from_ffi(&ffi.host, &scheme),
|
||||
port: ffi.has_port.then_some(ffi.port),
|
||||
path,
|
||||
has_opaque_path: ffi.has_opaque_path,
|
||||
query: ffi.has_query.then(|| decode_utf8(ffi.query)),
|
||||
fragment: ffi.has_fragment.then(|| decode_utf8(ffi.fragment)),
|
||||
}
|
||||
}
|
||||
|
||||
fn url_to_ffi_result<'a>(url: &'a Url, path_slices: &'a [RustUrlByteSlice]) -> RustFfiUrl {
|
||||
let null_slice = RustUrlByteSlice {
|
||||
data: std::ptr::null(),
|
||||
length: 0,
|
||||
};
|
||||
RustFfiUrl {
|
||||
scheme: RustUrlByteSlice {
|
||||
data: url.scheme.as_ptr(),
|
||||
length: url.scheme.len(),
|
||||
},
|
||||
username: RustUrlByteSlice {
|
||||
data: url.username.as_ptr(),
|
||||
length: url.username.len(),
|
||||
},
|
||||
password: RustUrlByteSlice {
|
||||
data: url.password.as_ptr(),
|
||||
length: url.password.len(),
|
||||
},
|
||||
host: host_to_ffi(url.host.as_ref()),
|
||||
has_port: url.port.is_some(),
|
||||
port: url.port.unwrap_or(0),
|
||||
path_segments: path_slices.as_ptr(),
|
||||
path_segment_count: path_slices.len(),
|
||||
has_opaque_path: url.has_opaque_path,
|
||||
has_query: url.query.is_some(),
|
||||
query: url.query.as_deref().map_or(null_slice, |s: &str| RustUrlByteSlice {
|
||||
data: s.as_ptr(),
|
||||
length: s.len(),
|
||||
}),
|
||||
has_fragment: url.fragment.is_some(),
|
||||
fragment: url.fragment.as_deref().map_or(null_slice, |s: &str| RustUrlByteSlice {
|
||||
data: s.as_ptr(),
|
||||
length: s.len(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `input` must be valid for `input_length` bytes. `options` must be a valid pointer
|
||||
/// whose embedded URL pointers remain valid for the duration of this call. `on_complete`
|
||||
/// is called exactly once with the parse result.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_basic_parse(
|
||||
input: *const u8,
|
||||
input_length: usize,
|
||||
options: *const RustBasicParseOptions,
|
||||
ctx: *mut c_void,
|
||||
on_complete: FfiUrlResultFn,
|
||||
) -> bool {
|
||||
abort_on_panic(|| {
|
||||
// SAFETY: caller guarantees input and options are valid.
|
||||
let input_bytes = unsafe { std::slice::from_raw_parts(input, input_length) };
|
||||
let input_str = String::from_utf8_lossy(input_bytes);
|
||||
|
||||
let options = unsafe { options.as_ref() };
|
||||
|
||||
let base_url = options.filter(|o| o.has_base_url).map(|o| url_from_ffi(&o.base_url));
|
||||
|
||||
let existing_url = options.filter(|o| o.has_url).map(|o| url_from_ffi(&o.url));
|
||||
|
||||
let state_override = options.filter(|o| o.has_state_override).map(|o| o.state_override);
|
||||
|
||||
let encoding = options.and_then(|o| {
|
||||
if o.encoding.data.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(decode_utf8(o.encoding))
|
||||
}
|
||||
});
|
||||
|
||||
let mut parse_options = BasicParseOptions::new()
|
||||
.state_override(state_override)
|
||||
.encoding(encoding.as_deref());
|
||||
parse_options.base_url = base_url.as_ref();
|
||||
|
||||
let (did_succeed, maybe_url) = if let Some(mut existing_url) = existing_url {
|
||||
let did_succeed = basic_parse_into(&input_str, &mut existing_url, &parse_options);
|
||||
(did_succeed, Some(existing_url))
|
||||
} else if let Some(parsed) = basic_parse(&input_str, parse_options) {
|
||||
(true, Some(parsed))
|
||||
} else {
|
||||
(false, None)
|
||||
};
|
||||
|
||||
let Some(url) = maybe_url else {
|
||||
// SAFETY: on_complete is a valid function pointer; ctx is caller-provided.
|
||||
unsafe { on_complete(ctx, std::ptr::null()) };
|
||||
return false;
|
||||
};
|
||||
|
||||
// Build path slices borrowing from url.path — all live until end of closure.
|
||||
let path_slices: Vec<RustUrlByteSlice> = url
|
||||
.path
|
||||
.iter()
|
||||
.map(|s: &String| RustUrlByteSlice {
|
||||
data: s.as_ptr(),
|
||||
length: s.len(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ffi_result = url_to_ffi_result(&url, &path_slices);
|
||||
|
||||
// SAFETY: ffi_result borrows from url and path_slices, both live here.
|
||||
unsafe { on_complete(ctx, &raw const ffi_result) };
|
||||
did_succeed
|
||||
})
|
||||
}
|
||||
20
Libraries/LibURL/Rust/src/lib.rs
Normal file
20
Libraries/LibURL/Rust/src/lib.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#[cfg(feature = "allocator")]
|
||||
#[path = "../../../RustAllocator.rs"]
|
||||
mod rust_allocator;
|
||||
|
||||
mod ffi;
|
||||
mod textcodec;
|
||||
pub mod url;
|
||||
|
||||
pub use url::BasicParseOptions;
|
||||
pub use url::Host;
|
||||
pub use url::State;
|
||||
pub use url::Url;
|
||||
pub use url::basic_parse;
|
||||
pub use url::basic_parse_into;
|
||||
75
Libraries/LibURL/Rust/src/textcodec.rs
Normal file
75
Libraries/LibURL/Rust/src/textcodec.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum EncodeItem {
|
||||
Byte(u8),
|
||||
Error(u32),
|
||||
}
|
||||
|
||||
type FfiByteFn = unsafe extern "C" fn(*mut c_void, u8);
|
||||
type FfiCodePointFn = unsafe extern "C" fn(*mut c_void, u32);
|
||||
|
||||
unsafe extern "C" {
|
||||
fn textcodec_rust_encode(
|
||||
encoding: *const u8,
|
||||
encoding_length: usize,
|
||||
input: *const u8,
|
||||
input_length: usize,
|
||||
ctx: *mut c_void,
|
||||
on_byte: FfiByteFn,
|
||||
on_error: FfiCodePointFn,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
struct EncodeCallbacks<'a> {
|
||||
on_item: &'a mut dyn FnMut(EncodeItem),
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_encode_byte_with_callbacks(ctx: *mut c_void, byte: u8) {
|
||||
// SAFETY: `ctx` was set to `addr_of_mut!(callbacks)` in `encode_into`.
|
||||
let callbacks = unsafe { &mut *(ctx as *mut EncodeCallbacks<'_>) };
|
||||
(callbacks.on_item)(EncodeItem::Byte(byte));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_encode_error_with_callbacks(ctx: *mut c_void, error: u32) {
|
||||
// SAFETY: `ctx` was set to `addr_of_mut!(callbacks)` in `encode_into`.
|
||||
let callbacks = unsafe { &mut *(ctx as *mut EncodeCallbacks<'_>) };
|
||||
(callbacks.on_item)(EncodeItem::Error(error));
|
||||
}
|
||||
|
||||
// https://encoding.spec.whatwg.org/#get-an-output-encoding
|
||||
pub(crate) fn get_output_encoding(encoding: &str) -> &str {
|
||||
// 1. If encoding is replacement or UTF-16BE/LE, then return UTF-8.
|
||||
if encoding.eq_ignore_ascii_case("replacement")
|
||||
|| encoding.eq_ignore_ascii_case("utf-16le")
|
||||
|| encoding.eq_ignore_ascii_case("utf-16be")
|
||||
{
|
||||
return "UTF-8";
|
||||
}
|
||||
|
||||
// 2. Return encoding.
|
||||
encoding
|
||||
}
|
||||
|
||||
pub(crate) fn encode_into(encoding: &str, input: &str, mut on_item: impl FnMut(EncodeItem)) -> bool {
|
||||
let mut callbacks = EncodeCallbacks { on_item: &mut on_item };
|
||||
|
||||
// SAFETY: `encoding`, `input`, and `callbacks` are valid for the duration of the call.
|
||||
unsafe {
|
||||
textcodec_rust_encode(
|
||||
encoding.as_ptr(),
|
||||
encoding.len(),
|
||||
input.as_ptr(),
|
||||
input.len(),
|
||||
std::ptr::addr_of_mut!(callbacks) as *mut c_void,
|
||||
on_encode_byte_with_callbacks,
|
||||
on_encode_error_with_callbacks,
|
||||
)
|
||||
}
|
||||
}
|
||||
566
Libraries/LibURL/Rust/src/url/host.rs
Normal file
566
Libraries/LibURL/Rust/src/url/host.rs
Normal file
|
|
@ -0,0 +1,566 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
use libunicode_rust::idna::ToAsciiOptions;
|
||||
use libunicode_rust::idna::idna_to_ascii;
|
||||
|
||||
use super::Host;
|
||||
use super::State;
|
||||
use super::parser::report_validation_error;
|
||||
use super::percent_encoding::PercentEncodeSet;
|
||||
use super::percent_encoding::percent_decode;
|
||||
use super::percent_encoding::percent_encode;
|
||||
|
||||
// https://url.spec.whatwg.org/#forbidden-host-code-point
|
||||
fn is_forbidden_host_code_point(code_point: char) -> bool {
|
||||
// A forbidden host code point is U+0000 NULL, U+0009 TAB, U+000A LF, U+000D CR, U+0020 SPACE, U+0023 (#), U+002F (/),
|
||||
// U+003A (:), U+003C (<), U+003E (>), U+003F (?), U+0040 (@), U+005B ([), U+005C (\), U+005D (]), U+005E (^), or U+007C (|).
|
||||
"\0\t\n\r #/:<>?@[\\]^|".contains(code_point)
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#forbidden-domain-code-point
|
||||
pub(super) fn is_forbidden_domain_code_point(code_point: char) -> bool {
|
||||
// A forbidden domain code point is a forbidden host code point, a C0 control, U+0025 (%), or U+007F DELETE.
|
||||
is_forbidden_host_code_point(code_point)
|
||||
|| code_point.is_ascii_control()
|
||||
|| code_point == '%'
|
||||
|| code_point == '\u{007F}'
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-opaque-host-parser
|
||||
fn parse_opaque_host(input: &str) -> Option<Host> {
|
||||
// 1. If input contains a forbidden host code point, host-invalid-code-point validation error, return failure.
|
||||
if input.chars().any(is_forbidden_host_code_point) {
|
||||
report_validation_error(State::Host, 0, None, "host-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. If input contains a code point that is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
|
||||
// 3. If input contains a U+0025 (%) and the two code points following it are not ASCII hex digits, invalid-URL-unit validation error.
|
||||
// NOTE: These steps are not implemented because they are not cheap checks and exist just to report validation errors. With how we
|
||||
// currently report validation errors, they are only useful for debugging efforts in the URL parsing code.
|
||||
|
||||
// 4. Return the result of running UTF-8 percent-encode on input using the C0 control percent-encode set.
|
||||
Some(Host::Opaque(percent_encode(input, PercentEncodeSet::C0Control, false)))
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-domain-to-ascii
|
||||
pub(crate) fn domain_to_ascii(domain: &str, be_strict: bool) -> Option<String> {
|
||||
// 1. Let result be the result of running Unicode ToASCII with domain_name set to domain,
|
||||
// CheckHyphens set to beStrict,
|
||||
// CheckBidi set to true,
|
||||
// CheckJoiners set to true,
|
||||
// UseSTD3ASCIIRules set to beStrict,
|
||||
// Transitional_Processing set to false,
|
||||
// VerifyDnsLength set to beStrict,
|
||||
// and IgnoreInvalidPunycode set to false. [UTS46]
|
||||
//
|
||||
// NOTE: If beStrict is false, domain is an ASCII string, and strictly splitting domain on U+002E (.) does not
|
||||
// produce any item that starts with an ASCII case-insensitive match for "xn--", this step is equivalent to
|
||||
// ASCII lowercasing domain.
|
||||
|
||||
// OPTIMIZATION: See spec note above.
|
||||
if !be_strict && domain.is_ascii() {
|
||||
// 3. If result is the empty string, domain-to-ASCII validation error, return failure.
|
||||
if domain.is_empty() {
|
||||
report_validation_error(State::Host, 0, None, "domain-to-ASCII");
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut slow_path = false;
|
||||
|
||||
for part in domain.split('.') {
|
||||
if part.len() >= 4 && part[..4].eq_ignore_ascii_case("xn--") {
|
||||
slow_path = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !slow_path {
|
||||
let result = domain.to_ascii_lowercase();
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If result is a failure value, domain-to-ASCII validation error, return failure.
|
||||
let result = idna_to_ascii(
|
||||
domain,
|
||||
ToAsciiOptions {
|
||||
check_hyphens: be_strict,
|
||||
check_bidi: true,
|
||||
check_joiners: true,
|
||||
use_std3_ascii_rules: be_strict,
|
||||
transitional_processing: false,
|
||||
verify_dns_length: be_strict,
|
||||
ignore_invalid_punycode: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
// 3. If beStrict is false:
|
||||
if !be_strict {
|
||||
// 1. If result is the empty string, domain-to-ASCII validation error, return failure.
|
||||
if result.is_empty() {
|
||||
report_validation_error(State::Host, 0, None, "domain-to-ASCII");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. If result contains a forbidden domain code point, domain-invalid-code-point validation error, return failure.
|
||||
// NOTE: Due to web compatibility and compatibility with non-DNS-based systems the forbidden domain code points
|
||||
// are a subset of those disallowed when UseSTD3ASCIIRules is true. See also issue #397.
|
||||
if result.chars().any(is_forbidden_domain_code_point) {
|
||||
report_validation_error(State::Host, 0, None, "domain-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Assert: result is not the empty string and does not contain a forbidden domain code point.
|
||||
// NOTE: Unicode IDNA Compatibility Processing guarantees this holds when beStrict is true. [UTS46]
|
||||
assert!(!result.is_empty());
|
||||
assert!(!result.chars().any(is_forbidden_domain_code_point));
|
||||
|
||||
// 5. Return result.
|
||||
// NOTE: This document and the web platform at large use Unicode IDNA Compatibility Processing and not IDNA2008. For
|
||||
// instance, ☕.example becomes xn--53h.example and not failure. [UTS46] [RFC5890]
|
||||
Some(result)
|
||||
}
|
||||
|
||||
struct ParsedIpv4Number {
|
||||
number: u32,
|
||||
validation_error: bool,
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#ipv4-number-parser
|
||||
fn parse_ipv4_number(mut input: &str) -> Option<ParsedIpv4Number> {
|
||||
// 1. If input is the empty string, then return failure.
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Let validationError be false.
|
||||
let mut validation_error = false;
|
||||
|
||||
// 3. Let R be 10.
|
||||
let mut radix = 10;
|
||||
|
||||
// 4. If input contains at least two code points and the first two code points are either "0X" or "0x", then:
|
||||
if input.len() >= 2 && (input.starts_with("0X") || input.starts_with("0x")) {
|
||||
// 1. Set validationError to true.
|
||||
validation_error = true;
|
||||
|
||||
// 2. Remove the first two code points from input.
|
||||
input = &input[2..];
|
||||
|
||||
// 3. Set R to 16.
|
||||
radix = 16;
|
||||
}
|
||||
// 5. Otherwise, if input contains at least two code points and the first code point is U+0030 (0), then:
|
||||
else if input.len() >= 2 && input.starts_with('0') {
|
||||
// 1. Set validationError to true.
|
||||
validation_error = true;
|
||||
|
||||
// 2. Remove the first code point from input.
|
||||
input = &input[1..];
|
||||
|
||||
// 3. Set R to 8.
|
||||
radix = 8;
|
||||
}
|
||||
|
||||
// 6. If input is the empty string, then return (0, true).
|
||||
if input.is_empty() {
|
||||
return Some(ParsedIpv4Number {
|
||||
number: 0,
|
||||
validation_error: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. If input contains a code point that is not a radix-R digit, then return failure.
|
||||
// 8. Let output be the mathematical integer value that is represented by input in radix-R notation, using ASCII hex
|
||||
// digits for digits with values 0 through 15.
|
||||
let number = u32::from_str_radix(input, radix).ok()?;
|
||||
|
||||
// 9. Return (output, validationError).
|
||||
Some(ParsedIpv4Number {
|
||||
number,
|
||||
validation_error,
|
||||
})
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-ipv4-parser
|
||||
fn parse_ipv4_address(input: &str) -> Option<Ipv4Addr> {
|
||||
// 1. Let parts be the result of strictly splitting input on U+002E (.).
|
||||
let mut parts: Vec<&str> = input.split('.').collect();
|
||||
|
||||
// 2. If the last item in parts is the empty string, then:
|
||||
if parts.last().unwrap().is_empty() {
|
||||
// 1. IPv4-empty-part validation error.
|
||||
report_validation_error(State::Host, 0, None, "IPv4-empty-part");
|
||||
|
||||
// 2. If parts’s size is greater than 1, then remove the last item from parts.
|
||||
if parts.len() > 1 {
|
||||
parts.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If parts’s size is greater than 4, IPv4-too-many-parts validation error, return failure.
|
||||
if parts.len() > 4 {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-too-many-parts");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 4. Let numbers be an empty list.
|
||||
let mut numbers = Vec::with_capacity(parts.len());
|
||||
|
||||
// 5. For each part of parts:
|
||||
for part in parts {
|
||||
// 1. Let result be the result of parsing part.
|
||||
// 2. If result is failure, IPv4-non-numeric-part validation error, return failure.
|
||||
let result = parse_ipv4_number(part)?;
|
||||
|
||||
// 3. If result[1] is true, IPv4-non-decimal-part validation error.
|
||||
if result.validation_error {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-non-decimal-part");
|
||||
}
|
||||
|
||||
// 4. Append result[0] to numbers.
|
||||
numbers.push(result.number);
|
||||
}
|
||||
|
||||
// 6. If any item in numbers is greater than 255, IPv4-out-of-range-part validation error.
|
||||
// 7. If any but the last item in numbers is greater than 255, then return failure.
|
||||
for (index, number) in numbers.iter().copied().enumerate() {
|
||||
if number > 255 {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-out-of-range-part");
|
||||
if index != numbers.len() - 1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. If the last item in numbers is greater than or equal to 256(5 − numbers’s size), then return failure.
|
||||
if u64::from(numbers.last().copied()?) >= 256u64.pow(5 - numbers.len() as u32) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 9. Let ipv4 be the last item in numbers.
|
||||
// 10. Remove the last item from numbers.
|
||||
let mut ipv4 = numbers.pop().unwrap();
|
||||
|
||||
// 11. Let counter be 0.
|
||||
// 12. For each n of numbers:
|
||||
for (counter, number) in numbers.into_iter().enumerate() {
|
||||
// 1. Increment ipv4 by n × 256(3 − counter).
|
||||
ipv4 += number * 256u32.pow(3 - counter as u32);
|
||||
|
||||
// 2. Increment counter by 1.
|
||||
}
|
||||
|
||||
// 13. Return ipv4.
|
||||
Some(Ipv4Addr::from(ipv4))
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-ipv6-parser
|
||||
fn parse_ipv6_address(input: &str) -> Option<Ipv6Addr> {
|
||||
// 1. Let address be a new IPv6 address whose pieces are all 0.
|
||||
let mut address = [0u16; 8];
|
||||
|
||||
// 2. Let pieceIndex be 0.
|
||||
let mut piece_index = 0usize;
|
||||
|
||||
// 3. Let compress be null.
|
||||
let mut compress = None;
|
||||
|
||||
// 4. Let pointer be a pointer for input.
|
||||
let bytes = input.as_bytes();
|
||||
let mut pointer = 0usize;
|
||||
|
||||
// 5. If c is U+003A (:), then:
|
||||
if bytes.get(pointer) == Some(&b':') {
|
||||
// 1. If remaining does not start with U+003A (:), IPv6-invalid-compression validation error, return failure.
|
||||
if bytes.get(pointer + 1) != Some(&b':') {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-invalid-compression");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Increase pointer by 2.
|
||||
pointer += 2;
|
||||
|
||||
// 3. Increase pieceIndex by 1 and then set compress to pieceIndex.
|
||||
piece_index += 1;
|
||||
compress = Some(piece_index);
|
||||
}
|
||||
|
||||
// 6. While c is not the EOF code point:
|
||||
while let Some(&code_point) = bytes.get(pointer) {
|
||||
// 1. If pieceIndex is 8, IPv6-too-many-pieces validation error, return failure.
|
||||
if piece_index == 8 {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-too-many-pieces");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. If c is U+003A (:), then:
|
||||
if code_point == b':' {
|
||||
// 1. If compress is non-null, IPv6-multiple-compression validation error, return failure.
|
||||
if compress.is_some() {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-multiple-compression");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Increase pointer and pieceIndex by 1, set compress to pieceIndex, and then continue.
|
||||
pointer += 1;
|
||||
piece_index += 1;
|
||||
compress = Some(piece_index);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Let value and length be 0.
|
||||
let mut value = 0u32;
|
||||
let mut length = 0usize;
|
||||
|
||||
// 4. While length is less than 4 and c is an ASCII hex digit, set value to value × 0x10 + c interpreted as
|
||||
// hexadecimal number, and increase pointer and length by 1.
|
||||
while length < 4 {
|
||||
let Some(&code_point) = bytes.get(pointer) else {
|
||||
break;
|
||||
};
|
||||
let Some(digit) = char::from(code_point).to_digit(16) else {
|
||||
break;
|
||||
};
|
||||
value = value * 0x10 + digit;
|
||||
pointer += 1;
|
||||
length += 1;
|
||||
}
|
||||
|
||||
// 5. If c is U+002E (.), then:
|
||||
if bytes.get(pointer) == Some(&b'.') {
|
||||
// 1. If length is 0, IPv4-in-IPv6-invalid-code-point validation error, return failure.
|
||||
if length == 0 {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Decrease pointer by length.
|
||||
pointer -= length;
|
||||
|
||||
// 3. If pieceIndex is greater than 6, IPv4-in-IPv6-too-many-pieces validation error, return failure.
|
||||
if piece_index > 6 {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-too-many-pieces");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 4. Let numbersSeen be 0.
|
||||
let mut numbers_seen = 0usize;
|
||||
|
||||
// 5. While c is not the EOF code point:
|
||||
while let Some(&code_point) = bytes.get(pointer) {
|
||||
// 1. Let ipv4Piece be null.
|
||||
let mut ipv4_piece = None;
|
||||
|
||||
// 2. If numbersSeen is greater than 0, then:
|
||||
if numbers_seen > 0 {
|
||||
// 1. If c is a U+002E (.) and numbersSeen is less than 4, then increase pointer by 1.
|
||||
if code_point == b'.' && numbers_seen < 4 {
|
||||
pointer += 1;
|
||||
}
|
||||
// 2. Otherwise, IPv4-in-IPv6-invalid-code-point validation error, return failure.
|
||||
else {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If c is not an ASCII digit, IPv4-in-IPv6-invalid-code-point validation error, return failure.
|
||||
let Some(&code_point) = bytes.get(pointer) else {
|
||||
break;
|
||||
};
|
||||
if !code_point.is_ascii_digit() {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 4. While c is an ASCII digit:
|
||||
while let Some(&code_point) = bytes.get(pointer) {
|
||||
if !code_point.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
|
||||
// 1. Let number be c interpreted as decimal number.
|
||||
let number = u32::from(code_point - b'0');
|
||||
|
||||
// 2. If ipv4Piece is null, then set ipv4Piece to number.
|
||||
if ipv4_piece.is_none() {
|
||||
ipv4_piece = Some(number);
|
||||
}
|
||||
// 3. Otherwise, if ipv4Piece is 0, IPv4-in-IPv6-invalid-code-point validation error, return failure.
|
||||
else if ipv4_piece == Some(0) {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
// 4. Otherwise, set ipv4Piece to ipv4Piece × 10 + number.
|
||||
else {
|
||||
ipv4_piece = Some(ipv4_piece.unwrap() * 10 + number);
|
||||
}
|
||||
|
||||
// 5. If ipv4Piece is greater than 255, IPv4-in-IPv6-out-of-range-part validation error, return failure.
|
||||
if ipv4_piece.unwrap() > 255 {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-out-of-range-part");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 6. Increase pointer by 1.
|
||||
pointer += 1;
|
||||
}
|
||||
|
||||
// 5. Set address[pieceIndex] to address[pieceIndex] × 0x100 + ipv4Piece.
|
||||
address[piece_index] = address[piece_index] * 0x100 + ipv4_piece.unwrap() as u16;
|
||||
|
||||
// 6. Increase numbersSeen by 1.
|
||||
numbers_seen += 1;
|
||||
|
||||
// 7. If numbersSeen is 2 or 4, then increase pieceIndex by 1.
|
||||
if numbers_seen == 2 || numbers_seen == 4 {
|
||||
piece_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. If numbersSeen is not 4, IPv4-in-IPv6-too-few-parts validation error, return failure.
|
||||
if numbers_seen != 4 {
|
||||
report_validation_error(State::Host, 0, None, "IPv4-in-IPv6-too-few-parts");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 7. Break.
|
||||
break;
|
||||
}
|
||||
// 6. Otherwise, if c is U+003A (:):
|
||||
else if bytes.get(pointer) == Some(&b':') {
|
||||
// 1. Increase pointer by 1.
|
||||
pointer += 1;
|
||||
|
||||
// 2. If c is the EOF code point, IPv6-invalid-code-point validation error, return failure.
|
||||
if bytes.get(pointer).is_none() {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// 7. Otherwise, if c is not the EOF code point, IPv6-invalid-code-point validation error, return failure.
|
||||
else if bytes.get(pointer).is_some() {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 8. Set address[pieceIndex] to value.
|
||||
address[piece_index] = value as u16;
|
||||
|
||||
// 9. Increase pieceIndex by 1.
|
||||
piece_index += 1;
|
||||
}
|
||||
|
||||
// 7. If compress is non-null, then:
|
||||
if let Some(compress) = compress {
|
||||
// 1. Let swaps be pieceIndex − compress.
|
||||
let mut swaps = piece_index - compress;
|
||||
|
||||
// 2. Set pieceIndex to 7.
|
||||
piece_index = 7;
|
||||
|
||||
// 3. While pieceIndex is not 0 and swaps is greater than 0, swap address[pieceIndex] with
|
||||
// address[compress + swaps − 1], and then decrease both pieceIndex and swaps by 1.
|
||||
while piece_index != 0 && swaps > 0 {
|
||||
address.swap(piece_index, compress + swaps - 1);
|
||||
piece_index -= 1;
|
||||
swaps -= 1;
|
||||
}
|
||||
}
|
||||
// 8. Otherwise, if compress is null and pieceIndex is not 8, IPv6-too-few-pieces validation error, return failure.
|
||||
else if piece_index != 8 {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-too-few-pieces");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 9. Return address.
|
||||
Some(Ipv6Addr::from(address))
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#ends-in-a-number-checker
|
||||
fn ends_in_a_number_checker(input: &str) -> bool {
|
||||
// 1. Let parts be the result of strictly splitting input on U+002E (.).
|
||||
let mut parts: Vec<&str> = input.split('.').collect();
|
||||
|
||||
// 2. If the last item in parts is the empty string, then:
|
||||
if parts.last().unwrap().is_empty() {
|
||||
// 1. If parts’s size is 1, then return false.
|
||||
if parts.len() == 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Remove the last item from parts.
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
// 3. Let last be the last item in parts.
|
||||
let last = parts.last().unwrap();
|
||||
|
||||
// 4. If last is non-empty and contains only ASCII digits, then return true.
|
||||
// NOTE: The erroneous input "09" will be caught by the IPv4 parser at a later stage.
|
||||
if !last.is_empty() && last.chars().all(|code_point| code_point.is_ascii_digit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. If parsing last as an IPv4 number does not return failure, then return true.
|
||||
// NOTE: This is equivalent to checking that last is "0X" or "0x", followed by zero or more ASCII hex digits.
|
||||
// 6. Return false.
|
||||
last.len() >= 2
|
||||
&& last[..2].eq_ignore_ascii_case("0x")
|
||||
&& last[2..].chars().all(|code_point| code_point.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-host-parser
|
||||
pub(super) 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.
|
||||
if !input.ends_with(']') {
|
||||
report_validation_error(State::Host, 0, None, "IPv6-unclosed");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Return the result of IPv6 parsing input with its leading U+005B ([) and trailing U+005D (]) removed.
|
||||
let address = parse_ipv6_address(&input[1..input.len() - 1])?;
|
||||
return Some(Host::Ipv6(address));
|
||||
}
|
||||
|
||||
// 2. If isOpaque is true, then return the result of opaque-host parsing input.
|
||||
if is_opaque {
|
||||
return parse_opaque_host(input);
|
||||
}
|
||||
|
||||
// 3. Assert: input is not the empty string.
|
||||
assert!(!input.is_empty());
|
||||
|
||||
// 4. Let domain be the result of running UTF-8 decode without BOM on the percent-decoding of input.
|
||||
let domain = String::from_utf8_lossy(&percent_decode(input)).into_owned();
|
||||
|
||||
// 5. Let asciiDomain be the result of running domain to ASCII with domain and false.
|
||||
// 6. If asciiDomain is failure, then return failure.
|
||||
let ascii_domain = domain_to_ascii(&domain, false)?;
|
||||
|
||||
// 7. If asciiDomain contains a forbidden domain code point, domain-invalid-code-point validation error, return failure.
|
||||
if ascii_domain.chars().any(is_forbidden_domain_code_point) {
|
||||
report_validation_error(State::Host, 0, None, "domain-invalid-code-point");
|
||||
return None;
|
||||
}
|
||||
|
||||
// 8. If asciiDomain ends in a number, then return the result of IPv4 parsing asciiDomain.
|
||||
if ends_in_a_number_checker(&ascii_domain) {
|
||||
return parse_ipv4_address(&ascii_domain).map(Host::Ipv4);
|
||||
}
|
||||
|
||||
// 9. Return asciiDomain.
|
||||
Some(Host::Domain(ascii_domain))
|
||||
}
|
||||
60
Libraries/LibURL/Rust/src/url/mod.rs
Normal file
60
Libraries/LibURL/Rust/src/url/mod.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
mod host;
|
||||
mod parser;
|
||||
mod percent_encoding;
|
||||
mod scheme;
|
||||
#[allow(dead_code)]
|
||||
mod serialize;
|
||||
mod types;
|
||||
|
||||
pub(crate) use self::scheme::default_port_for_scheme;
|
||||
pub(crate) use self::scheme::is_special_scheme;
|
||||
pub use self::types::Host;
|
||||
pub use self::types::State;
|
||||
pub use self::types::Url;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BasicParseOptions<'a> {
|
||||
pub base_url: Option<&'a Url>,
|
||||
state_override: Option<State>,
|
||||
encoding: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> BasicParseOptions<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn base_url(mut self, base_url: &'a Url) -> Self {
|
||||
self.base_url = Some(base_url);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state_override(mut self, state: impl Into<Option<State>>) -> Self {
|
||||
self.state_override = state.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn encoding(mut self, encoding: impl Into<Option<&'a str>>) -> Self {
|
||||
self.encoding = encoding.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn basic_parse(input: &str, options: BasicParseOptions<'_>) -> Option<Url> {
|
||||
let mut url = Url::default();
|
||||
if parser::basic_parse_into(input, &mut url, &options, false) {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn basic_parse_into(input: &str, url: &mut Url, options: &BasicParseOptions<'_>) -> bool {
|
||||
parser::basic_parse_into(input, url, options, true)
|
||||
}
|
||||
1213
Libraries/LibURL/Rust/src/url/parser.rs
Normal file
1213
Libraries/LibURL/Rust/src/url/parser.rs
Normal file
File diff suppressed because it is too large
Load diff
195
Libraries/LibURL/Rust/src/url/percent_encoding.rs
Normal file
195
Libraries/LibURL/Rust/src/url/percent_encoding.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::textcodec::EncodeItem;
|
||||
use crate::textcodec::encode_into as textcodec_encode_into;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum PercentEncodeSet {
|
||||
C0Control,
|
||||
Fragment,
|
||||
Query,
|
||||
SpecialQuery,
|
||||
Path,
|
||||
Userinfo,
|
||||
Component,
|
||||
ApplicationXWWWFormUrlencoded,
|
||||
}
|
||||
|
||||
fn code_point_is_in_percent_encode_set(code_point: char, set: PercentEncodeSet) -> bool {
|
||||
let code_point_u32 = code_point as u32;
|
||||
match set {
|
||||
// https://url.spec.whatwg.org/#c0-control-percent-encode-set
|
||||
// The C0 control percent-encode set are the C0 controls and all code points greater than U+007E (~).
|
||||
PercentEncodeSet::C0Control => !(0x20..=0x7e).contains(&code_point_u32),
|
||||
// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||
// The query percent-encode set is the C0 control percent-encode set and U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>).
|
||||
PercentEncodeSet::Fragment => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::C0Control)
|
||||
|| [' ', '"', '<', '>', '`'].contains(&code_point)
|
||||
}
|
||||
// https://url.spec.whatwg.org/#query-percent-encode-set
|
||||
// The query percent-encode set is the C0 control percent-encode set and U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>).
|
||||
// NOTE: The query percent-encode set cannot be defined in terms of the fragment percent-encode set due to the omission of U+0060 (`).
|
||||
PercentEncodeSet::Query => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::C0Control)
|
||||
|| [' ', '"', '#', '<', '>'].contains(&code_point)
|
||||
}
|
||||
// https://url.spec.whatwg.org/#special-query-percent-encode-set
|
||||
// The special-query percent-encode set is the query percent-encode set and U+0027 (').
|
||||
PercentEncodeSet::SpecialQuery => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::Query) || code_point == '\''
|
||||
}
|
||||
// https://url.spec.whatwg.org/#path-percent-encode-set
|
||||
// The path percent-encode set is the query percent-encode set and U+003F (?), U+005E (^), U+0060 (`), U+007B ({), and U+007D (}).
|
||||
PercentEncodeSet::Path => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::Query)
|
||||
|| ['?', '^', '`', '{', '}'].contains(&code_point)
|
||||
}
|
||||
// https://url.spec.whatwg.org/#userinfo-percent-encode-set
|
||||
// The userinfo percent-encode set is the path percent-encode set and U+002F (/), U+003A (:), U+003B (;),
|
||||
// U+003D (=), U+0040 (@), U+005B ([) to U+005D (]), inclusive, and U+007C (|).
|
||||
PercentEncodeSet::Userinfo => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::Path)
|
||||
|| ['/', ':', ';', '=', '@', '[', '\\', ']', '|'].contains(&code_point)
|
||||
}
|
||||
// https://url.spec.whatwg.org/#component-percent-encode-set
|
||||
// The component percent-encode set is the userinfo percent-encode set and U+0024 ($) to U+0026 (&), inclusive, U+002B (+), and U+002C (,).
|
||||
// NOTE: This is used by HTML for registerProtocolHandler(), and could also be used by other standards to
|
||||
// percent-encode data that can then be embedded in a URL’s path, query, or fragment; or in an opaque host.
|
||||
// Using it with UTF-8 percent-encode gives identical results to JavaScript’s encodeURIComponent() [sic]. [HTML] [ECMA-262]
|
||||
PercentEncodeSet::Component => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::Userinfo)
|
||||
|| ['$', '%', '&', '+', ','].contains(&code_point)
|
||||
}
|
||||
// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
|
||||
// The application/x-www-form-urlencoded percent-encode set is the component percent-encode set and U+0021 (!),
|
||||
// U+0027 (') to U+0029 RIGHT PARENTHESIS, inclusive, and U+007E (~).
|
||||
PercentEncodeSet::ApplicationXWWWFormUrlencoded => {
|
||||
code_point_is_in_percent_encode_set(code_point, PercentEncodeSet::Component)
|
||||
|| ['!', '\'', '(', ')', '~'].contains(&code_point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_percent_encoded(builder: &mut String, code_point: char) {
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
|
||||
let mut utf8 = [0u8; 4];
|
||||
for byte in code_point.encode_utf8(&mut utf8).as_bytes() {
|
||||
builder.push('%');
|
||||
builder.push(HEX[(byte >> 4) as usize] as char);
|
||||
builder.push(HEX[(byte & 0x0f) as usize] as char);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_percent_encoded_byte(builder: &mut String, byte: u8) {
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
|
||||
builder.push('%');
|
||||
builder.push(HEX[(byte >> 4) as usize] as char);
|
||||
builder.push(HEX[(byte & 0x0f) as usize] as char);
|
||||
}
|
||||
|
||||
pub(super) fn percent_encode(input: &str, set: PercentEncodeSet, space_as_plus: bool) -> String {
|
||||
let mut output = String::new();
|
||||
for code_point in input.chars() {
|
||||
if space_as_plus && code_point == ' ' {
|
||||
output.push('+');
|
||||
} else if code_point_is_in_percent_encode_set(code_point, set) {
|
||||
append_percent_encoded(&mut output, code_point);
|
||||
} else {
|
||||
output.push(code_point);
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub(super) fn percent_decode(input: &str) -> Vec<u8> {
|
||||
fn decode_hex(byte: u8) -> Option<u8> {
|
||||
match byte {
|
||||
b'0'..=b'9' => Some(byte - b'0'),
|
||||
b'a'..=b'f' => Some(byte - b'a' + 10),
|
||||
b'A'..=b'F' => Some(byte - b'A' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = input.as_bytes();
|
||||
let mut output = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
|
||||
while index < bytes.len() {
|
||||
if bytes[index] == b'%'
|
||||
&& index + 2 < bytes.len()
|
||||
&& let (Some(high), Some(low)) = (decode_hex(bytes[index + 1]), decode_hex(bytes[index + 2]))
|
||||
{
|
||||
output.push((high << 4) | low);
|
||||
index += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
output.push(bytes[index]);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#string-percent-encode-after-encoding
|
||||
pub(super) fn percent_encode_after_encoding(
|
||||
encoding: &str,
|
||||
input: &str,
|
||||
set: PercentEncodeSet,
|
||||
space_as_plus: bool,
|
||||
) -> String {
|
||||
// 1. Let encoder be the result of getting an encoder from encoding.
|
||||
// 2. Let inputQueue be input converted to an I/O queue.
|
||||
// 3. Let output be the empty string.
|
||||
let mut result = String::new();
|
||||
|
||||
let did_succeed = textcodec_encode_into(encoding, input, |item| match item {
|
||||
EncodeItem::Byte(byte) => {
|
||||
// 1. If spaceAsPlus is true and byte is 0x20 (SP), then append U+002B (+) to output and continue.
|
||||
if space_as_plus && byte == b' ' {
|
||||
result.push('+');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Let isomorph be a code point whose value is byte’s value.
|
||||
let code_point = char::from(byte);
|
||||
|
||||
// 4. If isomorphic is not in percentEncodeSet, then append isomorph to output.
|
||||
if !code_point_is_in_percent_encode_set(code_point, set) {
|
||||
result.push(code_point);
|
||||
} else {
|
||||
append_percent_encoded_byte(&mut result, byte);
|
||||
}
|
||||
}
|
||||
EncodeItem::Error(error) => {
|
||||
result.push_str("%26%23");
|
||||
result.push_str(&error.to_string());
|
||||
result.push_str("%3B");
|
||||
}
|
||||
});
|
||||
assert!(
|
||||
did_succeed,
|
||||
"TextCodec::encode should succeed for a valid output encoding"
|
||||
);
|
||||
|
||||
// 6. Return output.
|
||||
result
|
||||
}
|
||||
|
||||
pub(super) fn append_percent_encoded_if_necessary(buffer: &mut String, code_point: char, set: PercentEncodeSet) {
|
||||
if code_point_is_in_percent_encode_set(code_point, set) {
|
||||
append_percent_encoded(buffer, code_point);
|
||||
} else {
|
||||
buffer.push(code_point);
|
||||
}
|
||||
}
|
||||
29
Libraries/LibURL/Rust/src/url/scheme.rs
Normal file
29
Libraries/LibURL/Rust/src/url/scheme.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
// https://url.spec.whatwg.org/#special-scheme
|
||||
pub(crate) fn special_schemes() -> &'static [&'static str] {
|
||||
&["ftp", "file", "http", "https", "ws", "wss"]
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#is-special
|
||||
pub(crate) fn is_special_scheme(scheme: &[u8]) -> bool {
|
||||
special_schemes()
|
||||
.iter()
|
||||
.any(|special_scheme| scheme == special_scheme.as_bytes())
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#default-port
|
||||
pub(crate) fn default_port_for_scheme(scheme: &str) -> Option<u16> {
|
||||
match scheme {
|
||||
"ftp" => Some(21),
|
||||
"http" => Some(80),
|
||||
"https" => Some(443),
|
||||
"ws" => Some(80),
|
||||
"wss" => Some(443),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
228
Libraries/LibURL/Rust/src/url/serialize.rs
Normal file
228
Libraries/LibURL/Rust/src/url/serialize.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
use super::parser::url_includes_credentials;
|
||||
use super::types::ExcludeFragment;
|
||||
use super::types::Host;
|
||||
use super::types::Url;
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-ipv4-serializer
|
||||
fn serialize_ipv4_address(address: Ipv4Addr) -> String {
|
||||
address.to_string()
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#find-the-ipv6-address-compressed-piece-index
|
||||
fn find_the_ipv6_address_compressed_piece_index(address: Ipv6Addr) -> Option<usize> {
|
||||
let address = address.segments();
|
||||
|
||||
// 1. Let longestIndex be null.
|
||||
let mut longest_index = None;
|
||||
|
||||
// 2. Let longestSize be 1.
|
||||
let mut longest_size = 1;
|
||||
|
||||
// 3. Let foundIndex be null.
|
||||
let mut found_index = None;
|
||||
|
||||
// 4. Let foundSize be 0.
|
||||
let mut found_size = 0;
|
||||
|
||||
// 5. For each pieceIndex of address’s pieces’s indices:
|
||||
for (piece_index, piece) in address.iter().enumerate() {
|
||||
// 1. If address’s pieces[pieceIndex] is not 0:
|
||||
if *piece != 0 {
|
||||
// 1. If foundSize is greater than longestSize, then set longestIndex to foundIndex and longestSize to foundSize.
|
||||
if found_size > longest_size {
|
||||
longest_index = found_index;
|
||||
longest_size = found_size;
|
||||
}
|
||||
|
||||
// 2. Set foundIndex to null.
|
||||
found_index = None;
|
||||
|
||||
// 3. Set foundSize to 0.
|
||||
found_size = 0;
|
||||
}
|
||||
// 2. Otherwise:
|
||||
else {
|
||||
// 1. If foundIndex is null, then set foundIndex to pieceIndex.
|
||||
if found_index.is_none() {
|
||||
found_index = Some(piece_index);
|
||||
}
|
||||
|
||||
// 2. Increment foundSize by 1.
|
||||
found_size += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. If foundSize is greater than longestSize, then return foundIndex.
|
||||
if found_size > longest_size {
|
||||
return found_index;
|
||||
}
|
||||
|
||||
// 7. Return longestIndex.
|
||||
longest_index
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-ipv6-serializer
|
||||
fn serialize_ipv6_address(address: Ipv6Addr, output: &mut String) {
|
||||
let address = address.segments();
|
||||
|
||||
// 1. Let output be the empty string.
|
||||
|
||||
// 2. Let compress be the result of finding the IPv6 address compressed piece index given address.
|
||||
let compress = find_the_ipv6_address_compressed_piece_index(Ipv6Addr::from(address));
|
||||
|
||||
// 3. Let ignore0 be false.
|
||||
let mut ignore0 = false;
|
||||
|
||||
// 4. For each pieceIndex of address’s pieces’s indices:
|
||||
for (piece_index, piece) in address.iter().enumerate() {
|
||||
// 1. If ignore0 is true and address[pieceIndex] is 0, then continue.
|
||||
if ignore0 && *piece == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Otherwise, if ignore0 is true, set ignore0 to false.
|
||||
if ignore0 {
|
||||
ignore0 = false;
|
||||
}
|
||||
|
||||
// 3. If compress is pieceIndex, then:
|
||||
if compress == Some(piece_index) {
|
||||
// 1. Let separator be "::" if pieceIndex is 0, and U+003A (:) otherwise.
|
||||
let separator = if piece_index == 0 { "::" } else { ":" };
|
||||
|
||||
// 2. Append separator to output.
|
||||
output.push_str(separator);
|
||||
|
||||
// 3. Set ignore0 to true and continue.
|
||||
ignore0 = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Append address[pieceIndex], represented as the shortest possible lowercase hexadecimal number, to output.
|
||||
output.push_str(&format!("{piece:x}"));
|
||||
|
||||
// 5. If pieceIndex is not 7, then append U+003A (:) to output.
|
||||
if piece_index != 7 {
|
||||
output.push(':');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return output.
|
||||
}
|
||||
|
||||
impl Host {
|
||||
// https://url.spec.whatwg.org/#concept-host-serializer
|
||||
pub(crate) fn serialize(&self) -> String {
|
||||
match self {
|
||||
// 1. If host is an IPv4 address, return the result of running the IPv4 serializer on host.
|
||||
Self::Ipv4(address) => serialize_ipv4_address(*address),
|
||||
// 2. Otherwise, if host is an IPv6 address, return U+005B ([), followed by the result of running the
|
||||
// IPv6 serializer on host, followed by U+005D (]).
|
||||
Self::Ipv6(address) => {
|
||||
let mut output = String::new();
|
||||
output.push('[');
|
||||
serialize_ipv6_address(*address, &mut output);
|
||||
output.push(']');
|
||||
output
|
||||
}
|
||||
// 3. Otherwise, host is a domain, opaque host, or empty host, return host.
|
||||
Self::Domain(string) | Self::Opaque(string) => string.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Url {
|
||||
// https://url.spec.whatwg.org/#url-path-serializer
|
||||
pub(crate) fn serialize_path(&self) -> String {
|
||||
// 1. If url has an opaque path, then return url's path.
|
||||
if self.has_opaque_path {
|
||||
return self.path[0].clone();
|
||||
}
|
||||
|
||||
// 2. Let output be the empty string.
|
||||
let mut output = String::new();
|
||||
|
||||
// 3. For each segment of url's path: append U+002F (/) followed by segment to output.
|
||||
for segment in &self.path {
|
||||
output.push('/');
|
||||
output.push_str(segment);
|
||||
}
|
||||
|
||||
// 4. Return output.
|
||||
output
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-url-serializer
|
||||
pub(crate) fn serialize(&self, exclude_fragment: ExcludeFragment) -> String {
|
||||
// 1. Let output be url's scheme and U+003A (:) concatenated.
|
||||
let mut output = String::new();
|
||||
output.push_str(&self.scheme);
|
||||
output.push(':');
|
||||
|
||||
// 2. If url's host is non-null:
|
||||
if let Some(host) = self.host.as_ref() {
|
||||
// 1. Append "//" to output.
|
||||
output.push_str("//");
|
||||
|
||||
// 2. If url includes credentials, then:
|
||||
if url_includes_credentials(self) {
|
||||
// 1. Append url's username to output.
|
||||
output.push_str(&self.username);
|
||||
|
||||
// 2. If url's password is not the empty string, then append U+003A (:), followed by url's password, to output.
|
||||
if !self.password.is_empty() {
|
||||
output.push(':');
|
||||
output.push_str(&self.password);
|
||||
}
|
||||
|
||||
// 3. Append U+0040 (@) to output.
|
||||
output.push('@');
|
||||
}
|
||||
|
||||
// 3. Append url's host, serialized, to output.
|
||||
output.push_str(&host.serialize());
|
||||
|
||||
// 4. If url's port is non-null, append U+003A (:) followed by url's port, serialized, to output.
|
||||
if let Some(port) = self.port {
|
||||
output.push(':');
|
||||
output.push_str(&port.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If url's host is null, url does not have an opaque path, url's path's size is greater than 1, and url's
|
||||
// path[0] is the empty string, then append U+002F (/) followed by U+002E (.) to output.
|
||||
if self.host.is_none() && !self.has_opaque_path && self.path.len() > 1 && self.path[0].is_empty() {
|
||||
output.push_str("/.");
|
||||
}
|
||||
|
||||
// 4. Append the result of URL path serializing url to output.
|
||||
output.push_str(&self.serialize_path());
|
||||
|
||||
// 5. If url's query is non-null, append U+003F (?), followed by url's query, to output.
|
||||
if let Some(query) = &self.query {
|
||||
output.push('?');
|
||||
output.push_str(query);
|
||||
}
|
||||
|
||||
// 6. If exclude fragment is false and url's fragment is non-null, then append U+0023 (#), followed by url's
|
||||
// fragment, to output.
|
||||
if exclude_fragment == ExcludeFragment::No
|
||||
&& let Some(fragment) = &self.fragment
|
||||
{
|
||||
output.push('#');
|
||||
output.push_str(fragment);
|
||||
}
|
||||
|
||||
// 7. Return output.
|
||||
output
|
||||
}
|
||||
}
|
||||
147
Libraries/LibURL/Rust/src/url/types.rs
Normal file
147
Libraries/LibURL/Rust/src/url/types.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
use super::percent_encoding::PercentEncodeSet;
|
||||
use super::percent_encoding::percent_encode;
|
||||
use super::scheme::is_special_scheme;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum State {
|
||||
SchemeStart,
|
||||
Scheme,
|
||||
NoScheme,
|
||||
SpecialRelativeOrAuthority,
|
||||
PathOrAuthority,
|
||||
Relative,
|
||||
RelativeSlash,
|
||||
SpecialAuthoritySlashes,
|
||||
SpecialAuthorityIgnoreSlashes,
|
||||
Authority,
|
||||
Host,
|
||||
Hostname,
|
||||
Port,
|
||||
File,
|
||||
FileSlash,
|
||||
FileHost,
|
||||
PathStart,
|
||||
Path,
|
||||
OpaquePath,
|
||||
Query,
|
||||
Fragment,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ExcludeFragment {
|
||||
No,
|
||||
Yes,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Url {
|
||||
pub(crate) fn set_scheme(&mut self, scheme: String) {
|
||||
self.scheme = scheme;
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#set-the-username
|
||||
pub(crate) fn set_username(&mut self, username: &str) {
|
||||
// To set the username given a url and username, set url’s username to the result of running UTF-8 percent-encode
|
||||
// on username using the userinfo percent-encode set.
|
||||
self.username = percent_encode(username, PercentEncodeSet::Userinfo, false);
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#set-the-password
|
||||
pub(crate) fn set_password(&mut self, password: &str) {
|
||||
// To set the password given a url and password, set url’s password to the result of running UTF-8 percent-encode
|
||||
// on password using the userinfo percent-encode set.
|
||||
self.password = percent_encode(password, PercentEncodeSet::Userinfo, false);
|
||||
}
|
||||
|
||||
pub(crate) fn serialized_host(&self) -> String {
|
||||
self.host.as_ref().expect("host should be present").serialize()
|
||||
}
|
||||
|
||||
pub(crate) fn set_paths(&mut self, paths: &[&str]) {
|
||||
self.path.clear();
|
||||
self.path.reserve(paths.len());
|
||||
for segment in paths {
|
||||
self.path.push(percent_encode(segment, PercentEncodeSet::Path, false));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_query(&mut self, query: Option<String>) {
|
||||
self.query = query;
|
||||
}
|
||||
|
||||
pub(crate) fn set_fragment(&mut self, fragment: Option<String>) {
|
||||
self.fragment = fragment;
|
||||
}
|
||||
|
||||
pub(crate) fn set_has_an_opaque_path(&mut self, value: bool) {
|
||||
self.has_opaque_path = value;
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#is-special
|
||||
pub(crate) fn is_special(&self) -> bool {
|
||||
is_special_scheme(self.scheme.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#concept-host
|
||||
// A host is a domain, an IP address, an opaque host, or an empty host. Typically a host serves as a network address,
|
||||
// but it is sometimes used as opaque identifier in URLs where a network address is not necessary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Host {
|
||||
Domain(String),
|
||||
Ipv4(Ipv4Addr),
|
||||
Ipv6(Ipv6Addr),
|
||||
Opaque(String),
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub(crate) fn is_empty_host(&self) -> bool {
|
||||
matches!(self, Self::Domain(host) if host.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
// https://url.spec.whatwg.org/#url-representation
|
||||
// A URL is a struct that represents a universal identifier.
|
||||
// To disambiguate from a valid URL string it can also be referred to as a URL record.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Url {
|
||||
// A URL’s scheme is an ASCII string that identifies the type of URL and can be used to dispatch a URL for further
|
||||
// processing after parsing. It is initially the empty string.
|
||||
pub(crate) scheme: String,
|
||||
|
||||
// A URL’s username is an ASCII string identifying a username. It is initially the empty string.
|
||||
pub(crate) username: String,
|
||||
|
||||
// A URL’s password is an ASCII string identifying a password. It is initially the empty string.
|
||||
pub(crate) password: String,
|
||||
|
||||
// A URL’s host is null or a host. It is initially null.
|
||||
pub(crate) host: Option<Host>,
|
||||
|
||||
// A URL’s port is either null or a 16-bit unsigned integer that identifies a networking port. It is initially null.
|
||||
pub(crate) port: Option<u16>,
|
||||
|
||||
// A URL’s path is either a URL path segment or a list of zero or more URL path segments, usually identifying a location. It is initially « ».
|
||||
// A URL path segment is an ASCII string. It commonly refers to a directory or a file, but has no predefined meaning.
|
||||
pub(crate) path: Vec<String>,
|
||||
pub(crate) has_opaque_path: bool,
|
||||
|
||||
// A URL’s query is either null or an ASCII string. It is initially null.
|
||||
pub(crate) query: Option<String>,
|
||||
|
||||
// A URL’s fragment is either null or an ASCII string that can be used for further processing on the resource the
|
||||
// URL’s other components identify. It is initially null.
|
||||
pub(crate) fragment: Option<String>,
|
||||
}
|
||||
235
Libraries/LibURL/RustIntegration.cpp
Normal file
235
Libraries/LibURL/RustIntegration.cpp
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (c) 2026, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Array.h>
|
||||
#include <AK/IPv4Address.h>
|
||||
#include <AK/IPv6Address.h>
|
||||
#include <AK/StringUtils.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibTextCodec/Encoder.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibURL/RustFFI.h>
|
||||
#include <LibURL/RustIntegration.h>
|
||||
#include <LibURL/URL.h>
|
||||
|
||||
namespace URL::RustIntegration {
|
||||
|
||||
static String string_from_ffi(FFI::RustUrlByteSlice slice)
|
||||
{
|
||||
return String::from_ascii_without_validation({ reinterpret_cast<char const*>(slice.data), slice.length });
|
||||
}
|
||||
|
||||
static FFI::FfiUrlHost host_to_ffi(Optional<Host> const& host)
|
||||
{
|
||||
FFI::FfiUrlHost result {};
|
||||
if (!host.has_value()) {
|
||||
result.has_host = false;
|
||||
return result;
|
||||
}
|
||||
result.has_host = true;
|
||||
host->value().visit(
|
||||
[&](IPv4Address const& addr) {
|
||||
result.kind = FFI::RustUrlHostKind::Ipv4;
|
||||
u32 const n = addr.to_u32();
|
||||
result.ipv4[0] = static_cast<u8>(n >> 24);
|
||||
result.ipv4[1] = static_cast<u8>(n >> 16);
|
||||
result.ipv4[2] = static_cast<u8>(n >> 8);
|
||||
result.ipv4[3] = static_cast<u8>(n);
|
||||
},
|
||||
[&](IPv6Address const& addr) {
|
||||
result.kind = FFI::RustUrlHostKind::Ipv6;
|
||||
for (size_t i = 0; i < 8; i++) {
|
||||
u16 piece = addr[i];
|
||||
result.ipv6[i * 2] = static_cast<u8>(piece >> 8);
|
||||
result.ipv6[(i * 2) + 1] = static_cast<u8>(piece & 0xff);
|
||||
}
|
||||
},
|
||||
[&](String const& str) {
|
||||
result.kind = FFI::RustUrlHostKind::String;
|
||||
result.string_data = reinterpret_cast<uint8_t const*>(str.bytes().data());
|
||||
result.string_length = str.bytes().size();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static Optional<URL> url_from_ffi(FFI::RustFfiUrl const& ffi)
|
||||
{
|
||||
URL url;
|
||||
url.set_scheme(string_from_ffi(ffi.scheme));
|
||||
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 (ffi.has_port)
|
||||
url.set_port(ffi.port);
|
||||
else
|
||||
url.set_port({});
|
||||
|
||||
url.set_has_an_opaque_path(ffi.has_opaque_path);
|
||||
Vector<String> paths;
|
||||
if (ffi.path_segments) {
|
||||
paths.ensure_capacity(ffi.path_segment_count);
|
||||
for (size_t i = 0; i < ffi.path_segment_count; i++)
|
||||
paths.unchecked_append(string_from_ffi(ffi.path_segments[i]));
|
||||
}
|
||||
url.set_raw_paths(move(paths));
|
||||
|
||||
if (ffi.has_query)
|
||||
url.set_query(string_from_ffi(ffi.query));
|
||||
else
|
||||
url.set_query({});
|
||||
|
||||
if (ffi.has_fragment)
|
||||
url.set_fragment(string_from_ffi(ffi.fragment));
|
||||
else
|
||||
url.set_fragment({});
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
struct UrlFfiStorage {
|
||||
Vector<FFI::RustUrlByteSlice> path_segments;
|
||||
FFI::RustFfiUrl ffi_url {};
|
||||
};
|
||||
|
||||
static UrlFfiStorage url_to_ffi(URL const& url)
|
||||
{
|
||||
UrlFfiStorage storage;
|
||||
|
||||
storage.ffi_url.scheme = { reinterpret_cast<uint8_t const*>(url.scheme().bytes().data()), url.scheme().bytes().size() };
|
||||
storage.ffi_url.username = { reinterpret_cast<uint8_t const*>(url.username().bytes().data()), url.username().bytes().size() };
|
||||
storage.ffi_url.password = { reinterpret_cast<uint8_t const*>(url.password().bytes().data()), url.password().bytes().size() };
|
||||
|
||||
storage.ffi_url.host = host_to_ffi(url.host());
|
||||
|
||||
storage.ffi_url.has_port = url.port().has_value();
|
||||
storage.ffi_url.port = url.port().value_or(0);
|
||||
|
||||
storage.path_segments.ensure_capacity(url.paths().size());
|
||||
for (auto const& segment : url.paths()) {
|
||||
storage.path_segments.unchecked_append({
|
||||
reinterpret_cast<uint8_t const*>(segment.bytes().data()),
|
||||
segment.bytes().size(),
|
||||
});
|
||||
}
|
||||
storage.ffi_url.path_segments = storage.path_segments.data();
|
||||
storage.ffi_url.path_segment_count = storage.path_segments.size();
|
||||
|
||||
storage.ffi_url.has_opaque_path = url.has_an_opaque_path();
|
||||
|
||||
storage.ffi_url.has_query = url.query().has_value();
|
||||
if (url.query().has_value())
|
||||
storage.ffi_url.query = { reinterpret_cast<uint8_t const*>(url.query()->bytes().data()), url.query()->bytes().size() };
|
||||
|
||||
storage.ffi_url.has_fragment = url.fragment().has_value();
|
||||
if (url.fragment().has_value())
|
||||
storage.ffi_url.fragment = { reinterpret_cast<uint8_t const*>(url.fragment()->bytes().data()), url.fragment()->bytes().size() };
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
struct ParseCallbackCtx {
|
||||
Optional<URL>* result;
|
||||
URL* url_inout;
|
||||
};
|
||||
|
||||
static void on_basic_parse_complete(void* ctx_ptr, FFI::RustFfiUrl const* ffi_result)
|
||||
{
|
||||
auto* ctx = static_cast<ParseCallbackCtx*>(ctx_ptr);
|
||||
if (!ffi_result)
|
||||
return;
|
||||
*ctx->result = url_from_ffi(*ffi_result);
|
||||
if (ctx->url_inout && ctx->result->has_value())
|
||||
*ctx->url_inout = **ctx->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) {
|
||||
return static_cast<FFI::State>(to_underlying(state));
|
||||
};
|
||||
|
||||
Optional<UrlFfiStorage> base_storage;
|
||||
if (base_url.has_value())
|
||||
base_storage = url_to_ffi(*base_url);
|
||||
|
||||
Optional<UrlFfiStorage> url_storage;
|
||||
if (url)
|
||||
url_storage = url_to_ffi(*url);
|
||||
|
||||
FFI::RustBasicParseOptions options {
|
||||
.has_base_url = base_url.has_value(),
|
||||
.has_url = url != nullptr,
|
||||
.base_url = base_storage.has_value() ? base_storage->ffi_url : FFI::RustFfiUrl {},
|
||||
.url = url_storage.has_value() ? url_storage->ffi_url : FFI::RustFfiUrl {},
|
||||
.has_state_override = state_override.has_value(),
|
||||
.state_override = state_override.map(state_override_from_cpp).value_or(FFI::State::SchemeStart),
|
||||
.encoding = {
|
||||
reinterpret_cast<uint8_t const*>(encoding.has_value() ? encoding->characters_without_null_termination() : nullptr),
|
||||
encoding.value_or(""sv).length(),
|
||||
},
|
||||
};
|
||||
|
||||
Optional<URL> result;
|
||||
ParseCallbackCtx ctx { .result = &result, .url_inout = url };
|
||||
bool const did_succeed = rust_url_basic_parse(
|
||||
reinterpret_cast<uint8_t const*>(input.characters_without_null_termination()),
|
||||
input.length(),
|
||||
&options,
|
||||
&ctx,
|
||||
on_basic_parse_complete);
|
||||
if (!did_succeed)
|
||||
return {};
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace URL::FFI {
|
||||
|
||||
extern "C" bool textcodec_rust_encode(uint8_t const* encoding, size_t encoding_length, uint8_t const* input, size_t input_length, void* ctx, FfiByteFn on_byte, FfiCodePointFn on_error)
|
||||
{
|
||||
auto encoder = TextCodec::encoder_for(StringView { encoding, encoding_length });
|
||||
if (!encoder.has_value())
|
||||
return false;
|
||||
|
||||
auto result = encoder->process(
|
||||
Utf8View { StringView { input, input_length } },
|
||||
[&](u8 byte) -> ErrorOr<void> {
|
||||
on_byte(ctx, byte);
|
||||
return {};
|
||||
},
|
||||
[&](u32 error) -> ErrorOr<void> {
|
||||
on_error(ctx, error);
|
||||
return {};
|
||||
});
|
||||
return !result.is_error();
|
||||
}
|
||||
|
||||
}
|
||||
17
Libraries/LibURL/RustIntegration.h
Normal file
17
Libraries/LibURL/RustIntegration.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2026, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibURL/URL.h>
|
||||
|
||||
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 = {});
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue