From 62bbb468be7e79fd4f2aeb2d66ab6a5cf43fd66b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 7 Oct 2026 09:40:19 +0800 Subject: [PATCH] fix(url): return colon protocol for invalid hyperlinks --- .../element/resource_elements.rs | 29 +++++++++++++++---- .../tests/hyperlink_null_url_protocol.rs | 27 +++++++++++++++++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/hyperlink_null_url_protocol.rs diff --git a/moli-renderer-v8/src/native_bridge/element/resource_elements.rs b/moli-renderer-v8/src/native_bridge/element/resource_elements.rs index 9d80ccebbb..113f216493 100644 --- a/moli-renderer-v8/src/native_bridge/element/resource_elements.rs +++ b/moli-renderer-v8/src/native_bridge/element/resource_elements.rs @@ -3,6 +3,7 @@ use super::*; fn anchor_url_string_function_getter<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, + parse_failure_value: &'static str, project: impl FnOnce(&url::Url) -> String, mut rv: v8::ReturnValue<'_, v8::Value>, ) { @@ -14,7 +15,7 @@ fn anchor_url_string_function_getter<'s>( }; let value = parsed_url_like_attribute(unsafe { &*runtime_ptr }, handle, "href") .map(|url| project(&url)) - .unwrap_or_default(); + .unwrap_or_else(|| parse_failure_value.to_owned()); if let Some(value) = v8_string(scope, &value) { rv.set(value.into()); } else { @@ -280,6 +281,7 @@ fn anchor_host_getter_function<'s>( anchor_url_string_function_getter( scope, args.this(), + "", |url| { url.host_str() .map(|host| { @@ -336,6 +338,7 @@ fn anchor_hostname_getter_function<'s>( anchor_url_string_function_getter( scope, args.this(), + "", |url| url.host_str().unwrap_or_default().to_owned(), rv, ); @@ -368,6 +371,7 @@ fn anchor_port_getter_function<'s>( anchor_url_string_function_getter( scope, args.this(), + "", |url| url.port().map(|port| port.to_string()).unwrap_or_default(), rv, ); @@ -397,7 +401,7 @@ fn anchor_pathname_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - anchor_url_string_function_getter(scope, args.this(), |url| url.path().to_owned(), rv); + anchor_url_string_function_getter(scope, args.this(), "", |url| url.path().to_owned(), rv); } fn anchor_pathname_setter_function<'s>( @@ -426,6 +430,7 @@ fn anchor_search_getter_function<'s>( anchor_url_string_function_getter( scope, args.this(), + "", |url| url::quirks::search(url).to_owned(), rv, ); @@ -454,6 +459,7 @@ fn anchor_hash_getter_function<'s>( anchor_url_string_function_getter( scope, args.this(), + "", |url| url::quirks::hash(url).to_owned(), rv, ); @@ -479,7 +485,13 @@ fn anchor_origin_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - anchor_url_string_function_getter(scope, args.this(), moli_url::origin_ascii_serialization, rv); + anchor_url_string_function_getter( + scope, + args.this(), + "", + moli_url::origin_ascii_serialization, + rv, + ); } fn anchor_protocol_getter_function<'s>( @@ -487,7 +499,13 @@ fn anchor_protocol_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - anchor_url_string_function_getter(scope, args.this(), |url| format!("{}:", url.scheme()), rv); + anchor_url_string_function_getter( + scope, + args.this(), + ":", + |url| format!("{}:", url.scheme()), + rv, + ); } fn anchor_protocol_setter_function<'s>( @@ -511,7 +529,7 @@ fn anchor_username_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { - anchor_url_string_function_getter(scope, args.this(), |url| url.username().to_owned(), rv); + anchor_url_string_function_getter(scope, args.this(), "", |url| url.username().to_owned(), rv); } fn anchor_username_setter_function<'s>( @@ -540,6 +558,7 @@ fn anchor_password_getter_function<'s>( anchor_url_string_function_getter( scope, args.this(), + "", |url| url.password().unwrap_or("").to_owned(), rv, ); diff --git a/moli-renderer-v8/src/script_vm/tests/hyperlink_null_url_protocol.rs b/moli-renderer-v8/src/script_vm/tests/hyperlink_null_url_protocol.rs new file mode 100644 index 0000000000..7bffa61f12 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/hyperlink_null_url_protocol.rs @@ -0,0 +1,27 @@ +use super::*; + +#[test] +fn hyperlink_protocol_preserves_null_url_fallbacks_across_documents() { + let mut vm = new_parsed_test_vm( + "https://hyperlink-null-url-protocol.test/", + "", + ); + assert_eq!(vm.eval(r#"(() => { + for(const owner of [document,document.implementation.createHTMLDocument('')]) { + const base=owner.createElement('base');base.href='about:blank';owner.head.append(base); + for(const tag of ['a','area']) { + const link=owner.createElement(tag); + if(link.protocol!==':')throw Error('missing href '+tag); + owner.body.append(link); + for(const href of ['', 'http://[', 'javascript://:443', 'javascript://test:test', 'javascript://[:1]', 'mailto://:443', 'mailto://test:test', 'mailto://[:1]']) { + link.setAttribute('href',href); + if(link.protocol!==':'||link.href!==href||link.getAttribute('href')!==href)throw Error('invalid href '+tag+':'+href); + for(const name of ['host','hostname','port','pathname','search','hash','username','password','origin'])if(link[name]!=='')throw Error('fallback '+name); + } + link.href='https://valid.test:8443/path?q#fragment'; + if(link.protocol!=='https:'||link.host!=='valid.test:8443'||link.pathname!=='/path')throw Error('valid href'); + } + } + return 'ok'; +})()"#).expect("native regression"), "ok"); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index f8da8a20de..b0640ff230 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2151,6 +2151,7 @@ mod media_recorder_shell; mod svg_animation_interfaces; mod audio_event_interfaces; +mod hyperlink_null_url_protocol; mod import_meta; mod indexed_db; mod inspector_unwrap;