1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 /* Class ReadableStream. */
8
9 #include "builtin/streams/ReadableStream.h"
10
11 #include "mozilla/Attributes.h" // MOZ_MUST_USE
12
13 #include "jsapi.h" // JS_ReportErrorNumberASCII
14 #include "jsfriendapi.h" // js::GetErrorMessage, JSMSG_*
15 #include "jspubtd.h" // JSProto_ReadableStream
16
17 #include "builtin/Array.h" // js::NewDenseFullyAllocatedArray
18 #include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC
19 #include "builtin/streams/MiscellaneousOperations.h" // js::MakeSizeAlgorithmFromSizeFunction, js::ValidateAndNormalizeHighWaterMark, js::ReturnPromiseRejectedWithPendingError
20 #include "builtin/streams/ReadableStreamController.h" // js::ReadableStream{,Default}Controller, js::ReadableByteStreamController
21 #include "builtin/streams/ReadableStreamDefaultControllerOperations.h" // js::SetUpReadableStreamDefaultControllerFromUnderlyingSource
22 #include "builtin/streams/ReadableStreamInternals.h" // js::ReadableStreamCancel
23 #include "builtin/streams/ReadableStreamOperations.h" // js::ReadableStream{PipeTo,Tee}
24 #include "builtin/streams/ReadableStreamReader.h" // js::CreateReadableStream{BYOB,Default}Reader, js::ForAuthorCodeBool
25 #include "builtin/streams/WritableStream.h" // js::WritableStream
26 #include "js/CallArgs.h" // JS::CallArgs{,FromVp}
27 #include "js/Class.h" // JSCLASS_PRIVATE_IS_NSISUPPORTS, JSCLASS_HAS_PRIVATE, JS_NULL_CLASS_OPS
28 #include "js/Conversions.h" // JS::ToBoolean
29 #include "js/PropertySpec.h" // JS{Function,Property}Spec, JS_FN, JS_PSG, JS_{FS,PS}_END
30 #include "js/RootingAPI.h" // JS::Handle, JS::Rooted, js::CanGC
31 #include "js/Stream.h" // JS::ReadableStream{Mode,UnderlyingSource}
32 #include "js/Value.h" // JS::Value
33 #include "vm/JSContext.h" // JSContext
34 #include "vm/JSObject.h" // js::GetPrototypeFromBuiltinConstructor
35 #include "vm/ObjectOperations.h" // js::GetProperty
36 #include "vm/PlainObject.h" // js::PlainObject
37 #include "vm/Runtime.h" // JSAtomState
38 #include "vm/StringType.h" // js::EqualStrings, js::ToString
39
40 #include "vm/Compartment-inl.h" // js::UnwrapAndTypeCheck{Argument,This}
41 #include "vm/JSObject-inl.h" // js::NewBuiltinClassInstance
42 #include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing
43
44 using js::CanGC;
45 using js::ClassSpec;
46 using js::CreateReadableStreamDefaultReader;
47 using js::EqualStrings;
48 using js::ForAuthorCodeBool;
49 using js::GetErrorMessage;
50 using js::NativeObject;
51 using js::NewBuiltinClassInstance;
52 using js::NewDenseFullyAllocatedArray;
53 using js::PlainObject;
54 using js::ReadableStream;
55 using js::ReadableStreamPipeTo;
56 using js::ReadableStreamTee;
57 using js::ReturnPromiseRejectedWithPendingError;
58 using js::ToString;
59 using js::UnwrapAndTypeCheckArgument;
60 using js::UnwrapAndTypeCheckThis;
61 using js::WritableStream;
62
63 using JS::CallArgs;
64 using JS::CallArgsFromVp;
65 using JS::Handle;
66 using JS::ObjectValue;
67 using JS::Rooted;
68 using JS::Value;
69
70 /*** 3.2. Class ReadableStream **********************************************/
71
mode() const72 JS::ReadableStreamMode ReadableStream::mode() const {
73 ReadableStreamController* controller = this->controller();
74 if (controller->is<ReadableStreamDefaultController>()) {
75 return JS::ReadableStreamMode::Default;
76 }
77 return controller->as<ReadableByteStreamController>().hasExternalSource()
78 ? JS::ReadableStreamMode::ExternalSource
79 : JS::ReadableStreamMode::Byte;
80 }
81
createExternalSourceStream(JSContext * cx,JS::ReadableStreamUnderlyingSource * source,void * nsISupportsObject_alreadyAddreffed,Handle<JSObject * > proto)82 ReadableStream* ReadableStream::createExternalSourceStream(
83 JSContext* cx, JS::ReadableStreamUnderlyingSource* source,
84 void* nsISupportsObject_alreadyAddreffed /* = nullptr */,
85 Handle<JSObject*> proto /* = nullptr */) {
86 Rooted<ReadableStream*> stream(
87 cx, create(cx, nsISupportsObject_alreadyAddreffed, proto));
88 if (!stream) {
89 return nullptr;
90 }
91
92 if (!SetUpExternalReadableByteStreamController(cx, stream, source)) {
93 return nullptr;
94 }
95
96 return stream;
97 }
98
99 /**
100 * Streams spec, 3.2.3. new ReadableStream(underlyingSource = {}, strategy = {})
101 */
constructor(JSContext * cx,unsigned argc,JS::Value * vp)102 bool ReadableStream::constructor(JSContext* cx, unsigned argc, JS::Value* vp) {
103 CallArgs args = CallArgsFromVp(argc, vp);
104
105 if (!ThrowIfNotConstructing(cx, args, "ReadableStream")) {
106 return false;
107 }
108
109 // Implicit in the spec: argument default values.
110 Rooted<Value> underlyingSource(cx, args.get(0));
111 if (underlyingSource.isUndefined()) {
112 JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
113 if (!emptyObj) {
114 return false;
115 }
116 underlyingSource = ObjectValue(*emptyObj);
117 }
118
119 Rooted<Value> strategy(cx, args.get(1));
120 if (strategy.isUndefined()) {
121 JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
122 if (!emptyObj) {
123 return false;
124 }
125 strategy = ObjectValue(*emptyObj);
126 }
127
128 // Implicit in the spec: Set this to
129 // OrdinaryCreateFromConstructor(NewTarget, ...).
130 // Step 1: Perform ! InitializeReadableStream(this).
131 Rooted<JSObject*> proto(cx);
132 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ReadableStream,
133 &proto)) {
134 return false;
135 }
136 Rooted<ReadableStream*> stream(cx,
137 ReadableStream::create(cx, nullptr, proto));
138 if (!stream) {
139 return false;
140 }
141
142 // Step 2: Let size be ? GetV(strategy, "size").
143 Rooted<Value> size(cx);
144 if (!GetProperty(cx, strategy, cx->names().size, &size)) {
145 return false;
146 }
147
148 // Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark").
149 Rooted<Value> highWaterMarkVal(cx);
150 if (!GetProperty(cx, strategy, cx->names().highWaterMark,
151 &highWaterMarkVal)) {
152 return false;
153 }
154
155 // Step 4: Let type be ? GetV(underlyingSource, "type").
156 Rooted<Value> type(cx);
157 if (!GetProperty(cx, underlyingSource, cx->names().type, &type)) {
158 return false;
159 }
160
161 // Step 5: Let typeString be ? ToString(type).
162 Rooted<JSString*> typeString(cx, ToString<CanGC>(cx, type));
163 if (!typeString) {
164 return false;
165 }
166
167 // Step 6: If typeString is "bytes",
168 bool equal;
169 if (!EqualStrings(cx, typeString, cx->names().bytes, &equal)) {
170 return false;
171 }
172 if (equal) {
173 // The rest of step 6 is unimplemented, since we don't support
174 // user-defined byte streams yet.
175 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
176 JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
177 return false;
178 }
179
180 // Step 7: Otherwise, if type is undefined,
181 if (type.isUndefined()) {
182 // Step 7.a: Let sizeAlgorithm be ? MakeSizeAlgorithmFromSizeFunction(size).
183 if (!MakeSizeAlgorithmFromSizeFunction(cx, size)) {
184 return false;
185 }
186
187 // Step 7.b: If highWaterMark is undefined, let highWaterMark be 1.
188 double highWaterMark;
189 if (highWaterMarkVal.isUndefined()) {
190 highWaterMark = 1;
191 } else {
192 // Step 7.c: Set highWaterMark to ?
193 // ValidateAndNormalizeHighWaterMark(highWaterMark).
194 if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal,
195 &highWaterMark)) {
196 return false;
197 }
198 }
199
200 // Step 7.d: Perform
201 // ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(
202 // this, underlyingSource, highWaterMark, sizeAlgorithm).
203 if (!SetUpReadableStreamDefaultControllerFromUnderlyingSource(
204 cx, stream, underlyingSource, highWaterMark, size)) {
205 return false;
206 }
207
208 args.rval().setObject(*stream);
209 return true;
210 }
211
212 // Step 8: Otherwise, throw a RangeError exception.
213 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
214 JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
215 return false;
216 }
217
218 /**
219 * Streams spec, 3.2.5.1. get locked
220 */
ReadableStream_locked(JSContext * cx,unsigned argc,JS::Value * vp)221 static MOZ_MUST_USE bool ReadableStream_locked(JSContext* cx, unsigned argc,
222 JS::Value* vp) {
223 CallArgs args = CallArgsFromVp(argc, vp);
224
225 // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
226 Rooted<ReadableStream*> unwrappedStream(
227 cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "get locked"));
228 if (!unwrappedStream) {
229 return false;
230 }
231
232 // Step 2: Return ! IsReadableStreamLocked(this).
233 args.rval().setBoolean(unwrappedStream->locked());
234 return true;
235 }
236
237 /**
238 * Streams spec, 3.2.5.2. cancel ( reason )
239 */
ReadableStream_cancel(JSContext * cx,unsigned argc,JS::Value * vp)240 static MOZ_MUST_USE bool ReadableStream_cancel(JSContext* cx, unsigned argc,
241 JS::Value* vp) {
242 CallArgs args = CallArgsFromVp(argc, vp);
243
244 // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
245 // with a TypeError exception.
246 Rooted<ReadableStream*> unwrappedStream(
247 cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "cancel"));
248 if (!unwrappedStream) {
249 return ReturnPromiseRejectedWithPendingError(cx, args);
250 }
251
252 // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise
253 // rejected with a TypeError exception.
254 if (unwrappedStream->locked()) {
255 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
256 JSMSG_READABLESTREAM_LOCKED_METHOD, "cancel");
257 return ReturnPromiseRejectedWithPendingError(cx, args);
258 }
259
260 // Step 3: Return ! ReadableStreamCancel(this, reason).
261 Rooted<JSObject*> cancelPromise(
262 cx, js::ReadableStreamCancel(cx, unwrappedStream, args.get(0)));
263 if (!cancelPromise) {
264 return false;
265 }
266 args.rval().setObject(*cancelPromise);
267 return true;
268 }
269
270 // Streams spec, 3.2.5.3.
271 // getIterator({ preventCancel } = {})
272 //
273 // Not implemented.
274
275 /**
276 * Streams spec, 3.2.5.4. getReader({ mode } = {})
277 */
ReadableStream_getReader(JSContext * cx,unsigned argc,JS::Value * vp)278 static MOZ_MUST_USE bool ReadableStream_getReader(JSContext* cx, unsigned argc,
279 JS::Value* vp) {
280 CallArgs args = CallArgsFromVp(argc, vp);
281
282 // Implicit in the spec: Argument defaults and destructuring.
283 Rooted<Value> optionsVal(cx, args.get(0));
284 if (optionsVal.isUndefined()) {
285 JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
286 if (!emptyObj) {
287 return false;
288 }
289 optionsVal.setObject(*emptyObj);
290 }
291 Rooted<Value> modeVal(cx);
292 if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) {
293 return false;
294 }
295
296 // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
297 Rooted<ReadableStream*> unwrappedStream(
298 cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "getReader"));
299 if (!unwrappedStream) {
300 return false;
301 }
302
303 // Step 2: If mode is undefined, return
304 // ? AcquireReadableStreamDefaultReader(this, true).
305 Rooted<JSObject*> reader(cx);
306 if (modeVal.isUndefined()) {
307 reader = CreateReadableStreamDefaultReader(cx, unwrappedStream,
308 ForAuthorCodeBool::Yes);
309 } else {
310 // Step 3: Set mode to ? ToString(mode) (implicit).
311 Rooted<JSString*> mode(cx, ToString<CanGC>(cx, modeVal));
312 if (!mode) {
313 return false;
314 }
315
316 // Step 5: (If mode is not "byob",) Throw a RangeError exception.
317 bool equal;
318 if (!EqualStrings(cx, mode, cx->names().byob, &equal)) {
319 return false;
320 }
321 if (!equal) {
322 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
323 JSMSG_READABLESTREAM_INVALID_READER_MODE);
324 return false;
325 }
326
327 // Step 4: If mode is "byob",
328 // return ? AcquireReadableStreamBYOBReader(this, true).
329 reader = CreateReadableStreamBYOBReader(cx, unwrappedStream,
330 ForAuthorCodeBool::Yes);
331 }
332
333 // Reordered second part of steps 2 and 4.
334 if (!reader) {
335 return false;
336 }
337 args.rval().setObject(*reader);
338 return true;
339 }
340
341 // Streams spec, 3.2.5.5.
342 // pipeThrough({ writable, readable },
343 // { preventClose, preventAbort, preventCancel, signal })
344 //
345 // Not implemented.
346
347 /**
348 * Streams spec, 3.2.5.6.
349 * pipeTo(dest, { preventClose, preventAbort, preventCancel, signal } = {})
350 */
ReadableStream_pipeTo(JSContext * cx,unsigned argc,Value * vp)351 static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) {
352 CallArgs args = CallArgsFromVp(argc, vp);
353
354 // Implicit in the spec: argument default values.
355 Rooted<Value> options(cx, args.get(1));
356 if (options.isUndefined()) {
357 JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
358 if (!emptyObj) {
359 return false;
360 }
361 options.setObject(*emptyObj);
362 }
363 // Step 3 (reordered).
364 // Implicit in the spec: get the values of the named parameters inside the
365 // second argument destructuring pattern. But as |ToBoolean| is infallible
366 // and has no observable side effects, we may as well do step 3 here too.
367 bool preventClose, preventAbort, preventCancel;
368 Rooted<Value> signalVal(cx);
369 {
370 // (P)(Re)use the |signal| root.
371 auto& v = signalVal;
372
373 if (!GetProperty(cx, options, cx->names().preventClose, &v)) {
374 return false;
375 }
376 preventClose = JS::ToBoolean(v);
377
378 if (!GetProperty(cx, options, cx->names().preventAbort, &v)) {
379 return false;
380 }
381 preventAbort = JS::ToBoolean(v);
382
383 if (!GetProperty(cx, options, cx->names().preventCancel, &v)) {
384 return false;
385 }
386 preventCancel = JS::ToBoolean(v);
387 }
388 if (!GetProperty(cx, options, cx->names().signal, &signalVal)) {
389 return false;
390 }
391
392 // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
393 // with a TypeError exception.
394 Rooted<ReadableStream*> unwrappedThis(
395 cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "pipeTo"));
396 if (!unwrappedThis) {
397 return ReturnPromiseRejectedWithPendingError(cx, args);
398 }
399
400 // Step 2: If ! IsWritableStream(dest) is false, return a promise rejected
401 // with a TypeError exception.
402 Rooted<WritableStream*> unwrappedDest(
403 cx, UnwrapAndTypeCheckArgument<WritableStream>(cx, args, "pipeTo", 0));
404 if (!unwrappedDest) {
405 return ReturnPromiseRejectedWithPendingError(cx, args);
406 }
407
408 // Step 3: Set preventClose to ! ToBoolean(preventClose), set preventAbort to
409 // ! ToBoolean(preventAbort), and set preventCancel to
410 // ! ToBoolean(preventCancel).
411 // This already happened above.
412
413 // Step 4: If signal is not undefined, and signal is not an instance of the
414 // AbortSignal interface, return a promise rejected with a TypeError
415 // exception.
416 Rooted<JSObject*> signal(cx, nullptr);
417 do {
418 if (signalVal.isUndefined()) {
419 break;
420 }
421
422 if (signalVal.isObject()) {
423 // XXX jwalden need some JSAPI hooks to detect AbortSignal instances, or
424 // something
425
426 signal = &signalVal.toObject();
427 }
428
429 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
430 JSMSG_READABLESTREAM_PIPETO_BAD_SIGNAL);
431 return ReturnPromiseRejectedWithPendingError(cx, args);
432 } while (false);
433
434 // Step 5: If ! IsReadableStreamLocked(this) is true, return a promise
435 // rejected with a TypeError exception.
436 if (unwrappedThis->locked()) {
437 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
438 JSMSG_READABLESTREAM_LOCKED_METHOD, "pipeTo");
439 return ReturnPromiseRejectedWithPendingError(cx, args);
440 }
441
442 // Step 6: If ! IsWritableStreamLocked(dest) is true, return a promise
443 // rejected with a TypeError exception.
444 if (unwrappedDest->isLocked()) {
445 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
446 JSMSG_WRITABLESTREAM_ALREADY_LOCKED);
447 return ReturnPromiseRejectedWithPendingError(cx, args);
448 }
449
450 // Step 7: Return
451 // ! ReadableStreamPipeTo(this, dest, preventClose, preventAbort,
452 // preventCancel, signal).
453 JSObject* promise =
454 ReadableStreamPipeTo(cx, unwrappedThis, unwrappedDest, preventClose,
455 preventAbort, preventCancel, signal);
456 if (!promise) {
457 return false;
458 }
459
460 args.rval().setObject(*promise);
461 return true;
462 }
463
464 /**
465 * Streams spec, 3.2.5.7. tee()
466 */
ReadableStream_tee(JSContext * cx,unsigned argc,Value * vp)467 static bool ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp) {
468 CallArgs args = CallArgsFromVp(argc, vp);
469
470 // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
471 Rooted<ReadableStream*> unwrappedStream(
472 cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "tee"));
473 if (!unwrappedStream) {
474 return false;
475 }
476
477 // Step 2: Let branches be ? ReadableStreamTee(this, false).
478 Rooted<ReadableStream*> branch1(cx);
479 Rooted<ReadableStream*> branch2(cx);
480 if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1, &branch2)) {
481 return false;
482 }
483
484 // Step 3: Return ! CreateArrayFromList(branches).
485 Rooted<NativeObject*> branches(cx, NewDenseFullyAllocatedArray(cx, 2));
486 if (!branches) {
487 return false;
488 }
489 branches->setDenseInitializedLength(2);
490 branches->initDenseElement(0, ObjectValue(*branch1));
491 branches->initDenseElement(1, ObjectValue(*branch2));
492
493 args.rval().setObject(*branches);
494 return true;
495 }
496
497 // Streams spec, 3.2.5.8.
498 // [@@asyncIterator]({ preventCancel } = {})
499 //
500 // Not implemented.
501
502 static const JSFunctionSpec ReadableStream_methods[] = {
503 JS_FN("cancel", ReadableStream_cancel, 1, 0),
504 JS_FN("getReader", ReadableStream_getReader, 0, 0),
505 // pipeTo is only conditionally supported right now, so it must be manually
506 // added below if desired.
507 JS_FN("tee", ReadableStream_tee, 0, 0), JS_FS_END};
508
509 static const JSPropertySpec ReadableStream_properties[] = {
510 JS_PSG("locked", ReadableStream_locked, 0), JS_PS_END};
511
FinishReadableStreamClassInit(JSContext * cx,Handle<JSObject * > ctor,Handle<JSObject * > proto)512 static bool FinishReadableStreamClassInit(JSContext* cx, Handle<JSObject*> ctor,
513 Handle<JSObject*> proto) {
514 // This function and everything below should be replaced with
515 //
516 // JS_STREAMS_CLASS_SPEC(ReadableStream, 0, SlotCount, 0,
517 // JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
518 // JS_NULL_CLASS_OPS);
519 //
520 // when "pipeTo" is always enabled.
521 const auto& rco = cx->realm()->creationOptions();
522 if (rco.getStreamsEnabled() && rco.getWritableStreamsEnabled() &&
523 rco.getReadableStreamPipeToEnabled()) {
524 Rooted<jsid> pipeTo(cx, NameToId(cx->names().pipeTo));
525 if (!DefineFunction(cx, proto, pipeTo, ReadableStream_pipeTo, 2,
526 JSPROP_RESOLVING)) {
527 return false;
528 }
529 }
530
531 return true;
532 }
533
534 const ClassSpec ReadableStream::classSpec_ = {
535 js::GenericCreateConstructor<ReadableStream::constructor, 2,
536 js::gc::AllocKind::FUNCTION>,
537 js::GenericCreatePrototype<ReadableStream>,
538 nullptr,
539 nullptr,
540 ReadableStream_methods,
541 ReadableStream_properties,
542 FinishReadableStreamClassInit,
543 0};
544
545 const JSClass ReadableStream::class_ = {
546 "ReadableStream",
547 JSCLASS_HAS_RESERVED_SLOTS(ReadableStream::SlotCount) |
548 JSCLASS_HAS_CACHED_PROTO(JSProto_ReadableStream) |
549 JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
550 JS_NULL_CLASS_OPS, &ReadableStream::classSpec_};
551
552 const JSClass ReadableStream::protoClass_ = {
553 "object", JSCLASS_HAS_CACHED_PROTO(JSProto_ReadableStream),
554 JS_NULL_CLASS_OPS, &ReadableStream::classSpec_};
555