char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
ec742cf
main
1mod common; 2 3use common:: run; 4 5# [ test ] 6fn compression_formats_round_trip_incrementally_and_match_external_fixtures () { 7let output =run ( 8r#" 9function equal(a, b) { return a.length === b.length && a.every((value, i) => value === b[i]); } 10async function collect(stream) { 11const reader = stream.getReader(), chunks = []; 12let total = 0; 13for (;;) { 14const {value, done} = await reader.read(); 15if (done) break; 16if (!(value instanceof Uint8Array)) throw Error('Output chunk type'); 17const copy = new Uint8Array(value); 18chunks.push(copy); 19total += copy.length; 20} 21const result = new Uint8Array(total); 22let offset = 0; 23for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } 24return result; 25} 26async function encode(format, chunks) { 27const stream = new CompressionStream(format), output = collect(stream.readable); 28const writer = stream.writable.getWriter(); 29for (const chunk of chunks) await writer.write(chunk); 30await writer.close(); 31return output; 32} 33async function decode(format, bytes, split = 1) { 34const stream = new DecompressionStream(format), output = collect(stream.readable); 35const writer = stream.writable.getWriter(); 36for (let offset = 0; offset < bytes.length; offset += split) { 37await writer.write(bytes.subarray(offset, Math.min(offset + split, bytes.length))); 38} 39await writer.close(); 40return output; 41} 42const input = new Uint8Array([...new TextEncoder().encode('whatwg compression streaming test\0'), 255]); 43const fixtures = { 44'deflate': '789c2bcf482c294f5748cecf2d284a2d2ececccf53282e294a4dcccdcc4b5728492d2e61f80f00fcbc0e2e', 45'deflate-raw': '2bcf482c294f5748cecf2d284a2d2ececccf53282e294a4dcccdcc4b5728492d2e61f80f00', 46'gzip': '1f8b08000000000002ff2bcf482c294f5748cecf2d284a2d2ececccf53282e294a4dcccdcc4b5728492d2e61f80f0037951d5f23000000', 47}; 48for (const format of Object.keys(fixtures)) { 49const bytes = Uint8Array.from(fixtures[format].match(/../g), pair => parseInt(pair, 16)); 50if (!equal(await decode(format, bytes), input)) throw Error('External ' + format + ' fixture'); 51const encoded = await encode(format, [input.subarray(0, 5), input.subarray(5, 13), input.subarray(13)]); 52if (!equal(await decode(format, encoded, 2), input)) throw Error('Round trip ' + format); 53} 54const brotliFixture = Uint8Array.from('213800046578706563746564206f757470757403'.match(/../g), pair => parseInt(pair, 16)); 55const brotliExpected = new TextEncoder().encode('expected output'); 56for (const format of ['brotli']) { 57if (!equal(await decode(format, brotliFixture), brotliExpected)) throw Error('External ' + format + ' fixture'); 58const encoded = await encode(format, [input.subarray(0, 7), input.subarray(7)]); 59if (!equal(await decode(format, encoded, 3), input)) throw Error('Round trip ' + format); 60const empty = await encode(format, [new Uint8Array(0)]); 61if ((await decode(format, empty)).length !== 0) throw Error('Empty stream ' + format); 62} 63const nativeTransform = globalThis.TransformStream, transformPrototype = nativeTransform.prototype; 64const readableDescriptor = Object.getOwnPropertyDescriptor(transformPrototype, 'readable'); 65const writableDescriptor = Object.getOwnPropertyDescriptor(transformPrototype, 'writable'); 66const enqueueDescriptor = Object.getOwnPropertyDescriptor(TransformStreamDefaultController.prototype, 'enqueue'); 67try { 68Object.defineProperty(transformPrototype, 'readable', {get() { throw Error('Poisoned readable getter'); }, configurable: true}); 69Object.defineProperty(transformPrototype, 'writable', {get() { throw Error('Poisoned writable getter'); }, configurable: true}); 70Object.defineProperty(TransformStreamDefaultController.prototype, 'enqueue', {value() { throw Error('Poisoned enqueue'); }, configurable: true}); 71globalThis.TransformStream = function() { throw Error('Poisoned constructor'); }; 72const poisoned = new CompressionStream('deflate'), poisonedOutput = collect(poisoned.readable); 73const poisonedWriter = poisoned.writable.getWriter(); 74await poisonedWriter.write(new Uint8Array([1, 2, 3])); 75await poisonedWriter.close(); 76if ((await decode('deflate', await poisonedOutput)).join() !== '1,2,3') throw Error('Compression used poisoned stream hooks'); 77} finally { 78Object.defineProperty(transformPrototype, 'readable', readableDescriptor); 79Object.defineProperty(transformPrototype, 'writable', writableDescriptor); 80Object.defineProperty(TransformStreamDefaultController.prototype, 'enqueue', enqueueDescriptor); 81globalThis.TransformStream = nativeTransform; 82} 83const empty = await encode('gzip', [new Uint8Array(0)]); 84if ((await decode('gzip', empty)).length !== 0) throw Error('Empty stream'); 85const backing = new Uint8Array([9, 1, 2, 3, 8]); 86const viewed = await encode('deflate', [new DataView(backing.buffer, 1, 3)]); 87if (!equal(await decode('deflate', viewed), new Uint8Array([1, 2, 3]))) throw Error('BufferSource view bounds'); 88const resizable = new ArrayBuffer(5, {maxByteLength: 10}); 89new Uint8Array(resizable).set([9, 1, 2, 3, 8]); 90const resized = await encode('gzip', [new DataView(resizable, 1, 3)]); 91resizable.resize(0); 92if (!equal(await decode('gzip', resized), new Uint8Array([1, 2, 3]))) { 93throw Error('Resizable BufferSource'); 94} 95const direct = await encode('gzip', [new Uint8Array([4, 5, 6]).buffer]); 96if (!equal(await decode('gzip', direct), new Uint8Array([4, 5, 6]))) throw Error('ArrayBuffer input'); 97const transferable = new CompressionStream('gzip'), sourceReadable = transferable.readable; 98const movedReadable = structuredClone(sourceReadable, {transfer: [sourceReadable]}); 99const transferredOutput = collect(movedReadable), transferredWriter = transferable.writable.getWriter(); 100await transferredWriter.write(input); 101await transferredWriter.close(); 102if (!equal(await decode('gzip', await transferredOutput), input)) throw Error('Transferred compression output'); 103console.log('compression formats'); 104"# , 105& [], 106); 107assert_eq! ( output. stdout , b"compression formats\n" ); 108} 109 110# [ test ] 111fn compression_stream_interfaces_validate_brands_formats_and_buffer_sources () { 112let output =run ( 113r#" 114for (const C of [CompressionStream, DecompressionStream]) { 115try { new C(); throw Error('Missing format'); } catch (error) { if (!(error instanceof TypeError)) throw error; } 116try { new C(Symbol()); throw Error('Symbol format'); } catch (error) { if (!(error instanceof TypeError)) throw error; } 117for (const format of ['GZIP', 'gzip ', 'bzip2', '']) { 118try { new C(format); throw Error('Unsupported format'); } catch (error) { if (!(error instanceof TypeError)) throw error; } 119} 120const descriptor = Object.getOwnPropertyDescriptor(C.prototype, 'readable'); 121if (!descriptor.get || descriptor.set || !descriptor.enumerable) throw Error('Readable descriptor'); 122for (const key of ['readable', 'writable']) { 123const get = Object.getOwnPropertyDescriptor(C.prototype, key).get; 124const other = C === CompressionStream ? DecompressionStream : CompressionStream; 125for (const receiver of [{}, new other('gzip')]) { 126try { get.call(receiver); throw Error('Unbranded getter'); } 127catch (error) { if (!(error instanceof TypeError)) throw error; } 128} 129} 130} 131for (const C of [CompressionStream, DecompressionStream]) { 132for (const value of ['text', 1, null, {}, new SharedArrayBuffer(4), new Uint8Array(new SharedArrayBuffer(4))]) { 133const stream = new C('deflate'); 134const reader = stream.readable.getReader(), writer = stream.writable.getWriter(); 135writer.closed.catch(() => {}); 136const read = reader.read().catch(error => error); 137try { await writer.write(value); throw Error('Accepted non-BufferSource'); } 138catch (error) { if (!(error instanceof TypeError)) throw error; } 139if (!(await read instanceof TypeError)) throw Error('Readable side did not error'); 140} 141} 142const detached = new ArrayBuffer(3); detached.transfer(); 143for (const value of [detached, new Uint8Array(new SharedArrayBuffer(4))]) { 144const stream = new CompressionStream('gzip'); 145const reader = stream.readable.getReader(), writer = stream.writable.getWriter(); 146writer.closed.catch(() => {}); 147const read = reader.read().catch(error => error); 148try { await writer.write(value); throw Error('Accepted invalid buffer'); } 149catch (error) { if (!(error instanceof TypeError)) throw error; } 150if (!(await read instanceof TypeError)) throw Error('Readable side did not error'); 151} 152const aborted = new CompressionStream('gzip'); 153const abortedReader = aborted.readable.getReader(), abortedWriter = aborted.writable.getWriter(); 154const abortedRead = abortedReader.read().catch(error => error); 155abortedReader.closed.catch(() => {}); 156await abortedWriter.abort('abort reason'); 157if (await abortedRead !== 'abort reason') throw Error('Abort did not error the readable side'); 158const canceled = new CompressionStream('gzip'); 159const canceledReader = canceled.readable.getReader(), canceledWriter = canceled.writable.getWriter(); 160canceledWriter.closed.catch(() => {}); 161const canceledWrite = canceledWriter.write(new Uint8Array([1])).catch(error => error); 162await canceledReader.cancel('cancel reason'); 163if (await canceledWrite !== 'cancel reason') throw Error('Cancel did not error the writable side'); 164console.log('compression interfaces'); 165"# , 166& [], 167); 168assert_eq! ( output. stdout , b"compression interfaces\n" ); 169} 170 171# [ test ] 172fn compression_constructor_conversion_can_reenter_without_changing_callable_shape () { 173let output =run ( 174r#" 175for (const C of [CompressionStream, DecompressionStream]) { 176let calls = 0, nested; 177const stream = new C({ 178[Symbol.toPrimitive](hint) { 179if (hint !== 'string') throw Error('Conversion hint'); 180calls++; 181nested = new C('deflate'); 182return 'gzip'; 183}, 184}); 185if (calls !== 1 || C.length !== 1) throw Error('Constructor conversion or arity'); 186for (const key of ['readable', 'writable']) { 187const get = Object.getOwnPropertyDescriptor(C.prototype, key).get; 188if (get.length !== 0 || get.call(stream) !== stream[key]) throw Error('Accessor shape'); 189} 190await stream.readable.cancel(); 191await nested.readable.cancel(); 192} 193console.log('compression conversion'); 194"# , 195& [], 196); 197assert_eq! ( output. stdout , b"compression conversion\n" ); 198} 199 200# [ test ] 201fn compression_wrappers_keep_captured_reader_writer_and_controller_methods () { 202let output =run ( 203r#" 204const hooks = [ 205[ReadableStream.prototype, 'getReader'], 206[WritableStream.prototype, 'getWriter'], 207[ReadableStreamDefaultReader.prototype, 'read'], 208[WritableStreamDefaultWriter.prototype, 'write'], 209[WritableStreamDefaultWriter.prototype, 'close'], 210[ReadableStreamDefaultController.prototype, 'enqueue'], 211[ReadableStreamDefaultController.prototype, 'close'], 212]; 213const descriptors = hooks.map(([prototype, name]) => Object.getOwnPropertyDescriptor(prototype, name)); 214const [getReader, getWriter, read, write, close] = descriptors.map(descriptor => descriptor.value); 215try { 216for (const [prototype, name] of hooks) { 217Object.defineProperty(prototype, name, { 218value() { throw Error('Poisoned stream method: ' + name); }, 219configurable: true, 220}); 221} 222const stream = new CompressionStream('gzip'); 223const reader = getReader.call(stream.readable), writer = getWriter.call(stream.writable); 224const consumed = (async () => { 225const bytes = []; 226for (;;) { 227const result = await read.call(reader); 228if (result.done) return bytes; 229bytes.push(...result.value); 230} 231})(); 232await write.call(writer, new Uint8Array([1, 2, 3])); 233await close.call(writer); 234const bytes = await consumed; 235if (bytes[0] !== 31 || bytes[1] !== 139) throw Error('Missing gzip output'); 236} finally { 237for (let i = 0; i < hooks.length; i++) { 238Object.defineProperty(hooks[i][0], hooks[i][1], descriptors[i]); 239} 240} 241console.log('captured compression methods'); 242"# , 243& [], 244); 245assert_eq! ( output. stdout , b"captured compression methods\n" ); 246} 247 248# [ test ] 249fn decompression_rejects_corruption_truncation_trailing_data_and_members () { 250let output =run ( 251r#" 252const fixtures = { 253'deflate': '789c2bcf482c294f5748cecf2d284a2d2ececccf53282e294a4dcccdcc4b5728492d2e61f80f00fcbc0e2e', 254'deflate-raw': '2bcf482c294f5748cecf2d284a2d2ececccf53282e294a4dcccdcc4b5728492d2e61f80f00', 255'gzip': '1f8b08000000000002ff2bcf482c294f5748cecf2d284a2d2ececccf53282e294a4dcccdcc4b5728492d2e61f80f0037951d5f23000000', 256}; 257const bytesFor = format => Uint8Array.from(fixtures[format].match(/../g), x => parseInt(x, 16)); 258async function fails(format, bytes) { 259const stream = new DecompressionStream(format), writer = stream.writable.getWriter(), reader = stream.readable.getReader(); 260const read = (async () => { 261try { 262for (;;) if ((await reader.read()).done) throw Error('Readable side accepted corrupt input'); 263} catch (error) { return error; } 264})(); 265const writerClosed = writer.closed.then(() => null, error => error); 266const readerClosed = reader.closed.then(() => null, error => error); 267try { 268await writer.write(bytes); 269await writer.close(); 270throw Error('Writable side accepted corrupt input'); 271} catch (error) { 272if (!(error instanceof TypeError)) throw error; 273} 274const reason = await read; 275if (!(reason instanceof TypeError)) throw Error('Readable side did not error'); 276if (!(await writerClosed instanceof TypeError) || !(await readerClosed instanceof TypeError)) { 277throw Error('Stream endpoints did not both error'); 278} 279} 280for (const format of Object.keys(fixtures)) { 281const bytes = bytesFor(format); 282await fails(format, bytes.subarray(0, bytes.length - 1)); 283const trailing = new Uint8Array(bytes.length + 1); trailing.set(bytes); trailing[trailing.length - 1] = 1; 284await fails(format, trailing); 285} 286const brotli = Uint8Array.from('213800046578706563746564206f757470757403'.match(/../g), x => parseInt(x, 16)); 287for (const format of ['brotli']) { 288await fails(format, brotli.subarray(0, brotli.length - 1)); 289const members = new Uint8Array(brotli.length * 2); members.set(brotli); members.set(brotli, brotli.length); 290await fails(format, members); 291} 292const gzip = bytesFor('gzip'), corrupt = new Uint8Array(gzip); 293corrupt[corrupt.length - 8] ^= 1; 294await fails('gzip', corrupt); 295const members = new Uint8Array(gzip.length * 2); members.set(gzip); members.set(gzip, gzip.length); 296await fails('gzip', members); 297console.log('compression errors'); 298"# , 299& [], 300); 301assert_eq! ( output. stdout , b"compression errors\n" ); 302} 303 304# [ test ] 305fn decompression_emits_valid_output_before_rejecting_trailing_data () { 306let output =run ( 307r#" 308const fixtures = { 309'deflate': '78 9c 4b ad 28 48 4d 2e 49 4d 51 c8 2f 2d 29 28 2d 01 00 30 ad 06 24', 310'gzip': '1f 8b 08 00 00 00 00 00 00 03 4b ad 28 48 4d 2e 49 4d 51 c8 2f 2d 29 28 2d 01 00 b0 01 39 b3 0f 00 00 00', 311'deflate-raw': '4b ad 28 48 4d 2e 49 4d 51 c8 2f 2d 29 28 2d 01 00', 312'brotli': '21 38 00 04 65 78 70 65 63 74 65 64 20 6f 75 74 70 75 74 03', 313}; 314const expected = new TextEncoder().encode('expected output'); 315for (const format of Object.keys(fixtures)) { 316const bytes = Uint8Array.from(fixtures[format].trim().split(/\s+/), x => parseInt(x, 16)); 317const input = new Uint8Array(bytes.length + 1); 318input.set(bytes); 319const stream = new DecompressionStream(format); 320const reader = stream.readable.getReader(), writer = stream.writable.getWriter(); 321const write = writer.write(input).then(() => null, error => error); 322const first = await reader.read(); 323if (first.done || first.value.length !== expected.length || !first.value.every((x, i) => x === expected[i])) { 324throw Error(format + ' lost valid output before trailing-data error'); 325} 326const second = await reader.read().then(() => null, error => error); 327if (!(second instanceof TypeError)) throw Error(format + ' did not error after valid output'); 328if (!(await write instanceof TypeError)) throw Error(format + ' writable side did not reject'); 329 330const nextStream = new DecompressionStream(format); 331const nextReader = nextStream.readable.getReader(), nextWriter = nextStream.writable.getWriter(); 332const nextRead = nextReader.read(); 333await nextWriter.write(bytes); 334const decoded = await nextRead; 335if (decoded.done || decoded.value.length !== expected.length || !decoded.value.every((x, i) => x === expected[i])) { 336throw Error(format + ' lost valid output before later trailing data'); 337} 338const terminalRead = nextReader.read().then(() => null, error => error); 339const terminalWrite = nextWriter.write(new Uint8Array([0])).then(() => null, error => error); 340if (!(await terminalWrite instanceof TypeError) || !(await terminalRead instanceof TypeError)) { 341throw Error(format + ' did not reject later trailing data'); 342} 343} 344console.log('trailing input preserves valid output'); 345"# , 346& [], 347); 348assert_eq! ( output. stdout , b"trailing input preserves valid output\n" ); 349} 350 351# [ test ] 352fn compression_streams_emit_large_output_before_writable_close () { 353let output =run ( 354r#" 355const stream = new CompressionStream('deflate-raw'); 356const reader = stream.readable.getReader(), writer = stream.writable.getWriter(); 357const input = new Uint8Array(512 * 1024); 358let seed = 0x9e3779b9; 359for (let i = 0; i < input.length; i++) { 360seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; 361input[i] = seed & 255; 362} 363const firstRead = reader.read(); 364await writer.write(input); 365const first = await Promise.race([ 366firstRead, 367new Promise(resolve => setTimeout(() => resolve(null), 1000)), 368]); 369if (!first || first.done || first.value.length === 0) throw Error('Compression buffered until close'); 370await writer.close(); 371console.log('compression streaming'); 372"# , 373& [], 374); 375assert_eq! ( output. stdout , b"compression streaming\n" ); 376} 377 378# [ test ] 379fn compression_efficiency_does_not_depend_on_small_write_boundaries () { 380let out =run ( 381r#" 382const input=new TextEncoder().encode('hello world, '.repeat(128)); 383async function encode(size){ 384const stream=new CompressionStream('gzip'); 385const reader=stream.readable.getReader(),writer=stream.writable.getWriter(); 386const output=(async()=>{let length=0;for(;;){const result=await reader.read();if(result.done)return length;length+=result.value.length;}})(); 387for(let i=0;i<input.length;i+=size)await writer.write(input.subarray(i,i+size)); 388await writer.close(); 389return output; 390} 391const whole=await encode(input.length),small=await encode(1); 392if(small>whole+64||small>=input.length/4)throw Error('Write boundaries degrade compression'); 393console.log('small chunks compress'); 394"# , 395& [], 396); 397assert_eq! ( out. stdout , b"small chunks compress\n" ); 398}