Skip to content

Commit e48eb37

Browse files
Jake ChampionJakeChampion
authored andcommitted
Add implementation of DecompressionStream
This is an implementation of the WICG DecompressionStream builtin from https://wicg.github.io/compression/#decompression-stream We already have CompressionStream, this is the counterpart to that. An example c@e application which uses the feature would be: ```js addEventListener("fetch", event => { event.respondWith(app(event)) }) /** * @param {FetchEvent} event * @returns {Response} */ async function app(event) { const res = await fetch('https://httpbin.org/deflate', {backend: "httpbin"}) const ds = new DecompressionStream('deflate'); return new Response(res.body.pipeThrough(ds)) } ```
1 parent cd7c75a commit e48eb37

15 files changed

+867
-3
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@
7373
"thread": "cpp",
7474
"typeinfo": "cpp",
7575
"unordered_map": "cpp",
76-
"utility": "cpp"
76+
"utility": "cpp",
77+
"*.msg": "cpp"
7778
},
7879
"git.ignoreLimitWarning": true
7980
}

c-dependencies/js-compute-runtime/error-numbers.msg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@
4242
MSG_DEF(JSMSG_NOT_AN_ERROR, 0, JSEXN_ERR, "<Error #0 is reserved>")
4343
MSG_DEF(JSMSG_BUILTIN_CTOR_NO_NEW, 1, JSEXN_TYPEERR, "calling a builtin {0} constructor without new is forbidden")
4444
MSG_DEF(JSMSG_INVALID_BUFFER_ARG, 2, JSEXN_TYPEERR, "{0} must be of type ArrayBuffer or ArrayBufferView but got \"{1}\"")
45+
MSG_DEF(JSMSG_INVALID_COMPRESSION_FORMAT, 1, JSEXN_TYPEERR, "'format' has to be \"deflate\", \"deflate-raw\", or \"gzip\", but got \"{0}\"")
46+
MSG_DEF(JSMSG_DECOMPRESSING_ERROR, 0, JSEXN_TYPEERR, "DecompressionStream transform: error decompressing chunk")
4547
//clang-format on

c-dependencies/js-compute-runtime/js-compute-builtins.cpp

Lines changed: 341 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3322,6 +3322,341 @@ JSObject *create_rs_proxy(JSContext *cx, HandleObject input_readable) {
33223322
}
33233323
} // namespace TransformStream
33243324

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+
33253660
/**
33263661
* Implementation of the WICG CompressionStream builtin.
33273662
*
@@ -3517,8 +3852,10 @@ bool writable_get(JSContext *cx, unsigned argc, Value *vp) {
35173852

35183853
const JSFunctionSpec methods[] = {JS_FS_END};
35193854

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};
35223859

35233860
bool constructor(JSContext *cx, unsigned argc, Value *vp);
35243861

@@ -7736,6 +8073,8 @@ bool define_fastly_sys(JSContext *cx, HandleObject global) {
77368073
return false;
77378074
if (!CompressionStream::init_class(cx, global))
77388075
return false;
8076+
if (!DecompressionStream::init_class(cx, global))
8077+
return false;
77398078
if (!Request::init_class(cx, global))
77408079
return false;
77418080
if (!Response::init_class(cx, global))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"data should be correctly compressed even if input is detached partway": {
3+
"status": 1
4+
}
5+
}

0 commit comments

Comments
 (0)