use std::cell::RefCell; use std::io; use std::rc::Rc; use std::sync::{Arc, Weak}; use avril_core::{Extension, Op, Runtime, RuntimeOptions, message, v8}; struct Payload(u32); fn runtime() -> (Runtime, Rc>>>) { let messages = Rc::new(RefCell::new(Vec::>::new())); let payloads = Rc::new(RefCell::new(Vec::new())); let runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![ Op::with_scope("encode_payload", { let payloads = payloads.clone(); move |scope, value: u32| { let payload = Arc::new(Payload(value)); payloads.borrow_mut().push(Arc::downgrade(&payload)); message::serialize_host(scope, payload) } }), Op::with_scope("decode_payload", |scope, id: u32| { Ok::<_, io::Error>(message::deserialize_host::(scope, id)?.0) }), Op::raw("send", { let messages = messages.clone(); move |scope, args| { let args = v8::Local::::try_from(args.get(0)).ok()?; let value = args.get_index(scope, 0)?; let mut callbacks = Vec::new(); for index in 1..6 { callbacks.push(v8::Local::::try_from(args.get_index(scope, index)?).ok()?); } let raw_transfer = args.get_index(scope, 6)?; let transfer = v8::Local::::try_from(raw_transfer).unwrap_or_else(|_| v8::Array::new(scope, 0)); let packet = message::serialize(scope, value, transfer, callbacks.try_into().ok()?)?; let mut messages = messages.borrow_mut(); let id = messages.len() as u32; messages.push(Some(packet)); Some(v8::Integer::new_from_unsigned(scope, id).into()) } }), Op::new("duplicate", { let messages = messages.clone(); move |id: usize| { let mut messages = messages.borrow_mut(); let copy = messages[id].as_ref().unwrap().try_clone(); let id = copy.map(|message| { let id = messages.len(); messages.push(Some(message)); id }); Ok::<_, std::convert::Infallible>(id) } }), Op::raw("receive", move |scope, args| { let args = v8::Local::::try_from(args.get(0)).ok()?; let id = args.get_index(scope, 0)?.uint32_value(scope)?; let host = v8::Local::::try_from(args.get_index(scope, 1)?).ok()?; let error = v8::Local::::try_from(args.get_index(scope, 2)?).ok()?; let packet = messages.borrow_mut().get_mut(id as usize)?.take()?; message::deserialize(scope, packet, host, error) }), ], modules: &[("ext:test/main.js", r#" import {encode_payload,decode_payload,send,receive,duplicate} from 'ext:core/ops'; const hosts=new WeakMap(); globalThis.makeHost=value=>{const host={};hosts.set(host,value);return host;}; globalThis.hostValue=host=>hosts.get(host); globalThis.encodePayload=encode_payload; globalThis.decodePayload=decode_payload; const error=message=>new Error(message); globalThis.send=(value,encode=host=>encode_payload(hosts.get(host)))=> send([value,host=>hosts.has(host)?1:0,encode,error,()=>{},()=>{}]); globalThis.receive=(id,decode=index=>makeHost(decode_payload(index)))=>receive([id,decode,error]); globalThis.duplicate=duplicate; globalThis.sendTransfer=(value,transfer)=>send([value,()=>0,()=>{},error,()=>{},()=>{},transfer]); "#)], entry_point: Some("ext:test/main.js"), }], ..Default::default() }).unwrap(); (runtime, payloads) } #[test] fn opaque_payloads_live_until_messages_are_consumed_or_dropped() { let (mut runtime, payloads) = runtime(); runtime .execute_script::<()>("send", "globalThis.id=send(makeHost(42));") .unwrap(); assert!(payloads.borrow()[0].upgrade().is_some()); assert_eq!( runtime .execute_script::("receive", "hostValue(receive(id))") .unwrap(), 42 ); assert!(payloads.borrow()[0].upgrade().is_none()); runtime .execute_script::<()>("queued", "send(makeHost(7));") .unwrap(); assert!(payloads.borrow()[1].upgrade().is_some()); drop(runtime); assert!(payloads.borrow()[1].upgrade().is_none()); } #[test] fn failed_and_nested_messages_do_not_leak_or_replace_the_active_payloads() { let (mut runtime, payloads) = runtime(); runtime .execute_script::<()>( "nested", r#" for(const call of [()=>encodePayload(1),()=>decodePayload(0)]) { try{call();throw Error('Allowed outside callback');} catch(e){if(!e.message.includes('No matching host serialization callback'))throw e;} } const id=send(makeHost(42),host=>{ try{send({host:makeHost(7),fail(){}});throw Error('Accepted function');} catch(e){if(e.message==='Accepted function')throw e;} const inner=send(makeHost(11)); if(hostValue(receive(inner))!==11)throw Error('Nested payload'); return encodePayload(hostValue(host)); }); const result=receive(id,index=>{ const inner=send(makeHost(13)); if(hostValue(receive(inner))!==13)throw Error('Nested decode'); try{decodePayload(999);throw Error('Accepted invalid index');} catch(e){if(!e.message.includes('Invalid host payload'))throw e;} return makeHost(decodePayload(index)); }); if(hostValue(result)!==42)throw Error('Outer payload'); try{send({host:makeHost(17),fail(){}});throw Error('Accepted function');} catch(e){if(e.message==='Accepted function')throw e;} try{encodePayload(1);throw Error('Callback context leaked');} catch(e){if(!e.message.includes('No matching host serialization callback'))throw e;} "#, ) .unwrap(); assert!(!payloads.borrow().is_empty()); assert!( payloads .borrow() .iter() .all(|payload| payload.upgrade().is_none()) ); } #[test] fn only_messages_without_transfers_can_be_duplicated() { let (mut runtime, _) = runtime(); runtime.execute_script::<()>("copy", r#" const original = send({bytes:new Uint8Array([1,2])}); const copy = duplicate(original); const a = receive(original), b = receive(copy); a.bytes[0] = 9; if (b.bytes[0] !== 1) throw Error('Aliased nonshared buffer'); const buffer = new ArrayBuffer(4); const transferred = sendTransfer(buffer,[buffer]); if (buffer.byteLength !== 0 || duplicate(transferred) !== null) throw Error('Duplicated transfer'); if (receive(transferred).byteLength !== 4) throw Error('Lost transferred buffer'); const unused = new ArrayBuffer(8); const withUnusedTransfer = sendTransfer('value',[unused]); if (duplicate(withUnusedTransfer) !== null) throw Error('Ignored unused transfer'); "#).unwrap(); }