char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
20.5 KiB283 linesraw
1mod common;
2
3use common::run;
4
5#[test]
6fn primitives_cycles_and_property_serialization() {
7    assert_eq!(run(r#"
8        for(const value of [undefined,null,true,false,0,-0,NaN,Infinity,-Infinity,123n,'\ud800']){
9            if(!Object.is(structuredClone(value),value))throw Error('Primitive');
10        }
11        let calls=0;
12        const child={x:1}, source=Object.create(null);
13        Object.defineProperties(source,{
14            a:{value:child,enumerable:true},b:{value:child,enumerable:true},
15            hidden:{value:()=>{},enumerable:false},
16            getter:{enumerable:true,get(){calls++;return child;}}
17        });
18        source.self=source;source[Symbol('ignored')]=()=>{};
19        const result=structuredClone(source);
20        if(result===source || result.self!==result || result.a!==result.b || result.a!==result.getter || result.a===child || calls!==1)throw Error('Graph');
21        if(Object.getPrototypeOf(result)!==Object.prototype || 'hidden' in result || Object.getOwnPropertySymbols(result).length)throw Error('Properties');
22        const d=Object.getOwnPropertyDescriptor(result,'getter');
23        if(!d.writable || !d.enumerable || !d.configurable || d.get)throw Error('Descriptor');
24        const sparse=[];sparse.length=4;sparse[2]=child;sparse.extra=child;
25        const array=structuredClone(sparse);
26        if(array.length!==4 || 0 in array || array[2]!==array.extra)throw Error('Sparse array');
27        class Custom { constructor(){this.value=7;} method(){} }
28        if(Object.getPrototypeOf(structuredClone(new Custom()))!==Object.prototype)throw Error('Custom prototype');
29        if(!Object.getOwnPropertyDescriptor(globalThis,'structuredClone').enumerable || structuredClone.name!=='structuredClone' || structuredClone.length!==1)throw Error('Function shape');
30        try{new structuredClone({});throw Error('Constructor');}catch(e){if(!(e instanceof TypeError))throw e;}
31        console.log('graphs');
32    "#,&[]).stdout,b"graphs\n");
33}
34
35#[test]
36fn maps_sets_dates_regexps_and_boxed_primitives() {
37    assert_eq!(run(r#"
38        const key={}, map=new Map(), set=new Set();
39        map.set(key,set);map.set(map,key);set.add(key);set.add(map);
40        const date=new Date(123456), re=/a+/giy;re.lastIndex=9;
41        for(const value of [map,set,date,re,new Number(NaN),new Boolean(false),new String('hi'),Object(9n)])value.extra=()=>{};
42        const result=structuredClone({map,set,key,date,re,boxed:[new Number(NaN),new Boolean(false),new String('hi'),Object(9n)]});
43        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');
44        if(result.date.getTime()!==123456 || result.re.source!=='a+' || result.re.flags!=='giy' || result.re.lastIndex!==0)throw Error('Date/RegExp');
45        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');
46        if(!Number.isNaN(structuredClone(new Date(NaN)).getTime()))throw Error('Invalid Date');
47        console.log('builtins');
48    "#,&[]).stdout,b"builtins\n");
49}
50
51#[test]
52fn buffers_and_views_copy_bytes_and_preserve_shared_graph_backing() {
53    assert_eq!(run(r#"
54        const buffer=new ArrayBuffer(64);new Uint8Array(buffer).set([1,2,3,4]);
55        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)];
56        if(typeof Float16Array==='function')views.push(new Float16Array(buffer,2,3));
57        const result=structuredClone({buffer,views,again:views[0]});
58        if(result.buffer===buffer || result.again!==result.views[0] || buffer.detached)throw Error('Buffer identity');
59        for(let i=0;i<views.length;i++){
60            const a=views[i],b=result.views[i];
61            if(b.buffer!==result.buffer || b.byteOffset!==a.byteOffset || b.byteLength!==a.byteLength || Object.getPrototypeOf(b)!==Object.getPrototypeOf(a))throw Error('View');
62        }
63        new Uint8Array(buffer).fill(0);
64        if(new Uint8Array(result.buffer).slice(0,4).join()!=='1,2,3,4')throw Error('Aliasing');
65        console.log('views');
66    "#,&[]).stdout,b"views\n");
67}
68
69#[test]
70fn transfers_move_buffers_and_detach_even_unreferenced_entries() {
71    assert_eq!(run(r#"
72        const buffer=new ArrayBuffer(4), extra=new ArrayBuffer(2), view=new Uint8Array(buffer);view.set([1,2,3,4]);
73        const result=structuredClone({buffer,view,again:buffer},{transfer:new Set([buffer,extra])});
74        if(!buffer.detached || !extra.detached || view.byteLength!==0 || result.buffer===buffer || result.again!==result.buffer || result.view.buffer!==result.buffer || result.view.join()!=='1,2,3,4')throw Error('Transfer');
75        const empty=new ArrayBuffer(0), moved=structuredClone(empty,{transfer:[empty]});
76        if(!empty.detached || moved.detached || moved.byteLength!==0)throw Error('Empty transfer');
77        console.log('transfers');
78    "#,&[]).stdout,b"transfers\n");
79}
80
81#[test]
82fn options_and_transfer_lists_follow_webidl_conversion_order() {
83    assert_eq!(run(r#"
84        for(const options of [1,'x',true,Symbol()]){
85            try{structuredClone({},options);throw Error('Dictionary');}catch(e){if(!(e instanceof TypeError))throw e;}
86        }
87        for(const transfer of [null,1,'x',{},[null],[1],[Symbol()]]){
88            try{structuredClone({},{transfer});throw Error('Sequence');}catch(e){if(!(e instanceof TypeError))throw e;}
89        }
90        try{structuredClone();throw Error('Missing value');}catch(e){if(!(e instanceof TypeError))throw e;}
91        if(structuredClone(undefined,null)!==undefined)throw Error('Null dictionary');
92        const log=[], buffer=new ArrayBuffer(2);
93        const list={get [Symbol.iterator](){log.push('iterator');return function(){log.push('call');let n=0;return {get next(){log.push('next');return function(){log.push('step');return n++ ? {done:true} : {done:false,value:buffer};}}};};}};
94        const source={get x(){log.push('value');return 1;}};
95        structuredClone(source,{get transfer(){log.push('transfer');return list;}});
96        if(log.join()!=='transfer,iterator,call,next,step,step,value' || !buffer.detached)throw Error('Order '+log);
97        const reason={}, bad={get transfer(){throw reason;}};
98        try{structuredClone({},bad);throw Error('Getter');}catch(e){if(e!==reason)throw e;}
99        console.log('conversions');
100    "#,&[]).stdout,b"conversions\n");
101}
102
103#[test]
104fn clone_failures_are_dom_exceptions_and_do_not_detach_before_serialization() {
105    assert_eq!(run(r#"
106        function fails(value,options){try{structuredClone(value,options);throw Error('Accepted');}catch(e){if(!(e instanceof DOMException) || e.name!=='DataCloneError' || e.code!==25)throw e;}}
107        for(const value of [()=>{},Symbol(),Object(Symbol()),new WeakMap(),new WeakSet(),new WeakRef({}),new Promise(()=>{}),new Proxy({},{}),new Proxy([],{}),[()=>{}],new Map([[()=>{},1]])])fails(value);
108        const buffer=new ArrayBuffer(4);
109        fails({fn:()=>{}},{transfer:[buffer]});
110        fails({},{transfer:[buffer,buffer]});
111        fails({},{transfer:[buffer,{}]});
112        fails({},{transfer:[new SharedArrayBuffer(1)]});
113        if(buffer.detached)throw Error('Early detach');
114        const detached=new ArrayBuffer(1), view=new Uint8Array(detached), data=new DataView(detached);detached.transfer();
115        for(const value of [detached,view,data])fails(value);
116        fails({},{transfer:[detached]});
117        const beforeDetached={};
118        try{structuredClone({get x(){throw beforeDetached;}},{transfer:[detached]});throw Error('Early detached check');}catch(e){if(e!==beforeDetached)throw e;}
119        const frozenStream=new ReadableStream(), frozenBuffer=new ArrayBuffer(1);
120        Object.freeze(frozenStream);
121        fails({stream:frozenStream},{transfer:[frozenBuffer,frozenStream]});
122        if(frozenBuffer.detached || frozenStream.locked)throw Error('Frozen stream transfer changed sources');
123        const reason={exact:true};
124        try{structuredClone({get x(){throw reason;}},{transfer:[buffer]});throw Error('Getter');}catch(e){if(e!==reason)throw e;}
125        if(buffer.detached || structuredClone({x:1}).x!==1)throw Error('Failed state');
126        console.log('failures');
127    "#,&[]).stdout,b"failures\n");
128}
129
130#[test]
131fn getters_can_resize_or_detach_transfers_and_fail_in_list_order() {
132    assert_eq!(run(r#"
133        const first=new ArrayBuffer(2), second=new ArrayBuffer(3);
134        try{structuredClone({get x(){second.transfer();return 1;}},{transfer:[first,second]});throw Error('Detached late buffer');}catch(e){if(e.name!=='DataCloneError')throw e;}
135        if(!first.detached || !second.detached)throw Error('Transfer order');
136        const a=new ArrayBuffer(2), b=new ArrayBuffer(3);
137        try{structuredClone({get x(){a.transfer();return 1;}},{transfer:[a,b]});throw Error('Detached first');}catch(e){if(e.name!=='DataCloneError')throw e;}
138        if(!a.detached || b.detached)throw Error('Failure order');
139        const rab=new ArrayBuffer(2,{maxByteLength:8});new Uint8Array(rab).set([3,4]);
140        const result=structuredClone({rab,get x(){rab.resize(4);return 1;}},{transfer:[rab]});
141        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');
142        const memory=new WebAssembly.Memory({initial:1});
143        try{structuredClone(memory.buffer,{transfer:[memory.buffer]});throw Error('Wasm buffer transfer');}catch(e){if(!(e instanceof TypeError))throw e;}
144        const shrunk=new ArrayBuffer(4,{maxByteLength:8}), fixed=new Uint8Array(shrunk,1,3);
145        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;}
146        if(!shrunk.detached || structuredClone(3)!==3)throw Error('Deserialization failure state');
147        console.log('late transfers');
148    "#,&[]).stdout,b"late transfers\n");
149}
150
151#[test]
152fn resizable_and_shared_buffers_keep_their_native_semantics() {
153    assert_eq!(run(r#"
154        const buffer=new ArrayBuffer(4,{maxByteLength:12}), tracking=new Uint8Array(buffer), fixed=new Uint8Array(buffer,1,2);
155        tracking.set([1,2,3,4]);const result=structuredClone({buffer,tracking,fixed});
156        if(!result.buffer.resizable || result.buffer.maxByteLength!==12 || result.tracking.buffer!==result.buffer || result.fixed.buffer!==result.buffer)throw Error('Resizable');
157        result.buffer.resize(8);
158        if(result.tracking.length!==8 || result.fixed.length!==2 || buffer.byteLength!==4)throw Error('Tracking');
159        const shared=new SharedArrayBuffer(4,{maxByteLength:12}), view=new Uint8Array(shared);view[0]=5;
160        const copy=structuredClone({shared,view,again:shared});
161        if(copy.shared===shared || copy.shared!==copy.again || copy.view.buffer!==copy.shared || !copy.shared.growable || copy.shared.maxByteLength!==12)throw Error('Shared graph');
162        copy.view[0]=9;copy.shared.grow(8);
163        if(view[0]!==9 || shared.byteLength!==8 || copy.view.length!==8 || view.length!==8)throw Error('Shared memory');
164        console.log('native buffers');
165    "#,&[]).stdout,b"native buffers\n");
166}
167
168#[test]
169fn errors_dom_exceptions_and_webassembly_modules_clone() {
170    assert_eq!(run(r#"
171        for(const C of [Error,EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError]){
172            const error=new C('message',{cause:{x:1}});error.extra=()=>{};
173            const result=structuredClone(error);
174            if(!(result instanceof C) || result.message!=='message' || result.cause.x!==1 || result.cause===error.cause || 'extra' in result)throw Error('Error');
175        }
176        const e=new Error('cyclic');e.cause=e;const c=structuredClone(e);if(c.cause!==c)throw Error('Error cycle');
177        const source=new DOMException('message','DataCloneError');source.extra=()=>{};
178        const stack=source.stack;
179        Object.defineProperty(source,'name',{get(){throw Error('Own name getter');}});
180        const d=structuredClone(source);
181        if(!Error.isError(d) || d.stack!==stack || DOMException.DATA_CLONE_ERR!==25 || d.DATA_CLONE_ERR!==25)throw Error('DOMException error data');
182        if(!(d instanceof DOMException) || d.name!=='DataCloneError' || d.code!==25 || d.message!=='message' || 'extra' in d)throw Error('DOMException');
183        const module=new WebAssembly.Module(new Uint8Array([0,97,115,109,1,0,0,0]));
184        const modules=structuredClone([module,module]);
185        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');
186        console.log('errors and wasm');
187    "#,&[]).stdout,b"errors and wasm\n");
188}
189
190#[test]
191fn blobs_and_files_clone_private_immutable_storage_and_metadata() {
192    assert_eq!(run(r#"
193        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});
194        bytes.fill(0);blob.extra=()=>{};file.extra=()=>{};
195        Object.defineProperty(file,'name',{get(){throw Error('Public metadata');}});
196        Object.setPrototypeOf(blob,null);
197        const result=structuredClone({blob,file,again:file,map:new Map([[file,blob]])});
198        if(!(result.blob instanceof Blob) || result.blob===blob || result.blob.type!=='text/plain' || result.blob.size!==3)throw Error('Blob');
199        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');
200        if('extra' in result.blob || 'extra' in result.file)throw Error('Expandos');
201        (await result.file.bytes()).fill(0);
202        if((await result.blob.bytes()).join()!=='1,2,3' || (await File.prototype.bytes.call(file)).join()!=='1,2,3')throw Error('Mutable clone');
203        const mime=await new Response('x',{headers:{'Content-Type':'Text/HTML; Charset=UTF-8'}}).blob();
204        if(structuredClone(mime).type!=='text/html;charset=UTF-8')throw Error('MIME case');
205        let rope=new Blob(['x']);for(let i=0;i<50;i++)rope=new Blob([rope,rope]);
206        const copied=structuredClone(rope);if(copied.size!==2**50 || await copied.slice(-1).text()!=='x')throw Error('Flattened rope');
207        console.log('blob clones');
208    "#,&[]).stdout,b"blob clones\n");
209}
210
211#[test]
212fn platform_brands_reject_cloning_without_reading_public_properties() {
213    assert_eq!(run(r#"
214        let readableController, byteController, writableController, transformController;
215        const readable=new ReadableStream({start(c){readableController=c;}}), bytes=new ReadableStream({type:'bytes',start(c){byteController=c;}});
216        const writable=new WritableStream({start(c){writableController=c;}}), transform=new TransformStream({start(c){transformController=c;}});
217        const reader=readable.getReader(), byob=bytes.getReader({mode:'byob'}), writer=writable.getWriter();
218        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];
219        for(const value of values){
220            Object.defineProperty(value,'trap',{enumerable:true,get(){throw Error('Read a platform expando');}});
221            Object.setPrototypeOf(value,null);
222            try{structuredClone(value);throw Error('Cloned platform object');}catch(e){if(e.name!=='DataCloneError')throw e;}
223        }
224        for(const stream of [readable,writable]){
225            const buffer=new ArrayBuffer(1);let getter=false;
226            try{structuredClone({get value(){getter=true;return 1;}}, {transfer:[buffer,stream]});throw Error('Transferred locked stream');}catch(e){if(e.name!=='DataCloneError')throw e;}
227            if(buffer.detached||getter)throw Error('Invalid transfer was not rejected first');
228        }
229        const transferBuffer=new ArrayBuffer(1), movedTransform=structuredClone(transform,{transfer:[transferBuffer,transform]});
230        if(!transferBuffer.detached || !(movedTransform instanceof TransformStream)
231            || !(movedTransform.readable instanceof ReadableStream)
232            || !(movedTransform.writable instanceof WritableStream))throw Error('Transform transfer');
233        const fake=Object.create(Blob.prototype);fake.value=1;
234        if(structuredClone(fake).value!==1)throw Error('Prototype mistaken for brand');
235        console.log('brands');
236    "#,&[]).stdout,b"brands\n");
237}
238
239#[test]
240fn cloning_is_permissionless_and_native_helpers_remain_private() {
241    assert_eq!(run(r#"
242        import {pledge} from 'avril:std';pledge({net:false,read:false,jsExec:false});
243        const buffer=new Uint8Array([1,2]).buffer;
244        if(structuredClone(buffer,{transfer:[buffer]}).byteLength!==2 || !buffer.detached)throw Error('Permissionless clone');
245        if(await structuredClone(new Blob(['x'])).text()!=='x')throw Error('Permissionless Blob');
246        for(const name of ['ext:core/structured_clone','ext:web/serialization.js','ext:structured_clone/main.js']){
247            try{await import(name);throw Error('Private import');}catch(e){if(!e.message.includes('private'))throw e;}
248        }
249        const opsSymbol=Symbol.for('__avril.ops');
250        if(opsSymbol in globalThis || globalThis[opsSymbol]!==undefined || Reflect.ownKeys(globalThis).includes(opsSymbol))throw Error('Public native op registry');
251        console.log('private and permissionless');
252    "#,&[]).stdout,b"private and permissionless\n");
253}
254
255#[test]
256fn private_cloning_uses_primordials_not_guest_constructors_or_prototype_hooks() {
257    assert_eq!(run(r#"
258        const blob=new Blob(['x']), file=new File([blob],'name',{lastModified:7}), exception=new DOMException('message','DataCloneError');
259        const buffer=new Uint8Array([1,2,3]).buffer, originalDOMException=DOMException;
260        const define=Object.defineProperty, getDescriptor=Object.getOwnPropertyDescriptor, getPrototype=Object.getPrototypeOf, setPrototype=Object.setPrototypeOf;
261        const restore=[];
262        const poison=()=>{throw Error('Guest hook invoked');};
263        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']]){
264            restore.push([object,key,getDescriptor(object,key)]);
265        }
266        for(let i=0;i<restore.length;i++)define(restore[i][0],restore[i][1],{value:poison,writable:true,configurable:true});
267        const oldSuper=getPrototype(originalDOMException);setPrototype(originalDOMException,poison);
268        define(Array.prototype,'0',{set:poison,configurable:true});
269        define(blob,'size',{get:poison});define(file,'name',{get:poison});
270        globalThis.DOMException=poison;
271        let used=false;
272        const transfer={ [Symbol.iterator](){return {next(){if(used)return {done:true};used=true;return {done:false,value:buffer};}};} };
273        const result=structuredClone({blob,file,exception,buffer},{transfer});
274        let failure;
275        try{structuredClone(()=>{});}catch(e){failure=e;}
276        delete Array.prototype[0];
277        for(let i=0;i<restore.length;i++)define(restore[i][0],restore[i][1],restore[i][2]);
278        setPrototype(originalDOMException,oldSuper);globalThis.DOMException=originalDOMException;
279        if(!buffer.detached || new Uint8Array(result.buffer).join()!=='1,2,3' || result.file.name!=='name' || result.file.lastModified!==7 || await result.blob.text()!=='x' || result.exception.message!=='message')throw Error('Tampered clone');
280        if(!(failure instanceof originalDOMException) || failure.name!=='DataCloneError' || failure.code!==25 || !Error.isError(failure))throw Error('Tampered clone error');
281        console.log('primordials');
282    "#,&[]).stdout,b"primordials\n");
283}