use std::pin::pin; use moli_v8_test_util::ensure_v8; use moli_webapi_declare::{ WebApiObject, implements_interface, initialize_web_api_object, register_web_api_interfaces, web_api_object_type, }; #[derive(WebApiObject)] #[webapi(interface = interfaces::TestBase)] struct Base {} #[derive(WebApiObject)] #[webapi(interface = interfaces::TestDerived)] struct Derived {} #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable)] struct PlainObject { value: u32, } #[derive(WebApiObject)] #[webapi(fragment, prototype = ::DESCRIPTOR.name())] struct PrototypeMembers {} 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 declarations_brand_instances_and_preserve_derived_identity_across_realms() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); register_web_api_interfaces( scope, [("TestBase", None), ("TestDerived", Some("TestBase"))], ) .unwrap(); let first = v8::Context::new(scope, Default::default()); let second = v8::Context::new(scope, Default::default()); let object = { let scope = &mut v8::ContextScope::new(scope, first); let object = Base::new().bind(scope).unwrap(); Derived::new().initialize(scope, object).unwrap(); Base::new().initialize(scope, object).unwrap(); PlainObject::new(7).initialize(scope, object).unwrap(); assert_eq!( web_api_object_type(scope, object).unwrap().name(), "TestDerived" ); assert!(implements_interface(scope, object, "TestBase")); assert!(implements_interface(scope, object, "TestDerived")); assert!(!implements_interface(scope, object, "Unrelated")); assert!(initialize_web_api_object(scope, object, "Unrelated").is_err()); object }; let scope = &mut v8::ContextScope::new(scope, second); assert!(implements_interface(scope, object, "TestBase")); let key = v8::String::new(scope, "real").unwrap(); second .global(scope) .set(scope, key.into(), object.into()) .unwrap(); eval( scope, "Object.setPrototypeOf(real, null); real.__moliWebApiType = 'Unrelated'", ); assert_eq!( web_api_object_type(scope, object).unwrap().name(), "TestDerived" ); assert!(implements_interface(scope, object, "TestBase")); } #[test] fn identity_is_own_private_and_never_invokes_author_code() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = 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 real = Base::new().bind(scope).unwrap(); let key = v8::String::new(scope, "real").unwrap(); context .global(scope) .set(scope, key.into(), real.into()) .unwrap(); assert_eq!( eval(scope, "Reflect.ownKeys(real).length").uint32_value(scope), Some(0) ); for source in [ "({__moliWebApiType: 0})", "({[Symbol('__moliWebApiType')]: 0})", "Object.create(real)", "new Proxy(real, {get() { throw Error('trap'); }, getPrototypeOf() { throw Error('trap'); }})", "(() => { const {proxy, revoke} = Proxy.revocable(real, {}); revoke(); return proxy; })()", ] { let object = v8::Local::::try_from(eval(scope, source)).unwrap(); assert_eq!(web_api_object_type(scope, object), None, "{source}"); assert!(!implements_interface(scope, object, "TestBase"), "{source}"); } let plain = PlainObject::new(7).bind(scope).unwrap(); let prototype = PrototypeMembers::new().bind(scope).unwrap(); assert_eq!(web_api_object_type(scope, plain), None); assert_eq!(web_api_object_type(scope, prototype), None); } #[test] fn inheritance_registration_is_atomic_and_rejects_conflicts_and_cycles() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = 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 object = Derived::new().bind(scope).unwrap(); register_web_api_interfaces( scope, [("TestDerived", Some("TestBase")), ("TestBase", None)], ) .unwrap(); register_web_api_interfaces(scope, [("TestDerived", Some("TestBase"))]).unwrap(); assert!(register_web_api_interfaces(scope, [("TestDerived", None)]).is_err()); assert!(register_web_api_interfaces(scope, [("A", Some("B")), ("B", Some("A"))]).is_err()); assert!(implements_interface(scope, object, "TestBase")); // A rejected batch has not left conflicting declarations behind. register_web_api_interfaces(scope, [("A", None), ("B", Some("A"))]).unwrap(); } #[derive(moli_webapi_declare::WebApiFunctionTemplate)] #[webapi(interface = interfaces::NativeConstructor, constructor_callback = native_constructor)] struct NativeConstructor {} fn native_constructor<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s>, ) { let key = v8::String::new(scope, "capturedReceiver").unwrap(); scope .get_current_context() .global(scope) .set(scope, key.into(), args.this().into()); if args.get(0).is_true() { let message = v8::String::new(scope, "construction failed").unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); } else if args.get(0).is_object() { rv.set(args.get(0)); } } #[test] fn native_constructors_brand_successful_receivers_and_replacement_objects() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let template = NativeConstructor::build(scope); let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); let constructor = template.get_function(scope).unwrap(); let key = v8::String::new(scope, "NativeConstructor").unwrap(); context .global(scope) .set(scope, key.into(), constructor.into()); for source in [ "new NativeConstructor()", "new (class Derived extends NativeConstructor {})()", "new NativeConstructor({replacement: true})", "NativeConstructor({replacement: true})", ] { let object = v8::Local::::try_from(eval(scope, source)).unwrap(); assert!( implements_interface(scope, object, "NativeConstructor"), "{source}" ); } let failed = eval( scope, "try { new NativeConstructor(true); } catch (e) { if (e.message !== 'construction failed') throw e; } capturedReceiver", ); let failed = v8::Local::::try_from(failed).unwrap(); assert_eq!(web_api_object_type(scope, failed), None); } #[test] fn only_explicitly_registered_native_proxies_share_target_identity() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = 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 target = Base::new().bind(scope).unwrap(); let handler = v8::Object::new(scope); let native = v8::Proxy::new(scope, target, handler).unwrap(); let native_object = v8::Local::::from(native); assert_eq!(web_api_object_type(scope, native_object), None); moli_webapi_declare::register_web_api_proxy(scope, native).unwrap(); assert!(implements_interface(scope, native_object, "TestBase")); initialize_web_api_object(scope, native_object, "TestBase").unwrap(); let impostor = v8::Proxy::new(scope, target, handler).unwrap(); assert_eq!(web_api_object_type(scope, impostor.into()), None); let outer_handler = v8::Object::new(scope); let outer = v8::Proxy::new(scope, native_object, outer_handler).unwrap(); assert_eq!(web_api_object_type(scope, outer.into()), None); assert!(moli_webapi_declare::register_web_api_proxy(scope, outer).is_err()); native.revoke(); assert_eq!(web_api_object_type(scope, native_object), None); } #[derive(moli_webapi_declare::WebApiFunctionTemplate)] #[webapi(interface = interfaces::TestBase, receiver)] struct CheckedBaseTemplate { #[webapi(method, callback = convert_argument)] convert: (), #[webapi(method, callback = convert_argument, returns_promise)] convert_async: (), } fn convert_argument<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s>, ) { if let Some(value) = args.get(0).to_string(scope) { rv.set(value.into()); } } #[test] fn generated_interface_receivers_accept_subtypes_and_reject_forgery_before_conversion() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); let template = CheckedBaseTemplate::build(scope); let context = v8::Context::new(scope, Default::default()); let other = v8::Context::new(scope, Default::default()); let object = { let scope = &mut v8::ContextScope::new(scope, other); register_web_api_interfaces(scope, [("TestDerived", Some("TestBase"))]).unwrap(); Derived::new().bind(scope).unwrap() }; let scope = &mut v8::ContextScope::new(scope, context); let constructor = template.get_function(scope).unwrap(); let global = context.global(scope); let constructor_key = v8::String::new(scope, "TestBase").unwrap(); let object_key = v8::String::new(scope, "real").unwrap(); global.set(scope, constructor_key.into(), constructor.into()); global.set(scope, object_key.into(), object.into()); assert!(eval(scope, r#" (() => { let conversions = 0; const input = {toString() { conversions++; return 'converted'; }}; const method = TestBase.prototype.convert; Object.setPrototypeOf(real, null); if (method.call(real, input) !== 'converted') return false; for (const fake of [{}, Object.create(TestBase.prototype), Object.create(real), new Proxy(real, {})]) { try { method.call(fake, input); return false; } catch (error) { if (!(error instanceof TypeError)) return false; } } return conversions === 1; })() "#).is_true()); let promise = eval( scope, "TestBase.prototype.convertAsync.call({}, {toString() { throw Error('must not convert'); }})", ); let promise = v8::Local::::try_from(promise).unwrap(); assert_eq!(promise.state(), v8::PromiseState::Rejected); let reason = promise.result(scope); let reason = v8::Local::::try_from(reason).unwrap(); let name = v8::String::new(scope, "name").unwrap(); assert_eq!( reason .get(scope, name.into()) .unwrap() .to_string(scope) .unwrap() .to_rust_string_lossy(scope), "TypeError" ); } mod interfaces { moli_webapi_declare::declare_web_api_interfaces! { pub(super) NativeConstructor; pub(super) TestBase; pub(super) TestDerived: TestBase; } } #[test] fn descriptor_registers_unexposed_ancestry_and_rejects_forged_receivers() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = 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 real = v8::Object::new(scope); interfaces::TestDerived::DESCRIPTOR .initialize(scope, real) .unwrap(); assert!(interfaces::TestBase::is_instance(scope, real)); assert!(interfaces::TestDerived::is_instance(scope, real)); let forged = v8::Object::new(scope); forged.set_prototype(scope, real.into()).unwrap(); assert!(!interfaces::TestBase::is_instance(scope, forged)); assert!(!interfaces::TestDerived::is_instance(scope, forged)); real.set_prototype(scope, v8::null(scope).into()).unwrap(); assert!(interfaces::TestBase::is_instance(scope, real)); } #[test] fn cyclic_native_descriptors_fail_during_registration() { use moli_webapi_declare::WebApiInterfaceDescriptor; static FIRST: WebApiInterfaceDescriptor = WebApiInterfaceDescriptor::new("CycleFirst", Some(&SECOND)); static SECOND: WebApiInterfaceDescriptor = WebApiInterfaceDescriptor::new("CycleSecond", Some(&FIRST)); ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = pin!(v8::HandleScope::new(&mut isolate)); let scope = &mut scope.init(); assert!( FIRST .register(scope) .unwrap_err() .to_string() .contains("cycle") ); } #[derive(WebApiObject)] #[webapi(fragment)] struct SharedState { #[webapi(slot = "__sharedState")] value: u32, } #[test] fn shared_fragments_preserve_target_identity_and_prototype() { ensure_v8(); let mut isolate = v8::Isolate::new(Default::default()); let scope = 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 real = Derived::new().bind(scope).unwrap(); let prototype = v8::Object::new(scope); real.set_prototype(scope, prototype.into()).unwrap(); SharedState::new(7).bind_into(scope, real).unwrap(); assert!( real.get_prototype(scope) .unwrap() .strict_equals(prototype.into()) ); assert!(interfaces::TestDerived::is_instance(scope, real)); assert!(interfaces::TestBase::is_instance(scope, real)); assert_eq!( moli_v8_util::get_private_value(scope, real, "__sharedState") .unwrap() .uint32_value(scope), Some(7) ); }