Skip to content

Commit 6f3e4f8

Browse files
Jake ChampionJakeChampion
authored andcommitted
extract decompression-stream into its own file
1 parent 3bf4ad7 commit 6f3e4f8

File tree

3 files changed

+359
-335
lines changed

3 files changed

+359
-335
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
// TODO: remove these once the warnings are fixed
2+
#pragma clang diagnostic push
3+
#pragma clang diagnostic ignored "-Winvalid-offsetof"
4+
#include "js/experimental/TypedData.h"
5+
#pragma clang diagnostic pop
6+
7+
#include "zlib.h"
8+
9+
#include "builtin.h"
10+
#include "builtins/transform-stream-default-controller.h"
11+
#include "builtins/transform-stream.h"
12+
#include "host_call.h"
13+
#include "js-compute-builtins.h"
14+
/**
15+
* Implementation of the WICG DecompressionStream builtin.
16+
*
17+
* All algorithm names and steps refer to spec algorithms defined at
18+
* https://wicg.github.io/compression/#decompression-stream
19+
*/
20+
namespace DecompressionStream {
21+
namespace Slots {
22+
enum { Transform, Format, State, Buffer, Count };
23+
};
24+
25+
enum class Format {
26+
GZIP,
27+
Deflate,
28+
DeflateRaw,
29+
};
30+
31+
// Using the same fixed encoding buffer size as Chromium, see
32+
// https://chromium.googlesource.com/chromium/src/+/457f48d3d8635c8bca077232471228d75290cc29/third_party/blink/renderer/modules/compression/deflate_transformer.cc#29
33+
constexpr size_t BUFFER_SIZE = 16384;
34+
35+
bool is_instance(JSObject *obj);
36+
37+
JSObject *transform(JSObject *self) {
38+
MOZ_ASSERT(is_instance(self));
39+
return &JS::GetReservedSlot(self, Slots::Transform).toObject();
40+
}
41+
42+
Format format(JSObject *self) {
43+
MOZ_ASSERT(is_instance(self));
44+
return (Format)JS::GetReservedSlot(self, Slots::Format).toInt32();
45+
}
46+
47+
z_stream *state(JSObject *self) {
48+
MOZ_ASSERT(is_instance(self));
49+
void *ptr = JS::GetReservedSlot(self, Slots::State).toPrivate();
50+
MOZ_ASSERT(ptr);
51+
return (z_stream *)ptr;
52+
}
53+
54+
uint8_t *output_buffer(JSObject *self) {
55+
MOZ_ASSERT(is_instance(self));
56+
void *ptr = JS::GetReservedSlot(self, Slots::Buffer).toPrivate();
57+
MOZ_ASSERT(ptr);
58+
return (uint8_t *)ptr;
59+
}
60+
61+
const unsigned ctor_length = 1;
62+
bool check_receiver(JSContext *cx, JS::HandleValue receiver, const char *method_name);
63+
64+
// Steps 1-5 of the transform algorithm, and 1-5 of the flush algorithm.
65+
bool inflate_chunk(JSContext *cx, JS::HandleObject self, JS::HandleValue chunk, bool finished) {
66+
z_stream *zstream = state(self);
67+
68+
if (!finished) {
69+
// 1. If _chunk_ is not a `BufferSource` type, then throw a `TypeError`.
70+
// Step 2 of transform:
71+
size_t length;
72+
uint8_t *data = value_to_buffer(cx, chunk, "DecompressionStream transform: chunks", &length);
73+
if (!data) {
74+
return false;
75+
}
76+
77+
if (length == 0) {
78+
return true;
79+
}
80+
81+
// 2. Let _buffer_ be the result of decompressing _chunk_ with _ds_'s format
82+
// and context. This just sets up step 2. The actual decompression happen in
83+
// the `do` loop below.
84+
zstream->avail_in = length;
85+
86+
// `data` is a live view into `chunk`. That's ok here because it'll be fully
87+
// used in the `do` loop below before any content can execute again and
88+
// could potentially invalidate the pointer to `data`.
89+
zstream->next_in = data;
90+
} else {
91+
// Step 1 of flush:
92+
// 1. Let _buffer_ be the result of decompressing an empty input with _ds_'s
93+
// format and
94+
// context, with the finish flag.
95+
96+
// Step 2 of flush:
97+
// 2. If the end of the compressed input has not been reached, then throw a TypeError.
98+
if (zstream->avail_in != 0) {
99+
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_DECOMPRESSING_ERROR);
100+
return false;
101+
}
102+
// This just sets up step 3. The actual decompression happens in the `do` loop
103+
// below.
104+
zstream->avail_in = 0;
105+
zstream->next_in = nullptr;
106+
}
107+
108+
JS::RootedObject controller(cx, TransformStream::controller(transform(self)));
109+
110+
// Steps 3-5 of transform are identical to steps 3-5 of flush, so numbers
111+
// below refer to the former for those. Also, the compression happens in
112+
// potentially smaller chunks in the `do` loop below, so the three steps are
113+
// reordered and somewhat intertwined with each other.
114+
115+
uint8_t *buffer = output_buffer(self);
116+
117+
// Call `inflate` in a loop, enqueuing compressed chunks until the input
118+
// buffer has been fully consumed. That is the case when `zstream->avail_out`
119+
// is non-zero, i.e. when the last chunk wasn't completely filled. See zlib
120+
// docs for details:
121+
// https://searchfox.org/mozilla-central/rev/87ecd21d3ca517f8d90e49b32bf042a754ed8f18/modules/zlib/src/zlib.h#319-324
122+
do {
123+
// 4. Split _buffer_ into one or more non-empty pieces and convert them
124+
// into `Uint8Array`s.
125+
// 5. For each `Uint8Array` _array_, enqueue _array_ in _cds_'s transform.
126+
// This loop does the actual decompression, one output-buffer sized chunk at a
127+
// time, and then creates and enqueues the Uint8Arrays immediately.
128+
zstream->avail_out = BUFFER_SIZE;
129+
zstream->next_out = buffer;
130+
int err = inflate(zstream, finished ? Z_FINISH : Z_NO_FLUSH);
131+
if (err != Z_OK && err != Z_STREAM_END && err != Z_BUF_ERROR) {
132+
133+
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_DECOMPRESSING_ERROR);
134+
return false;
135+
}
136+
137+
size_t bytes = BUFFER_SIZE - zstream->avail_out;
138+
if (bytes) {
139+
JS::RootedObject out_obj(cx, JS_NewUint8Array(cx, bytes));
140+
if (!out_obj) {
141+
return false;
142+
}
143+
144+
{
145+
bool is_shared;
146+
JS::AutoCheckCannotGC nogc;
147+
uint8_t *out_buffer = JS_GetUint8ArrayData(out_obj, &is_shared, nogc);
148+
memcpy(out_buffer, buffer, bytes);
149+
}
150+
151+
JS::RootedValue out_chunk(cx, JS::ObjectValue(*out_obj));
152+
if (!TransformStreamDefaultController::Enqueue(cx, controller, out_chunk)) {
153+
return false;
154+
}
155+
}
156+
157+
// 3. If _buffer_ is empty, return.
158+
} while (zstream->avail_out == 0);
159+
160+
return true;
161+
}
162+
163+
// https://wicg.github.io/compression/#decompress-and-enqueue-a-chunk
164+
// All steps inlined into `inflate_chunk`.
165+
bool transformAlgorithm(JSContext *cx, unsigned argc, JS::Value *vp) {
166+
METHOD_HEADER_WITH_NAME(1, "Decompression stream transform algorithm")
167+
168+
if (!inflate_chunk(cx, self, args[0], false)) {
169+
return false;
170+
}
171+
172+
args.rval().setUndefined();
173+
return true;
174+
}
175+
176+
// https://wicg.github.io/compression/#decompress-flush-and-enqueue
177+
// All steps inlined into `inflate_chunk`.
178+
bool flushAlgorithm(JSContext *cx, unsigned argc, JS::Value *vp) {
179+
METHOD_HEADER_WITH_NAME(0, "Decompression stream flush algorithm")
180+
181+
if (!inflate_chunk(cx, self, JS::UndefinedHandleValue, true)) {
182+
return false;
183+
}
184+
185+
inflateEnd(state(self));
186+
JS_free(cx, output_buffer(self));
187+
188+
// These fields shouldn't ever be accessed again, but we should be able to
189+
// assert that.
190+
#ifdef DEBUG
191+
JS::SetReservedSlot(self, Slots::State, JS::PrivateValue(nullptr));
192+
JS::SetReservedSlot(self, Slots::Buffer, JS::PrivateValue(nullptr));
193+
#endif
194+
195+
args.rval().setUndefined();
196+
return true;
197+
}
198+
199+
bool readable_get(JSContext *cx, unsigned argc, JS::Value *vp) {
200+
METHOD_HEADER_WITH_NAME(0, "get readable")
201+
args.rval().setObject(*TransformStream::readable(transform(self)));
202+
return true;
203+
}
204+
205+
bool writable_get(JSContext *cx, unsigned argc, JS::Value *vp) {
206+
METHOD_HEADER_WITH_NAME(0, "get writable")
207+
args.rval().setObject(*TransformStream::writable(transform(self)));
208+
return true;
209+
}
210+
211+
const JSFunctionSpec methods[] = {JS_FS_END};
212+
213+
const JSPropertySpec properties[] = {
214+
JS_PSG("readable", readable_get, JSPROP_ENUMERATE),
215+
JS_PSG("writable", writable_get, JSPROP_ENUMERATE),
216+
JS_STRING_SYM_PS(toStringTag, "DecompressionStream", JSPROP_READONLY), JS_PS_END};
217+
218+
bool constructor(JSContext *cx, unsigned argc, JS::Value *vp);
219+
220+
CLASS_BOILERPLATE_CUSTOM_INIT(DecompressionStream)
221+
222+
static JS::PersistentRooted<JSObject *> transformAlgo;
223+
static JS::PersistentRooted<JSObject *> flushAlgo;
224+
225+
// Steps 2-6 of `new DecompressionStream()`.
226+
JSObject *create(JSContext *cx, JS::HandleObject stream, Format format) {
227+
JS::RootedValue stream_val(cx, JS::ObjectValue(*stream));
228+
229+
// 2. Set this's format to _format_.
230+
JS::SetReservedSlot(stream, Slots::Format, JS::Int32Value((int32_t)format));
231+
232+
// 3. Let _transformAlgorithm_ be an algorithm which takes a _chunk_ argument
233+
// and runs the
234+
// `compress and enqueue a chunk algorithm with this and _chunk_.
235+
// 4. Let _flushAlgorithm_ be an algorithm which takes no argument and runs
236+
// the
237+
// `compress flush and enqueue` algorithm with this.
238+
// (implicit)
239+
240+
// 5. Set this's transform to a new `TransformStream`.
241+
// 6. [Set up](https://streams.spec.whatwg.org/#transformstream-set-up)
242+
// this's transform with _transformAlgorithm_ set to _transformAlgorithm_ and
243+
// _flushAlgorithm_ set to _flushAlgorithm_.
244+
JS::RootedObject transform(cx, TransformStream::create(cx, 1, nullptr, 0, nullptr, stream_val,
245+
nullptr, transformAlgo, flushAlgo));
246+
if (!transform) {
247+
return nullptr;
248+
}
249+
250+
TransformStream::set_used_as_mixin(transform);
251+
JS::SetReservedSlot(stream, Slots::Transform, JS::ObjectValue(*transform));
252+
253+
// The remainder of the function deals with setting up the inflate state used
254+
// for decompressing chunks.
255+
256+
z_stream *zstream = (z_stream *)JS_malloc(cx, sizeof(z_stream));
257+
if (!zstream) {
258+
JS_ReportOutOfMemory(cx);
259+
return nullptr;
260+
}
261+
262+
memset(zstream, 0, sizeof(z_stream));
263+
JS::SetReservedSlot(stream, Slots::State, JS::PrivateValue(zstream));
264+
265+
uint8_t *buffer = (uint8_t *)JS_malloc(cx, BUFFER_SIZE);
266+
if (!buffer) {
267+
JS_ReportOutOfMemory(cx);
268+
return nullptr;
269+
}
270+
271+
JS::SetReservedSlot(stream, Slots::Buffer, JS::PrivateValue(buffer));
272+
273+
// Using the same window bits as Chromium's Compression stream, see
274+
// https://chromium.googlesource.com/chromium/src/+/457f48d3d8635c8bca077232471228d75290cc29/third_party/blink/renderer/modules/compression/inflate_transformer.cc#31
275+
int window_bits = 15;
276+
if (format == Format::GZIP) {
277+
window_bits += 16;
278+
} else if (format == Format::DeflateRaw) {
279+
window_bits = -15;
280+
}
281+
282+
int err = inflateInit2(zstream, window_bits);
283+
if (err != Z_OK) {
284+
JS_ReportErrorASCII(cx, "Error initializing decompression stream");
285+
return nullptr;
286+
}
287+
288+
return stream;
289+
}
290+
291+
/**
292+
* https://wicg.github.io/compression/#dom-compressionstream-compressionstream
293+
*/
294+
bool constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
295+
// 1. If _format_ is unsupported in `CompressionStream`, then throw a
296+
// `TypeError`.
297+
CTOR_HEADER("DecompressionStream", 1);
298+
299+
size_t format_len;
300+
JS::UniqueChars format_chars = encode(cx, args[0], &format_len);
301+
if (!format_chars) {
302+
return false;
303+
}
304+
305+
Format format;
306+
if (!strcmp(format_chars.get(), "deflate-raw")) {
307+
format = Format::DeflateRaw;
308+
} else if (!strcmp(format_chars.get(), "deflate")) {
309+
format = Format::Deflate;
310+
} else if (!strcmp(format_chars.get(), "gzip")) {
311+
format = Format::GZIP;
312+
} else {
313+
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_COMPRESSION_FORMAT,
314+
format_chars.get());
315+
return false;
316+
}
317+
318+
JS::RootedObject decompressionStreamInstance(cx, JS_NewObjectForConstructor(cx, &class_, args));
319+
// Steps 2-6.
320+
JS::RootedObject stream(cx, create(cx, decompressionStreamInstance, format));
321+
if (!stream) {
322+
return false;
323+
}
324+
325+
args.rval().setObject(*stream);
326+
return true;
327+
}
328+
329+
bool init_class(JSContext *cx, JS::HandleObject global) {
330+
if (!init_class_impl(cx, global)) {
331+
return false;
332+
}
333+
334+
JSFunction *transformFun = JS_NewFunction(cx, transformAlgorithm, 1, 0, "DS Transform");
335+
if (!transformFun)
336+
return false;
337+
transformAlgo.init(cx, JS_GetFunctionObject(transformFun));
338+
339+
JSFunction *flushFun = JS_NewFunction(cx, flushAlgorithm, 1, 0, "DS Flush");
340+
if (!flushFun)
341+
return false;
342+
flushAlgo.init(cx, JS_GetFunctionObject(flushFun));
343+
344+
return true;
345+
}
346+
347+
} // namespace DecompressionStream
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#ifndef JS_COMPUTE_RUNTIME_DECOMPRESSION_STREAM_H
2+
#define JS_COMPUTE_RUNTIME_DECOMPRESSION_STREAM_H
3+
4+
#include "builtin.h"
5+
6+
namespace DecompressionStream {
7+
// Register the class.
8+
bool init_class(JSContext *cx, JS::HandleObject global);
9+
} // namespace DecompressionStream
10+
11+
#endif

0 commit comments

Comments
 (0)