feat(webrtc): implement RTCIceCandidate construction and serialization

This commit is contained in:
ldm0
2026-09-08 16:38:12 +08:00
parent 4c00e6dab6
commit b31beb2e7f
10 changed files with 641 additions and 1 deletions
@@ -55,7 +55,7 @@ use super::super::{
build_audio_context_constructor_template, build_audio_worklet_node_constructor_template,
offline_audio_context_constructor_callback,
},
webrtc::rtc_peer_connection_constructor_callback,
webrtc::{rtc_ice_candidate_constructor_callback, rtc_peer_connection_constructor_callback},
websocket::{
websocket_constructor_callback, websocket_error_constructor_callback,
websocket_stream_constructor_callback,
@@ -424,6 +424,11 @@ pub(in crate::context_bootstrap) fn build_constructor_template<'s>(
.length(0)
.build(scope)
}
ConstructorKind::RtcIceCandidate => {
v8::FunctionTemplate::builder(rtc_ice_candidate_constructor_callback)
.length(0)
.build(scope)
}
ConstructorKind::Navigator
| ConstructorKind::WorkerNavigator
| ConstructorKind::Permissions
@@ -1826,6 +1826,7 @@ pub(crate) fn finish_context_bootstrap(
("WebSocketError", "WebSocketError"),
("WebSocketStream", "WebSocketStream"),
("RTCPeerConnection", "RTCPeerConnection"),
("RTCIceCandidate", "RTCIceCandidate"),
("RTCRtpReceiver", "RTCRtpReceiver"),
("RTCDataChannel", "RTCDataChannel"),
("Blob", "Blob"),
@@ -1026,6 +1026,11 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
parent: Some("EventTarget"),
kind: ConstructorKind::RtcPeerConnection,
},
ConstructorSpec {
name: "RTCIceCandidate",
parent: None,
kind: ConstructorKind::RtcIceCandidate,
},
ConstructorSpec {
name: "RTCRtpReceiver",
parent: None,
@@ -84,6 +84,7 @@ pub(in crate::context_bootstrap) enum ConstructorKind {
SharedWorker,
WebSocket,
RtcPeerConnection,
RtcIceCandidate,
Navigator,
WorkerNavigator,
Permissions,
@@ -4,6 +4,10 @@ use crate::util::{
};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
mod ice_candidate;
mod ice_candidate_parser;
pub(in crate::context_bootstrap) use ice_candidate::rtc_ice_candidate_constructor_callback;
const RTC_PEER_CONNECTION_BRAND_SLOT: &str = "__moliRtcPeerConnectionBrand";
const RTC_PEER_CONNECTION_CONFIGURATION_SLOT: &str = "__moliRtcPeerConnectionConfiguration";
const RTC_PEER_CONNECTION_SIGNALING_STATE_SLOT: &str = "__moliRtcPeerConnectionSignalingState";
@@ -202,6 +206,9 @@ pub(in crate::context_bootstrap) fn install_webrtc_template_bindings<'s>(
) {
let prototype = template.prototype_template(scope);
match interface_name {
"RTCIceCandidate" => {
ice_candidate::install_ice_candidate_template_bindings(scope, template)
}
"RTCPeerConnection" => {
RtcPeerConnectionPrototypeDeclaration::initialize_prototype_template(scope, prototype);
}
@@ -0,0 +1,256 @@
use super::ice_candidate_parser::parse_ice_candidate;
use crate::{
util::{
apply_webidl_constructor_prototype_fallback, callback_data_index_value, get_private_value,
throw_type_error, v8str,
},
webidl::{self, WebIdlConverter},
};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
const CANDIDATE_VALUES_SLOT: &str = "__moliRtcIceCandidateValues";
#[derive(WebApiObject)]
#[webapi(interface = "RTCIceCandidate")]
struct IceCandidateObjectDeclaration<'scope> {
#[webapi(slot = CANDIDATE_VALUES_SLOT)]
values: v8::Local<'scope, v8::Array>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(name = "RTCIceCandidate", enumerable)]
struct IceCandidatePrototypeDeclaration {
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 0))]
candidate: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 1))]
sdp_mid: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 2))]
sdp_m_line_index: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 3))]
foundation: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 4))]
component: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 5))]
priority: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 6))]
address: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 7))]
protocol: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 8))]
port: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 9))]
r#type: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 10))]
tcp_type: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 11))]
related_address: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 12))]
related_port: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 13))]
username_fragment: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 14))]
relay_protocol: (),
#[webapi(accessor_property, getter = candidate_attribute_getter, data = callback_data_index_value(scope, 15))]
url: (),
#[webapi(method = "toJSON", length = 0, callback = candidate_to_json_callback)]
to_json: (),
}
#[derive(WebApiObject)]
#[webapi(interface = "Object", data_properties, enumerable)]
struct IceCandidateJsonDeclaration<'scope> {
candidate: v8::Local<'scope, v8::Value>,
sdp_mid: v8::Local<'scope, v8::Value>,
sdp_m_line_index: v8::Local<'scope, v8::Value>,
username_fragment: v8::Local<'scope, v8::Value>,
}
pub(super) fn install_ice_candidate_template_bindings<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
IceCandidatePrototypeDeclaration::initialize_prototype_template(
scope,
template.prototype_template(scope),
);
}
pub(in crate::context_bootstrap) fn rtc_ice_candidate_constructor_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if !args.is_construct_call() {
throw_type_error(scope, "RTCIceCandidate constructor requires 'new'.");
return;
}
let dictionary = match webidl::dictionary_value(
args.get(0),
webidl::Context::argument("RTCIceCandidate", 1),
) {
Ok(dictionary) => dictionary,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let Some(mut values) = parse_candidate_init(scope, dictionary) else {
return;
};
if values[1].is_null() && values[2].is_null() {
throw_type_error(scope, "RTCIceCandidate requires sdpMid or sdpMLineIndex.");
return;
}
let candidate =
v8::Local::<v8::String>::try_from(values[0]).expect("converted candidate string");
let candidate = candidate.to_rust_string_lossy(scope);
if let Some(parsed) = parse_ice_candidate(&candidate) {
values[3] = v8::String::new(scope, parsed.foundation).unwrap().into();
values[4] = v8str(scope, parsed.component).into();
values[5] = v8::Integer::new_from_unsigned(scope, parsed.priority).into();
values[6] = v8::String::new(scope, parsed.address).unwrap().into();
values[7] = v8str(scope, parsed.protocol).into();
values[8] = v8::Integer::new_from_unsigned(scope, parsed.port.into()).into();
values[9] = v8str(scope, parsed.kind).into();
if let Some(tcp_type) = parsed.tcp_type {
values[10] = v8str(scope, tcp_type).into();
}
if let Some(address) = parsed.related_address {
values[11] = v8::String::new(scope, address).unwrap().into();
}
if let Some(port) = parsed.related_port {
values[12] = v8::Integer::new_from_unsigned(scope, port.into()).into();
}
}
let values = v8::Array::new_with_elements(scope, &values);
if IceCandidateObjectDeclaration::new(values)
.initialize(scope, args.this())
.is_err()
{
return;
}
apply_webidl_constructor_prototype_fallback(
scope,
args.this(),
args.new_target(),
"RTCIceCandidate",
);
rv.set(args.this().into());
}
fn parse_candidate_init<'s>(
scope: &mut v8::PinScope<'s, '_>,
dictionary: Option<v8::Local<'s, v8::Object>>,
) -> Option<[v8::Local<'s, v8::Value>; 16]> {
let mut values = [v8::null(scope).into(); 16];
values[0] = v8str(scope, "").into();
let Some(dictionary) = dictionary else {
return Some(values);
};
// Convert base dictionary members in lexical order before derived members.
// Keep DOMStrings in V8, preserving lone UTF-16 surrogates; only `url` is a USVString.
for (name, index) in [
("candidate", 0),
("sdpMLineIndex", 2),
("sdpMid", 1),
("usernameFragment", 13),
("relayProtocol", 14),
("url", 15),
] {
let raw = dictionary.get(scope, v8str(scope, name).into())?;
if raw.is_undefined() || (index != 0 && raw.is_null()) {
continue;
}
let context = webidl::Context::member("RTCLocalIceCandidateInit", name);
if index == 2 {
let value = match webidl::UnsignedShort::convert(scope, raw, context, &()) {
Ok(value) => value.0,
Err(error) => {
webidl::throw_error(scope, &error);
return None;
}
};
values[index] = v8::Integer::new_from_unsigned(scope, value.into()).into();
} else if index == 15 {
let value = match webidl::UsvString::convert(scope, raw, context, &Default::default()) {
Ok(value) => value.0,
Err(error) => {
webidl::throw_error(scope, &error);
return None;
}
};
values[index] = v8::String::new(scope, &value)?.into();
} else {
if raw.is_symbol() {
throw_type_error(scope, "Cannot convert a Symbol to a DOMString.");
return None;
}
let value = raw.to_string(scope)?;
if index == 14
&& !matches!(
value.to_rust_string_lossy(scope).as_str(),
"udp" | "tcp" | "tls"
)
{
throw_type_error(scope, "Invalid RTCIceServerTransportProtocol.");
return None;
}
values[index] = value.into();
}
}
Some(values)
}
fn candidate_values<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Array>> {
let values = get_private_value(scope, receiver, CANDIDATE_VALUES_SLOT)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok());
if values.is_none() {
throw_type_error(scope, "Illegal invocation");
}
values
}
fn candidate_attribute_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(values) = candidate_values(scope, args.this()) else {
return;
};
let Some(index) = args.data().uint32_value(scope) else {
return;
};
if let Some(value) = values.get_index(scope, index) {
rv.set(value);
}
}
fn candidate_to_json_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(values) = candidate_values(scope, args.this()) else {
return;
};
let Some(candidate) = values.get_index(scope, 0) else {
return;
};
let Some(mid) = values.get_index(scope, 1) else {
return;
};
let Some(line) = values.get_index(scope, 2) else {
return;
};
let Some(fragment) = values.get_index(scope, 13) else {
return;
};
if let Ok(result) = IceCandidateJsonDeclaration::new(candidate, mid, line, fragment).bind(scope)
{
rv.set(result.into());
}
}
@@ -0,0 +1,181 @@
use std::net::IpAddr;
#[derive(Debug, PartialEq)]
pub(super) struct ParsedIceCandidate<'a> {
pub(super) foundation: &'a str,
pub(super) component: &'static str,
pub(super) priority: u32,
pub(super) address: &'a str,
pub(super) protocol: &'static str,
pub(super) port: u16,
pub(super) kind: &'static str,
pub(super) tcp_type: Option<&'static str>,
pub(super) related_address: Option<&'a str>,
pub(super) related_port: Option<u16>,
}
// RFC 5245 section 15.1 and the TCP extension in RFC 6544 section 4.5.
// Return no derived fields if parsing or an attribute's value is invalid;
// RTCIceCandidate itself still retains the original candidate string.
pub(super) fn parse_ice_candidate(input: &str) -> Option<ParsedIceCandidate<'_>> {
let fields: Vec<_> = input.strip_prefix("candidate:")?.split(' ').collect();
if fields.len() < 8 || fields.iter().any(|field| field.is_empty()) {
return None;
}
let foundation = fields[0];
if foundation.len() > 32
|| !foundation
.bytes()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, b'+' | b'/'))
{
return None;
}
let component = match decimal(fields[1], 5)? {
1 => "rtp",
2 => "rtcp",
_ => return None,
};
let protocol = if fields[2].eq_ignore_ascii_case("udp") {
"udp"
} else if fields[2].eq_ignore_ascii_case("tcp") {
"tcp"
} else {
return None;
};
let priority = decimal(fields[3], 10)?;
if !(1..=i32::MAX as u32).contains(&priority) || !address_is_valid(fields[4]) {
return None;
}
let port = u16::try_from(decimal(fields[5], usize::MAX)?).ok()?;
if !fields[6].eq_ignore_ascii_case("typ") {
return None;
}
let kind = ["host", "srflx", "prflx", "relay"]
.into_iter()
.find(|kind| fields[7].eq_ignore_ascii_case(kind))?;
let mut candidate = ParsedIceCandidate {
foundation,
component,
priority,
address: fields[4],
protocol,
port,
kind,
tcp_type: None,
related_address: None,
related_port: None,
};
let extensions = fields[8..].chunks_exact(2);
if !extensions.remainder().is_empty() {
return None;
}
for pair in extensions {
if pair
.iter()
.any(|field| field.bytes().any(|ch| ch.is_ascii_control()))
{
return None;
}
if pair[0].eq_ignore_ascii_case("raddr") {
if !address_is_valid(pair[1]) || candidate.related_address.replace(pair[1]).is_some() {
return None;
}
} else if pair[0].eq_ignore_ascii_case("rport") {
let port = u16::try_from(decimal(pair[1], usize::MAX)?).ok()?;
if candidate.related_port.replace(port).is_some() {
return None;
}
} else if pair[0].eq_ignore_ascii_case("tcptype") && protocol == "tcp" {
let tcp_type = ["active", "passive", "so"]
.into_iter()
.find(|kind| pair[1].eq_ignore_ascii_case(kind))?;
if candidate.tcp_type.replace(tcp_type).is_some() {
return None;
}
}
}
if protocol == "tcp" && candidate.tcp_type.is_none() {
return None;
}
Some(candidate)
}
fn decimal(input: &str, max_digits: usize) -> Option<u32> {
if input.is_empty() || input.len() > max_digits || !input.bytes().all(|ch| ch.is_ascii_digit())
{
return None;
}
input.parse().ok()
}
fn address_is_valid(input: &str) -> bool {
if input.parse::<IpAddr>().is_ok() {
return true;
}
let hostname = input.strip_suffix('.').unwrap_or(input);
!hostname.is_empty()
&& hostname.len() <= 253
&& hostname.split('.').all(|label| {
!label.is_empty()
&& label.len() <= 63
&& label.as_bytes()[0].is_ascii_alphanumeric()
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
&& label
.bytes()
.all(|ch| ch.is_ascii_alphanumeric() || ch == b'-')
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ice_candidate_parser_handles_udp_tcp_ipv6_and_mdns() {
let udp = parse_ice_candidate(
"candidate:ab+/12 1 UDP 2113937151 2001:db8::1 65535 typ host generation 0 ufrag test",
)
.unwrap();
assert_eq!(udp.foundation, "ab+/12");
assert_eq!(udp.component, "rtp");
assert_eq!(udp.protocol, "udp");
assert_eq!(udp.priority, 2113937151);
assert_eq!(udp.address, "2001:db8::1");
assert_eq!(udp.port, 65535);
assert_eq!(udp.tcp_type, None);
let tcp = parse_ice_candidate("candidate:123 2 tcp 123456 foo.local 9 typ srflx raddr www.example.com rport 22222 tcptype active").unwrap();
assert_eq!(tcp.component, "rtcp");
assert_eq!(tcp.tcp_type, Some("active"));
assert_eq!(tcp.related_address, Some("www.example.com"));
assert_eq!(tcp.related_port, Some(22222));
}
#[test]
fn ice_candidate_parser_rejects_invalid_fields_without_partial_results() {
for invalid in [
"",
"arbitrary candidate",
"candidate:",
"candidate:a 1 udp 0 127.0.0.1 9 typ host",
"candidate:a 1 udp 2147483648 127.0.0.1 9 typ host",
"candidate:a 1 udp 1 127.0.0.1 65536 typ host",
"candidate:a 1 udp 1 127.0.0.1 -1 typ host",
"candidate:a 3 udp 1 127.0.0.1 9 typ host",
"candidate:a 1 sctp 1 127.0.0.1 9 typ host",
"candidate:a 1 udp 1 127.0.0.1 9 typ unknown",
"candidate:a 1 udp 1 [::1] 9 typ host",
"candidate:a 1 udp 1 -invalid.local 9 typ host",
"candidate:a 1 tcp 1 127.0.0.1 9 typ host",
"candidate:a 1 tcp 1 127.0.0.1 9 typ host tcptype unknown",
"candidate:a 1 udp 1 127.0.0.1 9 typ host rport 65536",
"candidate:a 1 udp 1 127.0.0.1 9 typ host generation",
"candidate:a 1 udp 1 127.0.0.1 9 typ host\r\n",
"candidate:a 1 udp 1 127.0.0.1 9 typ host x y\n",
] {
assert!(
parse_ice_candidate(invalid).is_none(),
"accepted {invalid:?}"
);
}
}
}
@@ -0,0 +1,131 @@
use super::*;
#[test]
fn rtc_ice_candidate_has_branded_readonly_attributes_and_four_json_members() {
let mut vm = new_storage_test_vm("https://ice-candidate.test/");
let result = vm.eval(r#"
(() => {
const candidate = new RTCIceCandidate({sdpMid: 'audio', relayProtocol: 'tls', url: 'turn:example.com'});
const names = ['candidate', 'sdpMid', 'sdpMLineIndex', 'foundation', 'component',
'priority', 'address', 'protocol', 'port', 'type', 'tcpType', 'relatedAddress',
'relatedPort', 'usernameFragment', 'relayProtocol', 'url'];
const rejects = callback => {
try { callback(); return false; } catch (error) { return error instanceof TypeError; }
};
const checks = [
RTCIceCandidate.length === 0, candidate instanceof RTCIceCandidate,
Object.prototype.toString.call(candidate) === '[object RTCIceCandidate]',
Object.getOwnPropertyNames(candidate).length === 0,
candidate.toJSON.length === 0,
rejects(() => RTCIceCandidate({sdpMid: 'audio'})),
rejects(() => new RTCIceCandidate()), rejects(() => new RTCIceCandidate(null)),
rejects(() => new RTCIceCandidate({})), rejects(() => new RTCIceCandidate(true)),
rejects(() => candidate.toJSON.call(Object.create(candidate)))
];
for (const name of names) {
const descriptor = Object.getOwnPropertyDescriptor(RTCIceCandidate.prototype, name);
if (!descriptor) return 'missing:' + name;
checks.push(descriptor.get.name === 'get ' + name, descriptor.get.length === 0,
descriptor.set === undefined, descriptor.enumerable, descriptor.configurable);
const value = candidate[name];
candidate[name] = 'changed';
checks.push(candidate[name] === value, rejects(() => { 'use strict'; candidate[name] = 'changed'; }));
for (const fake of [{}, RTCIceCandidate.prototype, Object.create(candidate)]) {
checks.push(rejects(() => descriptor.get.call(fake)));
}
}
const first = candidate.toJSON();
const second = candidate.toJSON();
checks.push(first !== second, JSON.stringify(first) ===
'{"candidate":"","sdpMid":"audio","sdpMLineIndex":null,"usernameFragment":null}');
const cloned = new RTCIceCandidate(candidate);
const signaled = new RTCIceCandidate(first);
checks.push(cloned.relayProtocol === 'tls', cloned.url === 'turn:example.com',
signaled.relayProtocol === null, signaled.url === null);
return checks.every(Boolean);
})()
"#).expect("RTCIceCandidate should expose the WebIDL surface without mutable own attributes");
assert_eq!(result, "true");
}
#[test]
fn rtc_ice_candidate_converts_dictionary_members_in_order_and_preserves_dom_strings() {
let mut vm = new_storage_test_vm("https://ice-candidate-conversion.test/");
let result = vm.eval(r#"
(() => {
const reads = [];
const raw = {
candidate: '\ud800', sdpMid: '\udc00', sdpMLineIndex: -1.9,
usernameFragment: '\ud800', relayProtocol: 'udp', url: 'turn:\ud800'
};
const candidate = new RTCIceCandidate(new Proxy(raw, {
get(target, name) { reads.push(name); return target[name]; }
}));
const checks = [
reads.join(',') === 'candidate,sdpMLineIndex,sdpMid,usernameFragment,relayProtocol,url',
candidate.candidate === raw.candidate, candidate.sdpMid === raw.sdpMid,
candidate.usernameFragment === raw.usernameFragment,
candidate.sdpMLineIndex === 65535, candidate.url === 'turn:\ufffd',
new RTCIceCandidate({sdpMLineIndex: 65536}).sdpMLineIndex === 0,
new RTCIceCandidate({sdpMLineIndex: Infinity}).sdpMLineIndex === 0,
new RTCIceCandidate({candidate: null, sdpMid: false}).candidate === 'null',
new RTCIceCandidate({sdpMid: false}).sdpMid === 'false'
];
for (const bad of [{candidate: Symbol()}, {sdpMid: Symbol()}, {sdpMLineIndex: 1n},
{sdpMLineIndex: Symbol()}, {usernameFragment: Symbol()}, {relayProtocol: 'UDP'}, {url: Symbol()}]) {
try { new RTCIceCandidate({sdpMid: 'audio', ...bad}); checks.push(false); }
catch (error) { checks.push(error instanceof TypeError); }
}
const error = new RangeError('conversion');
try { new RTCIceCandidate({candidate: {toString() { throw error; }}, sdpMid: 'audio'}); checks.push(false); }
catch (caught) { checks.push(caught === error); }
return checks.every(Boolean);
})()
"#).expect("ICE dictionary conversions should preserve WebIDL ordering and original exceptions");
assert_eq!(result, "true");
}
#[test]
fn rtc_ice_candidate_invalid_strings_preserve_raw_input_without_partial_parsing() {
let mut vm = new_storage_test_vm("https://ice-candidate-parsing.test/");
let result = vm.eval(r#"
(() => {
const derived = ['foundation', 'component', 'priority', 'address', 'protocol',
'port', 'type', 'tcpType', 'relatedAddress', 'relatedPort'];
return ['', 'arbitrary string', 'candidate:x 1 udp 1 127.0.0.1 65536 typ host',
'candidate:x 1 udp 1 127.0.0.1 9 typ host rport 65536'].every(raw => {
const candidate = new RTCIceCandidate({candidate: raw, sdpMid: 'video', usernameFragment: 'keep'});
return candidate.candidate === raw && candidate.usernameFragment === 'keep' &&
derived.every(name => candidate[name] === null);
});
})()
"#).expect("invalid candidate text should not throw or expose partial derived attributes");
assert_eq!(result, "true");
}
#[test]
fn rtc_ice_candidate_respects_new_target_and_accepts_genuine_foreign_receivers() {
let mut vm = new_parsed_test_vm(
"https://ice-candidate-realms.test/",
"<!doctype html><iframe></iframe>",
);
let result = vm.eval(r#"
(() => {
const child = document.querySelector('iframe').contentWindow;
class Derived extends RTCIceCandidate {}
const derived = new Derived({sdpMid: 'audio'});
const foreign = new child.RTCIceCandidate({sdpMid: 'foreign'});
const newTarget = child.Function('');
newTarget.prototype = 1;
const fallback = Reflect.construct(RTCIceCandidate, [{sdpMid: 'fallback'}], newTarget);
return [
derived instanceof Derived, derived instanceof RTCIceCandidate,
Object.getPrototypeOf(foreign) === child.RTCIceCandidate.prototype,
Object.getPrototypeOf(fallback) === child.RTCIceCandidate.prototype,
Object.getOwnPropertyDescriptor(RTCIceCandidate.prototype, 'sdpMid').get.call(foreign) === 'foreign',
RTCIceCandidate.prototype.toJSON.call(foreign).sdpMid === 'foreign'
].every(Boolean);
})()
"#).expect("ICE constructors should retain subclass and relevant NewTarget prototypes");
assert_eq!(result, "true");
}
@@ -13,6 +13,7 @@ mod crypto_subtle_x25519;
mod details;
mod event_handlers;
mod events_selection_storage;
mod ice_candidate;
mod idle_callbacks;
mod idle_detection;
mod images;
+52
View File
@@ -116,6 +116,58 @@ pub(crate) fn callable_relevant_context<'s>(
.get_creation_context(scope)
}
pub(crate) fn new_target_realm_constructor_prototype<'s>(
scope: &mut v8::PinScope<'s, '_>,
new_target: v8::Local<'s, v8::Value>,
constructor_name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let context = callable_relevant_context(scope, new_target)?;
let prototype = {
let context_scope = &mut v8::ContextScope::new(scope, context);
let prototype = global_constructor_prototype(context_scope, constructor_name)?;
v8::Global::new(context_scope, prototype)
};
Some(v8::Local::new(scope, &prototype))
}
pub(crate) fn receiver_uses_new_target_realm_object_fallback<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
new_target: v8::Local<'s, v8::Value>,
) -> bool {
let Some(receiver_prototype) = receiver.get_prototype(scope) else {
return false;
};
let Some(object_prototype) =
new_target_realm_constructor_prototype(scope, new_target, "Object")
else {
return false;
};
receiver_prototype.strict_equals(object_prototype.into())
}
pub(crate) fn apply_webidl_constructor_prototype_fallback<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
new_target: v8::Local<'s, v8::Value>,
default_constructor_name: &str,
) {
// V8 has already read `NewTarget.prototype` while preparing the native
// constructor receiver. Do not read it again: a Proxy getter must run only
// once. V8 represents a non-object result with NewTarget's realm-local
// Object prototype, which lets us replace precisely that fallback with
// the WebIDL interface's default prototype from the same realm.
if !receiver_uses_new_target_realm_object_fallback(scope, receiver, new_target) {
return;
}
let Some(prototype) =
new_target_realm_constructor_prototype(scope, new_target, default_constructor_name)
else {
return;
};
let _ = receiver.set_prototype(scope, prototype.into());
}
pub(crate) fn define_v8_array_data_properties<'s, I, T>(
scope: &mut v8::PinScope<'s, '_>,
array: v8::Local<'s, v8::Array>,