From cbee0de1cb0cc2947d024bc1228c965ae115a55f Mon Sep 17 00:00:00 2001 From: ikeyan Date: Tue, 19 May 2026 22:38:22 +0900 Subject: [PATCH 001/143] stream: align `Readable.toWeb` termination with eos PR-URL: https://github.com/nodejs/node/pull/62394 Reviewed-By: Mattias Buelens Reviewed-By: James M Snell --- lib/internal/streams/end-of-stream.js | 234 +++++++++++------- lib/internal/webstreams/adapters.js | 137 +++++----- test/parallel/test-stream-finished.js | 41 ++- ...test-stream-readable-to-web-termination.js | 36 ++- ...g-webstreams-adapters-to-readablestream.js | 141 ++++++++++- 5 files changed, 421 insertions(+), 168 deletions(-) diff --git a/lib/internal/streams/end-of-stream.js b/lib/internal/streams/end-of-stream.js index e995755079f8c1..f310cd6936fbe7 100644 --- a/lib/internal/streams/end-of-stream.js +++ b/lib/internal/streams/end-of-stream.js @@ -7,6 +7,7 @@ const { Promise, PromisePrototypeThen, ReflectApply, + Symbol, SymbolDispose, } = primordials; @@ -66,6 +67,50 @@ function bindAsyncResource(fn, type) { }; } +/** + * Returns the current stream error tracked by eos(), if any. + * @param {import('stream').Stream} stream + * @returns {Error | null} + */ +function getEosErrored(stream) { + const errored = isWritableErrored(stream) || isReadableErrored(stream); + return typeof errored !== 'boolean' && errored || null; +} + +/** + * Returns the error eos() would report from an immediate close, including + * premature close detection for unfinished readable or writable sides. + * @param {import('stream').Stream} stream + * @param {boolean} readable + * @param {boolean | null} readableFinished + * @param {boolean} writable + * @param {boolean | null} writableFinished + * @returns {Error | null} + */ +function getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished) { + const errored = getEosErrored(stream); + if (errored) { + return errored; + } + + if (readable && !readableFinished && isReadableNodeStream(stream, true)) { + if (!isReadableFinished(stream, false)) { + return new ERR_STREAM_PREMATURE_CLOSE(); + } + } + if (writable && !writableFinished) { + if (!isWritableFinished(stream, false)) { + return new ERR_STREAM_PREMATURE_CLOSE(); + } + } + + return null; +} + +// Internal only: if eos() can settle immediately, invoke the callback before +// returning cleanup. Callers must tolerate cleanup yet to be assigned. +const kEosNodeSynchronousCallback = Symbol('kEosNodeSynchronousCallback'); + function eos(stream, options, callback) { if (arguments.length === 2) { callback = options; @@ -78,14 +123,6 @@ function eos(stream, options, callback) { validateFunction(callback, 'callback'); validateAbortSignal(options.signal, 'options.signal'); - if (AsyncContextFrame.current() || enabledHooksExist()) { - // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which - // is a bottleneck here. - callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM')); - } else { - callback = once(callback); - } - if (isReadableStream(stream) || isWritableStream(stream)) { return eosWeb(stream, options, callback); } @@ -97,15 +134,6 @@ function eos(stream, options, callback) { const readable = options.readable ?? isReadableNodeStream(stream); const writable = options.writable ?? isWritableNodeStream(stream); - const wState = stream._writableState; - const rState = stream._readableState; - - const onlegacyfinish = () => { - if (!stream.writable) { - onfinish(); - } - }; - // TODO (ronag): Improve soft detection to include core modules and // common ecosystem modules that do properly emit 'close' but fail // this generic check. @@ -114,8 +142,85 @@ function eos(stream, options, callback) { isReadableNodeStream(stream) === readable && isWritableNodeStream(stream) === writable ); - let writableFinished = isWritableFinished(stream, false); + let readableFinished = isReadableFinished(stream, false); + + const wState = stream._writableState; + const rState = stream._readableState; + + /** + * @type {Error | null | undefined} + * undefined: to be determined + * null: no error + * Error: an error occurred + */ + let immediateResult; + if (isClosed(stream)) { + immediateResult = getEosOnCloseError( + stream, + readable, + readableFinished, + writable, + writableFinished, + ); + } else if (wState?.errorEmitted || rState?.errorEmitted) { + if (!willEmitClose) { + immediateResult = getEosErrored(stream); + } + } else if ( + !readable && + (!willEmitClose || isReadable(stream)) && + (writableFinished || isWritable(stream) === false) && + (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) + ) { + immediateResult = getEosErrored(stream); + } else if ( + !writable && + (!willEmitClose || isWritable(stream)) && + (readableFinished || isReadable(stream) === false) + ) { + immediateResult = getEosErrored(stream); + } else if ((rState && stream.req && stream.aborted)) { + immediateResult = getEosErrored(stream); + } + let cleanup = () => { + callback = nop; + }; + if (immediateResult !== undefined) { + if (options.error !== false) { + stream.on('error', nop); + cleanup = () => { + callback = nop; + stream.removeListener('error', nop); + }; + } + } else if (options.signal?.aborted) { + immediateResult = new AbortError(undefined, { cause: options.signal.reason }); + } + if (immediateResult !== undefined && options[kEosNodeSynchronousCallback]) { + ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult]); + return cleanup; + } + + if (AsyncContextFrame.current() || enabledHooksExist()) { + // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which + // is a bottleneck here. + callback = bindAsyncResource(callback, 'STREAM_END_OF_STREAM'); + } + + if (immediateResult !== undefined) { + process.nextTick(() => ReflectApply(callback, stream, immediateResult === null ? [] : [immediateResult])); + return cleanup; + } + + callback = once(callback); + + const onlegacyfinish = () => { + if (!stream.writable) { + onfinish(); + } + }; + const onfinish = () => { writableFinished = true; // Stream should not be destroyed here. If it is that @@ -134,7 +239,6 @@ function eos(stream, options, callback) { } }; - let readableFinished = isReadableFinished(stream, false); const onend = () => { readableFinished = true; // Stream should not be destroyed here. If it is that @@ -157,41 +261,13 @@ function eos(stream, options, callback) { callback.call(stream, err); }; - let closed = isClosed(stream); - const onclose = () => { - closed = true; - - const errored = isWritableErrored(stream) || isReadableErrored(stream); - - if (errored && typeof errored !== 'boolean') { - return callback.call(stream, errored); - } - - if (readable && !readableFinished && isReadableNodeStream(stream, true)) { - if (!isReadableFinished(stream, false)) - return callback.call(stream, - new ERR_STREAM_PREMATURE_CLOSE()); - } - if (writable && !writableFinished) { - if (!isWritableFinished(stream, false)) - return callback.call(stream, - new ERR_STREAM_PREMATURE_CLOSE()); - } - - callback.call(stream); - }; - - const onclosed = () => { - closed = true; - - const errored = isWritableErrored(stream) || isReadableErrored(stream); - - if (errored && typeof errored !== 'boolean') { - return callback.call(stream, errored); + const error = getEosOnCloseError(stream, readable, readableFinished, writable, writableFinished); + if (error === null) { + callback.call(stream); + } else { + callback.call(stream, error); } - - callback.call(stream); }; const onrequest = () => { @@ -225,30 +301,7 @@ function eos(stream, options, callback) { } stream.on('close', onclose); - if (closed) { - process.nextTick(onclose); - } else if (wState?.errorEmitted || rState?.errorEmitted) { - if (!willEmitClose) { - process.nextTick(onclosed); - } - } else if ( - !readable && - (!willEmitClose || isReadable(stream)) && - (writableFinished || isWritable(stream) === false) && - (wState == null || wState.pendingcb === undefined || wState.pendingcb === 0) - ) { - process.nextTick(onclosed); - } else if ( - !writable && - (!willEmitClose || isWritable(stream)) && - (readableFinished || isReadable(stream) === false) - ) { - process.nextTick(onclosed); - } else if ((rState && stream.req && stream.aborted)) { - process.nextTick(onclosed); - } - - const cleanup = () => { + cleanup = () => { callback = nop; stream.removeListener('aborted', onclose); stream.removeListener('complete', onfinish); @@ -263,7 +316,7 @@ function eos(stream, options, callback) { stream.removeListener('close', onclose); }; - if (options.signal && !closed) { + if (options.signal) { const abort = () => { // Keep it because cleanup removes it. const endCallback = callback; @@ -272,23 +325,27 @@ function eos(stream, options, callback) { stream, new AbortError(undefined, { cause: options.signal.reason })); }; - if (options.signal.aborted) { - process.nextTick(abort); - } else { - addAbortListener ??= require('internal/events/abort_listener').addAbortListener; - const disposable = addAbortListener(options.signal, abort); - const originalCallback = callback; - callback = once((...args) => { - disposable[SymbolDispose](); - ReflectApply(originalCallback, stream, args); - }); - } + addAbortListener ??= require('internal/events/abort_listener').addAbortListener; + const disposable = addAbortListener(options.signal, abort); + const originalCallback = callback; + callback = once((...args) => { + disposable[SymbolDispose](); + ReflectApply(originalCallback, stream, args); + }); } return cleanup; } function eosWeb(stream, options, callback) { + if (AsyncContextFrame.current() || enabledHooksExist()) { + // Avoid AsyncResource.bind() because it calls ObjectDefineProperties which + // is a bottleneck here. + callback = once(bindAsyncResource(callback, 'STREAM_END_OF_STREAM')); + } else { + callback = once(callback); + } + let isAborted = false; let abort = nop; if (options.signal) { @@ -347,4 +404,5 @@ function finished(stream, opts) { module.exports = { eos, finished, + kEosNodeSynchronousCallback, }; diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index a57c1241bf82be..6fb78b816db7fd 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -90,7 +90,10 @@ const { streamBaseState, } = internalBinding('stream_wrap'); -const { eos } = require('internal/streams/end-of-stream'); +const { + eos, + kEosNodeSynchronousCallback, +} = require('internal/streams/end-of-stream'); const { zlib } = internalBinding('constants'); const { UV_EOF } = internalBinding('uv'); @@ -136,6 +139,8 @@ function handleKnownInternalErrors(cause) { } } +const noop = () => {}; + /** * @typedef {import('../../stream').Writable} Writable * @typedef {import('../../stream').Readable} Readable @@ -443,6 +448,8 @@ function newStreamWritableFromWritableStream(writableStream, options = kEmptyObj return writable; } +const kErrorSentinelAttached = Symbol('kErrorSentinelAttached'); + /** * @typedef {import('./queuingstrategies').QueuingStrategy} QueuingStrategy * @param {Readable} streamReadable @@ -469,89 +476,81 @@ function newReadableStreamFromStreamReadable(streamReadable, options = kEmptyObj } const isBYOB = options.type === 'bytes'; - - if (isDestroyed(streamReadable) || !isReadable(streamReadable)) { - const readable = new ReadableStream(); - readable.cancel(); - return readable; - } - - const objectMode = streamReadable.readableObjectMode; - const highWaterMark = streamReadable.readableHighWaterMark; - - const evaluateStrategyOrFallback = (strategy) => { - // If the stream is BYOB, we only use highWaterMark - if (isBYOB) - return { highWaterMark }; - // If there is a strategy available, use it - if (strategy) - return strategy; - - if (objectMode) { - // When running in objectMode explicitly but no strategy, we just fall - // back to CountQueuingStrategy - return new CountQueuingStrategy({ highWaterMark }); - } - - return new ByteLengthQueuingStrategy({ highWaterMark }); - }; - - const strategy = evaluateStrategyOrFallback(options?.strategy); - let controller; let wasCanceled = false; + let strategy; - function onData(chunk) { - // Copy the Buffer to detach it from the pool. - if (Buffer.isBuffer(chunk) && !objectMode) - chunk = new Uint8Array(chunk); - controller.enqueue(chunk); - if (controller.desiredSize <= 0) - streamReadable.pause(); - } + /** @type {UnderlyingSource} */ + const underlyingSource = { + __proto__: null, + type: isBYOB ? 'bytes' : undefined, + start(c) { controller = c; }, + cancel(reason) { + wasCanceled = true; + destroy(streamReadable, reason); + }, + }; - streamReadable.pause(); + const readable = isReadable(streamReadable); + const objectMode = streamReadable.readableObjectMode; + if (readable) { + underlyingSource.pull = function pull() { + streamReadable.resume(); + }; + + const highWaterMark = streamReadable.readableHighWaterMark; + strategy = isBYOB ? { highWaterMark } : + options.strategy ?? new (objectMode ? CountQueuingStrategy : ByteLengthQueuingStrategy)({ highWaterMark }); + } + const readableStream = new ReadableStream(underlyingSource, strategy); - const cleanup = eos(streamReadable, (error) => { + // When adapting a Duplex as a ReadableStream, readable completion should not + // wait for a half-open writable side to finish as well. + let cleanup = noop; + cleanup = eos(streamReadable, { + __proto__: null, + writable: false, + [kEosNodeSynchronousCallback]: true, + }, (error) => { error = handleKnownInternalErrors(error); + // If eos calls the callback synchronously, cleanup is still a no-op here. cleanup(); - // This is a protection against non-standard, legacy streams - // that happen to emit an error event again after finished is called. - streamReadable.on('error', () => {}); - if (error) - return controller.error(error); - // Was already canceled + + if (!(kErrorSentinelAttached in streamReadable)) { + // This is a protection against non-standard, legacy streams + // that happen to emit an error event again after finished is called. + streamReadable.on('error', noop); + streamReadable[kErrorSentinelAttached] = true; + } if (wasCanceled) { return; } + wasCanceled = true; + if (error) + return controller.error(error); controller.close(); + if (isBYOB) + controller.byobRequest?.respond(0); }); - streamReadable.on('data', onData); - - return new ReadableStream({ - type: isBYOB ? 'bytes' : undefined, - start(c) { - controller = c; - if (isBYOB) { - streamReadable.once('end', () => { - // close the controller - controller.close(); - // And unlock the last BYOB read request - controller.byobRequest?.respond(0); - wasCanceled = true; - }); - } - }, - - pull() { streamReadable.resume(); }, + if (wasCanceled) { + // `eos` called the callback synchronously + cleanup(); + } else if (readable) { + streamReadable.pause(); + + streamReadable.on('data', function onData(chunk) { + // Copy the Buffer to detach it from the pool. + if (Buffer.isBuffer(chunk) && !objectMode) + chunk = new Uint8Array(chunk); + controller.enqueue(chunk); + if (controller.desiredSize <= 0) + streamReadable.pause(); + }); + } - cancel(reason) { - wasCanceled = true; - destroy(streamReadable, reason); - }, - }, strategy); + return readableStream; } /** diff --git a/test/parallel/test-stream-finished.js b/test/parallel/test-stream-finished.js index b55107a7a6440f..6c2f3cad68e1d8 100644 --- a/test/parallel/test-stream-finished.js +++ b/test/parallel/test-stream-finished.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; const common = require('../common'); @@ -15,6 +16,7 @@ const EE = require('events'); const fs = require('fs'); const { promisify } = require('util'); const http = require('http'); +const { kEosNodeSynchronousCallback } = require('internal/streams/end-of-stream'); { const rs = new Readable({ @@ -95,13 +97,20 @@ const http = require('http'); { // Check pre-cancelled - const signal = new EventTarget(); - signal.aborted = true; + const signal = AbortSignal.abort(); const rs = Readable.from((function* () {})()); - finished(rs, { signal }, common.mustCall((err) => { + const cleanup = finished(rs, { signal }, common.mustCall((err) => { + assert.strictEqual(err.name, 'AbortError'); + cleanup(); + })); + const unset = Symbol('unset'); + let cleanup2 = unset; + cleanup2 = finished(rs, { signal, [kEosNodeSynchronousCallback]: true }, common.mustCall((err) => { assert.strictEqual(err.name, 'AbortError'); + assert.strictEqual(cleanup2, unset); })); + cleanup2(); } { @@ -160,8 +169,7 @@ const http = require('http'); // Promisified pre-aborted works const finishedPromise = promisify(finished); async function run() { - const signal = new EventTarget(); - signal.aborted = true; + const signal = AbortSignal.abort(); const rs = Readable.from((function* () {})()); await finishedPromise(rs, { signal }); } @@ -592,6 +600,29 @@ testClosed((opts) => new Writable({ write() {}, ...opts })); })); } +{ + let serverRes; + const server = http.createServer(common.mustCall((req, res) => { + serverRes = res; + res.write('hello'); + })).listen(0, common.mustCall(function() { + http.get({ port: this.address().port }, common.mustCall((res) => { + res.on('aborted', common.mustCall(() => { + finished(res, common.mustCall((err) => { + assert.strictEqual(err.code, 'ECONNRESET'); + assert.strictEqual(err.message, 'aborted'); + server.close(); + })); + })); + res.on('error', common.expectsError({ + code: 'ECONNRESET', + message: 'aborted', + })); + serverRes.destroy(); + })).on('error', common.mustNotCall()); + })); +} + { const w = new Writable({ write(chunk, encoding, callback) { diff --git a/test/parallel/test-stream-readable-to-web-termination.js b/test/parallel/test-stream-readable-to-web-termination.js index 13fce9bc715e1e..f30cf721e14cd8 100644 --- a/test/parallel/test-stream-readable-to-web-termination.js +++ b/test/parallel/test-stream-readable-to-web-termination.js @@ -1,6 +1,8 @@ 'use strict'; -require('../common'); -const { Readable } = require('stream'); +const common = require('../common'); +const assert = require('assert'); +const { Duplex, Readable } = require('stream'); +const { setTimeout: delay } = require('timers/promises'); { const r = Readable.from([]); @@ -10,3 +12,33 @@ const { Readable } = require('stream'); const reader = Readable.toWeb(r).getReader(); reader.read(); } + +{ + const duplex = new Duplex({ + read() { + this.push(Buffer.from('x')); + this.push(null); + }, + write(_chunk, _encoding, callback) { + callback(); + }, + }); + + const reader = Readable.toWeb(duplex).getReader(); + + (async () => { + const result = await reader.read(); + assert.deepStrictEqual(result, { + value: new Uint8Array(Buffer.from('x')), + done: false, + }); + + const closeResult = await Promise.race([ + reader.read(), + delay(common.platformTimeout(100)).then(() => 'timeout'), + ]); + + assert.notStrictEqual(closeResult, 'timeout'); + assert.deepStrictEqual(closeResult, { value: undefined, done: true }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js b/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js index 66af7b128c4d5a..eb01a39d8dc168 100644 --- a/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js +++ b/test/parallel/test-whatwg-webstreams-adapters-to-readablestream.js @@ -4,6 +4,7 @@ const common = require('../common'); const assert = require('assert'); +const { once } = require('events'); const { newReadableStreamFromStreamReadable, @@ -11,6 +12,7 @@ const { const { Duplex, + PassThrough, Readable, } = require('stream'); @@ -188,11 +190,142 @@ const { } { - const readable = new Readable(); + /** + * Runs the same assertion across finalize-before/after and + * default/BYOB adapter creation orders. + * @param {(readable: Readable) => void | Promise} finalize + * Finalizes the source stream before or after adaptation. + * @param {(readAndAssert: () => Promise, reader: ReadableStreamReader) => Promise} postAssert + * Asserts the resulting web stream state for the current case. + * @param {() => Readable} [createReadable] + * Creates the source stream for each case. + */ + function testConfluence(finalize, postAssert, createReadable = () => new PassThrough()) { + const cases = [false, true].flatMap((finalizeFirst) => [false, true].map((isBYOB) => ({ finalizeFirst, isBYOB }))); + Promise.all(cases.map(async (case_) => { + try { + const { isBYOB } = case_; + const readable = createReadable(); + if (case_.finalizeFirst) { + await finalize(readable); + } + /** @type {ReadableStream} */ + const readableStream = newReadableStreamFromStreamReadable(readable, { type: isBYOB ? 'bytes' : undefined }); + const reader = readableStream.getReader({ mode: isBYOB ? 'byob' : undefined }); + if (!case_.finalizeFirst) { + await finalize(readable); + } + + const readAndAssert = common.mustCall(() => { + return reader.read(isBYOB ? new Uint8Array(1) : undefined).then((result) => { + assert.deepStrictEqual(result, { + value: isBYOB ? new Uint8Array(0) : undefined, + done: true, + }); + }); + }); + await postAssert(readAndAssert, reader); + } catch (cause) { + throw new Error(`Case failed: ${JSON.stringify(case_)}`, { cause }); + } + })).then(common.mustCall()); + } + const error = new Error('boom'); + // Ending the readable without an error => closes the readableStream without an error + testConfluence( + async (readable) => { + readable.resume(); + readable.end(); + await once(readable, 'end'); + }, + common.mustCall(async (readAndAssert, reader) => { + await readAndAssert(); + await reader.closed; + }, 4) + ); + // Prematurely destroying the stream.Readable without an error + // => errors the ReadableStream with a premature close error + testConfluence( + (readable) => readable.destroy(), + common.mustCall(async (readAndAssert, reader) => { + const errorPredicate = { code: 'ABORT_ERR' }; + await assert.rejects(readAndAssert(), errorPredicate); + await assert.rejects(reader.closed, errorPredicate); + }, 4) + ); + // Asynchronously destroyed readable => errors the ReadableStream with a + // premature close error regardless of adapter creation order. + class AsyncDestroyReadable extends Readable { + _read() {} + + _destroy(error, callback) { + setImmediate(callback, error); + } + } + testConfluence( + (readable) => readable.destroy(), + common.mustCall(async (readAndAssert, reader) => { + const errorPredicate = { code: 'ABORT_ERR' }; + await assert.rejects(readAndAssert(), errorPredicate); + await assert.rejects(reader.closed, errorPredicate); + }, 4), + () => new AsyncDestroyReadable() + ); + // Destroying the readable with an error => errors the readableStream + testConfluence( + common.mustCall((readable) => { + readable.on('error', common.mustCall((reason) => { + assert.strictEqual(reason, error); + })); + readable.destroy(error); + }, 4), + common.mustCall(async (readAndAssert, reader) => { + await assert.rejects(readAndAssert(), error); + await assert.rejects(reader.closed, error); + }, 4) + ); +} + +{ + const readable = new PassThrough(); + readable.end(); readable.destroy(); - const readableStream = newReadableStreamFromStreamReadable(readable); - const reader = readableStream.getReader(); - reader.closed.then(common.mustCall()); + + (async () => { + await new Promise((resolve) => readable.once('close', resolve)); + assert.strictEqual(readable.listenerCount('error'), 0); + + const readableStream = newReadableStreamFromStreamReadable(readable); + // Only one error listener from the adapter should be added + assert.strictEqual(readable.listenerCount('error'), 1); + newReadableStreamFromStreamReadable(readable); + // No duplicate listeners should be added. + assert.strictEqual(readable.listenerCount('error'), 1); + + const readResult = await readableStream.getReader().read(); + assert.deepStrictEqual(readResult, { value: undefined, done: true }); + })().then(common.mustCall()); +} + +{ + const readable = new PassThrough(); + const readableStream = newReadableStreamFromStreamReadable(readable, { + type: 'bytes', + }); + const reader = readableStream.getReader({ mode: 'byob' }); + + (async () => { + const readPromise = reader.read(new Uint8Array(8)); + + readable.end(); + await once(readable, 'end'); + + assert.deepStrictEqual(await readPromise, { + value: new Uint8Array(0), + done: true, + }); + await reader.closed; + })().then(common.mustCall()); } { From 381f4b1b106a63e20ae9477bbaa1a3a4b2f9da81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Tue, 19 May 2026 16:52:32 +0100 Subject: [PATCH 002/143] stream: disallow writing string chunk with 'buffer' encoding Signed-off-by: Renegade334 PR-URL: https://github.com/nodejs/node/pull/63062 Refs: https://github.com/nodejs/node/pull/33075 Reviewed-By: Luigi Pinca Reviewed-By: Ethan Arrowood Reviewed-By: Matteo Collina --- lib/internal/streams/writable.js | 3 +++ src/stream_base.cc | 6 +---- .../parallel/test-stream-base-typechecking.js | 18 --------------- .../test-stream-writable-write-error.js | 22 +++++++++++++------ 4 files changed, 19 insertions(+), 30 deletions(-) delete mode 100644 test/parallel/test-stream-base-typechecking.js diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index 89d384e9f2fb8f..94913461cb6044 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -466,6 +466,9 @@ function _write(stream, chunk, encoding, cb) { } if (typeof chunk === 'string') { + if (encoding === 'buffer') { + throw new ERR_UNKNOWN_ENCODING(encoding); + } if ((state[kState] & kDecodeStrings) !== 0) { chunk = Buffer.from(chunk, encoding); encoding = 'buffer'; diff --git a/src/stream_base.cc b/src/stream_base.cc index 6a631921341307..370b8f682eadae 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -296,14 +296,10 @@ int StreamBase::Writev(const FunctionCallbackInfo& args) { int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { CHECK(args[0]->IsObject()); + CHECK(args[1]->IsUint8Array()); Environment* env = Environment::GetCurrent(args); - if (!args[1]->IsUint8Array()) { - node::THROW_ERR_INVALID_ARG_TYPE(env, "Second argument must be a buffer"); - return 0; - } - Local req_wrap_obj = args[0].As(); uv_buf_t buf; buf.base = Buffer::Data(args[1]); diff --git a/test/parallel/test-stream-base-typechecking.js b/test/parallel/test-stream-base-typechecking.js deleted file mode 100644 index ae8582642344ee..00000000000000 --- a/test/parallel/test-stream-base-typechecking.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const net = require('net'); - -const server = net.createServer().listen(0, common.mustCall(() => { - const client = net.connect(server.address().port, common.mustCall(() => { - assert.throws(() => { - client.write('broken', 'buffer'); - }, { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - message: 'Second argument must be a buffer' - }); - client.destroy(); - server.close(); - })); -})); diff --git a/test/parallel/test-stream-writable-write-error.js b/test/parallel/test-stream-writable-write-error.js index 069e32e1be8e3e..76f797fb1bd583 100644 --- a/test/parallel/test-stream-writable-write-error.js +++ b/test/parallel/test-stream-writable-write-error.js @@ -31,7 +31,7 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); w.end(); expectError(w, ['asd'], 'ERR_STREAM_WRITE_AFTER_END'); @@ -40,7 +40,7 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); w.destroy(); } @@ -48,7 +48,7 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); expectError(w, [null], 'ERR_STREAM_NULL_VALUES', true); } @@ -56,18 +56,26 @@ function test(autoDestroy) { { const w = new Writable({ autoDestroy, - _write() {} + write() {} }); expectError(w, [{}], 'ERR_INVALID_ARG_TYPE', true); } { const w = new Writable({ - decodeStrings: false, autoDestroy, - _write() {} + write() {} + }); + expectError(w, ['asd', 'buffer'], 'ERR_UNKNOWN_ENCODING', true); + } + + { + const w = new Writable({ + autoDestroy, + decodeStrings: false, + write() {} }); - expectError(w, ['asd', 'noencoding'], 'ERR_UNKNOWN_ENCODING', true); + expectError(w, ['asd', 'buffer'], 'ERR_UNKNOWN_ENCODING', true); } } From 5f4d79405274c93e4498507ba72f7fe842824e7a Mon Sep 17 00:00:00 2001 From: Mike McCready <66998419+MikeMcC399@users.noreply.github.com> Date: Tue, 19 May 2026 22:00:55 +0200 Subject: [PATCH 003/143] build,win: add Rust toolchain automated configuration Windows Signed-off-by: Mike McCready <66998419+MikeMcC399@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/63381 Refs: https://github.com/nodejs/node/issues/63225 Refs: https://github.com/nodejs/node/pull/63367 Reviewed-By: Joyee Cheung Reviewed-By: Stefan Stojanovic Reviewed-By: Chengzhong Wu --- .configurations/configuration.dsc.yaml | 9 ++++++++- .configurations/configuration.vsBuildTools.dsc.yaml | 9 ++++++++- .configurations/configuration.vsEnterprise.dsc.yaml | 9 ++++++++- .configurations/configuration.vsProfessional.dsc.yaml | 9 ++++++++- BUILDING.md | 1 + 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.configurations/configuration.dsc.yaml b/.configurations/configuration.dsc.yaml index 9e38aaba78a412..8c637d7dae819a 100644 --- a/.configurations/configuration.dsc.yaml +++ b/.configurations/configuration.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.NativeDesktop - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.configurations/configuration.vsBuildTools.dsc.yaml b/.configurations/configuration.vsBuildTools.dsc.yaml index 5434b44b3e0459..3e3a51ca90260f 100644 --- a/.configurations/configuration.vsBuildTools.dsc.yaml +++ b/.configurations/configuration.vsBuildTools.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.VCTools - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.configurations/configuration.vsEnterprise.dsc.yaml b/.configurations/configuration.vsEnterprise.dsc.yaml index 4faf7d77d371d6..124a391b7843fa 100644 --- a/.configurations/configuration.vsEnterprise.dsc.yaml +++ b/.configurations/configuration.vsEnterprise.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.NativeDesktop - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/.configurations/configuration.vsProfessional.dsc.yaml b/.configurations/configuration.vsProfessional.dsc.yaml index e094059e826c0e..7d2a8b36984ccf 100644 --- a/.configurations/configuration.vsProfessional.dsc.yaml +++ b/.configurations/configuration.vsProfessional.dsc.yaml @@ -35,6 +35,13 @@ properties: - Microsoft.VisualStudio.Workload.NativeDesktop - Microsoft.VisualStudio.Component.VC.Llvm.Clang - Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset + - resource: Microsoft.WinGet.DSC/WinGetPackage + id: rustPackage + directives: + description: Install Rust with MSVC toolchain + settings: + id: Rustlang.Rust.MSVC + source: winget - resource: Microsoft.WinGet.DSC/WinGetPackage id: gitPackage directives: @@ -51,4 +58,4 @@ properties: settings: id: Nasm.Nasm source: winget - configurationVersion: 0.1.1 + configurationVersion: 0.2.0 diff --git a/BUILDING.md b/BUILDING.md index f7364e1499febd..d74d4ae4d0ada3 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -796,6 +796,7 @@ easily. These files will install the following * `Python 3.14` * `Visual Studio 2022` (Build Tools, Community, Professional or Enterprise Edition) and "Desktop development with C++" workload, Clang and ClangToolset optional components +* `Rust Toolchain MSVC` * `NetWide Assembler` The following Desired State Configuration (DSC) files are available: From 259d8b3dcee4193c745c412fb702240c2582b53c Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Wed, 20 May 2026 16:21:23 -0400 Subject: [PATCH 004/143] test: update WPT for WebCryptoAPI to 97bbc7247a PR-URL: https://github.com/nodejs/node/pull/63417 Reviewed-By: Filip Skokan Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig --- test/fixtures/wpt/README.md | 2 +- .../import_export/ML-KEM_importKey.js | 10 ++-- .../ML-KEM_importKey_fixtures.js | 18 ++++++ .../serialization/aes-cbc.https.any.js | 11 ++++ .../serialization/aes-ctr.https.any.js | 11 ++++ .../serialization/aes-gcm.https.any.js | 11 ++++ .../serialization/aes-kw.https.any.js | 11 ++++ .../aes-ocb.tentative.https.any.js | 11 ++++ .../chacha20-poly1305.tentative.https.any.js | 11 ++++ .../serialization/ecdh.https.any.js | 12 ++++ .../serialization/ecdsa.https.any.js | 12 ++++ .../serialization/ed25519.https.any.js | 12 ++++ .../ed448.tentative.https.any.js | 12 ++++ .../serialization/hmac.https.any.js | 11 ++++ .../serialization/kmac.tentative.https.any.js | 17 ++++++ .../mldsa.tentative.https.any.js | 27 +++++++++ .../mlkem.tentative.https.any.js | 32 +++++++++++ .../serialization/rsa-oaep.https.any.js | 12 ++++ .../serialization/rsa-pss.https.any.js | 12 ++++ .../rsassa-pkcs1-v1_5.https.any.js | 12 ++++ .../serialization/serialization.js | 55 +++++++++++++++++++ .../serialization/x25519.https.any.js | 12 ++++ .../serialization/x448.tentative.https.any.js | 12 ++++ test/fixtures/wpt/versions.json | 2 +- test/wpt/status/WebCryptoAPI.cjs | 7 +++ 25 files changed, 348 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/aes-cbc.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/aes-ctr.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/aes-gcm.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/aes-kw.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/aes-ocb.tentative.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/chacha20-poly1305.tentative.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/ecdh.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/ecdsa.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/ed25519.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/ed448.tentative.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/hmac.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/kmac.tentative.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/mldsa.tentative.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/mlkem.tentative.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/rsa-oaep.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/rsa-pss.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/rsassa-pkcs1-v1_5.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/serialization.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js create mode 100644 test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index c98284f664a0af..10cedb4149edde 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -34,7 +34,7 @@ Last update: - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/288c467d35/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi - web-locks: https://github.com/web-platform-tests/wpt/tree/10a122a6bc/web-locks -- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/8b5cd267b4/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/97bbc7247a/WebCryptoAPI - webidl: https://github.com/web-platform-tests/wpt/tree/63ca529a02/webidl - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js index ad2f75048556e4..d9257ac6982505 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js @@ -3,14 +3,14 @@ var subtle = crypto.subtle; function runTests(algorithmName) { var algorithm = { name: algorithmName }; var data = keyData[algorithmName]; - // var jwkData = {jwk: {kty: data.jwk.kty, alg: data.jwk.alg, pub: data.jwk.pub}}; - - // TODO: add JWK when its definition is done in IETF JOSE WG + var jwkData = { + jwk: { kty: data.jwk.kty, alg: data.jwk.alg, pub: data.jwk.pub }, + }; [true, false].forEach(function (extractable) { // Test public keys first allValidUsages(data.publicUsages, true).forEach(function (usages) { - ['spki', /*'jwk',*/ 'raw-public'].forEach(function (format) { + ['spki', 'jwk', 'raw-public'].forEach(function (format) { if (format === 'jwk') { // Not all fields used for public keys testFormat( @@ -36,7 +36,7 @@ function runTests(algorithmName) { // Next, test private keys allValidUsages(data.privateUsages).forEach(function (usages) { - ['pkcs8', /*'jwk',*/ 'raw-seed'].forEach(function (format) { + ['pkcs8', 'jwk', 'raw-seed'].forEach(function (format) { testFormat(format, algorithm, data, algorithmName, usages, extractable); }); }); diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js index 1c25d37614e92e..f3a5f6687dc5e1 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js @@ -23,6 +23,12 @@ var keyData = { 80, 130, 235, 161, 49, 141, 160, 11, 40, 152, 18, 142, 30, 56, 252, 129, 211, ]), + jwk: { + priv: 'pSbBpIR6aK1T1uMf57eY5GCMQRdTgwjN9cB64vRehm12PK3LGUu9kBYlozVOlblQguuhMY2gCyiYEo4eOPyB0w', + kty: 'AKP', + alg: 'ML-KEM-512', + pub: 'NmgRwWK5Bbwb-FQJRnK_kDCVThmIH7wdYMRUaaUaIFQoZJQK5bJh0zEtyFpBboqtcZLG7tibd5wJQpNDnnkvmHZjC3QZl2iOLolHnjQPe7FuOPGP_EqSF0kziAZ7cHQ2UPgJVcVhQWYsqhdjAkEsTqyrUvOlclGMjxSOdNF0e9qVxWZRjvFzTudlmfKGdPsO9gmnKRYsHqbDm4mdVJOlRhpeZIUUwPRF3KrIUBul5CpUPVuUdPrM1Ola15ISYJU_mDI0ECKpElWtiAmRcOWxoiGd3tGWwPyorGqmJCabuEI7ZaqjIQoygXlWanvIxEVI75d2aroa4ZM1dgMJAiWM_Pcet7EEieafA7pdP1EQ6utJs9oYDXsngBZaOXiqc_hnm3KzKQUZIkiPA9BQgxN53RMG51ZC2HVHwoC7iaVQsDi4u9iFRZCplXWPZXQMcSpH_sBy4cEVDgW-AZweDpIW9rICT9xOHwwwmvWtNMmXmDZ0-vNN14kHGdBOP4N0-YIj1nwdkwIHskKfAnlDVcrA0Rx-EFixxcQiXDq-xeBc-gY6UdR6LDQeifp7BRwqTicqBYOCdYHJpIN2pweK70puM3Iy_kHIPhIazCBnJvOKl3YlYRsPR8BU12W3LJS4kOUfvXiNqIKrqXxFv7BxURULIfvN7myEBrc9OpivU_IfiOOy-5jA8RRxLGsWnppqQdINFuqvYtEn31obk2CVnESA7hnOERuM7EOdU-M8MsN0QSlmZJxAN3VavFBl6uOq8FJJvfydUvEWc3vPrnB2g_KmXuwtprObdlO9_1JWp2UnenFpOClD7QnMKtaiBmWOGZZV6dmIWrtY9_RZ01Gn_LORv8JoWQhAb2tMrRVtF9M1Jjx0jIO5jlSx9MgVzYte18YCMTCPY-J5Zuo3Q6o-LdLInnUq6cVugdQMjhJ_8SuXmsBYIHXN9FMKttWI6YKcLVYjHstsXiEP0kOx4fd4kZaKH-S3vCIl9CA5BiUXjxK108ooEpY9lTxH_yGCQRnEkve0HwWwHfvagpKsDDWzRIh4xAHRsY9_xcPO-JlXbUZ6_HA', + }, }, 'ML-KEM-768': { privateUsages: ['decapsulateBits', 'decapsulateKey'], @@ -47,6 +53,12 @@ var keyData = { 201, 9, 240, 170, 192, 86, 52, 228, 122, 16, 73, 139, 30, 11, 224, 117, 143, 2, 124, 21, 63, 91, 210, 94, 160, 123, 99, 189, 172, 61, 235, 191, ]), + jwk: { + priv: 'psgdZMYwUKSP1wJsaqLna2-PAg63_ZwRW9VLyHBZ8zfJCfCqwFY05HoQSYseC-B1jwJ8FT9b0l6ge2O9rD3rvw', + kty: 'AKP', + alg: 'ML-KEM-768', + pub: '7lhoaxGSnpU0O6dvndAgrKiCDLwlHeMBR4ZatDw5lYwtPCIEI9CbmfqjX7kTAcoJsEKh-oityrERRVegtAmShCZFdzGGVjRh3Yh594KhKZfGPYyUnTq8eBETGfl0Gldu8BBimWuzEGhghoOHYPa0ygEfUQZhU8qZ8nA6TbiuNSdPJ8xdfxtgiEiCe3Z9SqhF3aerJoyuc2uAKbGPafrCd7rDsitzFPI1TVYaKWC1OblyKVu8PhWtEdx8VYRftaUJBwfKEnQSNvWLZTVEbTqgJQNwHLmQyMSktnjNKDmrF2C-TkKFOrenC4QAEGQvcrAuidpcuPNl8FVMDKUsIRY5aiQvEsdRxJcWZGJ6hUVzzseVrgBM0NFrIDkpe5Wga-hSZggbACIvWzISJgNkznp7oJM36FxeVbkHDXTNaLQ-PHR7pTwrpzUCmiecewGZ5ZIE6ooPaMC84vdxlqB-WKQV2gs_t1mO20NnZcCyqPkGNNtueOkGZlJORvhYBbqEhdKdtLF4b-oo-CBrHgAKRGVaAGoFEleSCJFusCsX_ajBKQkvNSdJXDJC82QGXbnKL1sK9QLOzTYV_xgZWMW3tRFYMMWSz5w000irtFDNcmJcUbkn0BGg-vdzceY46eOeg3JE2rh-9fVLzhZMq6UG8YyhBZMonEx-JalyninCurYWk1aEVic0n7cQ1MMPvnJyKolXubWNKFGiB7K1H2mG5TafRrJvCQlXfSoncaLHr9SGr2UQvyvGNMTGYcFIxnCp3lyU42a157x3OwINDcA39OLD0hJ7JhdeVGkeIQp9VRt1vaOUEEPFVXuXIPtBqMu-9gOfL3hNh2aixMCP6VS6gypYfYNNCTZ9O7pJ81sFfEJv4-yR5zBZ0IYi4saeeoUJQgmohBcm9IaUDoWAlVhbxKoV1SQ22juWvXas5flgyzVh-GV7bpVoH-SawqlAEnxE-vWWVXLMRXYuh_CByxYvFPVCYeS4GuKF9kpT7rWQ6gM-T-gV5zS62EQ7qrxZL7DOjkqXnqeaD7pMxWRKt4QFdYupnNgfI1wt8Mxjn_CzKOOBHZIdKooppSI2BOpmvyu1zcDJX1snbBNcLVtMdbBYJXOf1RXIEXWCAOKf1jNGLRyOazhlbOpmtBxRsqO5-Lq9odptmUO3FtQ23VqQ0nhFHeFO8yMpOAp2H3widhpFQbKbYKKrtfjGL_kXwxqlh4k1nAmCXzvKe7wzlyow1NgEx_e6MRkc_PIsekgytkAhsPSYc0t_AuEy5xyqN_m_JMbLJCwaFVtoPVoKPNRzFtM1YrNSrtAOg0MLMna5eJN9CotPh0MywwxzKfyvFnEr-FKMr9UgOExqrZWY1gFeDot5S9rJhErLrZWAI8cn2tUuOGMg1LozqOxJIOyHsAERJ4W7jJQ5dWosbzdsiSeXAauX4fB_VWbAxmxNhreT1mJAF5Owx1OLJGYTFsmKqSI4HsGvlVpwQcClh8ZIBmWiLwUstOO401BGBsh2lbyWUmRGQCgSymgMP6IuQ7hAnWAp7CyO92VWNfXMV3ynevpq4a8MzDsgXnFisMWx_7w1vsdnSvNfo8E', + }, }, 'ML-KEM-1024': { privateUsages: ['decapsulateBits', 'decapsulateKey'], @@ -72,5 +84,11 @@ var keyData = { 251, 65, 39, 3, 101, 145, 174, 21, 195, 144, 234, 14, 158, 32, 49, 57, 142, 113, ]), + jwk: { + priv: 'mHWy5rWG9IGnfaYItV_O0ohw230GdnpTJvQ5zTJiOy7oQhGRzPyiuXR08hyEwftBJwNlka4Vw5DqDp4gMTmOcQ', + kty: 'AKP', + alg: 'ML-KEM-1024', + pub: 'IUe4I2Yu3_Ix1zZYFhRrTaBXm9KI8sZbdfbBXtsk8yV5ZBa0HYip0qHDrSEniEVDDngDv9Nhibg-OKdEgFy2NEaVa4WbHQxsVrUO11sV5LZ3H4Ww7CNG_3Ye-4h-vRw0B8eYJSLFGouhMRs8Q4q0ZhVp1wI8fIOoM-cXxypf6Kw_hLZD2TCkA3dN8hxk_tC5o5aZ8bFw7UyFJhgjmcEvzaYV6VqzokdodPty1jWOmguRXmydfvE1RjUHodUFOIhSzwuH3BgYlVk2Xcp3IPyyjnSNLBwLKEg4LlANCUvENPY6EJNgp-toE9wqk9ao_0t5HPpaqDAU0frBe9MhAHuvH3DHWlukxSRy7rmqC2tra9XCbTxp9_QRrCEs8Aa956fFmjN_z_EAGEdS7QqIxfi414ECFAWcTpGmC4y-VHSUC3UUGjJW1eNHzLOrk6g0dgqNFwF2h9h56NKddiQ3S2JUSTmAaiQj5FKmswNW61Ie24Yo4qcQOGKg9ioKGPmPyZDJ5Xwgchh6vsqu7-cOWZiRd8xAjvCrf6oiDXEjs7i8faEZqJAkVmqbfqyy4CW5astIffZYnBxsckuqHylk6VyrE8GalasJ65aY2ft0femcNJh5xSTBsmTHJtyj2hU1FcyNndgrLbdpcQobXdmyX0i3nsN6HXwKv_iAV6B6jCaTJVgLINwr0PW9XHaKLXKSw9QgmGc47qzCjiiEBZVrzxKz0ohDzlcGCvRapgDGakg1nBW54eRbuJi7kRynkSAaK2FKkuJ60Exw5PgGpBRRrFGI_KVbKMw7GkvOP6c3F0obH0iuk1TOsim92KJIixrOK2KECCrMODGAtDOVlxQxhDG57UGAp2KeGIwzhknHMCO081UV4Iy9dPctfVs6DlUwjHiMb9c5deJX7vwSVHxXqSUL52N-QNpBMXEX3TlsHafGewhSQhk3SfRNVxLAqqpWxyRxAKMKKsBEFDi1LsQGlQdF_RwjtNHMBOwJEeSy3lSAiLmInLVQ9-xUYXWZJmyY8jCwU4OUrSg3v0ge0cAtglA3UwAX5DU4i2AOT8YPkuR3DseMsirFEEOc2pI7tCpy4YxdRyqY9XWRNYTCI2UAtAdIKaaslRMEhnZpI8VYBqUehNCvkPIY8GxuA_o3XlopD8g8gBlqqqqxqYNdrQVLiWjCZyajXOBwL8uWiMOSQWcmHztxlRJ3TjqDZSYlZ2RZzBWGy7O1bXsSPAcu64EyWPFYgWEHAwGUAOodl5F95acdk0C1ogE3aBSz-SGqWddCZUUE1YlZ8etr-Dt8_9y4IUaixqxDZcZcU0ZiaDBNiKQWkIl3yIvKF3YwUdgXeyBg3jkUDDyTE2vLFYtzH-Vl6aOBNJTHaKESEUFyo4FW6qkwU5bNuXJed6ygiLZES6MKXGkHZzxt0xN_-1GWMcwa5Rxfq7V6KSh64sWSmEyXtXBnhesfUuMXfMFXFNzCanuIhIdabku6uPutNSOuRgW-5aajfVV_Bbtgx9eibpZHfYw42eqcJUdtsZEYgRUkzVgYxtFn6WyhQqhDeXGry4lUJrMGUWmmr5yGvbW8u1GFS7LITjODYvms1kKtRpEdNCIJICwlyQwhn_eVsYc-OWMG71iNeqELTgRj1olWgJpXenFMe0K8ctyMnSNY6LQOd-tTtUx8cQnKmxA5ThtrhYtS8JS0iDmUUcikf8aJxxZs7hh9HQp9jttI4PsBo8dKdMRHJQLEW1cY8nGG21C1nRzKKeet8hNA2va3T-xznbdzkEJ1FNae1fMWhZUO1pRAGUfB_ARPnAeNRRFn-XmLCwtKtRy_GrpHsmhlVZdvioJVZGoB9JusQamo7PhciUmTbpx69rc55nF7EKjOyHFaH3O5GCmulSLJK0fKpTG2d0KWlKDK13IK4PMvVpd0kegv-VSyF3as7QwEuVQrdJJOHZi5MZMsF4vISwgGdcN5JBdFdCXAvgmOqlpR0Li6HCUoD0GXs7KPZwCADLk1lynACrq_HodeXadFvpORW0ok07UF58Swb-SbdatO2dQjfwWB0dC2a9JINjSS6WGfrIKrOriqP-5wjVKdo5w1493fQtp3aXqOEio', + }, }, }; diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/aes-cbc.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-cbc.https.any.js new file mode 100644 index 00000000000000..840566639cd799 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-cbc.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'AES-CBC', + resultType: 'CryptoKey', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + exportFormat: 'raw' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/aes-ctr.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-ctr.https.any.js new file mode 100644 index 00000000000000..3b3fb6822043f3 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-ctr.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'AES-CTR', + resultType: 'CryptoKey', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + exportFormat: 'raw' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/aes-gcm.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-gcm.https.any.js new file mode 100644 index 00000000000000..b54eba677b965b --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-gcm.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'AES-GCM', + resultType: 'CryptoKey', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + exportFormat: 'raw' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/aes-kw.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-kw.https.any.js new file mode 100644 index 00000000000000..9948dce9b395e7 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-kw.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'AES-KW', + resultType: 'CryptoKey', + usages: ['wrapKey', 'unwrapKey'], + exportFormat: 'raw' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/aes-ocb.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-ocb.tentative.https.any.js new file mode 100644 index 00000000000000..33b814410c3ff4 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/aes-ocb.tentative.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'AES-OCB', + resultType: 'CryptoKey', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + exportFormat: 'raw-secret' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/chacha20-poly1305.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/chacha20-poly1305.tentative.https.any.js new file mode 100644 index 00000000000000..aaaa24a0b662bc --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/chacha20-poly1305.tentative.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'ChaCha20-Poly1305', + resultType: 'CryptoKey', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + exportFormat: 'raw-secret' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/ecdh.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/ecdh.https.any.js new file mode 100644 index 00000000000000..17df8a87ba6ffb --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/ecdh.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'ECDH', + resultType: 'CryptoKeyPair', + usages: ['deriveKey', 'deriveBits'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/ecdsa.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/ecdsa.https.any.js new file mode 100644 index 00000000000000..679dc066f53193 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/ecdsa.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'ECDSA', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/ed25519.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/ed25519.https.any.js new file mode 100644 index 00000000000000..831e91725b6bae --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/ed25519.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'Ed25519', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/ed448.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/ed448.tentative.https.any.js new file mode 100644 index 00000000000000..4abe56285689b8 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/ed448.tentative.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'Ed448', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/hmac.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/hmac.https.any.js new file mode 100644 index 00000000000000..99efea6bcaa996 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/hmac.https.any.js @@ -0,0 +1,11 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'HMAC', + resultType: 'CryptoKey', + usages: ['sign', 'verify'], + exportFormat: 'raw' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/kmac.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/kmac.tentative.https.any.js new file mode 100644 index 00000000000000..645d16bec776fb --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/kmac.tentative.https.any.js @@ -0,0 +1,17 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'KMAC128', + resultType: 'CryptoKey', + usages: ['sign', 'verify'], + exportFormat: 'raw-secret' + }, + { + name: 'KMAC256', + resultType: 'CryptoKey', + usages: ['sign', 'verify'], + exportFormat: 'raw-secret' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/mldsa.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/mldsa.tentative.https.any.js new file mode 100644 index 00000000000000..3f61248b3d9e49 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/mldsa.tentative.https.any.js @@ -0,0 +1,27 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js + +run_test([ + { + name: 'ML-DSA-44', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'raw-public', + privateFormat: 'raw-seed' + }, + { + name: 'ML-DSA-65', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'raw-public', + privateFormat: 'raw-seed' + }, + { + name: 'ML-DSA-87', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'raw-public', + privateFormat: 'raw-seed' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/mlkem.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/mlkem.tentative.https.any.js new file mode 100644 index 00000000000000..62a210f0cda218 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/mlkem.tentative.https.any.js @@ -0,0 +1,32 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'ML-KEM-512', + resultType: 'CryptoKeyPair', + usages: [ + 'decapsulateBits', 'decapsulateKey', 'encapsulateBits', 'encapsulateKey' + ], + publicFormat: 'raw-public', + privateFormat: 'raw-seed' + }, + { + name: 'ML-KEM-768', + resultType: 'CryptoKeyPair', + usages: [ + 'decapsulateBits', 'decapsulateKey', 'encapsulateBits', 'encapsulateKey' + ], + publicFormat: 'raw-public', + privateFormat: 'raw-seed' + }, + { + name: 'ML-KEM-1024', + resultType: 'CryptoKeyPair', + usages: [ + 'decapsulateBits', 'decapsulateKey', 'encapsulateBits', 'encapsulateKey' + ], + publicFormat: 'raw-public', + privateFormat: 'raw-seed' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/rsa-oaep.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/rsa-oaep.https.any.js new file mode 100644 index 00000000000000..42469307155d50 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/rsa-oaep.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'RSA-OAEP', + resultType: 'CryptoKeyPair', + usages: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + publicFormat: 'spki', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/rsa-pss.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/rsa-pss.https.any.js new file mode 100644 index 00000000000000..a1255b7638bc37 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/rsa-pss.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'RSA-PSS', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'spki', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/rsassa-pkcs1-v1_5.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/rsassa-pkcs1-v1_5.https.any.js new file mode 100644 index 00000000000000..051eb87e07856e --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/rsassa-pkcs1-v1_5.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'RSASSA-PKCS1-v1_5', + resultType: 'CryptoKeyPair', + usages: ['sign', 'verify'], + publicFormat: 'spki', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/serialization.js b/test/fixtures/wpt/WebCryptoAPI/serialization/serialization.js new file mode 100644 index 00000000000000..a9444032503268 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/serialization.js @@ -0,0 +1,55 @@ +function run_test(vectors) { + function testCryptoKeySerialization( + generateKeyAlgorithm, generateKeyUsages, exportFormat) { + promise_test(async t => { + var cryptoKey = await crypto.subtle.generateKey( + generateKeyAlgorithm, true, generateKeyUsages); + const keyExported = + await crypto.subtle.exportKey(exportFormat, cryptoKey); + + const {key} = structuredClone({key: cryptoKey}); + const newKeyExported = + await crypto.subtle.exportKey(exportFormat, key); + assert_true(equalBuffers(keyExported, newKeyExported)); + }, 'serialization test ' + objectToString(generateKeyAlgorithm)); + }; + + function testCryptoKeyPairSerialization( + generateKeyAlgorithm, generateKeyUsages, publicExportFormat, + privateExportFormat) { + promise_test(async t => { + var keyPair = await crypto.subtle.generateKey( + generateKeyAlgorithm, true, generateKeyUsages); + const publicKeyExported = + await crypto.subtle.exportKey(publicExportFormat, keyPair.publicKey); + const privateKeyExported = await crypto.subtle.exportKey( + privateExportFormat, keyPair.privateKey); + + const {publicKey, privateKey} = structuredClone( + {publicKey: keyPair.publicKey, privateKey: keyPair.privateKey}); + const newPublicKeyExported = + await crypto.subtle.exportKey(publicExportFormat, publicKey); + assert_true(equalBuffers(publicKeyExported, newPublicKeyExported)); + const newPrivateKeyExported = await crypto.subtle.exportKey( + privateExportFormat, privateKey); + assert_true(equalBuffers(privateKeyExported, newPrivateKeyExported)); + }, 'serialization test ' + objectToString(generateKeyAlgorithm)); + }; + + vectors.forEach(function(vector) { + if (vector.resultType === 'CryptoKey') { + allAlgorithmSpecifiersFor(vector.name) + .forEach(function(generateKeyAlgorithm) { + testCryptoKeySerialization( + generateKeyAlgorithm, vector.usages, vector.exportFormat); + }); + } else { + allAlgorithmSpecifiersFor(vector.name) + .forEach(function(generateKeyAlgorithm) { + testCryptoKeyPairSerialization( + generateKeyAlgorithm, vector.usages, vector.publicFormat, + vector.privateFormat); + }); + } + }); +} diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js new file mode 100644 index 00000000000000..618bf29ebfe206 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'X25519', + resultType: 'CryptoKeyPair', + usages: ['deriveKey', 'deriveBits'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js new file mode 100644 index 00000000000000..cafc00144a3ce7 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'X448', + resultType: 'CryptoKeyPair', + usages: ['deriveKey', 'deriveBits'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 5a461bda392613..470592c842b925 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -96,7 +96,7 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "8b5cd267b480d75bce41aa306bebbd07ce414fa5", + "commit": "97bbc7247a16231f4744a47a1d9b3d29633d5292", "path": "WebCryptoAPI" }, "webidl": { diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 4b01978511548f..c31831a6ea6a12 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -36,6 +36,8 @@ if (!hasOpenSSL(3, 0)) { 'generateKey/successes_kmac.tentative.https.any.js', 'import_export/AES-OCB_importKey.tentative.https.any.js', 'import_export/KMAC_importKey.tentative.https.any.js', + 'serialization/aes-ocb.tentative.https.any.js', + 'serialization/kmac.tentative.https.any.js', 'sign_verify/kmac.tentative.https.any.js'); } @@ -55,6 +57,8 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { 'generateKey/successes_ML-KEM.tentative.https.any.js', 'import_export/ML-DSA_importKey.tentative.https.any.js', 'import_export/ML-KEM_importKey.tentative.https.any.js', + 'serialization/mldsa.tentative.https.any.js', + 'serialization/mlkem.tentative.https.any.js', 'sign_verify/mldsa.tentative.https.any.js'); skipSubtests( @@ -75,6 +79,8 @@ if (process.features.openssl_is_boringssl) { 'import_export/okp_importKey_failures_Ed448.tentative.https.any.js', 'import_export/okp_importKey_failures_X448.tentative.https.any.js', 'import_export/okp_importKey_X448.tentative.https.any.js', + 'serialization/ed448.tentative.https.any.js', + 'serialization/x448.tentative.https.any.js', 'sign_verify/eddsa_curve448.tentative.https.any.js'); skipSubtests( @@ -83,6 +89,7 @@ if (process.features.openssl_is_boringssl) { ['generateKey/failures_ML-KEM.tentative.https.any.js', /ml-kem-512/i], ['generateKey/successes_ML-KEM.tentative.https.any.js', /ml-kem-512/i], ['import_export/ML-KEM_importKey.tentative.https.any.js', /ml-kem-512/i], + ['serialization/mlkem.tentative.https.any.js', /ml-kem-512/i], ['supports-modern.tentative.https.any.js', /ml-kem-512/i]); } From 86032758e4058fd9d907a926d49623845872817d Mon Sep 17 00:00:00 2001 From: Stefan Stojanovic Date: Thu, 21 May 2026 01:25:49 +0200 Subject: [PATCH 005/143] build,win: replace LTCG with Thin LTO for releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: StefanStojanovic PR-URL: https://github.com/nodejs/node/pull/63114 Refs: https://github.com/nodejs/node/issues/61964 Reviewed-By: Michaël Zasso --- common.gypi | 7 +++++++ configure.py | 9 +++++++++ vcbuild.bat | 32 +++++++++++++++++++------------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/common.gypi b/common.gypi index c0a3d785a3dab3..26c2ef3675fd0e 100644 --- a/common.gypi +++ b/common.gypi @@ -272,6 +272,13 @@ }, }, },], + ['(enable_thin_lto=="true" or enable_lto=="true") and lto_jobs!=""', { + 'msvs_settings': { + 'VCLinkerTool': { + 'AdditionalOptions': ['/opt:lldltojobs=<(lto_jobs)'], + }, + }, + },], ], 'target_conditions': [ ['_toolset=="target"', { diff --git a/configure.py b/configure.py index eea76312119385..ae5f9e70a69a35 100755 --- a/configure.py +++ b/configure.py @@ -225,6 +225,14 @@ help="Enable compiling with thin lto of a binary. This feature is only available " "on windows.") +parser.add_argument("--lto-jobs", + action="store", + dest="lto_jobs", + default=None, + help="Set the number of parallel LTO code generation jobs during linking. " + "Defaults to the number of CPU cores. Lower values reduce peak memory " + "usage at the cost of longer link times. Only effective with LTO enabled.") + parser.add_argument("--link-module", action="append", dest="linked_module", @@ -1995,6 +2003,7 @@ def configure_node(o): o['variables']['enable_lto'] = b(options.enable_lto) o['variables']['enable_thin_lto'] = b(options.enable_thin_lto) + o['variables']['lto_jobs'] = options.lto_jobs or '' if options.node_use_large_pages or options.node_use_large_pages_script_lld: warn('''The `--use-largepages` and `--use-largepages-script-lld` options diff --git a/vcbuild.bat b/vcbuild.bat index 0d3ce0fd021fb6..28494815df1d64 100644 --- a/vcbuild.bat +++ b/vcbuild.bat @@ -26,6 +26,7 @@ set target_arch=x64 set ltcg= set thin_lto= set lto= +set lto_jobs= set pgo_generate= set pgo_use= set target_env= @@ -110,6 +111,7 @@ if /i "%1"=="nonpm" set nonpm=1&goto arg-ok if /i "%1"=="ltcg" set ltcg=1&goto arg-ok if /i "%1"=="thin-lto" set thin_lto=1&goto arg-ok if /i "%1"=="lto" set lto=1&goto arg-ok +if /i "%1"=="lto-jobs" set "lto_jobs=%2"&goto arg-ok-2 if /i "%1"=="pgo-generate" set pgo_generate=1&goto arg-ok if /i "%1"=="pgo-use" set pgo_use=1&goto arg-ok if /i "%1"=="v8temporal" set v8temporal=1&goto arg-ok @@ -187,6 +189,20 @@ goto next-arg :args-done +if defined build_release ( + set config=Release + set package=1 + set msi=1 + set licensertf=1 + set download_arg="--download=all" + set i18n_arg=full-icu + set projgen=1 + set cctest=1 + set thin_lto=1 + @REM Parallel LTO link jobs can cause OOM issues, so we limit it to 2 by default for release builds in the release CI. + set lto_jobs=2 +) + :: LTO mutual exclusion set lto_count=0 if defined ltcg set /a lto_count+=1 @@ -208,18 +224,6 @@ if defined pgo_generate if defined pgo_use ( exit /b 1 ) -if defined build_release ( - set config=Release - set package=1 - set msi=1 - set licensertf=1 - set download_arg="--download=all" - set i18n_arg=full-icu - set projgen=1 - set cctest=1 - set ltcg=1 -) - if defined msi set stage_package=1 if defined package set stage_package=1 @@ -243,6 +247,7 @@ if defined nonpm set configure_flags=%configure_flags% --without-npm if defined ltcg set configure_flags=%configure_flags% --with-ltcg if defined thin_lto set configure_flags=%configure_flags% --enable-thin-lto if defined lto set configure_flags=%configure_flags% --enable-lto +if defined lto_jobs set configure_flags=%configure_flags% --lto-jobs=%lto_jobs% if defined pgo_generate set configure_flags=%configure_flags% --enable-pgo-generate if defined pgo_use set configure_flags=%configure_flags% --enable-pgo-use if defined release_urlbase set configure_flags=%configure_flags% --release-urlbase=%release_urlbase% @@ -908,7 +913,7 @@ set exit_code=1 goto exit :help -echo vcbuild.bat [debug/release] [msi] [doc] [test/test-all/test-addons/test-doc/test-js-native-api/test-node-api/test-internet/test-tick-processor/test-known-issues/test-node-inspect/test-check-deopts/test-npm/test-v8/test-v8-intl/test-v8-benchmarks/test-v8-all] [build-addons/build-js-native-api-tests/build-node-api-tests/build-ffi-tests] [ignore-flaky] [static/dll] [noprojgen] [projgen] [clang-cl] [ccache path-to-ccache] [small-icu/full-icu/without-intl] [nobuild] [nosnapshot] [nonpm] [ltcg] [thin-lto] [lto] [pgo-generate] [pgo-use] [licensetf] [sign] [x64/arm64] [vs2022/vs2026] [download-all] [enable-vtune] [lint/lint-ci/lint-js/lint-md] [lint-md-build] [format-md] [package] [build-release] [upload] [no-NODE-OPTIONS] [link-module path-to-module] [debug-http2] [debug-nghttp2] [clean] [cctest] [no-cctest] [openssl-no-asm] +echo vcbuild.bat [debug/release] [msi] [doc] [test/test-all/test-addons/test-doc/test-js-native-api/test-node-api/test-internet/test-tick-processor/test-known-issues/test-node-inspect/test-check-deopts/test-npm/test-v8/test-v8-intl/test-v8-benchmarks/test-v8-all] [build-addons/build-js-native-api-tests/build-node-api-tests/build-ffi-tests] [ignore-flaky] [static/dll] [noprojgen] [projgen] [clang-cl] [ccache path-to-ccache] [small-icu/full-icu/without-intl] [nobuild] [nosnapshot] [nonpm] [ltcg] [thin-lto] [lto] [lto-jobs number-of-jobs] [pgo-generate] [pgo-use] [licensetf] [sign] [x64/arm64] [vs2022/vs2026] [download-all] [enable-vtune] [lint/lint-ci/lint-js/lint-md] [lint-md-build] [format-md] [package] [build-release] [upload] [no-NODE-OPTIONS] [link-module path-to-module] [debug-http2] [debug-nghttp2] [clean] [cctest] [no-cctest] [openssl-no-asm] echo Examples: echo vcbuild.bat : builds release build echo vcbuild.bat debug : builds debug build @@ -922,6 +927,7 @@ echo vcbuild.bat no-cctest : skip building cctest.exe echo vcbuild.bat ccache c:\ccache\ : use ccache to speed build echo vcbuild.bat thin-lto : builds with Thin LTO applied globally to all targets echo vcbuild.bat lto : builds with Full LTO applied globally to all targets +echo vcbuild.bat thin-lto lto-jobs 2 : builds with Thin LTO applied globally to all targets and limits parallel LTO link jobs (reduces peak memory usage) echo vcbuild.bat pgo-generate : builds instrumented binary for PGO (profile first, then rebuild with pgo-use) echo vcbuild.bat pgo-use : builds optimized binary using PGO profile data goto exit From db808ad77d91390195b5de70977b06efc5ab1e17 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Thu, 21 May 2026 11:06:29 +0200 Subject: [PATCH 006/143] test: unskip snapshot reproducibility test Signed-off-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/63307 Reviewed-By: Filip Skokan Reviewed-By: Antoine du Hamel Reviewed-By: Stewart X Addison Reviewed-By: Richard Lau Reviewed-By: Luigi Pinca --- test/parallel/parallel.status | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status index 5c7fe47927611f..73a866bbef8b75 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status @@ -19,9 +19,6 @@ test-fs-read-stream-concurrent-reads: PASS, FLAKY # https://github.com/nodejs/build/issues/3043 test-snapshot-incompatible: SKIP -# https://github.com/nodejs/node/issues/53579 -test-snapshot-reproducible: SKIP - [$system==win32] # https://github.com/nodejs/node/issues/59090 test-inspector-network-fetch: PASS, FLAKY From 5a0b32dc2467f92ab73ff7a5512212440ac40efd Mon Sep 17 00:00:00 2001 From: Nad Alaba <37968805+nadalaba@users.noreply.github.com> Date: Thu, 21 May 2026 13:25:21 +0300 Subject: [PATCH 007/143] gyp: update deps gypfiles Signed-off-by: Nad Alaba <37968805+nadalaba@users.noreply.github.com> PR-URL: https://github.com/nodejs/node/pull/63117 Reviewed-By: Antoine du Hamel Reviewed-By: Luigi Pinca --- deps/inspector_protocol/inspector_protocol.gyp | 1 - deps/uv/uv.gyp | 1 - tools/v8_gypfiles/abseil.gyp | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/deps/inspector_protocol/inspector_protocol.gyp b/deps/inspector_protocol/inspector_protocol.gyp index 0eb551c769f55d..bd03b7ded1220f 100644 --- a/deps/inspector_protocol/inspector_protocol.gyp +++ b/deps/inspector_protocol/inspector_protocol.gyp @@ -14,7 +14,6 @@ 'crdtp/json.h', 'crdtp/json_platform.cc', 'crdtp/json_platform.h', - 'crdtp/maybe.h', 'crdtp/parser_handler.h', 'crdtp/protocol_core.cc', 'crdtp/protocol_core.h', diff --git a/deps/uv/uv.gyp b/deps/uv/uv.gyp index 540445f1f3249b..760a3cdb0019d1 100644 --- a/deps/uv/uv.gyp +++ b/deps/uv/uv.gyp @@ -62,7 +62,6 @@ 'uv_sources_win': [ 'include/uv/win.h', 'src/win/async.c', - 'src/win/atomicops-inl.h', 'src/win/core.c', 'src/win/detect-wakeup.c', 'src/win/dl.c', diff --git a/tools/v8_gypfiles/abseil.gyp b/tools/v8_gypfiles/abseil.gyp index fdc2fae1ab4d75..1a1de3378605b0 100644 --- a/tools/v8_gypfiles/abseil.gyp +++ b/tools/v8_gypfiles/abseil.gyp @@ -35,14 +35,11 @@ '<(ABSEIL_ROOT)/absl/base/internal/direct_mmap.h', '<(ABSEIL_ROOT)/absl/base/internal/endian.h', '<(ABSEIL_ROOT)/absl/base/internal/errno_saver.h', - '<(ABSEIL_ROOT)/absl/base/internal/fast_type_id.h', '<(ABSEIL_ROOT)/absl/base/internal/hide_ptr.h', - '<(ABSEIL_ROOT)/absl/base/internal/identity.h', '<(ABSEIL_ROOT)/absl/base/internal/iterator_traits.h', '<(ABSEIL_ROOT)/absl/base/internal/low_level_alloc.h', '<(ABSEIL_ROOT)/absl/base/internal/low_level_alloc.cc', '<(ABSEIL_ROOT)/absl/base/internal/low_level_scheduling.h', - '<(ABSEIL_ROOT)/absl/base/internal/nullability_impl.h', '<(ABSEIL_ROOT)/absl/base/internal/per_thread_tls.h', '<(ABSEIL_ROOT)/absl/base/internal/poison.h', '<(ABSEIL_ROOT)/absl/base/internal/poison.cc', @@ -75,6 +72,7 @@ '<(ABSEIL_ROOT)/absl/base/internal/unscaledcycleclock.h', '<(ABSEIL_ROOT)/absl/base/internal/unscaledcycleclock.cc', '<(ABSEIL_ROOT)/absl/base/internal/unscaledcycleclock_config.h', + '<(ABSEIL_ROOT)/absl/base/fast_type_id.h', '<(ABSEIL_ROOT)/absl/base/log_severity.h', '<(ABSEIL_ROOT)/absl/base/log_severity.cc', '<(ABSEIL_ROOT)/absl/base/macros.h', From b1fa59cbb687c43d9ee6bbb0efd52a38b8af5ce3 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 19 May 2026 14:07:05 +0300 Subject: [PATCH 008/143] test_runner: preserve run duration when using test-rerun Signed-off-by: Moshe Atlow PR-URL: https://github.com/nodejs/node/pull/63429 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Chemi Atlow Reviewed-By: Aviv Keller --- lib/internal/test_runner/reporter/rerun.js | 1 + lib/internal/test_runner/test.js | 15 ++++++++++++++- test/fixtures/test-runner/rerun-duration.js | 13 +++++++++++++ test/parallel/test-runner-test-rerun-failures.js | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/test-runner/rerun-duration.js diff --git a/lib/internal/test_runner/reporter/rerun.js b/lib/internal/test_runner/reporter/rerun.js index 9658a7fb70ff0b..ecdd53243e887a 100644 --- a/lib/internal/test_runner/reporter/rerun.js +++ b/lib/internal/test_runner/reporter/rerun.js @@ -62,6 +62,7 @@ function reportReruns(previousRuns, globalOptions) { name: data.name, children, passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt, + duration_ms: data.details.duration_ms, }; } } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index eb888905798bcd..cae309ffc23938 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -10,10 +10,12 @@ const { ArrayPrototypeSplice, ArrayPrototypeUnshift, ArrayPrototypeUnshiftApply, + BigInt, Error, FunctionPrototype, MathFloor, MathMax, + MathRound, Number, NumberPrototypeToFixed, ObjectFreeze, @@ -803,14 +805,25 @@ class Test extends AsyncResource { const previousAttempt = this.root.harness.previousRuns[this.attempt - 1]?.[testIdentifier]; if (previousAttempt != null) { this.passedAttempt = previousAttempt.passed_on_attempt; + if (previousAttempt.duration_ms !== undefined) { + this.replayedDurationNs = BigInt(MathRound(previousAttempt.duration_ms * 1_000_000)); + } this.fn = () => { + // Restore the original duration on the synthetic replay. Suites are + // skipped here because Suite.run() unconditionally reassigns + // startTime later; only non-suite tests benefit from setting it. + if (this.reportedType !== 'suite') { + this.startTime = hrtime(); + this.endTime = this.startTime + (this.replayedDurationNs ?? 0n); + } for (let i = 0; i < (previousAttempt.children?.length ?? 0); i++) { const child = previousAttempt.children[i]; const t = this.createSubtest(Test, child.name, { __proto__: null }, noop, { __proto__: null, loc: [child.line, child.column, child.file], }, noop); - t.endTime = t.startTime = hrtime(); + t.startTime = hrtime(); + t.endTime = t.startTime + (t.replayedDurationNs ?? 0n); // For suites, Suite.run() starts the subtests via SafePromiseAll. // Starting them here as well would run them twice, re-invoking the // synthetic children-creator against a now-incremented disambiguator diff --git a/test/fixtures/test-runner/rerun-duration.js b/test/fixtures/test-runner/rerun-duration.js new file mode 100644 index 00000000000000..522ca187c52de4 --- /dev/null +++ b/test/fixtures/test-runner/rerun-duration.js @@ -0,0 +1,13 @@ +'use strict'; +// A passing test with a measurable duration alongside a failing test. The +// failing test forces the rerun feature to retry on a second invocation, +// causing the passing test to be replayed via the synthetic noop stub. +const { test } = require('node:test'); + +test('passing slow test', async () => { + await new Promise((resolve) => setTimeout(resolve, 25)); +}); + +test('always failing', () => { + throw new Error('boom'); +}); diff --git a/test/parallel/test-runner-test-rerun-failures.js b/test/parallel/test-runner-test-rerun-failures.js index 585d5f25f04a59..f6ae8735e9b77a 100644 --- a/test/parallel/test-runner-test-rerun-failures.js +++ b/test/parallel/test-runner-test-rerun-failures.js @@ -89,6 +89,7 @@ const getStateFile = async () => { res.forEach((entry) => { for (const item in entry) { delete entry[item].children; + delete entry[item].duration_ms; } }); return res; @@ -152,6 +153,20 @@ test('test should pass on third rerun with `--test`', async () => { assert.deepStrictEqual(await getStateFile(), expectedStateFile); }); +test('rerun preserves the original duration on the replayed pass', async () => { + const durationFixture = fixtures.path('test-runner', 'rerun-duration.js'); + const args = ['--test-rerun-failures', stateFile, durationFixture]; + + await common.spawnPromisified(process.execPath, args); + await common.spawnPromisified(process.execPath, args); + + const raw = JSON.parse(await readFile(stateFile, 'utf8')); + const passKey = Object.keys(raw[0]).find((k) => raw[0][k].name === 'passing slow test'); + assert.ok(passKey, 'expected the passing test to be recorded on attempt 0'); + assert.ok(raw[0][passKey].duration_ms > 0, 'expected a measurable duration on attempt 0'); + assert.strictEqual(raw[1][passKey].duration_ms, raw[0][passKey].duration_ms); +}); + test('using `run` api', async () => { let stream = run({ files: [fixture], rerunFailuresFilePath: stateFile }); stream.on('test:pass', common.mustCall(19)); From a646c93254a94287c9fa6902dd3ba575abc72603 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 19 May 2026 14:10:41 +0300 Subject: [PATCH 009/143] test_runner: show replayed-from-attempt hint in spec reporter Signed-off-by: Moshe Atlow PR-URL: https://github.com/nodejs/node/pull/63429 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Chemi Atlow Reviewed-By: Aviv Keller --- lib/internal/test_runner/reporter/utils.js | 5 ++++- test/parallel/test-runner-test-rerun-failures.js | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/internal/test_runner/reporter/utils.js b/lib/internal/test_runner/reporter/utils.js index d90040b9727aa2..67030680019838 100644 --- a/lib/internal/test_runner/reporter/utils.js +++ b/lib/internal/test_runner/reporter/utils.js @@ -73,7 +73,10 @@ function formatTestReport(type, data, showErrorDetails = true, prefix = '', inde let symbol = reporterUnicodeSymbolMap[type] ?? ' '; const { skip, todo, expectFailure } = data; const duration_ms = data.details?.duration_ms ? ` ${colors.gray}(${data.details.duration_ms}ms)${colors.white}` : ''; - let title = `${data.name}${duration_ms}`; + const replayed = data.details?.passed_on_attempt !== undefined ? + ` ${colors.gray}(passed on attempt ${data.details.passed_on_attempt})${colors.white}` : + ''; + let title = `${data.name}${duration_ms}${replayed}`; if (skip !== undefined) { title += ` # ${typeof skip === 'string' && skip.length ? skip : 'SKIP'}`; diff --git a/test/parallel/test-runner-test-rerun-failures.js b/test/parallel/test-runner-test-rerun-failures.js index f6ae8735e9b77a..3a9249cc52277b 100644 --- a/test/parallel/test-runner-test-rerun-failures.js +++ b/test/parallel/test-runner-test-rerun-failures.js @@ -157,8 +157,15 @@ test('rerun preserves the original duration on the replayed pass', async () => { const durationFixture = fixtures.path('test-runner', 'rerun-duration.js'); const args = ['--test-rerun-failures', stateFile, durationFixture]; - await common.spawnPromisified(process.execPath, args); - await common.spawnPromisified(process.execPath, args); + const first = await common.spawnPromisified(process.execPath, args); + assert.doesNotMatch(first.stdout, /passed on attempt/, + 'no replay marker should appear on the initial run'); + + const second = await common.spawnPromisified(process.execPath, args); + assert.match(second.stdout, /passing slow test[^\n]*\(passed on attempt 0\)/, + 'spec reporter should mark the replayed test on the retry'); + assert.doesNotMatch(second.stdout, /always failing[^\n]*\(passed on attempt/, + 'the failing test must not show the replay marker'); const raw = JSON.parse(await readFile(stateFile, 'utf8')); const passKey = Object.keys(raw[0]).find((k) => raw[0][k].name === 'passing slow test'); From 7a36ca46cdd242f17a6cbb839a1372731a27c711 Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Thu, 21 May 2026 09:50:23 -0400 Subject: [PATCH 010/143] src: expose `node::RegisterContext` to make a node managed context Signed-off-by: Chengzhong Wu PR-URL: https://github.com/nodejs/node/pull/62322 Reviewed-By: Joyee Cheung --- src/api/environment.cc | 13 ++++ src/env.h | 2 + src/node.h | 14 ++++ test/addons/new-context-inspector/binding.cc | 69 +++++++++++++++++++ test/addons/new-context-inspector/binding.gyp | 9 +++ .../new-context-inspector/test-inspector.js | 51 ++++++++++++++ test/addons/new-context-inspector/test.js | 18 +++++ 7 files changed, 176 insertions(+) create mode 100644 test/addons/new-context-inspector/binding.cc create mode 100644 test/addons/new-context-inspector/binding.gyp create mode 100644 test/addons/new-context-inspector/test-inspector.js create mode 100644 test/addons/new-context-inspector/test.js diff --git a/src/api/environment.cc b/src/api/environment.cc index 25657d99bddaa3..c3fd1e58373516 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -2,6 +2,7 @@ #if HAVE_OPENSSL #include "crypto/crypto_util.h" #endif // HAVE_OPENSSL +#include "env.h" #include "env_properties.h" #include "node.h" #include "node_builtins.h" @@ -1053,6 +1054,18 @@ Maybe InitializeContext(Local context) { return Just(true); } +void RegisterContext(Environment* env, + v8::Local context, + std::string_view name, + std::string_view origin) { + ContextInfo info{std::string(name), std::string(origin)}; + env->AssignToContext(context, nullptr, info); +} + +void UnregisterContext(Environment* env, v8::Local context) { + env->UnassignFromContext(context); +} + uv_loop_t* GetCurrentEventLoop(Isolate* isolate) { HandleScope handle_scope(isolate); Local context = isolate->GetCurrentContext(); diff --git a/src/env.h b/src/env.h index 616f6e7d04109a..61512d9494ff61 100644 --- a/src/env.h +++ b/src/env.h @@ -257,6 +257,8 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer { struct ContextInfo { explicit ContextInfo(const std::string& name) : name(name) {} + ContextInfo(const std::string& name, const std::string& origin) + : name(name), origin(origin) {} const std::string name; std::string origin; bool is_default = false; diff --git a/src/node.h b/src/node.h index eba7b8698187b1..e25fe19c9ab30c 100644 --- a/src/node.h +++ b/src/node.h @@ -563,6 +563,8 @@ NODE_EXTERN v8::Isolate* NewIsolate( const IsolateSettings& settings = {}); // Creates a new context with Node.js-specific tweaks. +// Call `RegisterContext` after the context been created to register +// the context with Node.js specific setups like the inspector. NODE_EXTERN v8::Local NewContext( v8::Isolate* isolate, v8::Local object_template = @@ -572,6 +574,18 @@ NODE_EXTERN v8::Local NewContext( // Return value indicates success of operation NODE_EXTERN v8::Maybe InitializeContext(v8::Local context); +// Associate the context with the given Environment. This registers the context +// as known to Node.js, makes it available to the inspector. This also registers +// Node.js promise hooks on the context. +NODE_EXTERN void RegisterContext(Environment* env, + v8::Local context, + std::string_view name = "", + std::string_view origin = ""); +// Unregister the context. Call this when the embedder finished all work with +// this context. +NODE_EXTERN void UnregisterContext(Environment* env, + v8::Local context); + // If `platform` is passed, it will be used to register new Worker instances. // It can be `nullptr`, in which case creating new Workers inside of // Environments that use this `IsolateData` will not work. diff --git a/test/addons/new-context-inspector/binding.cc b/test/addons/new-context-inspector/binding.cc new file mode 100644 index 00000000000000..b5a025b18d0df6 --- /dev/null +++ b/test/addons/new-context-inspector/binding.cc @@ -0,0 +1,69 @@ +#include +#include + +namespace { + +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Script; +using v8::String; +using v8::Value; + +void MakeContext(const FunctionCallbackInfo& args) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope handle_scope(isolate); + Local context = isolate->GetCurrentContext(); + node::Environment* env = node::GetCurrentEnvironment(context); + assert(env); + + // Create a new context with Node.js-specific setup. + v8::MaybeLocal maybe_context = node::NewContext(isolate); + v8::Local new_context; + if (!maybe_context.ToLocal(&new_context)) { + return; + } + node::RegisterContext(env, new_context, "Addon Context", "addon://about"); + + // Return the global proxy object. + args.GetReturnValue().Set(new_context->Global()); +} + +void RunInContext(const FunctionCallbackInfo& args) { + Isolate* isolate = Isolate::GetCurrent(); + HandleScope handle_scope(isolate); + assert(args.Length() == 2); + + assert(args[0]->IsObject()); + Local global_proxy = args[0].As(); + v8::MaybeLocal maybe_context = global_proxy->GetCreationContext(); + v8::Local new_context; + if (!maybe_context.ToLocal(&new_context)) { + return; + } + Context::Scope context_scope(new_context); + + assert(args[1]->IsString()); + Local source = args[1].As(); + Local