mirror of
https://github.com/lexmount/moli.git
synced 2026-09-29 00:01:38 +00:00
398 lines
16 KiB
Rust
398 lines
16 KiB
Rust
use std::pin::pin;
|
|
|
|
use moli_v8_test_util::ensure_v8;
|
|
use moli_v8_util::{get_private_value, set_private_value, v8str};
|
|
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
|
|
|
|
const BRAND: &str = "__receiverTestBrand";
|
|
const OTHER_BRAND: &str = "__receiverTestOtherBrand";
|
|
const PROMISE: &str = "__receiverTestPromise";
|
|
|
|
fn has_brand<'s>(scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>) -> bool {
|
|
get_private_value(scope, receiver, BRAND).is_some()
|
|
}
|
|
|
|
fn has_other_brand<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
receiver: v8::Local<'s, v8::Object>,
|
|
) -> bool {
|
|
get_private_value(scope, receiver, OTHER_BRAND).is_some()
|
|
}
|
|
|
|
fn constructor<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
args: v8::FunctionCallbackArguments<'s>,
|
|
_rv: v8::ReturnValue<'s>,
|
|
) {
|
|
let brand = v8::Boolean::new(scope, true);
|
|
set_private_value(scope, args.this(), BRAND, brand.into());
|
|
let resolver = v8::PromiseResolver::new(scope).unwrap();
|
|
resolver.resolve(scope, args.this().into()).unwrap();
|
|
let promise = resolver.get_promise(scope);
|
|
set_private_value(scope, args.this(), PROMISE, promise.into());
|
|
}
|
|
|
|
fn callback<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
args: v8::FunctionCallbackArguments<'s>,
|
|
mut rv: v8::ReturnValue<'s>,
|
|
) {
|
|
// This observable conversion must never precede the receiver check.
|
|
if args.length() > 0 && args.get(0).to_string(scope).is_none() {
|
|
return;
|
|
}
|
|
rv.set(args.data());
|
|
}
|
|
|
|
fn promise_callback<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
args: v8::FunctionCallbackArguments<'s>,
|
|
mut rv: v8::ReturnValue<'s>,
|
|
) {
|
|
if args.length() > 0 && args.get(0).to_string(scope).is_none() {
|
|
return;
|
|
}
|
|
rv.set(get_private_value(scope, args.this(), PROMISE).unwrap());
|
|
}
|
|
|
|
#[derive(WebApiFunctionTemplate)]
|
|
#[webapi(interface = interfaces::NativeSample, constructor_callback = constructor, receiver = has_brand, enumerable)]
|
|
struct NativeSample {
|
|
#[webapi(method, callback = callback, length = 1, data = 7)]
|
|
method: (),
|
|
#[webapi(alias = "method")]
|
|
alias: (),
|
|
#[webapi(accessor_property, getter = callback, setter = callback, data = 9, setter_data = 11)]
|
|
value: (),
|
|
#[webapi(method, callback = promise_callback, returns_promise)]
|
|
load: (),
|
|
#[webapi(accessor_property, getter = promise_callback, setter = callback, returns_promise)]
|
|
ready: (),
|
|
#[webapi(method, callback = callback, receiver = has_other_brand, data = 13)]
|
|
other: (),
|
|
#[webapi(static_method, callback = callback, data = 17)]
|
|
static_value: (),
|
|
#[webapi(static_method, callback = callback, returns_promise)]
|
|
static_promise: (),
|
|
}
|
|
|
|
#[derive(WebApiFunctionTemplate)]
|
|
#[webapi(name = "PlainInterface", receiver = has_brand)]
|
|
struct PlainInterface {
|
|
#[webapi(method, callback = callback, data = 7)]
|
|
method: (),
|
|
#[webapi(accessor_property, getter = callback, setter = callback, data = 9)]
|
|
value: (),
|
|
#[webapi(accessor_property, getter = promise_callback, returns_promise)]
|
|
ready: (),
|
|
}
|
|
|
|
#[derive(WebApiObject)]
|
|
#[webapi(interface = interfaces::PlainInterface, receiver = has_brand)]
|
|
struct PlainObject {
|
|
#[webapi(slot = BRAND)]
|
|
brand: bool,
|
|
#[webapi(method, callback = callback, data = 7)]
|
|
method: (),
|
|
#[webapi(accessor_property, getter = callback, setter = callback, data = 9)]
|
|
value: (),
|
|
#[webapi(accessor_property, getter = promise_callback, returns_promise)]
|
|
ready: (),
|
|
}
|
|
|
|
fn run_script<'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()
|
|
}
|
|
|
|
fn install<'s>(scope: &mut v8::PinScope<'s, '_>) {
|
|
let template = NativeSample::build(scope);
|
|
let constructor = template.get_function(scope).unwrap();
|
|
let global = scope.get_current_context().global(scope);
|
|
global
|
|
.set(
|
|
scope,
|
|
v8str(scope, "NativeSample").into(),
|
|
constructor.into(),
|
|
)
|
|
.unwrap();
|
|
let key = v8::Boolean::new(scope, true);
|
|
let other = v8::Object::new(scope);
|
|
set_private_value(scope, other, OTHER_BRAND, key.into());
|
|
global
|
|
.set(scope, v8str(scope, "other").into(), other.into())
|
|
.unwrap();
|
|
}
|
|
|
|
const ASSERTIONS: &str = r#"
|
|
function assert(ok, message) { if (!ok) throw new Error(message); }
|
|
function throwsTypeError(fn) {
|
|
try { fn(); } catch (error) { return error instanceof TypeError; }
|
|
return false;
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
fn template_receiver_checks_preserve_data_descriptors_and_precede_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 context = v8::Context::new(scope, Default::default());
|
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
install(scope);
|
|
run_script(scope, ASSERTIONS);
|
|
run_script(
|
|
scope,
|
|
r#"
|
|
const proto = NativeSample.prototype;
|
|
const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
const real = new NativeSample();
|
|
class Derived extends NativeSample {}
|
|
assert(new Derived().method() === 7, 'native subclass');
|
|
assert(real.value === 9 && real.method() === 7, 'callback data');
|
|
assert(descriptor.set.call(real, 'ok') === 11, 'separate setter data');
|
|
assert(descriptor.get.name === 'get value' && descriptor.set.length === 1, 'accessor metadata');
|
|
assert(descriptor.enumerable && descriptor.configurable, 'accessor flags');
|
|
assert(proto.method.length === 1 && /\[native code\]/.test(proto.method.toString()), 'native method');
|
|
assert(proto.alias === proto.method, 'aliases retain checked function identity');
|
|
assert(NativeSample.staticValue.call({}) === 17, 'static receiver is unchecked');
|
|
assert(proto.other.call(other) === 13, 'field receiver overrides default');
|
|
assert(throwsTypeError(() => proto.other.call(real)), 'override rejects default brand');
|
|
let conversions = 0;
|
|
const input = { toString() { conversions++; return 'x'; } };
|
|
const bad = [proto, {}, Object.create(proto), new Proxy(real, {}), null, undefined, 7];
|
|
const revoked = Proxy.revocable(real, {});
|
|
revoked.revoke();
|
|
bad.push(revoked.proxy);
|
|
for (const receiver of bad) {
|
|
assert(throwsTypeError(() => proto.method.call(receiver, input)), 'method rejects');
|
|
assert(throwsTypeError(() => descriptor.get.call(receiver)), 'getter rejects');
|
|
assert(throwsTypeError(() => descriptor.set.call(receiver, input)), 'setter rejects');
|
|
}
|
|
assert(conversions === 0, 'brand before conversion');
|
|
const original = new Error('conversion');
|
|
let thrown;
|
|
try { descriptor.set.call(real, { toString() { throw original; } }); } catch (e) { thrown = e; }
|
|
assert(thrown === original, 'valid setter preserves conversion exception');
|
|
Object.setPrototypeOf(real, null);
|
|
assert(proto.method.call(real) === 7, 'brand is independent of mutable prototype');
|
|
let traps = 0;
|
|
const proxy = new Proxy(real, {
|
|
get() { traps++; }, getPrototypeOf() { traps++; }, has() { traps++; }
|
|
});
|
|
assert(throwsTypeError(() => proto.method.call(proxy)), 'proxy is not the native receiver');
|
|
assert(traps === 0, 'brand check does not execute page code');
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn promise_members_reject_all_synchronous_errors_and_preserve_success_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);
|
|
install(scope);
|
|
run_script(scope, ASSERTIONS);
|
|
let result = run_script(
|
|
scope,
|
|
r#"(async () => {
|
|
const proto = NativeSample.prototype;
|
|
const descriptor = Object.getOwnPropertyDescriptor(proto, 'ready');
|
|
const real = new NativeSample();
|
|
assert(real.ready === real.ready && real.load() === real.ready, 'cached Promise identity');
|
|
for (const receiver of [{}, proto, Object.create(proto), new Proxy(real, {}), null]) {
|
|
for (const invoke of [() => descriptor.get.call(receiver), () => proto.load.call(receiver)]) {
|
|
const promise = invoke();
|
|
assert(promise instanceof Promise, 'invalid receiver must not throw synchronously');
|
|
const reason = await promise.then(() => null, e => e);
|
|
assert(reason instanceof TypeError, 'Promise rejects with TypeError');
|
|
}
|
|
}
|
|
const original = new Error('conversion');
|
|
const input = { toString() { throw original; } };
|
|
for (const promise of [real.load(input), NativeSample.staticPromise(input)]) {
|
|
assert(promise instanceof Promise, 'callback exception becomes Promise');
|
|
assert(await promise.then(() => null, e => e) === original, 'preserve exception identity');
|
|
}
|
|
assert(throwsTypeError(() => descriptor.set.call({}, input)), 'Promise attribute setter still throws');
|
|
assert(await real.ready === real, 'successful load result');
|
|
return true;
|
|
})()"#,
|
|
);
|
|
let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
|
|
scope.perform_microtask_checkpoint();
|
|
assert_eq!(
|
|
promise.state(),
|
|
v8::PromiseState::Fulfilled,
|
|
"{}",
|
|
promise
|
|
.result(scope)
|
|
.to_string(scope)
|
|
.unwrap()
|
|
.to_rust_string_lossy(scope)
|
|
);
|
|
assert!(promise.result(scope).is_true());
|
|
}
|
|
|
|
#[test]
|
|
fn object_and_template_declarations_use_the_same_receiver_policy() {
|
|
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 global = context.global(scope);
|
|
let constructor = PlainInterface::build(scope).get_function(scope).unwrap();
|
|
global
|
|
.define_own_property(
|
|
scope,
|
|
v8str(scope, "PlainInterface").into(),
|
|
constructor.into(),
|
|
v8::PropertyAttribute::DONT_ENUM,
|
|
)
|
|
.unwrap();
|
|
let object = PlainObject::new(true).bind(scope).unwrap();
|
|
global
|
|
.set(scope, v8str(scope, "object").into(), object.into())
|
|
.unwrap();
|
|
run_script(scope, ASSERTIONS);
|
|
run_script(
|
|
scope,
|
|
r#"
|
|
for (const surface of [object, PlainInterface.prototype]) {
|
|
assert(surface.method.call(object) === 7, 'valid receiver');
|
|
assert(throwsTypeError(() => surface.method.call({})), 'invalid receiver');
|
|
const value = Object.getOwnPropertyDescriptor(surface, 'value');
|
|
assert(value.get.call(object) === 9, 'getter data');
|
|
assert(value.set.call(object, 'ok') === 9, 'setter falls back to getter data');
|
|
assert(value.get.length === 0 && value.set.length === 1, 'accessor lengths');
|
|
assert(throwsTypeError(() => value.set.call({})), 'invalid setter receiver');
|
|
const promise = Object.getOwnPropertyDescriptor(surface, 'ready').get.call({});
|
|
assert(promise instanceof Promise, 'invalid Promise getter receiver');
|
|
promise.catch(() => {});
|
|
}
|
|
"#,
|
|
);
|
|
scope.perform_microtask_checkpoint();
|
|
}
|
|
|
|
#[test]
|
|
fn cross_realm_receiver_validation_uses_the_callee_error_and_promise_realm() {
|
|
ensure_v8();
|
|
let mut isolate = v8::Isolate::new(Default::default());
|
|
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
|
let scope = &mut scope.init();
|
|
let child = v8::Context::new(scope, Default::default());
|
|
let token = v8str(scope, "same-origin");
|
|
child.set_security_token(token.into());
|
|
{
|
|
let scope = &mut v8::ContextScope::new(scope, child);
|
|
install(scope);
|
|
run_script(scope, "globalThis.real = new NativeSample()");
|
|
}
|
|
let context = v8::Context::new(scope, Default::default());
|
|
context.set_security_token(token.into());
|
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
install(scope);
|
|
let child_global = child.global(scope);
|
|
context
|
|
.global(scope)
|
|
.set(scope, v8str(scope, "child").into(), child_global.into())
|
|
.unwrap();
|
|
run_script(scope, ASSERTIONS);
|
|
let result = run_script(
|
|
scope,
|
|
r#"(async () => {
|
|
assert(NativeSample.prototype.method.call(child.real) === 7, 'cross-realm receiver');
|
|
assert(child.NativeSample.prototype.method.call(new NativeSample()) === 7, 'reverse cross-realm receiver');
|
|
let caught;
|
|
try { child.NativeSample.prototype.method.call({}); } catch (e) { caught = e; }
|
|
assert(caught instanceof child.TypeError && !(caught instanceof TypeError), 'callee TypeError realm');
|
|
const ready = Object.getOwnPropertyDescriptor(child.NativeSample.prototype, 'ready').get;
|
|
for (const promise of [ready.call({}), child.NativeSample.prototype.load.call({})]) {
|
|
assert(promise instanceof child.Promise && !(promise instanceof Promise), 'callee Promise realm');
|
|
const error = await promise.catch(e => e);
|
|
assert(error instanceof child.TypeError && !(error instanceof TypeError), 'rejection realm');
|
|
}
|
|
assert(ready.call(new NativeSample()) instanceof Promise, 'successful Promise is not rewrapped');
|
|
return true;
|
|
})()"#,
|
|
);
|
|
let promise = v8::Local::<v8::Promise>::try_from(result).unwrap();
|
|
scope.perform_microtask_checkpoint();
|
|
assert_eq!(
|
|
promise.state(),
|
|
v8::PromiseState::Fulfilled,
|
|
"{}",
|
|
promise
|
|
.result(scope)
|
|
.to_string(scope)
|
|
.unwrap()
|
|
.to_rust_string_lossy(scope)
|
|
);
|
|
assert!(promise.result(scope).is_true());
|
|
}
|
|
|
|
#[test]
|
|
fn promise_callbacks_do_not_convert_execution_termination_into_rejection() {
|
|
fn terminate(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
_args: v8::FunctionCallbackArguments<'_>,
|
|
_rv: v8::ReturnValue<'_>,
|
|
) {
|
|
scope.terminate_execution();
|
|
}
|
|
|
|
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);
|
|
install(scope);
|
|
let terminate = v8::Function::new(scope, terminate).unwrap();
|
|
context
|
|
.global(scope)
|
|
.set(scope, v8str(scope, "terminate").into(), terminate.into())
|
|
.unwrap();
|
|
let source = v8str(
|
|
scope,
|
|
// TerminateExecution requests an interrupt; reach a V8 interrupt check
|
|
// before the conversion returns, instead of relying on a short call to
|
|
// happen to consume the request.
|
|
"new NativeSample().load({toString() { terminate(); for (;;) {} }})",
|
|
);
|
|
let script = v8::Script::compile(scope, source, None).unwrap();
|
|
{
|
|
v8::tc_scope!(let scope, scope);
|
|
let result = script.run(scope);
|
|
assert!(
|
|
result.is_none(),
|
|
"promise={:?}, caught={}, terminated={}, terminating={}",
|
|
result.map(|value| value.is_promise()),
|
|
scope.has_caught(),
|
|
scope.has_terminated(),
|
|
scope.is_execution_terminating()
|
|
);
|
|
assert!(scope.has_terminated());
|
|
assert!(!scope.can_continue());
|
|
}
|
|
scope.cancel_terminate_execution();
|
|
assert!(run_script(scope, "new NativeSample().method() === 7").is_true());
|
|
}
|
|
|
|
mod interfaces {
|
|
moli_webapi_declare::declare_web_api_interfaces! {
|
|
pub(super) NativeSample;
|
|
pub(super) PlainInterface;
|
|
}
|
|
}
|