Bug 1507952 - Part 2: Rewrite ReadableStream constructor to match the standard. r=arai

We were very close to compliance, but all the step numbers have changed and
some user-visible behavior around default arguments was a bit off.

Also, update step numbers in ValidateAndNormalizeHighWaterMark, implement
MakeSizeAlgorithmFromSizeFunction, and generally validate size/highWaterMark
arguments earlier.

Differential Revision: https://phabricator.services.mozilla.com/D12456

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jason Orendorff 2018-11-22 13:41:37 +00:00
Родитель 6c4c37739b
Коммит eed287d20a
3 изменённых файлов: 162 добавлений и 108 удалений

Просмотреть файл

@ -493,16 +493,20 @@ CreateReadableStreamDefaultController(JSContext* cx,
Handle<ReadableStream*> stream,
HandleValue underlyingSource,
HandleValue size,
HandleValue highWaterMarkVal);
double highWaterMarkVal);
/**
* Streams spec, 3.2.3., steps 1-4, 8.
*/
ReadableStream*
ReadableStream::createDefaultStream(JSContext* cx, HandleValue underlyingSource,
HandleValue size, HandleValue highWaterMark,
HandleValue size, double highWaterMark,
HandleObject proto /* = nullptr */)
{
cx->check(underlyingSource, size, proto);
MOZ_ASSERT(size.isUndefined() || IsCallable(size));
MOZ_ASSERT(highWaterMark >= 0);
// Steps 1-4.
Rooted<ReadableStream*> stream(cx, create(cx));
if (!stream) {
@ -514,7 +518,8 @@ ReadableStream::createDefaultStream(JSContext* cx, HandleValue underlyingSource,
// « this, underlyingSource, size,
// highWaterMark »).
ReadableStreamDefaultController* controller =
CreateReadableStreamDefaultController(cx, stream, underlyingSource, size, highWaterMark);
CreateReadableStreamDefaultController(cx, stream, underlyingSource, size,
highWaterMark);
if (!controller) {
return nullptr;
}
@ -547,93 +552,120 @@ ReadableStream::createExternalSourceStream(JSContext* cx, void* underlyingSource
return stream;
}
static MOZ_MUST_USE bool
MakeSizeAlgorithmFromSizeFunction(JSContext* cx, HandleValue size);
static MOZ_MUST_USE bool
ValidateAndNormalizeHighWaterMark(JSContext* cx,
HandleValue highWaterMarkVal,
double* highWaterMark);
/**
* Streams spec, 3.2.3.
* Streams spec, 3.2.3. new ReadableStream(underlyingSource = {}, strategy = {})
*/
bool
ReadableStream::constructor(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
RootedValue underlyingSource(cx, args.get(0));
RootedValue options(cx, args.get(1));
// Do argument handling first to keep the right order of error reporting.
if (underlyingSource.isUndefined()) {
RootedObject sourceObj(cx, NewBuiltinClassInstance<PlainObject>(cx));
if (!sourceObj) {
return false;
}
underlyingSource = ObjectValue(*sourceObj);
}
RootedValue size(cx);
RootedValue highWaterMark(cx);
if (!options.isUndefined()) {
if (!GetProperty(cx, options, cx->names().size, &size)) {
return false;
}
if (!GetProperty(cx, options, cx->names().highWaterMark, &highWaterMark)) {
return false;
}
}
if (!ThrowIfNotConstructing(cx, args, "ReadableStream")) {
return false;
}
// Step 5: Let type be ? GetV(underlyingSource, "type").
RootedValue typeVal(cx);
if (!GetProperty(cx, underlyingSource, cx->names().type, &typeVal)) {
// Implicit in the spec: argument default values.
RootedValue underlyingSource(cx, args.get(0));
if (underlyingSource.isUndefined()) {
JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
if (!emptyObj) {
return false;
}
underlyingSource = ObjectValue(*emptyObj);
}
RootedValue strategy(cx, args.get(1));
if (strategy.isUndefined()) {
JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
if (!emptyObj) {
return false;
}
strategy = ObjectValue(*emptyObj);
}
// Step 1: Perform ! InitializeReadableStream(this).
// This is moved down to step 7.d.
// Step 2: Let size be ? GetV(strategy, "size").
RootedValue size(cx);
if (!GetProperty(cx, strategy, cx->names().size, &size)) {
return false;
}
// Step 6: Let typeString be ? ToString(type).
RootedString type(cx, ToString<CanGC>(cx, typeVal));
if (!type) {
// Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark").
RootedValue highWaterMarkVal(cx);
if (!GetProperty(cx, strategy, cx->names().highWaterMark, &highWaterMarkVal)) {
return false;
}
int32_t notByteStream;
if (!CompareStrings(cx, type, cx->names().bytes, &notByteStream)) {
// Step 4: Let type be ? GetV(underlyingSource, "type").
RootedValue type(cx);
if (!GetProperty(cx, underlyingSource, cx->names().type, &type)) {
return false;
}
// Step 7.a & 8.a (reordered): If highWaterMark is undefined, let
// highWaterMark be 1 (or 0 for byte streams).
if (highWaterMark.isUndefined()) {
highWaterMark = Int32Value(notByteStream ? 1 : 0);
// Step 5: Let typeString be ? ToString(type).
RootedString typeString(cx, ToString<CanGC>(cx, type));
if (!typeString) {
return false;
}
Rooted<ReadableStream*> stream(cx);
// Step 7: If typeString is "bytes",
if (!notByteStream) {
// Step 7.b: Set this.[[readableStreamController]] to
// ? Construct(ReadableByteStreamController,
// « this, underlyingSource, highWaterMark »).
// Step 6: If typeString is "bytes",
int32_t cmp;
if (!CompareStrings(cx, typeString, cx->names().bytes, &cmp)) {
return false;
}
if (cmp == 0) {
// The rest of step 6 is unimplemented, since we don't support
// user-defined byte streams yet.
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
return false;
} else if (typeVal.isUndefined()) {
// Step 8: Otherwise, if type is undefined,
// Step 8.b: Set this.[[readableStreamController]] to
// ? Construct(ReadableStreamDefaultController,
// « this, underlyingSource, size, highWaterMark »).
stream = createDefaultStream(cx, underlyingSource, size, highWaterMark);
} else {
// Step 9: Otherwise, throw a RangeError exception.
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
return false;
}
if (!stream) {
return false;
}
args.rval().setObject(*stream);
return true;
// Step 7: Otherwise, if type is undefined,
if (type.isUndefined()) {
// Step 7.a: Let sizeAlgorithm be ? MakeSizeAlgorithmFromSizeFunction(size).
if (!MakeSizeAlgorithmFromSizeFunction(cx, size)) {
return false;
}
// Step 7.b: If highWaterMark is undefined, let highWaterMark be 1.
double highWaterMark;
if (highWaterMarkVal.isUndefined()) {
highWaterMark = 1;
} else {
// Step 7.c: Set highWaterMark to ? ValidateAndNormalizeHighWaterMark(highWaterMark).
if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, &highWaterMark)) {
return false;
}
}
// Step 7.d: Perform
// ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(
// this, underlyingSource, highWaterMark, sizeAlgorithm).
Rooted<ReadableStream*> stream(cx,
createDefaultStream(cx, underlyingSource, size, highWaterMark));
if (!stream) {
return false;
}
args.rval().setObject(*stream);
return true;
}
// Step 8: Otherwise, throw a RangeError exception.
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
return false;
}
/**
@ -1241,11 +1273,9 @@ ReadableStreamTee(JSContext* cx,
// Step 16: Set branch1 to
// ! CreateReadableStream(startAlgorithm, pullAlgorithm,
// cancel1Algorithm).
RootedValue hwmValue(cx, NumberValue(1));
RootedValue underlyingSource(cx, ObjectValue(*teeState));
branch1Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource,
UndefinedHandleValue,
hwmValue));
UndefinedHandleValue, 1));
if (!branch1Stream) {
return false;
}
@ -1259,8 +1289,7 @@ ReadableStreamTee(JSContext* cx,
// ! CreateReadableStream(startAlgorithm, pullAlgorithm,
// cancel2Algorithm).
branch2Stream.set(ReadableStream::createDefaultStream(cx, underlyingSource,
UndefinedHandleValue,
hwmValue));
UndefinedHandleValue, 1));
if (!branch2Stream) {
return false;
}
@ -2262,17 +2291,6 @@ ControllerStartFailedHandler(JSContext* cx, unsigned argc, Value* vp)
return true;
}
static MOZ_MUST_USE bool
ValidateAndNormalizeHighWaterMark(JSContext* cx,
HandleValue highWaterMarkVal,
double* highWaterMark);
static MOZ_MUST_USE bool
ValidateAndNormalizeQueuingStrategy(JSContext* cx,
HandleValue size,
HandleValue highWaterMarkVal,
double* highWaterMark);
/**
* Streams spec, 3.8.3
* new ReadableStreamDefaultController ( stream, underlyingSource,
@ -2287,9 +2305,11 @@ CreateReadableStreamDefaultController(JSContext* cx,
Handle<ReadableStream*> stream,
HandleValue underlyingSource,
HandleValue size,
HandleValue highWaterMarkVal)
double highWaterMark)
{
cx->check(stream, underlyingSource, size, highWaterMarkVal);
cx->check(stream, underlyingSource, size);
MOZ_ASSERT(highWaterMark >= 0);
MOZ_ASSERT(size.isUndefined() || IsCallable(size));
Rooted<ReadableStreamDefaultController*> controller(cx,
NewBuiltinClassInstance<ReadableStreamDefaultController>(cx));
@ -2313,11 +2333,8 @@ CreateReadableStreamDefaultController(JSContext* cx,
controller->setFlags(0);
// Step 7: Let normalizedStrategy be
// ? ValidateAndNormalizeQueuingStrategy(size, highWaterMark).
double highWaterMark;
if (!ValidateAndNormalizeQueuingStrategy(cx, size, highWaterMarkVal, &highWaterMark)) {
return nullptr;
}
// ? ValidateAndNormalizeQueuingStrategy(size, highWaterMark)
// (implicit).
// Step 8: Set this.[[strategySize]] to normalizedStrategy.[[size]] and
// this.[[strategyHWM]] to normalizedStrategy.[[highWaterMark]].
@ -4071,47 +4088,59 @@ PromiseInvokeOrNoop(JSContext* cx, HandleValue O, HandlePropertyName P, HandleVa
return PromiseObject::unforgeableResolve(cx, returnValue);
}
// Streams spec, 6.3.7. ValidateAndNormalizeHighWaterMark ( highWaterMark )
/**
* Streams spec, 6.3.7. ValidateAndNormalizeHighWaterMark ( highWaterMark )
*/
static MOZ_MUST_USE bool
ValidateAndNormalizeHighWaterMark(JSContext* cx, HandleValue highWaterMarkVal, double* highWaterMark)
ValidateAndNormalizeHighWaterMark(JSContext* cx, HandleValue highWaterMarkVal,
double* highWaterMark)
{
// Step 1: Set highWaterMark to ? ToNumber(highWaterMark).
if (!ToNumber(cx, highWaterMarkVal, highWaterMark)) {
return false;
}
// Step 2: If highWaterMark is NaN, throw a TypeError exception.
// Step 3: If highWaterMark < 0, throw a RangeError exception.
// Step 2: If highWaterMark is NaN or highWaterMark < 0, throw a RangeError exception.
if (mozilla::IsNaN(*highWaterMark) || *highWaterMark < 0) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_STREAM_INVALID_HIGHWATERMARK);
return false;
}
// Step 4: Return highWaterMark.
// Step 3: Return highWaterMark.
return true;
}
// Streams spec, obsolete (previously 6.4.6)
// ValidateAndNormalizeQueuingStrategy ( size, highWaterMark )
/**
* Streams spec, 6.3.8. MakeSizeAlgorithmFromSizeFunction ( size )
*
* The standard makes a big deal of turning JavaScript functions (grubby,
* touched by users, covered with germs) into algorithms (pristine,
* respectable, purposeful). We don't bother. Here we only check for errors and
* leave `size` unchanged. Then, in ReadableStreamDefaultControllerEnqueue,
* where this value is used, we have to check for undefined and behave as if we
* had "made" an "algorithm" as described below.
*/
static MOZ_MUST_USE bool
ValidateAndNormalizeQueuingStrategy(JSContext* cx, HandleValue size,
HandleValue highWaterMarkVal, double* highWaterMark)
MakeSizeAlgorithmFromSizeFunction(JSContext* cx, HandleValue size)
{
// Step 1: If size is not undefined and ! IsCallable(size) is false, throw a
// TypeError exception.
if (!size.isUndefined() && !IsCallable(size)) {
// Step 1: If size is undefined, return an algorithm that returns 1.
if (size.isUndefined()) {
// Deferred. Size algorithm users must check for undefined.
return true;
}
// Step 2: If ! IsCallable(size) is false, throw a TypeError exception.
if (!IsCallable(size)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_FUNCTION,
"ReadableStream argument options.size");
return false;
}
// Step 2: Let highWaterMark be
// ? ValidateAndNormalizeHighWaterMark(highWaterMark).
if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal, highWaterMark)) {
return false;
}
// Step 3: Return Record {[[size]]: size, [[highWaterMark]]: highWaterMark}.
// Step 3: Return an algorithm that performs the following steps, taking a
// chunk argument:
// a. Return ? Call(size, undefined, « chunk »).
// Deferred. Size algorithm users must know how to call the size function.
return true;
}
@ -4177,6 +4206,7 @@ JS::NewReadableDefaultStreamObject(JSContext* cx,
AssertHeapIsIdle();
CHECK_THREAD(cx);
cx->check(underlyingSource, size, proto);
MOZ_ASSERT(highWaterMark >= 0);
RootedObject source(cx, underlyingSource);
if (!source) {
@ -4187,8 +4217,7 @@ JS::NewReadableDefaultStreamObject(JSContext* cx,
}
RootedValue sourceVal(cx, ObjectValue(*source));
RootedValue sizeVal(cx, size ? ObjectValue(*size) : UndefinedValue());
RootedValue highWaterMarkVal(cx, NumberValue(highWaterMark));
return ReadableStream::createDefaultStream(cx, sourceVal, sizeVal, highWaterMarkVal, proto);
return ReadableStream::createDefaultStream(cx, sourceVal, sizeVal, highWaterMark, proto);
}
JS_PUBLIC_API JSObject*

Просмотреть файл

@ -99,7 +99,7 @@ class ReadableStream : public NativeObject
public:
static ReadableStream* createDefaultStream(JSContext* cx, HandleValue underlyingSource,
HandleValue size, HandleValue highWaterMark,
HandleValue size, double highWaterMark,
HandleObject proto = nullptr);
static ReadableStream* createExternalSourceStream(JSContext* cx, void* underlyingSource,
uint8_t flags, HandleObject proto = nullptr);

Просмотреть файл

@ -0,0 +1,25 @@
// The second argument to `new ReadableStream` defaults to `{}`, so it observes
// properties hacked onto Object.prototype.
let log = [];
Object.defineProperty(Object.prototype, "size", {
configurable: true,
get() {
log.push("size");
log.push(this);
return undefined;
}
});
Object.prototype.highWaterMark = 1337;
let s = new ReadableStream({
start(controller) {
log.push("start");
log.push(controller.desiredSize);
}
});
assertDeepEq(log, ["size", {}, "start", 1337]);
if (typeof reportCompare == "function")
reportCompare(0, 0);