mod common; use common::run; #[test] fn blob_parts_are_snapshots_and_every_byte_result_is_independent() { let output = run( r#" const bytes = new Uint8Array([0,1,2,3,4]); const b = new Blob([bytes.subarray(1,3), new DataView(bytes.buffer,3,1), new Blob(['x']), '\ud800']); bytes.fill(9); if (b.size !== 7 || (await b.bytes()).join() !== '1,2,3,120,239,191,189') throw Error('Parts'); const a = await b.arrayBuffer(), c = await b.arrayBuffer(), d = await b.bytes(); if (a === c || a.byteLength !== b.size) throw Error('Buffer alias'); new Uint8Array(a).fill(0); d.fill(0); if ((await b.bytes()).join() !== '1,2,3,120,239,191,189') throw Error('Mutable Blob'); if (new Blob().size || new Blob(undefined,null).type) throw Error('Empty Blob'); if (await new Blob(new Uint8Array([1,2,3])).text() !== '123') throw Error('Sequence'); console.log('snapshots'); "#, &[], ); assert_eq!(output.stdout, b"snapshots\n"); } #[test] fn blob_snapshotting_follows_conversion_and_handles_detached_buffers() { let output = run( r#" for (const view of ['buffer','typed','data']) { const a = new ArrayBuffer(2), v = view === 'buffer' ? a : view === 'typed' ? new Uint8Array(a) : new DataView(a); const b = new Blob([v,{toString(){a.transfer();return 'x';}}]); if (await b.text() !== 'x') throw Error('Detached source'); } const a = new Uint8Array([1]); const b = new Blob([a],{get type(){a[0]=9;return 'a';}}); if ((await b.bytes())[0] !== 9) throw Error('Snapshot too early'); const c = new ArrayBuffer(3); if (new Blob([c],{get endings(){c.transfer();return 'transparent';}}).size) throw Error('Detached options'); for (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))]) { try {new Blob([value]);throw Error('Unsupported buffer');} catch(e){if (!(e instanceof TypeError)) throw e;} } if (await new Blob([new SharedArrayBuffer(1)]).text() !== '[object SharedArrayBuffer]') throw Error('Shared union'); console.log('buffers'); "#, &[], ); assert_eq!(output.stdout, b"buffers\n"); } #[test] fn blob_and_file_conversion_order_and_iterator_protocol_are_webidl() { let output = run( r#" const log=[]; const sequence={ 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'); if(n++)return {done:true}; return {get done(){log.push('done');return false;},get value(){ log.push('value');return {toString(){log.push('part');return 'a';}}; }}; }; }}; }; } }; const opts={get endings(){log.push('endings');return 'transparent';},get type(){log.push('type');return 'A';},get lastModified(){log.push('modified');return 5;}}; const file=new File(sequence,{toString(){log.push('name');return 'x';}},opts); if (log.join() !== 'iterator,call,next,step,done,value,part,step,name,endings,type,modified') throw Error(log.join()); let closed=false; const bad={ [Symbol.iterator](){return {next(){return {done:false,value:Symbol()};},return(){closed=true;return {};}};}}; try {new Blob(bad);throw Error('Bad part');}catch(e){if (!(e instanceof TypeError))throw e;} if (closed) throw Error('Unexpected IteratorClose'); for (const value of [null,'abc',1,true,1n,Symbol()]) { try {new Blob(value);throw Error('Bad sequence');}catch(e){if (!(e instanceof TypeError))throw e;} } for (const opts of [true,1,'x',{endings:null},{endings:'NATIVE'}]) { try {new Blob([],opts);throw Error('Bad options');}catch(e){if (!(e instanceof TypeError))throw e;} } try {new File();throw Error('Missing File args');}catch(e){if (!(e instanceof TypeError))throw e;} if (await new Blob([Object.create(Blob.prototype)]).text() !== '[object Blob]') throw Error('False Blob brand'); console.log(file.name,file.type,file.lastModified); "#, &[], ); assert_eq!(output.stdout, b"x a 5\n"); } #[test] fn blob_slice_clamps_rounds_even_and_returns_a_base_blob() { let output = run( r#" class Sub extends Blob {} const b = new Sub(['0123456789'],{type:'Text/Plain'}); for (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,'']]) { const s=b.slice(start,end); if (await s.text() !== expected || s.type || Object.getPrototypeOf(s) !== Blob.prototype) throw Error('Slice '+start+','+end); } const s=b.slice(2,8,' SOME TYPE '); if (await s.slice(1,-1).text() !== '3456' || s.type !== ' some type ') throw Error('Nested slice'); if (b.slice(0,1,null).type !== 'null' || b.slice(0,1,'a\x7f').type) throw Error('Slice type'); if (Blob.length !== 0 || File.length !== 2 || Blob.prototype.slice.length !== 0) throw Error('Lengths'); let converted=false; try {Blob.prototype.slice.call({}, {valueOf(){converted=true;return 0;}});}catch(e){if (!(e instanceof TypeError)) throw e;} if (converted) throw Error('Brand conversion order'); for (const method of ['bytes','arrayBuffer','text']) { try {await Blob.prototype[method].call({});throw Error('Invalid receiver');}catch(e){if (!(e instanceof TypeError))throw e;} } console.log('slice'); "#, &[], ); assert_eq!(output.stdout, b"slice\n"); } #[test] fn blob_slice_keeps_numeric_coercion_order_and_rejects_bigints() { let output = run( r#" const blob = new Blob(['012345']); const log = []; const slice = blob.slice( {valueOf() {log.push('start'); return 1.5;}}, {valueOf() {log.push('end'); return 4.5;}}, {toString() {log.push('type'); return 'TEXT/PLAIN';}}, ); if (log.join() !== 'start,end,type' || await slice.text() !== '23' || slice.type !== 'text/plain') throw Error('Slice conversions'); for (const start of [1n, {valueOf() {return 1n;}}]) { let converted = false; try { blob.slice(start, {valueOf() {converted = true; return 5;}}); throw Error('Accepted BigInt start'); } catch (error) { if (!(error instanceof TypeError)) throw error; } if (converted) throw Error('Converted end after invalid start'); } try { blob.slice(0, 1n); throw Error('Accepted BigInt end'); } catch (error) { if (!(error instanceof TypeError)) throw error; } console.log('slice coercion'); "#, &[], ); assert_eq!(output.stdout, b"slice coercion\n"); } #[test] fn file_inherits_blob_and_converts_name_timestamp_type_and_endings() { let source = r#" const before=Date.now(), f=new File(['hello'],'a/\ud800',{type:' TeXT/Plain '}), after=Date.now(); if (!(f instanceof Blob) || !(f instanceof File) || f.name !== 'a/\ufffd' || f.type !== ' text/plain ' || f.lastModifiedafter) throw Error('File'); if (Object.prototype.toString.call(f) !== '[object File]' || Object.prototype.toString.call(f.slice()) !== '[object Blob]') throw Error('Tags'); for (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]]) { if (!Object.is(new File([],'x',{lastModified:input}).lastModified,expected)) throw Error('Timestamp '+input); } try {new File([],'x',{lastModified:1n});throw Error('BigInt timestamp');}catch(e){if (!(e instanceof TypeError))throw e;} const b=new Blob(['a\r\nb\rc\n\r',new Uint8Array([13,10])],{endings:'native'}); if (await b.text() !== NATIVE_EXPECTED) throw Error('Native endings'); if (new Blob([],{type:'a\n'}).type || new Blob([],{type:'\u212a'}).type) throw Error('Type range'); Object.setPrototypeOf(File,function(){throw Error('Poisoned super');}); if (await new File(['ok'],'x').text() !== 'ok') throw Error('Super'); console.log('file'); "#.replace("NATIVE_EXPECTED", if cfg!(windows) { r#"'a\r\nb\r\nc\r\n\r\n\r\n'"# } else { r#"'a\nb\nc\n\n\r\n'"# }); let output = run(&source, &[]); assert_eq!(output.stdout, b"file\n"); } #[test] fn blob_streams_are_independent_byte_streams_with_byob_eof_and_text_decoding() { let output = run( r#" const bytes=new Uint8Array(65540);bytes.fill(65);bytes.set([0xf0,0x9f,0x98,0x80],65534); const b=new Blob([bytes]); const r=b.stream().getReader({mode:'byob'}), collected=[]; while(true){const v=await r.read(new Uint8Array(10000));if(v.done)break;collected.push(...v.value);} r.releaseLock(); if (collected.length !== b.size || collected[65534] !== 0xf0) throw Error('BYOB bytes'); const text=b.textStream().getReader();let decoded=''; while(true){const result=await text.read();if(result.done)break;decoded+=result.value;} if (decoded !== await b.text() || !decoded.includes('😀')) throw Error('Stream decoding'); const empty=await new Blob().stream().getReader({mode:'byob'}).read(new Uint8Array(8)); if (!empty.done || empty.value.byteLength) throw Error('Empty BYOB'); const tail=await b.slice(-2).stream().getReader({mode:'byob'}).read(new Uint8Array(20)); if (tail.value.join() !== '65,65' || (await b.bytes())[65534] !== 0xf0) throw Error('Detached Blob backing'); if (await new Blob([new Uint8Array([239,187,191,65,255])]).text() !== 'A\ufffd') throw Error('UTF8'); const cancel=b.stream().getReader();await cancel.cancel(); if (b.size !== 65540) throw Error('Stream cancellation'); console.log('streams'); "#, &[], ); assert_eq!(output.stdout, b"streams\n"); } #[test] fn blob_ropes_share_repeated_parts_without_exponential_expansion() { let output = run( r#" let b=new Blob(['x']);for(let i=0;i<40;i++)b=new Blob([b,b]); if (b.size !== 2**40 || await b.slice(-3).text() !== 'xxx') throw Error('Rope'); const reader=b.stream().getReader();if((await reader.read()).value.byteLength !== 65536)throw Error('Huge stream');await reader.cancel(); for(let i=0;i<14;i++) { try {b=new Blob([b,b]);}catch(e){if (!(e instanceof RangeError) || b.size !== 2**52) throw e;break;} } let c=new Blob(['a']);for(let i=0;i<2000;i++)c=new Blob([c,'b']); if (await c.slice(1998).text() !== 'bbb') throw Error('Deep rope'); console.log('ropes'); "#, &[], ); assert_eq!(output.stdout, b"ropes\n"); } #[test] fn blob_internal_bindings_and_immutable_storage_survive_guest_poisoning() { let output = run( r#" const b=new Blob(['abc']), slice=b.slice(1), U8=Uint8Array; Object.defineProperty(Object.prototype,'start',{get(){throw Error('Prototype source leak');},configurable:true}); Object.defineProperty(Object.prototype,'then',{get(){if(this instanceof U8)this.fill(90);return undefined;},configurable:true}); Blob.prototype.stream=null;Blob.prototype.bytes=null; ReadableStream.prototype.getReader=null; globalThis.TextEncoder=null;globalThis.TextDecoder=null;globalThis.Uint8Array=null; String.fromCharCode=()=>{throw Error('Poisoned character conversion');}; if(await b.text() !== 'abc' || await slice.text() !== 'bc')throw Error('Blob mutation'); const response=new Response('abc'); const result=await response.blob(); if (await result.text() !== 'abc' || result.type !== 'text/plain;charset=UTF-8')throw Error('Consume storage leak'); const fromBlob=new Response(b);if(await fromBlob.text() !== 'abc')throw Error('Public Blob override'); const f=new File(['abc'],'file',{type:'TEXT/PLAIN'}); if(f.type !== 'text/plain')throw Error('Type capture'); console.log('isolated'); "#, &[], ); assert_eq!(output.stdout, b"isolated\n"); } #[test] fn fetch_blob_bodies_and_consumption_keep_types_used_flags_and_copies() { let output = run( r#" const b=new Blob(['hello'],{type:'Text/Plain'}), f=new File(['file'],'f',{type:'TEXT/HTML'}); const r=new Response(b), clone=r.clone(); if(r.headers.get('content-type') !== 'text/plain' || new Response(new Blob()).headers.has('content-type'))throw Error('Blob content type'); const result=await r.blob(); if(!r.bodyUsed || await result.text() !== 'hello' || result.type !== 'text/plain' || await clone.text() !== 'hello' || await b.text() !== 'hello')throw Error('Blob body'); try {await r.blob();throw Error('Used body');}catch(e){if(!(e instanceof TypeError))throw e;} const req=new Request('https://example.com',{method:'POST',body:f}); if(req.headers.get('content-type') !== 'text/html' || await req.text() !== 'file')throw Error('File body'); const empty=await new Response(null,{headers:{'content-type':'TEXT/HTML; charset=GBK'}}).blob(); if(empty.size || empty.type !== 'text/html;charset=GBK')throw Error('MIME case'); let controller; const pending=new Response(new ReadableStream({start(c){controller=c;}}),{headers:{'content-type':'text/plain'}}); const promise=pending.blob();pending.headers.set('content-type','text/html');controller.enqueue(new Uint8Array([65]));controller.close(); if((await promise).type !== 'text/html')throw Error('MIME timing'); console.log('fetch blobs'); "#, &[], ); assert_eq!(output.stdout, b"fetch blobs\n"); } #[test] fn fetch_blob_mime_types_parse_serialize_and_extract_header_lists() { let output = run( r#" const rows=[ ['not a mime',''],['*/*',''],['Text/HTML; Charset=GBK','text/html;charset=GBK'], ['text/html;charset=gbk;a=b, text/html;x=y','text/html;x=y;charset=gbk'], ['text/html;charset=a, text/html;charset=b, text/html','text/html;charset=a'], ['text/html;charset="shift_jis"iso-2022-jp','text/html;charset=shift_jis'], ['text/plain;x="";y=','text/plain;x=""'], ['text/plain;b=1;1=2;__proto__=3;b=4','text/plain;b=1;1=2;__proto__=3'], ['text/plain; x =y;z=hello world','text/plain;z="hello world"'], ['text/plain;x="a,b\\\"c"','text/plain;x="a,b\\\"c"'], ['text/plain;x="\u00e9"','text/plain;x="\u00e9"'], ['text/plain;charset=a, invalid, application/json','application/json'] ]; for(const [value,expected] of rows){ const blob=await new Response('x',{headers:{'content-type':value}}).blob(); if(blob.type !== expected)throw Error(value+' => '+blob.type+' expected '+expected); } console.log('mime'); "#, &[], ); assert_eq!(output.stdout, b"mime\n"); } #[test] fn omitted_blob_dictionaries_and_text_stream_options_ignore_prototypes() { let output = run( r#" Object.prototype.type='x/evil';Object.prototype.lastModified=7;Object.prototype.endings='bogus'; const b=new Blob(['abc']), file=new File([],'file'); if(b.type || file.type || file.lastModified===7)throw Error('Default dictionary inherited properties'); delete Object.prototype.type;delete Object.prototype.lastModified;delete Object.prototype.endings; Object.prototype.preventClose=true;Object.prototype.signal=AbortSignal.abort(); Object.defineProperty(TextDecoderStream.prototype,'readable',{get(){throw Error('Public transform getter');}}); Object.defineProperty(TextDecoderStream.prototype,'writable',{get(){throw Error('Public transform getter');}}); const reader=b.textStream().getReader(); const first=await reader.read(), last=await reader.read(); if(first.value !== 'abc' || !last.done)throw Error('Poisoned pipeThrough options'); console.log('defaults'); "#, &[], ); assert_eq!(output.stdout, b"defaults\n"); } #[test] fn empty_content_type_headers_are_not_replaced_by_blob_or_text_defaults() { let output = run( r#" const bodies=['text',new Blob(['bytes'],{type:'text/plain'}),new File(['file'],'name',{type:'text/html'})]; for(const body of bodies){ const r=new Response(body,{headers:{'content-type':''}}), req=new Request('https://example.com',{method:'POST',body,headers:{'content-type':''}}); if(r.headers.get('content-type') !== '' || req.headers.get('content-type') !== '')throw Error('Empty header replaced'); if((await r.blob()).type || (await req.blob()).type)throw Error('Empty header MIME'); } if(Response.json({},{headers:{'content-type':''}}).headers.get('content-type') !== '')throw Error('JSON default'); console.log('empty types'); "#, &[], ); assert_eq!(output.stdout, b"empty types\n"); } #[test] fn blob_and_file_are_permissionless_and_their_modules_remain_private() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("main.js"); std::fs::write(&path,r#" import {pledge} from 'avril:std'; pledge({net:false,read:false,jsExec:false}); const file=new File(['bytes'],'not/a/filesystem/path'); if(await new Response(file).text() !== 'bytes')throw Error('Permissionless File'); for(const name of ['ext:blob/main.js','ext:blob/blob.js','ext:blob/file.js','ext:blob/platform.js','ext:http_types/mime.js']){ try {await import(name);throw Error('Private source leaked');}catch(e){if(!e.message.includes('private'))throw e;} } console.log('private blobs'); "#).unwrap(); let child = std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .args(["-0", path.to_str().unwrap()]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(); let output = common::wait(child); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"private blobs\n"); } #[test] fn consuming_blob_bodies_does_not_adopt_internal_promises_through_public_then() { let output = run( r#" const r=new Response('body'); Promise.prototype.then=function(){throw Error('Public promise hook');}; const b=await r.blob(); if(await b.text() !== 'body' || await new Response(b).text() !== 'body')throw Error('Poisoned consumption'); try {await Response.prototype.blob.call({});throw Error('Illegal invocation');}catch(e){if(!(e instanceof TypeError))throw e;} console.log('promises'); "#, &[], ); assert_eq!(output.stdout, b"promises\n"); }