mod common; use common::run; #[test] fn primitives_cycles_and_property_serialization() { assert_eq!(run(r#" for(const value of [undefined,null,true,false,0,-0,NaN,Infinity,-Infinity,123n,'\ud800']){ if(!Object.is(structuredClone(value),value))throw Error('Primitive'); } let calls=0; const child={x:1}, source=Object.create(null); Object.defineProperties(source,{ a:{value:child,enumerable:true},b:{value:child,enumerable:true}, hidden:{value:()=>{},enumerable:false}, getter:{enumerable:true,get(){calls++;return child;}} }); source.self=source;source[Symbol('ignored')]=()=>{}; const result=structuredClone(source); if(result===source || result.self!==result || result.a!==result.b || result.a!==result.getter || result.a===child || calls!==1)throw Error('Graph'); if(Object.getPrototypeOf(result)!==Object.prototype || 'hidden' in result || Object.getOwnPropertySymbols(result).length)throw Error('Properties'); const d=Object.getOwnPropertyDescriptor(result,'getter'); if(!d.writable || !d.enumerable || !d.configurable || d.get)throw Error('Descriptor'); const sparse=[];sparse.length=4;sparse[2]=child;sparse.extra=child; const array=structuredClone(sparse); if(array.length!==4 || 0 in array || array[2]!==array.extra)throw Error('Sparse array'); class Custom { constructor(){this.value=7;} method(){} } if(Object.getPrototypeOf(structuredClone(new Custom()))!==Object.prototype)throw Error('Custom prototype'); if(!Object.getOwnPropertyDescriptor(globalThis,'structuredClone').enumerable || structuredClone.name!=='structuredClone' || structuredClone.length!==1)throw Error('Function shape'); try{new structuredClone({});throw Error('Constructor');}catch(e){if(!(e instanceof TypeError))throw e;} console.log('graphs'); "#,&[]).stdout,b"graphs\n"); } #[test] fn maps_sets_dates_regexps_and_boxed_primitives() { assert_eq!(run(r#" const key={}, map=new Map(), set=new Set(); map.set(key,set);map.set(map,key);set.add(key);set.add(map); const date=new Date(123456), re=/a+/giy;re.lastIndex=9; for(const value of [map,set,date,re,new Number(NaN),new Boolean(false),new String('hi'),Object(9n)])value.extra=()=>{}; const result=structuredClone({map,set,key,date,re,boxed:[new Number(NaN),new Boolean(false),new String('hi'),Object(9n)]}); if(result.map.get(result.key)!==result.set || result.map.get(result.map)!==result.key || !result.set.has(result.map) || !result.set.has(result.key))throw Error('Collection identity'); if(result.date.getTime()!==123456 || result.re.source!=='a+' || result.re.flags!=='giy' || result.re.lastIndex!==0)throw Error('Date/RegExp'); if(!Number.isNaN(result.boxed[0].valueOf()) || result.boxed[1].valueOf()!==false || result.boxed[2].valueOf()!=='hi' || result.boxed[3].valueOf()!==9n)throw Error('Boxed'); if(!Number.isNaN(structuredClone(new Date(NaN)).getTime()))throw Error('Invalid Date'); console.log('builtins'); "#,&[]).stdout,b"builtins\n"); } #[test] fn buffers_and_views_copy_bytes_and_preserve_shared_graph_backing() { assert_eq!(run(r#" const buffer=new ArrayBuffer(64);new Uint8Array(buffer).set([1,2,3,4]); const views=[new Int8Array(buffer,1,3),new Uint8Array(buffer,1,3),new Uint8ClampedArray(buffer,1,3),new Int16Array(buffer,2,3),new Uint16Array(buffer,2,3),new Int32Array(buffer,4,3),new Uint32Array(buffer,4,3),new Float32Array(buffer,4,3),new Float64Array(buffer,8,3),new BigInt64Array(buffer,8,3),new BigUint64Array(buffer,8,3),new DataView(buffer,3,7)]; if(typeof Float16Array==='function')views.push(new Float16Array(buffer,2,3)); const result=structuredClone({buffer,views,again:views[0]}); if(result.buffer===buffer || result.again!==result.views[0] || buffer.detached)throw Error('Buffer identity'); for(let i=0;i{},Symbol(),Object(Symbol()),new WeakMap(),new WeakSet(),new WeakRef({}),new Promise(()=>{}),new Proxy({},{}),new Proxy([],{}),[()=>{}],new Map([[()=>{},1]])])fails(value); const buffer=new ArrayBuffer(4); fails({fn:()=>{}},{transfer:[buffer]}); fails({},{transfer:[buffer,buffer]}); fails({},{transfer:[buffer,{}]}); fails({},{transfer:[new SharedArrayBuffer(1)]}); if(buffer.detached)throw Error('Early detach'); const detached=new ArrayBuffer(1), view=new Uint8Array(detached), data=new DataView(detached);detached.transfer(); for(const value of [detached,view,data])fails(value); fails({},{transfer:[detached]}); const beforeDetached={}; try{structuredClone({get x(){throw beforeDetached;}},{transfer:[detached]});throw Error('Early detached check');}catch(e){if(e!==beforeDetached)throw e;} const frozenStream=new ReadableStream(), frozenBuffer=new ArrayBuffer(1); Object.freeze(frozenStream); fails({stream:frozenStream},{transfer:[frozenBuffer,frozenStream]}); if(frozenBuffer.detached || frozenStream.locked)throw Error('Frozen stream transfer changed sources'); const reason={exact:true}; try{structuredClone({get x(){throw reason;}},{transfer:[buffer]});throw Error('Getter');}catch(e){if(e!==reason)throw e;} if(buffer.detached || structuredClone({x:1}).x!==1)throw Error('Failed state'); console.log('failures'); "#,&[]).stdout,b"failures\n"); } #[test] fn getters_can_resize_or_detach_transfers_and_fail_in_list_order() { assert_eq!(run(r#" const first=new ArrayBuffer(2), second=new ArrayBuffer(3); try{structuredClone({get x(){second.transfer();return 1;}},{transfer:[first,second]});throw Error('Detached late buffer');}catch(e){if(e.name!=='DataCloneError')throw e;} if(!first.detached || !second.detached)throw Error('Transfer order'); const a=new ArrayBuffer(2), b=new ArrayBuffer(3); try{structuredClone({get x(){a.transfer();return 1;}},{transfer:[a,b]});throw Error('Detached first');}catch(e){if(e.name!=='DataCloneError')throw e;} if(!a.detached || b.detached)throw Error('Failure order'); const rab=new ArrayBuffer(2,{maxByteLength:8});new Uint8Array(rab).set([3,4]); const result=structuredClone({rab,get x(){rab.resize(4);return 1;}},{transfer:[rab]}); if(!rab.detached || !result.rab.resizable || result.rab.maxByteLength!==8 || result.rab.byteLength!==4 || new Uint8Array(result.rab).join()!=='3,4,0,0')throw Error('Resize during serialization'); const memory=new WebAssembly.Memory({initial:1}); try{structuredClone(memory.buffer,{transfer:[memory.buffer]});throw Error('Wasm buffer transfer');}catch(e){if(!(e instanceof TypeError))throw e;} const shrunk=new ArrayBuffer(4,{maxByteLength:8}), fixed=new Uint8Array(shrunk,1,3); try{structuredClone({fixed,get x(){shrunk.resize(1);return 1;}},{transfer:[shrunk]});throw Error('Invalidated view');}catch(e){if(e.name!=='DataCloneError' || !e.message.includes('not yet implemented'))throw e;} if(!shrunk.detached || structuredClone(3)!==3)throw Error('Deserialization failure state'); console.log('late transfers'); "#,&[]).stdout,b"late transfers\n"); } #[test] fn resizable_and_shared_buffers_keep_their_native_semantics() { assert_eq!(run(r#" const buffer=new ArrayBuffer(4,{maxByteLength:12}), tracking=new Uint8Array(buffer), fixed=new Uint8Array(buffer,1,2); tracking.set([1,2,3,4]);const result=structuredClone({buffer,tracking,fixed}); if(!result.buffer.resizable || result.buffer.maxByteLength!==12 || result.tracking.buffer!==result.buffer || result.fixed.buffer!==result.buffer)throw Error('Resizable'); result.buffer.resize(8); if(result.tracking.length!==8 || result.fixed.length!==2 || buffer.byteLength!==4)throw Error('Tracking'); const shared=new SharedArrayBuffer(4,{maxByteLength:12}), view=new Uint8Array(shared);view[0]=5; const copy=structuredClone({shared,view,again:shared}); if(copy.shared===shared || copy.shared!==copy.again || copy.view.buffer!==copy.shared || !copy.shared.growable || copy.shared.maxByteLength!==12)throw Error('Shared graph'); copy.view[0]=9;copy.shared.grow(8); if(view[0]!==9 || shared.byteLength!==8 || copy.view.length!==8 || view.length!==8)throw Error('Shared memory'); console.log('native buffers'); "#,&[]).stdout,b"native buffers\n"); } #[test] fn errors_dom_exceptions_and_webassembly_modules_clone() { assert_eq!(run(r#" for(const C of [Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError]){ const error=new C('message',{cause:{x:1}});error.extra=()=>{}; const result=structuredClone(error); if(!(result instanceof C) || result.message!=='message' || result.cause.x!==1 || result.cause===error.cause || 'extra' in result)throw Error('Error'); } const e=new Error('cyclic');e.cause=e;const c=structuredClone(e);if(c.cause!==c)throw Error('Error cycle'); const source=new DOMException('message','DataCloneError');source.extra=()=>{}; const stack=source.stack; Object.defineProperty(source,'name',{get(){throw Error('Own name getter');}}); const d=structuredClone(source); if(!Error.isError(d) || d.stack!==stack || DOMException.DATA_CLONE_ERR!==25 || d.DATA_CLONE_ERR!==25)throw Error('DOMException error data'); if(!(d instanceof DOMException) || d.name!=='DataCloneError' || d.code!==25 || d.message!=='message' || 'extra' in d)throw Error('DOMException'); const module=new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0])); const modules=structuredClone([module,module]); if(modules[0]===module || modules[0]!==modules[1] || !(modules[0] instanceof WebAssembly.Module) || !(new WebAssembly.Instance(modules[0]) instanceof WebAssembly.Instance))throw Error('Wasm module'); console.log('errors and wasm'); "#,&[]).stdout,b"errors and wasm\n"); } #[test] fn blobs_and_files_clone_private_immutable_storage_and_metadata() { assert_eq!(run(r#" const bytes=new Uint8Array([1,2,3]), blob=new Blob([bytes],{type:'Text/Plain'}), file=new File([blob],'a\0\ud800.txt',{type:'x/file',lastModified:123}); bytes.fill(0);blob.extra=()=>{};file.extra=()=>{}; Object.defineProperty(file,'name',{get(){throw Error('Public metadata');}}); Object.setPrototypeOf(blob,null); const result=structuredClone({blob,file,again:file,map:new Map([[file,blob]])}); if(!(result.blob instanceof Blob) || result.blob===blob || result.blob.type!=='text/plain' || result.blob.size!==3)throw Error('Blob'); if(!(result.file instanceof File) || result.file===file || result.file!==result.again || result.map.get(result.file)!==result.blob || result.file.name!=='a\0\ufffd.txt' || result.file.lastModified!==123 || result.file.type!=='x/file')throw Error('File'); if('extra' in result.blob || 'extra' in result.file)throw Error('Expandos'); (await result.file.bytes()).fill(0); if((await result.blob.bytes()).join()!=='1,2,3' || (await File.prototype.bytes.call(file)).join()!=='1,2,3')throw Error('Mutable clone'); const mime=await new Response('x',{headers:{'Content-Type':'Text/HTML; Charset=UTF-8'}}).blob(); if(structuredClone(mime).type!=='text/html;charset=UTF-8')throw Error('MIME case'); let rope=new Blob(['x']);for(let i=0;i<50;i++)rope=new Blob([rope,rope]); const copied=structuredClone(rope);if(copied.size!==2**50 || await copied.slice(-1).text()!=='x')throw Error('Flattened rope'); console.log('blob clones'); "#,&[]).stdout,b"blob clones\n"); } #[test] fn platform_brands_reject_cloning_without_reading_public_properties() { assert_eq!(run(r#" let readableController, byteController, writableController, transformController; const readable=new ReadableStream({start(c){readableController=c;}}), bytes=new ReadableStream({type:'bytes',start(c){byteController=c;}}); const writable=new WritableStream({start(c){writableController=c;}}), transform=new TransformStream({start(c){transformController=c;}}); const reader=readable.getReader(), byob=bytes.getReader({mode:'byob'}), writer=writable.getWriter(); const values=[new URL('https://example.test'),new URLSearchParams(),new URLSearchParams().entries(),new FormData().values(),new URLPattern({pathname:'*'}),new FormData(),new Headers(),new Request('https://example.test'),new Response(),new Event('x'),new EventTarget(),new AbortController(),new AbortController().signal,new TextEncoder(),new TextDecoder(),new TextEncoderStream(),new TextDecoderStream(),new ByteLengthQueuingStrategy({highWaterMark:1}),new CountQueuingStrategy({highWaterMark:1}),readable,bytes,writable,transform,reader,byob,writer,readableController,byteController,writableController,transformController,transform.readable,transform.writable]; for(const value of values){ Object.defineProperty(value,'trap',{enumerable:true,get(){throw Error('Read a platform expando');}}); Object.setPrototypeOf(value,null); try{structuredClone(value);throw Error('Cloned platform object');}catch(e){if(e.name!=='DataCloneError')throw e;} } for(const stream of [readable,writable]){ const buffer=new ArrayBuffer(1);let getter=false; try{structuredClone({get value(){getter=true;return 1;}}, {transfer:[buffer,stream]});throw Error('Transferred locked stream');}catch(e){if(e.name!=='DataCloneError')throw e;} if(buffer.detached||getter)throw Error('Invalid transfer was not rejected first'); } const transferBuffer=new ArrayBuffer(1), movedTransform=structuredClone(transform,{transfer:[transferBuffer,transform]}); if(!transferBuffer.detached || !(movedTransform instanceof TransformStream) || !(movedTransform.readable instanceof ReadableStream) || !(movedTransform.writable instanceof WritableStream))throw Error('Transform transfer'); const fake=Object.create(Blob.prototype);fake.value=1; if(structuredClone(fake).value!==1)throw Error('Prototype mistaken for brand'); console.log('brands'); "#,&[]).stdout,b"brands\n"); } #[test] fn cloning_is_permissionless_and_native_helpers_remain_private() { assert_eq!(run(r#" import {pledge} from 'avril:std';pledge({net:false,read:false,jsExec:false}); const buffer=new Uint8Array([1,2]).buffer; if(structuredClone(buffer,{transfer:[buffer]}).byteLength!==2 || !buffer.detached)throw Error('Permissionless clone'); if(await structuredClone(new Blob(['x'])).text()!=='x')throw Error('Permissionless Blob'); for(const name of ['ext:core/structured_clone','ext:web/serialization.js','ext:structured_clone/main.js']){ try{await import(name);throw Error('Private import');}catch(e){if(!e.message.includes('private'))throw e;} } const opsSymbol=Symbol.for('__avril.ops'); if(opsSymbol in globalThis || globalThis[opsSymbol]!==undefined || Reflect.ownKeys(globalThis).includes(opsSymbol))throw Error('Public native op registry'); console.log('private and permissionless'); "#,&[]).stdout,b"private and permissionless\n"); } #[test] fn private_cloning_uses_primordials_not_guest_constructors_or_prototype_hooks() { assert_eq!(run(r#" const blob=new Blob(['x']), file=new File([blob],'name',{lastModified:7}), exception=new DOMException('message','DataCloneError'); const buffer=new Uint8Array([1,2,3]).buffer, originalDOMException=DOMException; const define=Object.defineProperty, getDescriptor=Object.getOwnPropertyDescriptor, getPrototype=Object.getPrototypeOf, setPrototype=Object.setPrototypeOf; const restore=[]; const poison=()=>{throw Error('Guest hook invoked');}; for(const [object,key] of [[WeakMap.prototype,'get'],[WeakMap.prototype,'set'],[Array.prototype,'push'],[ArrayBuffer.prototype,'slice'],[ArrayBuffer.prototype,'transfer'],[Object,'create'],[Object,'defineProperty'],[Reflect,'construct'],[Reflect,'apply']]){ restore.push([object,key,getDescriptor(object,key)]); } for(let i=0;i{});}catch(e){failure=e;} delete Array.prototype[0]; for(let i=0;i