@@ -3322,6 +3322,341 @@ JSObject *create_rs_proxy(JSContext *cx, HandleObject input_readable) {
3322
3322
}
3323
3323
} // namespace TransformStream
3324
3324
3325
+ /* *
3326
+ * Implementation of the WICG DecompressionStream builtin.
3327
+ *
3328
+ * All algorithm names and steps refer to spec algorithms defined at
3329
+ * https://wicg.github.io/compression/#decompression-stream
3330
+ */
3331
+ namespace DecompressionStream {
3332
+ namespace Slots {
3333
+ enum { Transform, Format, State, Buffer, Count };
3334
+ };
3335
+
3336
+ enum class Format {
3337
+ GZIP,
3338
+ Deflate,
3339
+ DeflateRaw,
3340
+ };
3341
+
3342
+ // Using the same fixed encoding buffer size as Chromium, see
3343
+ // https://chromium.googlesource.com/chromium/src/+/457f48d3d8635c8bca077232471228d75290cc29/third_party/blink/renderer/modules/compression/deflate_transformer.cc#29
3344
+ const size_t BUFFER_SIZE = 16384 ;
3345
+
3346
+ bool is_instance (JSObject *obj);
3347
+
3348
+ JSObject *transform (JSObject *self) {
3349
+ MOZ_ASSERT (is_instance (self));
3350
+ return &JS::GetReservedSlot (self, Slots::Transform).toObject ();
3351
+ }
3352
+
3353
+ Format format (JSObject *self) {
3354
+ MOZ_ASSERT (is_instance (self));
3355
+ return (Format)JS::GetReservedSlot (self, Slots::Format).toInt32 ();
3356
+ }
3357
+
3358
+ z_stream *state (JSObject *self) {
3359
+ MOZ_ASSERT (is_instance (self));
3360
+ void *ptr = JS::GetReservedSlot (self, Slots::State).toPrivate ();
3361
+ MOZ_ASSERT (ptr);
3362
+ return (z_stream *)ptr;
3363
+ }
3364
+
3365
+ uint8_t *output_buffer (JSObject *self) {
3366
+ MOZ_ASSERT (is_instance (self));
3367
+ void *ptr = JS::GetReservedSlot (self, Slots::Buffer).toPrivate ();
3368
+ MOZ_ASSERT (ptr);
3369
+ return (uint8_t *)ptr;
3370
+ }
3371
+
3372
+ const unsigned ctor_length = 1 ;
3373
+ bool check_receiver (JSContext *cx, HandleValue receiver, const char *method_name);
3374
+
3375
+ // Steps 1-5 of the transform algorithm, and 1-5 of the flush algorithm.
3376
+ bool inflate_chunk (JSContext *cx, HandleObject self, HandleValue chunk, bool finished) {
3377
+ z_stream *zstream = state (self);
3378
+
3379
+ if (!finished) {
3380
+ // 1. If _chunk_ is not a `BufferSource` type, then throw a `TypeError`.
3381
+ // Step 2 of transform:
3382
+ size_t length;
3383
+ uint8_t *data = value_to_buffer (cx, chunk, " DecompressionStream transform: chunks" , &length);
3384
+ if (!data) {
3385
+ return false ;
3386
+ }
3387
+
3388
+ if (length == 0 ) {
3389
+ return true ;
3390
+ }
3391
+
3392
+ // 2. Let _buffer_ be the result of decompressing _chunk_ with _ds_'s format
3393
+ // and context. This just sets up step 2. The actual decompression happen in
3394
+ // the `do` loop below.
3395
+ zstream->avail_in = length;
3396
+
3397
+ // `data` is a live view into `chunk`. That's ok here because it'll be fully
3398
+ // used in the `do` loop below before any content can execute again and
3399
+ // could potentially invalidate the pointer to `data`.
3400
+ zstream->next_in = data;
3401
+ } else {
3402
+ // Step 1 of flush:
3403
+ // 1. Let _buffer_ be the result of decompressing an empty input with _ds_'s
3404
+ // format and
3405
+ // context, with the finish flag.
3406
+
3407
+ // Step 2 of flush:
3408
+ // 2. If the end of the compressed input has not been reached, then throw a TypeError.
3409
+ if (zstream->avail_in != 0 ) {
3410
+ JS_ReportErrorNumberUTF8 (cx, GetErrorMessage, nullptr , JSMSG_DECOMPRESSING_ERROR);
3411
+ return false ;
3412
+ }
3413
+ // This just sets up step 3. The actual decompression happens in the `do` loop
3414
+ // below.
3415
+ zstream->avail_in = 0 ;
3416
+ zstream->next_in = nullptr ;
3417
+ }
3418
+
3419
+ RootedObject controller (cx, TransformStream::controller (transform (self)));
3420
+
3421
+ // Steps 3-5 of transform are identical to steps 3-5 of flush, so numbers
3422
+ // below refer to the former for those. Also, the compression happens in
3423
+ // potentially smaller chunks in the `do` loop below, so the three steps are
3424
+ // reordered and somewhat intertwined with each other.
3425
+
3426
+ uint8_t *buffer = output_buffer (self);
3427
+
3428
+ // Call `inflate` in a loop, enqueuing compressed chunks until the input
3429
+ // buffer has been fully consumed. That is the case when `zstream->avail_out`
3430
+ // is non-zero, i.e. when the last chunk wasn't completely filled. See zlib
3431
+ // docs for details:
3432
+ // https://searchfox.org/mozilla-central/rev/87ecd21d3ca517f8d90e49b32bf042a754ed8f18/modules/zlib/src/zlib.h#319-324
3433
+ do {
3434
+ // 4. Split _buffer_ into one or more non-empty pieces and convert them
3435
+ // into `Uint8Array`s.
3436
+ // 5. For each `Uint8Array` _array_, enqueue _array_ in _cds_'s transform.
3437
+ // This loop does the actual decompression, one output-buffer sized chunk at a
3438
+ // time, and then creates and enqueues the Uint8Arrays immediately.
3439
+ zstream->avail_out = BUFFER_SIZE;
3440
+ zstream->next_out = buffer;
3441
+ int err = inflate (zstream, finished ? Z_FINISH : Z_NO_FLUSH);
3442
+ if (err != Z_OK && err != Z_STREAM_END && err != Z_BUF_ERROR) {
3443
+
3444
+ JS_ReportErrorNumberUTF8 (cx, GetErrorMessage, nullptr , JSMSG_DECOMPRESSING_ERROR);
3445
+ return false ;
3446
+ }
3447
+
3448
+ size_t bytes = BUFFER_SIZE - zstream->avail_out ;
3449
+ if (bytes) {
3450
+ RootedObject out_obj (cx, JS_NewUint8Array (cx, bytes));
3451
+ if (!out_obj) {
3452
+ return false ;
3453
+ }
3454
+
3455
+ {
3456
+ bool is_shared;
3457
+ JS::AutoCheckCannotGC nogc;
3458
+ uint8_t *out_buffer = JS_GetUint8ArrayData (out_obj, &is_shared, nogc);
3459
+ memcpy (out_buffer, buffer, bytes);
3460
+ }
3461
+
3462
+ RootedValue out_chunk (cx, ObjectValue (*out_obj));
3463
+ if (!TransformStreamDefaultController::Enqueue (cx, controller, out_chunk)) {
3464
+ return false ;
3465
+ }
3466
+ }
3467
+
3468
+ // 3. If _buffer_ is empty, return.
3469
+ } while (zstream->avail_out == 0 );
3470
+
3471
+ return true ;
3472
+ }
3473
+
3474
+ // https://wicg.github.io/compression/#decompress-and-enqueue-a-chunk
3475
+ // All steps inlined into `inflate_chunk`.
3476
+ bool transformAlgorithm (JSContext *cx, unsigned argc, Value *vp) {
3477
+ METHOD_HEADER_WITH_NAME (1 , " Decompression stream transform algorithm" )
3478
+
3479
+ if (!inflate_chunk (cx, self, args[0 ], false )) {
3480
+ return false ;
3481
+ }
3482
+
3483
+ args.rval ().setUndefined ();
3484
+ return true ;
3485
+ }
3486
+
3487
+ // https://wicg.github.io/compression/#decompress-flush-and-enqueue
3488
+ // All steps inlined into `inflate_chunk`.
3489
+ bool flushAlgorithm (JSContext *cx, unsigned argc, Value *vp) {
3490
+ METHOD_HEADER_WITH_NAME (0 , " Decompression stream flush algorithm" )
3491
+
3492
+ if (!inflate_chunk (cx, self, JS::UndefinedHandleValue, true )) {
3493
+ return false ;
3494
+ }
3495
+
3496
+ inflateEnd (state (self));
3497
+ JS_free (cx, output_buffer (self));
3498
+
3499
+ // These fields shouldn't ever be accessed again, but we should be able to
3500
+ // assert that.
3501
+ #ifdef DEBUG
3502
+ JS::SetReservedSlot (self, Slots::State, PrivateValue (nullptr ));
3503
+ JS::SetReservedSlot (self, Slots::Buffer, PrivateValue (nullptr ));
3504
+ #endif
3505
+
3506
+ args.rval ().setUndefined ();
3507
+ return true ;
3508
+ }
3509
+
3510
+ bool readable_get (JSContext *cx, unsigned argc, Value *vp) {
3511
+ METHOD_HEADER_WITH_NAME (0 , " get readable" )
3512
+ args.rval ().setObject (*TransformStream::readable (transform (self)));
3513
+ return true ;
3514
+ }
3515
+
3516
+ bool writable_get (JSContext *cx, unsigned argc, Value *vp) {
3517
+ METHOD_HEADER_WITH_NAME (0 , " get writable" )
3518
+ args.rval ().setObject (*TransformStream::writable (transform (self)));
3519
+ return true ;
3520
+ }
3521
+
3522
+ const JSFunctionSpec methods[] = {JS_FS_END};
3523
+
3524
+ const JSPropertySpec properties[] = {
3525
+ JS_PSG (" readable" , readable_get, JSPROP_ENUMERATE),
3526
+ JS_PSG (" writable" , writable_get, JSPROP_ENUMERATE),
3527
+ JS_STRING_SYM_PS (toStringTag, " DecompressionStream" , JSPROP_READONLY), JS_PS_END};
3528
+
3529
+ bool constructor (JSContext *cx, unsigned argc, Value *vp);
3530
+
3531
+ CLASS_BOILERPLATE_CUSTOM_INIT (DecompressionStream)
3532
+
3533
+ static PersistentRooted<JSObject *> transformAlgo;
3534
+ static PersistentRooted<JSObject *> flushAlgo;
3535
+
3536
+ // Steps 2-6 of `new DecompressionStream()`.
3537
+ JSObject *create (JSContext *cx, HandleObject stream, Format format) {
3538
+ RootedValue stream_val (cx, ObjectValue (*stream));
3539
+
3540
+ // 2. Set this's format to _format_.
3541
+ JS::SetReservedSlot (stream, Slots::Format, JS::Int32Value ((int32_t )format));
3542
+
3543
+ // 3. Let _transformAlgorithm_ be an algorithm which takes a _chunk_ argument
3544
+ // and runs the
3545
+ // `compress and enqueue a chunk algorithm with this and _chunk_.
3546
+ // 4. Let _flushAlgorithm_ be an algorithm which takes no argument and runs
3547
+ // the
3548
+ // `compress flush and enqueue` algorithm with this.
3549
+ // (implicit)
3550
+
3551
+ // 5. Set this's transform to a new `TransformStream`.
3552
+ // 6. [Set up](https://streams.spec.whatwg.org/#transformstream-set-up)
3553
+ // this's transform with _transformAlgorithm_ set to _transformAlgorithm_ and
3554
+ // _flushAlgorithm_ set to _flushAlgorithm_.
3555
+ RootedObject transform (cx, TransformStream::create (cx, 1 , nullptr , 0 , nullptr , stream_val,
3556
+ nullptr , transformAlgo, flushAlgo));
3557
+ if (!transform) {
3558
+ return nullptr ;
3559
+ }
3560
+
3561
+ TransformStream::set_used_as_mixin (transform);
3562
+ JS::SetReservedSlot (stream, Slots::Transform, ObjectValue (*transform));
3563
+
3564
+ // The remainder of the function deals with setting up the inflate state used
3565
+ // for decompressing chunks.
3566
+
3567
+ z_stream *zstream = (z_stream *)JS_malloc (cx, sizeof (z_stream));
3568
+ if (!zstream) {
3569
+ JS_ReportOutOfMemory (cx);
3570
+ return nullptr ;
3571
+ }
3572
+
3573
+ memset (zstream, 0 , sizeof (z_stream));
3574
+ JS::SetReservedSlot (stream, Slots::State, PrivateValue (zstream));
3575
+
3576
+ uint8_t *buffer = (uint8_t *)JS_malloc (cx, BUFFER_SIZE);
3577
+ if (!buffer) {
3578
+ JS_ReportOutOfMemory (cx);
3579
+ return nullptr ;
3580
+ }
3581
+
3582
+ JS::SetReservedSlot (stream, Slots::Buffer, PrivateValue (buffer));
3583
+
3584
+ // Using the same window bits as Chromium's Compression stream, see
3585
+ // https://chromium.googlesource.com/chromium/src/+/457f48d3d8635c8bca077232471228d75290cc29/third_party/blink/renderer/modules/compression/inflate_transformer.cc#31
3586
+ int window_bits = 15 ;
3587
+ if (format == Format::GZIP) {
3588
+ window_bits += 16 ;
3589
+ } else if (format == Format::DeflateRaw) {
3590
+ window_bits = -15 ;
3591
+ }
3592
+
3593
+ int err = inflateInit2 (zstream, window_bits);
3594
+ if (err != Z_OK) {
3595
+ JS_ReportErrorASCII (cx, " Error initializing decompression stream" );
3596
+ return nullptr ;
3597
+ }
3598
+
3599
+ return stream;
3600
+ }
3601
+
3602
+ /* *
3603
+ * https://wicg.github.io/compression/#dom-compressionstream-compressionstream
3604
+ */
3605
+ bool constructor (JSContext *cx, unsigned argc, Value *vp) {
3606
+ // 1. If _format_ is unsupported in `CompressionStream`, then throw a
3607
+ // `TypeError`.
3608
+ CTOR_HEADER (" DecompressionStream" , 1 );
3609
+
3610
+ size_t format_len;
3611
+ UniqueChars format_chars = encode (cx, args[0 ], &format_len);
3612
+ if (!format_chars) {
3613
+ return false ;
3614
+ }
3615
+
3616
+ Format format;
3617
+ if (!strcmp (format_chars.get (), " deflate-raw" )) {
3618
+ format = Format::DeflateRaw;
3619
+ } else if (!strcmp (format_chars.get (), " deflate" )) {
3620
+ format = Format::Deflate;
3621
+ } else if (!strcmp (format_chars.get (), " gzip" )) {
3622
+ format = Format::GZIP;
3623
+ } else {
3624
+ JS_ReportErrorNumberUTF8 (cx, GetErrorMessage, nullptr , JSMSG_INVALID_COMPRESSION_FORMAT,
3625
+ format_chars.get ());
3626
+ return false ;
3627
+ }
3628
+
3629
+ RootedObject decompressionStreamInstance (cx, JS_NewObjectForConstructor (cx, &class_, args));
3630
+ // Steps 2-6.
3631
+ RootedObject stream (cx, create (cx, decompressionStreamInstance, format));
3632
+ if (!stream) {
3633
+ return false ;
3634
+ }
3635
+
3636
+ args.rval ().setObject (*stream);
3637
+ return true ;
3638
+ }
3639
+
3640
+ bool init_class (JSContext *cx, HandleObject global) {
3641
+ if (!init_class_impl (cx, global)) {
3642
+ return false ;
3643
+ }
3644
+
3645
+ JSFunction *transformFun = JS_NewFunction (cx, transformAlgorithm, 1 , 0 , " DS Transform" );
3646
+ if (!transformFun)
3647
+ return false ;
3648
+ transformAlgo.init (cx, JS_GetFunctionObject (transformFun));
3649
+
3650
+ JSFunction *flushFun = JS_NewFunction (cx, flushAlgorithm, 1 , 0 , " DS Flush" );
3651
+ if (!flushFun)
3652
+ return false ;
3653
+ flushAlgo.init (cx, JS_GetFunctionObject (flushFun));
3654
+
3655
+ return true ;
3656
+ }
3657
+
3658
+ } // namespace DecompressionStream
3659
+
3325
3660
/* *
3326
3661
* Implementation of the WICG CompressionStream builtin.
3327
3662
*
@@ -3517,8 +3852,10 @@ bool writable_get(JSContext *cx, unsigned argc, Value *vp) {
3517
3852
3518
3853
const JSFunctionSpec methods[] = {JS_FS_END};
3519
3854
3520
- const JSPropertySpec properties[] = {JS_PSG (" readable" , readable_get, JSPROP_ENUMERATE),
3521
- JS_PSG (" writable" , writable_get, JSPROP_ENUMERATE), JS_PS_END};
3855
+ const JSPropertySpec properties[] = {
3856
+ JS_PSG (" readable" , readable_get, JSPROP_ENUMERATE),
3857
+ JS_PSG (" writable" , writable_get, JSPROP_ENUMERATE),
3858
+ JS_STRING_SYM_PS (toStringTag, " CompressionStream" , JSPROP_READONLY), JS_PS_END};
3522
3859
3523
3860
bool constructor (JSContext *cx, unsigned argc, Value *vp);
3524
3861
@@ -7736,6 +8073,8 @@ bool define_fastly_sys(JSContext *cx, HandleObject global) {
7736
8073
return false ;
7737
8074
if (!CompressionStream::init_class (cx, global))
7738
8075
return false ;
8076
+ if (!DecompressionStream::init_class (cx, global))
8077
+ return false ;
7739
8078
if (!Request::init_class (cx, global))
7740
8079
return false ;
7741
8080
if (!Response::init_class (cx, global))
0 commit comments