char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
6d9bf5c
main
1mod common; 2 3use common:: run; 4 5# [ test ] 6fn blob_parts_are_snapshots_and_every_byte_result_is_independent () { 7let output =run ( 8r#" 9const bytes = new Uint8Array([0,1,2,3,4]); 10const b = new Blob([bytes.subarray(1,3), new DataView(bytes.buffer,3,1), new Blob(['x']), '\ud800']); 11bytes.fill(9); 12if (b.size !== 7 || (await b.bytes()).join() !== '1,2,3,120,239,191,189') throw Error('Parts'); 13const a = await b.arrayBuffer(), c = await b.arrayBuffer(), d = await b.bytes(); 14if (a === c || a.byteLength !== b.size) throw Error('Buffer alias'); 15new Uint8Array(a).fill(0); d.fill(0); 16if ((await b.bytes()).join() !== '1,2,3,120,239,191,189') throw Error('Mutable Blob'); 17if (new Blob().size || new Blob(undefined,null).type) throw Error('Empty Blob'); 18if (await new Blob(new Uint8Array([1,2,3])).text() !== '123') throw Error('Sequence'); 19console.log('snapshots'); 20"# , 21& [], 22); 23assert_eq! ( output. stdout , b"snapshots\n" ); 24} 25 26# [ test ] 27fn blob_snapshotting_follows_conversion_and_handles_detached_buffers () { 28let output =run ( 29r#" 30for (const view of ['buffer','typed','data']) { 31const a = new ArrayBuffer(2), v = view === 'buffer' ? a : view === 'typed' ? new Uint8Array(a) : new DataView(a); 32const b = new Blob([v,{toString(){a.transfer();return 'x';}}]); 33if (await b.text() !== 'x') throw Error('Detached source'); 34} 35const a = new Uint8Array([1]); 36const b = new Blob([a],{get type(){a[0]=9;return 'a';}}); 37if ((await b.bytes())[0] !== 9) throw Error('Snapshot too early'); 38const c = new ArrayBuffer(3); 39if (new Blob([c],{get endings(){c.transfer();return 'transparent';}}).size) throw Error('Detached options'); 40for (const value of [new ArrayBuffer(2,{maxByteLength:4}), new Uint8Array(new ArrayBuffer(2,{maxByteLength:4})), new Uint8Array(new SharedArrayBuffer(2)), new DataView(new SharedArrayBuffer(2))]) { 41try {new Blob([value]);throw Error('Unsupported buffer');} catch(e){if (!(e instanceof TypeError)) throw e;} 42} 43if (await new Blob([new SharedArrayBuffer(1)]).text() !== '[object SharedArrayBuffer]') throw Error('Shared union'); 44console.log('buffers'); 45"# , 46& [], 47); 48assert_eq! ( output. stdout , b"buffers\n" ); 49} 50 51# [ test ] 52fn blob_and_file_conversion_order_and_iterator_protocol_are_webidl () { 53let output =run ( 54r#" 55const log=[]; 56const sequence={ 57get [Symbol.iterator](){ 58log.push('iterator'); 59return function(){ 60log.push('call');let n=0; 61return {get next(){ 62log.push('next'); 63return function(){ 64log.push('step'); 65if(n++)return {done:true}; 66return {get done(){log.push('done');return false;},get value(){ 67log.push('value');return {toString(){log.push('part');return 'a';}}; 68}}; 69}; 70}}; 71}; 72} 73}; 74const opts={get endings(){log.push('endings');return 'transparent';},get type(){log.push('type');return 'A';},get lastModified(){log.push('modified');return 5;}}; 75const file=new File(sequence,{toString(){log.push('name');return 'x';}},opts); 76if (log.join() !== 'iterator,call,next,step,done,value,part,step,name,endings,type,modified') throw Error(log.join()); 77let closed=false; 78const bad={ [Symbol.iterator](){return {next(){return {done:false,value:Symbol()};},return(){closed=true;return {};}};}}; 79try {new Blob(bad);throw Error('Bad part');}catch(e){if (!(e instanceof TypeError))throw e;} 80if (closed) throw Error('Unexpected IteratorClose'); 81for (const value of [null,'abc',1,true,1n,Symbol()]) { 82try {new Blob(value);throw Error('Bad sequence');}catch(e){if (!(e instanceof TypeError))throw e;} 83} 84for (const opts of [true,1,'x',{endings:null},{endings:'NATIVE'}]) { 85try {new Blob([],opts);throw Error('Bad options');}catch(e){if (!(e instanceof TypeError))throw e;} 86} 87try {new File();throw Error('Missing File args');}catch(e){if (!(e instanceof TypeError))throw e;} 88if (await new Blob([Object.create(Blob.prototype)]).text() !== '[object Blob]') throw Error('False Blob brand'); 89console.log(file.name,file.type,file.lastModified); 90"# , 91& [], 92); 93assert_eq! ( output. stdout , b"x a 5\n" ); 94} 95 96# [ test ] 97fn blob_slice_clamps_rounds_even_and_returns_a_base_blob () { 98let output =run ( 99r#" 100class Sub extends Blob {} 101const b = new Sub(['0123456789'],{type:'Text/Plain'}); 102for (const [start,end,expected] of [[0.5,2.5,'01'],[1.5,3.5,'23'],[-1.5,undefined,'89'],[Infinity,undefined,''],[-Infinity,Infinity,'0123456789'],[0,null,''],[NaN,3,'012'],[8,2,'']]) { 103const s=b.slice(start,end); 104if (await s.text() !== expected || s.type || Object.getPrototypeOf(s) !== Blob.prototype) throw Error('Slice '+start+','+end); 105} 106const s=b.slice(2,8,' SOME TYPE '); 107if (await s.slice(1,-1).text() !== '3456' || s.type !== ' some type ') throw Error('Nested slice'); 108if (b.slice(0,1,null).type !== 'null' || b.slice(0,1,'a\x7f').type) throw Error('Slice type'); 109if (Blob.length !== 0 || File.length !== 2 || Blob.prototype.slice.length !== 0) throw Error('Lengths'); 110let converted=false; 111try {Blob.prototype.slice.call({}, {valueOf(){converted=true;return 0;}});}catch(e){if (!(e instanceof TypeError)) throw e;} 112if (converted) throw Error('Brand conversion order'); 113for (const method of ['bytes','arrayBuffer','text']) { 114try {await Blob.prototype[method].call({});throw Error('Invalid receiver');}catch(e){if (!(e instanceof TypeError))throw e;} 115} 116console.log('slice'); 117"# , 118& [], 119); 120assert_eq! ( output. stdout , b"slice\n" ); 121} 122 123# [ test ] 124fn blob_slice_keeps_numeric_coercion_order_and_rejects_bigints () { 125let output =run ( 126r#" 127const blob = new Blob(['012345']); 128const log = []; 129const slice = blob.slice( 130{valueOf() {log.push('start'); return 1.5;}}, 131{valueOf() {log.push('end'); return 4.5;}}, 132{toString() {log.push('type'); return 'TEXT/PLAIN';}}, 133); 134if (log.join() !== 'start,end,type' || await slice.text() !== '23' || 135slice.type !== 'text/plain') throw Error('Slice conversions'); 136for (const start of [1n, {valueOf() {return 1n;}}]) { 137let converted = false; 138try { 139blob.slice(start, {valueOf() {converted = true; return 5;}}); 140throw Error('Accepted BigInt start'); 141} catch (error) { 142if (!(error instanceof TypeError)) throw error; 143} 144if (converted) throw Error('Converted end after invalid start'); 145} 146try { blob.slice(0, 1n); throw Error('Accepted BigInt end'); } 147catch (error) { if (!(error instanceof TypeError)) throw error; } 148console.log('slice coercion'); 149"# , 150& [], 151); 152assert_eq! ( output. stdout , b"slice coercion\n" ); 153} 154 155# [ test ] 156fn file_inherits_blob_and_converts_name_timestamp_type_and_endings () { 157let source =r#" 158const before=Date.now(), f=new File(['hello'],'a/\ud800',{type:' TeXT/Plain '}), after=Date.now(); 159if (!(f instanceof Blob) || !(f instanceof File) || f.name !== 'a/\ufffd' || f.type !== ' text/plain ' || f.lastModified<before || f.lastModified>after) throw Error('File'); 160if (Object.prototype.toString.call(f) !== '[object File]' || Object.prototype.toString.call(f.slice()) !== '[object Blob]') throw Error('Tags'); 161for (const [input,expected] of [[-1,-1],[1.9,1],[-0.5,0],[NaN,0],[Infinity,0],[2**63,-(2**63)],[2**64,0],[2**64+4096,4096],[-(2**63)-4096,2**63-4096],['42',42],[new Date(5),5]]) { 162if (!Object.is(new File([],'x',{lastModified:input}).lastModified,expected)) throw Error('Timestamp '+input); 163} 164try {new File([],'x',{lastModified:1n});throw Error('BigInt timestamp');}catch(e){if (!(e instanceof TypeError))throw e;} 165const b=new Blob(['a\r\nb\rc\n\r',new Uint8Array([13,10])],{endings:'native'}); 166if (await b.text() !== NATIVE_EXPECTED) throw Error('Native endings'); 167if (new Blob([],{type:'a\n'}).type || new Blob([],{type:'\u212a'}).type) throw Error('Type range'); 168Object.setPrototypeOf(File,function(){throw Error('Poisoned super');}); 169if (await new File(['ok'],'x').text() !== 'ok') throw Error('Super'); 170console.log('file'); 171"# . replace ( "NATIVE_EXPECTED" , if cfg! ( windows) { 172r#"'a\r\nb\r\nc\r\n\r\n\r\n'"# 173} else { 174r#"'a\nb\nc\n\n\r\n'"# 175}); 176let output =run ( & source, & []); 177assert_eq! ( output. stdout , b"file\n" ); 178} 179 180# [ test ] 181fn blob_streams_are_independent_byte_streams_with_byob_eof_and_text_decoding () { 182let output =run ( 183r#" 184const bytes=new Uint8Array(65540);bytes.fill(65);bytes.set([0xf0,0x9f,0x98,0x80],65534); 185const b=new Blob([bytes]); 186const r=b.stream().getReader({mode:'byob'}), collected=[]; 187while(true){const v=await r.read(new Uint8Array(10000));if(v.done)break;collected.push(...v.value);} 188r.releaseLock(); 189if (collected.length !== b.size || collected[65534] !== 0xf0) throw Error('BYOB bytes'); 190const text=b.textStream().getReader();let decoded=''; 191while(true){const result=await text.read();if(result.done)break;decoded+=result.value;} 192if (decoded !== await b.text() || !decoded.includes('😀')) throw Error('Stream decoding'); 193const empty=await new Blob().stream().getReader({mode:'byob'}).read(new Uint8Array(8)); 194if (!empty.done || empty.value.byteLength) throw Error('Empty BYOB'); 195const tail=await b.slice(-2).stream().getReader({mode:'byob'}).read(new Uint8Array(20)); 196if (tail.value.join() !== '65,65' || (await b.bytes())[65534] !== 0xf0) throw Error('Detached Blob backing'); 197if (await new Blob([new Uint8Array([239,187,191,65,255])]).text() !== 'A\ufffd') throw Error('UTF8'); 198const cancel=b.stream().getReader();await cancel.cancel(); 199if (b.size !== 65540) throw Error('Stream cancellation'); 200console.log('streams'); 201"# , 202& [], 203); 204assert_eq! ( output. stdout , b"streams\n" ); 205} 206 207# [ test ] 208fn blob_ropes_share_repeated_parts_without_exponential_expansion () { 209let output =run ( 210r#" 211let b=new Blob(['x']);for(let i=0;i<40;i++)b=new Blob([b,b]); 212if (b.size !== 2**40 || await b.slice(-3).text() !== 'xxx') throw Error('Rope'); 213const reader=b.stream().getReader();if((await reader.read()).value.byteLength !== 65536)throw Error('Huge stream');await reader.cancel(); 214for(let i=0;i<14;i++) { 215try {b=new Blob([b,b]);}catch(e){if (!(e instanceof RangeError) || b.size !== 2**52) throw e;break;} 216} 217let c=new Blob(['a']);for(let i=0;i<2000;i++)c=new Blob([c,'b']); 218if (await c.slice(1998).text() !== 'bbb') throw Error('Deep rope'); 219console.log('ropes'); 220"# , 221& [], 222); 223assert_eq! ( output. stdout , b"ropes\n" ); 224} 225 226# [ test ] 227fn blob_internal_bindings_and_immutable_storage_survive_guest_poisoning () { 228let output =run ( 229r#" 230const b=new Blob(['abc']), slice=b.slice(1), U8=Uint8Array; 231Object.defineProperty(Object.prototype,'start',{get(){throw Error('Prototype source leak');},configurable:true}); 232Object.defineProperty(Object.prototype,'then',{get(){if(this instanceof U8)this.fill(90);return undefined;},configurable:true}); 233Blob.prototype.stream=null;Blob.prototype.bytes=null; 234ReadableStream.prototype.getReader=null; 235globalThis.TextEncoder=null;globalThis.TextDecoder=null;globalThis.Uint8Array=null; 236String.fromCharCode=()=>{throw Error('Poisoned character conversion');}; 237if(await b.text() !== 'abc' || await slice.text() !== 'bc')throw Error('Blob mutation'); 238const response=new Response('abc'); 239const result=await response.blob(); 240if (await result.text() !== 'abc' || result.type !== 'text/plain;charset=UTF-8')throw Error('Consume storage leak'); 241const fromBlob=new Response(b);if(await fromBlob.text() !== 'abc')throw Error('Public Blob override'); 242const f=new File(['abc'],'file',{type:'TEXT/PLAIN'}); 243if(f.type !== 'text/plain')throw Error('Type capture'); 244console.log('isolated'); 245"# , 246& [], 247); 248assert_eq! ( output. stdout , b"isolated\n" ); 249} 250 251# [ test ] 252fn fetch_blob_bodies_and_consumption_keep_types_used_flags_and_copies () { 253let output =run ( 254r#" 255const b=new Blob(['hello'],{type:'Text/Plain'}), f=new File(['file'],'f',{type:'TEXT/HTML'}); 256const r=new Response(b), clone=r.clone(); 257if(r.headers.get('content-type') !== 'text/plain' || new Response(new Blob()).headers.has('content-type'))throw Error('Blob content type'); 258const result=await r.blob(); 259if(!r.bodyUsed || await result.text() !== 'hello' || result.type !== 'text/plain' || await clone.text() !== 'hello' || await b.text() !== 'hello')throw Error('Blob body'); 260try {await r.blob();throw Error('Used body');}catch(e){if(!(e instanceof TypeError))throw e;} 261const req=new Request('https://example.com',{method:'POST',body:f}); 262if(req.headers.get('content-type') !== 'text/html' || await req.text() !== 'file')throw Error('File body'); 263const empty=await new Response(null,{headers:{'content-type':'TEXT/HTML; charset=GBK'}}).blob(); 264if(empty.size || empty.type !== 'text/html;charset=GBK')throw Error('MIME case'); 265let controller; 266const pending=new Response(new ReadableStream({start(c){controller=c;}}),{headers:{'content-type':'text/plain'}}); 267const promise=pending.blob();pending.headers.set('content-type','text/html');controller.enqueue(new Uint8Array([65]));controller.close(); 268if((await promise).type !== 'text/html')throw Error('MIME timing'); 269console.log('fetch blobs'); 270"# , 271& [], 272); 273assert_eq! ( output. stdout , b"fetch blobs\n" ); 274} 275 276# [ test ] 277fn fetch_blob_mime_types_parse_serialize_and_extract_header_lists () { 278let output =run ( 279r#" 280const rows=[ 281['not a mime',''],['*/*',''],['Text/HTML; Charset=GBK','text/html;charset=GBK'], 282['text/html;charset=gbk;a=b, text/html;x=y','text/html;x=y;charset=gbk'], 283['text/html;charset=a, text/html;charset=b, text/html','text/html;charset=a'], 284['text/html;charset="shift_jis"iso-2022-jp','text/html;charset=shift_jis'], 285['text/plain;x="";y=','text/plain;x=""'], 286['text/plain;b=1;1=2;__proto__=3;b=4','text/plain;b=1;1=2;__proto__=3'], 287['text/plain; x =y;z=hello world','text/plain;z="hello world"'], 288['text/plain;x="a,b\\\"c"','text/plain;x="a,b\\\"c"'], 289['text/plain;x="\u00e9"','text/plain;x="\u00e9"'], 290['text/plain;charset=a, invalid, application/json','application/json'] 291]; 292for(const [value,expected] of rows){ 293const blob=await new Response('x',{headers:{'content-type':value}}).blob(); 294if(blob.type !== expected)throw Error(value+' => '+blob.type+' expected '+expected); 295} 296console.log('mime'); 297"# , 298& [], 299); 300assert_eq! ( output. stdout , b"mime\n" ); 301} 302 303# [ test ] 304fn omitted_blob_dictionaries_and_text_stream_options_ignore_prototypes () { 305let output =run ( 306r#" 307Object.prototype.type='x/evil';Object.prototype.lastModified=7;Object.prototype.endings='bogus'; 308const b=new Blob(['abc']), file=new File([],'file'); 309if(b.type || file.type || file.lastModified===7)throw Error('Default dictionary inherited properties'); 310delete Object.prototype.type;delete Object.prototype.lastModified;delete Object.prototype.endings; 311Object.prototype.preventClose=true;Object.prototype.signal=AbortSignal.abort(); 312Object.defineProperty(TextDecoderStream.prototype,'readable',{get(){throw Error('Public transform getter');}}); 313Object.defineProperty(TextDecoderStream.prototype,'writable',{get(){throw Error('Public transform getter');}}); 314const reader=b.textStream().getReader(); 315const first=await reader.read(), last=await reader.read(); 316if(first.value !== 'abc' || !last.done)throw Error('Poisoned pipeThrough options'); 317console.log('defaults'); 318"# , 319& [], 320); 321assert_eq! ( output. stdout , b"defaults\n" ); 322} 323 324# [ test ] 325fn empty_content_type_headers_are_not_replaced_by_blob_or_text_defaults () { 326let output =run ( 327r#" 328const bodies=['text',new Blob(['bytes'],{type:'text/plain'}),new File(['file'],'name',{type:'text/html'})]; 329for(const body of bodies){ 330const r=new Response(body,{headers:{'content-type':''}}), req=new Request('https://example.com',{method:'POST',body,headers:{'content-type':''}}); 331if(r.headers.get('content-type') !== '' || req.headers.get('content-type') !== '')throw Error('Empty header replaced'); 332if((await r.blob()).type || (await req.blob()).type)throw Error('Empty header MIME'); 333} 334if(Response.json({},{headers:{'content-type':''}}).headers.get('content-type') !== '')throw Error('JSON default'); 335console.log('empty types'); 336"# , 337& [], 338); 339assert_eq! ( output. stdout , b"empty types\n" ); 340} 341 342# [ test ] 343fn blob_and_file_are_permissionless_and_their_modules_remain_private () { 344let dir = tempfile:: tempdir (). unwrap (); 345let path = dir. path (). join ( "main.js" ); 346 std:: fs:: write ( & path, r#" 347import {pledge} from 'avril:std'; 348pledge({net:false,read:false,jsExec:false}); 349const file=new File(['bytes'],'not/a/filesystem/path'); 350if(await new Response(file).text() !== 'bytes')throw Error('Permissionless File'); 351for(const name of ['ext:blob/main.js','ext:blob/blob.js','ext:blob/file.js','ext:blob/platform.js','ext:http_types/mime.js']){ 352try {await import(name);throw Error('Private source leaked');}catch(e){if(!e.message.includes('private'))throw e;} 353} 354console.log('private blobs'); 355"# ). unwrap (); 356let child = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 357. arg ( "run" ) 358. args ([ "-0" , path. to_str (). unwrap ()]) 359. stdout ( std:: process:: Stdio :: piped ()) 360. stderr ( std:: process:: Stdio :: piped ()) 361. spawn () 362. unwrap (); 363let output = common:: wait ( child); 364assert! ( 365 output. status . success (), 366"{}" , 367String :: from_utf8_lossy ( & output. stderr ) 368); 369assert_eq! ( output. stdout , b"private blobs\n" ); 370} 371 372# [ test ] 373fn consuming_blob_bodies_does_not_adopt_internal_promises_through_public_then () { 374let output =run ( 375r#" 376const r=new Response('body'); 377Promise.prototype.then=function(){throw Error('Public promise hook');}; 378const b=await r.blob(); 379if(await b.text() !== 'body' || await new Response(b).text() !== 'body')throw Error('Poisoned consumption'); 380try {await Response.prototype.blob.call({});throw Error('Illegal invocation');}catch(e){if(!(e instanceof TypeError))throw e;} 381console.log('promises'); 382"# , 383& [], 384); 385assert_eq! ( output. stdout , b"promises\n" ); 386}