char/avril

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

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

Charlotte Somport encoding extension to typescriptf2e5e0e

main
15.1 KiB229 linesraw
1mod common;
2
3#[test]
4fn utf8_encoding_and_encode_into_respect_code_points_and_view_bounds() {
5    let out = common::run(
6        r#"
7        const encoder=new TextEncoder();if(encoder.encoding!=='utf-8')throw Error('Encoding');
8        console.log([...encoder.encode('Aé😀\ud800')].join(','));
9        if(encoder.encode().length!==0||new TextDecoder().decode(encoder.encode(null))!=='null')throw Error('Defaults');
10        const source='Aé😀\ud800Z';
11        for(let capacity=0;capacity<=12;capacity++) {
12            const storage=new Uint8Array(capacity+2).fill(99), dest=storage.subarray(1,capacity+1);
13            const result=encoder.encodeInto(source,dest);
14            if(storage[0]!==99||storage[capacity+1]!==99||result.written>capacity)throw Error('Bounds');
15            const expected=encoder.encode(source.slice(0,result.read));
16            if(expected.length!==result.written||expected.some((b,i)=>b!==dest[i]))throw Error('Counts');
17            if(result.read===3)throw Error('Split scalar');
18        }
19        for (const source of ['\0', '\ud800', '\udc00', '\ud800\ud800', '\udc00\ud800', 'é\ud800😀\0z']) {
20            for (let capacity = 0; capacity < 20; capacity++) {
21                const expected = encoder.encode(source);
22                const storage = new Uint8Array(capacity + 4).fill(99);
23                const destination = storage.subarray(2, capacity + 2);
24                const {read, written} = encoder.encodeInto(source, destination);
25                if (written !== encoder.encode(source.slice(0, read)).length ||
26                    expected.slice(0, written).some((byte, i) => byte !== destination[i]) ||
27                    storage.slice(0, 2).some(byte => byte !== 99) ||
28                    storage.slice(written + 2).some(byte => byte !== 99)) throw Error('UTF8 prefix bounds');
29                if (read < source.length && encoder.encode(String.fromCodePoint(source.codePointAt(read))).length <= capacity - written) {
30                    throw Error('Stopped before a complete scalar');
31                }
32            }
33        }
34        const view=new Uint8Array(new SharedArrayBuffer(4));const result=encoder.encodeInto('😀x',view);
35        if(result.read!==2||result.written!==4||[...view].join(',')!=='240,159,152,128')throw Error('Shared destination');
36        for(const bad of [new Uint16Array(4),new DataView(new ArrayBuffer(4)),[],null]) {try{encoder.encodeInto('x',bad);throw Error('Accepted destination');}catch(e){if(!(e instanceof TypeError))throw e;}}
37        try{encoder.encode(Symbol());throw Error('Symbol source');}catch(e){if(!(e instanceof TypeError))throw e;}
38    "#,
39        &[],
40    );
41    assert_eq!(out.stdout, b"65,195,169,240,159,152,128,239,191,189\n");
42}
43
44#[test]
45fn decoding_labels_legacy_encodings_and_fatal_errors() {
46    let out = common::run(
47        r#"
48        const cases=[
49            [' UTF-8\t','utf-8',[195,169],'é'],
50            ['unicode-1-1-utf-8','utf-8',[240,159,152,128],'😀'],
51            ['ASCII','windows-1252',[128,233],'ێ'],
52            ['latin1','windows-1252',[128],'€'],
53            ['shift_jis','shift_jis',[130,160],'あ'],
54            ['euc-jp','euc-jp',[164,162],'あ'],
55            ['gb18030','gb18030',[196,227],'你'],
56            ['big5','big5',[167,65],'你'],
57            ['koi8-r','koi8-r',[225],'А'],
58            ['utf-16','utf-16le',[65,0,61,216,0,222],'A😀'],
59            ['utf-16be','utf-16be',[0,65,216,61,222,0],'A😀'],
60            ['x-user-defined','x-user-defined',[128],'\uf780'],
61        ];
62        for(const [label,encoding,input,text]of cases) {const d=new TextDecoder(label);if(d.encoding!==encoding||d.decode(new Uint8Array(input))!==text)throw Error(label);}
63        for(const label of ['bogus','replacement','iso-2022-cn','iso-2022-kr','utf-7','utf\u00a0-8']) {try{new TextDecoder(label);throw Error('Accepted label');}catch(e){if(!(e instanceof RangeError))throw e;}}
64        const malformed=new Uint8Array([240,128,128,65,255]);if(new TextDecoder().decode(malformed)!=='���A�')throw Error('Replacement');
65        const fatal=new TextDecoder('utf-8',{fatal:true});try{fatal.decode(malformed);throw Error('Fatal accepted');}catch(e){if(!(e instanceof TypeError))throw e;}
66        if(fatal.decode(new Uint8Array([65]))!=='A'||!fatal.fatal||fatal.ignoreBOM)throw Error('Reset');
67        console.log('labels');
68    "#,
69        &[],
70    );
71    assert_eq!(out.stdout, b"labels\n");
72}
73
74#[test]
75fn streaming_decode_preserves_partial_sequences_and_bom_state() {
76    let out = common::run(
77        r#"
78        const d=new TextDecoder();const pieces=[[239],[187],[191,240],[159],[152,128,239],[187,191]];
79        let text='';for(const piece of pieces)text+=d.decode(new Uint8Array(piece),{stream:true});text+=d.decode();
80        if(text!=='😀\ufeff')throw Error('UTF8 streaming/BOM');
81        if(d.decode(new Uint8Array([239,187,191,65]))!=='A')throw Error('BOM reset');
82        if(new TextDecoder('utf-8',{ignoreBOM:true}).decode(new Uint8Array([239,187,191,65]))!=='\ufeffA')throw Error('ignoreBOM');
83        if(new TextDecoder('windows-1252').decode(new Uint8Array([239,187,191]))!=='')throw Error('Legacy BOM');
84        const utf16=new TextDecoder('utf-16be');let result='';for(const byte of [254,255,216,61,222,0])result+=utf16.decode(new Uint8Array([byte]),{stream:true});result+=utf16.decode();if(result!=='😀')throw Error('UTF16 streaming');
85        const incomplete=new TextDecoder();if(incomplete.decode(new Uint8Array([226]),{stream:true})!==''||incomplete.decode()!=='�'||incomplete.decode()!=='')throw Error('Flush/reset');
86        const fatal=new TextDecoder('utf-8',{fatal:true});fatal.decode(new Uint8Array([226]),{stream:true});try{fatal.decode();throw Error('Incomplete fatal');}catch(e){if(!(e instanceof TypeError))throw e;}if(fatal.decode(new Uint8Array([65]))!=='A')throw Error('Fatal reset');
87        const recovering=new TextDecoder('utf-8',{fatal:true});try{recovering.decode(new Uint8Array([255,65]),{stream:true});throw Error('Malformed stream');}catch(e){if(!(e instanceof TypeError))throw e;}if(recovering.decode()!=='')throw Error('Fatal chunk recovery');
88        const shift=new TextDecoder('iso-2022-jp');let shifted='';for(const byte of [27,36,66,36,34,27,40,66,65])shifted+=shift.decode(new Uint8Array([byte]),{stream:true});shifted+=shift.decode();if(shifted!=='あA')throw Error('Stateful encoding');
89        console.log('streaming');
90    "#,
91        &[],
92    );
93    assert_eq!(out.stdout, b"streaming\n");
94}
95
96#[test]
97fn buffer_sources_include_shared_resizable_detached_and_offset_views() {
98    let out = common::run(
99        r#"
100        const d=new TextDecoder(), encoder=new TextEncoder();
101        const ab=new Uint8Array([0,65,66,0]).buffer;if(d.decode(new DataView(ab,1,2))!=='AB'||d.decode(new Uint8Array(ab,1,2))!=='AB')throw Error('View range');
102        const shared=new SharedArrayBuffer(2);new Uint8Array(shared).set([65,66]);if(d.decode(shared)!=='AB'||d.decode(new DataView(shared))!=='AB')throw Error('Shared');
103        const rab=new ArrayBuffer(4,{maxByteLength:8});new Uint8Array(rab).set([65,66,67,68]);const fixed=new Uint8Array(rab,0,4);rab.resize(2);
104        for(const operation of [()=>d.decode(fixed),()=>encoder.encodeInto('x',fixed)]){try{operation();throw Error('Resizable view');}catch(e){if(!(e instanceof TypeError))throw e;}}
105        for(const value of [rab,new DataView(rab),new Uint8Array(rab),new SharedArrayBuffer(2,{maxByteLength:8})]) {try{d.decode(value);throw Error('Resizable accepted');}catch(e){if(!(e instanceof TypeError))throw e;}}
106        const detached=new ArrayBuffer(4), typed=new Uint8Array(detached), dataView=new DataView(detached);detached.transfer();
107        for(const value of [detached,typed,dataView])if(d.decode(value)!=='')throw Error('Detached source');
108        const zero=encoder.encodeInto('x',typed);if(zero.read!==0||zero.written!==0)throw Error('Detached destination');
109        const transferred=new Uint8Array([65]);if(d.decode(transferred,{get stream(){transferred.buffer.transfer();return false;}})!=='')throw Error('Detached in conversion');
110        for(const bad of [null,{},[65],'A',1]) {try{d.decode(bad);throw Error('Invalid BufferSource');}catch(e){if(!(e instanceof TypeError))throw e;}}
111        console.log('buffers');
112    "#,
113        &[],
114    );
115    assert_eq!(out.stdout, b"buffers\n");
116}
117
118#[test]
119fn conversions_branding_and_captured_methods_follow_web_interfaces() {
120    let out = common::run(
121        r#"
122        const decoder=new TextDecoder(), bytes=new Uint8Array([65]);
123        if(decoder.decode(bytes,{get stream(){bytes[0]=66;return false;}})!=='B')throw Error('Option conversion/copy order');
124        const log=[];new TextDecoder({toString(){log.push('label');return 'utf-8';}},{get fatal(){log.push('fatal');return false;},get ignoreBOM(){log.push('bom');return false;}});if(log.join(',')!=='label,fatal,bom')throw Error('Conversion order');
125        for(const call of [()=>TextEncoder(),()=>TextDecoder(),()=>TextEncoder.prototype.encode.call({}),()=>TextDecoder.prototype.decode.call({}),()=>new TextDecoder(undefined,1),()=>decoder.decode(undefined,true)]) {try{call();throw Error('Invalid invocation');}catch(e){if(!(e instanceof TypeError))throw e;}}
126        if(Object.prototype.toString.call(decoder)!=='[object TextDecoder]'||!Object.getOwnPropertyDescriptor(TextDecoder.prototype,'decode').enumerable)throw Error('Interface');
127        const encode=new TextEncoder(), decode=new TextDecoder();const poison=()=>{throw Error('Poisoned');};
128        Uint8Array.prototype.set=poison;ArrayBuffer.isView=poison;String.prototype.toWellFormed=poison;Object.getOwnPropertyDescriptor=poison;
129        if(decode.decode(encode.encode('hello'))!=='hello')throw Error('Captured functions');const dest=new Uint8Array(3);if(encode.encodeInto('éx',dest).written!==3)throw Error('Captured set');
130        console.log('interfaces');
131    "#,
132        &[],
133    );
134    assert_eq!(out.stdout, b"interfaces\n");
135}
136
137#[test]
138fn encoding_transform_streams_handle_split_scalars_and_backpressure() {
139    let out = common::run(
140        r#"
141        const encoder=new TextEncoderStream(), decoder=new TextDecoderStream();
142        if(encoder.encoding!=='utf-8'||decoder.encoding!=='utf-8'||decoder.fatal||decoder.ignoreBOM)throw Error('Stream properties');
143        const received=[];const consumer=(async()=>{for await(const text of encoder.readable.pipeThrough(decoder))received.push(text);})();
144        const writer=encoder.writable.getWriter();await writer.write('A\ud83d');await writer.write('');await writer.write('\ude00B\ud800');await writer.close();await consumer;
145        if(received.join('')!=='A😀B�')throw Error('Surrogate carry');
146        const legacy=new TextDecoderStream('shift_jis');let text='';const collecting=(async()=>{for await(const chunk of legacy.readable)text+=chunk;})();const output=legacy.writable.getWriter();await output.write(new Uint8Array([130]));await output.write(new Uint8Array([160]));await output.close();await collecting;if(text!=='あ')throw Error('Legacy transform');
147        console.log('transforms');
148    "#,
149        &[],
150    );
151    assert_eq!(out.stdout, b"transforms\n");
152}
153
154#[test]
155fn stream_wrappers_keep_captured_accessors_and_controller_methods() {
156    let out = common::run(
157        r#"
158        const prototype = TransformStream.prototype;
159        const readable = Object.getOwnPropertyDescriptor(prototype, 'readable');
160        const writable = Object.getOwnPropertyDescriptor(prototype, 'writable');
161        const controller = TransformStreamDefaultController.prototype;
162        const enqueue = Object.getOwnPropertyDescriptor(controller, 'enqueue');
163        const poison = () => { throw Error('Poisoned stream hook'); };
164        try {
165            Object.defineProperty(prototype, 'readable', {get: poison, configurable: true});
166            Object.defineProperty(prototype, 'writable', {get: poison, configurable: true});
167            Object.defineProperty(controller, 'enqueue', {value: poison, configurable: true});
168            const encoder = new TextEncoderStream(), decoder = new TextDecoderStream();
169            const chunks = [];
170            const consumed = (async () => {
171                for await (const chunk of encoder.readable.pipeThrough(decoder)) chunks.push(chunk);
172            })();
173            const writer = encoder.writable.getWriter();
174            await writer.write('A\ud83d');
175            await writer.write('\ude00B');
176            await writer.close();
177            await consumed;
178            if (chunks.join('') !== 'A😀B') throw Error('Captured stream pipeline');
179            if (TextEncoder.length !== 0 || TextDecoder.length !== 0 ||
180                TextEncoderStream.length !== 0 || TextDecoderStream.length !== 0 ||
181                TextEncoder.prototype.encode.length !== 0 ||
182                TextEncoder.prototype.encodeInto.length !== 2 ||
183                TextDecoder.prototype.decode.length !== 0) throw Error('Callable arity');
184        } finally {
185            Object.defineProperty(prototype, 'readable', readable);
186            Object.defineProperty(prototype, 'writable', writable);
187            Object.defineProperty(controller, 'enqueue', enqueue);
188        }
189        console.log('captured streams');
190    "#,
191        &[],
192    );
193    assert_eq!(out.stdout, b"captured streams\n");
194}
195
196#[test]
197fn fatal_decoder_stream_errors_both_sides_without_unhandled_rejections() {
198    let out = common::run(
199        r#"
200        const stream=new TextDecoderStream('utf-8',{fatal:true});const reader=stream.readable.getReader(),writer=stream.writable.getWriter();
201        const reading=reader.read().then(()=>{throw Error('Expected read error');},e=>{if(!(e instanceof TypeError))throw e;});
202        try{await writer.write(new Uint8Array([255]));throw Error('Expected write error');}catch(e){if(!(e instanceof TypeError))throw e;}
203        await reading;
204        const bad=new TextDecoderStream(),r=bad.readable.getReader(),w=bad.writable.getWriter();const rejected=r.read().catch(e=>{if(!(e instanceof TypeError))throw e;});try{await w.write(undefined);throw Error('Invalid stream chunk');}catch(e){if(!(e instanceof TypeError))throw e;}await rejected;
205        console.log('errors');
206    "#,
207        &[],
208    );
209    assert_eq!(out.stdout, b"errors\n");
210}
211
212#[test]
213fn default_options_and_internal_transformers_ignore_prototype_pollution() {
214    let out = common::run(
215        r#"
216        Object.prototype.fatal=true;Object.prototype.ignoreBOM=true;Object.prototype.stream=true;
217        Object.prototype.start=c=>c.enqueue('injected');Object.prototype.readableType='bytes';Object.prototype.highWaterMark=-1;
218        const decoder=new TextDecoder();if(decoder.fatal||decoder.ignoreBOM||decoder.decode(new Uint8Array([226,130]))!=='�')throw Error('Default dictionary');
219        const pipeline=new TextDecoderStream(),encoder=new TextEncoderStream();
220        if(pipeline.fatal||pipeline.ignoreBOM)throw Error('Stream defaults');
221        delete Object.prototype.highWaterMark;
222        const result=[];const reading=(async()=>{for await(const chunk of pipeline.readable)result.push(chunk);})();
223        const writer=pipeline.writable.getWriter();await writer.write(new Uint8Array([65]));await writer.close();await reading;if(result.join('')!=='A')throw Error('Inherited transformer hook');
224        console.log('defaults');
225    "#,
226        &[],
227    );
228    assert_eq!(out.stdout, b"defaults\n");
229}