mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
refactor(webidl): derive dictionary snapshots for RTC configuration
This commit is contained in:
@@ -6,10 +6,11 @@ use super::{
|
||||
RTC_PEER_CONNECTION_SIGNALING_STATE_SLOT,
|
||||
};
|
||||
use crate::{
|
||||
util::{get_private_value, new_null_prototype_object, set_private_value, v8str},
|
||||
util::{get_private_value, set_private_value, v8str},
|
||||
web_api_interfaces, webidl,
|
||||
};
|
||||
use moli_webapi_declare::{WebApiObject, WebApiValue};
|
||||
use webidl::{SnapshotKind, SnapshotPrototype, WebIdlDictionarySnapshot, WebIdlSnapshotValue};
|
||||
|
||||
#[derive(Clone, Copy, webidl::WebIdlEnum)]
|
||||
#[webidl(name = "RTCBundlePolicy")]
|
||||
@@ -67,6 +68,10 @@ impl<'s> WebApiValue<'s> for RtcpMuxPolicy {
|
||||
|
||||
struct PoolSize(u8);
|
||||
|
||||
impl WebIdlSnapshotValue for PoolSize {
|
||||
const SNAPSHOT_KIND: SnapshotKind = SnapshotKind::Identity;
|
||||
}
|
||||
|
||||
impl<'s> webidl::WebIdlConverter<'s> for PoolSize {
|
||||
type Options = ();
|
||||
|
||||
@@ -91,6 +96,10 @@ impl<'s> WebApiValue<'s> for PoolSize {
|
||||
|
||||
struct Text(webidl::DomString16);
|
||||
|
||||
impl WebIdlSnapshotValue for Text {
|
||||
const SNAPSHOT_KIND: SnapshotKind = SnapshotKind::Identity;
|
||||
}
|
||||
|
||||
impl<'s> webidl::WebIdlConverter<'s> for Text {
|
||||
type Options = ();
|
||||
|
||||
@@ -112,6 +121,11 @@ impl<'s> WebApiValue<'s> for Text {
|
||||
|
||||
struct IceUrls(Vec<String>);
|
||||
|
||||
impl WebIdlSnapshotValue for IceUrls {
|
||||
// The union converter always binds its canonical value as a URL sequence.
|
||||
const SNAPSHOT_KIND: SnapshotKind = SnapshotKind::Sequence(&SnapshotKind::Identity);
|
||||
}
|
||||
|
||||
impl<'s> webidl::WebIdlConverter<'s> for IceUrls {
|
||||
type Options = ();
|
||||
|
||||
@@ -142,41 +156,32 @@ impl<'s> WebApiValue<'s> for IceUrls {
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary, WebApiObject)]
|
||||
#[webidl(prefix = "RTCIceServer")]
|
||||
#[webapi(plain, enumerable)]
|
||||
#[webidl(prefix = "RTCIceServer", snapshot)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct IceServer {
|
||||
#[webidl(converter = "raw")]
|
||||
#[webapi(data_property)]
|
||||
credential: Option<Text>,
|
||||
#[webidl(required, converter = "raw")]
|
||||
#[webapi(data_property)]
|
||||
urls: IceUrls,
|
||||
#[webidl(converter = "raw")]
|
||||
#[webapi(data_property)]
|
||||
username: Option<Text>,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary, WebApiObject)]
|
||||
#[webidl(prefix = "RTCConfiguration")]
|
||||
#[webapi(plain, enumerable)]
|
||||
#[webidl(prefix = "RTCConfiguration", snapshot)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct Configuration<'s> {
|
||||
#[webidl(converter = "enum", default = BundlePolicy::Balanced)]
|
||||
#[webapi(data_property)]
|
||||
bundle_policy: BundlePolicy,
|
||||
#[webidl(sequence, interface = web_api_interfaces::RTCCertificate, default = Vec::new())]
|
||||
#[webapi(data_property)]
|
||||
certificates: Vec<v8::Local<'s, v8::Object>>,
|
||||
#[webidl(converter = "raw", default = PoolSize(0))]
|
||||
#[webapi(data_property)]
|
||||
ice_candidate_pool_size: PoolSize,
|
||||
#[webidl(sequence, converter = "dictionary", default = Vec::new())]
|
||||
#[webapi(data_property)]
|
||||
ice_servers: Vec<IceServer>,
|
||||
#[webidl(converter = "enum", default = IceTransportPolicy::All)]
|
||||
#[webapi(data_property)]
|
||||
ice_transport_policy: IceTransportPolicy,
|
||||
#[webidl(converter = "enum", default = RtcpMuxPolicy::Require)]
|
||||
#[webapi(data_property)]
|
||||
rtcp_mux_policy: RtcpMuxPolicy,
|
||||
}
|
||||
|
||||
@@ -197,7 +202,10 @@ pub(super) fn constructor_configuration<'s>(
|
||||
.configuration
|
||||
.bind(scope)
|
||||
.expect("converted RTCConfiguration");
|
||||
Some(copy_configuration(scope, object, true))
|
||||
Some(
|
||||
Configuration::to_js_snapshot(scope, object, SnapshotPrototype::Null)
|
||||
.expect("converted RTCConfiguration snapshot"),
|
||||
)
|
||||
}
|
||||
|
||||
fn target<'s>(
|
||||
@@ -224,7 +232,9 @@ pub(super) fn get_configuration<'s>(
|
||||
) {
|
||||
let target = target(scope, args.this());
|
||||
let stored = configuration(scope, target);
|
||||
rv.set(copy_configuration(scope, stored, false).into());
|
||||
let snapshot = Configuration::to_js_snapshot(scope, stored, SnapshotPrototype::Ordinary)
|
||||
.expect("native RTCConfiguration snapshot");
|
||||
rv.set(snapshot.into());
|
||||
}
|
||||
|
||||
pub(super) fn set_configuration<'s>(
|
||||
@@ -271,7 +281,8 @@ pub(super) fn set_configuration<'s>(
|
||||
if validate_servers(scope, &parsed.configuration).is_none() {
|
||||
return;
|
||||
}
|
||||
let snapshot = copy_configuration(scope, next, true);
|
||||
let snapshot = Configuration::to_js_snapshot(scope, next, SnapshotPrototype::Null)
|
||||
.expect("converted RTCConfiguration snapshot");
|
||||
set_private_value(
|
||||
scope,
|
||||
target,
|
||||
@@ -381,78 +392,3 @@ fn same_certificates<'s>(
|
||||
.strict_equals(next.get_index(scope, index).expect("certificate"))
|
||||
})
|
||||
}
|
||||
|
||||
fn copy_configuration<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
internal: bool,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let output = if internal {
|
||||
new_null_prototype_object(scope)
|
||||
} else {
|
||||
v8::Object::new(scope)
|
||||
};
|
||||
for name in [
|
||||
"bundlePolicy",
|
||||
"certificates",
|
||||
"iceCandidatePoolSize",
|
||||
"iceServers",
|
||||
"iceTransportPolicy",
|
||||
"rtcpMuxPolicy",
|
||||
] {
|
||||
let mut value = property(scope, source, name);
|
||||
if matches!(name, "certificates" | "iceServers") {
|
||||
let array = v8::Local::<v8::Array>::try_from(value).expect("private sequence");
|
||||
let mut items = Vec::new();
|
||||
for index in 0..array.length() {
|
||||
let mut item = array
|
||||
.get_index(scope, index)
|
||||
.expect("private sequence item");
|
||||
if name == "iceServers" {
|
||||
let server =
|
||||
v8::Local::<v8::Object>::try_from(item).expect("private ICE server");
|
||||
item = copy_server(scope, server, internal).into();
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
value = v8::Array::new_with_elements(scope, &items).into();
|
||||
}
|
||||
assert_eq!(
|
||||
output.create_data_property(scope, v8str(scope, name).into(), value),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn copy_server<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
internal: bool,
|
||||
) -> v8::Local<'s, v8::Object> {
|
||||
let output = if internal {
|
||||
new_null_prototype_object(scope)
|
||||
} else {
|
||||
v8::Object::new(scope)
|
||||
};
|
||||
for name in ["credential", "urls", "username"] {
|
||||
let key = v8str(scope, name);
|
||||
// Optional members are absent, not inherited from a polluted prototype.
|
||||
if source.has_own_property(scope, key.into()) != Some(true) {
|
||||
continue;
|
||||
}
|
||||
let mut value = property(scope, source, name);
|
||||
if name == "urls" {
|
||||
let array = v8::Local::<v8::Array>::try_from(value).expect("private URL sequence");
|
||||
let items: Vec<_> = (0..array.length())
|
||||
.map(|index| array.get_index(scope, index).expect("private URL"))
|
||||
.collect();
|
||||
value = v8::Array::new_with_elements(scope, &items).into();
|
||||
}
|
||||
assert_eq!(
|
||||
output.create_data_property(scope, key.into(), value),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
@@ -63,6 +63,51 @@ fn rtc_configuration_uses_registered_native_proxy_identity() {
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_configuration_snapshots_preserve_certificate_identity_and_immutability() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://rtc-configuration.test/");
|
||||
let context_ptr = &vm.page_default_runtime.context as *const _;
|
||||
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
|
||||
let certificate = v8::Object::new(scope);
|
||||
crate::web_api_interfaces::RTCCertificate::DESCRIPTOR
|
||||
.initialize(scope, certificate)
|
||||
.unwrap();
|
||||
let handler = crate::util::new_null_prototype_object(scope);
|
||||
let proxy = v8::Proxy::new(scope, certificate, handler).unwrap();
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
|
||||
let global = scope.get_current_context().global(scope);
|
||||
for (name, value) in [
|
||||
("certificate", v8::Local::<v8::Value>::from(certificate)),
|
||||
("nativeCertificate", proxy.into()),
|
||||
] {
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, crate::util::v8str(scope, name).into(), value),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const input = [certificate, nativeCertificate];
|
||||
const pc = new RTCPeerConnection({certificates:input});
|
||||
input.length = 0;
|
||||
const first = pc.getConfiguration();
|
||||
if (first.certificates.length !== 2 || first.certificates[0] !== certificate || first.certificates[1] !== nativeCertificate) return false;
|
||||
first.certificates.reverse();
|
||||
pc.setConfiguration({certificates:[certificate, nativeCertificate], iceTransportPolicy:'relay'});
|
||||
const next = pc.getConfiguration();
|
||||
if (first.certificates === next.certificates || next.certificates[0] !== certificate || next.certificates[1] !== nativeCertificate || next.iceTransportPolicy !== 'relay') return false;
|
||||
for (const certificates of [[], [certificate], [nativeCertificate, certificate]]) {
|
||||
try {pc.setConfiguration({certificates}); return false;}
|
||||
catch (error) {if (error.name !== 'InvalidModificationError') return false;}
|
||||
}
|
||||
const unchanged = pc.getConfiguration();
|
||||
pc.close();
|
||||
return unchanged.certificates[0] === certificate && unchanged.certificates[1] === nativeCertificate && unchanged.iceTransportPolicy === 'relay';
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtc_configuration_applies_reentrant_updates_before_validating_the_outer_call() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://rtc-configuration.test/");
|
||||
|
||||
@@ -7,12 +7,20 @@ pub(crate) struct ContainerAttrs {
|
||||
pub(crate) prefix: Option<LitStr>,
|
||||
pub(crate) scope_lifetime: Option<syn::Lifetime>,
|
||||
pub(crate) rename_all: Option<RenameRule>,
|
||||
pub(crate) snapshot: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_container_attrs(attrs: &[syn::Attribute]) -> Result<ContainerAttrs, Error> {
|
||||
let mut parsed = ContainerAttrs::default();
|
||||
for attr in attrs.iter().filter(|attr| attr.path().is_ident("webidl")) {
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("snapshot") {
|
||||
if parsed.snapshot {
|
||||
return Err(meta.error("duplicate snapshot attribute"));
|
||||
}
|
||||
parsed.snapshot = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("prefix") {
|
||||
parsed.prefix = Some(meta.value()?.parse()?);
|
||||
return Ok(());
|
||||
|
||||
@@ -247,7 +247,7 @@ impl ConverterKind {
|
||||
/// A field conversion, optionally collecting an iterable of converted items.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct FieldConverter {
|
||||
kind: ConverterKind,
|
||||
pub(crate) kind: ConverterKind,
|
||||
sequence: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -119,9 +119,17 @@ pub(crate) fn expand_webidl_dictionary(
|
||||
}
|
||||
}
|
||||
}
|
||||
let bindings = inherited
|
||||
let ordered_fields = inherited
|
||||
.into_iter()
|
||||
.chain(members.into_values())
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = if attrs.snapshot {
|
||||
crate::snapshot::expand_snapshot(&struct_name, &generics, &ordered_fields, rename_all)?
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let bindings = ordered_fields
|
||||
.into_iter()
|
||||
.map(|field| expand_dictionary_field(field, &prefix, rename_all, &scope_lifetime))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let idents = fields
|
||||
@@ -130,6 +138,8 @@ pub(crate) fn expand_webidl_dictionary(
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#snapshot
|
||||
|
||||
impl #impl_generics ::moli_webidl::WebIdlDictionary<#scope_lifetime> for #struct_name #ty_generics #where_clause {
|
||||
fn parse_dictionary(
|
||||
scope: &mut v8::PinScope<#scope_lifetime, '_>,
|
||||
@@ -668,6 +678,25 @@ mod tests {
|
||||
use super::{expand_webidl_args, expand_webidl_dictionary, impl_parts_for_scope};
|
||||
use syn::parse_quote;
|
||||
|
||||
#[test]
|
||||
fn snapshots_reject_unsupported_ownership_and_member_policies() {
|
||||
for input in [
|
||||
parse_quote! { #[webidl(snapshot)] struct Options { #[webidl(inherit)] base: Base } },
|
||||
parse_quote! { #[webidl(snapshot)] struct Options { #[webidl(nullable)] value: Option<String> } },
|
||||
parse_quote! { #[webidl(snapshot)] struct Options { #[webidl(with = parse)] value: Custom } },
|
||||
parse_quote! { #[webidl(snapshot)] struct Options { value: BufferSource } },
|
||||
parse_quote! { #[webidl(snapshot)] struct Options { value: WebIdlCallbackFunction } },
|
||||
parse_quote! { #[webidl(snapshot)] struct Options { value: WebIdlCallbackInterface } },
|
||||
] {
|
||||
assert!(
|
||||
expand_webidl_dictionary(input)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.starts_with("dictionary snapshots")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dictionary_member_names_must_be_unique_after_renaming() {
|
||||
for input in [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod attrs;
|
||||
mod converter;
|
||||
mod expand;
|
||||
mod snapshot;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{DeriveInput, parse_macro_input};
|
||||
@@ -48,6 +49,10 @@ pub fn derive_webidl_args(input: TokenStream) -> TokenStream {
|
||||
/// Nested members support `dictionary`: null parses an empty dictionary while
|
||||
/// `Option<T>` skips missing/undefined and `nullable` also skips null.
|
||||
/// Iterable members support `sequence` with `Vec<T>` or `Option<Vec<T>>`.
|
||||
/// `#[webidl(snapshot)]` additionally derives `WebIdlDictionarySnapshot` for
|
||||
/// copying already-converted, native-owned dictionary objects. Raw converters
|
||||
/// must supply a `WebIdlSnapshotValue` policy. This opt-in pilot does not support
|
||||
/// inheritance, nullable members, custom member parsers, buffers, or callbacks.
|
||||
#[proc_macro_derive(WebIdlDictionary, attributes(webidl))]
|
||||
pub fn derive_webidl_dictionary(input: TokenStream) -> TokenStream {
|
||||
match expand::expand_webidl_dictionary(parse_macro_input!(input as DeriveInput)) {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use quote::quote;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Error, Field, Generics, Ident};
|
||||
|
||||
use crate::attrs::{RenameRule, field_member_name, parse_field_attrs};
|
||||
use crate::converter::{
|
||||
ConverterKind, converter_kind, inner_type_for_field, is_option_type, vec_inner_type,
|
||||
};
|
||||
|
||||
pub(crate) fn expand_snapshot(
|
||||
name: &Ident,
|
||||
generics: &Generics,
|
||||
fields: &[&Field],
|
||||
rename_all: RenameRule,
|
||||
) -> Result<proc_macro2::TokenStream, Error> {
|
||||
let mut members = Vec::with_capacity(fields.len());
|
||||
for field in fields {
|
||||
let attrs = parse_field_attrs(field)?;
|
||||
if attrs.inherit || attrs.nullable || attrs.with.is_some() {
|
||||
return Err(Error::new(
|
||||
field.span(),
|
||||
"dictionary snapshots do not yet support inherit, nullable, or with",
|
||||
));
|
||||
}
|
||||
let converter = converter_kind(field, &attrs)?;
|
||||
let mut ty = inner_type_for_field(field);
|
||||
if attrs.sequence {
|
||||
ty = vec_inner_type(ty).expect("validated sequence field");
|
||||
}
|
||||
let kind = match converter.kind {
|
||||
ConverterKind::Dictionary => quote! {
|
||||
::moli_webidl::SnapshotKind::Dictionary(<#ty as ::moli_webidl::WebIdlDictionarySnapshot>::SNAPSHOT_MEMBERS)
|
||||
},
|
||||
ConverterKind::Raw => quote! {
|
||||
<#ty as ::moli_webidl::WebIdlSnapshotValue>::SNAPSHOT_KIND
|
||||
},
|
||||
ConverterKind::BufferSource
|
||||
| ConverterKind::CallbackFunction
|
||||
| ConverterKind::CallbackInterface => {
|
||||
return Err(Error::new(
|
||||
field.span(),
|
||||
"dictionary snapshots require an explicit raw policy for buffers and callbacks",
|
||||
));
|
||||
}
|
||||
ConverterKind::DomString
|
||||
| ConverterKind::DomString16
|
||||
| ConverterKind::UsvString
|
||||
| ConverterKind::ByteString
|
||||
| ConverterKind::Octet
|
||||
| ConverterKind::Long
|
||||
| ConverterKind::LongLong
|
||||
| ConverterKind::EnforceRangeLong
|
||||
| ConverterKind::Short
|
||||
| ConverterKind::UnsignedShort
|
||||
| ConverterKind::EnforceRangeUnsignedShort
|
||||
| ConverterKind::ClampedUnsignedShort
|
||||
| ConverterKind::UnsignedLong
|
||||
| ConverterKind::EnforceRangeUnsignedLong
|
||||
| ConverterKind::EnforceRangeLongLong
|
||||
| ConverterKind::UnsignedLongLong
|
||||
| ConverterKind::EnforceRangeUnsignedLongLong
|
||||
| ConverterKind::Float
|
||||
| ConverterKind::UnrestrictedFloat
|
||||
| ConverterKind::Double
|
||||
| ConverterKind::UnrestrictedDouble
|
||||
| ConverterKind::Boolean
|
||||
| ConverterKind::Enum
|
||||
| ConverterKind::Interface => quote!(::moli_webidl::SnapshotKind::Identity),
|
||||
};
|
||||
let kind = if attrs.sequence {
|
||||
quote!(::moli_webidl::SnapshotKind::Sequence(&#kind))
|
||||
} else {
|
||||
kind
|
||||
};
|
||||
let member_name = field_member_name(field, &attrs, rename_all)?;
|
||||
let optional = is_option_type(&field.ty);
|
||||
members.push(quote! {
|
||||
::moli_webidl::SnapshotMember { name: #member_name, kind: #kind, optional: #optional }
|
||||
});
|
||||
}
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
Ok(quote! {
|
||||
impl #impl_generics ::moli_webidl::WebIdlDictionarySnapshot for #name #ty_generics #where_clause {
|
||||
const SNAPSHOT_MEMBERS: &'static [::moli_webidl::SnapshotMember] = &[#(#members),*];
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -27,8 +27,10 @@
|
||||
//! `#[derive(WebIdlArgs)]` generates positional argument parsing for a V8
|
||||
//! native binding's `FunctionCallbackArguments`. `#[derive(WebIdlDictionary)]`
|
||||
//! generates named property parsing for dictionary objects. Both derives use
|
||||
//! the converter wrappers in `types` and only produce Rust values; object shape
|
||||
//! declaration and Web API wrapper allocation remain outside this crate.
|
||||
//! the converter wrappers in `types` and produce Rust values. The opt-in
|
||||
//! `#[webidl(snapshot)]` also generates copy metadata for already-bound native
|
||||
//! dictionary snapshots; object shape declaration and Web API wrapper allocation
|
||||
//! remain outside this crate.
|
||||
//! Dictionary members are converted in lexicographical order of their final
|
||||
//! JavaScript names. `#[webidl(inherit)]` on one dictionary field converts the
|
||||
//! ancestor's members from the same object first; it does not read a nested
|
||||
@@ -66,6 +68,7 @@ mod convert;
|
||||
pub use buffer_source::{AllowSharedBufferSource, NonSharedBufferSource};
|
||||
mod error;
|
||||
mod helpers;
|
||||
mod snapshot;
|
||||
mod traits;
|
||||
mod types;
|
||||
|
||||
@@ -91,6 +94,9 @@ pub use moli_webidl_callback::{
|
||||
WebIdlCallbackInterface,
|
||||
};
|
||||
pub use moli_webidl_derive::{WebIdlArgs, WebIdlDictionary, WebIdlEnum};
|
||||
pub use snapshot::{
|
||||
SnapshotKind, SnapshotMember, SnapshotPrototype, WebIdlDictionarySnapshot, WebIdlSnapshotValue,
|
||||
};
|
||||
pub use traits::{ParseOutcome, WebIdlArguments, WebIdlConverter, WebIdlDictionary, WebIdlEnum};
|
||||
pub use types::{
|
||||
Boolean, BufferSource, ByteString, ClampedUnsignedShort, Dictionary, DomString, DomString16,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Snapshot copies of already-converted, native-owned dictionaries.
|
||||
//!
|
||||
//! This pilot deliberately keeps ownership in V8 private slots. Dictionary and
|
||||
//! sequence containers are copied, while interface leaves keep their identity
|
||||
//! and remain traced by V8. It does not turn scoped handles into Rust-owned
|
||||
//! state: doing that needs an explicit rooting/teardown policy and cannot be
|
||||
//! inferred from a raw converter or an interface's WebIDL name.
|
||||
//!
|
||||
//! RTCConfiguration therefore stays attached to its receiver, including when a
|
||||
//! method from another Realm is borrowed. A CommonRuntimeState map is useful
|
||||
//! for native payloads such as codecs, but a map holding Global<Object> leaves
|
||||
//! would root certificate -> connection cycles. Moving these dictionaries there
|
||||
//! would also require receiver-owner lookup, weak/finalizer cleanup, and isolate
|
||||
//! teardown ordering. Native certificate handles need a real native payload and
|
||||
//! wrapper identity policy first; this pilot adds neither a store nor globals.
|
||||
//!
|
||||
//! `to_js_snapshot` copies canonical binding output, not arbitrary JS input or
|
||||
//! Rust values. Binding remains the declaration layer's job. An `into_native`
|
||||
//! derive is deferred until field ownership policies (including custom raw
|
||||
//! converters) can be expressed without guessing or generating a parallel Rust
|
||||
//! type/lifetime graph. Only RTCConfiguration and its nested ICE server opt in.
|
||||
|
||||
use moli_v8_util::{new_null_prototype_object, v8str};
|
||||
|
||||
/// Prototype policy for every dictionary in a snapshot, including nested ones.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SnapshotPrototype {
|
||||
/// Internal records must not consult author-modifiable Object.prototype.
|
||||
Null,
|
||||
/// Public snapshots use the current (callee) Realm's Object.prototype.
|
||||
Ordinary,
|
||||
}
|
||||
|
||||
/// Copy policy for the canonical JS representation of a converted member.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SnapshotKind {
|
||||
/// Immutable scalar values or interface objects whose identity is retained.
|
||||
Identity,
|
||||
Sequence(&'static SnapshotKind),
|
||||
Dictionary(&'static [SnapshotMember]),
|
||||
}
|
||||
|
||||
/// A member generated from the same metadata as dictionary argument conversion.
|
||||
pub struct SnapshotMember {
|
||||
pub name: &'static str,
|
||||
pub kind: SnapshotKind,
|
||||
pub optional: bool,
|
||||
}
|
||||
|
||||
/// Explicit snapshot policy for custom raw converters.
|
||||
///
|
||||
/// Raw conversion can produce a scalar, an interface, or a canonical container;
|
||||
/// its representation cannot safely be inferred by the dictionary derive.
|
||||
pub trait WebIdlSnapshotValue {
|
||||
const SNAPSHOT_KIND: SnapshotKind;
|
||||
}
|
||||
|
||||
/// Opt-in dictionary copy metadata, generated by `#[webidl(snapshot)]`.
|
||||
///
|
||||
/// Sources must be fresh native binding output or private snapshots, with own
|
||||
/// data properties and dense arrays. This is not WebIDL input conversion and
|
||||
/// must never be applied directly to an author-supplied dictionary. It ignores
|
||||
/// absent optional members and unknown properties, without consulting iterators
|
||||
/// or prototypes. Missing non-optional members fail instead of silently losing
|
||||
/// a field when a binding declaration is incomplete.
|
||||
pub trait WebIdlDictionarySnapshot {
|
||||
const SNAPSHOT_MEMBERS: &'static [SnapshotMember];
|
||||
|
||||
fn to_js_snapshot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
prototype: SnapshotPrototype,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
copy_dictionary(scope, source, Self::SNAPSHOT_MEMBERS, prototype)
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_dictionary<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
members: &'static [SnapshotMember],
|
||||
prototype: SnapshotPrototype,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let output = match prototype {
|
||||
SnapshotPrototype::Null => new_null_prototype_object(scope),
|
||||
SnapshotPrototype::Ordinary => v8::Object::new(scope),
|
||||
};
|
||||
for member in members {
|
||||
let key = v8str(scope, member.name);
|
||||
if !source.has_own_property(scope, key.into())? {
|
||||
if member.optional {
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let value = source.get(scope, key.into())?;
|
||||
let value = member.kind.copy(scope, value, prototype)?;
|
||||
if !output.create_data_property(scope, key.into(), value)? {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
|
||||
impl SnapshotKind {
|
||||
fn copy<'s>(
|
||||
&self,
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Value>,
|
||||
prototype: SnapshotPrototype,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
match self {
|
||||
Self::Identity => Some(source),
|
||||
Self::Sequence(item_kind) => {
|
||||
let source = v8::Local::<v8::Array>::try_from(source).ok()?;
|
||||
let mut items = Vec::with_capacity(source.length() as usize);
|
||||
for index in 0..source.length() {
|
||||
let item = source.get_index(scope, index)?;
|
||||
items.push(item_kind.copy(scope, item, prototype)?);
|
||||
}
|
||||
Some(v8::Array::new_with_elements(scope, &items).into())
|
||||
}
|
||||
Self::Dictionary(members) => {
|
||||
let source = v8::Local::<v8::Object>::try_from(source).ok()?;
|
||||
copy_dictionary(scope, source, members, prototype).map(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
use moli_webidl::{SnapshotPrototype, WebIdlDictionary, WebIdlDictionarySnapshot};
|
||||
|
||||
mod interfaces {
|
||||
moli_webapi_declare::declare_web_api_interfaces! {
|
||||
pub Certificate;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(WebIdlDictionary, WebApiObject)]
|
||||
#[webidl(snapshot)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct Server {
|
||||
#[webidl(sequence, required)]
|
||||
urls: Vec<String>,
|
||||
username: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(WebIdlDictionary, WebApiObject)]
|
||||
#[webidl(snapshot)]
|
||||
#[webapi(plain, data_properties, enumerable)]
|
||||
struct Options<'s> {
|
||||
#[webidl(default = 7, name = "renamedNumber")]
|
||||
#[webapi(data_property = "renamedNumber")]
|
||||
number: u32,
|
||||
#[webidl(sequence, converter = "dictionary", default = Vec::new())]
|
||||
servers: Vec<Server>,
|
||||
#[webidl(sequence, interface = interfaces::Certificate, default = Vec::new())]
|
||||
certificates: Vec<v8::Local<'s, v8::Object>>,
|
||||
// Adding a field needs no separate snapshot list or copy implementation.
|
||||
#[webidl(default = "new member")]
|
||||
added_later: String,
|
||||
}
|
||||
|
||||
fn snapshot<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
let stored = v8::Local::<v8::Object>::try_from(args.data()).unwrap();
|
||||
let snapshot = Options::to_js_snapshot(scope, stored, SnapshotPrototype::Ordinary).unwrap();
|
||||
rv.set(snapshot.into());
|
||||
}
|
||||
|
||||
fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> {
|
||||
let source = v8::String::new(scope, source).unwrap();
|
||||
v8::Script::compile(scope, source, None)
|
||||
.unwrap()
|
||||
.run(scope)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshots_follow_dictionary_metadata_and_copy_only_containers() {
|
||||
moli_v8_test_util::ensure_v8();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let global = context.global(scope);
|
||||
let certificate = v8::Object::new(scope);
|
||||
interfaces::Certificate::DESCRIPTOR
|
||||
.initialize(scope, certificate)
|
||||
.unwrap();
|
||||
let key = v8::String::new(scope, "certificate").unwrap();
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, key.into(), certificate.into()),
|
||||
Some(true)
|
||||
);
|
||||
let source = eval(
|
||||
scope,
|
||||
"({servers:[{urls:['stun:host.example']}], certificates:[certificate], unknown:123})",
|
||||
);
|
||||
let parsed =
|
||||
moli_webidl::parse_dictionary_object::<Options>(scope, source.try_into().unwrap()).unwrap();
|
||||
let bound = parsed.bind(scope).unwrap();
|
||||
let stored = Options::to_js_snapshot(scope, bound, SnapshotPrototype::Null).unwrap();
|
||||
let key = v8::String::new(scope, "stored").unwrap();
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, key.into(), stored.into()),
|
||||
Some(true)
|
||||
);
|
||||
let snapshot = v8::Function::builder(snapshot)
|
||||
.data(stored.into())
|
||||
.build(scope)
|
||||
.unwrap();
|
||||
let key = v8::String::new(scope, "snapshot").unwrap();
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, key.into(), snapshot.into()),
|
||||
Some(true)
|
||||
);
|
||||
assert!(eval(scope, r#"(() => {
|
||||
const assert = (ok, message) => { if (!ok) throw Error(message); };
|
||||
assert(Object.getPrototypeOf(stored) === null, 'private root');
|
||||
assert(Object.getPrototypeOf(stored.servers[0]) === null, 'private nested dictionary');
|
||||
const first = snapshot();
|
||||
assert(Object.keys(first).join() === 'addedLater,certificates,renamedNumber,servers', 'renamed and newly added members');
|
||||
assert(first.addedLater === 'new member' && first.renamedNumber === 7, 'member defaults');
|
||||
assert(Object.getPrototypeOf(first) === Object.prototype, 'public root');
|
||||
assert(Object.getPrototypeOf(first.servers[0]) === Object.prototype, 'public nested dictionary');
|
||||
assert(first.servers instanceof Array && first.servers[0].urls instanceof Array, 'public arrays');
|
||||
assert(first.certificates !== stored.certificates && first.certificates[0] === certificate, 'interface identity');
|
||||
first.servers[0].urls[0] = 'stun:changed.example';
|
||||
first.servers[0].username = 'changed';
|
||||
first.certificates.length = 0;
|
||||
first.addedLater = 'changed';
|
||||
Object.defineProperty(Object.prototype, 'username', {configurable:true, get(){throw Error('inherited member');}});
|
||||
Object.defineProperty(Object.prototype, 'servers', {configurable:true, set(){throw Error('inherited setter');}});
|
||||
const iterator = Object.getOwnPropertyDescriptor(Array.prototype, Symbol.iterator);
|
||||
const index = Object.getOwnPropertyDescriptor(Array.prototype, '0');
|
||||
Object.defineProperty(Array.prototype, Symbol.iterator, {configurable:true, value(){throw Error('iterator');}});
|
||||
Object.defineProperty(Array.prototype, '0', {configurable:true, set(){throw Error('indexed setter');}});
|
||||
try {
|
||||
const next = snapshot();
|
||||
assert(next.servers[0].urls[0] === 'stun:host.example', 'deep copy');
|
||||
assert(!Object.hasOwn(next.servers[0], 'username'), 'optional member stays absent');
|
||||
assert(next.certificates.length === 1 && next.certificates[0] === certificate, 'interface array retained');
|
||||
assert(next.addedLater === 'new member', 'scalar copy');
|
||||
const descriptor = Object.getOwnPropertyDescriptor(next, 'servers');
|
||||
assert(descriptor.writable && descriptor.enumerable && descriptor.configurable, 'data property descriptors');
|
||||
} finally {
|
||||
delete Object.prototype.username;
|
||||
delete Object.prototype.servers;
|
||||
Object.defineProperty(Array.prototype, Symbol.iterator, iterator);
|
||||
if (index) Object.defineProperty(Array.prototype, '0', index); else delete Array.prototype[0];
|
||||
}
|
||||
return true;
|
||||
})()"#).is_true());
|
||||
|
||||
// An incomplete binding is an invariant violation, not a partial snapshot.
|
||||
let key = v8::String::new(scope, "addedLater").unwrap();
|
||||
assert_eq!(stored.delete(scope, key.into()), Some(true));
|
||||
assert!(Options::to_js_snapshot(scope, stored, SnapshotPrototype::Ordinary).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_snapshots_trace_interfaces_without_rooting_unreachable_cycles() {
|
||||
moli_v8_test_util::ensure_v8();
|
||||
let mut isolate = v8::Isolate::new(Default::default());
|
||||
let (owner, weak_owner, weak_certificate) = {
|
||||
let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let certificate = v8::Object::new(scope);
|
||||
interfaces::Certificate::DESCRIPTOR
|
||||
.initialize(scope, certificate)
|
||||
.unwrap();
|
||||
let bound = Options {
|
||||
number: 7,
|
||||
servers: Vec::new(),
|
||||
certificates: vec![certificate],
|
||||
added_later: "new member".into(),
|
||||
}
|
||||
.bind(scope)
|
||||
.unwrap();
|
||||
let stored = Options::to_js_snapshot(scope, bound, SnapshotPrototype::Null).unwrap();
|
||||
let owner = v8::Object::new(scope);
|
||||
let slot = v8::Private::new(scope, None);
|
||||
assert_eq!(owner.set_private(scope, slot, stored.into()), Some(true));
|
||||
let key = v8::String::new(scope, "owner").unwrap();
|
||||
assert_eq!(
|
||||
certificate.create_data_property(scope, key.into(), owner.into()),
|
||||
Some(true)
|
||||
);
|
||||
(
|
||||
v8::Global::new(scope, owner),
|
||||
v8::Weak::new(scope, owner),
|
||||
v8::Weak::new(scope, certificate),
|
||||
)
|
||||
};
|
||||
isolate.low_memory_notification();
|
||||
assert!(
|
||||
!weak_certificate.is_empty(),
|
||||
"the live owner traces its certificate"
|
||||
);
|
||||
drop(owner);
|
||||
isolate.low_memory_notification();
|
||||
assert!(
|
||||
weak_owner.is_empty(),
|
||||
"the owner/certificate cycle is collectible"
|
||||
);
|
||||
assert!(
|
||||
weak_certificate.is_empty(),
|
||||
"no native global roots the certificate"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user