mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-20 11:36:10 +00:00
LibWeb: Widen assertion to avoid accidentally writing to a closed stream
This widens the assertion from only checking if the WritableStream's state is Errored or Erroring to asserting that the WritableStream is not in a Writable state.
This commit is contained in:
parent
6ba60188b4
commit
b285202951
Notes:
github-actions[bot]
2025-04-14 18:51:59 +00:00
Author: https://github.com/kennethmyhra Commit: https://github.com/LadybirdBrowser/ladybird/commit/b2852029519 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4355 Reviewed-by: https://github.com/trflynn89 ✅
4 changed files with 530 additions and 2 deletions
|
@ -4974,8 +4974,8 @@ JS::Value writable_stream_default_controller_get_chunk_size(WritableStreamDefaul
|
|||
{
|
||||
// 1. If controller.[[strategySizeAlgorithm]] is undefined, then:
|
||||
if (!controller.strategy_size_algorithm()) {
|
||||
// 1. Assert: controller.[[stream]].[[state]] is "erroring" or "errored".
|
||||
VERIFY(controller.stream()->state() == WritableStream::State::Erroring || controller.stream()->state() == WritableStream::State::Errored);
|
||||
// 1. Assert: controller.[[stream]].[[state]] is not "writable".
|
||||
VERIFY(controller.stream()->state() != WritableStream::State::Writable);
|
||||
|
||||
// 2. Return 1.
|
||||
return JS::Value { 1.0 };
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 26 tests
|
||||
|
||||
26 Pass
|
||||
Pass fulfillment value of writer.close() call must be undefined even if the underlying sink returns a non-undefined value
|
||||
Pass when sink calls error asynchronously while sink close is in-flight, the stream should not become errored
|
||||
Pass when sink calls error synchronously while closing, the stream should not become errored
|
||||
Pass when the sink throws during close, and the close is requested while a write is still in-flight, the stream should become errored during the close
|
||||
Pass releaseLock on a stream with a pending write in which the stream has been errored
|
||||
Pass releaseLock on a stream with a pending close in which controller.error() was called
|
||||
Pass when close is called on a WritableStream in writable state, ready should return a fulfilled promise
|
||||
Pass when close is called on a WritableStream in waiting state, ready promise should be fulfilled
|
||||
Pass when close is called on a WritableStream in waiting state, ready should be fulfilled immediately even if close takes a long time
|
||||
Pass returning a thenable from close() should work
|
||||
Pass releaseLock() should not change the result of sync close()
|
||||
Pass releaseLock() should not change the result of async close()
|
||||
Pass close() should set state to CLOSED even if writer has detached
|
||||
Pass the promise returned by async abort during close should resolve
|
||||
Pass promises must fulfill/reject in the expected order on closure
|
||||
Pass promises must fulfill/reject in the expected order on aborted closure
|
||||
Pass promises must fulfill/reject in the expected order on aborted and errored closure
|
||||
Pass close() should not reject until no sink methods are in flight
|
||||
Pass ready promise should be initialised as fulfilled for a writer on a closed stream
|
||||
Pass close() on a writable stream should work
|
||||
Pass close() on a locked stream should reject
|
||||
Pass close() on an erroring stream should reject
|
||||
Pass close() on an errored stream should reject
|
||||
Pass close() on an closed stream should reject
|
||||
Pass close() on a stream with a pending close should reject
|
||||
Pass write() on a closed stream should reject
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
|
||||
<script>
|
||||
self.GLOBAL = {
|
||||
isWindow: function() { return true; },
|
||||
isWorker: function() { return false; },
|
||||
isShadowRealm: function() { return false; },
|
||||
};
|
||||
</script>
|
||||
<script src="../../resources/testharness.js"></script>
|
||||
<script src="../../resources/testharnessreport.js"></script>
|
||||
<script src="../resources/test-utils.js"></script>
|
||||
<script src="../resources/recording-streams.js"></script>
|
||||
<div id=log></div>
|
||||
<script src="../../streams/writable-streams/close.any.js"></script>
|
|
@ -0,0 +1,481 @@
|
|||
// META: global=window,worker,shadowrealm
|
||||
// META: script=../resources/test-utils.js
|
||||
// META: script=../resources/recording-streams.js
|
||||
'use strict';
|
||||
|
||||
const error1 = new Error('error1');
|
||||
error1.name = 'error1';
|
||||
|
||||
const error2 = new Error('error2');
|
||||
error2.name = 'error2';
|
||||
|
||||
promise_test(() => {
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
return 'Hello';
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
const closePromise = writer.close();
|
||||
return closePromise.then(value => assert_equals(value, undefined, 'fulfillment value must be undefined'));
|
||||
}, 'fulfillment value of writer.close() call must be undefined even if the underlying sink returns a non-undefined ' +
|
||||
'value');
|
||||
|
||||
promise_test(() => {
|
||||
let controller;
|
||||
let resolveClose;
|
||||
const ws = new WritableStream({
|
||||
start(c) {
|
||||
controller = c;
|
||||
},
|
||||
close() {
|
||||
return new Promise(resolve => {
|
||||
resolveClose = resolve;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
const closePromise = writer.close();
|
||||
return flushAsyncEvents().then(() => {
|
||||
controller.error(error1);
|
||||
return flushAsyncEvents();
|
||||
}).then(() => {
|
||||
resolveClose();
|
||||
return Promise.all([
|
||||
closePromise,
|
||||
writer.closed,
|
||||
flushAsyncEvents().then(() => writer.closed)]);
|
||||
});
|
||||
}, 'when sink calls error asynchronously while sink close is in-flight, the stream should not become errored');
|
||||
|
||||
promise_test(() => {
|
||||
let controller;
|
||||
const passedError = new Error('error me');
|
||||
const ws = new WritableStream({
|
||||
start(c) {
|
||||
controller = c;
|
||||
},
|
||||
close() {
|
||||
controller.error(passedError);
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
return writer.close().then(() => writer.closed);
|
||||
}, 'when sink calls error synchronously while closing, the stream should not become errored');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
throw error1;
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
return Promise.all([
|
||||
writer.write('y'),
|
||||
promise_rejects_exactly(t, error1, writer.close(), 'close() must reject with the error'),
|
||||
promise_rejects_exactly(t, error1, writer.closed, 'closed must reject with the error')
|
||||
]);
|
||||
}, 'when the sink throws during close, and the close is requested while a write is still in-flight, the stream should ' +
|
||||
'become errored during the close');
|
||||
|
||||
promise_test(() => {
|
||||
const ws = new WritableStream({
|
||||
write(chunk, controller) {
|
||||
controller.error(error1);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
writer.write('a');
|
||||
|
||||
return delay(0).then(() => {
|
||||
writer.releaseLock();
|
||||
});
|
||||
}, 'releaseLock on a stream with a pending write in which the stream has been errored');
|
||||
|
||||
promise_test(() => {
|
||||
let controller;
|
||||
const ws = new WritableStream({
|
||||
start(c) {
|
||||
controller = c;
|
||||
},
|
||||
close() {
|
||||
controller.error(error1);
|
||||
return new Promise(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
writer.close();
|
||||
|
||||
return delay(0).then(() => {
|
||||
writer.releaseLock();
|
||||
});
|
||||
}, 'releaseLock on a stream with a pending close in which controller.error() was called');
|
||||
|
||||
promise_test(() => {
|
||||
const ws = recordingWritableStream();
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
return writer.ready.then(() => {
|
||||
assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
|
||||
|
||||
writer.close();
|
||||
assert_equals(writer.desiredSize, 1, 'desiredSize should be still 1');
|
||||
|
||||
return writer.ready.then(v => {
|
||||
assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
|
||||
assert_array_equals(ws.events, ['close'], 'write and abort should not be called');
|
||||
});
|
||||
});
|
||||
}, 'when close is called on a WritableStream in writable state, ready should return a fulfilled promise');
|
||||
|
||||
promise_test(() => {
|
||||
const ws = recordingWritableStream({
|
||||
write() {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
return writer.ready.then(() => {
|
||||
writer.write('a');
|
||||
|
||||
assert_equals(writer.desiredSize, 0, 'desiredSize should be 0');
|
||||
|
||||
let calledClose = false;
|
||||
return Promise.all([
|
||||
writer.ready.then(v => {
|
||||
assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
|
||||
assert_true(calledClose, 'ready should not be fulfilled before writer.close() is called');
|
||||
assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called');
|
||||
}),
|
||||
flushAsyncEvents().then(() => {
|
||||
writer.close();
|
||||
calledClose = true;
|
||||
})
|
||||
]);
|
||||
});
|
||||
}, 'when close is called on a WritableStream in waiting state, ready promise should be fulfilled');
|
||||
|
||||
promise_test(() => {
|
||||
let asyncCloseFinished = false;
|
||||
const ws = recordingWritableStream({
|
||||
close() {
|
||||
return flushAsyncEvents().then(() => {
|
||||
asyncCloseFinished = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
return writer.ready.then(() => {
|
||||
writer.write('a');
|
||||
|
||||
writer.close();
|
||||
|
||||
return writer.ready.then(v => {
|
||||
assert_false(asyncCloseFinished, 'ready promise should be fulfilled before async close completes');
|
||||
assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
|
||||
assert_array_equals(ws.events, ['write', 'a', 'close'], 'sink abort() should not be called');
|
||||
});
|
||||
});
|
||||
}, 'when close is called on a WritableStream in waiting state, ready should be fulfilled immediately even if close ' +
|
||||
'takes a long time');
|
||||
|
||||
promise_test(t => {
|
||||
const rejection = { name: 'letter' };
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
return {
|
||||
then(onFulfilled, onRejected) { onRejected(rejection); }
|
||||
};
|
||||
}
|
||||
});
|
||||
return promise_rejects_exactly(t, rejection, ws.getWriter().close(), 'close() should return a rejection');
|
||||
}, 'returning a thenable from close() should work');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream();
|
||||
const writer = ws.getWriter();
|
||||
return writer.ready.then(() => {
|
||||
const closePromise = writer.close();
|
||||
const closedPromise = writer.closed;
|
||||
writer.releaseLock();
|
||||
return Promise.all([
|
||||
closePromise,
|
||||
promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected')
|
||||
]);
|
||||
});
|
||||
}, 'releaseLock() should not change the result of sync close()');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
return flushAsyncEvents();
|
||||
}
|
||||
});
|
||||
const writer = ws.getWriter();
|
||||
return writer.ready.then(() => {
|
||||
const closePromise = writer.close();
|
||||
const closedPromise = writer.closed;
|
||||
writer.releaseLock();
|
||||
return Promise.all([
|
||||
closePromise,
|
||||
promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected')
|
||||
]);
|
||||
});
|
||||
}, 'releaseLock() should not change the result of async close()');
|
||||
|
||||
promise_test(() => {
|
||||
let resolveClose;
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
const promise = new Promise(resolve => {
|
||||
resolveClose = resolve;
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
const writer = ws.getWriter();
|
||||
const closePromise = writer.close();
|
||||
writer.releaseLock();
|
||||
return delay(0).then(() => {
|
||||
resolveClose();
|
||||
return closePromise.then(() => {
|
||||
assert_equals(ws.getWriter().desiredSize, 0, 'desiredSize should be 0');
|
||||
});
|
||||
});
|
||||
}, 'close() should set state to CLOSED even if writer has detached');
|
||||
|
||||
promise_test(() => {
|
||||
let resolveClose;
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
const promise = new Promise(resolve => {
|
||||
resolveClose = resolve;
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
const writer = ws.getWriter();
|
||||
writer.close();
|
||||
writer.releaseLock();
|
||||
return delay(0).then(() => {
|
||||
const abortingWriter = ws.getWriter();
|
||||
const abortPromise = abortingWriter.abort();
|
||||
abortingWriter.releaseLock();
|
||||
resolveClose();
|
||||
return abortPromise;
|
||||
});
|
||||
}, 'the promise returned by async abort during close should resolve');
|
||||
|
||||
// Though the order in which the promises are fulfilled or rejected is arbitrary, we're checking it for
|
||||
// interoperability. We can change the order as long as we file bugs on all implementers to update to the latest tests
|
||||
// to keep them interoperable.
|
||||
|
||||
promise_test(() => {
|
||||
const ws = new WritableStream({});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
|
||||
const closePromise = writer.close();
|
||||
|
||||
const events = [];
|
||||
return Promise.all([
|
||||
closePromise.then(() => {
|
||||
events.push('closePromise');
|
||||
}),
|
||||
writer.closed.then(() => {
|
||||
events.push('closed');
|
||||
})
|
||||
]).then(() => {
|
||||
assert_array_equals(events, ['closePromise', 'closed'],
|
||||
'promises must fulfill/reject in the expected order');
|
||||
});
|
||||
}, 'promises must fulfill/reject in the expected order on closure');
|
||||
|
||||
promise_test(() => {
|
||||
const ws = new WritableStream({});
|
||||
|
||||
// Wait until the WritableStream starts so that the close() call gets processed. Otherwise, abort() will be
|
||||
// processed without waiting for completion of the close().
|
||||
return delay(0).then(() => {
|
||||
const writer = ws.getWriter();
|
||||
|
||||
const closePromise = writer.close();
|
||||
const abortPromise = writer.abort(error1);
|
||||
|
||||
const events = [];
|
||||
return Promise.all([
|
||||
closePromise.then(() => {
|
||||
events.push('closePromise');
|
||||
}),
|
||||
abortPromise.then(() => {
|
||||
events.push('abortPromise');
|
||||
}),
|
||||
writer.closed.then(() => {
|
||||
events.push('closed');
|
||||
})
|
||||
]).then(() => {
|
||||
assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
|
||||
'promises must fulfill/reject in the expected order');
|
||||
});
|
||||
});
|
||||
}, 'promises must fulfill/reject in the expected order on aborted closure');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
return Promise.reject(error1);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait until the WritableStream starts so that the close() call gets processed.
|
||||
return delay(0).then(() => {
|
||||
const writer = ws.getWriter();
|
||||
|
||||
const closePromise = writer.close();
|
||||
const abortPromise = writer.abort(error2);
|
||||
|
||||
const events = [];
|
||||
closePromise.catch(() => events.push('closePromise'));
|
||||
abortPromise.catch(() => events.push('abortPromise'));
|
||||
writer.closed.catch(() => events.push('closed'));
|
||||
return Promise.all([
|
||||
promise_rejects_exactly(t, error1, closePromise,
|
||||
'closePromise must reject with the error returned from the sink\'s close method'),
|
||||
promise_rejects_exactly(t, error1, abortPromise,
|
||||
'abortPromise must reject with the error returned from the sink\'s close method'),
|
||||
promise_rejects_exactly(t, error2, writer.closed,
|
||||
'writer.closed must reject with error2')
|
||||
]).then(() => {
|
||||
assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
|
||||
'promises must fulfill/reject in the expected order');
|
||||
});
|
||||
});
|
||||
}, 'promises must fulfill/reject in the expected order on aborted and errored closure');
|
||||
|
||||
promise_test(t => {
|
||||
let resolveWrite;
|
||||
let controller;
|
||||
const ws = new WritableStream({
|
||||
write(chunk, c) {
|
||||
controller = c;
|
||||
return new Promise(resolve => {
|
||||
resolveWrite = resolve;
|
||||
});
|
||||
}
|
||||
});
|
||||
const writer = ws.getWriter();
|
||||
return writer.ready.then(() => {
|
||||
const writePromise = writer.write('c');
|
||||
controller.error(error1);
|
||||
const closePromise = writer.close();
|
||||
let closeRejected = false;
|
||||
closePromise.catch(() => {
|
||||
closeRejected = true;
|
||||
});
|
||||
return flushAsyncEvents().then(() => {
|
||||
assert_false(closeRejected);
|
||||
resolveWrite();
|
||||
return Promise.all([
|
||||
writePromise,
|
||||
promise_rejects_exactly(t, error1, closePromise, 'close() should reject')
|
||||
]).then(() => {
|
||||
assert_true(closeRejected);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 'close() should not reject until no sink methods are in flight');
|
||||
|
||||
promise_test(() => {
|
||||
const ws = new WritableStream();
|
||||
const writer1 = ws.getWriter();
|
||||
return writer1.close().then(() => {
|
||||
writer1.releaseLock();
|
||||
const writer2 = ws.getWriter();
|
||||
const ready = writer2.ready;
|
||||
assert_equals(ready.constructor, Promise);
|
||||
return ready;
|
||||
});
|
||||
}, 'ready promise should be initialised as fulfilled for a writer on a closed stream');
|
||||
|
||||
promise_test(() => {
|
||||
const ws = new WritableStream();
|
||||
ws.close();
|
||||
const writer = ws.getWriter();
|
||||
return writer.closed;
|
||||
}, 'close() on a writable stream should work');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream();
|
||||
ws.getWriter();
|
||||
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
|
||||
}, 'close() on a locked stream should reject');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream({
|
||||
start(controller) {
|
||||
controller.error(error1);
|
||||
}
|
||||
});
|
||||
return promise_rejects_exactly(t, error1, ws.close(), 'close should reject with error1');
|
||||
}, 'close() on an erroring stream should reject');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream({
|
||||
start(controller) {
|
||||
controller.error(error1);
|
||||
}
|
||||
});
|
||||
const writer = ws.getWriter();
|
||||
return promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the error').then(() => {
|
||||
writer.releaseLock();
|
||||
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
|
||||
});
|
||||
}, 'close() on an errored stream should reject');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream();
|
||||
const writer = ws.getWriter();
|
||||
return writer.close().then(() => {
|
||||
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
|
||||
});
|
||||
}, 'close() on an closed stream should reject');
|
||||
|
||||
promise_test(t => {
|
||||
const ws = new WritableStream({
|
||||
close() {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
const writer = ws.getWriter();
|
||||
writer.close();
|
||||
writer.releaseLock();
|
||||
|
||||
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
|
||||
}, 'close() on a stream with a pending close should reject');
|
||||
|
||||
// See https://github.com/whatwg/streams/issues/1341.
|
||||
promise_test(async t => {
|
||||
const ws = new WritableStream();
|
||||
const writer = ws.getWriter();
|
||||
|
||||
await writer.write(1);
|
||||
await writer.close();
|
||||
|
||||
return promise_rejects_js(t, TypeError, writer.write(2), 'write should reject');
|
||||
}, 'write() on a closed stream should reject');
|
Loading…
Add table
Reference in a new issue