LibURL: Implement URLPattern in Rust
This commit is contained in:
parent
d44ff6628f
commit
b31e0df363
27 changed files with 5166 additions and 20 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -741,6 +741,7 @@ name = "liburl_rust"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cbindgen",
|
||||
"libregex_rust",
|
||||
"libunicode_rust",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
# After changing dependencies, regenerate the Flatpak sources:
|
||||
# python3 Meta/CMake/flatpak/generate-cargo-sources.py
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ ladybird_lib(LibURL url)
|
|||
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_link_libraries(liburl_rust INTERFACE LibUnicode LibTextCodec LibRegex)
|
||||
target_compile_definitions(LibURL PRIVATE ENABLE_RUST)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ edition = "2024"
|
|||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
libregex_rust = { path = "../../LibRegex/Rust" }
|
||||
libunicode_rust = { path = "../../LibUnicode/Rust" }
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
pub mod pattern;
|
||||
pub mod url;
|
||||
|
|
|
|||
640
Libraries/LibURL/Rust/src/ffi/pattern.rs
Normal file
640
Libraries/LibURL/Rust/src/ffi/pattern.rs
Normal file
|
|
@ -0,0 +1,640 @@
|
|||
/*
|
||||
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::panic::catch_unwind;
|
||||
|
||||
use crate::ffi::url::RustFfiUrl;
|
||||
use crate::ffi::url::RustUrlByteSlice;
|
||||
use crate::pattern::ComponentResult;
|
||||
use crate::pattern::GroupMatch;
|
||||
use crate::pattern::IgnoreCase;
|
||||
use crate::pattern::Init;
|
||||
use crate::pattern::Input;
|
||||
use crate::pattern::MatchInput;
|
||||
use crate::pattern::Pattern;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::pattern::Result as MatchResult;
|
||||
use crate::url::Url;
|
||||
|
||||
/// Opaque URLPattern handle.
|
||||
pub struct RustUrlPattern(Pattern);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RustUrlPatternInit {
|
||||
pub has_protocol: bool,
|
||||
pub protocol: RustUrlByteSlice,
|
||||
pub has_username: bool,
|
||||
pub username: RustUrlByteSlice,
|
||||
pub has_password: bool,
|
||||
pub password: RustUrlByteSlice,
|
||||
pub has_hostname: bool,
|
||||
pub hostname: RustUrlByteSlice,
|
||||
pub has_port: bool,
|
||||
pub port: RustUrlByteSlice,
|
||||
pub has_pathname: bool,
|
||||
pub pathname: RustUrlByteSlice,
|
||||
pub has_search: bool,
|
||||
pub search: RustUrlByteSlice,
|
||||
pub has_hash: bool,
|
||||
pub hash: RustUrlByteSlice,
|
||||
pub has_base_url: bool,
|
||||
pub base_url: RustUrlByteSlice,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RustUrlPatternComponent {
|
||||
Protocol,
|
||||
Username,
|
||||
Password,
|
||||
Hostname,
|
||||
Port,
|
||||
Pathname,
|
||||
Search,
|
||||
Hash,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RustUrlPatternGroup {
|
||||
pub name: RustUrlByteSlice,
|
||||
pub has_value: bool,
|
||||
pub value: RustUrlByteSlice,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RustUrlPatternComponentResult {
|
||||
pub input: RustUrlByteSlice,
|
||||
pub groups: *const RustUrlPatternGroup,
|
||||
pub group_count: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RustUrlPatternResultInput {
|
||||
pub is_string: bool,
|
||||
pub string: RustUrlByteSlice,
|
||||
pub init: RustUrlPatternInit,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct RustUrlPatternExecResult {
|
||||
pub inputs: *const RustUrlPatternResultInput,
|
||||
pub input_count: usize,
|
||||
pub protocol: RustUrlPatternComponentResult,
|
||||
pub username: RustUrlPatternComponentResult,
|
||||
pub password: RustUrlPatternComponentResult,
|
||||
pub hostname: RustUrlPatternComponentResult,
|
||||
pub port: RustUrlPatternComponentResult,
|
||||
pub pathname: RustUrlPatternComponentResult,
|
||||
pub search: RustUrlPatternComponentResult,
|
||||
pub hash: RustUrlPatternComponentResult,
|
||||
}
|
||||
|
||||
pub type FfiUrlPatternResultFn = unsafe extern "C" fn(*mut c_void, *const RustUrlPatternExecResult);
|
||||
|
||||
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) };
|
||||
String::from_utf8_lossy(bytes).into_owned()
|
||||
}
|
||||
|
||||
fn byte_slice(input: &str) -> RustUrlByteSlice {
|
||||
RustUrlByteSlice {
|
||||
data: input.as_ptr(),
|
||||
length: input.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_byte_slice() -> RustUrlByteSlice {
|
||||
RustUrlByteSlice {
|
||||
data: std::ptr::null(),
|
||||
length: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_string_from_raw(ptr: *const u8, len: usize) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(decode_utf8(RustUrlByteSlice { data: ptr, length: len }))
|
||||
}
|
||||
|
||||
fn optional_string_from_ffi(has_value: bool, slice: RustUrlByteSlice) -> Option<String> {
|
||||
if !has_value {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(decode_utf8(slice))
|
||||
}
|
||||
|
||||
fn init_from_ffi(ffi: &RustUrlPatternInit) -> Init {
|
||||
Init {
|
||||
protocol: optional_string_from_ffi(ffi.has_protocol, ffi.protocol),
|
||||
username: optional_string_from_ffi(ffi.has_username, ffi.username),
|
||||
password: optional_string_from_ffi(ffi.has_password, ffi.password),
|
||||
hostname: optional_string_from_ffi(ffi.has_hostname, ffi.hostname),
|
||||
port: optional_string_from_ffi(ffi.has_port, ffi.port),
|
||||
pathname: optional_string_from_ffi(ffi.has_pathname, ffi.pathname),
|
||||
search: optional_string_from_ffi(ffi.has_search, ffi.search),
|
||||
hash: optional_string_from_ffi(ffi.has_hash, ffi.hash),
|
||||
base_url: optional_string_from_ffi(ffi.has_base_url, ffi.base_url),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_to_ffi(init: &Init) -> RustUrlPatternInit {
|
||||
fn optional_string_to_ffi(value: Option<&String>) -> (bool, RustUrlByteSlice) {
|
||||
value
|
||||
.map(|value| (true, byte_slice(value)))
|
||||
.unwrap_or((false, empty_byte_slice()))
|
||||
}
|
||||
|
||||
let (has_protocol, protocol) = optional_string_to_ffi(init.protocol.as_ref());
|
||||
let (has_username, username) = optional_string_to_ffi(init.username.as_ref());
|
||||
let (has_password, password) = optional_string_to_ffi(init.password.as_ref());
|
||||
let (has_hostname, hostname) = optional_string_to_ffi(init.hostname.as_ref());
|
||||
let (has_port, port) = optional_string_to_ffi(init.port.as_ref());
|
||||
let (has_pathname, pathname) = optional_string_to_ffi(init.pathname.as_ref());
|
||||
let (has_search, search) = optional_string_to_ffi(init.search.as_ref());
|
||||
let (has_hash, hash) = optional_string_to_ffi(init.hash.as_ref());
|
||||
let (has_base_url, base_url) = optional_string_to_ffi(init.base_url.as_ref());
|
||||
|
||||
RustUrlPatternInit {
|
||||
has_protocol,
|
||||
protocol,
|
||||
has_username,
|
||||
username,
|
||||
has_password,
|
||||
password,
|
||||
has_hostname,
|
||||
hostname,
|
||||
has_port,
|
||||
port,
|
||||
has_pathname,
|
||||
pathname,
|
||||
has_search,
|
||||
search,
|
||||
has_hash,
|
||||
hash,
|
||||
has_base_url,
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn url_from_ffi(ffi: &RustFfiUrl) -> Url {
|
||||
super::url::url_from_ffi(ffi)
|
||||
}
|
||||
|
||||
fn component_pattern_string(pattern: &Pattern, component: RustUrlPatternComponent) -> &str {
|
||||
match component {
|
||||
RustUrlPatternComponent::Protocol => &pattern.protocol_component().pattern_string,
|
||||
RustUrlPatternComponent::Username => &pattern.username_component().pattern_string,
|
||||
RustUrlPatternComponent::Password => &pattern.password_component().pattern_string,
|
||||
RustUrlPatternComponent::Hostname => &pattern.hostname_component().pattern_string,
|
||||
RustUrlPatternComponent::Port => &pattern.port_component().pattern_string,
|
||||
RustUrlPatternComponent::Pathname => &pattern.pathname_component().pattern_string,
|
||||
RustUrlPatternComponent::Search => &pattern.search_component().pattern_string,
|
||||
RustUrlPatternComponent::Hash => &pattern.hash_component().pattern_string,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExecResultStorage {
|
||||
inputs: Vec<RustUrlPatternResultInput>,
|
||||
protocol_groups: Vec<RustUrlPatternGroup>,
|
||||
username_groups: Vec<RustUrlPatternGroup>,
|
||||
password_groups: Vec<RustUrlPatternGroup>,
|
||||
hostname_groups: Vec<RustUrlPatternGroup>,
|
||||
port_groups: Vec<RustUrlPatternGroup>,
|
||||
pathname_groups: Vec<RustUrlPatternGroup>,
|
||||
search_groups: Vec<RustUrlPatternGroup>,
|
||||
hash_groups: Vec<RustUrlPatternGroup>,
|
||||
ffi_result: Option<RustUrlPatternExecResult>,
|
||||
}
|
||||
|
||||
impl ExecResultStorage {
|
||||
fn input_to_ffi(input: &Input) -> RustUrlPatternResultInput {
|
||||
match input {
|
||||
Input::String(value) => RustUrlPatternResultInput {
|
||||
is_string: true,
|
||||
string: byte_slice(value),
|
||||
init: RustUrlPatternInit {
|
||||
has_protocol: false,
|
||||
protocol: empty_byte_slice(),
|
||||
has_username: false,
|
||||
username: empty_byte_slice(),
|
||||
has_password: false,
|
||||
password: empty_byte_slice(),
|
||||
has_hostname: false,
|
||||
hostname: empty_byte_slice(),
|
||||
has_port: false,
|
||||
port: empty_byte_slice(),
|
||||
has_pathname: false,
|
||||
pathname: empty_byte_slice(),
|
||||
has_search: false,
|
||||
search: empty_byte_slice(),
|
||||
has_hash: false,
|
||||
hash: empty_byte_slice(),
|
||||
has_base_url: false,
|
||||
base_url: empty_byte_slice(),
|
||||
},
|
||||
},
|
||||
Input::Init(value) => RustUrlPatternResultInput {
|
||||
is_string: false,
|
||||
string: empty_byte_slice(),
|
||||
init: init_to_ffi(value),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn component_result_to_ffi(
|
||||
component_result: &ComponentResult,
|
||||
groups: &mut Vec<RustUrlPatternGroup>,
|
||||
) -> RustUrlPatternComponentResult {
|
||||
groups.reserve(component_result.groups.len());
|
||||
for (name, value) in &component_result.groups {
|
||||
let (has_value, value) = match value {
|
||||
GroupMatch::String(value) => (true, byte_slice(value)),
|
||||
GroupMatch::Empty => (false, empty_byte_slice()),
|
||||
};
|
||||
|
||||
groups.push(RustUrlPatternGroup {
|
||||
name: byte_slice(name),
|
||||
has_value,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
RustUrlPatternComponentResult {
|
||||
input: byte_slice(&component_result.input),
|
||||
groups: groups.as_ptr(),
|
||||
group_count: groups.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_from_result(&mut self, result: &MatchResult) {
|
||||
self.inputs.reserve(result.inputs.len());
|
||||
for input in &result.inputs {
|
||||
self.inputs.push(Self::input_to_ffi(input));
|
||||
}
|
||||
|
||||
let protocol = Self::component_result_to_ffi(&result.protocol, &mut self.protocol_groups);
|
||||
let username = Self::component_result_to_ffi(&result.username, &mut self.username_groups);
|
||||
let password = Self::component_result_to_ffi(&result.password, &mut self.password_groups);
|
||||
let hostname = Self::component_result_to_ffi(&result.hostname, &mut self.hostname_groups);
|
||||
let port = Self::component_result_to_ffi(&result.port, &mut self.port_groups);
|
||||
let pathname = Self::component_result_to_ffi(&result.pathname, &mut self.pathname_groups);
|
||||
let search = Self::component_result_to_ffi(&result.search, &mut self.search_groups);
|
||||
let hash = Self::component_result_to_ffi(&result.hash, &mut self.hash_groups);
|
||||
|
||||
self.ffi_result = Some(RustUrlPatternExecResult {
|
||||
inputs: self.inputs.as_ptr(),
|
||||
input_count: self.inputs.len(),
|
||||
protocol,
|
||||
username,
|
||||
password,
|
||||
hostname,
|
||||
port,
|
||||
pathname,
|
||||
search,
|
||||
hash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn write_error(error_out: *mut *const u8, error_len_out: *mut usize, message: String) {
|
||||
if error_out.is_null() || error_len_out.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let leaked = message.into_bytes().into_boxed_slice();
|
||||
// SAFETY: caller provided valid output pointers.
|
||||
unsafe {
|
||||
*error_len_out = leaked.len();
|
||||
*error_out = Box::into_raw(leaked) as *const u8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile a URLPattern from a constructor string.
|
||||
///
|
||||
/// # Safety
|
||||
/// `input` must be valid for `input_len` bytes of UTF-8. If `base_url` is non-null,
|
||||
/// it must be valid for `base_url_len` bytes of UTF-8. `error_out` and
|
||||
/// `error_len_out` must be valid pointers when non-null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_create_from_string(
|
||||
input: *const u8,
|
||||
input_len: usize,
|
||||
base_url: *const u8,
|
||||
base_url_len: usize,
|
||||
ignore_case: IgnoreCase,
|
||||
error_out: *mut *const u8,
|
||||
error_len_out: *mut usize,
|
||||
) -> *mut RustUrlPattern {
|
||||
abort_on_panic(|| {
|
||||
if input.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees input is valid for input_len bytes.
|
||||
let input_bytes = unsafe { std::slice::from_raw_parts(input, input_len) };
|
||||
let Ok(input_str) = std::str::from_utf8(input_bytes) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let base_url = optional_string_from_raw(base_url, base_url_len);
|
||||
|
||||
match Pattern::create(&Input::String(input_str.to_string()), &base_url, ignore_case) {
|
||||
Ok(pattern) => Box::into_raw(Box::new(RustUrlPattern(pattern))),
|
||||
Err(error) => {
|
||||
write_error(error_out, error_len_out, error.message);
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Compile a URLPattern from an init dictionary.
|
||||
///
|
||||
/// # Safety
|
||||
/// `init` must be a valid pointer. `error_out` and `error_len_out` must be
|
||||
/// valid pointers when non-null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_create_from_init(
|
||||
init: *const RustUrlPatternInit,
|
||||
ignore_case: IgnoreCase,
|
||||
error_out: *mut *const u8,
|
||||
error_len_out: *mut usize,
|
||||
) -> *mut RustUrlPattern {
|
||||
abort_on_panic(|| {
|
||||
let Some(init) = (unsafe { init.as_ref() }) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let init = init_from_ffi(init);
|
||||
|
||||
match Pattern::create(&Input::Init(init), &None, ignore_case) {
|
||||
Ok(pattern) => Box::into_raw(Box::new(RustUrlPattern(pattern))),
|
||||
Err(error) => {
|
||||
write_error(error_out, error_len_out, error.message);
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Free an error string returned by a URLPattern create function.
|
||||
///
|
||||
/// # Safety
|
||||
/// `error` must be a pointer returned via a create function's `error_out`, or null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_free_error(error: *mut u8, len: usize) {
|
||||
if !error.is_null() {
|
||||
// SAFETY: pointer originates from `Box::into_raw` in `write_error`.
|
||||
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(error, len)) });
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a compiled URLPattern.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` must be a valid pointer returned by a create function, or null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_free(pattern: *mut RustUrlPattern) {
|
||||
if !pattern.is_null() {
|
||||
// SAFETY: pointer originates from `Box::into_raw` in a create function.
|
||||
drop(unsafe { Box::from_raw(pattern) });
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether the pattern contains regexp groups.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` must be a valid pointer returned by a create function.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_has_regexp_groups(pattern: *const RustUrlPattern) -> bool {
|
||||
if pattern.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pattern is valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
pattern.0.has_regexp_groups()
|
||||
}
|
||||
|
||||
/// Return a component's canonical pattern string as a borrowed slice.
|
||||
///
|
||||
/// The returned slice is valid until `pattern` is freed.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` must be a valid pointer returned by a create function.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_component_pattern_string(
|
||||
pattern: *const RustUrlPattern,
|
||||
component: RustUrlPatternComponent,
|
||||
) -> RustUrlByteSlice {
|
||||
if pattern.is_null() {
|
||||
return empty_byte_slice();
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pattern is valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
let pattern_string = component_pattern_string(&pattern.0, component);
|
||||
RustUrlByteSlice {
|
||||
data: pattern_string.as_ptr(),
|
||||
length: pattern_string.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_match_result(
|
||||
result: PatternErrorOr<Option<MatchResult>>,
|
||||
error_out: *mut *const u8,
|
||||
error_len_out: *mut usize,
|
||||
ctx: *mut c_void,
|
||||
on_complete: FfiUrlPatternResultFn,
|
||||
) -> bool {
|
||||
match result {
|
||||
Ok(Some(result)) => {
|
||||
let mut storage = ExecResultStorage::default();
|
||||
storage.fill_from_result(&result);
|
||||
// SAFETY: callback is provided by caller and `ffi_result` points into `storage`,
|
||||
// which remains alive for the duration of the callback.
|
||||
unsafe { on_complete(ctx, storage.ffi_result.as_ref().unwrap()) };
|
||||
true
|
||||
}
|
||||
Ok(None) => {
|
||||
// SAFETY: callback is provided by caller.
|
||||
unsafe { on_complete(ctx, std::ptr::null()) };
|
||||
true
|
||||
}
|
||||
Err(error) => {
|
||||
write_error(error_out, error_len_out, error.message);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a compiled pattern against a string input and return a borrowed view
|
||||
/// of the match result to a callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` must be valid. `input` must be valid for `input_len` bytes of UTF-8.
|
||||
/// If `base_url` is non-null, it must be valid for `base_url_len` bytes of UTF-8.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_exec_string(
|
||||
pattern: *const RustUrlPattern,
|
||||
input: *const u8,
|
||||
input_len: usize,
|
||||
base_url: *const u8,
|
||||
base_url_len: usize,
|
||||
error_out: *mut *const u8,
|
||||
error_len_out: *mut usize,
|
||||
ctx: *mut c_void,
|
||||
on_complete: FfiUrlPatternResultFn,
|
||||
) -> bool {
|
||||
abort_on_panic(|| {
|
||||
if pattern.is_null() || input.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pointers are valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
let input_bytes = unsafe { std::slice::from_raw_parts(input, input_len) };
|
||||
let Ok(input_str) = std::str::from_utf8(input_bytes) else {
|
||||
return false;
|
||||
};
|
||||
let base_url = optional_string_from_raw(base_url, base_url_len);
|
||||
|
||||
exec_match_result(
|
||||
pattern.0.r#match(&MatchInput::String(input_str.to_string()), &base_url),
|
||||
error_out,
|
||||
error_len_out,
|
||||
ctx,
|
||||
on_complete,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a compiled pattern against an init dictionary and return a borrowed
|
||||
/// view of the match result to a callback.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` and `input` must be valid pointers.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_exec_init(
|
||||
pattern: *const RustUrlPattern,
|
||||
input: *const RustUrlPatternInit,
|
||||
error_out: *mut *const u8,
|
||||
error_len_out: *mut usize,
|
||||
ctx: *mut c_void,
|
||||
on_complete: FfiUrlPatternResultFn,
|
||||
) -> bool {
|
||||
abort_on_panic(|| {
|
||||
if pattern.is_null() || input.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pointers are valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
let input = init_from_ffi(unsafe { &*input });
|
||||
|
||||
exec_match_result(
|
||||
pattern.0.r#match(&MatchInput::Init(input), &None),
|
||||
error_out,
|
||||
error_len_out,
|
||||
ctx,
|
||||
on_complete,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Test a compiled pattern against a string input.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` must be valid. `input` must be valid for `input_len` bytes of UTF-8.
|
||||
/// If `base_url` is non-null, it must be valid for `base_url_len` bytes of UTF-8.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_test_string(
|
||||
pattern: *const RustUrlPattern,
|
||||
input: *const u8,
|
||||
input_len: usize,
|
||||
base_url: *const u8,
|
||||
base_url_len: usize,
|
||||
) -> bool {
|
||||
abort_on_panic(|| {
|
||||
if pattern.is_null() || input.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pointers are valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
let input_bytes = unsafe { std::slice::from_raw_parts(input, input_len) };
|
||||
let Ok(input_str) = std::str::from_utf8(input_bytes) else {
|
||||
return false;
|
||||
};
|
||||
let base_url = optional_string_from_raw(base_url, base_url_len);
|
||||
|
||||
matches!(
|
||||
pattern.0.r#match(&MatchInput::String(input_str.to_string()), &base_url),
|
||||
Ok(Some(_))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Test a compiled pattern against an init dictionary.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` and `input` must be valid pointers.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_test_init(
|
||||
pattern: *const RustUrlPattern,
|
||||
input: *const RustUrlPatternInit,
|
||||
) -> bool {
|
||||
abort_on_panic(|| {
|
||||
if pattern.is_null() || input.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pointers are valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
let input = init_from_ffi(unsafe { &*input });
|
||||
|
||||
matches!(pattern.0.r#match(&MatchInput::Init(input), &None), Ok(Some(_)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Test a compiled pattern against a parsed URL.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` and `input` must be valid pointers.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_url_pattern_test_url(pattern: *const RustUrlPattern, input: *const RustFfiUrl) -> bool {
|
||||
abort_on_panic(|| {
|
||||
if pattern.is_null() || input.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: caller guarantees pointers are valid.
|
||||
let pattern = unsafe { &*pattern };
|
||||
let input = url_from_ffi(unsafe { &*input });
|
||||
|
||||
matches!(pattern.0.r#match(&MatchInput::Url(input), &None), Ok(Some(_)))
|
||||
})
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ fn host_to_ffi(host: Option<&Host>) -> FfiUrlHost {
|
|||
}
|
||||
}
|
||||
|
||||
fn url_from_ffi(ffi: &RustFfiUrl) -> Url {
|
||||
pub(crate) 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.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
mod rust_allocator;
|
||||
|
||||
mod ffi;
|
||||
pub mod pattern;
|
||||
mod textcodec;
|
||||
pub mod url;
|
||||
|
||||
|
|
|
|||
289
Libraries/LibURL/Rust/src/pattern/canonicalization.rs
Normal file
289
Libraries/LibURL/Rust/src/pattern/canonicalization.rs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::pattern::ErrorInfo;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::url::BasicParseOptions;
|
||||
use crate::url::State;
|
||||
use crate::url::Url;
|
||||
use crate::url::basic_parse;
|
||||
use crate::url::basic_parse_into;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-create-a-dummy-url
|
||||
fn create_a_dummy_url() -> Url {
|
||||
// 1. Let dummyInput be "https://dummy.invalid/".
|
||||
// 2. Return the result of running the basic URL parser on dummyInput.
|
||||
basic_parse("https://dummy.invalid/", BasicParseOptions::new()).unwrap()
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-protocol
|
||||
pub fn canonicalize_a_protocol(value: &str) -> PatternErrorOr<String> {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
|
||||
// 2. Let parseResult be the result of running the basic URL parser given value followed by "://dummy.invalid/".
|
||||
// NOTE: Note, state override is not used here because it enforces restrictions that are only appropriate for the
|
||||
// protocol setter. Instead we use the protocol to parse a dummy URL using the normal parsing entry point.
|
||||
let parse_result = basic_parse(&format!("{value}://dummy.invalid"), BasicParseOptions::new());
|
||||
|
||||
// 4. If parseResult is failure, then throw a TypeError.
|
||||
let Some(parse_result) = parse_result else {
|
||||
return Err(ErrorInfo::new("Failed to canonicalize URL protocol string"));
|
||||
};
|
||||
|
||||
// 5. Return parseResult’s scheme.
|
||||
Ok(parse_result.scheme)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-username
|
||||
pub fn canonicalize_a_username(value: &str) -> String {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. Set the username given dummyURL and value.
|
||||
dummy_url.set_username(value);
|
||||
|
||||
// 4. Return dummyURL’s username.
|
||||
dummy_url.username
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-password
|
||||
pub fn canonicalize_a_password(value: &str) -> String {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. Set the password given dummyURL and value.
|
||||
dummy_url.set_password(value);
|
||||
|
||||
// 4. Return dummyURL’s password.
|
||||
dummy_url.password
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-hostname
|
||||
pub fn canonicalize_a_hostname(value: &str) -> PatternErrorOr<String> {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. Let parseResult be the result of running the basic URL parser given value with dummyURL
|
||||
// as url and hostname state as state override.
|
||||
let parse_result = basic_parse_into(
|
||||
value,
|
||||
&mut dummy_url,
|
||||
&BasicParseOptions::new().state_override(State::Hostname),
|
||||
);
|
||||
|
||||
// 4. If parseResult is failure, then throw a TypeError.
|
||||
if !parse_result {
|
||||
return Err(ErrorInfo::new("Failed to canonicalize URL hostname string"));
|
||||
}
|
||||
|
||||
// 5. Return dummyURL’s host, serialized, or empty string if it is null.
|
||||
if dummy_url.host.is_none() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
Ok(dummy_url.serialized_host())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-an-ipv6-hostname
|
||||
pub fn canonicalize_an_ipv6_hostname(value: &str) -> PatternErrorOr<String> {
|
||||
// 1. Let result be the empty string.
|
||||
let mut result = String::new();
|
||||
|
||||
// 2. For each code point in value interpreted as a list of code points:
|
||||
for code_point in value.chars() {
|
||||
// 1. If all of the following are true:
|
||||
// * code point is not an ASCII hex digit;
|
||||
// * code point is not U+005B ([);
|
||||
// * code point is not U+005D (]); and
|
||||
// * code point is not U+003A (:),
|
||||
// then throw a TypeError.
|
||||
if !code_point.is_ascii_hexdigit() && code_point != '[' && code_point != ']' && code_point != ':' {
|
||||
return Err(ErrorInfo::new("Failed to canonicalize IPv6 hostname string"));
|
||||
}
|
||||
|
||||
// 2. Append the result of running ASCII lowercase given code point to the end of result.
|
||||
result.push(code_point.to_ascii_lowercase());
|
||||
}
|
||||
|
||||
// 3. Return result.
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-port
|
||||
pub fn canonicalize_a_port(port_value: &str, protocol_value: &Option<String>) -> PatternErrorOr<String> {
|
||||
// 1. If portValue is the empty string, return portValue.
|
||||
if port_value.is_empty() {
|
||||
return Ok(port_value.to_string());
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. If protocolValue was given, then set dummyURL’s scheme to protocolValue.
|
||||
// NOTE: Note, we set the URL record's scheme in order for the basic URL parser to
|
||||
// recognize and normalize default port values.
|
||||
if let Some(protocol_value) = protocol_value {
|
||||
dummy_url.set_scheme(protocol_value.clone());
|
||||
}
|
||||
|
||||
// 4. Let parseResult be the result of running basic URL parser given portValue with dummyURL
|
||||
// as url and port state as state override.
|
||||
let parse_result = basic_parse_into(
|
||||
port_value,
|
||||
&mut dummy_url,
|
||||
&BasicParseOptions::new().state_override(State::Port),
|
||||
);
|
||||
|
||||
// 4. If parseResult is failure, then throw a TypeError.
|
||||
if !parse_result {
|
||||
return Err(ErrorInfo::new("Failed to canonicalize port string"));
|
||||
}
|
||||
|
||||
// 5. Return dummyURL’s port, serialized, or empty string if it is null.
|
||||
if dummy_url.port.is_none() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
Ok(dummy_url.port.unwrap().to_string())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-pathname
|
||||
pub fn canonicalize_a_pathname(value: &str) -> String {
|
||||
// 1. If value is the empty string, then return value.
|
||||
if value.is_empty() {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Let leading slash be true if the first code point in value is U+002F (/) and otherwise false.
|
||||
let leading_slash = value.as_bytes()[0] == b'/';
|
||||
|
||||
// 3. Let modified value be "/-" if leading slash is false and otherwise the empty string.
|
||||
let mut modified_value = String::new();
|
||||
if !leading_slash {
|
||||
modified_value.push_str("/-");
|
||||
}
|
||||
|
||||
// 4. Append value to the end of modified value.
|
||||
modified_value.push_str(value);
|
||||
|
||||
// 5. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 6. Empty dummyURL’s path.
|
||||
dummy_url.set_paths(&[]);
|
||||
|
||||
// 7. Run basic URL parser given modified value with dummyURL as url and path start state as state override.
|
||||
let _ = basic_parse_into(
|
||||
&modified_value,
|
||||
&mut dummy_url,
|
||||
&BasicParseOptions::new().state_override(State::PathStart),
|
||||
);
|
||||
|
||||
// 8. Let result be the result of URL path serializing dummyURL.
|
||||
let mut result = dummy_url.serialize_path();
|
||||
|
||||
// 9. If leading slash is false, then set result to the code point substring from 2 to the end of the string within result.
|
||||
if !leading_slash {
|
||||
result = result.chars().skip(2).collect();
|
||||
}
|
||||
|
||||
// 10. Return result.
|
||||
result
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-an-opaque-pathname
|
||||
pub fn canonicalize_an_opaque_pathname(value: &str) -> PatternErrorOr<String> {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. Set dummyURL’s path to the empty string.
|
||||
dummy_url.set_paths(&[""]);
|
||||
dummy_url.set_has_an_opaque_path(true);
|
||||
|
||||
// 4. Let parseResult be the result of running URL parsing given value with dummyURL as url and opaque path state as state override.
|
||||
let parse_result = basic_parse_into(
|
||||
value,
|
||||
&mut dummy_url,
|
||||
&BasicParseOptions::new().state_override(State::OpaquePath),
|
||||
);
|
||||
|
||||
// 5. If parseResult is failure, then throw a TypeError.
|
||||
if !parse_result {
|
||||
return Err(ErrorInfo::new("Failed to canonicalize opaque pathname string"));
|
||||
}
|
||||
|
||||
// 6. Return the result of URL path serializing dummyURL.
|
||||
Ok(dummy_url.serialize_path())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-search
|
||||
pub fn canonicalize_a_search(value: &str) -> String {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. Set dummyURL’s query to the empty string.
|
||||
dummy_url.set_query(Some(String::new()));
|
||||
|
||||
// 4. Run basic URL parser given value with dummyURL as url and query state as state override.
|
||||
let _ = basic_parse_into(
|
||||
value,
|
||||
&mut dummy_url,
|
||||
&BasicParseOptions::new().state_override(State::Query),
|
||||
);
|
||||
|
||||
// 5. Return dummyURL’s query.
|
||||
dummy_url.query.expect("query should be present")
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#canonicalize-a-hash
|
||||
pub fn canonicalize_a_hash(value: &str) -> String {
|
||||
// 1. If value is the empty string, return value.
|
||||
if value.is_empty() {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Let dummyURL be the result of creating a dummy URL.
|
||||
let mut dummy_url = create_a_dummy_url();
|
||||
|
||||
// 3. Set dummyURL’s fragment to the empty string.
|
||||
dummy_url.set_fragment(Some(String::new()));
|
||||
|
||||
// 4. Run basic URL parser given value with dummyURL as url and fragment state as state override.
|
||||
let _ = basic_parse_into(
|
||||
value,
|
||||
&mut dummy_url,
|
||||
&BasicParseOptions::new().state_override(State::Fragment),
|
||||
);
|
||||
|
||||
// 5. Return dummyURL’s fragment.
|
||||
dummy_url.fragment.expect("fragment should be present")
|
||||
}
|
||||
448
Libraries/LibURL/Rust/src/pattern/component.rs
Normal file
448
Libraries/LibURL/Rust/src/pattern/component.rs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
use libregex_rust::ast::Flags;
|
||||
use libregex_rust::regex::Regex;
|
||||
use libregex_rust::vm::VmResult;
|
||||
|
||||
use crate::pattern::EncodingCallback;
|
||||
use crate::pattern::ErrorInfo;
|
||||
use crate::pattern::Options;
|
||||
use crate::pattern::Part;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::pattern::PatternParser;
|
||||
use crate::pattern::escape_a_regexp_string;
|
||||
use crate::pattern::full_wildcard_regexp_value;
|
||||
use crate::pattern::generate_a_pattern_string;
|
||||
use crate::pattern::generate_a_segment_wildcard_regexp;
|
||||
use crate::pattern::part::Modifier as PartModifier;
|
||||
use crate::pattern::part::Type as PartType;
|
||||
use crate::url::special_schemes;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#component
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Component {
|
||||
// https://urlpattern.spec.whatwg.org/#component-pattern-string
|
||||
// pattern string, a well formed pattern string
|
||||
pub pattern_string: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#component-regular-expression
|
||||
// regular expression, a RegExp
|
||||
pub regular_expression: Option<RegularExpression>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#component-group-name-list
|
||||
// group name list, a list of strings
|
||||
pub group_name_list: Vec<String>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#component-has-regexp-groups
|
||||
// has regexp groups, a boolean
|
||||
pub has_regexp_groups: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegularExpression(Rc<Regex>);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GroupMatch {
|
||||
String(String),
|
||||
Empty,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#dictdef-urlpatterncomponentresult
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Result {
|
||||
pub input: String,
|
||||
pub groups: BTreeMap<String, GroupMatch>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ExecutionResult {
|
||||
pub success: bool,
|
||||
pub captures: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#generate-a-regular-expression-and-name-list
|
||||
struct RegularExpressionAndNameList {
|
||||
regular_expression: String,
|
||||
name_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl Component {
|
||||
// https://urlpattern.spec.whatwg.org/#compile-a-component
|
||||
pub fn compile(input: &str, encoding_callback: EncodingCallback, options: &Options) -> PatternErrorOr<Self> {
|
||||
// 1. Let part list be the result of running parse a pattern string given input, options, and encoding callback.
|
||||
let part_list = PatternParser::parse(input, options, encoding_callback)?;
|
||||
|
||||
// 2. Let (regular expression string, name list) be the result of running generate a regular expression and name
|
||||
// list given part list and options.
|
||||
let RegularExpressionAndNameList {
|
||||
regular_expression: regular_expression_string,
|
||||
name_list,
|
||||
} = generate_a_regular_expression_and_name_list(&part_list, options);
|
||||
|
||||
// 3. Let flags be an empty string.
|
||||
// NOTE: These flags match the flags for the empty string of the LibJS RegExp implementation.
|
||||
let mut flags = Flags::default();
|
||||
|
||||
// 4. If options’s ignore case is true then set flags to "vi".
|
||||
if options.ignore_case {
|
||||
flags.unicode_sets = true;
|
||||
flags.ignore_case = true;
|
||||
}
|
||||
// 5. Otherwise set flags to "v"
|
||||
else {
|
||||
flags.unicode_sets = true;
|
||||
}
|
||||
|
||||
// 6. Let regular expression be RegExpCreate(regular expression string, flags). If this throws an exception, catch
|
||||
// it, and throw a TypeError.
|
||||
let regex = Regex::compile(®ular_expression_string, flags)
|
||||
.map_err(|error| ErrorInfo::new(format!("RegExp compile error: {error}")))?;
|
||||
|
||||
// 7. Let pattern string be the result of running generate a pattern string given part list and options.
|
||||
let pattern_string = generate_a_pattern_string(&part_list, options);
|
||||
|
||||
// 8. Let has regexp groups be false.
|
||||
let mut has_regexp_groups = false;
|
||||
|
||||
// 9. For each part of part list:
|
||||
for part in &part_list {
|
||||
// 1. If part’s type is "regexp", then set has regexp groups to true.
|
||||
if part.r#type == PartType::Regexp {
|
||||
has_regexp_groups = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Return a new component whose pattern string is pattern string, regular expression is regular expression,
|
||||
// group name list is name list, and has regexp groups is has regexp groups.
|
||||
Ok(Self {
|
||||
pattern_string,
|
||||
regular_expression: Some(RegularExpression::new(regex)),
|
||||
group_name_list: name_list,
|
||||
has_regexp_groups,
|
||||
})
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#create-a-component-match-result
|
||||
pub fn create_match_result(&self, input: &str, exec_result: &ExecutionResult) -> Result {
|
||||
// 1. Let result be a new URLPatternComponentResult.
|
||||
// 2. Set result["input"] to input.
|
||||
let mut result = Result {
|
||||
input: input.to_string(),
|
||||
..Result::default()
|
||||
};
|
||||
|
||||
// 3. Let groups be a record<USVString, (USVString or undefined)>.
|
||||
let mut groups = BTreeMap::new();
|
||||
|
||||
// 4. Let index be 1.
|
||||
// 5. While index is less than or equal to component’s group name list’s size:
|
||||
assert!(exec_result.captures.len() == self.group_name_list.len());
|
||||
for index in 1..=self.group_name_list.len() {
|
||||
// 1. Let name be component’s group name list[index − 1].
|
||||
let name = self.group_name_list[index - 1].clone();
|
||||
|
||||
// 2. Let value be Get(execResult, ToString(index)).
|
||||
// 3. Set groups[name] to value.
|
||||
let capture = &exec_result.captures[index - 1];
|
||||
if let Some(capture) = capture {
|
||||
groups.insert(name, GroupMatch::String(capture.clone()));
|
||||
} else {
|
||||
groups.insert(name, GroupMatch::Empty);
|
||||
}
|
||||
|
||||
// 4. Increment index by 1.
|
||||
}
|
||||
|
||||
// 6. Set result["groups"] to groups.
|
||||
result.groups = groups;
|
||||
|
||||
// 7. Return result.
|
||||
result
|
||||
}
|
||||
|
||||
pub fn execute(&self, input: &str) -> ExecutionResult {
|
||||
let Some(regular_expression) = self.regular_expression.as_ref() else {
|
||||
return ExecutionResult::default();
|
||||
};
|
||||
|
||||
let utf16_input: Vec<u16> = input.encode_utf16().collect();
|
||||
let capture_count = regular_expression.capture_count() as usize;
|
||||
let mut captures = vec![-1; (capture_count + 1) * 2];
|
||||
let match_result = regular_expression.exec_into(&utf16_input, 0, &mut captures);
|
||||
if match_result != VmResult::Match {
|
||||
return ExecutionResult::default();
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult {
|
||||
success: true,
|
||||
captures: Vec::with_capacity(self.group_name_list.len()),
|
||||
};
|
||||
|
||||
for index in 1..=self.group_name_list.len() {
|
||||
let start = captures[index * 2];
|
||||
let end = captures[index * 2 + 1];
|
||||
if start < 0 || end < 0 {
|
||||
result.captures.push(None);
|
||||
continue;
|
||||
}
|
||||
|
||||
let capture = String::from_utf16_lossy(&utf16_input[start as usize..end as usize]);
|
||||
result.captures.push(Some(capture));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn matches(&self, input: &str) -> bool {
|
||||
let Some(regular_expression) = self.regular_expression.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let utf16_input: Vec<u16> = input.encode_utf16().collect();
|
||||
regular_expression.test(&utf16_input, 0) == VmResult::Match
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#protocol-component-matches-a-special-scheme
|
||||
pub fn protocol_component_matches_a_special_scheme(protocol_component: &Component) -> bool {
|
||||
// 1. Let special scheme list be a list populated with all of the special schemes.
|
||||
// 2. For each scheme of special scheme list:
|
||||
for scheme in special_schemes() {
|
||||
// 1. Let test result be RegExpBuiltinExec(protocol component’s regular expression, scheme).
|
||||
let test_result = protocol_component.matches(scheme);
|
||||
|
||||
// 2. If test result is not null, then return true.
|
||||
if test_result {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
impl RegularExpression {
|
||||
fn new(regex: Regex) -> Self {
|
||||
Self(Rc::new(regex))
|
||||
}
|
||||
|
||||
fn capture_count(&self) -> u32 {
|
||||
self.0.capture_count()
|
||||
}
|
||||
|
||||
fn exec_into(&self, input: &[u16], start: usize, out: &mut [i32]) -> VmResult {
|
||||
self.0.exec_into(input, start, out)
|
||||
}
|
||||
|
||||
fn test(&self, input: &[u16], start: usize) -> VmResult {
|
||||
self.0.test(input, start)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Component {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Component")
|
||||
.field("pattern_string", &self.pattern_string)
|
||||
.field(
|
||||
"regular_expression",
|
||||
&self.regular_expression.as_ref().map(|_| "RegularExpression"),
|
||||
)
|
||||
.field("group_name_list", &self.group_name_list)
|
||||
.field("has_regexp_groups", &self.has_regexp_groups)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RegularExpression {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("RegularExpression(..)")
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_a_regular_expression_and_name_list(part_list: &[Part], options: &Options) -> RegularExpressionAndNameList {
|
||||
// 1. Let result be "^".
|
||||
let mut result = String::from("^");
|
||||
|
||||
// 2. Let name list be a new list.
|
||||
let mut name_list = Vec::new();
|
||||
|
||||
// 3. For each part of part list:
|
||||
for part in part_list {
|
||||
// 1. If part’s type is "fixed-text":
|
||||
if part.r#type == PartType::FixedText {
|
||||
// 1. If part’s modifier is "none", then append the result of running escape a regexp string given part’s
|
||||
// value to the end of result.
|
||||
if part.modifier == PartModifier::None {
|
||||
result.push_str(&escape_a_regexp_string(&part.value));
|
||||
}
|
||||
// 2. Otherwise:
|
||||
else {
|
||||
// 1. Append "(?:" to the end of result.
|
||||
result.push_str("(?:");
|
||||
|
||||
// 2. Append the result of running escape a regexp string given part’s value to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.value));
|
||||
|
||||
// 3. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
|
||||
// 4. Append the result of running convert a modifier to a string given part’s modifier to the end of result.
|
||||
result.push_str(Part::convert_modifier_to_string(part.modifier));
|
||||
}
|
||||
|
||||
// 3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Assert: part’s name is not the empty string.
|
||||
assert!(!part.name.is_empty());
|
||||
|
||||
// 3. Append part’s name to name list.
|
||||
name_list.push(part.name.clone());
|
||||
|
||||
// 4. Let regexp value be part’s value.
|
||||
let mut regexp_value = part.value.clone();
|
||||
|
||||
// 5. If part’s type is "segment-wildcard", then set regexp value to the result of running generate a segment wildcard regexp given options.
|
||||
if part.r#type == PartType::SegmentWildcard {
|
||||
regexp_value = generate_a_segment_wildcard_regexp(options);
|
||||
}
|
||||
// 6. Otherwise if part’s type is "full-wildcard", then set regexp value to full wildcard regexp value.
|
||||
else if part.r#type == PartType::FullWildcard {
|
||||
regexp_value = full_wildcard_regexp_value.to_string();
|
||||
}
|
||||
|
||||
// 7. If part’s prefix is the empty string and part’s suffix is the empty string:
|
||||
if part.prefix.is_empty() && part.suffix.is_empty() {
|
||||
// 1. If part’s modifier is "none" or "optional", then:
|
||||
if part.modifier == PartModifier::None || part.modifier == PartModifier::Optional {
|
||||
// 1. Append "(" to the end of result.
|
||||
result.push('(');
|
||||
|
||||
// 2. Append regexp value to the end of result.
|
||||
result.push_str(®exp_value);
|
||||
|
||||
// 3. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
|
||||
// 4. Append the result of running convert a modifier to a string given part’s modifier to the end of result.
|
||||
result.push_str(Part::convert_modifier_to_string(part.modifier));
|
||||
}
|
||||
// 2. Otherwise:
|
||||
else {
|
||||
// 1. Append "((?:" to the end of result.
|
||||
result.push_str("((?:");
|
||||
|
||||
// 2. Append regexp value to the end of result.
|
||||
result.push_str(®exp_value);
|
||||
|
||||
// 3. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
|
||||
// 4. Append the result of running convert a modifier to a string given part’s modifier to the end of result.
|
||||
result.push_str(Part::convert_modifier_to_string(part.modifier));
|
||||
|
||||
// 5. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
}
|
||||
|
||||
// 3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 8. If part’s modifier is "none" or "optional":
|
||||
if part.modifier == PartModifier::None || part.modifier == PartModifier::Optional {
|
||||
// 1. Append "(?:" to the end of result.
|
||||
result.push_str("(?:");
|
||||
|
||||
// 2. Append the result of running escape a regexp string given part’s prefix to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.prefix));
|
||||
|
||||
// 3. Append "(" to the end of result.
|
||||
result.push('(');
|
||||
|
||||
// 4. Append regexp value to the end of result.
|
||||
result.push_str(®exp_value);
|
||||
|
||||
// 5. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
|
||||
// 6. Append the result of running escape a regexp string given part’s suffix to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.suffix));
|
||||
|
||||
// 7. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
|
||||
// 8. Append the result of running convert a modifier to a string given part’s modifier to the end of result.
|
||||
result.push_str(Part::convert_modifier_to_string(part.modifier));
|
||||
|
||||
// 9. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 9. Assert: part’s modifier is "zero-or-more" or "one-or-more".
|
||||
assert!(part.modifier == PartModifier::ZeroOrMore || part.modifier == PartModifier::OneOrMore);
|
||||
|
||||
// 10. Assert: part’s prefix is not the empty string or part’s suffix is not the empty string.
|
||||
assert!(!part.prefix.is_empty() || !part.suffix.is_empty());
|
||||
|
||||
// 11. Append "(?:" to the end of result.
|
||||
result.push_str("(?:");
|
||||
|
||||
// 12. Append the result of running escape a regexp string given part’s prefix to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.prefix));
|
||||
|
||||
// 13. Append "((?:" to the end of result.
|
||||
result.push_str("((?:");
|
||||
|
||||
// 14. Append regexp value to the end of result.
|
||||
result.push_str(®exp_value);
|
||||
|
||||
// 15. Append ")(?:" to the end of result.
|
||||
result.push_str(")(?:");
|
||||
|
||||
// 16. Append the result of running escape a regexp string given part’s suffix to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.suffix));
|
||||
|
||||
// 17. Append the result of running escape a regexp string given part’s prefix to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.prefix));
|
||||
|
||||
// 18. Append "(?:" to the end of result.
|
||||
result.push_str("(?:");
|
||||
|
||||
// 19. Append regexp value to the end of result.
|
||||
result.push_str(®exp_value);
|
||||
|
||||
// 20. Append "))*)" to the end of result.
|
||||
result.push_str("))*)");
|
||||
|
||||
// 21. Append the result of running escape a regexp string given part’s suffix to the end of result.
|
||||
result.push_str(&escape_a_regexp_string(&part.suffix));
|
||||
|
||||
// 22. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
|
||||
// 23. If part’s modifier is "zero-or-more" then append "?" to the end of result.
|
||||
if part.modifier == PartModifier::ZeroOrMore {
|
||||
result.push('?');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Append "$" to the end of result.
|
||||
result.push('$');
|
||||
|
||||
// 5. Return (result, name list).
|
||||
RegularExpressionAndNameList {
|
||||
regular_expression: result,
|
||||
name_list,
|
||||
}
|
||||
}
|
||||
712
Libraries/LibURL/Rust/src/pattern/constructor_string_parser.rs
Normal file
712
Libraries/LibURL/Rust/src/pattern/constructor_string_parser.rs
Normal file
|
|
@ -0,0 +1,712 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::pattern::Component;
|
||||
use crate::pattern::Init;
|
||||
use crate::pattern::Options;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::pattern::Token;
|
||||
use crate::pattern::Tokenizer;
|
||||
use crate::pattern::canonicalize_a_protocol;
|
||||
use crate::pattern::protocol_component_matches_a_special_scheme;
|
||||
use crate::pattern::tokenizer::Policy as TokenizerPolicy;
|
||||
use crate::pattern::tokenizer::Type as TokenType;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser
|
||||
pub struct ConstructorStringParser {
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-input
|
||||
// A constructor string parser has an associated input, a string, which must be set upon creation.
|
||||
pub input: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-token-list
|
||||
// A constructor string parser has an associated token list, a token list, which must be set upon creation.
|
||||
pub token_list: Vec<Token>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-result
|
||||
// A constructor string parser has an associated result, a URLPatternInit, initially set to a new URLPatternInit.
|
||||
pub result: Init,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-component-start
|
||||
// A constructor string parser has an associated component start, a number, initially set to 0.
|
||||
pub component_start: u32,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-token-index
|
||||
// A constructor string parser has an associated token index, a number, initially set to 0.
|
||||
pub token_index: u32,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-token-increment
|
||||
// A constructor string parser has an associated token increment, a number, initially set to 1.
|
||||
pub token_increment: u32,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-group-depth
|
||||
// A constructor string parser has an associated group depth, a number, initially set to 0.
|
||||
pub group_depth: u32,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-hostname-ipv6-bracket-depth
|
||||
// A constructor string parser has an associated hostname IPv6 bracket depth, a number, initially set to 0.
|
||||
pub hostname_ipv6_bracket_depth: u32,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-protocol-matches-a-special-scheme-flag
|
||||
// A constructor string parser has an associated protocol matches a special scheme flag, a boolean, initially set to false.
|
||||
pub protocol_matches_a_special_scheme: bool,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-state
|
||||
// A constructor string parser has an associated state, a string, initially set to "init".
|
||||
pub state: State,
|
||||
|
||||
input_code_points: Vec<char>,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#constructor-string-parser-state
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum State {
|
||||
Initial,
|
||||
Protocol,
|
||||
Authority,
|
||||
Username,
|
||||
Password,
|
||||
Hostname,
|
||||
Port,
|
||||
Pathname,
|
||||
Search,
|
||||
Hash,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl ConstructorStringParser {
|
||||
pub(crate) fn new(input: &str, token_list: Vec<Token>) -> Self {
|
||||
Self {
|
||||
input: input.to_string(),
|
||||
token_list,
|
||||
result: Init::default(),
|
||||
component_start: 0,
|
||||
token_index: 0,
|
||||
token_increment: 1,
|
||||
group_depth: 0,
|
||||
hostname_ipv6_bracket_depth: 0,
|
||||
protocol_matches_a_special_scheme: false,
|
||||
state: State::Initial,
|
||||
input_code_points: input.chars().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#parse-a-constructor-string
|
||||
pub fn parse(input: &str) -> PatternErrorOr<Init> {
|
||||
// 1. Let parser be a new constructor string parser whose input is input and token list is the result of running
|
||||
// tokenize given input and "lenient".
|
||||
let mut parser = Self::new(input, Tokenizer::tokenize(input, TokenizerPolicy::Lenient)?);
|
||||
|
||||
// 2. While parser’s token index is less than parser’s token list size:
|
||||
while (parser.token_index as usize) < parser.token_list.len() {
|
||||
// 1. Set parser’s token increment to 1.
|
||||
parser.token_increment = 1;
|
||||
|
||||
// NOTE: On every iteration of the parse loop the parser’s token index will be incremented by its token
|
||||
// increment value. Typically this means incrementing by 1, but at certain times it is set to zero.
|
||||
// The token increment is then always reset back to 1 at the top of the loop.
|
||||
|
||||
// 2. If parser’s token list[parser’s token index]'s type is "end" then:
|
||||
if parser.token_list[parser.token_index as usize].r#type == TokenType::End {
|
||||
// 1. If parser’s state is "init":
|
||||
if parser.state == State::Initial {
|
||||
// NOTE: If we reached the end of the string in the "init" state, then we failed to find a protocol
|
||||
// terminator and this has to be a relative URLPattern constructor string.
|
||||
|
||||
// 1. Run rewind given parser.
|
||||
parser.rewind();
|
||||
|
||||
// NOTE: We next determine at which component the relative pattern begins. Relative pathnames are
|
||||
// most common, but URLs and URLPattern constructor strings can begin with the search or hash
|
||||
// components as well.
|
||||
|
||||
// 2. If the result of running is a hash prefix given parser is true, then run change state given parser,
|
||||
// "hash" and 1.
|
||||
if parser.is_a_hash_prefix() {
|
||||
parser.change_state(State::Hash, 1);
|
||||
}
|
||||
// 3. Otherwise if the result of running is a search prefix given parser is true:
|
||||
else if parser.is_a_search_prefix() {
|
||||
// 1. Run change state given parser, "search" and 1.
|
||||
parser.change_state(State::Search, 1);
|
||||
}
|
||||
// 4. Otherwise:
|
||||
else {
|
||||
// 1. Run change state given parser, "pathname" and 0.
|
||||
parser.change_state(State::Pathname, 0);
|
||||
}
|
||||
|
||||
// 5. Increment parser’s token index by parser’s token increment.
|
||||
parser.token_index += parser.token_increment;
|
||||
|
||||
// 6. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. If parser’s state is "authority":
|
||||
if parser.state == State::Authority {
|
||||
// NOTE: If we reached the end of the string in the "authority" state, then we failed to find an
|
||||
// "@". Therefore there is no username or password.
|
||||
|
||||
// 1. Run rewind and set state given parser, and "hostname".
|
||||
parser.rewind_and_set_state(State::Hostname);
|
||||
|
||||
// 2. Increment parser’s token index by parser’s token increment.
|
||||
parser.token_index += parser.token_increment;
|
||||
|
||||
// 3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Run change state given parser, "done" and 0.
|
||||
parser.change_state(State::Done, 0);
|
||||
|
||||
// 4. Break.
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. If the result of running is a group open given parser is true:
|
||||
if parser.is_a_group_open() {
|
||||
// NOTE: We ignore all code points within "{ ... }" pattern groupings. It would not make sense to allow
|
||||
// a URL component boundary to lie within a grouping; e.g. "https://example.c{om/fo}o". While not
|
||||
// supported within well formed pattern strings, we handle nested groupings here to avoid parser
|
||||
// confusion.
|
||||
//
|
||||
// It is not necessary to perform this logic for regexp or named groups since those values are collapsed into
|
||||
// individual tokens by the tokenize algorithm.
|
||||
|
||||
// 1. Increment parser’s group depth by 1.
|
||||
parser.group_depth += 1;
|
||||
|
||||
// 2. Increment parser’s token index by parser’s token increment.
|
||||
parser.token_index += parser.token_increment;
|
||||
|
||||
// 3. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. If parser’s group depth is greater than 0:
|
||||
if parser.group_depth > 0 {
|
||||
// 1. If the result of running is a group close given parser is true, then decrement parser’s group depth by 1.
|
||||
if parser.is_a_group_close() {
|
||||
assert!(parser.group_depth != 0);
|
||||
parser.group_depth -= 1;
|
||||
}
|
||||
// 2. Otherwise:
|
||||
else {
|
||||
// 1. Increment parser’s token index by parser’s token increment.
|
||||
parser.token_index += parser.token_increment;
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Switch on parser’s state and run the associated steps:
|
||||
match parser.state {
|
||||
// -> "init", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-init%E2%91%A2
|
||||
State::Initial => {
|
||||
// 1. If the result of running is a protocol suffix given parser is true:
|
||||
if parser.is_a_protocol_suffix() {
|
||||
// 1. Run rewind and set state given parser and "protocol".
|
||||
parser.rewind_and_set_state(State::Protocol);
|
||||
}
|
||||
}
|
||||
// -> "protocol", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-protocol%E2%91%A0
|
||||
State::Protocol => {
|
||||
// 1. If the result of running is a protocol suffix given parser is true:
|
||||
if parser.is_a_protocol_suffix() {
|
||||
// 1. Run compute protocol matches a special scheme flag given parser.
|
||||
parser.compute_protocol_matches_a_special_scheme_flag()?;
|
||||
|
||||
// NOTE: We need to eagerly compile the protocol component to determine if it matches any special
|
||||
// schemes. If it does then certain special rules apply. It determines if the pathname
|
||||
// defaults to a "/" and also whether we will look for the username, password, hostname, and
|
||||
// port components. Authority slashes can also cause us to look for these components as well.
|
||||
// Otherwise we treat this as an "opaque path URL" and go straight to the pathname component.
|
||||
|
||||
// 2. Let next state be "pathname".
|
||||
let mut next_state = State::Pathname;
|
||||
|
||||
// 3. Let skip be 1.
|
||||
let mut skip = 1;
|
||||
|
||||
// 4. If the result of running next is authority slashes given parser is true:
|
||||
if parser.next_is_authority_slashes() {
|
||||
// 1. Set next state to "authority".
|
||||
next_state = State::Authority;
|
||||
|
||||
// 2. Set skip to 3.
|
||||
skip = 3;
|
||||
}
|
||||
// 5. Otherwise if parser’s protocol matches a special scheme flag is true, then set next state to "authority".
|
||||
else if parser.protocol_matches_a_special_scheme {
|
||||
next_state = State::Authority;
|
||||
}
|
||||
|
||||
// 6. Run change state given parser, next state, and skip.
|
||||
parser.change_state(next_state, skip);
|
||||
}
|
||||
}
|
||||
// -> "authority", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-authority%E2%91%A3
|
||||
State::Authority => {
|
||||
// 1. If the result of running is an identity terminator given parser is true, then run rewind and set state
|
||||
// given parser and "username".
|
||||
if parser.is_an_identity_terminator() {
|
||||
parser.rewind_and_set_state(State::Username);
|
||||
}
|
||||
// 2. Otherwise if any of the following are true:
|
||||
// * the result of running is a pathname start given parser;
|
||||
// * the result of running is a search prefix given parser; or
|
||||
// * the result of running is a hash prefix given parser,
|
||||
// then run rewind and set state given parser and "hostname".
|
||||
else if parser.is_a_pathname_start() || parser.is_a_search_prefix() || parser.is_a_hash_prefix() {
|
||||
parser.rewind_and_set_state(State::Hostname);
|
||||
}
|
||||
}
|
||||
// -> "username", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-username%E2%91%A0
|
||||
State::Username => {
|
||||
// 1. If the result of running is a password prefix given parser is true, then run change state given
|
||||
// parser, "password", and 1.
|
||||
if parser.is_a_password_prefix() {
|
||||
parser.change_state(State::Password, 1);
|
||||
}
|
||||
// 2. Otherwise if the result of running is an identity terminator given parser is true, then run change
|
||||
// state given parser, "hostname", and 1.
|
||||
else if parser.is_an_identity_terminator() {
|
||||
parser.change_state(State::Hostname, 1);
|
||||
}
|
||||
}
|
||||
// -> "password", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-password%E2%91%A0
|
||||
State::Password => {
|
||||
// 1. If the result of running is an identity terminator given parser is true, then run change state
|
||||
// given parser, "hostname", and 1.
|
||||
if parser.is_an_identity_terminator() {
|
||||
parser.change_state(State::Hostname, 1);
|
||||
}
|
||||
}
|
||||
// -> "hostname", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-hostname%E2%91%A3
|
||||
State::Hostname => {
|
||||
// 1. If the result of running is an IPv6 open given parser is true, then increment parser’s hostname
|
||||
// IPv6 bracket depth by 1.
|
||||
if parser.is_an_ipv6_open() {
|
||||
parser.hostname_ipv6_bracket_depth += 1;
|
||||
}
|
||||
// 2. Otherwise if the result of running is an IPv6 close given parser is true, then decrement parser’s
|
||||
// hostname IPv6 bracket depth by 1.
|
||||
else if parser.is_an_ipv6_close() {
|
||||
assert!(parser.hostname_ipv6_bracket_depth != 0);
|
||||
parser.hostname_ipv6_bracket_depth -= 1;
|
||||
}
|
||||
// 3. Otherwise if the result of running is a port prefix given parser is true and parser’s hostname IPv6
|
||||
// bracket depth is zero, then run change state given parser, "port", and 1.
|
||||
else if parser.is_a_port_prefix() && parser.hostname_ipv6_bracket_depth == 0 {
|
||||
parser.change_state(State::Port, 1);
|
||||
}
|
||||
// 4. Otherwise if the result of running is a pathname start given parser is true, then run change state
|
||||
// given parser, "pathname", and 0.
|
||||
else if parser.is_a_pathname_start() {
|
||||
parser.change_state(State::Pathname, 0);
|
||||
}
|
||||
// 5. Otherwise if the result of running is a search prefix given parser is true, then run change state
|
||||
// given parser, "search", and 1.
|
||||
else if parser.is_a_search_prefix() {
|
||||
parser.change_state(State::Search, 1);
|
||||
}
|
||||
// 6. Otherwise if the result of running is a hash prefix given parser is true, then run change state
|
||||
// given parser, "hash", and 1.
|
||||
else if parser.is_a_hash_prefix() {
|
||||
parser.change_state(State::Hash, 1);
|
||||
}
|
||||
}
|
||||
// -> "port", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-port%E2%91%A0
|
||||
State::Port => {
|
||||
// 1. If the result of running is a pathname start given parser is true, then run change state given
|
||||
// parser, "pathname", and 0.
|
||||
if parser.is_a_pathname_start() {
|
||||
parser.change_state(State::Pathname, 0);
|
||||
}
|
||||
// 2. Otherwise if the result of running is a search prefix given parser is true, then run change state
|
||||
// given parser, "search", and 1.
|
||||
else if parser.is_a_search_prefix() {
|
||||
parser.change_state(State::Search, 1);
|
||||
}
|
||||
// 3. Otherwise if the result of running is a hash prefix given parser is true, then run change state given
|
||||
// parser, "hash", and 1.
|
||||
else if parser.is_a_hash_prefix() {
|
||||
parser.change_state(State::Hash, 1);
|
||||
}
|
||||
}
|
||||
// -> "pathname", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-pathname%E2%91%A3
|
||||
State::Pathname => {
|
||||
// 1. If the result of running is a search prefix given parser is true, then run change state given parser,
|
||||
// "search", and 1.
|
||||
if parser.is_a_search_prefix() {
|
||||
parser.change_state(State::Search, 1);
|
||||
}
|
||||
// 2. Otherwise if the result of running is a hash prefix given parser is true, then run change state given
|
||||
// parser, "hash", and 1.
|
||||
else if parser.is_a_hash_prefix() {
|
||||
parser.change_state(State::Hash, 1);
|
||||
}
|
||||
}
|
||||
// -> "search", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-search%E2%91%A3
|
||||
State::Search => {
|
||||
// 1. If the result of running is a hash prefix given parser is true, then run change state given parser,
|
||||
// "hash", and 1.
|
||||
if parser.is_a_hash_prefix() {
|
||||
parser.change_state(State::Hash, 1);
|
||||
}
|
||||
}
|
||||
// -> "hash", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-hash%E2%91%A4
|
||||
State::Hash => {
|
||||
// 1. Do nothing.
|
||||
}
|
||||
// -> "done", https://urlpattern.spec.whatwg.org/#ref-for-constructor-string-parser-state-done%E2%91%A0
|
||||
State::Done => {
|
||||
// 1. Assert: This step is never reached.
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Increment parser’s token index by parser’s token increment.
|
||||
parser.token_index += parser.token_increment;
|
||||
}
|
||||
|
||||
// 3. If parser’s result contains "hostname" and not "port", then set parser’s result["port"] to the empty string.
|
||||
if parser.result.hostname.is_some() && parser.result.port.is_none() {
|
||||
parser.result.port = Some(String::new());
|
||||
}
|
||||
|
||||
// NOTE: This is special-cased because when an author does not specify a port, they usually intend the default
|
||||
// port. If any port is acceptable, the author can specify it as a wildcard explicitly. For example,
|
||||
// "https://example.com/*" does not match URLs beginning with "https://example.com:8443/", which is a
|
||||
// different origin.
|
||||
|
||||
// 4. Return parser’s result.
|
||||
Ok(parser.result)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#make-a-component-string
|
||||
pub(crate) fn make_a_component_string(&self) -> String {
|
||||
// 1. Assert: parser’s token index is less than parser’s token list's size.
|
||||
assert!((self.token_index as usize) < self.token_list.len());
|
||||
|
||||
// 2. Let token be parser’s token list[parser’s token index].
|
||||
let token = &self.token_list[self.token_index as usize];
|
||||
|
||||
// 3. Let component start token be the result of running get a safe token given parser and parser’s component start.
|
||||
let component_start_token = self.get_a_safe_token(self.component_start);
|
||||
|
||||
// 4. Let component start input index be component start token’s index.
|
||||
let component_start_input_index = component_start_token.index;
|
||||
|
||||
// 5. Let end index be token’s index.
|
||||
let end_index = token.index;
|
||||
|
||||
// 6. Return the code point substring from component start input index to end index within parser’s input.
|
||||
self.input_code_points[component_start_input_index as usize..end_index as usize]
|
||||
.iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#compute-protocol-matches-a-special-scheme-flag
|
||||
pub(crate) fn compute_protocol_matches_a_special_scheme_flag(&mut self) -> PatternErrorOr<()> {
|
||||
// 1. Let protocol string be the result of running make a component string given parser.
|
||||
let protocol_string = self.make_a_component_string();
|
||||
|
||||
// 2. Let protocol component be the result of compiling a component given protocol string, canonicalize a protocol, and default options.
|
||||
let protocol_component = Component::compile(
|
||||
&protocol_string,
|
||||
Box::new(canonicalize_a_protocol),
|
||||
&Options::default_(),
|
||||
)?;
|
||||
|
||||
// 3. If the result of running protocol component matches a special scheme given protocol component is true, then set parser’s protocol matches a special scheme flag to true.
|
||||
if protocol_component_matches_a_special_scheme(&protocol_component) {
|
||||
self.protocol_matches_a_special_scheme = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_result_for_active_state(&mut self, value: Option<String>) {
|
||||
match self.state {
|
||||
State::Protocol => self.result.protocol = value,
|
||||
State::Username => self.result.username = value,
|
||||
State::Password => self.result.password = value,
|
||||
State::Hostname => self.result.hostname = value,
|
||||
State::Port => self.result.port = value,
|
||||
State::Pathname => self.result.pathname = value,
|
||||
State::Search => self.result.search = value,
|
||||
State::Hash => self.result.hash = value,
|
||||
State::Initial | State::Authority | State::Done => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#change-state
|
||||
pub(crate) fn change_state(&mut self, new_state: State, skip: u32) {
|
||||
// 1. If parser’s state is not "init", not "authority", and not "done", then set parser’s result[parser’s state] to
|
||||
// the result of running make a component string given parser.
|
||||
if self.state != State::Initial && self.state != State::Authority && self.state != State::Done {
|
||||
self.set_result_for_active_state(Some(self.make_a_component_string()));
|
||||
}
|
||||
|
||||
// 2. If parser’s state is not "init" and new state is not "done", then:
|
||||
if self.state != State::Initial && new_state != State::Done {
|
||||
// 1. If parser’s state is "protocol", "authority", "username", or "password"; new state is "port", "pathname",
|
||||
// "search", or "hash"; and parser’s result["hostname"] does not exist, then set parser’s result["hostname"]
|
||||
// to the empty string.
|
||||
if matches!(
|
||||
self.state,
|
||||
State::Protocol | State::Authority | State::Username | State::Password
|
||||
) && matches!(new_state, State::Port | State::Pathname | State::Search | State::Hash)
|
||||
&& self.result.hostname.is_none()
|
||||
{
|
||||
self.result.hostname = Some(String::new());
|
||||
}
|
||||
|
||||
// 2. If parser’s state is "protocol", "authority", "username", "password", "hostname", or "port"; new state is
|
||||
// "search" or "hash"; and parser’s result["pathname"] does not exist, then:
|
||||
if matches!(
|
||||
self.state,
|
||||
State::Protocol | State::Authority | State::Username | State::Password | State::Hostname | State::Port
|
||||
) && matches!(new_state, State::Search | State::Hash)
|
||||
&& self.result.pathname.is_none()
|
||||
{
|
||||
// 1. If parser’s protocol matches a special scheme flag is true, then set parser’s result["pathname"] to "/".
|
||||
if self.protocol_matches_a_special_scheme {
|
||||
self.result.pathname = Some("/".to_string());
|
||||
}
|
||||
// 2. Otherwise, set parser’s result["pathname"] to the empty string.
|
||||
else {
|
||||
self.result.pathname = Some(String::new());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If parser’s state is "protocol", "authority", "username", "password", "hostname", "port", or "pathname";
|
||||
// new state is "hash"; and parser’s result["search"] does not exist, then set parser’s result["search"]
|
||||
// to the empty string.
|
||||
if matches!(
|
||||
self.state,
|
||||
State::Protocol
|
||||
| State::Authority
|
||||
| State::Username
|
||||
| State::Password
|
||||
| State::Hostname
|
||||
| State::Port
|
||||
| State::Pathname
|
||||
) && new_state == State::Hash
|
||||
&& self.result.search.is_none()
|
||||
{
|
||||
self.result.search = Some(String::new());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Set parser’s state to new state.
|
||||
self.state = new_state;
|
||||
|
||||
// 4. Increment parser’s token index by skip.
|
||||
self.token_index += skip;
|
||||
|
||||
// 5. Set parser’s component start to parser’s token index.
|
||||
self.component_start = self.token_index;
|
||||
|
||||
// 6. Set parser’s token increment to 0.
|
||||
self.token_increment = 0;
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#next-is-authority-slashes
|
||||
pub(crate) fn next_is_authority_slashes(&self) -> bool {
|
||||
// 1. If the result of running is a non-special pattern char given parser, parser’s token index + 1, and "/" is false,
|
||||
// then return false.
|
||||
if !self.is_a_non_special_pattern_char(self.token_index + 1, '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. If the result of running is a non-special pattern char given parser, parser’s token index + 2, and "/" is false,
|
||||
// then return false.
|
||||
if !self.is_a_non_special_pattern_char(self.token_index + 2, '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Return true.
|
||||
true
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-an-identity-terminator
|
||||
pub(crate) fn is_an_identity_terminator(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and "@".
|
||||
self.is_a_non_special_pattern_char(self.token_index, '@')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-password-prefix
|
||||
pub(crate) fn is_a_password_prefix(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and ":".
|
||||
self.is_a_non_special_pattern_char(self.token_index, ':')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-port-prefix
|
||||
pub(crate) fn is_a_port_prefix(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and ":".
|
||||
self.is_a_non_special_pattern_char(self.token_index, ':')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-pathname-start
|
||||
pub(crate) fn is_a_pathname_start(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and "/".
|
||||
self.is_a_non_special_pattern_char(self.token_index, '/')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-search-prefix
|
||||
pub(crate) fn is_a_search_prefix(&self) -> bool {
|
||||
// 1. If result of running is a non-special pattern char given parser, parser’s token index and "?" is true,
|
||||
// then return true.
|
||||
if self.is_a_non_special_pattern_char(self.token_index, '?') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. If parser’s token list[parser’s token index]'s value is not "?", then return false.
|
||||
if self.token_list[self.token_index as usize].value != "?" {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Let previous index be parser’s token index − 1.
|
||||
// 4. If previous index is less than 0, then return true.
|
||||
if self.token_index == 0 {
|
||||
return true;
|
||||
}
|
||||
let previous_index = self.token_index - 1;
|
||||
|
||||
// 5. Let previous token be the result of running get a safe token given parser and previous index.
|
||||
let previous_token = self.get_a_safe_token(previous_index);
|
||||
|
||||
// 6. If any of the following are true, then return false:
|
||||
// * previous token’s type is "name".
|
||||
// * previous token’s type is "regexp".
|
||||
// * previous token’s type is "close".
|
||||
// * previous token’s type is "asterisk".
|
||||
if matches!(
|
||||
previous_token.r#type,
|
||||
TokenType::Name | TokenType::Regexp | TokenType::Close | TokenType::Asterisk
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 7. Return true.
|
||||
true
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-protocol-suffix
|
||||
pub(crate) fn is_a_protocol_suffix(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and ":".
|
||||
self.is_a_non_special_pattern_char(self.token_index, ':')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-hash-prefix
|
||||
pub(crate) fn is_a_hash_prefix(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index and "#".
|
||||
self.is_a_non_special_pattern_char(self.token_index, '#')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-group-open
|
||||
pub(crate) fn is_a_group_open(&self) -> bool {
|
||||
// 1. If parser’s token list[parser’s token index]'s type is "open", then return true.
|
||||
if self.token_list[self.token_index as usize].r#type == TokenType::Open {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Otherwise return false.
|
||||
false
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-group-close
|
||||
pub(crate) fn is_a_group_close(&self) -> bool {
|
||||
// 1. If parser’s token list[parser’s token index]'s type is "close", then return true.
|
||||
if self.token_list[self.token_index as usize].r#type == TokenType::Close {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Otherwise return false.
|
||||
false
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-an-ipv6-open
|
||||
pub(crate) fn is_an_ipv6_open(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and "[".
|
||||
self.is_a_non_special_pattern_char(self.token_index, '[')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-an-ipv6-close
|
||||
pub(crate) fn is_an_ipv6_close(&self) -> bool {
|
||||
// 1. Return the result of running is a non-special pattern char given parser, parser’s token index, and "]".
|
||||
self.is_a_non_special_pattern_char(self.token_index, ']')
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#get-a-safe-token
|
||||
pub(crate) fn get_a_safe_token(&self, index: u32) -> &Token {
|
||||
// 1. If index is less than parser’s token list's size, then return parser’s token list[index].
|
||||
if (index as usize) < self.token_list.len() {
|
||||
return &self.token_list[index as usize];
|
||||
}
|
||||
|
||||
// 2. Assert: parser’s token list's size is greater than or equal to 1.
|
||||
assert!(!self.token_list.is_empty());
|
||||
|
||||
// 3. Let last index be parser’s token list's size − 1.
|
||||
// 4. Let token be parser’s token list[last index].
|
||||
let token = self.token_list.last().unwrap();
|
||||
|
||||
// 5. Assert: token’s type is "end".
|
||||
assert!(token.r#type == TokenType::End);
|
||||
|
||||
// 6. Return token.
|
||||
token
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-non-special-pattern-char
|
||||
pub(crate) fn is_a_non_special_pattern_char(&self, index: u32, value: char) -> bool {
|
||||
// 1. Let token be the result of running get a safe token given parser and index.
|
||||
let token = self.get_a_safe_token(index);
|
||||
|
||||
// 2. If token’s value is not value, then return false.
|
||||
if token.value.is_empty() || token.value.chars().next().unwrap() != value {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. If any of the following are true:
|
||||
// * token’s type is "char";
|
||||
// * token’s type is "escaped-char"; or
|
||||
// * token’s type is "invalid-char",
|
||||
// then return true.
|
||||
if matches!(
|
||||
token.r#type,
|
||||
TokenType::Char | TokenType::EscapedChar | TokenType::InvalidChar
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#rewind
|
||||
pub(crate) fn rewind(&mut self) {
|
||||
// 1. Set parser’s token index to parser’s component start.
|
||||
self.token_index = self.component_start;
|
||||
|
||||
// 2. Set parser’s token increment to 0.
|
||||
self.token_increment = 0;
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#rewind-and-set-state
|
||||
pub(crate) fn rewind_and_set_state(&mut self, state: State) {
|
||||
// 1. Run rewind given parser.
|
||||
self.rewind();
|
||||
|
||||
// 2. Set parser’s state to state.
|
||||
self.state = state;
|
||||
}
|
||||
}
|
||||
454
Libraries/LibURL/Rust/src/pattern/init.rs
Normal file
454
Libraries/LibURL/Rust/src/pattern/init.rs
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::pattern::ErrorInfo;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::pattern::canonicalize_a_hash;
|
||||
use crate::pattern::canonicalize_a_hostname;
|
||||
use crate::pattern::canonicalize_a_password;
|
||||
use crate::pattern::canonicalize_a_pathname;
|
||||
use crate::pattern::canonicalize_a_port;
|
||||
use crate::pattern::canonicalize_a_protocol;
|
||||
use crate::pattern::canonicalize_a_search;
|
||||
use crate::pattern::canonicalize_a_username;
|
||||
use crate::pattern::canonicalize_an_opaque_pathname;
|
||||
use crate::pattern::escape_a_pattern_string;
|
||||
use crate::url::BasicParseOptions;
|
||||
use crate::url::basic_parse;
|
||||
use crate::url::is_special_scheme;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#dictdef-urlpatterninit
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Init {
|
||||
pub protocol: Option<String>,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
pub port: Option<String>,
|
||||
pub pathname: Option<String>,
|
||||
pub search: Option<String>,
|
||||
pub hash: Option<String>,
|
||||
pub base_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PatternProcessType {
|
||||
Pattern,
|
||||
Url,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-a-base-url-string
|
||||
fn process_a_base_url_string(input: &str, r#type: PatternProcessType) -> String {
|
||||
// 1. Assert: input is not null.
|
||||
// 2. If type is not "pattern" return input.
|
||||
if r#type != PatternProcessType::Pattern {
|
||||
return input.to_string();
|
||||
}
|
||||
|
||||
// 3. Return the result of escaping a pattern string given input.
|
||||
escape_a_pattern_string(input)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-an-absolute-pathname
|
||||
fn is_an_absolute_pathname(input: &str, r#type: PatternProcessType) -> bool {
|
||||
// 1. If input is the empty string, then return false.
|
||||
if input.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. If input[0] is U+002F (/), then return true.
|
||||
if input.as_bytes()[0] == b'/' {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. If type is "url", then return false.
|
||||
if r#type == PatternProcessType::Url {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. If input’s code point length is less than 2, then return false.
|
||||
if input.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. If input[0] is U+005C (\) and input[1] is U+002F (/), then return true.
|
||||
if input.as_bytes()[0] == b'\\' && input.as_bytes()[1] == b'/' {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. If input[0] is U+007B ({) and input[1] is U+002F (/), then return true.
|
||||
if input.as_bytes()[0] == b'{' && input.as_bytes()[1] == b'/' {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 7. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-protocol-for-init
|
||||
fn process_protocol_for_init(value: &str, r#type: PatternProcessType) -> PatternErrorOr<String> {
|
||||
// 1. Let strippedValue be the given value with a single trailing U+003A (:) removed, if any.
|
||||
let mut stripped_value = value.to_string();
|
||||
if stripped_value.ends_with(':') {
|
||||
stripped_value.pop();
|
||||
}
|
||||
|
||||
// 2. If type is "pattern" then return strippedValue.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return Ok(stripped_value);
|
||||
}
|
||||
|
||||
// 3. Return the result of running canonicalize a protocol given strippedValue.
|
||||
canonicalize_a_protocol(&stripped_value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-username-for-init
|
||||
fn process_username_for_init(value: &str, r#type: PatternProcessType) -> String {
|
||||
// 1. If type is "pattern" then return value.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Return the result of running canonicalize a username given value.
|
||||
canonicalize_a_username(value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-password-for-init
|
||||
fn process_password_for_init(value: &str, r#type: PatternProcessType) -> String {
|
||||
// 1. If type is "pattern" then return value.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
// 2. Return the result of running canonicalize a password given value.
|
||||
canonicalize_a_password(value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-hostname-for-init
|
||||
fn process_hostname_for_init(value: &str, r#type: PatternProcessType) -> PatternErrorOr<String> {
|
||||
// 1. If type is "pattern" then return value.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
|
||||
// 2. Return the result of running canonicalize a hostname given value.
|
||||
canonicalize_a_hostname(value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-port-for-init
|
||||
fn process_port_for_init(port_value: &str, protocol_value: &str, r#type: PatternProcessType) -> PatternErrorOr<String> {
|
||||
// 1. If type is "pattern" then return portValue.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return Ok(port_value.to_string());
|
||||
}
|
||||
|
||||
// 2. Return the result of running canonicalize a port given portValue and protocolValue.
|
||||
canonicalize_a_port(port_value, &Some(protocol_value.to_string()))
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-pathname-for-init
|
||||
fn process_pathname_for_init(
|
||||
pathname_value: &str,
|
||||
protocol_value: &str,
|
||||
r#type: PatternProcessType,
|
||||
) -> PatternErrorOr<String> {
|
||||
// 1. If type is "pattern" then return pathnameValue.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return Ok(pathname_value.to_string());
|
||||
}
|
||||
|
||||
// 2. If protocolValue is a special scheme or the empty string, then return the result of running canonicalize a
|
||||
// pathname given pathnameValue.
|
||||
// NOTE: If the protocolValue is the empty string then no value was provided for protocol in the constructor
|
||||
// dictionary. Normally we do not special case empty string dictionary values, but in this case we treat
|
||||
// it as a special scheme in order to default to the most common pathname canonicalization.
|
||||
if protocol_value.is_empty() || is_special_scheme(protocol_value.as_bytes()) {
|
||||
return Ok(canonicalize_a_pathname(pathname_value));
|
||||
}
|
||||
|
||||
// 3. Return the result of running canonicalize an opaque pathname given pathnameValue.
|
||||
canonicalize_an_opaque_pathname(pathname_value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-search-for-init
|
||||
fn process_search_for_init(value: &str, r#type: PatternProcessType) -> String {
|
||||
// 1. Let strippedValue be the given value with a single leading U+003F (?) removed, if any.
|
||||
let stripped_value = value.strip_prefix('?').unwrap_or(value);
|
||||
|
||||
// 2. If type is "pattern" then return strippedValue.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return stripped_value.to_string();
|
||||
}
|
||||
|
||||
// 3. Return the result of running canonicalize a search given strippedValue.
|
||||
canonicalize_a_search(stripped_value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-hash-for-init
|
||||
fn process_hash_for_init(value: &str, r#type: PatternProcessType) -> String {
|
||||
// 1. Let strippedValue be the given value with a single leading U+0023 (#) removed, if any.
|
||||
let stripped_value = value.strip_prefix('#').unwrap_or(value);
|
||||
|
||||
// 2. If type is "pattern" then return strippedValue.
|
||||
if r#type == PatternProcessType::Pattern {
|
||||
return stripped_value.to_string();
|
||||
}
|
||||
|
||||
// 3. Return the result of running canonicalize a hash given strippedValue.
|
||||
canonicalize_a_hash(stripped_value)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn process_a_url_pattern_init(
|
||||
init: &Init,
|
||||
r#type: PatternProcessType,
|
||||
protocol: &Option<String>,
|
||||
username: &Option<String>,
|
||||
password: &Option<String>,
|
||||
hostname: &Option<String>,
|
||||
port: &Option<String>,
|
||||
pathname: &Option<String>,
|
||||
search: &Option<String>,
|
||||
hash: &Option<String>,
|
||||
) -> PatternErrorOr<Init> {
|
||||
// 1. Let result be the result of creating a new URLPatternInit.
|
||||
let mut result = Init::default();
|
||||
|
||||
// 2. If protocol is not null, set result["protocol"] to protocol.
|
||||
if let Some(protocol) = protocol {
|
||||
result.protocol = Some(protocol.clone());
|
||||
}
|
||||
|
||||
// 3. If username is not null, set result["username"] to username.
|
||||
if let Some(username) = username {
|
||||
result.username = Some(username.clone());
|
||||
}
|
||||
|
||||
// 4. If password is not null, set result["password"] to password.
|
||||
if let Some(password) = password {
|
||||
result.password = Some(password.clone());
|
||||
}
|
||||
|
||||
// 5. If hostname is not null, set result["hostname"] to hostname.
|
||||
if let Some(hostname) = hostname {
|
||||
result.hostname = Some(hostname.clone());
|
||||
}
|
||||
|
||||
// 6. If port is not null, set result["port"] to port.
|
||||
if let Some(port) = port {
|
||||
result.port = Some(port.clone());
|
||||
}
|
||||
|
||||
// 7. If pathname is not null, set result["pathname"] to pathname.
|
||||
if let Some(pathname) = pathname {
|
||||
result.pathname = Some(pathname.clone());
|
||||
}
|
||||
|
||||
// 8. If search is not null, set result["search"] to search.
|
||||
if let Some(search) = search {
|
||||
result.search = Some(search.clone());
|
||||
}
|
||||
|
||||
// 9. If hash is not null, set result["hash"] to hash.
|
||||
if let Some(hash) = hash {
|
||||
result.hash = Some(hash.clone());
|
||||
}
|
||||
|
||||
// 10. Let baseURL be null.
|
||||
let mut base_url = None;
|
||||
|
||||
// 11. If init["baseURL"] exists:
|
||||
if let Some(init_base_url) = &init.base_url {
|
||||
// 1. Set baseURL to the result of running the basic URL parser on init["baseURL"].
|
||||
base_url = basic_parse(init_base_url, BasicParseOptions::new());
|
||||
|
||||
// 2. If baseURL is failure, then throw a TypeError.
|
||||
let Some(base_url_ref) = base_url.as_ref() else {
|
||||
return Err(ErrorInfo::new(format!(
|
||||
"Invalid base URL '{init_base_url}' provided for URLPattern"
|
||||
)));
|
||||
};
|
||||
|
||||
// 3. If init["protocol"] does not exist, then set result["protocol"] to the result of processing a base URL
|
||||
// string given baseURL’s scheme and type.
|
||||
if init.protocol.is_none() {
|
||||
result.protocol = Some(process_a_base_url_string(&base_url_ref.scheme, r#type));
|
||||
}
|
||||
|
||||
// 4. If type is not "pattern" and init contains none of "protocol", "hostname", "port" and "username", then
|
||||
// set result["username"] to the result of processing a base URL string given baseURL’s username and type.
|
||||
if r#type != PatternProcessType::Pattern
|
||||
&& init.protocol.is_none()
|
||||
&& init.hostname.is_none()
|
||||
&& init.port.is_none()
|
||||
&& init.username.is_none()
|
||||
{
|
||||
result.username = Some(process_a_base_url_string(&base_url_ref.username, r#type));
|
||||
}
|
||||
|
||||
// 5. If type is not "pattern" and init contains none of "protocol", "hostname", "port", "username" and
|
||||
// "password", then set result["password"] to the result of processing a base URL string given baseURL’s
|
||||
// password and type.
|
||||
if r#type != PatternProcessType::Pattern
|
||||
&& init.protocol.is_none()
|
||||
&& init.hostname.is_none()
|
||||
&& init.port.is_none()
|
||||
&& init.username.is_none()
|
||||
&& init.password.is_none()
|
||||
{
|
||||
result.password = Some(process_a_base_url_string(&base_url_ref.password, r#type));
|
||||
}
|
||||
|
||||
// 6. If init contains neither "protocol" nor "hostname", then:
|
||||
if init.protocol.is_none() && init.hostname.is_none() {
|
||||
// 1. Let baseHost be the serialization of baseURL's host, if it is not null, and the empty string otherwise.
|
||||
let base_host = if base_url_ref.host.is_some() {
|
||||
base_url_ref.serialized_host()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// 2. Set result["hostname"] to the result of processing a base URL string given baseHost and type.
|
||||
result.hostname = Some(process_a_base_url_string(&base_host, r#type));
|
||||
}
|
||||
|
||||
// 7. If init contains none of "protocol", "hostname", and "port", then:
|
||||
if init.protocol.is_none() && init.hostname.is_none() && init.port.is_none() {
|
||||
// 1. If baseURL’s port is null, then set result["port"] to the empty string.
|
||||
if base_url_ref.port.is_none() {
|
||||
result.port = Some(String::new());
|
||||
}
|
||||
// 2. Otherwise, set result["port"] to baseURL’s port, serialized.
|
||||
else if let Some(base_url_port) = base_url_ref.port {
|
||||
result.port = Some(base_url_port.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 8. If init contains none of "protocol", "hostname", "port", and "pathname", then set result["pathname"] to
|
||||
// the result of processing a base URL string given the result of URL path serializing baseURL and type.
|
||||
if init.protocol.is_none() && init.hostname.is_none() && init.port.is_none() && init.pathname.is_none() {
|
||||
result.pathname = Some(process_a_base_url_string(&base_url_ref.serialize_path(), r#type));
|
||||
}
|
||||
|
||||
// 9. If init contains none of "protocol", "hostname", "port", "pathname", and "search", then:
|
||||
if init.protocol.is_none()
|
||||
&& init.hostname.is_none()
|
||||
&& init.port.is_none()
|
||||
&& init.pathname.is_none()
|
||||
&& init.search.is_none()
|
||||
{
|
||||
// 1. Let baseQuery be baseURL’s query.
|
||||
let base_query = &base_url_ref.query;
|
||||
|
||||
// 2. If baseQuery is null, then set baseQuery to the empty string.
|
||||
// 3. Set result["search"] to the result of processing a base URL string given baseQuery and type.
|
||||
result.search = Some(process_a_base_url_string(base_query.as_deref().unwrap_or(""), r#type));
|
||||
}
|
||||
|
||||
// 10. If init contains none of "protocol", "hostname", "port", "pathname", "search", and "hash", then:
|
||||
if init.protocol.is_none()
|
||||
&& init.hostname.is_none()
|
||||
&& init.port.is_none()
|
||||
&& init.pathname.is_none()
|
||||
&& init.search.is_none()
|
||||
&& init.hash.is_none()
|
||||
{
|
||||
// 1. Let baseFragment be baseURL’s fragment.
|
||||
let base_fragment = &base_url_ref.fragment;
|
||||
|
||||
// 2. If baseFragment is null, then set baseFragment to the empty string.
|
||||
// 3. Set result["hash"] to the result of processing a base URL string given baseFragment and type.
|
||||
result.hash = Some(process_a_base_url_string(
|
||||
base_fragment.as_deref().unwrap_or(""),
|
||||
r#type,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 12. If init["protocol"] exists, then set result["protocol"] to the result of process protocol for init given init["protocol"] and type.
|
||||
if let Some(protocol) = &init.protocol {
|
||||
result.protocol = Some(process_protocol_for_init(protocol, r#type)?);
|
||||
}
|
||||
|
||||
// 13. If init["username"] exists, then set result["username"] to the result of process username for init given init["username"] and type.
|
||||
if let Some(username) = &init.username {
|
||||
result.username = Some(process_username_for_init(username, r#type));
|
||||
}
|
||||
|
||||
// 14. If init["password"] exists, then set result["password"] to the result of process password for init given init["password"] and type.
|
||||
if let Some(password) = &init.password {
|
||||
result.password = Some(process_password_for_init(password, r#type));
|
||||
}
|
||||
|
||||
// 15. If init["hostname"] exists, then set result["hostname"] to the result of process hostname for init given init["hostname"] and type.
|
||||
if let Some(hostname) = &init.hostname {
|
||||
result.hostname = Some(process_hostname_for_init(hostname, r#type)?);
|
||||
}
|
||||
|
||||
// 16. Let resultProtocolString be result["protocol"] if it exists; otherwise the empty string.
|
||||
let result_protocol_string = result.protocol.clone().unwrap_or_default();
|
||||
|
||||
// 17. If init["port"] exists, then set result["port"] to the result of process port for init given init["port"], resultProtocolString, and type.
|
||||
if let Some(port) = &init.port {
|
||||
result.port = Some(process_port_for_init(port, &result_protocol_string, r#type)?);
|
||||
}
|
||||
|
||||
// 18. If init["pathname"] exists:
|
||||
if let Some(init_pathname) = &init.pathname {
|
||||
// 1. Set result["pathname"] to init["pathname"].
|
||||
result.pathname = Some(init_pathname.clone());
|
||||
|
||||
// 2. If the following are all true:
|
||||
// * baseURL is not null;
|
||||
// * baseURL does not have an opaque path; and
|
||||
// * the result of running is an absolute pathname given result["pathname"] and type is false,
|
||||
// then:
|
||||
if let Some(base_url) = base_url.as_ref()
|
||||
&& !base_url.has_opaque_path
|
||||
&& !is_an_absolute_pathname(result.pathname.as_deref().unwrap(), r#type)
|
||||
{
|
||||
// 1. Let baseURLPath be the result of running process a base URL string given the result of URL path
|
||||
// serializing baseURL and type.
|
||||
let base_url_path = process_a_base_url_string(&base_url.serialize_path(), r#type);
|
||||
|
||||
// 2. Let slash index be the index of the last U+002F (/) code point found in baseURLPath, interpreted as a
|
||||
// sequence of code points, or null if there are no instances of the code point.
|
||||
let slash_index = base_url_path.rfind('/');
|
||||
|
||||
// 3. If slash index is not null:
|
||||
if let Some(slash_index) = slash_index {
|
||||
// 1. Let new pathname be the code point substring from 0 to slash index + 1 within baseURLPath.
|
||||
let mut new_pathname = base_url_path[..slash_index + 1].to_string();
|
||||
|
||||
// 2. Append result["pathname"] to the end of new pathname.
|
||||
// 3. Set result["pathname"] to new pathname.
|
||||
new_pathname.push_str(result.pathname.as_deref().unwrap());
|
||||
result.pathname = Some(new_pathname);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Set result["pathname"] to the result of process pathname for init given result["pathname"], resultProtocolString, and type.
|
||||
result.pathname = Some(process_pathname_for_init(
|
||||
result.pathname.as_deref().unwrap(),
|
||||
&result_protocol_string,
|
||||
r#type,
|
||||
)?);
|
||||
}
|
||||
|
||||
// 19. If init["search"] exists then set result["search"] to the result of process search for init given init["search"] and type.
|
||||
if let Some(search) = &init.search {
|
||||
result.search = Some(process_search_for_init(search, r#type));
|
||||
}
|
||||
|
||||
// 20. If init["hash"] exists then set result["hash"] to the result of process hash for init given init["hash"] and type.
|
||||
if let Some(hash) = &init.hash {
|
||||
result.hash = Some(process_hash_for_init(hash, r#type));
|
||||
}
|
||||
|
||||
// 21. Return result.
|
||||
Ok(result)
|
||||
}
|
||||
57
Libraries/LibURL/Rust/src/pattern/mod.rs
Normal file
57
Libraries/LibURL/Rust/src/pattern/mod.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#![allow(clippy::module_inception)]
|
||||
|
||||
mod canonicalization;
|
||||
mod component;
|
||||
mod constructor_string_parser;
|
||||
mod init;
|
||||
mod options;
|
||||
mod part;
|
||||
mod pattern;
|
||||
mod pattern_error;
|
||||
mod pattern_parser;
|
||||
mod string;
|
||||
mod tokenizer;
|
||||
|
||||
pub use canonicalization::canonicalize_a_hash;
|
||||
pub use canonicalization::canonicalize_a_hostname;
|
||||
pub use canonicalization::canonicalize_a_password;
|
||||
pub use canonicalization::canonicalize_a_pathname;
|
||||
pub use canonicalization::canonicalize_a_port;
|
||||
pub use canonicalization::canonicalize_a_protocol;
|
||||
pub use canonicalization::canonicalize_a_search;
|
||||
pub use canonicalization::canonicalize_a_username;
|
||||
pub use canonicalization::canonicalize_an_ipv6_hostname;
|
||||
pub use canonicalization::canonicalize_an_opaque_pathname;
|
||||
pub use component::Component;
|
||||
pub use component::GroupMatch;
|
||||
pub use component::RegularExpression;
|
||||
pub use component::Result as ComponentResult;
|
||||
pub use component::protocol_component_matches_a_special_scheme;
|
||||
pub use constructor_string_parser::ConstructorStringParser;
|
||||
pub use init::Init;
|
||||
pub use init::PatternProcessType;
|
||||
pub use init::process_a_url_pattern_init;
|
||||
pub use options::Options;
|
||||
pub use part::Part;
|
||||
pub use pattern::IgnoreCase;
|
||||
pub use pattern::Input;
|
||||
pub use pattern::MatchInput;
|
||||
pub use pattern::Pattern;
|
||||
pub use pattern::Result;
|
||||
pub use pattern_error::ErrorInfo;
|
||||
pub use pattern_error::PatternErrorOr;
|
||||
pub use pattern_parser::EncodingCallback;
|
||||
pub use pattern_parser::PatternParser;
|
||||
pub use string::escape_a_pattern_string;
|
||||
pub use string::escape_a_regexp_string;
|
||||
pub use string::full_wildcard_regexp_value;
|
||||
pub use string::generate_a_pattern_string;
|
||||
pub use string::generate_a_segment_wildcard_regexp;
|
||||
pub use tokenizer::Token;
|
||||
pub use tokenizer::Tokenizer;
|
||||
50
Libraries/LibURL/Rust/src/pattern/options.rs
Normal file
50
Libraries/LibURL/Rust/src/pattern/options.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#options
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Options {
|
||||
// https://urlpattern.spec.whatwg.org/#options-delimiter-code-point
|
||||
pub delimiter_code_point: Option<char>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#options-prefix-code-point
|
||||
pub prefix_code_point: Option<char>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#options-ignore-case
|
||||
pub ignore_case: bool,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
// https://urlpattern.spec.whatwg.org/#default-options
|
||||
pub fn default_() -> Self {
|
||||
// The default options is an options struct with delimiter code point set to the empty string and prefix code point set to the empty string.
|
||||
Self {
|
||||
delimiter_code_point: None,
|
||||
prefix_code_point: None,
|
||||
ignore_case: false,
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#hostname-options
|
||||
pub fn hostname() -> Self {
|
||||
// The hostname options is an options struct with delimiter code point set "." and prefix code point set to the empty string.
|
||||
Self {
|
||||
delimiter_code_point: Some('.'),
|
||||
prefix_code_point: None,
|
||||
ignore_case: false,
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pathname-options
|
||||
pub fn pathname() -> Self {
|
||||
// The pathname options is an options struct with delimiter code point set "/" and prefix code point set to "/".
|
||||
Self {
|
||||
delimiter_code_point: Some('/'),
|
||||
prefix_code_point: Some('/'),
|
||||
ignore_case: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
128
Libraries/LibURL/Rust/src/pattern/part.rs
Normal file
128
Libraries/LibURL/Rust/src/pattern/part.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Part {
|
||||
// https://urlpattern.spec.whatwg.org/#part-type
|
||||
// A part has an associated type, a string, which must be set upon creation.
|
||||
pub r#type: Type,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-value
|
||||
// A part has an associated value, a string, which must be set upon creation.
|
||||
pub value: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-modifier
|
||||
// A part has an associated modifier a string, which must be set upon creation.
|
||||
pub modifier: Modifier,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-name
|
||||
// A part has an associated name, a string, initially the empty string.
|
||||
pub name: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-prefix
|
||||
// A part has an associated prefix, a string, initially the empty string.
|
||||
pub prefix: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-suffix
|
||||
// A part has an associated suffix, a string, initially the empty string.
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-type
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Type {
|
||||
// The part represents a simple fixed text string.
|
||||
FixedText,
|
||||
|
||||
// The part represents a matching group with a custom regular expression.
|
||||
Regexp,
|
||||
|
||||
// The part represents a matching group that matches code points up to the next separator code point. This is
|
||||
// typically used for a named group like ":foo" that does not have a custom regular expression.
|
||||
SegmentWildcard,
|
||||
|
||||
// The part represents a matching group that greedily matches all code points. This is typically used for
|
||||
// the "*" wildcard matching group.
|
||||
FullWildcard,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#part-modifier
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Modifier {
|
||||
// The part does not have a modifier.
|
||||
None,
|
||||
|
||||
// The part has an optional modifier indicated by the U+003F (?) code point.
|
||||
Optional,
|
||||
|
||||
// The part has a "zero or more" modifier indicated by the U+002A (*) code point.
|
||||
ZeroOrMore,
|
||||
|
||||
// The part has a "one or more" modifier indicated by the U+002B (+) code point.
|
||||
OneOrMore,
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn new(r#type: Type, value: String, modifier: Modifier) -> Self {
|
||||
Self {
|
||||
r#type,
|
||||
value,
|
||||
modifier,
|
||||
name: String::new(),
|
||||
prefix: String::new(),
|
||||
suffix: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_name(
|
||||
r#type: Type,
|
||||
value: String,
|
||||
modifier: Modifier,
|
||||
name: String,
|
||||
prefix: String,
|
||||
suffix: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
r#type,
|
||||
value,
|
||||
modifier,
|
||||
name,
|
||||
prefix,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_to_string(r#type: Type) -> &'static str {
|
||||
match r#type {
|
||||
Type::FixedText => "FixedText",
|
||||
Type::Regexp => "Regexp",
|
||||
Type::SegmentWildcard => "SegmentWildcard",
|
||||
Type::FullWildcard => "FullWildcard",
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#convert-a-modifier-to-a-string
|
||||
pub fn convert_modifier_to_string(modifier: Modifier) -> &'static str {
|
||||
// 1. If modifier is "zero-or-more", then return "*".
|
||||
if modifier == Modifier::ZeroOrMore {
|
||||
return "*";
|
||||
}
|
||||
|
||||
// 2. If modifier is "optional", then return "?".
|
||||
if modifier == Modifier::Optional {
|
||||
return "?";
|
||||
}
|
||||
|
||||
// 3. If modifier is "one-or-more", then return "+".
|
||||
if modifier == Modifier::OneOrMore {
|
||||
return "+";
|
||||
}
|
||||
|
||||
// 4. Return the empty string.
|
||||
""
|
||||
}
|
||||
}
|
||||
687
Libraries/LibURL/Rust/src/pattern/pattern.rs
Normal file
687
Libraries/LibURL/Rust/src/pattern/pattern.rs
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::pattern::Component;
|
||||
use crate::pattern::ConstructorStringParser;
|
||||
use crate::pattern::Init;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::pattern::PatternProcessType;
|
||||
use crate::pattern::canonicalize_a_hash;
|
||||
use crate::pattern::canonicalize_a_hostname;
|
||||
use crate::pattern::canonicalize_a_password;
|
||||
use crate::pattern::canonicalize_a_pathname;
|
||||
use crate::pattern::canonicalize_a_port;
|
||||
use crate::pattern::canonicalize_a_protocol;
|
||||
use crate::pattern::canonicalize_a_search;
|
||||
use crate::pattern::canonicalize_a_username;
|
||||
use crate::pattern::canonicalize_an_ipv6_hostname;
|
||||
use crate::pattern::canonicalize_an_opaque_pathname;
|
||||
use crate::pattern::process_a_url_pattern_init;
|
||||
use crate::pattern::protocol_component_matches_a_special_scheme;
|
||||
use crate::url::BasicParseOptions;
|
||||
use crate::url::ExcludeFragment;
|
||||
use crate::url::Url;
|
||||
use crate::url::basic_parse;
|
||||
use crate::url::default_port_for_scheme;
|
||||
use crate::url::is_special_scheme;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#typedefdef-urlpatterninput
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Input {
|
||||
String(String),
|
||||
Init(Init),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MatchInput {
|
||||
String(String),
|
||||
Init(Init),
|
||||
Url(Url),
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#dictdef-urlpatternresult
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Result {
|
||||
pub inputs: Vec<Input>,
|
||||
|
||||
pub protocol: crate::pattern::component::Result,
|
||||
pub username: crate::pattern::component::Result,
|
||||
pub password: crate::pattern::component::Result,
|
||||
pub hostname: crate::pattern::component::Result,
|
||||
pub port: crate::pattern::component::Result,
|
||||
pub pathname: crate::pattern::component::Result,
|
||||
pub search: crate::pattern::component::Result,
|
||||
pub hash: crate::pattern::component::Result,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#dictdef-urlpatternoptions
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum IgnoreCase {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Pattern {
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-protocol-component
|
||||
// protocol component, a component
|
||||
protocol_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-username-component
|
||||
// username component, a component
|
||||
username_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-password-component
|
||||
// password component, a component
|
||||
password_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-hostname-component
|
||||
// hostname component, a component
|
||||
hostname_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-port-component
|
||||
// port component, a component
|
||||
port_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-pathname-component
|
||||
// pathname component, a component
|
||||
pathname_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-search-component
|
||||
// search component, a component
|
||||
search_component: Component,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-hash-component
|
||||
// hash component, a component
|
||||
hash_component: Component,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#hostname-pattern-is-an-ipv6-address
|
||||
fn hostname_pattern_is_an_ipv6_address(input: &str) -> bool {
|
||||
// 1. If input’s code point length is less than 2, then return false.
|
||||
if input.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Let input code points be input interpreted as a list of code points.
|
||||
let input_code_points = input.as_bytes();
|
||||
|
||||
// 3. If input code points[0] is U+005B ([), then return true.
|
||||
if input_code_points[0] == b'[' {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. If input code points[0] is U+007B ({) and input code points[1] is U+005B ([), then return true.
|
||||
if input_code_points[0] == b'{' && input_code_points[1] == b'[' {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. If input code points[0] is U+005C (\) and input code points[1] is U+005B ([), then return true.
|
||||
if input_code_points[0] == b'\\' && input_code_points[1] == b'[' {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-create
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
pub fn create(input: &Input, base_url: &Option<String>, ignore_case: IgnoreCase) -> PatternErrorOr<Self> {
|
||||
// 1. Let init be null.
|
||||
let mut init;
|
||||
|
||||
// 2. If input is a scalar value string then:
|
||||
if let Input::String(input_string) = input {
|
||||
// 1. Set init to the result of running parse a constructor string given input.
|
||||
init = ConstructorStringParser::parse(input_string)?;
|
||||
|
||||
// 2. If baseURL is null and init["protocol"] does not exist, then throw a TypeError.
|
||||
if base_url.is_none() && init.protocol.is_none() {
|
||||
return Err(crate::pattern::ErrorInfo::new(
|
||||
"Relative URLPattern constructor must provide one of baseURL or protocol",
|
||||
));
|
||||
}
|
||||
|
||||
// 3. If baseURL is not null, set init["baseURL"] to baseURL.
|
||||
if let Some(base_url) = base_url {
|
||||
init.base_url = Some(base_url.clone());
|
||||
}
|
||||
}
|
||||
// 3. Otherwise:
|
||||
else {
|
||||
// 1. Assert: input is a URLPatternInit.
|
||||
let Input::Init(input_init) = input else { unreachable!() };
|
||||
|
||||
// 2. If baseURL is not null, then throw a TypeError.
|
||||
if base_url.is_some() {
|
||||
return Err(crate::pattern::ErrorInfo::new(
|
||||
"Constructor with URLPatternInit should provide no baseURL",
|
||||
));
|
||||
}
|
||||
|
||||
// 3. Set init to input.
|
||||
init = input_init.clone();
|
||||
}
|
||||
|
||||
// 4. Let processedInit be the result of process a URLPatternInit given init, "pattern", null, null, null, null, null, null, null, and null.
|
||||
let none = None;
|
||||
let mut processed_init = process_a_url_pattern_init(
|
||||
&init,
|
||||
PatternProcessType::Pattern,
|
||||
&none,
|
||||
&none,
|
||||
&none,
|
||||
&none,
|
||||
&none,
|
||||
&none,
|
||||
&none,
|
||||
&none,
|
||||
)?;
|
||||
|
||||
// 5. For each componentName of « "protocol", "username", "password", "hostname", "port", "pathname", "search", "hash" »:
|
||||
// 1. If processedInit[componentName] does not exist, then set processedInit[componentName] to "*".
|
||||
if processed_init.protocol.is_none() {
|
||||
processed_init.protocol = Some("*".to_string());
|
||||
}
|
||||
if processed_init.username.is_none() {
|
||||
processed_init.username = Some("*".to_string());
|
||||
}
|
||||
if processed_init.password.is_none() {
|
||||
processed_init.password = Some("*".to_string());
|
||||
}
|
||||
if processed_init.hostname.is_none() {
|
||||
processed_init.hostname = Some("*".to_string());
|
||||
}
|
||||
if processed_init.port.is_none() {
|
||||
processed_init.port = Some("*".to_string());
|
||||
}
|
||||
if processed_init.pathname.is_none() {
|
||||
processed_init.pathname = Some("*".to_string());
|
||||
}
|
||||
if processed_init.search.is_none() {
|
||||
processed_init.search = Some("*".to_string());
|
||||
}
|
||||
if processed_init.hash.is_none() {
|
||||
processed_init.hash = Some("*".to_string());
|
||||
}
|
||||
|
||||
// 6. If processedInit["protocol"] is a special scheme and processedInit["port"] is a string which represents its
|
||||
// corresponding default port in radix-10 using ASCII digits then set processedInit["port"] to the empty string.
|
||||
if is_special_scheme(processed_init.protocol.as_ref().unwrap().as_bytes())
|
||||
&& let Ok(maybe_port) = processed_init.port.as_ref().unwrap().parse::<u16>()
|
||||
&& Some(maybe_port) == default_port_for_scheme(processed_init.protocol.as_ref().unwrap())
|
||||
{
|
||||
processed_init.port = Some(String::new());
|
||||
}
|
||||
|
||||
// 7. Let urlPattern be a new URL pattern.
|
||||
let mut url_pattern = Self::default();
|
||||
|
||||
// 8. Set urlPattern’s protocol component to the result of compiling a component given processedInit["protocol"],
|
||||
// canonicalize a protocol, and default options.
|
||||
url_pattern.protocol_component = Component::compile(
|
||||
processed_init.protocol.as_deref().unwrap(),
|
||||
Box::new(canonicalize_a_protocol),
|
||||
&crate::pattern::Options::default_(),
|
||||
)?;
|
||||
|
||||
// 9. Set urlPattern’s username component to the result of compiling a component given processedInit["username"],
|
||||
// canonicalize a username, and default options.
|
||||
url_pattern.username_component = Component::compile(
|
||||
processed_init.username.as_deref().unwrap(),
|
||||
Box::new(|value| Ok(canonicalize_a_username(value))),
|
||||
&crate::pattern::Options::default_(),
|
||||
)?;
|
||||
|
||||
// 10. Set urlPattern’s password component to the result of compiling a component given processedInit["password"],
|
||||
// canonicalize a password, and default options.
|
||||
url_pattern.password_component = Component::compile(
|
||||
processed_init.password.as_deref().unwrap(),
|
||||
Box::new(|value| Ok(canonicalize_a_password(value))),
|
||||
&crate::pattern::Options::default_(),
|
||||
)?;
|
||||
|
||||
// 11. If the result running hostname pattern is an IPv6 address given processedInit["hostname"] is true, then set
|
||||
// urlPattern’s hostname component to the result of compiling a component given processedInit["hostname"],
|
||||
// canonicalize an IPv6 hostname, and hostname options.
|
||||
if hostname_pattern_is_an_ipv6_address(processed_init.hostname.as_deref().unwrap()) {
|
||||
url_pattern.hostname_component = Component::compile(
|
||||
processed_init.hostname.as_deref().unwrap(),
|
||||
Box::new(canonicalize_an_ipv6_hostname),
|
||||
&crate::pattern::Options::hostname(),
|
||||
)?;
|
||||
}
|
||||
// 12. Otherwise, set urlPattern’s hostname component to the result of compiling a component given
|
||||
// processedInit["hostname"], canonicalize a hostname, and hostname options.
|
||||
else {
|
||||
url_pattern.hostname_component = Component::compile(
|
||||
processed_init.hostname.as_deref().unwrap(),
|
||||
Box::new(canonicalize_a_hostname),
|
||||
&crate::pattern::Options::hostname(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// 13. Set urlPattern’s port component to the result of compiling a component given processedInit["port"],
|
||||
// canonicalize a port, and default options.
|
||||
url_pattern.port_component = Component::compile(
|
||||
processed_init.port.as_deref().unwrap(),
|
||||
Box::new(|value| canonicalize_a_port(value, &None)),
|
||||
&crate::pattern::Options::default_(),
|
||||
)?;
|
||||
|
||||
// 14. Let compileOptions be a copy of the default options with the ignore case property set to options["ignoreCase"].
|
||||
let mut compile_options = crate::pattern::Options::default_();
|
||||
compile_options.ignore_case = ignore_case == IgnoreCase::Yes;
|
||||
|
||||
// 15. If the result of running protocol component matches a special scheme given urlPattern’s protocol component is true, then:
|
||||
if protocol_component_matches_a_special_scheme(&url_pattern.protocol_component) {
|
||||
// 1. Let pathCompileOptions be copy of the pathname options with the ignore case property set to options["ignoreCase"].
|
||||
let mut path_compile_options = crate::pattern::Options::pathname();
|
||||
path_compile_options.ignore_case = ignore_case == IgnoreCase::Yes;
|
||||
|
||||
// 2. Set urlPattern’s pathname component to the result of compiling a component given processedInit["pathname"],
|
||||
// canonicalize a pathname, and pathCompileOptions.
|
||||
url_pattern.pathname_component = Component::compile(
|
||||
processed_init.pathname.as_deref().unwrap(),
|
||||
Box::new(|value| Ok(canonicalize_a_pathname(value))),
|
||||
&path_compile_options,
|
||||
)?;
|
||||
}
|
||||
// 16. Otherwise set urlPattern’s pathname component to the result of compiling a component given
|
||||
// processedInit["pathname"], canonicalize an opaque pathname, and compileOptions.
|
||||
else {
|
||||
url_pattern.pathname_component = Component::compile(
|
||||
processed_init.pathname.as_deref().unwrap(),
|
||||
Box::new(canonicalize_an_opaque_pathname),
|
||||
&compile_options,
|
||||
)?;
|
||||
}
|
||||
|
||||
// 17. Set urlPattern’s search component to the result of compiling a component given processedInit["search"],
|
||||
// canonicalize a search, and compileOptions.
|
||||
url_pattern.search_component = Component::compile(
|
||||
processed_init.search.as_deref().unwrap(),
|
||||
Box::new(|value| Ok(canonicalize_a_search(value))),
|
||||
&compile_options,
|
||||
)?;
|
||||
|
||||
// 18. Set urlPattern’s hash component to the result of compiling a component given processedInit["hash"],
|
||||
// canonicalize a hash, and compileOptions.
|
||||
url_pattern.hash_component = Component::compile(
|
||||
processed_init.hash.as_deref().unwrap(),
|
||||
Box::new(|value| Ok(canonicalize_a_hash(value))),
|
||||
&compile_options,
|
||||
)?;
|
||||
|
||||
// 19. Return urlPattern.
|
||||
Ok(url_pattern)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#url-pattern-match
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
pub fn r#match(&self, input: &MatchInput, base_url_string: &Option<String>) -> PatternErrorOr<Option<Result>> {
|
||||
// 1. Let protocol be the empty string.
|
||||
let mut protocol = String::new();
|
||||
|
||||
// 2. Let username be the empty string.
|
||||
let mut username = String::new();
|
||||
|
||||
// 3. Let password be the empty string.
|
||||
let mut password = String::new();
|
||||
|
||||
// 4. Let hostname be the empty string.
|
||||
let mut hostname = String::new();
|
||||
|
||||
// 5. Let port be the empty string.
|
||||
let mut port = String::new();
|
||||
|
||||
// 6. Let pathname be the empty string.
|
||||
let mut pathname = String::new();
|
||||
|
||||
// 7. Let search be the empty string.
|
||||
let mut search = String::new();
|
||||
|
||||
// 8. Let hash be the empty string.
|
||||
let mut hash = String::new();
|
||||
|
||||
// 9. Let inputs be an empty list.
|
||||
let mut inputs = Vec::new();
|
||||
|
||||
// 10. If input is a URL, then append the serialization of input to inputs.
|
||||
if let MatchInput::Url(input_url) = input {
|
||||
inputs.push(Input::String(input_url.serialize(ExcludeFragment::No)));
|
||||
}
|
||||
// 11. Otherwise, append input to inputs.
|
||||
else {
|
||||
match input {
|
||||
MatchInput::String(input_string) => {
|
||||
inputs.push(Input::String(input_string.clone()));
|
||||
}
|
||||
MatchInput::Init(input_init) => inputs.push(Input::Init(input_init.clone())),
|
||||
MatchInput::Url(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// 12. If input is a URLPatternInit then:
|
||||
if let MatchInput::Init(input_init) = input {
|
||||
// 1. If baseURLString was given, throw a TypeError.
|
||||
if base_url_string.is_some() {
|
||||
return Err(crate::pattern::ErrorInfo::new(
|
||||
"Base URL cannot be provided when URLPatternInput is provided",
|
||||
));
|
||||
}
|
||||
|
||||
// 2. Let applyResult be the result of process a URLPatternInit given input, "url", protocol, username, password,
|
||||
// hostname, port, pathname, search, and hash. If this throws an exception, catch it, and return null.
|
||||
let protocol_option = Some(protocol.clone());
|
||||
let username_option = Some(username.clone());
|
||||
let password_option = Some(password.clone());
|
||||
let hostname_option = Some(hostname.clone());
|
||||
let port_option = Some(port.clone());
|
||||
let pathname_option = Some(pathname.clone());
|
||||
let search_option = Some(search.clone());
|
||||
let hash_option = Some(hash.clone());
|
||||
let apply_result = process_a_url_pattern_init(
|
||||
input_init,
|
||||
PatternProcessType::Url,
|
||||
&protocol_option,
|
||||
&username_option,
|
||||
&password_option,
|
||||
&hostname_option,
|
||||
&port_option,
|
||||
&pathname_option,
|
||||
&search_option,
|
||||
&hash_option,
|
||||
);
|
||||
let Ok(apply_result) = apply_result else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// 3. Set protocol to applyResult["protocol"].
|
||||
protocol = apply_result.protocol.unwrap();
|
||||
|
||||
// 4. Set username to applyResult["username"].
|
||||
username = apply_result.username.unwrap();
|
||||
|
||||
// 5. Set password to applyResult["password"].
|
||||
password = apply_result.password.unwrap();
|
||||
|
||||
// 6. Set hostname to applyResult["hostname"].
|
||||
hostname = apply_result.hostname.unwrap();
|
||||
|
||||
// 7. Set port to applyResult["port"].
|
||||
port = apply_result.port.unwrap();
|
||||
|
||||
// 8. Set pathname to applyResult["pathname"].
|
||||
pathname = apply_result.pathname.unwrap();
|
||||
|
||||
// 9. Set search to applyResult["search"].
|
||||
search = apply_result.search.unwrap();
|
||||
|
||||
// 10. Set hash to applyResult["hash"].
|
||||
hash = apply_result.hash.unwrap();
|
||||
}
|
||||
// 13. Otherwise:
|
||||
else {
|
||||
// 1. Let url be input.
|
||||
let url;
|
||||
|
||||
// 2. If input is a USVString:
|
||||
if let MatchInput::String(input_string) = input {
|
||||
// 1. Let baseURL be null.
|
||||
let mut base_url = None;
|
||||
|
||||
// 2. If baseURLString was given, then:
|
||||
if let Some(base_url_string) = base_url_string {
|
||||
// 1. Set baseURL to the result of running the basic URL parser on baseURLString.
|
||||
base_url = basic_parse(base_url_string, BasicParseOptions::new());
|
||||
|
||||
// 2. If baseURL is failure, return null.
|
||||
if base_url.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 3. Append baseURLString to inputs.
|
||||
inputs.push(Input::String(base_url_string.clone()));
|
||||
}
|
||||
|
||||
// 3. Set url to the result of running the basic URL parser on input with baseURL.
|
||||
// 4. If url is failure, return null.
|
||||
let maybe_url = if let Some(base_url) = base_url.as_ref() {
|
||||
basic_parse(input_string, BasicParseOptions::new().base_url(base_url))
|
||||
} else {
|
||||
basic_parse(input_string, BasicParseOptions::new())
|
||||
};
|
||||
let Some(parsed_url) = maybe_url else {
|
||||
return Ok(None);
|
||||
};
|
||||
url = parsed_url;
|
||||
} else {
|
||||
// 3. Assert: url is a URL.
|
||||
let MatchInput::Url(input_url) = input else {
|
||||
unreachable!()
|
||||
};
|
||||
url = input_url.clone();
|
||||
}
|
||||
|
||||
// 4. Set protocol to url’s scheme.
|
||||
protocol = url.scheme.clone();
|
||||
|
||||
// 5. Set username to url’s username.
|
||||
username = url.username.clone();
|
||||
|
||||
// 6. Set password to url’s password.
|
||||
password = url.password.clone();
|
||||
|
||||
// 7. Set hostname to url’s host, serialized, or the empty string if the value is null.
|
||||
if let Some(host) = &url.host {
|
||||
hostname = host.serialize();
|
||||
} else {
|
||||
hostname = String::new();
|
||||
}
|
||||
|
||||
// 8. Set port to url’s port, serialized, or the empty string if the value is null.
|
||||
if let Some(url_port) = url.port {
|
||||
port = url_port.to_string();
|
||||
} else {
|
||||
port = String::new();
|
||||
}
|
||||
|
||||
// 9. Set pathname to the result of URL path serializing url.
|
||||
pathname = url.serialize_path();
|
||||
|
||||
// 10. Set search to url’s query or the empty string if the value is null.
|
||||
search = url.query.unwrap_or_default();
|
||||
|
||||
// 11. Set hash to url’s fragment or the empty string if the value is null.
|
||||
hash = url.fragment.unwrap_or_default();
|
||||
}
|
||||
|
||||
// 14. Let protocolExecResult be RegExpBuiltinExec(urlPattern’s protocol component's regular expression, protocol).
|
||||
let protocol_exec_result = self.protocol_component.execute(&protocol);
|
||||
if !protocol_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 15. Let usernameExecResult be RegExpBuiltinExec(urlPattern’s username component's regular expression, username).
|
||||
let username_exec_result = self.username_component.execute(&username);
|
||||
if !username_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 16. Let passwordExecResult be RegExpBuiltinExec(urlPattern’s password component's regular expression, password).
|
||||
let password_exec_result = self.password_component.execute(&password);
|
||||
if !password_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 17. Let hostnameExecResult be RegExpBuiltinExec(urlPattern’s hostname component's regular expression, hostname).
|
||||
let hostname_exec_result = self.hostname_component.execute(&hostname);
|
||||
if !hostname_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 18. Let portExecResult be RegExpBuiltinExec(urlPattern’s port component's regular expression, port).
|
||||
let port_exec_result = self.port_component.execute(&port);
|
||||
if !port_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 19. Let pathnameExecResult be RegExpBuiltinExec(urlPattern’s pathname component's regular expression, pathname).
|
||||
let pathname_exec_result = self.pathname_component.execute(&pathname);
|
||||
if !pathname_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 20. Let searchExecResult be RegExpBuiltinExec(urlPattern’s search component's regular expression, search).
|
||||
let search_exec_result = self.search_component.execute(&search);
|
||||
if !search_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 21. Let hashExecResult be RegExpBuiltinExec(urlPattern’s hash component's regular expression, hash).
|
||||
let hash_exec_result = self.hash_component.execute(&hash);
|
||||
if !hash_exec_result.success {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 22. If protocolExecResult, usernameExecResult, passwordExecResult, hostnameExecResult, portExecResult,
|
||||
// pathnameExecResult, searchExecResult, or hashExecResult are null then return null.
|
||||
// NOTE: Done in steps above at point of exec.
|
||||
|
||||
// 23. Let result be a new URLPatternResult.
|
||||
let mut result = Result::default();
|
||||
|
||||
// 24. Set result["inputs"] to inputs.
|
||||
result.inputs = inputs;
|
||||
|
||||
// 25. Set result["protocol"] to the result of creating a component match result given urlPattern’s protocol
|
||||
// component, protocol, and protocolExecResult.
|
||||
result.protocol = self
|
||||
.protocol_component
|
||||
.create_match_result(&protocol, &protocol_exec_result);
|
||||
|
||||
// 26. Set result["username"] to the result of creating a component match result given urlPattern’s username
|
||||
// component, username, and usernameExecResult.
|
||||
result.username = self
|
||||
.username_component
|
||||
.create_match_result(&username, &username_exec_result);
|
||||
|
||||
// 27. Set result["password"] to the result of creating a component match result given urlPattern’s password
|
||||
// component, password, and passwordExecResult.
|
||||
result.password = self
|
||||
.password_component
|
||||
.create_match_result(&password, &password_exec_result);
|
||||
|
||||
// 28. Set result["hostname"] to the result of creating a component match result given urlPattern’s hostname
|
||||
// component, hostname, and hostnameExecResult.
|
||||
result.hostname = self
|
||||
.hostname_component
|
||||
.create_match_result(&hostname, &hostname_exec_result);
|
||||
|
||||
// 29. Set result["port"] to the result of creating a component match result given urlPattern’s port component,
|
||||
// port, and portExecResult.
|
||||
result.port = self.port_component.create_match_result(&port, &port_exec_result);
|
||||
|
||||
// 30. Set result["pathname"] to the result of creating a component match result given urlPattern’s pathname
|
||||
// component, pathname, and pathnameExecResult.
|
||||
result.pathname = self
|
||||
.pathname_component
|
||||
.create_match_result(&pathname, &pathname_exec_result);
|
||||
|
||||
// 31. Set result["search"] to the result of creating a component match result given urlPattern’s search component,
|
||||
// search, and searchExecResult.
|
||||
result.search = self.search_component.create_match_result(&search, &search_exec_result);
|
||||
|
||||
// 32. Set result["hash"] to the result of creating a component match result given urlPattern’s hash component,
|
||||
// hash, and hashExecResult.
|
||||
result.hash = self.hash_component.create_match_result(&hash, &hash_exec_result);
|
||||
|
||||
// 33. Return result.
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
pub fn has_regexp_groups(&self) -> bool {
|
||||
// 1. If urlPattern’s protocol component has regexp groups is true, then return true.
|
||||
if self.protocol_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. If urlPattern’s username component has regexp groups is true, then return true.
|
||||
if self.username_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. If urlPattern’s password component has regexp groups is true, then return true.
|
||||
if self.password_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. If urlPattern’s hostname component has regexp groups is true, then return true.
|
||||
if self.hostname_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. If urlPattern’s port component has regexp groups is true, then return true.
|
||||
if self.port_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6. If urlPattern’s pathname component has regexp groups is true, then return true.
|
||||
if self.pathname_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 7. If urlPattern’s search component has regexp groups is true, then return true.
|
||||
if self.search_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 8. If urlPattern’s hash component has regexp groups is true, then return true.
|
||||
if self.hash_component.has_regexp_groups {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 9. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
pub fn protocol_component(&self) -> &Component {
|
||||
&self.protocol_component
|
||||
}
|
||||
|
||||
pub fn username_component(&self) -> &Component {
|
||||
&self.username_component
|
||||
}
|
||||
|
||||
pub fn password_component(&self) -> &Component {
|
||||
&self.password_component
|
||||
}
|
||||
|
||||
pub fn hostname_component(&self) -> &Component {
|
||||
&self.hostname_component
|
||||
}
|
||||
|
||||
pub fn port_component(&self) -> &Component {
|
||||
&self.port_component
|
||||
}
|
||||
|
||||
pub fn pathname_component(&self) -> &Component {
|
||||
&self.pathname_component
|
||||
}
|
||||
|
||||
pub fn search_component(&self) -> &Component {
|
||||
&self.search_component
|
||||
}
|
||||
|
||||
pub fn hash_component(&self) -> &Component {
|
||||
&self.hash_component
|
||||
}
|
||||
}
|
||||
22
Libraries/LibURL/Rust/src/pattern/pattern_error.rs
Normal file
22
Libraries/LibURL/Rust/src/pattern/pattern_error.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
// NOTE: All exceptions which are thrown by the URLPattern spec are TypeErrors which web-based callers are expected to assume.
|
||||
// If this ever does not become the case, this should change to also include the error type.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ErrorInfo {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub type PatternErrorOr<ValueT> = Result<ValueT, ErrorInfo>;
|
||||
|
||||
impl ErrorInfo {
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
472
Libraries/LibURL/Rust/src/pattern/pattern_parser.rs
Normal file
472
Libraries/LibURL/Rust/src/pattern/pattern_parser.rs
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::pattern::ErrorInfo;
|
||||
use crate::pattern::Options;
|
||||
use crate::pattern::Part;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
use crate::pattern::Token;
|
||||
use crate::pattern::Tokenizer;
|
||||
use crate::pattern::full_wildcard_regexp_value;
|
||||
use crate::pattern::generate_a_segment_wildcard_regexp;
|
||||
use crate::pattern::part::Modifier as PartModifier;
|
||||
use crate::pattern::part::Type as PartType;
|
||||
use crate::pattern::tokenizer::Policy as TokenizerPolicy;
|
||||
use crate::pattern::tokenizer::Type as TokenType;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser
|
||||
pub struct PatternParser {
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-token-list
|
||||
// A pattern parser has an associated token list, a token list, initially an empty list.
|
||||
pub token_list: Vec<Token>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-encoding-callback
|
||||
// A pattern parser has an associated encoding callback, a encoding callback, that must be set upon creation.
|
||||
pub encoding_callback: EncodingCallback,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-segment-wildcard-regexp
|
||||
// A pattern parser has an associated segment wildcard regexp, a string, that must be set upon creation.
|
||||
pub segment_wildcard_regexp: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-part-list
|
||||
// A pattern parser has an associated part list, a part list, initially an empty list.
|
||||
pub part_list: Vec<Part>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-pending-fixed-value
|
||||
// A pattern parser has an associated pending fixed value, a string, initially the empty string.
|
||||
pub pending_fixed_value: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-index
|
||||
// A pattern parser has an associated index, a number, initially 0.
|
||||
pub index: usize,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#pattern-parser-next-numeric-name
|
||||
// A pattern parser has an associated next numeric name, a number, initially 0.
|
||||
pub next_numeric_name: usize,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#encoding-callback
|
||||
// An encoding callback is an abstract algorithm that takes a given string input. The input will be a simple text
|
||||
// piece of a pattern string. An implementing algorithm will validate and encode the input. It must return the
|
||||
// encoded string or throw an exception.
|
||||
pub type EncodingCallback = Box<dyn Fn(&str) -> PatternErrorOr<String>>;
|
||||
|
||||
impl PatternParser {
|
||||
pub(crate) fn new(encoding_callback: EncodingCallback, segment_wildcard_regexp: String) -> Self {
|
||||
Self {
|
||||
token_list: Vec::new(),
|
||||
encoding_callback,
|
||||
segment_wildcard_regexp,
|
||||
part_list: Vec::new(),
|
||||
pending_fixed_value: String::new(),
|
||||
index: 0,
|
||||
next_numeric_name: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#consume-a-required-token
|
||||
pub(crate) fn consume_a_required_token(&mut self, r#type: TokenType) -> PatternErrorOr<()> {
|
||||
// 1. Let result be the result of running try to consume a token given parser and type.
|
||||
let result = self.try_to_consume_a_token(r#type);
|
||||
|
||||
// 2. If result is null, then throw a TypeError.
|
||||
if result.is_none() {
|
||||
return Err(ErrorInfo::new(format!(
|
||||
"Missing required token '{}' in URL pattern",
|
||||
Token::type_to_string(r#type)
|
||||
)));
|
||||
}
|
||||
|
||||
// 3. Return result.
|
||||
// NOTE: No caller actually needs the result, so we just ignore it.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#consume-text
|
||||
pub(crate) fn consume_text(&mut self) -> String {
|
||||
// 1. Let result be the empty string.
|
||||
let mut result = String::new();
|
||||
|
||||
// 1. While true:
|
||||
loop {
|
||||
// 1. Let token be the result of running try to consume a token given parser and "char".
|
||||
let mut token = self.try_to_consume_a_token(TokenType::Char);
|
||||
|
||||
// 2. If token is null, then set token to the result of running try to consume a token given parser and "escaped-char".
|
||||
if token.is_none() {
|
||||
token = self.try_to_consume_a_token(TokenType::EscapedChar);
|
||||
}
|
||||
|
||||
// 3. If token is null, then break.
|
||||
let Some(token) = token else {
|
||||
break;
|
||||
};
|
||||
|
||||
// 4. Append token’s value to the end of result.
|
||||
result.push_str(&token.value);
|
||||
}
|
||||
|
||||
// 2. Return result.
|
||||
result
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#maybe-add-a-part-from-the-pending-fixed-value
|
||||
pub(crate) fn maybe_add_a_part_from_the_pending_fixed_value(&mut self) -> PatternErrorOr<()> {
|
||||
// 1. If parser’s pending fixed value is the empty string, then return.
|
||||
if self.pending_fixed_value.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 2. Let encoded value be the result of running parser’s encoding callback given parser’s pending fixed value.
|
||||
let encoded_value = (self.encoding_callback)(&self.pending_fixed_value)?;
|
||||
|
||||
// 3. Set parser’s pending fixed value to the empty string.
|
||||
self.pending_fixed_value.clear();
|
||||
|
||||
// 4. Let part be a new part whose type is "fixed-text", value is encoded value, and modifier is "none".
|
||||
// 5. Append part to parser’s part list.
|
||||
self.part_list
|
||||
.push(Part::new(PartType::FixedText, encoded_value, PartModifier::None));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-duplicate-name
|
||||
pub(crate) fn is_a_duplicate_name(&self, name: &str) -> bool {
|
||||
// 1. For each part of parser’s part list:
|
||||
for part in &self.part_list {
|
||||
// 1. If part’s name is name, then return true.
|
||||
if part.name == name {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Return false.
|
||||
false
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#add-a-part
|
||||
pub(crate) fn add_a_part(
|
||||
&mut self,
|
||||
prefix: &str,
|
||||
name_token: Option<Token>,
|
||||
regexp_or_wildcard_token: Option<Token>,
|
||||
suffix: &str,
|
||||
modifier_token: Option<Token>,
|
||||
) -> PatternErrorOr<()> {
|
||||
// 1. Let modifier be "none".
|
||||
let mut modifier = PartModifier::None;
|
||||
|
||||
// 2. If modifier token is not null:
|
||||
if let Some(modifier_token) = modifier_token {
|
||||
// 1. If modifier token’s value is "?" then set modifier to "optional".
|
||||
if modifier_token.value == "?" {
|
||||
modifier = PartModifier::Optional;
|
||||
}
|
||||
// 2. Otherwise if modifier token’s value is "*" then set modifier to "zero-or-more".
|
||||
else if modifier_token.value == "*" {
|
||||
modifier = PartModifier::ZeroOrMore;
|
||||
}
|
||||
// 3. Otherwise if modifier token’s value is "+" then set modifier to "one-or-more".
|
||||
else if modifier_token.value == "+" {
|
||||
modifier = PartModifier::OneOrMore;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If name token is null and regexp or wildcard token is null and modifier is "none":
|
||||
// NOTE: This was a "{foo}" grouping. We add this to the pending fixed value so that it will be combined with
|
||||
// any previous or subsequent text.
|
||||
if name_token.is_none() && regexp_or_wildcard_token.is_none() && modifier == PartModifier::None {
|
||||
// 1. Append prefix to the end of parser’s pending fixed value.
|
||||
self.pending_fixed_value.push_str(prefix);
|
||||
|
||||
// 2. Return.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 4. Run maybe add a part from the pending fixed value given parser.
|
||||
self.maybe_add_a_part_from_the_pending_fixed_value()?;
|
||||
|
||||
// 5. If name token is null and regexp or wildcard token is null:
|
||||
// NOTE: This was a "{foo}?" grouping. The modifier means we cannot combine it with other text. Therefore we
|
||||
// add it as a part immediately.
|
||||
if name_token.is_none() && regexp_or_wildcard_token.is_none() {
|
||||
// 1. Assert: suffix is the empty string.
|
||||
assert!(suffix.is_empty());
|
||||
|
||||
// 2. If prefix is the empty string, then return.
|
||||
if prefix.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3. Let encoded value be the result of running parser’s encoding callback given prefix.
|
||||
let encoded_value = (self.encoding_callback)(prefix)?;
|
||||
|
||||
// 4. Let part be a new part whose type is "fixed-text", value is encoded value, and modifier is modifier.
|
||||
// 5. Append part to parser’s part list.
|
||||
self.part_list
|
||||
.push(Part::new(PartType::FixedText, encoded_value, modifier));
|
||||
|
||||
// 6. Return.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 6. Let regexp value be the empty string.
|
||||
// NOTE: Next, we convert the regexp or wildcard token into a regular expression.
|
||||
let mut regexp_value =
|
||||
// 7. If regexp or wildcard token is null, then set regexp value to parser’s segment wildcard regexp.
|
||||
if let Some(regexp_or_wildcard_token) = regexp_or_wildcard_token.as_ref() {
|
||||
// 8. Otherwise if regexp or wildcard token’s type is "asterisk", then set regexp value to the full wildcard regexp value.
|
||||
if regexp_or_wildcard_token.r#type == TokenType::Asterisk {
|
||||
full_wildcard_regexp_value.to_string()
|
||||
}
|
||||
// 9. Otherwise set regexp value to regexp or wildcard token’s value.
|
||||
else {
|
||||
regexp_or_wildcard_token.value.clone()
|
||||
}
|
||||
} else {
|
||||
self.segment_wildcard_regexp.clone()
|
||||
};
|
||||
|
||||
// 10. Let type be "regexp".
|
||||
// NOTE: Next, we convert regexp value into a part type. We make sure to go to a regular expression first so
|
||||
// that an equivalent "regexp" token will be treated the same as a "name" or "asterisk" token.
|
||||
let mut r#type = PartType::Regexp;
|
||||
|
||||
// 11. If regexp value is parser’s segment wildcard regexp:
|
||||
if regexp_value == self.segment_wildcard_regexp {
|
||||
// 1. Set type to "segment-wildcard".
|
||||
r#type = PartType::SegmentWildcard;
|
||||
|
||||
// 2. Set regexp value to the empty string.
|
||||
regexp_value = String::new();
|
||||
}
|
||||
// 12. Otherwise if regexp value is the full wildcard regexp value:
|
||||
else if regexp_value == full_wildcard_regexp_value {
|
||||
// 1. Set type to "full-wildcard".
|
||||
r#type = PartType::FullWildcard;
|
||||
|
||||
// 2. Set regexp value to the empty string.
|
||||
regexp_value = String::new();
|
||||
}
|
||||
|
||||
// 13. Let name be the empty string.
|
||||
// NOTE: Next, we determine the part name. This can be explicitly provided by a "name" token or be automatically assigned.
|
||||
let mut name = String::new();
|
||||
|
||||
// 14. If name token is not null, then set name to name token’s value.
|
||||
if let Some(name_token) = name_token {
|
||||
name = name_token.value;
|
||||
}
|
||||
// 15. Otherwise if regexp or wildcard token is not null:
|
||||
else if regexp_or_wildcard_token.is_some() {
|
||||
// 1. Set name to parser’s next numeric name, serialized.
|
||||
name = self.next_numeric_name.to_string();
|
||||
|
||||
// 2. Increment parser’s next numeric name by 1.
|
||||
self.next_numeric_name += 1;
|
||||
}
|
||||
|
||||
// 16. If the result of running is a duplicate name given parser and name is true, then throw a TypeError.
|
||||
if self.is_a_duplicate_name(&name) {
|
||||
return Err(ErrorInfo::new(format!(
|
||||
"Duplicate name '{name}' provided in URL pattern"
|
||||
)));
|
||||
}
|
||||
|
||||
// 17. Let encoded prefix be the result of running parser’s encoding callback given prefix.
|
||||
// NOTE: Finally, we encode the fixed text values and create the part.
|
||||
let encoded_prefix = (self.encoding_callback)(prefix)?;
|
||||
|
||||
// 18. Let encoded suffix be the result of running parser’s encoding callback given suffix.
|
||||
let encoded_suffix = (self.encoding_callback)(suffix)?;
|
||||
|
||||
// 19. Let part be a new part whose type is type, value is regexp value, modifier is modifier, name is name, prefix
|
||||
// is encoded prefix, and suffix is encoded suffix.
|
||||
// 20. Append part to parser’s part list.
|
||||
self.part_list.push(Part::new_with_name(
|
||||
r#type,
|
||||
regexp_value,
|
||||
modifier,
|
||||
name,
|
||||
encoded_prefix,
|
||||
encoded_suffix,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#try-to-consume-a-modifier-token
|
||||
pub(crate) fn try_to_consume_a_modifier_token(&mut self) -> Option<Token> {
|
||||
// 1. Let token be the result of running try to consume a token given parser and "other-modifier".
|
||||
let mut token = self.try_to_consume_a_token(TokenType::OtherModifier);
|
||||
|
||||
// 2. If token is not null, then return token.
|
||||
if token.is_some() {
|
||||
return token;
|
||||
}
|
||||
|
||||
// 3. Set token to the result of running try to consume a token given parser and "asterisk".
|
||||
token = self.try_to_consume_a_token(TokenType::Asterisk);
|
||||
|
||||
// 4. Return token.
|
||||
token
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#try-to-consume-a-regexp-or-wildcard-token
|
||||
pub(crate) fn try_to_consume_a_regexp_or_wildcard_token(&mut self, name_token: Option<Token>) -> Option<Token> {
|
||||
// 1. Let token be the result of running try to consume a token given parser and "regexp".
|
||||
let mut token = self.try_to_consume_a_token(TokenType::Regexp);
|
||||
|
||||
// 2. If name token is null and token is null, then set token to the result of running try to consume a token given
|
||||
// parser and "asterisk".
|
||||
if name_token.is_none() && token.is_none() {
|
||||
token = self.try_to_consume_a_token(TokenType::Asterisk);
|
||||
}
|
||||
|
||||
// 3. Return token.
|
||||
token
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#try-to-consume-a-token
|
||||
pub(crate) fn try_to_consume_a_token(&mut self, r#type: TokenType) -> Option<Token> {
|
||||
// 1. Assert: parser’s index is less than parser’s token list size.
|
||||
assert!(self.index < self.token_list.len());
|
||||
|
||||
// 2. Let next token be parser’s token list[parser’s index].
|
||||
let next_token = self.token_list[self.index].clone();
|
||||
|
||||
// 3. If next token’s type is not type return null.
|
||||
if next_token.r#type != r#type {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 4. Increment parser’s index by 1.
|
||||
self.index += 1;
|
||||
|
||||
// 5. Return next token.
|
||||
Some(next_token)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#parse-a-pattern-string
|
||||
pub fn parse(input: &str, options: &Options, encoding_callback: EncodingCallback) -> PatternErrorOr<Vec<Part>> {
|
||||
// 1. Let parser be a new pattern parser whose encoding callback is encoding callback and segment wildcard regexp
|
||||
// is the result of running generate a segment wildcard regexp given options.
|
||||
let mut parser = Self::new(encoding_callback, generate_a_segment_wildcard_regexp(options));
|
||||
|
||||
// 2. Set parser’s token list to the result of running tokenize given input and "strict".
|
||||
parser.token_list = Tokenizer::tokenize(input, TokenizerPolicy::Strict)?;
|
||||
|
||||
// 3. While parser’s index is less than parser’s token list's size:
|
||||
while parser.index < parser.token_list.len() {
|
||||
// 1. Let char token be the result of running try to consume a token given parser and "char".
|
||||
let char_token = parser.try_to_consume_a_token(TokenType::Char);
|
||||
|
||||
// 2. Let name token be the result of running try to consume a token given parser and "name".
|
||||
let mut name_token = parser.try_to_consume_a_token(TokenType::Name);
|
||||
|
||||
// 3. Let regexp or wildcard token be the result of running try to consume a regexp or wildcard token given
|
||||
// parser and name token.
|
||||
let mut regexp_or_wildcard_token = parser.try_to_consume_a_regexp_or_wildcard_token(name_token.clone());
|
||||
|
||||
// 4. If name token is not null or regexp or wildcard token is not null:
|
||||
// NOTE: If there is a matching group, we need to add the part immediately.
|
||||
if name_token.is_some() || regexp_or_wildcard_token.is_some() {
|
||||
// 1. Let prefix be the empty string.
|
||||
let mut prefix = String::new();
|
||||
|
||||
// 2. If char token is not null then set prefix to char token’s value.
|
||||
if let Some(char_token) = char_token {
|
||||
prefix = char_token.value;
|
||||
}
|
||||
|
||||
// 3. If prefix is not the empty string and not options’s prefix code point:
|
||||
if !prefix.is_empty()
|
||||
&& (options.prefix_code_point.is_none() || prefix != options.prefix_code_point.unwrap().to_string())
|
||||
{
|
||||
// 1. Append prefix to the end of parser’s pending fixed value.
|
||||
parser.pending_fixed_value.push_str(&prefix);
|
||||
|
||||
// 2. Set prefix to the empty string.
|
||||
prefix.clear();
|
||||
}
|
||||
|
||||
// 4. Run maybe add a part from the pending fixed value given parser.
|
||||
parser.maybe_add_a_part_from_the_pending_fixed_value()?;
|
||||
|
||||
// 5. Let modifier token be the result of running try to consume a modifier token given parser.
|
||||
let modifier_token = parser.try_to_consume_a_modifier_token();
|
||||
|
||||
// 6. Run add a part given parser, prefix, name token, regexp or wildcard token, the empty string,
|
||||
// and modifier token.
|
||||
parser.add_a_part(&prefix, name_token, regexp_or_wildcard_token, "", modifier_token)?;
|
||||
|
||||
// 7. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Let fixed token be char token.
|
||||
// NOTE: If there was no matching group, then we need to buffer any fixed text. We want to collect as
|
||||
// much text as possible before adding it as a "fixed-text" part.
|
||||
let mut fixed_token = char_token;
|
||||
|
||||
// 6. If fixed token is null, then set fixed token to the result of running try to consume a token given
|
||||
// parser and "escaped-char".
|
||||
if fixed_token.is_none() {
|
||||
fixed_token = parser.try_to_consume_a_token(TokenType::EscapedChar);
|
||||
}
|
||||
|
||||
// 7. If fixed token is not null:
|
||||
if let Some(fixed_token) = fixed_token {
|
||||
// 1. Append fixed token’s value to parser’s pending fixed value.
|
||||
parser.pending_fixed_value.push_str(&fixed_token.value);
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 8. Let open token be the result of running try to consume a token given parser and "open".
|
||||
let open_token = parser.try_to_consume_a_token(TokenType::Open);
|
||||
|
||||
// 9. If open token is not null:
|
||||
if open_token.is_some() {
|
||||
// 1. Let prefix be the result of running consume text given parser.
|
||||
let prefix = parser.consume_text();
|
||||
|
||||
// 2. Set name token to the result of running try to consume a token given parser and "name".
|
||||
name_token = parser.try_to_consume_a_token(TokenType::Name);
|
||||
|
||||
// 3. Set regexp or wildcard token to the result of running try to consume a regexp or wildcard token
|
||||
// given parser and name token.
|
||||
regexp_or_wildcard_token = parser.try_to_consume_a_regexp_or_wildcard_token(name_token.clone());
|
||||
|
||||
// 4. Let suffix be the result of running consume text given parser.
|
||||
let suffix = parser.consume_text();
|
||||
|
||||
// 5. Run consume a required token given parser and "close".
|
||||
parser.consume_a_required_token(TokenType::Close)?;
|
||||
|
||||
// 6. Let modifier token to the result of running try to consume a modifier token given parser.
|
||||
let modifier_token = parser.try_to_consume_a_modifier_token();
|
||||
|
||||
// 7. Run add a part given parser, prefix, name token, regexp or wildcard token, suffix, and modifier token.
|
||||
parser.add_a_part(&prefix, name_token, regexp_or_wildcard_token, &suffix, modifier_token)?;
|
||||
|
||||
// 8. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 10. Run maybe add a part from the pending fixed value given parser.
|
||||
parser.maybe_add_a_part_from_the_pending_fixed_value()?;
|
||||
|
||||
// 11. Run consume a required token given parser and "end".
|
||||
parser.consume_a_required_token(TokenType::End)?;
|
||||
}
|
||||
|
||||
// 4. Return parser’s part list.
|
||||
Ok(parser.part_list)
|
||||
}
|
||||
}
|
||||
329
Libraries/LibURL/Rust/src/pattern/string.rs
Normal file
329
Libraries/LibURL/Rust/src/pattern/string.rs
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::pattern::Options;
|
||||
use crate::pattern::Part;
|
||||
use crate::pattern::part::Type as PartType;
|
||||
use crate::pattern::tokenizer::Tokenizer;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
// https://urlpattern.spec.whatwg.org/#full-wildcard-regexp-value
|
||||
pub const full_wildcard_regexp_value: &str = ".*";
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#escape-a-pattern-string
|
||||
pub fn escape_a_pattern_string(input: &str) -> String {
|
||||
// 1. Assert: input is an ASCII string.
|
||||
assert!(input.is_ascii());
|
||||
|
||||
// 2. Let result be the empty string.
|
||||
let mut result = String::new();
|
||||
|
||||
// 3. Let index be 0.
|
||||
// 4. While index is less than input’s length:
|
||||
for c in input.chars() {
|
||||
// 1. Let c be input[index].
|
||||
// 2. Increment index by 1.
|
||||
|
||||
// 3. If c is one of:
|
||||
// * U+002B (+);
|
||||
// * U+002A (*);
|
||||
// * U+003F (?);
|
||||
// * U+003A (:);
|
||||
// * U+007B ({);
|
||||
// * U+007D (});
|
||||
// * U+0028 (();
|
||||
// * U+0029 ()); or
|
||||
// * U+005C (\),
|
||||
// then append U+005C (\) to the end of result.
|
||||
if "+*?:{}()\\".contains(c) {
|
||||
result.push('\\');
|
||||
}
|
||||
|
||||
// 4. Append c to the end of result.
|
||||
result.push(c);
|
||||
}
|
||||
|
||||
// 5. Return result.
|
||||
result
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#escape-a-regexp-string
|
||||
pub fn escape_a_regexp_string(input: &str) -> String {
|
||||
// 1. Assert: input is an ASCII string.
|
||||
assert!(input.is_ascii());
|
||||
|
||||
// 2. Let result be the empty string.
|
||||
let mut result = String::new();
|
||||
|
||||
// 3. Let index be 0.
|
||||
// 4. While index is less than input’s length:
|
||||
for c in input.chars() {
|
||||
// 1. Let c be input[index].
|
||||
// 2. Increment index by 1.
|
||||
|
||||
// 3. If c is one of:
|
||||
// * U+002E (.);
|
||||
// * U+002B (+);
|
||||
// * U+002A (*);
|
||||
// * U+003F (?);
|
||||
// * U+005E (^);
|
||||
// * U+0024 ($);
|
||||
// * U+007B ({);
|
||||
// * U+007D (});
|
||||
// * U+0028 (();
|
||||
// * U+0029 ());
|
||||
// * U+005B ([);
|
||||
// * U+005D (]);
|
||||
// * U+007C (|);
|
||||
// * U+002F (/); or
|
||||
// * U+005C (\),
|
||||
// then append "\" to the end of result.
|
||||
if ".+*?^${}()[]|/\\".contains(c) {
|
||||
result.push('\\');
|
||||
}
|
||||
|
||||
// 4. Append c to the end of result.
|
||||
result.push(c);
|
||||
}
|
||||
|
||||
// 5. Return result.
|
||||
result
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#generate-a-segment-wildcard-regexp
|
||||
pub fn generate_a_segment_wildcard_regexp(options: &Options) -> String {
|
||||
// 1. Let result be "[^".
|
||||
let mut result = String::from("[^");
|
||||
|
||||
// 2. Append the result of running escape a regexp string given options’s delimiter code point to the end of result.
|
||||
if let Some(delimiter_code_point) = options.delimiter_code_point {
|
||||
result.push_str(&escape_a_regexp_string(&delimiter_code_point.to_string()));
|
||||
}
|
||||
|
||||
// 3. Append "]+?" to the end of result.
|
||||
result.push_str("]+?");
|
||||
|
||||
// 4. Return result.
|
||||
result
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#generate-a-pattern-string
|
||||
pub fn generate_a_pattern_string(part_list: &[Part], options: &Options) -> String {
|
||||
// 1. Let result be the empty string.
|
||||
let mut result = String::new();
|
||||
|
||||
// 2. Let index list be the result of getting the indices for part list.
|
||||
// 3. For each index of index list:
|
||||
for index in 0..part_list.len() {
|
||||
// 1. Let part be part list[index].
|
||||
let part = &part_list[index];
|
||||
|
||||
// 2. Let previous part be part list[index - 1] if index is greater than 0, otherwise let it be null.
|
||||
let previous_part = if index > 0 { Some(&part_list[index - 1]) } else { None };
|
||||
|
||||
// 3. Let next part be part list[index + 1] if index is less than index list’s size - 1, otherwise let it be null.
|
||||
let next_part = if index + 1 < part_list.len() {
|
||||
Some(&part_list[index + 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 4. If part’s type is "fixed-text" then:
|
||||
if part.r#type == PartType::FixedText {
|
||||
// 1. If part’s modifier is "none" then:
|
||||
if part.modifier == crate::pattern::part::Modifier::None {
|
||||
// 1. Append the result of running escape a pattern string given part’s value to the end of result.
|
||||
result.push_str(&escape_a_pattern_string(&part.value));
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Append "{" to the end of result.
|
||||
result.push('{');
|
||||
|
||||
// 3. Append the result of running escape a pattern string given part’s value to the end of result.
|
||||
result.push_str(&escape_a_pattern_string(&part.value));
|
||||
|
||||
// 4. Append "}" to the end of result.
|
||||
result.push('}');
|
||||
|
||||
// 5. Append the result of running convert a modifier to a string given part’s modifier to the end of result.
|
||||
result.push_str(Part::convert_modifier_to_string(part.modifier));
|
||||
|
||||
// 6. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Let custom name be true if part’s name[0] is not an ASCII digit; otherwise false.
|
||||
let custom_name = !part.name.as_bytes()[0].is_ascii_digit();
|
||||
|
||||
// 6. Let needs grouping be true if at least one of the following are true, otherwise let it be false:
|
||||
// * part’s suffix is not the empty string.
|
||||
// * part’s prefix is not the empty string and is not options’s prefix code point.
|
||||
let mut needs_grouping = !part.suffix.is_empty()
|
||||
|| (!part.prefix.is_empty()
|
||||
&& (options.prefix_code_point.is_some()
|
||||
&& part.prefix != options.prefix_code_point.unwrap().to_string()));
|
||||
|
||||
// 7. If all of the following are true:
|
||||
// * needs grouping is false; and
|
||||
// * custom name is true; and
|
||||
// * part’s type is "segment-wildcard"; and
|
||||
// * part’s modifier is "none"; and
|
||||
// * next part is not null; and
|
||||
// * next part’s prefix is the empty string; and
|
||||
// * next part’s suffix is the empty string
|
||||
// then:
|
||||
if !needs_grouping
|
||||
&& custom_name
|
||||
&& part.r#type == PartType::SegmentWildcard
|
||||
&& part.modifier == crate::pattern::part::Modifier::None
|
||||
&& let Some(next_part) = next_part
|
||||
&& next_part.prefix.is_empty()
|
||||
&& next_part.suffix.is_empty()
|
||||
{
|
||||
// 1. If next part’s type is "fixed-text":
|
||||
if next_part.r#type == PartType::FixedText {
|
||||
// 1. Set needs grouping to true if the result of running is a valid name code point given next part’s
|
||||
// value's first code point and the boolean false is true.
|
||||
needs_grouping = next_part
|
||||
.value
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|code_point| Tokenizer::is_a_valid_name_code_point(code_point as u32, false));
|
||||
}
|
||||
// 2. Otherwise:
|
||||
else {
|
||||
// 1. Set needs grouping to true if next part’s name[0] is an ASCII digit.
|
||||
needs_grouping = next_part.name.as_bytes()[0].is_ascii_digit();
|
||||
}
|
||||
}
|
||||
|
||||
// 8. If all of the following are true:
|
||||
// * needs grouping is false; and
|
||||
// * part’s prefix is the empty string; and
|
||||
// * previous part is not null; and
|
||||
// * previous part’s type is "fixed-text"; and
|
||||
// * previous part’s value's last code point is options’s prefix code point.
|
||||
// then set needs grouping to true.
|
||||
if !needs_grouping
|
||||
&& part.prefix.is_empty()
|
||||
&& previous_part.is_some()
|
||||
&& previous_part.unwrap().r#type == PartType::FixedText
|
||||
&& ((previous_part.unwrap().value.is_empty() && options.prefix_code_point.is_none())
|
||||
|| (options.prefix_code_point.is_some()
|
||||
&& previous_part.unwrap().value == options.prefix_code_point.unwrap().to_string()))
|
||||
{
|
||||
needs_grouping = true;
|
||||
}
|
||||
|
||||
// 9. Assert: part’s name is not the empty string or null.
|
||||
assert!(!part.name.is_empty());
|
||||
|
||||
// 10. If needs grouping is true, then append "{" to the end of result.
|
||||
if needs_grouping {
|
||||
result.push('{');
|
||||
}
|
||||
|
||||
// 11. Append the result of running escape a pattern string given part’s prefix to the end of result.
|
||||
result.push_str(&escape_a_pattern_string(&part.prefix));
|
||||
|
||||
// 12. If custom name is true:
|
||||
if custom_name {
|
||||
// 1. Append ":" to the end of result.
|
||||
result.push(':');
|
||||
|
||||
// 2. Append part’s name to the end of result.
|
||||
result.push_str(&part.name);
|
||||
}
|
||||
|
||||
// 13. If part’s type is "regexp" then:
|
||||
if part.r#type == PartType::Regexp {
|
||||
// 1. Append "(" to the end of result.
|
||||
result.push('(');
|
||||
|
||||
// 2. Append part’s value to the end of result.
|
||||
result.push_str(&part.value);
|
||||
|
||||
// 3. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
}
|
||||
// 14. Otherwise if part’s type is "segment-wildcard" and custom name is false:
|
||||
else if part.r#type == PartType::SegmentWildcard && !custom_name {
|
||||
// 1. Append "(" to the end of result.
|
||||
result.push('(');
|
||||
|
||||
// 2. Append the result of running generate a segment wildcard regexp given options to the end of result.
|
||||
result.push_str(&generate_a_segment_wildcard_regexp(options));
|
||||
|
||||
// 3. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
}
|
||||
// 15. Otherwise if part’s type is "full-wildcard":
|
||||
else if part.r#type == PartType::FullWildcard {
|
||||
// 1. If custom name is false and one of the following is true:
|
||||
// * previous part is null; or
|
||||
// * previous part’s type is "fixed-text"; or
|
||||
// * previous part’s modifier is not "none"; or
|
||||
// * needs grouping is true; or
|
||||
// * part’s prefix is not the empty string
|
||||
// then append "*" to the end of result.
|
||||
if !custom_name
|
||||
&& (previous_part.is_none()
|
||||
|| previous_part.unwrap().r#type == PartType::FixedText
|
||||
|| previous_part.unwrap().modifier != crate::pattern::part::Modifier::None
|
||||
|| needs_grouping
|
||||
|| !part.prefix.is_empty())
|
||||
{
|
||||
result.push('*');
|
||||
}
|
||||
// 2. Otherwise:
|
||||
else {
|
||||
// 1. Append "(" to the end of result.
|
||||
result.push('(');
|
||||
|
||||
// 2. Append full wildcard regexp value to the end of result.
|
||||
result.push_str(full_wildcard_regexp_value);
|
||||
|
||||
// 3. Append ")" to the end of result.
|
||||
result.push(')');
|
||||
}
|
||||
}
|
||||
|
||||
// 16. If all of the following are true:
|
||||
// * part’s type is "segment-wildcard"; and
|
||||
// * custom name is true; and
|
||||
// * part’s suffix is not the empty string; and
|
||||
// * The result of running is a valid name code point given part’s suffix's first code point and the boolean false is true
|
||||
// then append U+005C (\) to the end of result.
|
||||
if part.r#type == PartType::SegmentWildcard
|
||||
&& custom_name
|
||||
&& !part.suffix.is_empty()
|
||||
&& part
|
||||
.suffix
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|code_point| Tokenizer::is_a_valid_name_code_point(code_point as u32, false))
|
||||
{
|
||||
result.push('\\');
|
||||
}
|
||||
|
||||
// 17. Append the result of running escape a pattern string given part’s suffix to the end of result.
|
||||
result.push_str(&escape_a_pattern_string(&part.suffix));
|
||||
|
||||
// 18. If needs grouping is true, then append "}" to the end of result.
|
||||
if needs_grouping {
|
||||
result.push('}');
|
||||
}
|
||||
|
||||
// 19. Append the result of running convert a modifier to a string given part’s modifier to the end of result.
|
||||
result.push_str(Part::convert_modifier_to_string(part.modifier));
|
||||
}
|
||||
|
||||
// 4. Return result.
|
||||
result
|
||||
}
|
||||
540
Libraries/LibURL/Rust/src/pattern/tokenizer.rs
Normal file
540
Libraries/LibURL/Rust/src/pattern/tokenizer.rs
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use libunicode_rust::character_types::code_point_has_identifier_continue_property;
|
||||
use libunicode_rust::character_types::code_point_has_identifier_start_property;
|
||||
|
||||
use crate::pattern::ErrorInfo;
|
||||
use crate::pattern::PatternErrorOr;
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#token
|
||||
// A token is a struct representing a single lexical token within a pattern string.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token {
|
||||
// https://urlpattern.spec.whatwg.org/#token-type
|
||||
// A token has an associated type, a string, initially "invalid-char".
|
||||
pub r#type: Type,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#token-index
|
||||
// A token has an associated index, a number, initially 0. It is the position of the first code point in the pattern string represented by the token.
|
||||
pub index: u32,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#token-value
|
||||
// A token has an associated value, a string, initially the empty string. It contains the code points from the pattern string represented by the token.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#token-type
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Type {
|
||||
// The token represents a U+007B ({) code point.
|
||||
Open,
|
||||
|
||||
// The token represents a U+007D (}) code point.
|
||||
Close,
|
||||
|
||||
// The token represents a string of the form "(<regular expression>)". The regular expression is required to consist of only ASCII code points.
|
||||
Regexp,
|
||||
|
||||
// The token represents a string of the form ":<name>". The name value is restricted to code points that are consistent with JavaScript identifiers.
|
||||
Name,
|
||||
|
||||
// The token represents a valid pattern code point without any special syntactical meaning.
|
||||
Char,
|
||||
|
||||
// The token represents a code point escaped using a backslash like "\<char>".
|
||||
EscapedChar,
|
||||
|
||||
// The token represents a matching group modifier that is either the U+003F (?) or U+002B (+) code points.
|
||||
OtherModifier,
|
||||
|
||||
// The token represents a U+002A (*) code point that can be either a wildcard matching group or a matching group modifier.
|
||||
Asterisk,
|
||||
|
||||
// The token represents the end of the pattern string.
|
||||
End,
|
||||
|
||||
// The token represents a code point that is invalid in the pattern. This could be because of the code point value
|
||||
// itself or due to its location within the pattern relative to other syntactic elements.
|
||||
InvalidChar,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn type_to_string(r#type: Type) -> &'static str {
|
||||
match r#type {
|
||||
Type::Open => "Open",
|
||||
Type::Close => "Close",
|
||||
Type::Regexp => "Regexp",
|
||||
Type::Name => "Name",
|
||||
Type::Char => "Char",
|
||||
Type::EscapedChar => "EscapedChar",
|
||||
Type::OtherModifier => "OtherModifier",
|
||||
Type::Asterisk => "Asterisk",
|
||||
Type::End => "End",
|
||||
Type::InvalidChar => "InvalidChar",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Token {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}, index: {}, value: '{}'",
|
||||
Self::type_to_string(self.r#type),
|
||||
self.index,
|
||||
self.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer
|
||||
// A tokenizer is a struct.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tokenizer {
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer-input
|
||||
// A tokenizer has an associated input, a pattern string, initially the empty string.
|
||||
pub input: String,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer-policy
|
||||
// A tokenizer has an associated policy, a tokenize policy, initially "strict".
|
||||
pub policy: Policy,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer-token-list
|
||||
// A tokenizer has an associated token list, a token list, initially an empty list.
|
||||
pub token_list: Vec<Token>,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer-index
|
||||
// A tokenizer has an associated index, a number, initially 0.
|
||||
pub index: usize,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer-next-index
|
||||
// A tokenizer has an associated next index, a number, initially 0.
|
||||
pub next_index: usize,
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenizer-code-point
|
||||
// A tokenizer has an associated code point, a Unicode code point, initially null.
|
||||
pub code_point: u32,
|
||||
|
||||
input_code_points: Vec<char>,
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenize-policy
|
||||
// A tokenize policy is a string that must be either "strict" or "lenient".
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Policy {
|
||||
Strict,
|
||||
Lenient,
|
||||
}
|
||||
|
||||
impl Tokenizer {
|
||||
pub(crate) fn new(input: &str, policy: Policy) -> Self {
|
||||
Self {
|
||||
input: input.to_string(),
|
||||
policy,
|
||||
token_list: Vec::new(),
|
||||
index: 0,
|
||||
next_index: 0,
|
||||
code_point: 0,
|
||||
input_code_points: input.chars().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#tokenize
|
||||
pub fn tokenize(input: &str, policy: Policy) -> PatternErrorOr<Vec<Token>> {
|
||||
// 1. Let tokenizer be a new tokenizer.
|
||||
// 2. Set tokenizer’s input to input.
|
||||
// 3. Set tokenizer’s policy to policy.
|
||||
let mut tokenizer = Self::new(input, policy);
|
||||
|
||||
// 4. While tokenizer’s index is less than tokenizer’s input's code point length:
|
||||
while tokenizer.index < tokenizer.input_code_points.len() {
|
||||
// 1. Run seek and get the next code point given tokenizer and tokenizer’s index.
|
||||
tokenizer.seek_and_get_the_next_code_point(tokenizer.index as u32);
|
||||
|
||||
// 2. If tokenizer’s code point is U+002A (*):
|
||||
if tokenizer.code_point == '*' as u32 {
|
||||
// 1. Run add a token with default position and length given tokenizer and "asterisk".
|
||||
tokenizer.add_a_token_with_default_position_and_length(Type::Asterisk);
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. If tokenizer’s code point is U+002B (+) or U+003F (?):
|
||||
if tokenizer.code_point == '+' as u32 || tokenizer.code_point == '?' as u32 {
|
||||
// 1. Run add a token with default position and length given tokenizer and "other-modifier".
|
||||
tokenizer.add_a_token_with_default_position_and_length(Type::OtherModifier);
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. If tokenizer’s code point is U+005C (\):
|
||||
if tokenizer.code_point == '\\' as u32 {
|
||||
// 1. If tokenizer’s index is equal to tokenizer’s input's code point length − 1:
|
||||
if tokenizer.index == tokenizer.input_code_points.len() - 1 {
|
||||
// 1. Run process a tokenizing error given tokenizer, tokenizer’s next index, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(tokenizer.next_index as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Let escaped index be tokenizer’s next index.
|
||||
let escaped_index = tokenizer.next_index;
|
||||
|
||||
// 3. Run get the next code point given tokenizer.
|
||||
tokenizer.get_the_next_code_point();
|
||||
|
||||
// 4. Run add a token with default length given tokenizer, "escaped-char", tokenizer’s next index, and escaped index.
|
||||
tokenizer.add_a_token_with_default_length(
|
||||
Type::EscapedChar,
|
||||
tokenizer.next_index as u32,
|
||||
escaped_index as u32,
|
||||
);
|
||||
|
||||
// 5. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. If tokenizer’s code point is U+007B ({):
|
||||
if tokenizer.code_point == '{' as u32 {
|
||||
// 1. Run add a token with default position and length given tokenizer and "open".
|
||||
tokenizer.add_a_token_with_default_position_and_length(Type::Open);
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 6. If tokenizer’s code point is U+007D (}):
|
||||
if tokenizer.code_point == '}' as u32 {
|
||||
// 1. Run add a token with default position and length given tokenizer and "close".
|
||||
tokenizer.add_a_token_with_default_position_and_length(Type::Close);
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. If tokenizer’s code point is U+003A (:):
|
||||
if tokenizer.code_point == ':' as u32 {
|
||||
// 1. Let name position be tokenizer’s next index.
|
||||
let mut name_position = tokenizer.next_index;
|
||||
|
||||
// 2. Let name start be name position.
|
||||
let name_start = name_position;
|
||||
|
||||
// 3. While name position is less than tokenizer’s input's code point length:
|
||||
while name_position < tokenizer.input_code_points.len() {
|
||||
// 1. Run seek and get the next code point given tokenizer and name position.
|
||||
tokenizer.seek_and_get_the_next_code_point(name_position as u32);
|
||||
|
||||
// 2. Let first code point be true if name position equals name start and false otherwise.
|
||||
let first_code_point = name_position == name_start;
|
||||
|
||||
// 3. Let valid code point be the result of running is a valid name code point given tokenizer’s code point and first code point.
|
||||
let valid_code_point = Self::is_a_valid_name_code_point(tokenizer.code_point, first_code_point);
|
||||
|
||||
// 4. If valid code point is false break.
|
||||
if !valid_code_point {
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. Set name position to tokenizer’s next index.
|
||||
name_position = tokenizer.next_index;
|
||||
}
|
||||
|
||||
// 4. If name position is less than or equal to name start:
|
||||
if name_position <= name_start {
|
||||
// 1. Run process a tokenizing error given tokenizer, name start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(name_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Run add a token with default length given tokenizer, "name", name position, and name start.
|
||||
tokenizer.add_a_token_with_default_length(Type::Name, name_position as u32, name_start as u32);
|
||||
|
||||
// 6. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 8. If tokenizer’s code point is U+0028 (():
|
||||
if tokenizer.code_point == '(' as u32 {
|
||||
// 1. Let depth be 1.
|
||||
let mut depth = 1u32;
|
||||
|
||||
// 2. Let regexp position be tokenizer’s next index.
|
||||
let mut regexp_position = tokenizer.next_index;
|
||||
|
||||
// 3. Let regexp start be regexp position.
|
||||
let regexp_start = regexp_position;
|
||||
|
||||
// 4. Let error be false.
|
||||
let mut error = false;
|
||||
|
||||
// 5. While regexp position is less than tokenizer’s input's code point length:
|
||||
while regexp_position < tokenizer.input_code_points.len() {
|
||||
// 1. Run seek and get the next code point given tokenizer and regexp position.
|
||||
tokenizer.seek_and_get_the_next_code_point(regexp_position as u32);
|
||||
|
||||
// 2. If the result of running is ASCII given tokenizer’s code point is false:
|
||||
if tokenizer.code_point > 0x7f {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Set error to true.
|
||||
error = true;
|
||||
|
||||
// 3. Break.
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. If regexp position equals regexp start and tokenizer’s code point is U+003F (?):
|
||||
if regexp_position == regexp_start && tokenizer.code_point == '?' as u32 {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Set error to true.
|
||||
error = true;
|
||||
|
||||
// 3. Break.
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. If tokenizer’s code point is U+005C (\):
|
||||
if tokenizer.code_point == '\\' as u32 {
|
||||
// 1. If regexp position equals tokenizer’s input's code point length − 1:
|
||||
if regexp_position == tokenizer.input_code_points.len() - 1 {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Set error to true.
|
||||
error = true;
|
||||
|
||||
// 3. Break
|
||||
break;
|
||||
}
|
||||
|
||||
// 2. Run get the next code point given tokenizer.
|
||||
tokenizer.get_the_next_code_point();
|
||||
|
||||
// 3. If the result of running is ASCII given tokenizer’s code point is false:
|
||||
if tokenizer.code_point > 0x7f {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Set error to true.
|
||||
error = true;
|
||||
|
||||
// 3. Break.
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. Set regexp position to tokenizer’s next index.
|
||||
regexp_position = tokenizer.next_index;
|
||||
|
||||
// 5. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. If tokenizer’s code point is U+0029 ()):
|
||||
if tokenizer.code_point == ')' as u32 {
|
||||
// 1. Decrement depth by 1.
|
||||
depth -= 1;
|
||||
|
||||
// 1. If depth is 0:
|
||||
if depth == 0 {
|
||||
// 1. Set regexp position to tokenizer’s next index.
|
||||
regexp_position = tokenizer.next_index;
|
||||
|
||||
// 2. Break.
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 6. Otherwise if tokenizer’s code point is U+0028 (():
|
||||
else if tokenizer.code_point == '(' as u32 {
|
||||
// 1. Increment depth by 1.
|
||||
depth += 1;
|
||||
|
||||
// 2. If regexp position equals tokenizer’s input's code point length − 1:
|
||||
if regexp_position == tokenizer.input_code_points.len() - 1 {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Set error to true.
|
||||
error = true;
|
||||
|
||||
// 3. Break
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. Let temporary position be tokenizer’s next index.
|
||||
let temporary_position = tokenizer.next_index;
|
||||
|
||||
// 4. Run get the next code point given tokenizer.
|
||||
tokenizer.get_the_next_code_point();
|
||||
|
||||
// 5. If tokenizer’s code point is not U+003F (?):
|
||||
if tokenizer.code_point != '?' as u32 {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Set error to true.
|
||||
error = true;
|
||||
|
||||
// 3. Break.
|
||||
break;
|
||||
}
|
||||
|
||||
// 6. Set tokenizer’s next index to temporary position.
|
||||
tokenizer.next_index = temporary_position;
|
||||
}
|
||||
|
||||
// 7. Set regexp position to tokenizer’s next index.
|
||||
regexp_position = tokenizer.next_index;
|
||||
}
|
||||
|
||||
// 6. If error is true continue.
|
||||
if error {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 7. If depth is not zero:
|
||||
if depth != 0 {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 8. Let regexp length be regexp position − regexp start − 1.
|
||||
let regexp_length = regexp_position - regexp_start - 1;
|
||||
|
||||
// 9. If regexp length is zero:
|
||||
if regexp_length == 0 {
|
||||
// 1. Run process a tokenizing error given tokenizer, regexp start, and tokenizer’s index.
|
||||
tokenizer.process_a_tokenizing_error(regexp_start as u32, tokenizer.index as u32)?;
|
||||
|
||||
// 2. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 10. Run add a token given tokenizer, "regexp", regexp position, regexp start, and regexp length.
|
||||
tokenizer.add_a_token(
|
||||
Type::Regexp,
|
||||
regexp_position as u32,
|
||||
regexp_start as u32,
|
||||
regexp_length as u32,
|
||||
);
|
||||
|
||||
// 11. Continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// 9. Run add a token with default position and length given tokenizer and "char".
|
||||
tokenizer.add_a_token_with_default_position_and_length(Type::Char);
|
||||
}
|
||||
|
||||
// 5. Run add a token with default length given tokenizer, "end", tokenizer’s index, and tokenizer’s index.
|
||||
tokenizer.add_a_token_with_default_length(Type::End, tokenizer.index as u32, tokenizer.index as u32);
|
||||
|
||||
// 6. Return tokenizer’s token list.
|
||||
Ok(tokenizer.token_list)
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#get-the-next-code-point
|
||||
pub(crate) fn get_the_next_code_point(&mut self) {
|
||||
// 1. Set tokenizer’s code point to the Unicode code point in tokenizer’s input at the position indicated by tokenizer’s next index.
|
||||
self.code_point = self.input_code_points[self.next_index] as u32;
|
||||
|
||||
// 2. Increment tokenizer’s next index by 1.
|
||||
self.next_index += 1;
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#seek-and-get-the-next-code-point
|
||||
pub(crate) fn seek_and_get_the_next_code_point(&mut self, index: u32) {
|
||||
// 1. Set tokenizer’s next index to index.
|
||||
self.next_index = index as usize;
|
||||
|
||||
// 2. Run get the next code point given tokenizer.
|
||||
self.get_the_next_code_point();
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#add-a-token
|
||||
pub(crate) fn add_a_token(&mut self, r#type: Type, next_position: u32, value_position: u32, value_length: u32) {
|
||||
// 1. Let token be a new token.
|
||||
let token = Token {
|
||||
// 2. Set token’s type to type.
|
||||
r#type,
|
||||
|
||||
// 3. Set token’s index to tokenizer’s index.
|
||||
index: self.index as u32,
|
||||
|
||||
// 4. Set token’s value to the code point substring from value position with length value length within tokenizer’s input.
|
||||
value: self
|
||||
.input_code_points
|
||||
.iter()
|
||||
.skip(value_position as usize)
|
||||
.take(value_length as usize)
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// 5. Append token to the back of tokenizer’s token list.
|
||||
self.token_list.push(token);
|
||||
|
||||
// 5. Set tokenizer’s index to next position.
|
||||
self.index = next_position as usize;
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#add-a-token-with-default-length
|
||||
pub(crate) fn add_a_token_with_default_length(&mut self, r#type: Type, next_position: u32, value_position: u32) {
|
||||
// 1. Let computed length be next position − value position.
|
||||
let computed_length = next_position - value_position;
|
||||
|
||||
// 2. Run add a token given tokenizer, type, next position, value position, and computed length.
|
||||
self.add_a_token(r#type, next_position, value_position, computed_length);
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#add-a-token-with-default-position-and-length
|
||||
pub(crate) fn add_a_token_with_default_position_and_length(&mut self, r#type: Type) {
|
||||
// 1. Run add a token with default length given tokenizer, type, tokenizer’s next index, and tokenizer’s index.
|
||||
self.add_a_token_with_default_length(r#type, self.next_index as u32, self.index as u32);
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#process-a-tokenizing-error
|
||||
pub(crate) fn process_a_tokenizing_error(&mut self, next_position: u32, value_position: u32) -> PatternErrorOr<()> {
|
||||
// 1. If tokenizer’s policy is "strict", then throw a TypeError.
|
||||
if self.policy == Policy::Strict {
|
||||
return Err(ErrorInfo::new("Error processing a token"));
|
||||
}
|
||||
|
||||
// 2. Assert: tokenizer’s policy is "lenient".
|
||||
assert!(self.policy == Policy::Lenient);
|
||||
|
||||
// 3. Run add a token with default length given tokenizer, "invalid-char", next position, and value position.
|
||||
self.add_a_token_with_default_length(Type::InvalidChar, next_position, value_position);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://urlpattern.spec.whatwg.org/#is-a-valid-name-code-point
|
||||
pub fn is_a_valid_name_code_point(code_point: u32, first: bool) -> bool {
|
||||
// 1. If first is true return the result of checking if code point is contained in the IdentifierStart set of code points.
|
||||
if first {
|
||||
return code_point == '$' as u32
|
||||
|| code_point == '_' as u32
|
||||
|| code_point_has_identifier_start_property(code_point);
|
||||
}
|
||||
|
||||
// 2. Otherwise return the result of checking if code point is contained in the IdentifierPart set of code points.
|
||||
code_point == '$' as u32 || code_point_has_identifier_continue_property(code_point)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ mod types;
|
|||
|
||||
pub(crate) use self::scheme::default_port_for_scheme;
|
||||
pub(crate) use self::scheme::is_special_scheme;
|
||||
pub(crate) use self::scheme::special_schemes;
|
||||
pub(crate) use self::types::ExcludeFragment;
|
||||
pub use self::types::Host;
|
||||
pub use self::types::State;
|
||||
pub use self::types::Url;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ pub(crate) enum ExcludeFragment {
|
|||
Yes,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Url {
|
||||
pub(crate) fn set_scheme(&mut self, scheme: String) {
|
||||
self.scheme = scheme;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <AK/Array.h>
|
||||
#include <AK/IPv4Address.h>
|
||||
#include <AK/IPv6Address.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/StringUtils.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
|
|
@ -23,6 +24,123 @@ static String string_from_ffi(FFI::RustUrlByteSlice slice)
|
|||
return String::from_ascii_without_validation({ reinterpret_cast<char const*>(slice.data), slice.length });
|
||||
}
|
||||
|
||||
static FFI::RustUrlByteSlice string_to_ffi(StringView string)
|
||||
{
|
||||
return {
|
||||
.data = reinterpret_cast<u8 const*>(string.characters_without_null_termination()),
|
||||
.length = string.length(),
|
||||
};
|
||||
}
|
||||
|
||||
static FFI::RustUrlByteSlice string_to_ffi(String const& string)
|
||||
{
|
||||
return string_to_ffi(string.bytes_as_string_view());
|
||||
}
|
||||
|
||||
static FFI::RustUrlByteSlice optional_string_to_ffi(Optional<String> const& string)
|
||||
{
|
||||
if (!string.has_value())
|
||||
return string_to_ffi(""sv);
|
||||
return string_to_ffi(*string);
|
||||
}
|
||||
|
||||
static FFI::RustUrlPatternInit init_to_ffi(URLPattern::Init const& init)
|
||||
{
|
||||
return {
|
||||
.has_protocol = init.protocol.has_value(),
|
||||
.protocol = optional_string_to_ffi(init.protocol),
|
||||
.has_username = init.username.has_value(),
|
||||
.username = optional_string_to_ffi(init.username),
|
||||
.has_password = init.password.has_value(),
|
||||
.password = optional_string_to_ffi(init.password),
|
||||
.has_hostname = init.hostname.has_value(),
|
||||
.hostname = optional_string_to_ffi(init.hostname),
|
||||
.has_port = init.port.has_value(),
|
||||
.port = optional_string_to_ffi(init.port),
|
||||
.has_pathname = init.pathname.has_value(),
|
||||
.pathname = optional_string_to_ffi(init.pathname),
|
||||
.has_search = init.search.has_value(),
|
||||
.search = optional_string_to_ffi(init.search),
|
||||
.has_hash = init.hash.has_value(),
|
||||
.hash = optional_string_to_ffi(init.hash),
|
||||
.has_base_url = init.base_url.has_value(),
|
||||
.base_url = optional_string_to_ffi(init.base_url),
|
||||
};
|
||||
}
|
||||
|
||||
static URLPattern::Init init_from_ffi(FFI::RustUrlPatternInit const& init)
|
||||
{
|
||||
URLPattern::Init result;
|
||||
if (init.has_protocol)
|
||||
result.protocol = string_from_ffi(init.protocol);
|
||||
if (init.has_username)
|
||||
result.username = string_from_ffi(init.username);
|
||||
if (init.has_password)
|
||||
result.password = string_from_ffi(init.password);
|
||||
if (init.has_hostname)
|
||||
result.hostname = string_from_ffi(init.hostname);
|
||||
if (init.has_port)
|
||||
result.port = string_from_ffi(init.port);
|
||||
if (init.has_pathname)
|
||||
result.pathname = string_from_ffi(init.pathname);
|
||||
if (init.has_search)
|
||||
result.search = string_from_ffi(init.search);
|
||||
if (init.has_hash)
|
||||
result.hash = string_from_ffi(init.hash);
|
||||
if (init.has_base_url)
|
||||
result.base_url = string_from_ffi(init.base_url);
|
||||
return result;
|
||||
}
|
||||
|
||||
static URLPattern::Component::Result component_result_from_ffi(FFI::RustUrlPatternComponentResult const& result)
|
||||
{
|
||||
URLPattern::Component::Result component_result;
|
||||
component_result.input = string_from_ffi(result.input);
|
||||
|
||||
for (size_t i = 0; i < result.group_count; ++i) {
|
||||
auto const& group = result.groups[i];
|
||||
auto name = string_from_ffi(group.name);
|
||||
if (group.has_value)
|
||||
component_result.groups.set(move(name), string_from_ffi(group.value));
|
||||
else
|
||||
component_result.groups.set(move(name), Empty {});
|
||||
}
|
||||
|
||||
return component_result;
|
||||
}
|
||||
|
||||
static URLPattern::Result result_from_ffi(FFI::RustUrlPatternExecResult const& result)
|
||||
{
|
||||
URLPattern::Result converted;
|
||||
|
||||
converted.inputs.ensure_capacity(result.input_count);
|
||||
for (size_t i = 0; i < result.input_count; ++i) {
|
||||
auto const& input = result.inputs[i];
|
||||
if (input.is_string)
|
||||
converted.inputs.unchecked_append(string_from_ffi(input.string));
|
||||
else
|
||||
converted.inputs.unchecked_append(init_from_ffi(input.init));
|
||||
}
|
||||
|
||||
converted.protocol = component_result_from_ffi(result.protocol);
|
||||
converted.username = component_result_from_ffi(result.username);
|
||||
converted.password = component_result_from_ffi(result.password);
|
||||
converted.hostname = component_result_from_ffi(result.hostname);
|
||||
converted.port = component_result_from_ffi(result.port);
|
||||
converted.pathname = component_result_from_ffi(result.pathname);
|
||||
converted.search = component_result_from_ffi(result.search);
|
||||
converted.hash = component_result_from_ffi(result.hash);
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
static String take_error(unsigned char const* error_ptr, size_t error_len)
|
||||
{
|
||||
auto error = MUST(String::from_utf8({ reinterpret_cast<char const*>(error_ptr), error_len }));
|
||||
FFI::rust_url_pattern_free_error(const_cast<unsigned char*>(error_ptr), error_len);
|
||||
return error;
|
||||
}
|
||||
|
||||
static FFI::FfiUrlHost host_to_ffi(Optional<Host> const& host)
|
||||
{
|
||||
FFI::FfiUrlHost result {};
|
||||
|
|
@ -113,6 +231,117 @@ static Optional<URL> url_from_ffi(FFI::RustFfiUrl const& ffi)
|
|||
return url;
|
||||
}
|
||||
|
||||
URLPattern::Impl::~Impl()
|
||||
{
|
||||
if (rust_url_pattern)
|
||||
rust_url_pattern_free(rust_url_pattern);
|
||||
}
|
||||
|
||||
struct ExecCallbackContext {
|
||||
Optional<URLPattern::Result>* result;
|
||||
};
|
||||
|
||||
static void on_exec_complete(void* ctx_ptr, FFI::RustUrlPatternExecResult const* ffi_result)
|
||||
{
|
||||
auto* ctx = static_cast<ExecCallbackContext*>(ctx_ptr);
|
||||
if (!ffi_result)
|
||||
return;
|
||||
*ctx->result = result_from_ffi(*ffi_result);
|
||||
}
|
||||
|
||||
URLPattern::ErrorOr<URLPattern> URLPattern::create(Input const& input, Optional<String> const& base_url, FFI::IgnoreCase ignore_case)
|
||||
{
|
||||
unsigned char const* error_ptr = nullptr;
|
||||
size_t error_len = 0;
|
||||
FFI::RustUrlPattern* rust_url_pattern = nullptr;
|
||||
|
||||
auto ffi_ignore_case = ignore_case == FFI::IgnoreCase::Yes ? FFI::IgnoreCase::Yes : FFI::IgnoreCase::No;
|
||||
auto base_url_slice = optional_string_to_ffi(base_url);
|
||||
if (auto const* input_string = input.get_pointer<String>()) {
|
||||
auto input_string_view = input_string->bytes_as_string_view();
|
||||
rust_url_pattern = rust_url_pattern_create_from_string(
|
||||
reinterpret_cast<u8 const*>(input_string_view.characters_without_null_termination()),
|
||||
input_string_view.length(),
|
||||
base_url.has_value() ? base_url_slice.data : nullptr,
|
||||
base_url_slice.length,
|
||||
ffi_ignore_case,
|
||||
&error_ptr,
|
||||
&error_len);
|
||||
} else {
|
||||
VERIFY(input.has<Init>());
|
||||
if (base_url.has_value())
|
||||
return ErrorInfo { "Constructor with URLPatternInit should provide no baseURL"_string };
|
||||
|
||||
auto ffi_init = init_to_ffi(input.get<Init>());
|
||||
rust_url_pattern = rust_url_pattern_create_from_init(&ffi_init, ffi_ignore_case, &error_ptr, &error_len);
|
||||
}
|
||||
|
||||
if (!rust_url_pattern) {
|
||||
auto error = error_ptr ? take_error(error_ptr, error_len) : "Failed to create URLPattern"_string;
|
||||
return ErrorInfo { move(error) };
|
||||
}
|
||||
|
||||
bool has_regexp_groups = rust_url_pattern_has_regexp_groups(rust_url_pattern);
|
||||
|
||||
auto impl = make<Impl>();
|
||||
impl->rust_url_pattern = rust_url_pattern;
|
||||
URLPattern pattern;
|
||||
pattern.m_impl = move(impl);
|
||||
pattern.m_has_regexp_groups = has_regexp_groups;
|
||||
pattern.m_protocol_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Protocol));
|
||||
pattern.m_username_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Username));
|
||||
pattern.m_password_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Password));
|
||||
pattern.m_hostname_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Hostname));
|
||||
pattern.m_port_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Port));
|
||||
pattern.m_pathname_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Pathname));
|
||||
pattern.m_search_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Search));
|
||||
pattern.m_hash_component.pattern_string = string_from_ffi(rust_url_pattern_component_pattern_string(rust_url_pattern, FFI::RustUrlPatternComponent::Hash));
|
||||
return pattern;
|
||||
}
|
||||
|
||||
URLPattern::ErrorOr<Optional<URLPattern::Result>> URLPattern::match(Input const& input, Optional<String> const& base_url_string) const
|
||||
{
|
||||
Optional<Result> result;
|
||||
ExecCallbackContext callback_context { .result = &result };
|
||||
unsigned char const* error_ptr = nullptr;
|
||||
size_t error_len = 0;
|
||||
|
||||
bool did_succeed = false;
|
||||
auto base_url_slice = optional_string_to_ffi(base_url_string);
|
||||
if (auto const* input_string = input.get_pointer<String>()) {
|
||||
auto input_string_view = input_string->bytes_as_string_view();
|
||||
did_succeed = rust_url_pattern_exec_string(
|
||||
m_impl->rust_url_pattern,
|
||||
reinterpret_cast<u8 const*>(input_string_view.characters_without_null_termination()),
|
||||
input_string_view.length(),
|
||||
base_url_string.has_value() ? base_url_slice.data : nullptr,
|
||||
base_url_slice.length,
|
||||
&error_ptr,
|
||||
&error_len,
|
||||
&callback_context,
|
||||
on_exec_complete);
|
||||
} else {
|
||||
VERIFY(input.has<Init>());
|
||||
if (base_url_string.has_value())
|
||||
return ErrorInfo { "Base URL cannot be provided when URLPatternInput is provided"_string };
|
||||
|
||||
auto ffi_init = init_to_ffi(input.get<Init>());
|
||||
did_succeed = rust_url_pattern_exec_init(m_impl->rust_url_pattern, &ffi_init, &error_ptr, &error_len, &callback_context, on_exec_complete);
|
||||
}
|
||||
|
||||
if (!did_succeed) {
|
||||
auto error = error_ptr ? take_error(error_ptr, error_len) : "Failed to execute URLPattern"_string;
|
||||
return ErrorInfo { move(error) };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool URLPattern::has_regexp_groups() const
|
||||
{
|
||||
return m_has_regexp_groups;
|
||||
}
|
||||
|
||||
struct UrlFfiStorage {
|
||||
Vector<FFI::RustUrlByteSlice> path_segments;
|
||||
FFI::RustFfiUrl ffi_url {};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,96 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibURL/Parser.h>
|
||||
#include <LibURL/RustFFI.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 = {});
|
||||
|
||||
class URLPattern {
|
||||
public:
|
||||
// NOTE: All exceptions which are thrown by the URLPattern spec are TypeErrors which web-based callers are expected to assume.
|
||||
// If this ever does not become the case, this should change to also include the error type.
|
||||
struct ErrorInfo {
|
||||
String message;
|
||||
};
|
||||
|
||||
template<typename ValueT>
|
||||
using ErrorOr = AK::ErrorOr<ValueT, ErrorInfo>;
|
||||
|
||||
struct Component {
|
||||
String pattern_string;
|
||||
struct Result {
|
||||
String input;
|
||||
OrderedHashMap<String, Variant<String, Empty>> groups;
|
||||
};
|
||||
};
|
||||
|
||||
struct Init {
|
||||
Optional<String> protocol;
|
||||
Optional<String> username;
|
||||
Optional<String> password;
|
||||
Optional<String> hostname;
|
||||
Optional<String> port;
|
||||
Optional<String> pathname;
|
||||
Optional<String> search;
|
||||
Optional<String> hash;
|
||||
Optional<String> base_url;
|
||||
};
|
||||
|
||||
using Input = Variant<String, Init>;
|
||||
|
||||
struct Result {
|
||||
Vector<Input> inputs;
|
||||
|
||||
Component::Result protocol;
|
||||
Component::Result username;
|
||||
Component::Result password;
|
||||
Component::Result hostname;
|
||||
Component::Result port;
|
||||
Component::Result pathname;
|
||||
Component::Result search;
|
||||
Component::Result hash;
|
||||
};
|
||||
|
||||
static ErrorOr<URLPattern> create(Input const&, Optional<String> const& base_url = {}, FFI::IgnoreCase = FFI::IgnoreCase::No);
|
||||
|
||||
ErrorOr<Optional<Result>> match(Input const&, Optional<String> const& base_url_string) const;
|
||||
|
||||
bool has_regexp_groups() const;
|
||||
|
||||
Component const& protocol_component() const { return m_protocol_component; }
|
||||
Component const& username_component() const { return m_username_component; }
|
||||
Component const& password_component() const { return m_password_component; }
|
||||
Component const& hostname_component() const { return m_hostname_component; }
|
||||
Component const& port_component() const { return m_port_component; }
|
||||
Component const& pathname_component() const { return m_pathname_component; }
|
||||
Component const& search_component() const { return m_search_component; }
|
||||
Component const& hash_component() const { return m_hash_component; }
|
||||
|
||||
private:
|
||||
struct Impl {
|
||||
FFI::RustUrlPattern* rust_url_pattern { nullptr };
|
||||
~Impl();
|
||||
};
|
||||
OwnPtr<Impl> m_impl;
|
||||
Component m_protocol_component;
|
||||
Component m_username_component;
|
||||
Component m_password_component;
|
||||
Component m_hostname_component;
|
||||
Component m_port_component;
|
||||
Component m_pathname_component;
|
||||
Component m_search_component;
|
||||
Component m_hash_component;
|
||||
bool m_has_regexp_groups { false };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ namespace Web::URLPattern {
|
|||
|
||||
GC_DEFINE_ALLOCATOR(URLPattern);
|
||||
|
||||
static URL::Pattern::Init to_internal_url_pattern_init(Bindings::URLPatternInit const& init)
|
||||
static URL::RustIntegration::URLPattern::Init to_internal_url_pattern_init(Bindings::URLPatternInit const& init)
|
||||
{
|
||||
return {
|
||||
.protocol = init.protocol,
|
||||
|
|
@ -28,7 +28,7 @@ static URL::Pattern::Init to_internal_url_pattern_init(Bindings::URLPatternInit
|
|||
};
|
||||
}
|
||||
|
||||
static Bindings::URLPatternInit to_bindings_url_pattern_init(URL::Pattern::Init const& init)
|
||||
static Bindings::URLPatternInit to_bindings_url_pattern_init(URL::RustIntegration::URLPattern::Init const& init)
|
||||
{
|
||||
return {
|
||||
.base_url = init.base_url,
|
||||
|
|
@ -43,14 +43,14 @@ static Bindings::URLPatternInit to_bindings_url_pattern_init(URL::Pattern::Init
|
|||
};
|
||||
}
|
||||
|
||||
static URL::Pattern::Input to_internal_url_pattern_input(URLPatternInput const& input)
|
||||
static URL::RustIntegration::URLPattern::Input to_internal_url_pattern_input(URLPatternInput const& input)
|
||||
{
|
||||
return input.visit(
|
||||
[](String const& input_string) -> URL::Pattern::Input { return input_string; },
|
||||
[](Bindings::URLPatternInit const& input_init) -> URL::Pattern::Input { return to_internal_url_pattern_init(input_init); });
|
||||
[](String const& input_string) -> URL::RustIntegration::URLPattern::Input { return input_string; },
|
||||
[](Bindings::URLPatternInit const& input_init) -> URL::RustIntegration::URLPattern::Input { return to_internal_url_pattern_init(input_init); });
|
||||
}
|
||||
|
||||
static Bindings::URLPatternComponentResult to_bindings_url_pattern_component_result(URL::Pattern::Component::Result const& result)
|
||||
static Bindings::URLPatternComponentResult to_bindings_url_pattern_component_result(URL::RustIntegration::URLPattern::Component::Result const& result)
|
||||
{
|
||||
OrderedHashMap<String, Variant<String, Empty, Empty>> groups;
|
||||
for (auto const& [key, value] : result.groups) {
|
||||
|
|
@ -63,14 +63,14 @@ static Bindings::URLPatternComponentResult to_bindings_url_pattern_component_res
|
|||
};
|
||||
}
|
||||
|
||||
static Bindings::URLPatternResult to_bindings_url_pattern_result(URL::Pattern::Result const& result)
|
||||
static Bindings::URLPatternResult to_bindings_url_pattern_result(URL::RustIntegration::URLPattern::Result const& result)
|
||||
{
|
||||
Vector<Variant<String, Bindings::URLPatternInit>> inputs;
|
||||
inputs.ensure_capacity(result.inputs.size());
|
||||
for (auto const& input : result.inputs) {
|
||||
inputs.unchecked_append(input.visit(
|
||||
[](String const& string_value) -> Variant<String, Bindings::URLPatternInit> { return string_value; },
|
||||
[](URL::Pattern::Init const& init_value) -> Variant<String, Bindings::URLPatternInit> { return to_bindings_url_pattern_init(init_value); }));
|
||||
[](URL::RustIntegration::URLPattern::Init const& init_value) -> Variant<String, Bindings::URLPatternInit> { return to_bindings_url_pattern_init(init_value); }));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -86,7 +86,7 @@ static Bindings::URLPatternResult to_bindings_url_pattern_result(URL::Pattern::R
|
|||
};
|
||||
}
|
||||
|
||||
URLPattern::URLPattern(JS::Realm& realm, URL::Pattern::Pattern pattern)
|
||||
URLPattern::URLPattern(JS::Realm& realm, URL::RustIntegration::URLPattern pattern)
|
||||
: PlatformObject(realm)
|
||||
, m_url_pattern(move(pattern))
|
||||
{
|
||||
|
|
@ -118,7 +118,7 @@ WebIDL::ExceptionOr<GC::Ref<URLPattern>> URLPattern::construct_impl(JS::Realm& r
|
|||
WebIDL::ExceptionOr<GC::Ref<URLPattern>> URLPattern::create(JS::Realm& realm, URLPatternInput const& input, Optional<String> const& base_url, URLPatternOptions const& options)
|
||||
{
|
||||
// 1. Set this’s associated URL pattern to the result of create given input, baseURL, and options.
|
||||
auto pattern_or_error = URL::Pattern::Pattern::create(to_internal_url_pattern_input(input), base_url, options.ignore_case ? URL::Pattern::IgnoreCase::Yes : URL::Pattern::IgnoreCase::No);
|
||||
auto pattern_or_error = URL::RustIntegration::URLPattern::create(to_internal_url_pattern_input(input), base_url, options.ignore_case ? URL::FFI::IgnoreCase::Yes : URL::FFI::IgnoreCase::No);
|
||||
if (pattern_or_error.is_error())
|
||||
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, pattern_or_error.error().message };
|
||||
return realm.create<URLPattern>(realm, pattern_or_error.release_value());
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <LibURL/Pattern/Init.h>
|
||||
#include <LibURL/Pattern/Pattern.h>
|
||||
#include <LibURL/RustIntegration.h>
|
||||
#include <LibWeb/Bindings/PlatformObject.h>
|
||||
#include <LibWeb/Bindings/URLPattern.h>
|
||||
|
||||
|
|
@ -48,12 +47,12 @@ public:
|
|||
protected:
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
|
||||
explicit URLPattern(JS::Realm&, URL::Pattern::Pattern);
|
||||
explicit URLPattern(JS::Realm&, URL::RustIntegration::URLPattern);
|
||||
|
||||
private:
|
||||
// https://urlpattern.spec.whatwg.org/#ref-for-url-pattern%E2%91%A0
|
||||
// Each URLPattern has an associated URL pattern, a URL pattern.
|
||||
URL::Pattern::Pattern m_url_pattern;
|
||||
URL::RustIntegration::URLPattern m_url_pattern;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
#include <LibTest/TestCase.h>
|
||||
|
||||
#include <LibURL/Pattern/Pattern.h>
|
||||
#include <LibURL/RustIntegration.h>
|
||||
|
||||
TEST_CASE(url_pattern_matches_named_groups)
|
||||
{
|
||||
auto pattern = MUST(URL::Pattern::Pattern::create("https://example.com/:category/:id"_string));
|
||||
auto pattern = MUST(URL::RustIntegration::URLPattern::create("https://example.com/:category/:id"_string));
|
||||
auto result = MUST(pattern.match("https://example.com/books/42"_string, {}));
|
||||
VERIFY(result.has_value());
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ TEST_CASE(url_pattern_matches_named_groups)
|
|||
|
||||
TEST_CASE(url_pattern_ignore_case_matching)
|
||||
{
|
||||
auto pattern = MUST(URL::Pattern::Pattern::create("https://example.com/:value"_string, {}, URL::Pattern::IgnoreCase::Yes));
|
||||
auto pattern = MUST(URL::RustIntegration::URLPattern::create("https://example.com/:value"_string, {}, URL::FFI::IgnoreCase::Yes));
|
||||
auto result = MUST(pattern.match("https://example.com/CaseSensitive"_string, {}));
|
||||
VERIFY(result.has_value());
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue