Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Jint.Tests/Runtime/WebApi/FormDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,5 +308,41 @@ public void SupportsSubclassing()
engine.Execute("var m = new MyForm(); m.append('a', 'b');");
engine.Evaluate("m.get('a')").AsString().Should().Be("b");
}

[Fact]
public void TheIteratorPrototypesNextCarriesWebIdlsAttributes()
{
// "An iterator prototype object must have a next data property with attributes
// { [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true } and whose value is the
// built-in function object CreateBuiltinFunction(nextSteps, 0, "next", <<>>)" —
// https://webidl.spec.whatwg.org/#es-iterator-prototype-object. Enumerable is the surprise: a
// built-in function property is non-enumerable everywhere in ECMA-262
// (https://tc39.es/ecma262/#sec-ecmascript-standard-built-in-objects), and WebIDL is the one binding
// that says otherwise.
var engine = WebEngine();
engine.Execute("var proto = Object.getPrototypeOf(new FormData().entries());");

engine.Evaluate("Object.getOwnPropertyNames(proto).join(',')").AsString().Should().Be("next");
engine.Execute("var d = Object.getOwnPropertyDescriptor(proto, 'next');");
engine.Evaluate("d.writable").AsBoolean().Should().BeTrue("next must be writable");
engine.Evaluate("d.enumerable").AsBoolean().Should().BeTrue("next must be enumerable");
engine.Evaluate("d.configurable").AsBoolean().Should().BeTrue("next must be configurable");
engine.Evaluate("d.value.length").AsNumber().Should().Be(0);
engine.Evaluate("d.value.name").AsString().Should().Be("next");

// The three kinds share one prototype, so the attributes above are the attributes of all of them.
engine.Evaluate("Object.getPrototypeOf(new FormData().keys()) === proto").AsBoolean().Should().BeTrue("the three kinds share one prototype");
engine.Evaluate("Object.getPrototypeOf(new FormData().values()) === proto").AsBoolean().Should().BeTrue("the three kinds share one prototype");

// The class string is the object's only other own property, and it keeps the attributes a class
// string carries — { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true } — rather
// than WebIDL's operation attributes.
engine.Evaluate("Object.getOwnPropertySymbols(proto).length").AsNumber().Should().Be(1);
engine.Execute("var t = Object.getOwnPropertyDescriptor(proto, Symbol.toStringTag);");
engine.Evaluate("t.value").AsString().Should().Be("FormData Iterator");
engine.Evaluate("t.writable").AsBoolean().Should().BeFalse();
engine.Evaluate("t.enumerable").AsBoolean().Should().BeFalse();
engine.Evaluate("t.configurable").AsBoolean().Should().BeTrue("the class string is configurable");
}
}
#endif
136 changes: 136 additions & 0 deletions Jint.Tests/Runtime/WebApi/HeadersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,5 +241,141 @@ public void RequiresNew()
Assert.Throws<JavaScriptException>(() => Eval("Headers()"))
.Message.Should().Contain("requires 'new'");
}

[Fact]
public void TheIteratorPrototypesNextCarriesWebIdlsAttributes()
{
// "An iterator prototype object must have a next data property with attributes
// { [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true } and whose value is the
// built-in function object CreateBuiltinFunction(nextSteps, 0, "next", <<>>)" —
// https://webidl.spec.whatwg.org/#es-iterator-prototype-object. Enumerable is the surprise: a
// built-in function property is non-enumerable everywhere in ECMA-262
// (https://tc39.es/ecma262/#sec-ecmascript-standard-built-in-objects), and WebIDL is the one binding
// that says otherwise.
var engine = WebEngine();
engine.Execute("var proto = Object.getPrototypeOf(new Headers().entries());");

engine.Evaluate("Object.getOwnPropertyNames(proto).join(',')").AsString().Should().Be("next");
engine.Execute("var d = Object.getOwnPropertyDescriptor(proto, 'next');");
engine.Evaluate("d.writable").AsBoolean().Should().BeTrue("next must be writable");
engine.Evaluate("d.enumerable").AsBoolean().Should().BeTrue("next must be enumerable");
engine.Evaluate("d.configurable").AsBoolean().Should().BeTrue("next must be configurable");
engine.Evaluate("d.value.length").AsNumber().Should().Be(0);
engine.Evaluate("d.value.name").AsString().Should().Be("next");

// The three kinds share one prototype, so the attributes above are the attributes of all of them.
engine.Evaluate("Object.getPrototypeOf(new Headers().keys()) === proto").AsBoolean().Should().BeTrue("the three kinds share one prototype");
engine.Evaluate("Object.getPrototypeOf(new Headers().values()) === proto").AsBoolean().Should().BeTrue("the three kinds share one prototype");

// The class string is the object's only other own property, and it keeps the attributes a class
// string carries — { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true } — rather
// than WebIDL's operation attributes.
engine.Evaluate("Object.getOwnPropertySymbols(proto).length").AsNumber().Should().Be(1);
engine.Execute("var t = Object.getOwnPropertyDescriptor(proto, Symbol.toStringTag);");
engine.Evaluate("t.value").AsString().Should().Be("Headers Iterator");
engine.Evaluate("t.writable").AsBoolean().Should().BeFalse();
engine.Evaluate("t.enumerable").AsBoolean().Should().BeFalse();
engine.Evaluate("t.configurable").AsBoolean().Should().BeTrue("the class string is configurable");
}

/// <summary>
/// The proxy-and-log harness web-platform-tests' <c>headers-record.any.js</c> uses: every
/// <c>Reflect</c> operation the conversion performs is appended to <c>log</c> as
/// <c>trap:key</c>.
/// </summary>
private const string RecordProbe = """
var log = [];
var handler = {};
for (const prop of Object.getOwnPropertyNames(Reflect)) {
handler[prop] = function (...args) {
const key = args.length > 1 ? String(args[1]) : '';
log.push(key === '' ? prop : prop + ':' + key);
return Reflect[prop](...args);
};
}
function probe(record) {
log = [];
try { new Headers(new Proxy(record, handler)); return log.join(' | '); }
catch (e) { return e.constructor.name + ' >> ' + log.join(' | '); }
}
""";

[Fact]
public void ConvertsARecordInWebIdlsOwnOrder()
{
// https://webidl.spec.whatwg.org/#es-record, whose step 4 walks [[OwnPropertyKeys]]() and, for each
// key: 4.1 [[GetOwnProperty]], and then, only when the descriptor exists and is enumerable,
// 4.2.1 "let typedKey be key converted to an IDL value of type K", 4.2.2 "let value be ? Get(O, key)"
// and 4.2.3 the value's own conversion. The key is converted BEFORE the Get, and K here is
// ByteString — https://webidl.spec.whatwg.org/#es-ByteString — so a key that is a Symbol, or a
// string with a code unit above 255, ends the whole conversion at 4.2.1 with a TypeError and the
// Get never happens.
var engine = WebEngine();
engine.Execute(RecordProbe);

// One property, and a second: the shape everything else is measured against.
engine.Evaluate("probe({ a: 'b' })").AsString()
.Should().Be("get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a | get:a");
engine.Evaluate("probe({ a: 'b', c: 'd' })").AsString()
.Should().Be("get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a | get:a "
+ "| getOwnPropertyDescriptor:c | get:c");

// An invalid *name*: the conversion stops at step 4.2.1, so there is no second Get.
engine.Evaluate("probe({ a: 'b', '￿': 'd' })").AsString()
.Should().Be("TypeError >> get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a "
+ "| get:a | getOwnPropertyDescriptor:￿");

// An invalid *value* stops one step later, at 4.2.3, so the next key is never even looked at.
engine.Evaluate("probe({ a: '￿', c: 'd' })").AsString()
.Should().Be("TypeError >> get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a "
+ "| get:a");

// A Symbol key is an own property key like any other: its descriptor is fetched, and because it is
// enumerable the ByteString conversion of the key itself throws — before its Get, and after the
// string keys that preceded it have been read in full.
engine.Evaluate("probe({ a: 'b', [Symbol.toStringTag]: 'x', c: 'd' })").AsString()
.Should().Be("TypeError >> get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a "
+ "| get:a | getOwnPropertyDescriptor:c | get:c "
+ "| getOwnPropertyDescriptor:Symbol(Symbol.toStringTag)");

// A non-enumerable one is skipped at step 4.2 instead, so it costs a descriptor and nothing else —
// no key conversion, therefore no TypeError, therefore a Headers that was built.
engine.Execute("""
var withSymbol = { a: 'b', [Symbol.toStringTag]: 'x', c: 'd' };
Object.defineProperty(withSymbol, Symbol.toStringTag, { enumerable: false });
""");
engine.Evaluate("probe(withSymbol)").AsString()
.Should().Be("get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a | get:a "
+ "| getOwnPropertyDescriptor:c | get:c | getOwnPropertyDescriptor:Symbol(Symbol.toStringTag)");

// A non-enumerable string key and a descriptor a proxy lies about are the same step 4.2: a
// descriptor apiece, no Get.
engine.Evaluate("probe(Object.defineProperties({}, { a: { value: 'b' }, c: { value: 'd', enumerable: true } }))")
.AsString()
.Should().Be("get:Symbol(Symbol.iterator) | ownKeys | getOwnPropertyDescriptor:a "
+ "| getOwnPropertyDescriptor:c | get:c");
}

[Fact]
public void ARecordKeyThatIsNotAByteStringIsATypeError()
{
// The consequence of the order above that a script can see without a proxy. A Symbol fails at the
// ToString https://webidl.spec.whatwg.org/#es-ByteString starts with, rather than at its own
// above-0x00FF check, so the message is the language's rather than the binding's.
Assert.Throws<JavaScriptException>(() => Eval("new Headers({ [Symbol.toStringTag]: 'x' })"))
.Message.Should().Contain("Symbol");

Assert.Throws<JavaScriptException>(() => Eval("new Headers({ '￿': 'x' })"))
.Message.Should().Contain("ByteString");

// ... and the one it must not raise: a non-enumerable Symbol key is not a record member at all.
Eval("""
(() => {
const o = { a: 'b' };
Object.defineProperty(o, Symbol.toStringTag, { value: 'x', enumerable: false });
return new Headers(o).get('a');
})()
""").AsString().Should().Be("b");
}
}
#endif
23 changes: 23 additions & 0 deletions Jint.Tests/Runtime/WebApi/RequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,5 +428,28 @@ public void BrandChecksEveryMember()
.Message.Should().Contain("Request");
}
}

[Fact]
public void ConsumingABufferedRequestBodyLeavesItsStreamDisturbedAndLocked()
{
// The Body mixin is one mixin — https://fetch.spec.whatwg.org/#body-mixin — so what
// https://fetch.spec.whatwg.org/#concept-body-fully-read does to a Response's stream it does to a
// Request's: step 3 acquires a reader, which locks the stream, and read-all-bytes never releases it.
var engine = WebEngine();
engine.Execute("var a = new Request('https://example.org', { method: 'POST', body: 'hi' }); a.text();");

engine.Evaluate("a.bodyUsed").AsBoolean().Should().BeTrue();
engine.Evaluate("a.body === null").AsBoolean().Should().BeFalse();
engine.Evaluate("a.body.locked").AsBoolean().Should().BeTrue();

Assert.Throws<JavaScriptException>(() => engine.Evaluate("a.body.getReader()"))
.Message.Should().Contain("locked");

// And the neighbour: reading `body` first, then consuming, locks that same stream rather than a new
// one, which is what keeps a copy of an unread request legal.
engine.Execute("var b = new Request('https://example.org', { method: 'POST', body: 'hi' }); var s = b.body;");
engine.Evaluate("b.bodyUsed").AsBoolean().Should().BeFalse();
engine.Evaluate("new Request(b).method").AsString().Should().Be("POST");
}
}
#endif
70 changes: 70 additions & 0 deletions Jint.Tests/Runtime/WebApi/ResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,5 +291,75 @@ public void IsConstructibleAsABaseClass()
Eval("(() => { class R extends Response {}; const r = new R('hi'); return r.status + ':' + (r instanceof R); })()")
.AsString().Should().Be("200:true");
}

[Theory]
[InlineData("text")]
[InlineData("json")]
[InlineData("arrayBuffer")]
[InlineData("blob")]
[InlineData("bytes")]
[InlineData("formData")]
public void ConsumingABufferedBodyLeavesItsStreamDisturbedAndLocked(string consume)
{
// https://fetch.spec.whatwg.org/#concept-body-consume-body ends in *fully read*, whose step 3 is
// "let reader be the result of getting a reader for body's stream" —
// https://streams.spec.whatwg.org/#readablestream-get-a-reader, which locks the stream — and whose
// read-all-bytes never releases it. So a consumed body's stream is disturbed
// (https://fetch.spec.whatwg.org/#concept-body-disturbed) *and* locked for good, and a second
// getReader() is the TypeError https://streams.spec.whatwg.org/#set-up-readable-stream-default-reader
// raises. web-platform-tests' response-stream-disturbed-5.any.js asserts exactly that.
var engine = WebEngine();
engine.Execute("var r = new Response('a=1', { headers: { 'content-type': 'application/x-www-form-urlencoded' } });");

// The rejection json() produces is caught here only so it is not an unhandled one; the point of the
// call is what it does to the body, which is the same either way.
engine.Execute($"r.{consume}().then(() => {{}}, () => {{}});");

engine.Evaluate("r.bodyUsed").AsBoolean().Should().BeTrue("consuming disturbs the body");
engine.Evaluate("r.body === null").AsBoolean().Should().BeFalse("the body concept is still non-null");
engine.Evaluate("r.body.locked").AsBoolean().Should().BeTrue("fully read never releases its reader");

Assert.Throws<JavaScriptException>(() => engine.Evaluate("r.body.getReader()"))
.Message.Should().Contain("locked");

// The same object every time, and asking again does not un-disturb it.
engine.Evaluate("r.body === r.body").AsBoolean().Should().BeTrue();
engine.Evaluate("r.bodyUsed").AsBoolean().Should().BeTrue();
}

