char/avril

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

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

Charlotte Somport Blob/File extension to typescript6d9bf5c

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