mirror of
https://github.com/lexmount/moli.git
synced 2026-09-29 00:01:38 +00:00
379 lines
14 KiB
Rust
379 lines
14 KiB
Rust
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 = <interfaces::TestBase>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Object>::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::<v8::Promise>::try_from(promise).unwrap();
|
|
assert_eq!(promise.state(), v8::PromiseState::Rejected);
|
|
let reason = promise.result(scope);
|
|
let reason = v8::Local::<v8::Object>::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)
|
|
);
|
|
}
|