[Fact]
public void AskingForTheBodyBeforeConsumingItDoesNotDisturbIt()
{
// The other side of the same coin, and what response-stream-disturbed-1.any.js pins: materializing
// the stream is not reading it, and a reader that released its lock leaves the body usable.
var engine = WebEngine();
engine.Execute("var r = new Response('hi'); var s = r.body; s.getReader().releaseLock();");

engine.Evaluate("s.locked").AsBoolean().Should().BeFalse();
engine.Evaluate("r.bodyUsed").AsBoolean().Should().BeFalse();
engine.Evaluate("r.text()").UnwrapIfPromise().AsString().Should().Be("hi");

// Consuming it through the stream it already had locks that same stream, not a new one.
engine.Evaluate("r.body === s").AsBoolean().Should().BeTrue();
engine.Evaluate("s.locked").AsBoolean().Should().BeTrue();
}

[Fact]
public void CloneAfterConsumingStillThrows()
{
// https://fetch.spec.whatwg.org/#dom-response-clone step 1 refuses a body that is *unusable*, which
// https://fetch.spec.whatwg.org/#body-unusable defines as disturbed **or locked**. Locking the
// buffered body's stream must not change that answer, in either direction.
var engine = WebEngine();
engine.Execute("var r = new Response('hi'); r.text();");

Assert.Throws<JavaScriptException>(() => engine.Evaluate("r.clone()"))
.Message.Should().Contain("already used");

// And reading `body` first — which is what materializes the stream — does not change it either.
engine.Execute("var q = new Response('hi'); q.text(); var b = q.body;");
Assert.Throws<JavaScriptException>(() => engine.Evaluate("q.clone()"))
.Message.Should().Contain("already used");
}
}
#endif
36 changes: 36 additions & 0 deletions Jint.Tests/Runtime/WebApi/UrlSearchParamsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,5 +291,41 @@ class MyParams extends URLSearchParams {
engine.Evaluate("sub.first").AsString().Should().Be("1");
engine.Evaluate("sub.toString()").AsString().Should().Be("a=1");
}

[Fact]
public void TheIteratorPrototypesNextCarriesWebIdlsAttributes()
{
// "An iterator prototype object must have a next data property with attributes
// { [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true } and whose value is the
// built-in function object CreateBuiltinFunction(nextSteps, 0, "next", <<>>)" —
// https://webidl.spec.whatwg.org/#es-iterator-prototype-object. Enumerable is the surprise: a
// built-in function property is non-enumerable everywhere in ECMA-262
// (https://tc39.es/ecma262/#sec-ecmascript-standard-built-in-objects), and WebIDL is the one binding
// that says otherwise.
var engine = WebEngine();
engine.Execute("var proto = Object.getPrototypeOf(new URLSearchParams().entries());");

engine.Evaluate("Object.getOwnPropertyNames(proto).join(',')").AsString().Should().Be("next");
engine.Execute("var d = Object.getOwnPropertyDescriptor(proto, 'next');");
engine.Evaluate("d.writable").AsBoolean().Should().BeTrue("next must be writable");
engine.Evaluate("d.enumerable").AsBoolean().Should().BeTrue("next must be enumerable");
engine.Evaluate("d.configurable").AsBoolean().Should().BeTrue("next must be configurable");
engine.Evaluate("d.value.length").AsNumber().Should().Be(0);
engine.Evaluate("d.value.name").AsString().Should().Be("next");

// The three kinds share one prototype, so the attributes above are the attributes of all of them.
engine.Evaluate("Object.getPrototypeOf(new URLSearchParams().keys()) === proto").AsBoolean().Should().BeTrue("the three kinds share one prototype");
engine.Evaluate("Object.getPrototypeOf(new URLSearchParams().values()) === proto").AsBoolean().Should().BeTrue("the three kinds share one prototype");

// The class string is the object's only other own property, and it keeps the attributes a class
// string carries — { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true } — rather
// than WebIDL's operation attributes.
engine.Evaluate("Object.getOwnPropertySymbols(proto).length").AsNumber().Should().Be(1);
engine.Execute("var t = Object.getOwnPropertyDescriptor(proto, Symbol.toStringTag);");
engine.Evaluate("t.value").AsString().Should().Be("URLSearchParams Iterator");
engine.Evaluate("t.writable").AsBoolean().Should().BeFalse();
engine.Evaluate("t.enumerable").AsBoolean().Should().BeFalse();
engine.Evaluate("t.configurable").AsBoolean().Should().BeTrue("the class string is configurable");
}
}
#endif
Loading