From e24e698a9726c6a1602eb23e33d3387778e3df7b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 Apr 2026 23:43:41 +0200 Subject: [PATCH 01/89] crypto: add WebCrypto CryptoJob mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a WebCrypto-specific CryptoJob mode that returns a promise from run() and resolves it when native work is finished. Encode job output directly as Web Crypto values, including CryptoKey instances and CryptoKeyPair dictionaries. Convert operation-specific setup failures from AdditionalConfig into OperationError rejections. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63363 Reviewed-By: Antoine du Hamel Reviewed-By: René --- lib/internal/crypto/aes.js | 28 ++-- lib/internal/crypto/argon2.js | 67 ++++---- lib/internal/crypto/cfrg.js | 60 +++---- lib/internal/crypto/chacha20_poly1305.js | 20 ++- lib/internal/crypto/diffiehellman.js | 42 ++--- lib/internal/crypto/ec.js | 44 +++-- lib/internal/crypto/hash.js | 16 +- lib/internal/crypto/hkdf.js | 23 ++- lib/internal/crypto/mac.js | 38 +++-- lib/internal/crypto/ml_dsa.js | 61 +++---- lib/internal/crypto/ml_kem.js | 87 +++------- lib/internal/crypto/pbkdf2.js | 26 +-- lib/internal/crypto/rsa.js | 99 +++++------ lib/internal/crypto/util.js | 25 +-- src/crypto/README.md | 33 ++-- src/crypto/crypto_aes.cc | 12 +- src/crypto/crypto_argon2.cc | 4 +- src/crypto/crypto_chacha20_poly1305.cc | 6 +- src/crypto/crypto_cipher.h | 12 +- src/crypto/crypto_hash.cc | 7 +- src/crypto/crypto_hkdf.cc | 10 +- src/crypto/crypto_hmac.cc | 11 +- src/crypto/crypto_kem.cc | 22 ++- src/crypto/crypto_keygen.h | 120 ++++++++++++-- src/crypto/crypto_keys.cc | 33 ++++ src/crypto/crypto_keys.h | 6 + src/crypto/crypto_kmac.cc | 8 +- src/crypto/crypto_pbkdf2.cc | 10 +- src/crypto/crypto_rsa.cc | 2 +- src/crypto/crypto_scrypt.cc | 10 +- src/crypto/crypto_sig.cc | 13 +- src/crypto/crypto_turboshake.cc | 10 +- src/crypto/crypto_util.cc | 60 ++++++- src/crypto/crypto_util.h | 122 ++++++++++++-- test/parallel/test-crypto-argon2.js | 12 +- .../test-webcrypto-crypto-job-mode.js | 154 ++++++++++++++++++ .../test-webcrypto-derivebits-argon2.js | 33 ++++ .../test-webcrypto-encap-decap-ml-kem.js | 3 + test/parallel/test-webcrypto-keygen.js | 11 ++ 39 files changed, 904 insertions(+), 456 deletions(-) create mode 100644 test/parallel/test-webcrypto-crypto-job-mode.js diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 73fdde03d73ba8..2ed6c69f43e3d4 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -7,7 +7,7 @@ const { const { AESCipherJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyVariantAES_CTR_128, kKeyVariantAES_CBC_128, kKeyVariantAES_GCM_128, @@ -107,7 +107,7 @@ function getVariant(name, length) { function asyncAesCtrCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -118,7 +118,7 @@ function asyncAesCtrCipher(mode, key, data, algorithm) { function asyncAesCbcCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -128,7 +128,7 @@ function asyncAesCbcCipher(mode, key, data, algorithm) { function asyncAesKwCipher(mode, key, data) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -140,7 +140,7 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { const tagByteLength = tagLength / 8; return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -155,7 +155,7 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { const tagByteLength = tagLength / 8; return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -175,7 +175,7 @@ function aesCipher(mode, key, data, algorithm) { } } -async function aesGenerateKey(algorithm, extractable, keyUsages) { +function aesGenerateKey(algorithm, extractable, keyUsages) { const { name, length } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; @@ -188,14 +188,18 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) { 'Unsupported key usage for an AES key', 'SyntaxError'); } + if (usagesSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length }, getUsagesMask(usagesSet), - extractable); + extractable)); } function aesImportKey( diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index 6110c55c16dfb8..08b16aa1411aa4 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -3,8 +3,6 @@ const { FunctionPrototypeCall, MathPow, - StringPrototypeToLowerCase, - TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; @@ -14,6 +12,7 @@ const { Argon2Job, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, kTypeArgon2d, kTypeArgon2i, kTypeArgon2id, @@ -21,7 +20,6 @@ const { const { lazyDOMException, - promisify, } = require('internal/util'); const { @@ -30,6 +28,7 @@ const { const { getArrayBufferOrView, + jobPromise, } = require('internal/crypto/util'); const { @@ -143,20 +142,12 @@ function check(algorithm, parameters) { validateString(algorithm, 'algorithm'); validateOneOf(algorithm, 'algorithm', ['argon2d', 'argon2i', 'argon2id']); - let type; - switch (algorithm) { - case 'argon2d': - type = kTypeArgon2d; - break; - case 'argon2i': - type = kTypeArgon2i; - break; - case 'argon2id': - type = kTypeArgon2id; - break; - default: // unreachable - throw new ERR_CRYPTO_ARGON2_NOT_SUPPORTED(); - } + const type = { + '__proto__': null, + 'argon2d': kTypeArgon2d, + 'argon2i': kTypeArgon2i, + 'argon2id': kTypeArgon2id, + }[algorithm]; validateObject(parameters, 'parameters'); @@ -193,7 +184,6 @@ function check(algorithm, parameters) { return { message, nonce, secret, associatedData, tagLength, passes, parallelism, memory, type }; } -const argon2Promise = promisify(argon2); function validateArgon2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -211,32 +201,39 @@ function validateArgon2DeriveBitsLength(length) { } } -async function argon2DeriveBits(algorithm, baseKey, length) { +function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); - let result; + const type = { + '__proto__': null, + 'Argon2d': kTypeArgon2d, + 'Argon2i': kTypeArgon2i, + 'Argon2id': kTypeArgon2id, + }[algorithm.name]; + + let message; try { - result = await argon2Promise( - StringPrototypeToLowerCase(algorithm.name), - { - // TODO(panva): call the job directly without needing to re-export the handle - message: getCryptoKeyHandle(baseKey).export(), - nonce: algorithm.nonce, - parallelism: algorithm.parallelism, - tagLength: length / 8, - memory: algorithm.memory, - passes: algorithm.passes, - secret: algorithm.secretValue, - associatedData: algorithm.associatedData, - }, - ); + // TODO(panva): call the job directly without needing to re-export the handle + message = getCryptoKeyHandle(baseKey).export(); } catch (err) { throw lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err }); } - return TypedArrayPrototypeGetBuffer(result); + const empty = new Uint8Array(0); + + return jobPromise(() => new Argon2Job( + kCryptoJobWebCrypto, + message, + algorithm.nonce, + algorithm.parallelism, + length / 8, + algorithm.memory, + algorithm.passes, + algorithm.secretValue === undefined ? empty : algorithm.secretValue, + algorithm.associatedData === undefined ? empty : algorithm.associatedData, + type)); } module.exports = { diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 8d26a2888200ff..98b1862b7ad1f8 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -8,7 +8,7 @@ const { const { SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, kSignJobModeSign, @@ -73,7 +73,7 @@ function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { } } -async function cfrgGenerateKey(algorithm, extractable, keyUsages) { +function cfrgGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -97,23 +97,13 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { } break; } - let nid; - switch (name) { - case 'Ed25519': - nid = EVP_PKEY_ED25519; - break; - case 'Ed448': - nid = EVP_PKEY_ED448; - break; - case 'X25519': - nid = EVP_PKEY_X25519; - break; - case 'X448': - nid = EVP_PKEY_X448; - break; - } - - const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); + const nid = { + '__proto__': null, + 'Ed25519': EVP_PKEY_ED25519, + 'Ed448': EVP_PKEY_ED448, + 'X25519': EVP_PKEY_X25519, + 'X448': EVP_PKEY_X448, + }[name]; let publicUsages; let privateUsages; @@ -134,21 +124,19 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - getUsagesMask(publicUsages), - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - getUsagesMask(privateUsages), - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function cfrgExportKey(key, format) { @@ -243,15 +231,15 @@ function cfrgImportKey( extractable); } -async function eddsaSignVerify(key, data, algorithm, signature) { +function eddsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), undefined, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 1bd173cab36191..9d4606090af8ee 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -7,7 +7,7 @@ const { const { ChaCha20Poly1305CipherJob, SecretKeyGenJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -39,7 +39,7 @@ function validateKeyLength(length) { function c20pCipher(mode, key, data, algorithm) { return jobPromise(() => new ChaCha20Poly1305CipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -47,7 +47,7 @@ function c20pCipher(mode, key, data, algorithm) { algorithm.additionalData)); } -async function c20pGenerateKey(algorithm, extractable, keyUsages) { +function c20pGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; @@ -58,14 +58,18 @@ async function c20pGenerateKey(algorithm, extractable, keyUsages) { `Unsupported key usage for a ${algorithm.name} key`, 'SyntaxError'); } + if (usagesSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, 256)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + 256, { name }, getUsagesMask(usagesSet), - extractable); + extractable)); } function c20pImportKey( diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 81006c34b34758..ccdf4559154d64 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -5,6 +5,7 @@ const { FunctionPrototypeCall, MathCeil, ObjectDefineProperty, + PromisePrototypeThen, TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; @@ -19,6 +20,7 @@ const { ECDHConvertKey: _ECDHConvertKey, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -326,7 +328,7 @@ function diffieHellman(options, callback) { let masks; // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. -async function ecdhDeriveBits(algorithm, baseKey, length) { +function ecdhDeriveBits(algorithm, baseKey, length) { const { 'public': key } = algorithm; if (getCryptoKeyType(baseKey) !== 'private') { @@ -349,8 +351,8 @@ async function ecdhDeriveBits(algorithm, baseKey, length) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } - const bits = await jobPromise(() => new DHBitsJob( - kCryptoJobAsync, + const bits = jobPromise(() => new DHBitsJob( + kCryptoJobWebCrypto, getCryptoKeyHandle(key), undefined, undefined, @@ -366,27 +368,29 @@ async function ecdhDeriveBits(algorithm, baseKey, length) { if (length === null) return bits; - // If the length is not a multiple of 8 the nearest ceiled - // multiple of 8 is sliced. - const sliceLength = MathCeil(length / 8); + return PromisePrototypeThen(bits, (bits) => { + // If the length is not a multiple of 8 the nearest ceiled + // multiple of 8 is sliced. + const sliceLength = MathCeil(length / 8); - const { byteLength } = bits; - // If the length is larger than the derived secret, throw. - if (byteLength < sliceLength) - throw lazyDOMException('derived bit length is too small', 'OperationError'); + const { byteLength } = bits; + // If the length is larger than the derived secret, throw. + if (byteLength < sliceLength) + throw lazyDOMException('derived bit length is too small', 'OperationError'); - const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); + const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); - const mod = length % 8; - if (mod === 0) - return slice; + const mod = length % 8; + if (mod === 0) + return slice; - // eslint-disable-next-line no-sparse-arrays - masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; + // eslint-disable-next-line no-sparse-arrays + masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; - const masked = new Uint8Array(slice); - masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; - return TypedArrayPrototypeGetBuffer(masked); + const masked = new Uint8Array(slice); + masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; + return TypedArrayPrototypeGetBuffer(masked); + }); } module.exports = { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 983bfde2e8efa6..212ba75e0a9b11 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -10,7 +10,7 @@ const { EcKeyPairGenJob, KeyObjectHandle, SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, kKeyTypePublic, @@ -77,7 +77,7 @@ function verifyAcceptableEcKeyUse(name, isPublic, usages) { } } -async function ecGenerateKey(algorithm, extractable, keyUsages) { +function ecGenerateKey(algorithm, extractable, keyUsages) { const { name, namedCurve } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -98,9 +98,6 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { // Fall through } - const handles = await jobPromise(() => new EcKeyPairGenJob( - kCryptoJobAsync, namedCurve)); - let publicUsages; let privateUsages; switch (name) { @@ -116,21 +113,20 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { const keyAlgorithm = { name, namedCurve }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - getUsagesMask(publicUsages), - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - getUsagesMask(privateUsages), - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, publicKey, privateKey }; + return jobPromise(() => new EcKeyPairGenJob( + kCryptoJobWebCrypto, + namedCurve, + undefined, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function ecExportKey(key, format) { @@ -264,17 +260,15 @@ function ecImportKey( extractable); } -async function ecdsaSignVerify(key, data, { name, hash }, signature) { +function ecdsaSignVerify(key, data, { name, hash }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - const hashname = normalizeHashName(hash.name); - - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), undefined, @@ -282,7 +276,7 @@ async function ecdsaSignVerify(key, data, { name, hash }, signature) { undefined, undefined, data, - hashname, + normalizeHashName(hash.name), undefined, // Salt length, not used with ECDSA undefined, // PSS Padding, not used with ECDSA kSigEncP1363, diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 857753c2b39f9c..5aec1614cb92e9 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -12,7 +12,7 @@ const { Hash: _Hash, HashJob, Hmac: _Hmac, - kCryptoJobAsync, + kCryptoJobWebCrypto, oneShotDigest, TurboShakeJob, KangarooTwelveJob, @@ -200,7 +200,7 @@ Hmac.prototype._transform = Hash.prototype._transform; // Implementation for WebCrypto subtle.digest() -async function asyncDigest(algorithm, data) { +function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); switch (algorithm.name) { @@ -221,16 +221,16 @@ async function asyncDigest(algorithm, data) { case 'cSHAKE128': // Fall through case 'cSHAKE256': - return await jobPromise(() => new HashJob( - kCryptoJobAsync, + return jobPromise(() => new HashJob( + kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, algorithm.outputLength)); case 'TurboSHAKE128': // Fall through case 'TurboSHAKE256': - return await jobPromise(() => new TurboShakeJob( - kCryptoJobAsync, + return jobPromise(() => new TurboShakeJob( + kCryptoJobWebCrypto, algorithm.name, algorithm.domainSeparation ?? 0x1f, algorithm.outputLength / 8, @@ -238,8 +238,8 @@ async function asyncDigest(algorithm, data) { case 'KT128': // Fall through case 'KT256': - return await jobPromise(() => new KangarooTwelveJob( - kCryptoJobAsync, + return jobPromise(() => new KangarooTwelveJob( + kCryptoJobWebCrypto, algorithm.name, algorithm.customization, algorithm.outputLength / 8, diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 424c56fd894961..a54432494817db 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -3,12 +3,14 @@ const { ArrayBuffer, FunctionPrototypeCall, + PromiseResolve, } = primordials; const { HKDFJob, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -148,26 +150,29 @@ function validateHkdfDeriveBitsLength(length) { } } -async function hkdfDeriveBits(algorithm, baseKey, length) { +function hkdfDeriveBits(algorithm, baseKey, length) { validateHkdfDeriveBitsLength(length); const { hash, salt, info } = algorithm; if (length === 0) - return new ArrayBuffer(0); + return PromiseResolve(new ArrayBuffer(0)); + let normalizedHash; try { - return await jobPromise(() => new HKDFJob( - kCryptoJobAsync, - normalizeHashName(hash.name), - getCryptoKeyHandle(baseKey), - salt, - info, - length / 8)); + normalizedHash = normalizeHashName(hash.name); } catch (err) { throw lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err }); } + + return jobPromise(() => new HKDFJob( + kCryptoJobWebCrypto, + normalizedHash, + getCryptoKeyHandle(baseKey), + salt, + info, + length / 8)); } module.exports = { diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 57576f729b7b41..c3418231650b02 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -8,7 +8,7 @@ const { const { HmacJob, KmacJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kSignJobModeSign, kSignJobModeVerify, SecretKeyGenJob, @@ -40,7 +40,7 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -async function hmacGenerateKey(algorithm, extractable, keyUsages) { +function hmacGenerateKey(algorithm, extractable, keyUsages) { const { hash, name, @@ -53,17 +53,21 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) { 'Unsupported key usage for an HMAC key', 'SyntaxError'); } + if (usageSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length, hash }, getUsagesMask(usageSet), - extractable); + extractable)); } -async function kmacGenerateKey(algorithm, extractable, keyUsages) { +function kmacGenerateKey(algorithm, extractable, keyUsages) { const { name, length = { @@ -79,14 +83,18 @@ async function kmacGenerateKey(algorithm, extractable, keyUsages) { `Unsupported key usage for ${name} key`, 'SyntaxError'); } + if (usageSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - const handle = await jobPromise(() => new SecretKeyGenJob(kCryptoJobAsync, length)); - - return new InternalCryptoKey( - handle, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length }, getUsagesMask(usageSet), - extractable); + extractable)); } function macImportKey( @@ -168,7 +176,7 @@ function macImportKey( function hmacSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new HmacJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, normalizeHashName(getCryptoKeyAlgorithm(key).hash.name), getCryptoKeyHandle(key), @@ -179,7 +187,7 @@ function hmacSignVerify(key, data, algorithm, signature) { function kmacSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new KmacJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), algorithm.name, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index bd93327f93aa5f..5a08291562bcf2 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -9,7 +9,7 @@ const { const { SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, @@ -59,7 +59,7 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { } } -async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { +function mlDsaGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -69,40 +69,31 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let nid; - switch (name) { - case 'ML-DSA-44': - nid = EVP_PKEY_ML_DSA_44; - break; - case 'ML-DSA-65': - nid = EVP_PKEY_ML_DSA_65; - break; - case 'ML-DSA-87': - nid = EVP_PKEY_ML_DSA_87; - break; - } + const nid = { + '__proto__': null, + 'ML-DSA-44': EVP_PKEY_ML_DSA_44, + 'ML-DSA-65': EVP_PKEY_ML_DSA_65, + 'ML-DSA-87': EVP_PKEY_ML_DSA_87, + }[name]; - const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); - const publicUsagesMask = getUsagesMask(getUsagesUnion(usageSet, 'verify')); - const privateUsagesMask = getUsagesMask(getUsagesUnion(usageSet, 'sign')); + const publicUsages = getUsagesUnion(usageSet, 'verify'); + const privateUsages = getUsagesUnion(usageSet, 'sign'); const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - publicUsagesMask, - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - privateUsagesMask, - extractable); - - return { __proto__: null, privateKey, publicKey }; + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function mlDsaExportKey(key, format) { @@ -214,15 +205,15 @@ function mlDsaImportKey( extractable); } -async function mlDsaSignVerify(key, data, algorithm, signature) { +function mlDsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), undefined, diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 99367290ea22cd..530507be4e340d 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -1,7 +1,6 @@ 'use strict'; const { - PromiseWithResolvers, SafeSet, StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, @@ -9,7 +8,7 @@ const { } = primordials; const { - kCryptoJobAsync, + kCryptoJobWebCrypto, KEMDecapsulateJob, KEMEncapsulateJob, kKeyFormatDER, @@ -50,7 +49,7 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -async function mlKemGenerateKey(algorithm, extractable, keyUsages) { +function mlKemGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -67,29 +66,26 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { 'ML-KEM-1024': EVP_PKEY_ML_KEM_1024, }[name]; - const handles = await jobPromise(() => new NidKeyPairGenJob(kCryptoJobAsync, nid)); - const publicUsagesMask = getUsagesMask( - getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits')); - const privateUsagesMask = getUsagesMask( - getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits')); + const publicUsages = + getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); + const privateUsages = + getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - publicUsagesMask, - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - privateUsagesMask, - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function mlKemExportKey(key, format) { @@ -215,33 +211,13 @@ function mlKemEncapsulate(encapsulationKey) { throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError'); } - const { promise, resolve, reject } = PromiseWithResolvers(); - - const job = new KEMEncapsulateJob( - kCryptoJobAsync, + return jobPromise(() => new KEMEncapsulateJob( + kCryptoJobWebCrypto, getCryptoKeyHandle(encapsulationKey), undefined, undefined, undefined, - undefined); - - job.ondone = (error, result) => { - if (error) { - reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: error })); - } else { - const { 0: sharedKey, 1: ciphertext } = result; - - resolve({ - sharedKey: TypedArrayPrototypeGetBuffer(sharedKey), - ciphertext: TypedArrayPrototypeGetBuffer(ciphertext), - }); - } - }; - job.run(); - - return promise; + undefined)); } function mlKemDecapsulate(decapsulationKey, ciphertext) { @@ -249,29 +225,14 @@ function mlKemDecapsulate(decapsulationKey, ciphertext) { throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError'); } - const { promise, resolve, reject } = PromiseWithResolvers(); - - const job = new KEMDecapsulateJob( - kCryptoJobAsync, + return jobPromise(() => new KEMDecapsulateJob( + kCryptoJobWebCrypto, getCryptoKeyHandle(decapsulationKey), undefined, undefined, undefined, undefined, - ciphertext); - - job.ondone = (error, result) => { - if (error) { - reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: error })); - } else { - resolve(TypedArrayPrototypeGetBuffer(result)); - } - }; - job.run(); - - return promise; + ciphertext)); } module.exports = { diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 7f0fa0e1855efe..5cf4d8f46ecb6d 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -3,7 +3,7 @@ const { ArrayBuffer, FunctionPrototypeCall, - TypedArrayPrototypeGetBuffer, + PromiseResolve, } = primordials; const { Buffer } = require('buffer'); @@ -12,6 +12,7 @@ const { PBKDF2Job, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -23,11 +24,11 @@ const { const { getArrayBufferOrView, normalizeHashName, + jobPromise, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { @@ -95,7 +96,6 @@ function check(password, salt, iterations, keylen, digest) { return { password, salt, iterations, keylen, digest }; } -const pbkdf2Promise = promisify(pbkdf2); function validatePbkdf2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -107,26 +107,32 @@ function validatePbkdf2DeriveBitsLength(length) { } } -async function pbkdf2DeriveBits(algorithm, baseKey, length) { +function pbkdf2DeriveBits(algorithm, baseKey, length) { validatePbkdf2DeriveBitsLength(length); const { iterations, hash, salt } = algorithm; if (length === 0) - return new ArrayBuffer(0); + return PromiseResolve(new ArrayBuffer(0)); - let result; + let password; + let normalizedHash; try { // TODO(panva): call the job directly without needing to re-export the handle - result = await pbkdf2Promise( - getCryptoKeyHandle(baseKey).export(), salt, iterations, length / 8, normalizeHashName(hash.name), - ); + password = getCryptoKeyHandle(baseKey).export(); + normalizedHash = normalizeHashName(hash.name); } catch (err) { throw lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err }); } - return TypedArrayPrototypeGetBuffer(result); + return jobPromise(() => new PBKDF2Job( + kCryptoJobWebCrypto, + password, + salt, + iterations, + length / 8, + normalizedHash)); } module.exports = { diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 6034ed64e69514..d72d55c2bbff42 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -10,7 +10,7 @@ const { const { RSACipherJob, SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyFormatDER, kSignJobModeSign, kSignJobModeVerify, @@ -85,7 +85,7 @@ function validateRsaOaepAlgorithm(algorithm) { } } -async function rsaOaepCipher(mode, key, data, algorithm) { +function rsaOaepCipher(mode, key, data, algorithm) { validateRsaOaepAlgorithm(algorithm); const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private'; @@ -95,8 +95,8 @@ async function rsaOaepCipher(mode, key, data, algorithm) { 'InvalidAccessError'); } - return await jobPromise(() => new RSACipherJob( - kCryptoJobAsync, + return jobPromise(() => new RSACipherJob( + kCryptoJobWebCrypto, mode, getCryptoKeyHandle(key), data, @@ -105,7 +105,7 @@ async function rsaOaepCipher(mode, key, data, algorithm) { algorithm.label)); } -async function rsaKeyGenerate( +function rsaKeyGenerate( algorithm, extractable, keyUsages, @@ -142,12 +142,6 @@ async function rsaKeyGenerate( } } - const handles = await jobPromise(() => new RsaKeyPairGenJob( - kCryptoJobAsync, - kKeyVariantRSA_SSA_PKCS1_v1_5, - modulusLength, - publicExponentConverted)); - const keyAlgorithm = { name, modulusLength, @@ -155,6 +149,12 @@ async function rsaKeyGenerate( hash, }; + if (publicExponentConverted < 3 || publicExponentConverted % 2 === 0) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError'); + } + let publicUsages; let privateUsages; switch (name) { @@ -170,21 +170,21 @@ async function rsaKeyGenerate( } } - const publicKey = - new InternalCryptoKey( - handles[0], - keyAlgorithm, - getUsagesMask(publicUsages), - true); - - const privateKey = - new InternalCryptoKey( - handles[1], - keyAlgorithm, - getUsagesMask(privateUsages), - extractable); - - return { __proto__: null, publicKey, privateKey }; + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + + return jobPromise(() => new RsaKeyPairGenJob( + kCryptoJobWebCrypto, + kKeyVariantRSA_SSA_PKCS1_v1_5, + modulusLength, + publicExponentConverted, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function rsaExportKey(key, format) { @@ -276,39 +276,44 @@ function rsaImportKey( }, getUsagesMask(usagesSet), extractable); } -async function rsaSignVerify(key, data, { saltLength }, signature) { +function rsaSignVerify(key, data, { saltLength }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => { - const algorithm = getCryptoKeyAlgorithm(key); - if (algorithm.name === 'RSA-PSS') { + const algorithm = getCryptoKeyAlgorithm(key); + if (algorithm.name === 'RSA-PSS') { + try { validateInt32( saltLength, 'algorithm.saltLength', 0, - MathCeil((algorithm.modulusLength - 1) / 8) - getDigestSizeInBytes(algorithm.hash.name) - 2); + MathCeil((algorithm.modulusLength - 1) / 8) - + getDigestSizeInBytes(algorithm.hash.name) - 2); + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); } + } - return new SignJob( - kCryptoJobAsync, - signature === undefined ? kSignJobModeSign : kSignJobModeVerify, - getCryptoKeyHandle(key), - undefined, - undefined, - undefined, - undefined, - data, - normalizeHashName(algorithm.hash.name), - saltLength, - algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, - undefined, - undefined, - signature); - }); + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, + signature === undefined ? kSignJobModeSign : kSignJobModeVerify, + getCryptoKeyHandle(key), + undefined, + undefined, + undefined, + undefined, + data, + normalizeHashName(algorithm.hash.name), + saltLength, + algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, + undefined, + undefined, + signature)); } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 663375b9e155d2..72f8ef135168b8 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -9,13 +9,12 @@ const { DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, - FunctionPrototypeBind, Number, ObjectDefineProperty, ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, - PromiseWithResolvers, + PromiseReject, SafeMap, SafeSet, StringPrototypeToUpperCase, @@ -665,25 +664,17 @@ const validateByteSource = hideStackFrames((val, name) => { val); }); -function onDone(resolve, reject, err, result) { - if (err) { - return reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err })); - } - resolve(result); -} - +// CryptoJob constructors can synchronously throw while running their native +// AdditionalConfig hook. WebCrypto needs those operation-specific setup +// failures to reject with an OperationError. function jobPromise(getJob) { - const { promise, resolve, reject } = PromiseWithResolvers(); try { - const job = getJob(); - job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); - job.run(); + return getJob().run(); } catch (err) { - onDone(resolve, reject, err); + return PromiseReject(lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err })); } - return promise; } // In WebCrypto, the publicExponent option in RSA is represented as a diff --git a/src/crypto/README.md b/src/crypto/README.md index 263a512cdefc9b..4059ae23711b84 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -186,7 +186,8 @@ All operations that are not either Stream-based or single-use functions are built around the `CryptoJob` class. A `CryptoJob` encapsulates a single crypto operation that can be -invoked synchronously or asynchronously. +invoked synchronously, asynchronously, or as a Web Crypto API +Promise-based job. The `CryptoJob` class itself is a C++ template that takes a single `CryptoJobTraits` struct as a parameter. The `CryptoJobTraits` @@ -228,14 +229,15 @@ specializations and will either be called synchronously within the current thread or from within the libuv threadpool. Every `CryptoJob` instance exposes a `run()` function to the -JavaScript layer. When called, `run()` with either dispatch the -job to the libuv threadpool or invoke the Implementation -function synchronously. If invoked synchronously, run() will -return a JavaScript array. The first value in the array is -either an `Error` or `undefined`. If the operation was successful, -the second value in the array will contain the result of the -operation. Typically, the result is an `ArrayBuffer`, but -certain `CryptoJob` types can alter the output. +JavaScript layer. When called, `run()` will either dispatch the +job to the libuv threadpool, invoke the Implementation function +synchronously, or return a `Promise` for Web Crypto API jobs. If +invoked synchronously, `run()` will return a JavaScript array. +The first value in the array is either an `Error` or `undefined`. +If the operation was successful, the second value in the array +will contain the result of the operation. Typically, the result +is an `ArrayBuffer`, but certain `CryptoJob` types can alter the +output. If the `CryptoJob` is processed asynchronously, then the job must have an `ondone` property whose value is a function that @@ -244,11 +246,19 @@ be called with two arguments. The first is either an `Error` or `undefined`, and the second is the result of the operation if successful. +If the `CryptoJob` is processed as a Web Crypto API job, then +`run()` returns a Promise. Operation-specific failures are +rejected with an `OperationError`, and successful jobs resolve +with the Web Crypto API result shape expected by the JavaScript +implementation. + For `CipherJob` types, the output is always an `ArrayBuffer`. For `KeyGenJob` types, the output is either a single KeyObject, or an array containing a Public/Private key pair represented -either as a `KeyObjectHandle` object or a `Buffer`. +either as a `KeyObjectHandle` object or a `Buffer`. Web Crypto +API key generation jobs return a `CryptoKey` or a `CryptoKeyPair` +object. For `DeriveBitsJob` type output is typically an `ArrayBuffer` but can be other values (`RandomBytesJob` for instance, fills an @@ -273,11 +283,12 @@ should be used to throw JavaScript errors when necessary. ### Operation mode -All crypto functions in Node.js operate in one of three +All crypto functions in Node.js operate in one of these modes: * Synchronous single-call * Asynchronous single-call +* Web Crypto API Promise-based * Stream-oriented It is often possible to perform various operations across diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index 815c972837049a..9172def7d4ebee 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -418,9 +418,7 @@ bool ValidateIV( THROW_ERR_OUT_OF_RANGE(env, "iv is too big"); return false; } - params->iv = (mode == kCryptoJobAsync) - ? iv.ToCopy() - : iv.ToByteSource(); + params->iv = (IsCryptoJobAsync(mode)) ? iv.ToCopy() : iv.ToByteSource(); return true; } @@ -466,9 +464,9 @@ bool ValidateAdditionalData( THROW_ERR_OUT_OF_RANGE(env, "additionalData is too big"); return false; } - params->additional_data = mode == kCryptoJobAsync - ? additional.ToCopy() - : additional.ToByteSource(); + params->additional_data = IsCryptoJobAsync(mode) + ? additional.ToCopy() + : additional.ToByteSource(); } return true; } @@ -495,7 +493,7 @@ AESCipherConfig& AESCipherConfig::operator=(AESCipherConfig&& other) noexcept { void AESCipherConfig::MemoryInfo(MemoryTracker* tracker) const { // If mode is sync, then the data in each of these properties // is not owned by the AESCipherConfig, so we ignore it. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("iv", iv.size()); tracker->TrackFieldWithSize("additional_data", additional_data.size()); } diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index d5207f4be57bb2..dd71f038f0ce3e 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -36,7 +36,7 @@ Argon2Config& Argon2Config::operator=(Argon2Config&& other) noexcept { } void Argon2Config::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("secret", secret.size()); @@ -84,7 +84,7 @@ Maybe Argon2Traits::AdditionalConfig( return Nothing(); } - const bool isAsync = mode == kCryptoJobAsync; + const bool isAsync = IsCryptoJobAsync(mode); config->pass = isAsync ? pass.ToCopy() : pass.ToByteSource(); config->salt = isAsync ? salt.ToCopy() : salt.ToByteSource(); config->secret = isAsync ? secret.ToCopy() : secret.ToByteSource(); diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc index 43d63fa8c5e409..cfe43122d5aa55 100644 --- a/src/crypto/crypto_chacha20_poly1305.cc +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -48,7 +48,7 @@ bool ValidateIV(Environment* env, return false; } - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { params->iv = iv.ToCopy(); } else { params->iv = iv.ToByteSource(); @@ -68,7 +68,7 @@ bool ValidateAdditionalData(Environment* env, return false; } - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { params->additional_data = additional_data.ToCopy(); } else { params->additional_data = additional_data.ToByteSource(); @@ -96,7 +96,7 @@ ChaCha20Poly1305CipherConfig& ChaCha20Poly1305CipherConfig::operator=( void ChaCha20Poly1305CipherConfig::MemoryInfo(MemoryTracker* tracker) const { // If mode is sync, then the data in each of these properties // is not owned by the ChaCha20Poly1305CipherConfig, so we ignore it. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("iv", iv.size()); tracker->TrackFieldWithSize("additional_data", additional_data.size()); } diff --git a/src/crypto/crypto_cipher.h b/src/crypto/crypto_cipher.h index 006d18a7118761..a00afa6a0f9f81 100644 --- a/src/crypto/crypto_cipher.h +++ b/src/crypto/crypto_cipher.h @@ -164,13 +164,7 @@ class CipherJob final : public CryptoJob { } new CipherJob( - env, - args.This(), - mode, - key, - cipher_mode, - data, - std::move(params)); + env, args.This(), mode, key, cipher_mode, data, std::move(params)); } static void Initialize( @@ -197,7 +191,7 @@ class CipherJob final : public CryptoJob { std::move(params)), key_(key->Data().addRef()), cipher_mode_(cipher_mode), - in_(mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource()) {} + in_(IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource()) {} const KeyObjectData& key() const { return key_; } @@ -261,7 +255,7 @@ class CipherJob final : public CryptoJob { SET_SELF_SIZE(CipherJob) void MemoryInfo(MemoryTracker* tracker) const override { - if (CryptoJob::mode() == kCryptoJobAsync) + if (IsCryptoJobAsync(CryptoJob::mode())) tracker->TrackFieldWithSize("in", in_.size()); tracker->TrackFieldWithSize("out", out_.size()); CryptoJob::MemoryInfo(tracker); diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index c42926bb4ce61f..44181cd045b429 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -510,8 +510,7 @@ HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { void HashConfig::MemoryInfo(MemoryTracker* tracker) const { // If the Job is sync, then the HashConfig does not own the data. - if (mode == kCryptoJobAsync) - tracker->TrackFieldWithSize("in", in.size()); + if (IsCryptoJobAsync(mode)) tracker->TrackFieldWithSize("in", in.size()); } MaybeLocal HashTraits::EncodeOutput(Environment* env, @@ -542,9 +541,7 @@ Maybe HashTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->in = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); unsigned int expected = EVP_MD_size(params->digest); params->length = expected; diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc index 53b8f75c39bd97..fe12df11003604 100644 --- a/src/crypto/crypto_hkdf.cc +++ b/src/crypto/crypto_hkdf.cc @@ -77,13 +77,9 @@ Maybe HKDFTraits::AdditionalConfig( return Nothing(); } - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); - params->info = mode == kCryptoJobAsync - ? info.ToCopy() - : info.ToByteSource(); + params->info = IsCryptoJobAsync(mode) ? info.ToCopy() : info.ToByteSource(); params->length = args[offset + 4].As()->Value(); // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as the @@ -130,7 +126,7 @@ bool HKDFTraits::DeriveBits(Environment* env, void HKDFConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the HKDFConfig does not own the data - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("info", info.size()); } diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc index 80d8608b434c31..acd4b819de38fc 100644 --- a/src/crypto/crypto_hmac.cc +++ b/src/crypto/crypto_hmac.cc @@ -172,7 +172,7 @@ HmacConfig& HmacConfig::operator=(HmacConfig&& other) noexcept { void HmacConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the HmacConfig does not own the data - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); } @@ -210,9 +210,7 @@ Maybe HmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (!args[offset + 4]->IsUndefined()) { ArrayBufferOrViewContents signature(args[offset + 4]); @@ -220,9 +218,8 @@ Maybe HmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); } - params->signature = mode == kCryptoJobAsync - ? signature.ToCopy() - : signature.ToByteSource(); + params->signature = + IsCryptoJobAsync(mode) ? signature.ToCopy() : signature.ToByteSource(); } return JustVoid(); diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index d30c6aaef6253f..c16977f2d4636a 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -16,6 +16,7 @@ namespace node { using ncrypto::EVPKeyPointer; using v8::Array; +using v8::ArrayBufferView; using v8::FunctionCallbackInfo; using v8::Local; using v8::Maybe; @@ -41,7 +42,7 @@ KEMConfiguration& KEMConfiguration::operator=( void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("ciphertext", ciphertext.size()); } } @@ -173,6 +174,23 @@ MaybeLocal KEMEncapsulateTraits::EncodeOutput( return MaybeLocal(); } + if (params.job_mode == kCryptoJobWebCrypto) { + Local result = Object::New(env->isolate()); + if (result + ->Set(env->context(), + OneByteString(env->isolate(), "sharedKey"), + shared_key_obj.As()->Buffer()) + .IsNothing() || + result + ->Set(env->context(), + OneByteString(env->isolate(), "ciphertext"), + ciphertext_obj.As()->Buffer()) + .IsNothing()) { + return MaybeLocal(); + } + return result; + } + // Return an array [sharedKey, ciphertext]. Local result = Array::New(env->isolate(), 2); if (result->Set(env->context(), 0, shared_key_obj).IsNothing() || @@ -209,7 +227,7 @@ Maybe KEMDecapsulateTraits::AdditionalConfig( } params->ciphertext = - mode == kCryptoJobAsync ? ciphertext.ToCopy() : ciphertext.ToByteSource(); + IsCryptoJobAsync(mode) ? ciphertext.ToCopy() : ciphertext.ToByteSource(); return v8::JustVoid(); } diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index e43d8cb0475ff2..aa9bd745192c92 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -22,6 +22,20 @@ enum class KeyGenJobStatus { FAILED }; +struct WebCryptoKeyGenConfig final { + v8::Global algorithm; + uint32_t usages_mask = 0; + uint32_t public_usages_mask = 0; + uint32_t private_usages_mask = 0; + bool extractable = false; + + WebCryptoKeyGenConfig() = default; + WebCryptoKeyGenConfig(WebCryptoKeyGenConfig&&) = default; + WebCryptoKeyGenConfig& operator=(WebCryptoKeyGenConfig&&) = default; + WebCryptoKeyGenConfig(const WebCryptoKeyGenConfig&) = delete; + WebCryptoKeyGenConfig& operator=(const WebCryptoKeyGenConfig&) = delete; +}; + // A Base CryptoJob for generating secret keys or key pairs. // The KeyGenTraits is largely responsible for the details of // the implementation, while KeyGenJob handles the common @@ -48,7 +62,29 @@ class KeyGenJob final : public CryptoJob { return; } - new KeyGenJob(env, args.This(), mode, std::move(params)); + WebCryptoKeyGenConfig config; + if (mode == kCryptoJobWebCrypto) { + if constexpr (KeyGenTraits::kWebCryptoKeyPair) { + CHECK(args[offset]->IsObject()); + CHECK(args[offset + 1]->IsUint32()); + CHECK(args[offset + 2]->IsUint32()); + CHECK(args[offset + 3]->IsBoolean()); + config.algorithm.Reset(env->isolate(), args[offset]); + config.public_usages_mask = args[offset + 1].As()->Value(); + config.private_usages_mask = args[offset + 2].As()->Value(); + config.extractable = args[offset + 3]->IsTrue(); + } else { + CHECK(args[offset]->IsObject()); + CHECK(args[offset + 1]->IsUint32()); + CHECK(args[offset + 2]->IsBoolean()); + config.algorithm.Reset(env->isolate(), args[offset]); + config.usages_mask = args[offset + 1].As()->Value(); + config.extractable = args[offset + 2]->IsTrue(); + } + } + + new KeyGenJob( + env, args.This(), mode, std::move(params), std::move(config)); } static void Initialize( @@ -61,17 +97,14 @@ class KeyGenJob final : public CryptoJob { CryptoJob::RegisterExternalReferences(New, registry); } - KeyGenJob( - Environment* env, - v8::Local object, - CryptoJobMode mode, - AdditionalParams&& params) + KeyGenJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params, + WebCryptoKeyGenConfig&& config) : CryptoJob( - env, - object, - KeyGenTraits::Provider, - mode, - std::move(params)) {} + env, object, KeyGenTraits::Provider, mode, std::move(params)), + webcrypto_config_(std::move(config)) {} void DoThreadPoolWork() override { AdditionalParams* params = CryptoJob::params(); @@ -98,7 +131,11 @@ class KeyGenJob final : public CryptoJob { if (status_ == KeyGenJobStatus::OK) { v8::TryCatch try_catch(env->isolate()); - if (KeyGenTraits::EncodeKey(env, params).ToLocal(result)) { + v8::MaybeLocal encoded = + CryptoJob::mode() == kCryptoJobWebCrypto + ? EncodeWebCryptoKey(env, params) + : KeyGenTraits::EncodeKey(env, params); + if (encoded.ToLocal(result)) { *err = Undefined(env->isolate()); } else { CHECK(try_catch.HasCaught()); @@ -122,6 +159,53 @@ class KeyGenJob final : public CryptoJob { SET_SELF_SIZE(KeyGenJob) private: + v8::MaybeLocal EncodeWebCryptoKey(Environment* env, + AdditionalParams* params) { + v8::Isolate* isolate = env->isolate(); + v8::Local algorithm = + v8::Local::New(isolate, webcrypto_config_.algorithm); + + if constexpr (KeyGenTraits::kWebCryptoKeyPair) { + v8::Local public_key; + v8::Local private_key; + if (!NativeCryptoKey::Create(env, + params->key.addRefWithType(kKeyTypePublic), + algorithm, + webcrypto_config_.public_usages_mask, + true) + .ToLocal(&public_key) || + !NativeCryptoKey::Create(env, + params->key.addRefWithType(kKeyTypePrivate), + algorithm, + webcrypto_config_.private_usages_mask, + webcrypto_config_.extractable) + .ToLocal(&private_key)) { + return {}; + } + + v8::Local ret = v8::Object::New(isolate); + if (ret->Set(env->context(), + OneByteString(isolate, "publicKey"), + public_key) + .IsNothing() || + ret->Set(env->context(), + OneByteString(isolate, "privateKey"), + private_key) + .IsNothing()) { + return {}; + } + return ret; + } else { + auto data = KeyObjectData::CreateSecret(std::move(params->out)); + return NativeCryptoKey::Create(env, + data, + algorithm, + webcrypto_config_.usages_mask, + webcrypto_config_.extractable); + } + } + + WebCryptoKeyGenConfig webcrypto_config_; KeyGenJobStatus status_ = KeyGenJobStatus::FAILED; }; @@ -130,6 +214,7 @@ template struct KeyPairGenTraits final { using AdditionalParameters = typename KeyPairAlgorithmTraits::AdditionalParameters; + static constexpr bool kWebCryptoKeyPair = true; static const AsyncWrap::ProviderType Provider = AsyncWrap::PROVIDER_KEYPAIRGENREQUEST; @@ -146,8 +231,13 @@ struct KeyPairGenTraits final { // process input parameters. This allows each job to have a variable // number of input parameters specific to each job type. if (KeyPairAlgorithmTraits::AdditionalConfig(mode, args, offset, params) - .IsNothing() || - !KeyObjectData::GetPublicKeyEncodingFromJs( + .IsNothing()) { + return v8::Nothing(); + } + + if (mode == kCryptoJobWebCrypto) return v8::JustVoid(); + + if (!KeyObjectData::GetPublicKeyEncodingFromJs( args, offset, kKeyContextGenerate) .To(¶ms->public_key_encoding) || !KeyObjectData::GetPrivateKeyEncodingFromJs( @@ -204,6 +294,7 @@ struct SecretKeyGenConfig final : public MemoryRetainer { struct SecretKeyGenTraits final { using AdditionalParameters = SecretKeyGenConfig; + static constexpr bool kWebCryptoKeyPair = false; static const AsyncWrap::ProviderType Provider = AsyncWrap::PROVIDER_KEYGENREQUEST; static constexpr const char* JobName = "SecretKeyGenJob"; @@ -287,4 +378,3 @@ using SecretKeyGenJob = KeyGenJob; #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #endif // SRC_CRYPTO_CRYPTO_KEYGEN_H_ - diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index aac059696596e4..cfada2c766082e 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -27,6 +27,7 @@ using ncrypto::EVPKeyCtxPointer; using ncrypto::EVPKeyPointer; using ncrypto::MarkPopErrorOnReturn; using v8::Array; +using v8::Boolean; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -1716,6 +1717,38 @@ bool NativeCryptoKey::HasInstance(Environment* env, Local value) { return IsNativeCryptoKey(env, value); } +MaybeLocal NativeCryptoKey::Create(Environment* env, + const KeyObjectData& data, + Local algorithm, + uint32_t usages_mask, + bool extractable) { + Local context = env->context(); + Isolate* isolate = env->isolate(); + CHECK(algorithm->IsObject()); + + Local handle; + if (!KeyObjectHandle::Create(env, data).ToLocal(&handle)) return {}; + + if (env->crypto_internal_cryptokey_constructor().IsEmpty()) { + Local arg = FIXED_ONE_BYTE_STRING(isolate, "internal/crypto/keys"); + if (env->builtin_module_require() + ->Call(context, Null(isolate), 1, &arg) + .IsEmpty()) { + return {}; + } + } + + Local cryptokey_ctor = env->crypto_internal_cryptokey_constructor(); + CHECK(!cryptokey_ctor.IsEmpty()); + Local ctor_args[] = { + handle, + algorithm, + Uint32::NewFromUnsigned(isolate, usages_mask), + Boolean::New(isolate, extractable), + }; + return cryptokey_ctor->NewInstance(context, arraysize(ctor_args), ctor_args); +} + void NativeCryptoKey::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 4); diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index 8bba206a08239e..6adedc89fafffe 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -271,6 +271,12 @@ class NativeCryptoKey : public BaseObject { static void CreateCryptoKeyClass( const v8::FunctionCallbackInfo& args); + static v8::MaybeLocal Create(Environment* env, + const KeyObjectData& data, + v8::Local algorithm, + uint32_t usages_mask, + bool extractable); + // True if `value` is a real NativeCryptoKey instance. Uses the // FunctionTemplate stored on the Environment as a brand check. // Used by `GetSlots` to validate its receiver. diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index ed4a8e9d526983..1b685bb5f6983c 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -45,7 +45,7 @@ KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { void KmacConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the KmacConfig does not own the data. - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); tracker->TrackFieldWithSize("customization", customization.size()); @@ -90,7 +90,7 @@ Maybe KmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); return Nothing(); } - params->customization = mode == kCryptoJobAsync + params->customization = IsCryptoJobAsync(mode) ? customization.ToCopy() : customization.ToByteSource(); } @@ -104,7 +104,7 @@ Maybe KmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (!args[offset + 6]->IsUndefined()) { ArrayBufferOrViewContents signature(args[offset + 6]); @@ -113,7 +113,7 @@ Maybe KmacTraits::AdditionalConfig( return Nothing(); } params->signature = - mode == kCryptoJobAsync ? signature.ToCopy() : signature.ToByteSource(); + IsCryptoJobAsync(mode) ? signature.ToCopy() : signature.ToByteSource(); } return JustVoid(); diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc index 8bdb44d3bdca31..b4bd02a455786e 100644 --- a/src/crypto/crypto_pbkdf2.cc +++ b/src/crypto/crypto_pbkdf2.cc @@ -35,7 +35,7 @@ PBKDF2Config& PBKDF2Config::operator=(PBKDF2Config&& other) noexcept { void PBKDF2Config::MemoryInfo(MemoryTracker* tracker) const { // The job is sync, the PBKDF2Config does not own the data. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } @@ -76,13 +76,9 @@ Maybe PBKDF2Traits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? pass.ToCopy() - : pass.ToByteSource(); + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsInt32()); // iteration_count CHECK(args[offset + 3]->IsInt32()); // length diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index e722f87b23fcbe..310f9fef293138 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -223,7 +223,7 @@ RSACipherConfig::RSACipherConfig(RSACipherConfig&& other) noexcept digest(other.digest) {} void RSACipherConfig::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) + if (IsCryptoJobAsync(mode)) tracker->TrackFieldWithSize("label", label.size()); } diff --git a/src/crypto/crypto_scrypt.cc b/src/crypto/crypto_scrypt.cc index eba141f372f536..91ed9fee71f052 100644 --- a/src/crypto/crypto_scrypt.cc +++ b/src/crypto/crypto_scrypt.cc @@ -38,7 +38,7 @@ ScryptConfig& ScryptConfig::operator=(ScryptConfig&& other) noexcept { } void ScryptConfig::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } @@ -72,13 +72,9 @@ Maybe ScryptTraits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? pass.ToCopy() - : pass.ToByteSource(); + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsUint32()); // N CHECK(args[offset + 3]->IsUint32()); // r diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index d8a4fe395a5f47..153ac843677970 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -564,7 +564,7 @@ SignConfiguration& SignConfiguration::operator=( void SignConfiguration::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); tracker->TrackFieldWithSize("context_string", context_string.size()); @@ -603,9 +603,7 @@ Maybe SignTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (args[offset + 7]->IsString()) { Utf8Value digest(env->isolate(), args[offset + 7]); @@ -642,7 +640,7 @@ Maybe SignTraits::AdditionalConfig( return Nothing(); } params->flags |= SignConfiguration::kHasContextString; - params->context_string = mode == kCryptoJobAsync + params->context_string = IsCryptoJobAsync(mode) ? context_string.ToCopy() : context_string.ToByteSource(); } @@ -660,9 +658,8 @@ Maybe SignTraits::AdditionalConfig( if (UseP1363Encoding(akey, params->dsa_encoding)) { params->signature = ConvertSignatureToDER(akey, signature.ToByteSource()); } else { - params->signature = mode == kCryptoJobAsync - ? signature.ToCopy() - : signature.ToByteSource(); + params->signature = IsCryptoJobAsync(mode) ? signature.ToCopy() + : signature.ToByteSource(); } } diff --git a/src/crypto/crypto_turboshake.cc b/src/crypto/crypto_turboshake.cc index 26107f82aebbd3..06b2ef9d6f5aea 100644 --- a/src/crypto/crypto_turboshake.cc +++ b/src/crypto/crypto_turboshake.cc @@ -419,7 +419,7 @@ TurboShakeConfig& TurboShakeConfig::operator=( } void TurboShakeConfig::MemoryInfo(MemoryTracker* tracker) const { - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource tracker->TrackFieldWithSize("data", data.size()); } @@ -464,7 +464,7 @@ Maybe TurboShakeTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); return JustVoid(); } @@ -527,7 +527,7 @@ KangarooTwelveConfig& KangarooTwelveConfig::operator=( } void KangarooTwelveConfig::MemoryInfo(MemoryTracker* tracker) const { - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("customization", customization.size()); @@ -563,7 +563,7 @@ Maybe KangarooTwelveTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); return Nothing(); } - params->customization = mode == kCryptoJobAsync + params->customization = IsCryptoJobAsync(mode) ? customization.ToCopy() : customization.ToByteSource(); } @@ -578,7 +578,7 @@ Maybe KangarooTwelveTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); return JustVoid(); } diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index b9d037fb72352b..f6862f7954abf9 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -32,7 +32,9 @@ using ncrypto::DataPointer; using ncrypto::EnginePointer; #endif // !OPENSSL_NO_ENGINE using ncrypto::SSLPointer; +using v8::Array; using v8::ArrayBuffer; +using v8::ArrayBufferView; using v8::BackingStore; using v8::BackingStoreInitializationMode; using v8::BackingStoreOnFailureMode; @@ -40,6 +42,7 @@ using v8::BigInt; using v8::Context; using v8::EscapableHandleScope; using v8::Exception; +using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; @@ -709,10 +712,64 @@ Maybe SetEncodedValue(Environment* env, CryptoJobMode GetCryptoJobMode(v8::Local args) { CHECK(args->IsUint32()); uint32_t mode = args.As()->Value(); - CHECK_LE(mode, kCryptoJobSync); + CHECK_LE(mode, kCryptoJobWebCrypto); return static_cast(mode); } +bool IsCryptoJobAsync(CryptoJobMode mode) { + return mode == kCryptoJobAsync || mode == kCryptoJobWebCrypto; +} + +MaybeLocal CreateWebCryptoJobError(Environment* env, + Local cause) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Local per_context_bindings; + Local domexception_ctor; + if (!GetPerContextExports(context).ToLocal(&per_context_bindings) || + !per_context_bindings + ->Get(context, FIXED_ONE_BYTE_STRING(isolate, "DOMException")) + .ToLocal(&domexception_ctor)) { + return {}; + } + CHECK(domexception_ctor->IsFunction()); + + Local options = Object::New(isolate); + if (options + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "name"), + FIXED_ONE_BYTE_STRING(isolate, "OperationError")) + .IsNothing() || + options->Set(context, FIXED_ONE_BYTE_STRING(isolate, "cause"), cause) + .IsNothing()) { + return {}; + } + + Local argv[] = { + FIXED_ONE_BYTE_STRING(isolate, + "The operation failed for an operation-specific " + "reason"), + options, + }; + + return domexception_ctor.As()->NewInstance( + context, arraysize(argv), argv); +} + +MaybeLocal ToWebCryptoJobResult(Environment* env, Local value) { + if (value->IsArrayBuffer()) { + return value; + } + + if (Buffer::HasInstance(value)) { + return value.As()->Buffer(); + } + + CHECK(value->IsBoolean() || (value->IsObject() && !value->IsArray() && + !value->IsArrayBufferView())); + return value; +} + namespace { // SecureBuffer uses OpenSSL's secure heap feature to allocate a // Uint8Array. Without --secure-heap, OpenSSL's secure heap is disabled, @@ -780,6 +837,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kCryptoJobAsync); NODE_DEFINE_CONSTANT(target, kCryptoJobSync); + NODE_DEFINE_CONSTANT(target, kCryptoJobWebCrypto); SetMethod(context, target, "secureBuffer", SecureBuffer); SetMethodNoSideEffect(context, target, "secureHeapUsed", SecureHeapUsed); diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index a5b4829bc23cf2..66be7d00d2b5c8 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -249,12 +249,16 @@ class ByteSource final { : data_(data), allocated_data_(allocated_data), size_(size) {} }; -enum CryptoJobMode { - kCryptoJobAsync, - kCryptoJobSync -}; +enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync, kCryptoJobWebCrypto }; CryptoJobMode GetCryptoJobMode(v8::Local args); +bool IsCryptoJobAsync(CryptoJobMode mode); + +v8::MaybeLocal CreateWebCryptoJobError(Environment* env, + v8::Local cause); + +v8::MaybeLocal ToWebCryptoJobResult(Environment* env, + v8::Local value); template class CryptoJob : public AsyncWrap, public ThreadPoolWork { @@ -283,9 +287,53 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { void AfterThreadPoolWork(int status) override { Environment* env = AsyncWrap::env(); - CHECK_EQ(mode_, kCryptoJobAsync); + CHECK(IsCryptoJobAsync(mode_)); CHECK(status == 0 || status == UV_ECANCELED); std::unique_ptr ptr(this); + if (mode_ == kCryptoJobWebCrypto) { + v8::HandleScope handle_scope(env->isolate()); + v8::Context::Scope context_scope(env->context()); + InternalCallbackScope callback_scope(this); + + if (status == UV_ECANCELED) { + v8::Local exception = v8::Exception::Error( + OneByteString(env->isolate(), "The operation was canceled")); + ptr->RejectWebCrypto(exception); + return; + } + + v8::Local err; + v8::Local result; + { + node::errors::TryCatchScope try_catch(env); + if (ptr->ToResult(&err, &result).IsNothing()) { + CHECK(try_catch.HasCaught()); + CHECK(try_catch.CanContinue()); + err = try_catch.Exception(); + } + } + + if (!err.IsEmpty() && !err->IsUndefined()) { + ptr->RejectWebCrypto(err); + return; + } + + CHECK(!result.IsEmpty()); + v8::Local webcrypto_result; + { + node::errors::TryCatchScope try_catch(env); + if (!ToWebCryptoJobResult(env, result).ToLocal(&webcrypto_result)) { + CHECK(try_catch.HasCaught()); + CHECK(try_catch.CanContinue()); + ptr->RejectWebCrypto(try_catch.Exception()); + return; + } + } + + ptr->ResolveWebCrypto(webcrypto_result); + return; + } + // If the job was canceled do not execute the callback. // TODO(@jasnell): We should likely revisit skipping the // callback on cancel as that could leave the JS in a pending @@ -340,6 +388,19 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { CryptoJob* job; ASSIGN_OR_RETURN_UNWRAP(&job, args.This()); + if (job->mode() == kCryptoJobWebCrypto) { + v8::Local resolver; + if (!v8::Promise::Resolver::New(env->context()).ToLocal(&resolver)) { + return; + } + + CHECK(job->resolver_.IsEmpty()); + job->resolver_.Reset(env->isolate(), resolver); + args.GetReturnValue().Set(resolver->GetPromise()); + + return job->ScheduleWork(); + } + if (job->mode() == kCryptoJobAsync) return job->ScheduleWork(); @@ -376,9 +437,45 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { } private: + void ResolveWebCrypto(v8::Local value) { + Environment* env = AsyncWrap::env(); + v8::Local resolver = + v8::Local::New(env->isolate(), resolver_); + + v8::Local exception; + { + node::errors::TryCatchScope try_catch(env); + if (resolver->Resolve(env->context(), value).IsJust()) { + resolver_.Reset(); + return; + } + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } + + if (!exception.IsEmpty()) { + USE(resolver->Reject(env->context(), exception)); + } + resolver_.Reset(); + } + + void RejectWebCrypto(v8::Local cause) { + Environment* env = AsyncWrap::env(); + v8::Local exception; + if (!CreateWebCryptoJobError(env, cause).ToLocal(&exception)) { + exception = cause; + } + v8::Local resolver = + v8::Local::New(env->isolate(), resolver_); + USE(resolver->Reject(env->context(), exception)); + resolver_.Reset(); + } + const CryptoJobMode mode_; CryptoErrorStore errors_; AdditionalParams params_; + v8::Global resolver_; }; template @@ -413,17 +510,12 @@ class DeriveBitsJob final : public CryptoJob { CryptoJob::RegisterExternalReferences(New, registry); } - DeriveBitsJob( - Environment* env, - v8::Local object, - CryptoJobMode mode, - AdditionalParams&& params) + DeriveBitsJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params) : CryptoJob( - env, - object, - DeriveBitsTraits::Provider, - mode, - std::move(params)) {} + env, object, DeriveBitsTraits::Provider, mode, std::move(params)) {} void DoThreadPoolWork() override { ncrypto::ClearErrorOnReturn clear_error_on_return; diff --git a/test/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js index c8015d00458ac1..2137bf345d4ae9 100644 --- a/test/parallel/test-crypto-argon2.js +++ b/test/parallel/test-crypto-argon2.js @@ -95,7 +95,7 @@ const bad = [ ['argon2id', { nonce: nonce.subarray(0, 7) }, 'parameters.nonce.byteLength'], // nonce.byteLength < 8 ['argon2id', { tagLength: 3 }, 'parameters.tagLength'], // tagLength < 4 ['argon2id', { tagLength: 2 ** 32 }, 'parameters.tagLength'], // tagLength > 2^(32)-1 - ['argon2id', { passes: 0 }, 'parameters.passes'], // passes < 2 + ['argon2id', { passes: 0 }, 'parameters.passes'], // passes < 1 ['argon2id', { passes: 2 ** 32 }, 'parameters.passes'], // passes > 2^(32)-1 ['argon2id', { parallelism: 0 }, 'parameters.parallelism'], // parallelism < 1 ['argon2id', { parallelism: 2 ** 24 }, 'parameters.parallelism'], // Parallelism > 2^(24)-1 @@ -103,6 +103,16 @@ const bad = [ ['argon2id', { memory: 2 ** 32 }, 'parameters.memory'], // memory > 2^(32)-1 ]; +{ + const omitted = runArgon2('argon2id', defaults); + const explicitEmpty = runArgon2('argon2id', { + ...defaults, + secret: Buffer.alloc(0), + associatedData: Buffer.alloc(0), + }); + assert.deepStrictEqual(omitted, explicitEmpty); +} + for (const [algorithm, overrides, expected] of good) { const parameters = { ...defaults, ...overrides }; const actual = runArgon2(algorithm, parameters); diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js new file mode 100644 index 00000000000000..c55894c056c8ca --- /dev/null +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -0,0 +1,154 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { types: { isCryptoKey } } = require('util'); +const { internalBinding } = require('internal/test/binding'); +const { + getCryptoKeyHandle, +} = require('internal/crypto/keys'); +const { + getUsagesMask, +} = require('internal/crypto/util'); +const { + aesCipher, +} = require('internal/crypto/aes'); + +const { + AESCipherJob, + EcKeyPairGenJob, + HashJob, + SecretKeyGenJob, + kCryptoJobWebCrypto, + kKeyVariantAES_CBC_128, + kWebCryptoCipherEncrypt, +} = internalBinding('crypto'); + +const { subtle } = globalThis.crypto; + +(async function() { + { + const promise = new HashJob( + kCryptoJobWebCrypto, + 'sha256', + Buffer.from('hello'), + undefined).run(); + + assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); + + let settled = false; + promise.then(common.mustCall(() => { settled = true; })); + await Promise.resolve(); + assert.strictEqual(settled, false); + + const digest = await promise; + assert(digest instanceof ArrayBuffer); + assert.strictEqual(digest.byteLength, 32); + } + + { + const key = await new SecretKeyGenJob( + kCryptoJobWebCrypto, + 128, + { name: 'AES-CBC', length: 128 }, + getUsagesMask(new Set(['encrypt'])), + true).run(); + + assert(isCryptoKey(key)); + assert(key instanceof CryptoKey); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, ['encrypt']); + } + + { + const pair = await new EcKeyPairGenJob( + kCryptoJobWebCrypto, + 'P-256', + undefined, + { name: 'ECDSA', namedCurve: 'P-256' }, + getUsagesMask(new Set(['verify'])), + getUsagesMask(new Set(['sign'])), + true).run(); + + assert.strictEqual(Object.getPrototypeOf(pair), Object.prototype); + assert(isCryptoKey(pair.publicKey)); + assert(isCryptoKey(pair.privateKey)); + assert(pair.publicKey instanceof CryptoKey); + assert(pair.privateKey instanceof CryptoKey); + assert.strictEqual(pair.publicKey.type, 'public'); + assert.strictEqual(pair.privateKey.type, 'private'); + assert.deepStrictEqual(pair.publicKey.usages, ['verify']); + assert.deepStrictEqual(pair.privateKey.usages, ['sign']); + } + + { + const key = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + false, + ['encrypt']); + assert.throws( + () => new AESCipherJob( + kCryptoJobWebCrypto, + kWebCryptoCipherEncrypt, + getCryptoKeyHandle(key), + Buffer.alloc(16), + kKeyVariantAES_CBC_128, + Buffer.alloc(15)), + /Invalid initialization vector/); + + const promise = aesCipher( + kWebCryptoCipherEncrypt, + key, + Buffer.alloc(16), + { name: 'AES-CBC', iv: Buffer.alloc(15) }); + + assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); + await assert.rejects(promise, (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual( + err.message, + 'The operation failed for an operation-specific reason'); + assert(err.cause); + assert.match(err.cause.message, /Invalid initialization vector/); + return true; + }); + } + + { + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify']); + const data = Buffer.from('hello'); + const signature = await subtle.sign('HMAC', key, data); + assert(signature instanceof ArrayBuffer); + assert.strictEqual( + typeof await subtle.verify('HMAC', key, signature, data), + 'boolean'); + } + + { + Object.defineProperty(CryptoKey.prototype, 'then', { + __proto__: null, + configurable: true, + get() { throw new Error('resolve then getter'); }, + }); + + try { + await assert.rejects( + subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt']), + /resolve then getter/); + } finally { + delete CryptoKey.prototype.then; + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivebits-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js index b03447bb1e70b9..e2b465ab206bec 100644 --- a/test/parallel/test-webcrypto-derivebits-argon2.js +++ b/test/parallel/test-webcrypto-derivebits-argon2.js @@ -90,3 +90,36 @@ for (const { algorithm, length, password, params, tag } of vectors) { } })().then(common.mustCall()); } + +{ + (async () => { + const algorithm = { + name: 'Argon2id', + memory: 32, + passes: 3, + parallelism: 4, + nonce: Buffer.alloc(16, 0x02), + }; + const key = await subtle.importKey( + 'raw-secret', + Buffer.alloc(32, 0x01), + algorithm.name, + false, + ['deriveBits']); + + const omitted = await subtle.deriveBits(algorithm, key, 256); + const explicitEmpty = await subtle.deriveBits({ + ...algorithm, + secretValue: Buffer.alloc(0), + associatedData: Buffer.alloc(0), + }, key, 256); + assert.deepStrictEqual(omitted, explicitEmpty); + + await assert.rejects( + subtle.deriveBits({ ...algorithm, passes: 0 }, key, 256), + { + name: 'OperationError', + message: 'passes must be > 0', + }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-encap-decap-ml-kem.js b/test/parallel/test-webcrypto-encap-decap-ml-kem.js index 958a4d240db148..f3850dcdf02546 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -40,6 +40,7 @@ async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results } ['deriveBits'] ); + assert.strictEqual(Object.getPrototypeOf(encapsulated), Object.prototype); assert(encapsulated.sharedKey instanceof CryptoKey); assert(encapsulated.ciphertext instanceof ArrayBuffer); assert.strictEqual(encapsulated.sharedKey.type, 'secret'); @@ -59,6 +60,7 @@ async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results } ['sign', 'verify'] ); + assert.strictEqual(Object.getPrototypeOf(encapsulated2), Object.prototype); assert(encapsulated2.sharedKey instanceof CryptoKey); assert.strictEqual(encapsulated2.sharedKey.algorithm.name, 'HMAC'); assert.strictEqual(encapsulated2.sharedKey.extractable, false); @@ -93,6 +95,7 @@ async function testEncapsulateBits({ name, publicKeyPem, privateKeyPem, results // Test successful encapsulation const encapsulated = await subtle.encapsulateBits({ name }, publicKey); + assert.strictEqual(Object.getPrototypeOf(encapsulated), Object.prototype); assert(encapsulated.sharedKey instanceof ArrayBuffer); assert(encapsulated.ciphertext instanceof ArrayBuffer); assert.strictEqual(encapsulated.sharedKey.byteLength, 32); // ML-KEM shared secret is 32 bytes diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index d73ffd21e563a5..989fdbb476162a 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -297,6 +297,17 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { Promise.all(tests).then(common.mustCall()); } +// Test CryptoKeyPair prototype +{ + subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify']) + .then(common.mustCall((pair) => { + assert.strictEqual(Object.getPrototypeOf(pair), Object.prototype); + })); +} + // Test RSA key generation { async function test( From b8fc182a0ffb34c1555f07995136bd286660e60c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 25 Apr 2026 23:44:44 +0200 Subject: [PATCH 02/89] crypto: remove async from WebCrypto methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove async function wrappers from SubtleCrypto methods while keeping their public promise-returning behaviour. Route method entry points through a shared helper that converts synchronous validation errors into rejected promises. Let the internal implementations return native job promises directly. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63363 Reviewed-By: Antoine du Hamel Reviewed-By: René --- lib/internal/crypto/webcrypto.js | 412 +++++++++++------- test/parallel/test-webcrypto-derivekey.js | 30 ++ .../test-webcrypto-methods-not-async.js | 43 ++ test/parallel/test-webcrypto-webidl.js | 38 ++ 4 files changed, 364 insertions(+), 159 deletions(-) create mode 100644 test/parallel/test-webcrypto-methods-not-async.js diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 1d351ab90bc7c4..7f8726ba11e147 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -6,6 +6,9 @@ const { JSONParse, JSONStringify, ObjectDefineProperties, + PromisePrototypeThen, + PromiseReject, + PromiseResolve, ReflectApply, ReflectConstruct, StringPrototypeRepeat, @@ -70,7 +73,21 @@ const { let webidl; -async function digest(algorithm, data) { +// WebCrypto methods return promises, including for synchronous validation +// failures. Keep that conversion in one place so method bodies stay readable. +function callSubtleCryptoMethod(fn, receiver, args) { + try { + return PromiseResolve(ReflectApply(fn, receiver, args)); + } catch (err) { + return PromiseReject(err); + } +} + +function digest(algorithm, data) { + return callSubtleCryptoMethod(digestImpl, this, arguments); +} + +function digestImpl(algorithm, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -87,7 +104,7 @@ async function digest(algorithm, data) { algorithm = normalizeAlgorithm(algorithm, 'digest'); - return await FunctionPrototypeCall(asyncDigest, this, algorithm, data); + return FunctionPrototypeCall(asyncDigest, this, algorithm, data); } function randomUUID() { @@ -95,7 +112,14 @@ function randomUUID() { return _randomUUID(); } -async function generateKey( +function generateKey( + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(generateKeyImpl, this, arguments); +} + +function generateKeyImpl( algorithm, extractable, keyUsages) { @@ -118,18 +142,14 @@ async function generateKey( }); algorithm = normalizeAlgorithm(algorithm, 'generateKey'); - let result; - let resultType; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaKeyGenerate(algorithm, extractable, keyUsages); - break; case 'Ed25519': // Fall through case 'Ed448': @@ -137,22 +157,16 @@ async function generateKey( case 'X25519': // Fall through case 'X448': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgGenerateKey(algorithm, extractable, keyUsages); - break; case 'ECDSA': // Fall through case 'ECDH': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecGenerateKey(algorithm, extractable, keyUsages); - break; case 'HMAC': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') + return require('internal/crypto/mac') .hmacGenerateKey(algorithm, extractable, keyUsages); - break; case 'AES-CTR': // Fall through case 'AES-CBC': @@ -162,62 +176,40 @@ async function generateKey( case 'AES-OCB': // Fall through case 'AES-KW': - resultType = 'CryptoKey'; - result = await require('internal/crypto/aes') + return require('internal/crypto/aes') .aesGenerateKey(algorithm, extractable, keyUsages); - break; case 'ChaCha20-Poly1305': - resultType = 'CryptoKey'; - result = await require('internal/crypto/chacha20_poly1305') + return require('internal/crypto/chacha20_poly1305') .c20pGenerateKey(algorithm, extractable, keyUsages); - break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ml_dsa') + return require('internal/crypto/ml_dsa') .mlDsaGenerateKey(algorithm, extractable, keyUsages); - break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemGenerateKey(algorithm, extractable, keyUsages); - break; case 'KMAC128': // Fall through case 'KMAC256': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') + return require('internal/crypto/mac') .kmacGenerateKey(algorithm, extractable, keyUsages); - break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } +} - if (resultType === 'CryptoKey') { - const type = getCryptoKeyType(result); - if ((type === 'secret' || type === 'private') && - getCryptoKeyUsagesMask(result) === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } - } else if (getCryptoKeyUsagesMask(result.privateKey) === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } - - return result; +function deriveBits(algorithm, baseKey, length = null) { + return callSubtleCryptoMethod(deriveBitsImpl, this, arguments); } -async function deriveBits(algorithm, baseKey, length = null) { +function deriveBitsImpl(algorithm, baseKey, length = null) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -252,20 +244,20 @@ async function deriveBits(algorithm, baseKey, length = null) { case 'X448': // Fall through case 'ECDH': - return await require('internal/crypto/diffiehellman') + return require('internal/crypto/diffiehellman') .ecdhDeriveBits(algorithm, baseKey, length); case 'HKDF': - return await require('internal/crypto/hkdf') + return require('internal/crypto/hkdf') .hkdfDeriveBits(algorithm, baseKey, length); case 'PBKDF2': - return await require('internal/crypto/pbkdf2') + return require('internal/crypto/pbkdf2') .pbkdf2DeriveBits(algorithm, baseKey, length); case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': - return await require('internal/crypto/argon2') + return require('internal/crypto/argon2') .argon2DeriveBits(algorithm, baseKey, length); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); @@ -310,7 +302,16 @@ function getKeyLength({ name, length, hash }) { } } -async function deriveKey( +function deriveKey( + algorithm, + baseKey, + derivedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(deriveKeyImpl, this, arguments); +} + +function deriveKeyImpl( algorithm, baseKey, derivedKeyAlgorithm, @@ -360,15 +361,15 @@ async function deriveKey( case 'X448': // Fall through case 'ECDH': - bits = await require('internal/crypto/diffiehellman') + bits = require('internal/crypto/diffiehellman') .ecdhDeriveBits(algorithm, baseKey, length); break; case 'HKDF': - bits = await require('internal/crypto/hkdf') + bits = require('internal/crypto/hkdf') .hkdfDeriveBits(algorithm, baseKey, length); break; case 'PBKDF2': - bits = await require('internal/crypto/pbkdf2') + bits = require('internal/crypto/pbkdf2') .pbkdf2DeriveBits(algorithm, baseKey, length); break; case 'Argon2d': @@ -376,21 +377,21 @@ async function deriveKey( case 'Argon2i': // Fall through case 'Argon2id': - bits = await require('internal/crypto/argon2') + bits = require('internal/crypto/argon2') .argon2DeriveBits(algorithm, baseKey, length); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return FunctionPrototypeCall( + return PromisePrototypeThen(bits, (bits) => FunctionPrototypeCall( importKeySync, this, 'raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages, - ); + )); } -async function exportKeySpki(key) { +function exportKeySpki(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through @@ -432,7 +433,7 @@ async function exportKeySpki(key) { } } -async function exportKeyPkcs8(key) { +function exportKeyPkcs8(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through @@ -474,7 +475,7 @@ async function exportKeyPkcs8(key) { } } -async function exportKeyRawPublic(key, format) { +function exportKeyRawPublic(key, format) { switch (getCryptoKeyAlgorithm(key).name) { case 'ECDSA': // Fall through @@ -519,7 +520,7 @@ async function exportKeyRawPublic(key, format) { } } -async function exportKeyRawSeed(key) { +function exportKeyRawSeed(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'ML-DSA-44': // Fall through @@ -540,7 +541,7 @@ async function exportKeyRawSeed(key) { } } -async function exportKeyRawSecret(key, format) { +function exportKeyRawSecret(key, format) { switch (getCryptoKeyAlgorithm(key).name) { case 'AES-CTR': // Fall through @@ -568,7 +569,7 @@ async function exportKeyRawSecret(key, format) { } } -async function exportKeyJWK(key) { +function exportKeyJWK(key) { const algorithm = getCryptoKeyAlgorithm(key); const parameters = { key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), @@ -657,21 +658,7 @@ async function exportKeyJWK(key) { return getCryptoKeyHandle(key).exportJwk(parameters, true); } -async function exportKey(format, key) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - +function exportKeySync(format, key) { const algorithm = getCryptoKeyAlgorithm(key); try { normalizeAlgorithm(algorithm, 'exportKey'); @@ -688,43 +675,43 @@ async function exportKey(format, key) { switch (format) { case 'spki': { if (type === 'public') { - result = await exportKeySpki(key); + result = exportKeySpki(key); } break; } case 'pkcs8': { if (type === 'private') { - result = await exportKeyPkcs8(key); + result = exportKeyPkcs8(key); } break; } case 'jwk': { - result = await exportKeyJWK(key); + result = exportKeyJWK(key); break; } case 'raw-secret': { if (type === 'secret') { - result = await exportKeyRawSecret(key, format); + result = exportKeyRawSecret(key, format); } break; } case 'raw-public': { if (type === 'public') { - result = await exportKeyRawPublic(key, format); + result = exportKeyRawPublic(key, format); } break; } case 'raw-seed': { if (type === 'private') { - result = await exportKeyRawSeed(key); + result = exportKeyRawSeed(key); } break; } case 'raw': { if (type === 'secret') { - result = await exportKeyRawSecret(key, format); + result = exportKeyRawSecret(key, format); } else if (type === 'public') { - result = await exportKeyRawPublic(key, format); + result = exportKeyRawPublic(key, format); } break; } @@ -739,6 +726,28 @@ async function exportKey(format, key) { return result; } +function exportKey(format, key) { + return callSubtleCryptoMethod(exportKeyImpl, this, arguments); +} + +function exportKeyImpl(format, key) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); + + webidl ??= require('internal/crypto/webidl'); + const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + format = webidl.converters.KeyFormat(format, { + prefix, + context: '1st argument', + }); + key = webidl.converters.CryptoKey(key, { + prefix, + context: '2nd argument', + }); + + return exportKeySync(format, key); +} + function aliasKeyFormat(format) { switch (format) { case 'raw-public': @@ -862,7 +871,16 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { return result; } -async function importKey( +function importKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(importKeyImpl, this, arguments); +} + +function importKeyImpl( format, keyData, algorithm, @@ -906,7 +924,11 @@ async function importKey( // subtle.wrapKey() is essentially a subtle.exportKey() followed // by a subtle.encrypt(). -async function wrapKey(format, key, wrappingKey, algorithm) { +function wrapKey(format, key, wrappingKey, algorithm) { + return callSubtleCryptoMethod(wrapKeyImpl, this, arguments); +} + +function wrapKeyImpl(format, key, wrappingKey, algorithm) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -942,7 +964,7 @@ async function wrapKey(format, key, wrappingKey, algorithm) { throw lazyDOMException( 'Unable to use this key to wrapKey', 'InvalidAccessError'); - let keyData = await FunctionPrototypeCall(exportKey, this, format, key); + let keyData = exportKeySync(format, key); if (format === 'jwk') { const ec = new TextEncoder(); @@ -957,7 +979,7 @@ async function wrapKey(format, key, wrappingKey, algorithm) { } } - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, algorithm, wrappingKey, @@ -967,7 +989,18 @@ async function wrapKey(format, key, wrappingKey, algorithm) { // subtle.unwrapKey() is essentially a subtle.decrypt() followed // by a subtle.importKey(). -async function unwrapKey( +function unwrapKey( + format, + wrappedKey, + unwrappingKey, + unwrapAlgo, + unwrappedKeyAlgo, + extractable, + keyUsages) { + return callSubtleCryptoMethod(unwrapKeyImpl, this, arguments); +} + +function unwrapKeyImpl( format, wrappedKey, unwrappingKey, @@ -1027,33 +1060,35 @@ async function unwrapKey( throw lazyDOMException( 'Unable to use this key to unwrapKey', 'InvalidAccessError'); - let keyData = await cipherOrWrap( + const keyData = cipherOrWrap( kWebCryptoCipherDecrypt, unwrapAlgo, unwrappingKey, wrappedKey, 'unwrapKey'); - if (format === 'jwk') { - // The fatal: true option is only supported in builds that have ICU. - const options = process.versions.icu !== undefined ? - { fatal: true } : undefined; - const dec = new TextDecoder('utf-8', options); - try { - keyData = JSONParse(dec.decode(keyData)); - } catch { - throw lazyDOMException('Invalid wrapped JWK key', 'DataError'); + return PromisePrototypeThen(keyData, (keyData) => { + if (format === 'jwk') { + // The fatal: true option is only supported in builds that have ICU. + const options = process.versions.icu !== undefined ? + { fatal: true } : undefined; + const dec = new TextDecoder('utf-8', options); + try { + keyData = JSONParse(dec.decode(keyData)); + } catch { + throw lazyDOMException('Invalid wrapped JWK key', 'DataError'); + } } - } - return FunctionPrototypeCall( - importKeySync, - this, - format, keyData, unwrappedKeyAlgo, extractable, keyUsages, - ); + return FunctionPrototypeCall( + importKeySync, + this, + format, keyData, unwrappedKeyAlgo, extractable, keyUsages, + ); + }); } -async function signVerify(algorithm, key, data, signature) { +function signVerify(algorithm, key, data, signature) { const op = signature !== undefined ? 'verify' : 'sign'; // This is also usage algorithm = normalizeAlgorithm(algorithm, op); @@ -1068,37 +1103,41 @@ async function signVerify(algorithm, key, data, signature) { case 'RSA-PSS': // Fall through case 'RSASSA-PKCS1-v1_5': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaSignVerify(key, data, algorithm, signature); case 'ECDSA': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecdsaSignVerify(key, data, algorithm, signature); case 'Ed25519': // Fall through case 'Ed448': // Fall through - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .eddsaSignVerify(key, data, algorithm, signature); case 'HMAC': - return await require('internal/crypto/mac') + return require('internal/crypto/mac') .hmacSignVerify(key, data, algorithm, signature); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - return await require('internal/crypto/ml_dsa') + return require('internal/crypto/ml_dsa') .mlDsaSignVerify(key, data, algorithm, signature); case 'KMAC128': // Fall through case 'KMAC256': - return await require('internal/crypto/mac') + return require('internal/crypto/mac') .kmacSignVerify(key, data, algorithm, signature); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function sign(algorithm, key, data) { +function sign(algorithm, key, data) { + return callSubtleCryptoMethod(signImpl, this, arguments); +} + +function signImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1117,10 +1156,14 @@ async function sign(algorithm, key, data) { context: '3rd argument', }); - return await signVerify(algorithm, key, data); + return signVerify(algorithm, key, data); } -async function verify(algorithm, key, signature, data) { +function verify(algorithm, key, signature, data) { + return callSubtleCryptoMethod(verifyImpl, this, arguments); +} + +function verifyImpl(algorithm, key, signature, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1143,10 +1186,10 @@ async function verify(algorithm, key, signature, data) { context: '4th argument', }); - return await signVerify(algorithm, key, data, signature); + return signVerify(algorithm, key, data, signature); } -async function cipherOrWrap(mode, algorithm, key, data, op) { +function cipherOrWrap(mode, algorithm, key, data, op) { // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. @@ -1154,7 +1197,7 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { switch (algorithm.name) { case 'RSA-OAEP': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaCipher(mode, key, data, algorithm); case 'AES-CTR': // Fall through @@ -1163,21 +1206,25 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-GCM': // Fall through case 'AES-OCB': - return await require('internal/crypto/aes') + return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); case 'ChaCha20-Poly1305': - return await require('internal/crypto/chacha20_poly1305') + return require('internal/crypto/chacha20_poly1305') .c20pCipher(mode, key, data, algorithm); case 'AES-KW': if (op === 'wrapKey' || op === 'unwrapKey') { - return await require('internal/crypto/aes') + return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); } } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function encrypt(algorithm, key, data) { +function encrypt(algorithm, key, data) { + return callSubtleCryptoMethod(encryptImpl, this, arguments); +} + +function encryptImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1205,7 +1252,7 @@ async function encrypt(algorithm, key, data) { throw lazyDOMException( 'Unable to use this key to encrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, algorithm, key, @@ -1214,7 +1261,11 @@ async function encrypt(algorithm, key, data) { ); } -async function decrypt(algorithm, key, data) { +function decrypt(algorithm, key, data) { + return callSubtleCryptoMethod(decryptImpl, this, arguments); +} + +function decryptImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1242,7 +1293,7 @@ async function decrypt(algorithm, key, data) { throw lazyDOMException( 'Unable to use this key to decrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherDecrypt, algorithm, key, @@ -1252,7 +1303,11 @@ async function decrypt(algorithm, key, data) { } // Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-getPublicKey -async function getPublicKey(key, keyUsages) { +function getPublicKey(key, keyUsages) { + return callSubtleCryptoMethod(getPublicKeyImpl, this, arguments); +} + +function getPublicKeyImpl(key, keyUsages) { emitExperimentalWarning('The getPublicKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1273,14 +1328,17 @@ async function getPublicKey(key, keyUsages) { throw lazyDOMException('key must be a private key', type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); - // TODO(panva): this is by no means a hot path, but let's still follow up to get // rid of this awkwardness const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key))); return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, keyUsages); } -async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { +function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { + return callSubtleCryptoMethod(encapsulateBitsImpl, this, arguments); +} + +function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { emitExperimentalWarning('The encapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1296,7 +1354,8 @@ async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { context: '2nd argument', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1315,14 +1374,28 @@ async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - return await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages) { +function encapsulateKey( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + usages) { + return callSubtleCryptoMethod(encapsulateKeyImpl, this, arguments); +} + +function encapsulateKeyImpl( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + usages) { emitExperimentalWarning('The encapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1350,8 +1423,10 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe context: '5th argument', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); - const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedSharedKeyAlgorithm = + normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1371,28 +1446,33 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - encapsulateBits = await require('internal/crypto/ml_kem') + encapsulateBits = require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - const sharedKey = FunctionPrototypeCall( - importKeySync, - this, - 'raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, extractable, usages, - ); - - const encapsulatedKey = { - ciphertext: encapsulateBits.ciphertext, - sharedKey, - }; + return PromisePrototypeThen(encapsulateBits, (encapsulateBits) => { + const sharedKey = FunctionPrototypeCall( + importKeySync, + this, + 'raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, + extractable, usages, + ); + + return { + ciphertext: encapsulateBits.ciphertext, + sharedKey, + }; + }); +} - return encapsulatedKey; +function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { + return callSubtleCryptoMethod(decapsulateBitsImpl, this, arguments); } -async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { +function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertext) { emitExperimentalWarning('The decapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1412,7 +1492,8 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert context: '3rd argument', }); - const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const normalizedDecapsulationAlgorithm = + normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1431,16 +1512,26 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - return await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function decapsulateKey( +function decapsulateKey( decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages, ) { + return callSubtleCryptoMethod(decapsulateKeyImpl, this, arguments); +} + +function decapsulateKeyImpl( + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + usages) { emitExperimentalWarning('The decapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1472,8 +1563,10 @@ async function decapsulateKey( context: '6th argument', }); - const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); - const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const normalizedDecapsulationAlgorithm = + normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const normalizedSharedKeyAlgorithm = + normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1493,18 +1586,19 @@ async function decapsulateKey( case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - decapsulatedBits = await require('internal/crypto/ml_kem') + decapsulatedBits = require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return FunctionPrototypeCall( + return PromisePrototypeThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, usages, - ); + 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, + usages, + )); } // The SubtleCrypto and Crypto classes are defined as part of the diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index e04a7eab1bd8ef..32c6a93efbbc4e 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -265,6 +265,36 @@ const { KeyObject } = require('crypto'); })().then(common.mustCall()); } +if (hasOpenSSL(3)) { + (async () => { + const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; + const usages = ['sign']; + for (const [algorithm, baseKeyAlgorithm] of [ + [ + { name: 'HKDF', salt: new Uint8Array(), info: new Uint8Array(), hash: 'SHA-256' }, + { name: 'HKDF' }, + ], + [ + { name: 'PBKDF2', salt: new Uint8Array(), hash: 'SHA-256', iterations: 20 }, + { name: 'PBKDF2' }, + ], + ]) { + const baseKey = await subtle.importKey( + 'raw', + new Uint8Array(), + baseKeyAlgorithm, + false, + ['deriveKey']); + await assert.rejects( + subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), + { + name: 'DataError', + message: /KmacImportParams\.length cannot be 0/, + }); + } + })().then(common.mustCall()); +} + // Test X25519 and X448 key derivation { async function test(name) { diff --git a/test/parallel/test-webcrypto-methods-not-async.js b/test/parallel/test-webcrypto-methods-not-async.js new file mode 100644 index 00000000000000..c3507b0c103580 --- /dev/null +++ b/test/parallel/test-webcrypto-methods-not-async.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const AsyncFunction = async function() {}.constructor; + +const methods = [ + 'decrypt', + 'decapsulateBits', + 'decapsulateKey', + 'deriveBits', + 'deriveKey', + 'digest', + 'encapsulateBits', + 'encapsulateKey', + 'encrypt', + 'exportKey', + 'generateKey', + 'getPublicKey', + 'importKey', + 'sign', + 'unwrapKey', + 'verify', + 'wrapKey', +]; + +(async function() { + for (const name of methods) { + assert.notStrictEqual(subtle[name].constructor, AsyncFunction); + + const promise = subtle[name].call({}); + assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); + await assert.rejects(promise, { + code: 'ERR_INVALID_THIS', + }); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index 0f8f57ad6fb6a1..493d0093996b6d 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -519,6 +519,44 @@ function assertJsonWebKey(actual, expected) { } } +// Argon2Params +{ + const good = { + name: 'Argon2id', + memory: 8, + nonce: Buffer.alloc(8), + parallelism: 1, + passes: 1, + }; + + assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); + + assertIdlDictionary( + converters.Argon2Params({ + ...good, + associatedData: Buffer.alloc(0), + secretValue: Buffer.alloc(0), + }, opts), + { + ...good, + associatedData: Buffer.alloc(0), + secretValue: Buffer.alloc(0), + }); + + for (const required of ['memory', 'nonce', 'parallelism', 'passes']) { + assert.throws(() => converters.Argon2Params({ ...good, [required]: undefined }, opts), { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: `${prefix}: ${context} cannot be converted to 'Argon2Params' because '${required}' is required in 'Argon2Params'.`, + }); + } + + assert.throws(() => converters.Argon2Params({ ...good, passes: 0 }, opts), { + name: 'OperationError', + message: 'passes must be > 0', + }); +} + // AesCbcParams { const good = { name: 'AES-CBC', iv: Buffer.alloc(16) }; From 0cabd66f31d05634bbcc335876773b08003f61d7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sun, 26 Apr 2026 10:08:46 +0200 Subject: [PATCH 03/89] crypto: pass CryptoKey handles to KDF jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass CryptoKey handles directly into KDF jobs instead of exporting secret bytes in lib. Normalize HKDF, PBKDF2, and Argon2 around the same job construction pattern so WebCrypto derivation paths avoid extra key material copies and keep operation failures in native job handling. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63363 Reviewed-By: Antoine du Hamel Reviewed-By: René --- lib/internal/crypto/argon2.js | 18 +++-------------- lib/internal/crypto/hkdf.js | 20 +++++------------- lib/internal/crypto/pbkdf2.js | 16 ++------------- src/crypto/crypto_argon2.cc | 33 +++++++++++++++++++++++------- src/crypto/crypto_argon2.h | 2 ++ src/crypto/crypto_hkdf.cc | 37 +++++++++++++++++++++++++--------- src/crypto/crypto_hkdf.h | 1 + src/crypto/crypto_pbkdf2.cc | 38 ++++++++++++++++++++++++----------- src/crypto/crypto_pbkdf2.h | 4 +++- 9 files changed, 95 insertions(+), 74 deletions(-) diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index 08b16aa1411aa4..6d9f9e462d01fe 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -211,28 +211,16 @@ function argon2DeriveBits(algorithm, baseKey, length) { 'Argon2id': kTypeArgon2id, }[algorithm.name]; - let message; - try { - // TODO(panva): call the job directly without needing to re-export the handle - message = getCryptoKeyHandle(baseKey).export(); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - - const empty = new Uint8Array(0); - return jobPromise(() => new Argon2Job( kCryptoJobWebCrypto, - message, + getCryptoKeyHandle(baseKey), algorithm.nonce, algorithm.parallelism, length / 8, algorithm.memory, algorithm.passes, - algorithm.secretValue === undefined ? empty : algorithm.secretValue, - algorithm.associatedData === undefined ? empty : algorithm.associatedData, + algorithm.secretValue === undefined ? new Uint8Array() : algorithm.secretValue, + algorithm.associatedData === undefined ? new Uint8Array() : algorithm.associatedData, type)); } diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index a54432494817db..73b16da6923024 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -29,10 +29,9 @@ const { } = require('internal/crypto/util'); const { - createSecretKey, getCryptoKeyHandle, - getKeyObjectHandle, isKeyObject, + prepareSecretKey, } = require('internal/crypto/keys'); const { @@ -78,10 +77,10 @@ const validateParameters = hideStackFrames((hash, key, salt, info, length) => { function prepareKey(key) { if (isKeyObject(key)) - return getKeyObjectHandle(key); + return prepareSecretKey(key); if (isAnyArrayBuffer(key)) - return getKeyObjectHandle(createSecretKey(key)); + return key; key = toBuf(key); @@ -99,7 +98,7 @@ function prepareKey(key) { key); } - return getKeyObjectHandle(createSecretKey(key)); + return key; } function hkdf(hash, key, salt, info, length, callback) { @@ -157,18 +156,9 @@ function hkdfDeriveBits(algorithm, baseKey, length) { if (length === 0) return PromiseResolve(new ArrayBuffer(0)); - let normalizedHash; - try { - normalizedHash = normalizeHashName(hash.name); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - return jobPromise(() => new HKDFJob( kCryptoJobWebCrypto, - normalizedHash, + normalizeHashName(hash.name), getCryptoKeyHandle(baseKey), salt, info, diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 5cf4d8f46ecb6d..87f763da9ba8a3 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -114,25 +114,13 @@ function pbkdf2DeriveBits(algorithm, baseKey, length) { if (length === 0) return PromiseResolve(new ArrayBuffer(0)); - let password; - let normalizedHash; - try { - // TODO(panva): call the job directly without needing to re-export the handle - password = getCryptoKeyHandle(baseKey).export(); - normalizedHash = normalizeHashName(hash.name); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - return jobPromise(() => new PBKDF2Job( kCryptoJobWebCrypto, - password, + getCryptoKeyHandle(baseKey), salt, iterations, length / 8, - normalizedHash)); + normalizeHashName(hash.name))); } module.exports = { diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index dd71f038f0ce3e..42c5179f2b2340 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -1,5 +1,8 @@ #include "crypto/crypto_argon2.h" #include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" +#include "memory_tracker-inl.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_ARGON2 @@ -19,6 +22,7 @@ using v8::Value; Argon2Config::Argon2Config(Argon2Config&& other) noexcept : mode{other.mode}, + key{std::move(other.key)}, pass{std::move(other.pass)}, salt{std::move(other.salt)}, secret{std::move(other.secret)}, @@ -36,8 +40,9 @@ Argon2Config& Argon2Config::operator=(Argon2Config&& other) noexcept { } void Argon2Config::MemoryInfo(MemoryTracker* tracker) const { + if (key) tracker->TrackField("key", key); if (IsCryptoJobAsync(mode)) { - tracker->TrackFieldWithSize("pass", pass.size()); + if (!key) tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("secret", secret.size()); tracker->TrackFieldWithSize("ad", ad.size()); @@ -59,14 +64,23 @@ Maybe Argon2Traits::AdditionalConfig( config->mode = mode; - ArrayBufferOrViewContents pass(args[offset]); + CHECK(KeyObjectHandle::HasInstance(env, args[offset]) || + IsAnyBufferSource(args[offset])); // pass ArrayBufferOrViewContents salt(args[offset + 1]); ArrayBufferOrViewContents secret(args[offset + 6]); ArrayBufferOrViewContents ad(args[offset + 7]); - if (!pass.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); - return Nothing(); + if (KeyObjectHandle::HasInstance(env, args[offset])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset], Nothing()); + config->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents pass(args[offset]); + if (!pass.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + config->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); } if (!salt.CheckSizeInt32()) [[unlikely]] { @@ -85,7 +99,6 @@ Maybe Argon2Traits::AdditionalConfig( } const bool isAsync = IsCryptoJobAsync(mode); - config->pass = isAsync ? pass.ToCopy() : pass.ToByteSource(); config->salt = isAsync ? salt.ToCopy() : salt.ToByteSource(); config->secret = isAsync ? secret.ToCopy() : secret.ToByteSource(); config->ad = isAsync ? ad.ToCopy() : ad.ToByteSource(); @@ -119,7 +132,13 @@ bool Argon2Traits::DeriveBits(Environment* env, } // Both the pass and salt may be zero-length at this point - auto dp = ncrypto::argon2(config.pass, + const ncrypto::Buffer pass{ + .data = config.key ? config.key.GetSymmetricKey() + : config.pass.data(), + .len = config.key ? config.key.GetSymmetricKeySize() : config.pass.size(), + }; + + auto dp = ncrypto::argon2(pass, config.salt, config.lanes, config.keylen, diff --git a/src/crypto/crypto_argon2.h b/src/crypto/crypto_argon2.h index 354d0a4be6f392..058293805c073a 100644 --- a/src/crypto/crypto_argon2.h +++ b/src/crypto/crypto_argon2.h @@ -3,6 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include "crypto/crypto_keys.h" #include "crypto/crypto_util.h" namespace node::crypto { @@ -22,6 +23,7 @@ namespace node::crypto { struct Argon2Config final : public MemoryRetainer { CryptoJobMode mode; + KeyObjectData key; ByteSource pass; ByteSource salt; ByteSource secret; diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc index fe12df11003604..eb40ddad41c6e3 100644 --- a/src/crypto/crypto_hkdf.cc +++ b/src/crypto/crypto_hkdf.cc @@ -24,6 +24,7 @@ HKDFConfig::HKDFConfig(HKDFConfig&& other) noexcept length(other.length), digest(other.digest), key(std::move(other.key)), + key_data(std::move(other.key_data)), salt(std::move(other.salt)), info(std::move(other.info)) {} @@ -49,7 +50,8 @@ Maybe HKDFTraits::AdditionalConfig( params->mode = mode; CHECK(args[offset]->IsString()); // Hash - CHECK(args[offset + 1]->IsObject()); // Key + CHECK(KeyObjectHandle::HasInstance(env, args[offset + 1]) || + IsAnyBufferSource(args[offset + 1])); // Key CHECK(IsAnyBufferSource(args[offset + 2])); // Salt CHECK(IsAnyBufferSource(args[offset + 3])); // Info CHECK(args[offset + 4]->IsUint32()); // Length @@ -61,9 +63,19 @@ Maybe HKDFTraits::AdditionalConfig( return Nothing(); } - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); - params->key = key->Data().addRef(); + if (KeyObjectHandle::HasInstance(env, args[offset + 1])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); + params->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents key_data(args[offset + 1]); + if (!key_data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "key is too big"); + return Nothing(); + } + params->key_data = + IsCryptoJobAsync(mode) ? key_data.ToCopy() : key_data.ToByteSource(); + } ArrayBufferOrViewContents salt(args[offset + 2]); ArrayBufferOrViewContents info(args[offset + 3]); @@ -98,12 +110,16 @@ bool HKDFTraits::DeriveBits(Environment* env, ByteSource* out, CryptoJobMode mode, CryptoErrorStore* errors) { + const ncrypto::Buffer key_data{ + .data = params.key ? reinterpret_cast( + params.key.GetSymmetricKey()) + : params.key_data.data(), + .len = params.key ? params.key.GetSymmetricKeySize() + : params.key_data.size(), + }; + auto dp = ncrypto::hkdf(params.digest, - ncrypto::Buffer{ - .data = reinterpret_cast( - params.key.GetSymmetricKey()), - .len = params.key.GetSymmetricKeySize(), - }, + key_data, ncrypto::Buffer{ .data = params.info.data(), .len = params.info.size(), @@ -124,9 +140,10 @@ bool HKDFTraits::DeriveBits(Environment* env, } void HKDFConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("key", key); + if (key) tracker->TrackField("key", key); // If the job is sync, then the HKDFConfig does not own the data if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("key", key_data.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("info", info.size()); } diff --git a/src/crypto/crypto_hkdf.h b/src/crypto/crypto_hkdf.h index 9f624d73dc1936..bda6df6341219a 100644 --- a/src/crypto/crypto_hkdf.h +++ b/src/crypto/crypto_hkdf.h @@ -16,6 +16,7 @@ struct HKDFConfig final : public MemoryRetainer { size_t length; ncrypto::Digest digest; KeyObjectData key; + ByteSource key_data; ByteSource salt; ByteSource info; diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc index b4bd02a455786e..5c3fc438774334 100644 --- a/src/crypto/crypto_pbkdf2.cc +++ b/src/crypto/crypto_pbkdf2.cc @@ -1,5 +1,7 @@ #include "crypto/crypto_pbkdf2.h" #include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -21,6 +23,7 @@ using v8::Value; namespace crypto { PBKDF2Config::PBKDF2Config(PBKDF2Config&& other) noexcept : mode(other.mode), + key(std::move(other.key)), pass(std::move(other.pass)), salt(std::move(other.salt)), iterations(other.iterations), @@ -34,9 +37,10 @@ PBKDF2Config& PBKDF2Config::operator=(PBKDF2Config&& other) noexcept { } void PBKDF2Config::MemoryInfo(MemoryTracker* tracker) const { - // The job is sync, the PBKDF2Config does not own the data. + // If the job is sync, PBKDF2Config does not own the data. + if (key) tracker->TrackField("key", key); if (IsCryptoJobAsync(mode)) { - tracker->TrackFieldWithSize("pass", pass.size()); + if (!key) tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } } @@ -63,12 +67,21 @@ Maybe PBKDF2Traits::AdditionalConfig( params->mode = mode; - ArrayBufferOrViewContents pass(args[offset]); + CHECK(KeyObjectHandle::HasInstance(env, args[offset]) || + IsAnyBufferSource(args[offset])); // pass ArrayBufferOrViewContents salt(args[offset + 1]); - if (!pass.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); - return Nothing(); + if (KeyObjectHandle::HasInstance(env, args[offset])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset], Nothing()); + params->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents pass(args[offset]); + if (!pass.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); } if (!salt.CheckSizeInt32()) [[unlikely]] { @@ -76,8 +89,6 @@ Maybe PBKDF2Traits::AdditionalConfig( return Nothing(); } - params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); - params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsInt32()); // iteration_count @@ -112,11 +123,14 @@ bool PBKDF2Traits::DeriveBits(Environment* env, CryptoJobMode mode, CryptoErrorStore* errors) { // Both pass and salt may be zero length here. + const ncrypto::Buffer pass{ + .data = params.key ? params.key.GetSymmetricKey() + : params.pass.data(), + .len = params.key ? params.key.GetSymmetricKeySize() : params.pass.size(), + }; + auto dp = ncrypto::pbkdf2(params.digest, - ncrypto::Buffer{ - .data = params.pass.data(), - .len = params.pass.size(), - }, + pass, ncrypto::Buffer{ .data = params.salt.data(), .len = params.salt.size(), diff --git a/src/crypto/crypto_pbkdf2.h b/src/crypto/crypto_pbkdf2.h index 5ce3077e1aff8c..639fb4293ee1b4 100644 --- a/src/crypto/crypto_pbkdf2.h +++ b/src/crypto/crypto_pbkdf2.h @@ -3,8 +3,9 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#include "crypto/crypto_util.h" #include "async_wrap.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" #include "env.h" #include "memory_tracker.h" #include "v8.h" @@ -26,6 +27,7 @@ namespace crypto { struct PBKDF2Config final : public MemoryRetainer { CryptoJobMode mode; + KeyObjectData key; ByteSource pass; ByteSource salt; int32_t iterations; From dfe2d47fe1e12b7935983e60ccea946c5ba3f529 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 16 May 2026 18:14:04 +0200 Subject: [PATCH 04/89] crypto: harden WebCrypto against prototype pollution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid re-wrapping native WebCrypto promises with PromiseResolve(), since resolving a promise can read its user-mutated constructor. Add a helper for chaining internal WebCrypto job promises without consulting Promise species state, and use it for intermediate job results. Also align JWK wrapping and unwrapping with the spec's fresh-global JSON handling by detaching internal JWK values from user prototypes. Use the internal UTF-8 encoder/decoder bindings instead of shared TextEncoder/TextDecoder prototype methods. Expand the WebCrypto prototype pollution regression test to cover SubtleCrypto methods, export formats, zero-length KDF results, JWK toJSON/kty pollution, and encoder/decoder prototype poisoning. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63363 Reviewed-By: Antoine du Hamel Reviewed-By: René --- lib/internal/crypto/diffiehellman.js | 4 +- lib/internal/crypto/util.js | 70 ++ lib/internal/crypto/webcrypto.js | 157 +++- src/crypto/crypto_ec.cc | 34 +- src/crypto/crypto_kem.cc | 20 +- src/crypto/crypto_keygen.h | 16 +- src/crypto/crypto_keys.cc | 8 +- src/crypto/crypto_pqc.cc | 25 +- src/crypto/crypto_rsa.cc | 6 +- src/crypto/crypto_util.cc | 5 +- src/crypto/crypto_util.h | 39 +- .../test-webcrypto-crypto-job-mode.js | 104 ++- ...-webcrypto-promise-prototype-pollution.mjs | 677 ++++++++++++++++-- 13 files changed, 1010 insertions(+), 155 deletions(-) diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index ccdf4559154d64..d17b06ef155f76 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -5,7 +5,6 @@ const { FunctionPrototypeCall, MathCeil, ObjectDefineProperty, - PromisePrototypeThen, TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; @@ -59,6 +58,7 @@ const { const { getArrayBufferOrView, jobPromise, + jobPromiseThen, toBuf, kHandle, } = require('internal/crypto/util'); @@ -368,7 +368,7 @@ function ecdhDeriveBits(algorithm, baseKey, length) { if (length === null) return bits; - return PromisePrototypeThen(bits, (bits) => { + return jobPromiseThen(bits, (bits) => { // If the length is not a multiple of 8 the nearest ceiled // multiple of 8 is sliced. const sliceLength = MathCeil(length / 8); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 72f8ef135168b8..046efc4554ca36 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -14,7 +14,9 @@ const { ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, + PromisePrototypeThen, PromiseReject, + PromiseWithResolvers, SafeMap, SafeSet, StringPrototypeToUpperCase, @@ -77,6 +79,7 @@ const { emitExperimentalWarning, filterDuplicateStrings, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -90,6 +93,7 @@ const { isDataView, isArrayBufferView, isAnyArrayBuffer, + isPromise, } = require('internal/util/types'); const kHandle = Symbol('kHandle'); @@ -677,6 +681,69 @@ function jobPromise(getJob) { } } +// Temporarily shadow inherited then accessors on WebCrypto result objects. +// Promise resolution reads "then" synchronously for thenable assimilation. +// Returning an own undefined data property keeps that lookup from reaching +// user-mutated prototypes. +function prepareWebCryptoResult(value) { + if ((value === null || typeof value !== 'object') && + typeof value !== 'function') { + return false; + } + if (isPromise(value) || ObjectPrototypeHasOwnProperty(value, 'then')) + return false; + setOwnProperty(value, 'then', undefined); + return true; +} + +// Remove the temporary then property installed by prepareWebCryptoResult(). +function cleanupWebCryptoResult(value) { + delete value.then; +} + +// Resolve a WebCrypto promise while inherited then accessors are shadowed. +function resolveWebCryptoResult(resolve, value) { + const shouldCleanupResult = prepareWebCryptoResult(value); + try { + resolve(value); + } finally { + if (shouldCleanupResult) + cleanupWebCryptoResult(value); + } +} + +// Run a WebCrypto promise reaction and settle the outer promise. +function settleJobPromise(handler, resolve, reject, value, isRejected) { + try { + if (typeof handler === 'function') { + resolveWebCryptoResult(resolve, handler(value)); + } else if (isRejected) { + reject(value); + } else { + resolveWebCryptoResult(resolve, value); + } + } catch (err) { + reject(err); + } +} + +// Promise.prototype.then gets promise.constructor to determine the result +// promise's species. These promises are internal WebCrypto intermediates, so +// make that lookup stay on the promise itself instead of user-mutated state. +function jobPromiseThen(promise, onFulfilled, onRejected) { + const { + promise: resultPromise, + resolve, + reject, + } = PromiseWithResolvers(); + setOwnProperty(promise, 'constructor', undefined); + PromisePrototypeThen( + promise, + (value) => settleJobPromise(onFulfilled, resolve, reject, value, false), + (value) => settleJobPromise(onRejected, resolve, reject, value, true)); + return resultPromise; +} + // In WebCrypto, the publicExponent option in RSA is represented as a // WebIDL "BigInteger"... that is, a Uint8Array that allows an arbitrary // number of leading zero bits. Our conventional APIs for reading @@ -899,6 +966,9 @@ module.exports = { validateByteSource, validateKeyOps, jobPromise, + jobPromiseThen, + cleanupWebCryptoResult, + prepareWebCryptoResult, validateMaxBufferLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 7f8726ba11e147..996bcb1a729275 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1,19 +1,23 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeSlice, FunctionPrototypeCall, JSONParse, JSONStringify, ObjectDefineProperties, - PromisePrototypeThen, + ObjectKeys, + ObjectSetPrototypeOf, PromiseReject, PromiseResolve, ReflectApply, ReflectConstruct, + SafeArrayIterator, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeStartsWith, + SymbolIterator, SymbolToStringTag, TypedArrayPrototypeGetBuffer, } = primordials; @@ -26,7 +30,10 @@ const { kWebCryptoCipherDecrypt, } = internalBinding('crypto'); -const { TextDecoder, TextEncoder } = require('internal/encoding'); +const { + decodeUTF8, + encodeUtf8String, +} = internalBinding('encoding_binding'); const { codes: { @@ -54,9 +61,12 @@ const { } = require('internal/crypto/hash'); const { + cleanupWebCryptoResult, getBlockSize, + jobPromiseThen, normalizeAlgorithm, normalizeHashName, + prepareWebCryptoResult, validateMaxBufferLength, } = require('internal/crypto/util'); @@ -64,6 +74,7 @@ const { emitExperimentalWarning, kEnumerableProperty, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -71,13 +82,28 @@ const { randomUUID: _randomUUID, } = require('internal/crypto/random'); +const { + isPromise, +} = require('internal/util/types'); + let webidl; // WebCrypto methods return promises, including for synchronous validation // failures. Keep that conversion in one place so method bodies stay readable. function callSubtleCryptoMethod(fn, receiver, args) { try { - return PromiseResolve(ReflectApply(fn, receiver, args)); + const result = ReflectApply(fn, receiver, args); + if (isPromise(result)) + return result; + // PromiseResolve() performs thenable assimilation for object results. + // Shadow inherited then accessors while it resolves synchronous results. + const shouldCleanupResult = prepareWebCryptoResult(result); + try { + return PromiseResolve(result); + } finally { + if (shouldCleanupResult) + cleanupWebCryptoResult(result); + } } catch (err) { return PromiseReject(err); } @@ -384,7 +410,7 @@ function deriveKeyImpl( throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return PromisePrototypeThen(bits, (bits) => FunctionPrototypeCall( + return jobPromiseThen(bits, (bits) => FunctionPrototypeCall( importKeySync, this, 'raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages, @@ -571,30 +597,24 @@ function exportKeyRawSecret(key, format) { function exportKeyJWK(key) { const algorithm = getCryptoKeyAlgorithm(key); - const parameters = { - key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), - ext: getCryptoKeyExtractable(key), - }; + let alg; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkRsa); - if (alg) parameters.alg = alg; break; } case 'RSA-PSS': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkRsaPss); - if (alg) parameters.alg = alg; break; } case 'RSA-OAEP': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkRsaOaep); - if (alg) parameters.alg = alg; break; } case 'ECDSA': @@ -620,7 +640,7 @@ function exportKeyJWK(key) { case 'Ed25519': // Fall through case 'Ed448': - parameters.alg = algorithm.name; + alg = algorithm.name; break; case 'AES-CTR': // Fall through @@ -631,30 +651,39 @@ function exportKeyJWK(key) { case 'AES-OCB': // Fall through case 'AES-KW': - parameters.alg = require('internal/crypto/aes') + alg = require('internal/crypto/aes') .getAlgorithmName(algorithm.name, algorithm.length); break; case 'ChaCha20-Poly1305': - parameters.alg = 'C20P'; + alg = 'C20P'; break; case 'HMAC': { - const alg = normalizeHashName( + alg = normalizeHashName( algorithm.hash.name, normalizeHashName.kContextJwkHmac); - if (alg) parameters.alg = alg; break; } case 'KMAC128': - parameters.alg = 'K128'; + alg = 'K128'; break; case 'KMAC256': { - parameters.alg = 'K256'; + alg = 'K256'; break; } default: return undefined; } + // Keep `alg` in the object literal so an inherited setter cannot capture + // `parameters` before native export populates key material. Delete it for + // algorithms without a JWK alg value to keep the expected shape. + const parameters = { + key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), + ext: getCryptoKeyExtractable(key), + alg, + }; + if (alg === undefined) delete parameters.alg; + return getCryptoKeyHandle(key).exportJwk(parameters, true); } @@ -748,6 +777,60 @@ function exportKeyImpl(format, key) { return exportKeySync(format, key); } +// Parsed JWK arrays are detached from Array.prototype but still need to pass +// WebIDL sequence conversion, which reads @@iterator from the value. +function safeArrayIterator() { + return new SafeArrayIterator(this); +} + +// The WebCrypto spec parses and stringifies JWKs in a fresh global object. +// Detach internal JSON values from the current global's mutable prototypes to +// approximate those fresh-realm semantics without creating a new realm. +function detachFromUserPrototypes(value) { + if (value === null || typeof value !== 'object') + return; + + ObjectSetPrototypeOf(value, null); + + if (ArrayIsArray(value)) { + setOwnProperty(value, SymbolIterator, safeArrayIterator); + for (let n = 0; n < value.length; n++) + detachFromUserPrototypes(value[n]); + return; + } + + const keys = ObjectKeys(value); + for (let n = 0; n < keys.length; n++) + detachFromUserPrototypes(value[keys[n]]); +} + +// Parse wrapped JWK bytes according to WebCrypto's "parse a JWK" procedure. +function parseJwk(keyData) { + let key; + try { + // WebCrypto parses JWKs in a fresh global. Detach parsed JSON values + // from user-mutated prototypes before WebIDL dictionary conversion. + // Wrapped JWKs may be produced outside WebCrypto, so parse using the + // spec-required UTF-8. + const json = decodeUTF8(keyData, false, true); + const result = JSONParse(json); + detachFromUserPrototypes(result); + key = webidl.converters.JsonWebKey(result); + } catch (err) { + throw lazyDOMException( + 'Invalid wrapped JWK key', + { name: 'DataError', cause: err }); + } + + if (key.kty === undefined) { + throw lazyDOMException( + 'Invalid wrapped JWK key', + 'DataError'); + } + + return key; +} + function aliasKeyFormat(format) { switch (format) { case 'raw-public': @@ -967,15 +1050,21 @@ function wrapKeyImpl(format, key, wrappingKey, algorithm) { let keyData = exportKeySync(format, key); if (format === 'jwk') { - const ec = new TextEncoder(); - const raw = JSONStringify(keyData); + // The WebCrypto spec stringifies JWKs in a new global object. Rather + // than create a new realm here, detach this internally generated JWK from + // user-mutated prototypes so JSON.stringify cannot read inherited toJSON + // hooks from the current global. + detachFromUserPrototypes(keyData); + const json = JSONStringify(keyData); // As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey // we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes // in length - if (algorithm.name === 'AES-KW' && raw.length % 8 !== 0) { - keyData = ec.encode(raw + StringPrototypeRepeat(' ', 8 - (raw.length % 8))); + // The spec then UTF-8 encodes json. + if (algorithm.name === 'AES-KW' && json.length % 8 !== 0) { + keyData = encodeUtf8String( + json + StringPrototypeRepeat(' ', 8 - (json.length % 8))); } else { - keyData = ec.encode(raw); + keyData = encodeUtf8String(json); } } @@ -1067,17 +1156,9 @@ function unwrapKeyImpl( wrappedKey, 'unwrapKey'); - return PromisePrototypeThen(keyData, (keyData) => { + return jobPromiseThen(keyData, (keyData) => { if (format === 'jwk') { - // The fatal: true option is only supported in builds that have ICU. - const options = process.versions.icu !== undefined ? - { fatal: true } : undefined; - const dec = new TextDecoder('utf-8', options); - try { - keyData = JSONParse(dec.decode(keyData)); - } catch { - throw lazyDOMException('Invalid wrapped JWK key', 'DataError'); - } + keyData = parseJwk(keyData); } return FunctionPrototypeCall( @@ -1453,7 +1534,7 @@ function encapsulateKeyImpl( throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return PromisePrototypeThen(encapsulateBits, (encapsulateBits) => { + return jobPromiseThen(encapsulateBits, (encapsulateBits) => { const sharedKey = FunctionPrototypeCall( importKeySync, this, @@ -1593,7 +1674,7 @@ function decapsulateKeyImpl( throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return PromisePrototypeThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( + return jobPromiseThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( importKeySync, this, 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 9355b7f7a6ca64..a89e3391dbf896 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -488,10 +488,10 @@ bool ExportJWKEcKey(Environment* env, return false; } - if (target->Set( - env->context(), - env->jwk_kty_string(), - env->jwk_ec_string()).IsNothing()) { + if (!target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_ec_string()) + .FromMaybe(false)) { return false; } @@ -531,10 +531,9 @@ bool ExportJWKEcKey(Environment* env, return false; } } - if (target->Set( - env->context(), - env->jwk_crv_string(), - crv_name).IsNothing()) { + if (!target + ->DefineOwnProperty(env->context(), env->jwk_crv_string(), crv_name) + .FromMaybe(false)) { return false; } @@ -577,20 +576,23 @@ bool ExportJWKEdKey(Environment* env, const ncrypto::Buffer out = data; return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) .ToLocal(&encoded) && - target->Set(env->context(), key, encoded).IsJust(); + target->DefineOwnProperty(env->context(), key, encoded) + .FromMaybe(false); }; return !( - target - ->Set(env->context(), - env->jwk_crv_string(), - OneByteString(env->isolate(), curve)) - .IsNothing() || + !target + ->DefineOwnProperty(env->context(), + env->jwk_crv_string(), + OneByteString(env->isolate(), curve)) + .FromMaybe(false) || (key.GetKeyType() == kKeyTypePrivate && !trySetKey(env, pkey.rawPrivateKey(), target, env->jwk_d_string())) || !trySetKey(env, pkey.rawPublicKey(), target, env->jwk_x_string()) || - target->Set(env->context(), env->jwk_kty_string(), env->jwk_okp_string()) - .IsNothing()); + !target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_okp_string()) + .FromMaybe(false)); } KeyObjectData ImportJWKEdKey(Environment* env, Local jwk) { Local crv_value; diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index c16977f2d4636a..09fbf0844f48f2 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -176,16 +176,16 @@ MaybeLocal KEMEncapsulateTraits::EncodeOutput( if (params.job_mode == kCryptoJobWebCrypto) { Local result = Object::New(env->isolate()); - if (result - ->Set(env->context(), - OneByteString(env->isolate(), "sharedKey"), - shared_key_obj.As()->Buffer()) - .IsNothing() || - result - ->Set(env->context(), - OneByteString(env->isolate(), "ciphertext"), - ciphertext_obj.As()->Buffer()) - .IsNothing()) { + if (!result + ->DefineOwnProperty(env->context(), + OneByteString(env->isolate(), "sharedKey"), + shared_key_obj.As()->Buffer()) + .FromMaybe(false) || + !result + ->DefineOwnProperty(env->context(), + OneByteString(env->isolate(), "ciphertext"), + ciphertext_obj.As()->Buffer()) + .FromMaybe(false)) { return MaybeLocal(); } return result; diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index aa9bd745192c92..1702dfabb4af2a 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -184,14 +184,14 @@ class KeyGenJob final : public CryptoJob { } v8::Local ret = v8::Object::New(isolate); - if (ret->Set(env->context(), - OneByteString(isolate, "publicKey"), - public_key) - .IsNothing() || - ret->Set(env->context(), - OneByteString(isolate, "privateKey"), - private_key) - .IsNothing()) { + if (!ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "publicKey"), + public_key) + .FromMaybe(false) || + !ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "privateKey"), + private_key) + .FromMaybe(false)) { return {}; } return ret; diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index cfada2c766082e..c1b7aee576519e 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -156,9 +156,11 @@ bool ExportJWKSecretKey(Environment* env, BASE64URL) .ToLocal(&raw) && target - ->Set(env->context(), env->jwk_kty_string(), env->jwk_oct_string()) - .IsJust() && - target->Set(env->context(), env->jwk_k_string(), raw).IsJust(); + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_oct_string()) + .FromMaybe(false) && + target->DefineOwnProperty(env->context(), env->jwk_k_string(), raw) + .FromMaybe(false); } KeyObjectData ImportJWKSecretKey(Environment* env, Local jwk) { diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc index 8d4af1e7801180..e12894bc596317 100644 --- a/src/crypto/crypto_pqc.cc +++ b/src/crypto/crypto_pqc.cc @@ -145,7 +145,8 @@ bool TrySetEncodedKey(Environment* env, const ncrypto::Buffer out = data; return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) .ToLocal(&encoded) && - target->Set(env->context(), key, encoded).IsJust(); + target->DefineOwnProperty(env->context(), key, encoded) + .FromMaybe(false); } } // namespace @@ -172,16 +173,18 @@ bool ExportJwkPqcKey(Environment* env, } } - return !( - target->Set(env->context(), env->jwk_kty_string(), env->jwk_akp_string()) - .IsNothing() || - target - ->Set(env->context(), - env->jwk_alg_string(), - OneByteString(env->isolate(), alg->name)) - .IsNothing() || - !TrySetEncodedKey( - env, pkey.rawPublicKey(), target, env->jwk_pub_string())); + return !(!target + ->DefineOwnProperty(env->context(), + env->jwk_kty_string(), + env->jwk_akp_string()) + .FromMaybe(false) || + !target + ->DefineOwnProperty(env->context(), + env->jwk_alg_string(), + OneByteString(env->isolate(), alg->name)) + .FromMaybe(false) || + !TrySetEncodedKey( + env, pkey.rawPublicKey(), target, env->jwk_pub_string())); } KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index 310f9fef293138..acec4b993613cd 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -295,8 +295,10 @@ bool ExportJWKRsaKey(Environment* env, const ncrypto::Rsa rsa = m_pkey; if (!rsa || - target->Set(env->context(), env->jwk_kty_string(), env->jwk_rsa_string()) - .IsNothing()) { + !target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_rsa_string()) + .FromMaybe(false)) { return false; } diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index f6862f7954abf9..42b248d84b43e5 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -705,8 +705,9 @@ Maybe SetEncodedValue(Environment* env, if (!EncodeBignum(env, bn, size).ToLocal(&value)) { return Nothing(); } - return target->Set(env->context(), name, value).IsJust() ? JustVoid() - : Nothing(); + return target->DefineOwnProperty(env->context(), name, value).FromMaybe(false) + ? JustVoid() + : Nothing(); } CryptoJobMode GetCryptoJobMode(v8::Local args) { diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 66be7d00d2b5c8..742f23b0f5e789 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -439,13 +439,45 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { private: void ResolveWebCrypto(v8::Local value) { Environment* env = AsyncWrap::env(); + v8::Local context = env->context(); v8::Local resolver = v8::Local::New(env->isolate(), resolver_); + bool should_delete_then = false; + v8::Local then_key; v8::Local exception; { node::errors::TryCatchScope try_catch(env); - if (resolver->Resolve(env->context(), value).IsJust()) { + if (value->IsObject()) { + then_key = FIXED_ONE_BYTE_STRING(env->isolate(), "then"); + v8::Local object = value.As(); + v8::Maybe has_own_then = + object->HasOwnProperty(context, then_key); + if (has_own_then.IsNothing()) { + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } else if (!has_own_then.FromJust()) { + if (object + ->DefineOwnProperty(context, + then_key, + v8::Undefined(env->isolate()), + v8::DontEnum) + .FromMaybe(false)) { + should_delete_then = true; + } else if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } else { + exception = v8::Exception::Error(OneByteString( + env->isolate(), "Failed to prepare WebCrypto job result")); + } + } + } + + if (exception.IsEmpty() && resolver->Resolve(context, value).IsJust()) { + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } resolver_.Reset(); return; } @@ -454,8 +486,11 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { } } + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } if (!exception.IsEmpty()) { - USE(resolver->Reject(env->context(), exception)); + USE(resolver->Reject(context, exception)); } resolver_.Reset(); } diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js index c55894c056c8ca..7fd50de44f39b6 100644 --- a/test/parallel/test-webcrypto-crypto-job-mode.js +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -7,6 +7,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasOpenSSL } = require('../common/crypto'); const { types: { isCryptoKey } } = require('util'); const { internalBinding } = require('internal/test/binding'); const { @@ -31,6 +32,34 @@ const { const { subtle } = globalThis.crypto; +// Defines Object.prototype setters that fail the test if native result objects +// carrying key or shared secret material use [[Set]]. +async function withObjectPrototypeSetters(names, fn) { + const descriptors = new Map(); + for (const name of names) { + descriptors.set(name, Object.getOwnPropertyDescriptor(Object.prototype, name)); + Object.defineProperty(Object.prototype, name, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`Object.prototype.${name} getter`), + set: common.mustNotCall(`Object.prototype.${name} setter`), + }); + } + + try { + return await fn(); + } finally { + for (const name of names) { + const descriptor = descriptors.get(name); + if (descriptor === undefined) { + delete Object.prototype[name]; + } else { + Object.defineProperty(Object.prototype, name, descriptor); + } + } + } +} + (async function() { { const promise = new HashJob( @@ -49,6 +78,7 @@ const { subtle } = globalThis.crypto; const digest = await promise; assert(digest instanceof ArrayBuffer); assert.strictEqual(digest.byteLength, 32); + assert.strictEqual(Object.hasOwn(digest, 'then'), false); } { @@ -67,16 +97,19 @@ const { subtle } = globalThis.crypto; } { - const pair = await new EcKeyPairGenJob( - kCryptoJobWebCrypto, - 'P-256', - undefined, - { name: 'ECDSA', namedCurve: 'P-256' }, - getUsagesMask(new Set(['verify'])), - getUsagesMask(new Set(['sign'])), - true).run(); + const pair = await withObjectPrototypeSetters( + ['publicKey', 'privateKey'], + () => new EcKeyPairGenJob( + kCryptoJobWebCrypto, + 'P-256', + undefined, + { name: 'ECDSA', namedCurve: 'P-256' }, + getUsagesMask(new Set(['verify'])), + getUsagesMask(new Set(['sign'])), + true).run()); assert.strictEqual(Object.getPrototypeOf(pair), Object.prototype); + assert.strictEqual(Object.hasOwn(pair, 'then'), false); assert(isCryptoKey(pair.publicKey)); assert(isCryptoKey(pair.privateKey)); assert(pair.publicKey instanceof CryptoKey); @@ -120,6 +153,32 @@ const { subtle } = globalThis.crypto; }); } + { + const key = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + false, + ['encrypt', 'decrypt']); + const iv = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = new Uint8Array(await subtle.encrypt( + { name: 'AES-CBC', iv }, + key, + Buffer.alloc(16))); + ciphertext[ciphertext.length - 1] ^= 0xff; + + await assert.rejects( + subtle.decrypt({ name: 'AES-CBC', iv }, key, ciphertext), + (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual( + err.message, + 'The operation failed for an operation-specific reason'); + assert(err.cause); + assert.strictEqual(typeof err.cause.message, 'string'); + assert.notStrictEqual(err.cause.message, ''); + return true; + }); + } + { const key = await subtle.generateKey( { name: 'HMAC', hash: 'SHA-256' }, @@ -137,18 +196,33 @@ const { subtle } = globalThis.crypto; Object.defineProperty(CryptoKey.prototype, 'then', { __proto__: null, configurable: true, - get() { throw new Error('resolve then getter'); }, + get: common.mustNotCall('CryptoKey.prototype.then getter'), }); try { - await assert.rejects( - subtle.generateKey( - { name: 'AES-CBC', length: 128 }, - true, - ['encrypt']), - /resolve then getter/); + const key = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt']); + assert(isCryptoKey(key)); + assert.strictEqual(Object.hasOwn(key, 'then'), false); } finally { delete CryptoKey.prototype.then; } } + + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + const pair = await subtle.generateKey( + { name: 'ML-KEM-768' }, + true, + ['encapsulateBits', 'decapsulateBits']); + const result = await withObjectPrototypeSetters( + ['sharedKey', 'ciphertext'], + () => subtle.encapsulateBits({ name: 'ML-KEM-768' }, pair.publicKey)); + + assert.strictEqual(Object.getPrototypeOf(result), Object.prototype); + assert.strictEqual(Object.hasOwn(result, 'then'), false); + assert(result.sharedKey instanceof ArrayBuffer); + assert(result.ciphertext instanceof ArrayBuffer); + } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index d479abe3dcc989..17cc5c97716df0 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -1,9 +1,10 @@ import * as common from '../common/index.mjs'; +import assert from 'node:assert'; if (!common.hasCrypto) common.skip('missing crypto'); // WebCrypto subtle methods must not leak intermediate values -// through Promise.prototype.then pollution. +// through Promise.prototype.then or constructor pollution. // Regression test for https://github.com/nodejs/node/pull/61492 // and https://github.com/nodejs/node/issues/59699. @@ -13,51 +14,442 @@ const { subtle } = globalThis.crypto; Promise.prototype.then = common.mustNotCall('Promise.prototype.then'); -await subtle.digest('SHA-256', new Uint8Array([1, 2, 3])); +// WebCrypto methods return native promises. Re-wrapping a promise with +// PromiseResolve() or chaining it with Promise.prototype.then can read +// user-mutated constructor/species accessors. +async function assertNoPromiseConstructorAccess(name, fn) { + const constructorDescriptor = + Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor'); + const speciesDescriptor = + Object.getOwnPropertyDescriptor(Promise, Symbol.species); + let promise; + Object.defineProperty(Promise.prototype, 'constructor', { + __proto__: null, + configurable: true, + get: common.mustNotCall( + `${name} Promise.prototype.constructor getter`), + }); + Object.defineProperty(Promise, Symbol.species, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`${name} Promise[Symbol.species] getter`), + }); + try { + promise = fn(); + } finally { + Object.defineProperty( + Promise.prototype, + 'constructor', + constructorDescriptor); + Object.defineProperty(Promise, Symbol.species, speciesDescriptor); + } + return await promise; +} -await subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +// Exercise each export format through the same promise-constructor guard. +function assertExportKeyNoPromiseConstructorAccess(name, format, key) { + return assertNoPromiseConstructorAccess(`exportKey ${name}`, () => + subtle.exportKey(format, key)); +} -await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); +// Non-promise object results must be fulfilled without thenable assimilation +// observing inherited then accessors on the returned object. +async function assertNoInheritedThenAccess(name, prototype, prototypeName, fn) { + const descriptor = Object.getOwnPropertyDescriptor(prototype, 'then'); + Object.defineProperty(prototype, 'then', { + __proto__: null, + configurable: true, + get: common.mustNotCall(`${name} ${prototypeName}.prototype.then`), + }); + try { + return await fn(); + } finally { + if (descriptor === undefined) { + delete prototype.then; + } else { + Object.defineProperty(prototype, 'then', descriptor); + } + } +} -const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); +function assertNoInheritedArrayBufferThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + ArrayBuffer.prototype, + 'ArrayBuffer', + fn); +} + +function assertNoInheritedCryptoKeyThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + CryptoKey.prototype, + 'CryptoKey', + fn); +} + +function assertNoInheritedObjectThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + Object.prototype, + 'Object', + fn); +} + +// wrapKey('jwk') stringifies an internally exported JWK. The spec does this +// in a fresh global, so inherited toJSON hooks from the current global must +// not observe or replace key material. +async function assertNoInheritedToJSONAccess(name, fn) { + const objectDescriptor = + Object.getOwnPropertyDescriptor(Object.prototype, 'toJSON'); + const arrayDescriptor = + Object.getOwnPropertyDescriptor(Array.prototype, 'toJSON'); + Object.defineProperty(Object.prototype, 'toJSON', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Object.prototype.toJSON`), + }); + Object.defineProperty(Array.prototype, 'toJSON', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Array.prototype.toJSON`), + }); + try { + return await fn(); + } finally { + if (objectDescriptor === undefined) { + delete Object.prototype.toJSON; + } else { + Object.defineProperty(Object.prototype, 'toJSON', objectDescriptor); + } + if (arrayDescriptor === undefined) { + delete Array.prototype.toJSON; + } else { + Object.defineProperty(Array.prototype, 'toJSON', arrayDescriptor); + } + } +} + +// JWK export creates and fills a result object. The exported members must be +// own data properties, not writes that can observe inherited accessors. +async function assertNoInheritedJwkPropertyAccess(name, fn) { + const properties = [ + 'alg', + 'crv', + 'd', + 'dp', + 'dq', + 'e', + 'ext', + 'k', + 'key_ops', + 'kty', + 'n', + 'p', + 'priv', + 'pub', + 'q', + 'qi', + 'x', + 'y', + ]; + const descriptors = new Map(); + for (const property of properties) { + descriptors.set( + property, + Object.getOwnPropertyDescriptor(Object.prototype, property)); + Object.defineProperty(Object.prototype, property, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`${name} Object.prototype.${property} getter`), + set: common.mustNotCall(`${name} Object.prototype.${property} setter`), + }); + } + try { + return await fn(); + } finally { + for (const property of properties) { + const descriptor = descriptors.get(property); + if (descriptor === undefined) { + delete Object.prototype[property]; + } else { + Object.defineProperty(Object.prototype, property, descriptor); + } + } + } +} + +// unwrapKey('jwk') parses a JWK and then converts it to the JsonWebKey IDL +// dictionary. The parsed JWK must provide its own kty member; an inherited +// Object.prototype.kty must not satisfy that required WebCrypto step. +async function assertMissingJwkKtyIgnoresPrototype(fn) { + const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, 'kty'); + Object.defineProperty(Object.prototype, 'kty', { + __proto__: null, + configurable: true, + value: 'oct', + }); + try { + await assert.rejects(fn(), { name: 'DataError' }); + } finally { + if (descriptor === undefined) { + delete Object.prototype.kty; + } else { + Object.defineProperty(Object.prototype, 'kty', descriptor); + } + } +} + +// wrapKey('jwk') UTF-8 encodes the JSON string. That step must not rely on +// user-mutable encoding APIs such as TextEncoder or Buffer. +async function assertNoUserMutableEncodeAccess(name, fn) { + const textEncoderDescriptor = + Object.getOwnPropertyDescriptor(TextEncoder.prototype, 'encode'); + const bufferFromDescriptor = Object.getOwnPropertyDescriptor(Buffer, 'from'); + Object.defineProperty(TextEncoder.prototype, 'encode', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} TextEncoder.prototype.encode`), + }); + Object.defineProperty(Buffer, 'from', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Buffer.from`), + }); + try { + return await fn(); + } finally { + Object.defineProperty( + TextEncoder.prototype, + 'encode', + textEncoderDescriptor); + Object.defineProperty(Buffer, 'from', bufferFromDescriptor); + } +} + +// unwrapKey('jwk') decodes the wrapped bytes as UTF-8. That step must not +// rely on user-mutable encoding APIs such as TextDecoder or Buffer. +async function assertNoUserMutableDecodeAccess(name, fn) { + const textDecoderDescriptor = + Object.getOwnPropertyDescriptor(TextDecoder.prototype, 'decode'); + const bufferFromDescriptor = Object.getOwnPropertyDescriptor(Buffer, 'from'); + const bufferToStringDescriptor = + Object.getOwnPropertyDescriptor(Buffer.prototype, 'toString'); + Object.defineProperty(TextDecoder.prototype, 'decode', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} TextDecoder.prototype.decode`), + }); + Object.defineProperty(Buffer, 'from', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Buffer.from`), + }); + Object.defineProperty(Buffer.prototype, 'toString', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Buffer.prototype.toString`), + }); + try { + return await fn(); + } finally { + Object.defineProperty( + TextDecoder.prototype, + 'decode', + textDecoderDescriptor); + Object.defineProperty(Buffer, 'from', bufferFromDescriptor); + Object.defineProperty( + Buffer.prototype, + 'toString', + bufferToStringDescriptor); + } +} + +// encapsulateKey() first resolves an internal encapsulateBits job whose result +// object contains a raw sharedKey. The final method result is also an object, +// but its sharedKey is a CryptoKey and is intentionally returned to the caller. +async function assertNoRawSharedKeyObjectThenAccess(name, fn) { + const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, 'then'); + Object.defineProperty(Object.prototype, 'then', { + __proto__: null, + configurable: true, + get() { + if (Object.hasOwn(this, 'sharedKey') && + this.sharedKey instanceof ArrayBuffer) { + assert.fail(`${name} Object.prototype.then observed raw sharedKey`); + } + return undefined; + }, + }); + try { + return await fn(); + } finally { + if (descriptor === undefined) { + delete Object.prototype.then; + } else { + Object.defineProperty(Object.prototype, 'then', descriptor); + } + } +} + +await assertNoPromiseConstructorAccess('digest', () => + subtle.digest('SHA-256', new Uint8Array([1, 2, 3]))); -const importedKey = await subtle.importKey( - 'raw', rawKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']); +const secretKey = await assertNoPromiseConstructorAccess( + 'generateKey secret', + () => subtle.generateKey( + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); -const exportableKey = await subtle.importKey( - 'raw', rawKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +const extractableKeyPair = await assertNoPromiseConstructorAccess('generateKey pair', () => + subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'])); + +const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); -await subtle.exportKey('raw', exportableKey); +const importedKey = await assertNoPromiseConstructorAccess('importKey', () => + subtle.importKey( + 'raw', + rawKey, + { name: 'AES-CBC', length: 256 }, + false, + ['encrypt', 'decrypt'])); + +await assertNoInheritedCryptoKeyThenAccess('importKey', () => + subtle.importKey( + 'raw', + rawKey, + { name: 'AES-CBC', length: 256 }, + false, + ['encrypt', 'decrypt'])); + +await assertNoInheritedJwkPropertyAccess('exportKey jwk secret', () => + assertExportKeyNoPromiseConstructorAccess( + 'jwk secret', + 'jwk', + secretKey)); +await assertNoInheritedObjectThenAccess('exportKey jwk secret', () => + subtle.exportKey('jwk', secretKey)); +await assertNoInheritedJwkPropertyAccess('exportKey jwk public', () => + assertExportKeyNoPromiseConstructorAccess( + 'jwk public', + 'jwk', + extractableKeyPair.publicKey)); +await assertNoInheritedObjectThenAccess('exportKey jwk public', () => + subtle.exportKey('jwk', extractableKeyPair.publicKey)); +await assertNoInheritedJwkPropertyAccess('exportKey jwk private', () => + assertExportKeyNoPromiseConstructorAccess( + 'jwk private', + 'jwk', + extractableKeyPair.privateKey)); +await assertNoInheritedObjectThenAccess('exportKey jwk private', () => + subtle.exportKey('jwk', extractableKeyPair.privateKey)); +await assertNoInheritedArrayBufferThenAccess('exportKey raw secret', () => + subtle.exportKey('raw', secretKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'raw secret', + 'raw', + secretKey); +await assertNoInheritedArrayBufferThenAccess('exportKey spki', () => + subtle.exportKey('spki', extractableKeyPair.publicKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'spki', + 'spki', + extractableKeyPair.publicKey); +await assertNoInheritedArrayBufferThenAccess('exportKey pkcs8', () => + subtle.exportKey('pkcs8', extractableKeyPair.privateKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'pkcs8', + 'pkcs8', + extractableKeyPair.privateKey); +await assertNoInheritedArrayBufferThenAccess('exportKey raw-public', () => + subtle.exportKey('raw-public', extractableKeyPair.publicKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'raw-public', + 'raw-public', + extractableKeyPair.publicKey); const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); const plaintext = new TextEncoder().encode('Hello, world!'); -const ciphertext = await subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext); +const ciphertext = await assertNoPromiseConstructorAccess('encrypt', () => + subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext)); -await subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext); +await assertNoPromiseConstructorAccess('decrypt', () => + subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext)); const signingKey = await subtle.generateKey( - { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']); + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify']); const data = new TextEncoder().encode('test data'); -const signature = await subtle.sign('HMAC', signingKey, data); +const signature = await assertNoPromiseConstructorAccess('sign', () => + subtle.sign('HMAC', signingKey, data)); -await subtle.verify('HMAC', signingKey, signature, data); +await assertNoPromiseConstructorAccess('verify', () => + subtle.verify('HMAC', signingKey, signature, data)); const pbkdf2Key = await subtle.importKey( 'raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']); -await subtle.deriveBits( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, 256); - -await subtle.deriveKey( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt']); +await assertNoPromiseConstructorAccess('deriveBits', () => + subtle.deriveBits( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + 256)); + +await assertNoPromiseConstructorAccess('deriveBits PBKDF2 zero-length', () => + subtle.deriveBits( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + 0)); + +const hkdfKey = await subtle.importKey( + 'raw', rawKey, 'HKDF', false, ['deriveBits']); + +await assertNoPromiseConstructorAccess('deriveBits HKDF zero-length', () => + subtle.deriveBits( + { name: 'HKDF', hash: 'SHA-256', salt: rawKey, info: rawKey }, + hkdfKey, + 0)); + +const ecdhKeyPair = await subtle.generateKey( + { name: 'ECDH', namedCurve: 'P-256' }, + false, + ['deriveBits']); + +await assertNoPromiseConstructorAccess('deriveBits ECDH', () => + subtle.deriveBits( + { name: 'ECDH', public: ecdhKeyPair.publicKey }, + ecdhKeyPair.privateKey, + 256)); + +await assertNoPromiseConstructorAccess('deriveKey', () => + subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedArrayBufferThenAccess('deriveKey', () => + subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedCryptoKeyThenAccess('deriveKey result', () => + subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); const wrappingKey = await subtle.generateKey( { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); @@ -65,31 +457,224 @@ const wrappingKey = await subtle.generateKey( const keyToWrap = await subtle.generateKey( { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); -const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); - -await subtle.unwrapKey( - 'raw', wrapped, wrappingKey, 'AES-KW', - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); - -const { privateKey } = await subtle.generateKey( - { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); +const wrapped = await assertNoPromiseConstructorAccess('wrapKey', () => + subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW')); + +const wrappedJwk = await assertNoInheritedJwkPropertyAccess('wrapKey jwk', () => + assertNoInheritedToJSONAccess('wrapKey jwk', () => + assertNoUserMutableEncodeAccess('wrapKey jwk', () => + assertNoPromiseConstructorAccess('wrapKey jwk', () => + subtle.wrapKey('jwk', keyToWrap, wrappingKey, 'AES-KW'))))); + +await assertNoPromiseConstructorAccess('unwrapKey', () => + subtle.unwrapKey( + 'raw', + wrapped, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedArrayBufferThenAccess('unwrapKey', () => + subtle.unwrapKey( + 'raw', + wrapped, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedCryptoKeyThenAccess('unwrapKey result', () => + subtle.unwrapKey( + 'raw', + wrapped, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); + +await assertNoUserMutableDecodeAccess('unwrapKey jwk', () => + assertNoPromiseConstructorAccess('unwrapKey jwk', () => + subtle.unwrapKey( + 'jwk', + wrappedJwk, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt']))); +await assertNoInheritedArrayBufferThenAccess('unwrapKey jwk', () => + subtle.unwrapKey( + 'jwk', + wrappedJwk, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedCryptoKeyThenAccess('unwrapKey jwk result', () => + subtle.unwrapKey( + 'jwk', + wrappedJwk, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); + +{ + const jwkUnwrappingKey = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt', 'unwrapKey']); + + { + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const validWrappedJwk = await subtle.encrypt( + { name: 'AES-CBC', iv }, + jwkUnwrappingKey, + Buffer.from('{"kty":"oct","k":"AAAAAAAAAAAAAAAAAAAAAA"}')); + + await assertNoUserMutableDecodeAccess('unwrapKey jwk AES-CBC', () => + assertNoPromiseConstructorAccess('unwrapKey jwk AES-CBC', () => + subtle.unwrapKey( + 'jwk', + validWrappedJwk, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt']))); + await assertNoInheritedCryptoKeyThenAccess( + 'unwrapKey jwk AES-CBC result', + () => subtle.unwrapKey( + 'jwk', + validWrappedJwk, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt'])); + } + + { + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const wrappedRawKey = await subtle.encrypt( + { name: 'AES-CBC', iv }, + jwkUnwrappingKey, + rawKey); + + await assertNoPromiseConstructorAccess('unwrapKey raw AES-CBC', () => + subtle.unwrapKey( + 'raw', + wrappedRawKey, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt'])); + await assertNoInheritedArrayBufferThenAccess('unwrapKey raw AES-CBC', () => + subtle.unwrapKey( + 'raw', + wrappedRawKey, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt'])); + await assertNoInheritedCryptoKeyThenAccess( + 'unwrapKey raw AES-CBC result', + () => subtle.unwrapKey( + 'raw', + wrappedRawKey, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt'])); + } + + { + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const missingKtyWrappedJwk = await subtle.encrypt( + { name: 'AES-CBC', iv }, + jwkUnwrappingKey, + Buffer.from('{"k":"AAAAAAAAAAAAAAAAAAAAAA"}')); + + await assertMissingJwkKtyIgnoresPrototype(() => + subtle.unwrapKey( + 'jwk', + missingKtyWrappedJwk, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt'])); + } +} -await subtle.getPublicKey(privateKey, ['verify']); +await assertNoPromiseConstructorAccess('getPublicKey', () => + subtle.getPublicKey(extractableKeyPair.privateKey, ['verify'])); +await assertNoInheritedCryptoKeyThenAccess('getPublicKey', () => + subtle.getPublicKey(extractableKeyPair.privateKey, ['verify'])); if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kemPair = await subtle.generateKey( - { name: 'ML-KEM-768' }, false, + { name: 'ML-KEM-768' }, true, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); - const { ciphertext: ct1 } = await subtle.encapsulateKey( - { name: 'ML-KEM-768' }, kemPair.publicKey, 'HKDF', false, ['deriveBits']); - - await subtle.decapsulateKey( - { name: 'ML-KEM-768' }, kemPair.privateKey, ct1, 'HKDF', false, ['deriveBits']); - - const { ciphertext: ct2 } = await subtle.encapsulateBits( - { name: 'ML-KEM-768' }, kemPair.publicKey); - - await subtle.decapsulateBits( - { name: 'ML-KEM-768' }, kemPair.privateKey, ct2); + await assertNoInheritedArrayBufferThenAccess('exportKey raw-seed', () => + subtle.exportKey('raw-seed', kemPair.privateKey)); + await assertExportKeyNoPromiseConstructorAccess( + 'raw-seed', + 'raw-seed', + kemPair.privateKey); + + const { ciphertext: ct1 } = + await assertNoRawSharedKeyObjectThenAccess('encapsulateKey', () => + assertNoPromiseConstructorAccess('encapsulateKey', () => + subtle.encapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.publicKey, + 'HKDF', + false, + ['deriveBits']))); + + await assertNoPromiseConstructorAccess('decapsulateKey', () => + subtle.decapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct1, + 'HKDF', + false, + ['deriveBits'])); + await assertNoInheritedArrayBufferThenAccess('decapsulateKey', () => + subtle.decapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct1, + 'HKDF', + false, + ['deriveBits'])); + await assertNoInheritedCryptoKeyThenAccess('decapsulateKey result', () => + subtle.decapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct1, + 'HKDF', + false, + ['deriveBits'])); + + const { ciphertext: ct2 } = + await assertNoPromiseConstructorAccess('encapsulateBits', () => + subtle.encapsulateBits( + { name: 'ML-KEM-768' }, + kemPair.publicKey)); + + await assertNoPromiseConstructorAccess('decapsulateBits', () => + subtle.decapsulateBits( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct2)); } From c9dbb866838f7f175ee42aaa38f72af436859475 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sun, 24 May 2026 02:02:02 +0900 Subject: [PATCH 05/89] inspector: expose precise coverage start to JS runtime Add a `startCoverage` method on the `profiler` internal binding so that V8 precise coverage can be enabled after bootstrap. The method is idempotent against the existing bootstrap path (which creates a V8CoverageConnection when NODE_V8_COVERAGE or --experimental-test-coverage is set) and a no-op when the inspector is unavailable, e.g. in the parent process of `--test --test-isolation=process` where workers handle coverage and Environment::should_create_inspector() returns false. Refs: https://github.com/nodejs/node/issues/60023 Signed-off-by: sangwook PR-URL: https://github.com/nodejs/node/pull/63079 Reviewed-By: Chemi Atlow Reviewed-By: Pietro Marchini Reviewed-By: Aviv Keller --- lib/internal/test_runner/coverage.js | 2 + lib/internal/test_runner/runner.js | 2 + src/inspector_profiler.cc | 26 +++++++ .../coverage-isolation-none/runner.mjs | 18 +++++ .../coverage-isolation-none/src/foo.mjs | 11 +++ .../tests/foo.test.mjs | 11 +++ ...est-runner-coverage-isolation-none-api.mjs | 71 +++++++++++++++++++ test/parallel/test-runner-run-coverage.mjs | 10 ++- 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/test-runner/coverage-isolation-none/runner.mjs create mode 100644 test/fixtures/test-runner/coverage-isolation-none/src/foo.mjs create mode 100644 test/fixtures/test-runner/coverage-isolation-none/tests/foo.test.mjs create mode 100644 test/parallel/test-runner-coverage-isolation-none-api.mjs diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..577ce15f147d03 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -533,6 +533,8 @@ function setupCoverage(options) { return null; } + internalBinding('profiler').startCoverage(); + // Ensure that NODE_V8_COVERAGE is set so that coverage can propagate to // child processes. process.env.NODE_V8_COVERAGE = coverageDirectory; diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index d6cb6438d2b52a..92b963cd72bcf3 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -871,6 +871,8 @@ function run(options = kEmptyObject) { coverageExcludeGlobs = [coverageExcludeGlobs]; } validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs'); + } else if (coverage) { + coverageExcludeGlobs = [kDefaultPattern]; } if (coverageIncludeGlobs != null) { if (!ArrayIsArray(coverageIncludeGlobs)) { diff --git a/src/inspector_profiler.cc b/src/inspector_profiler.cc index 28653a3939daef..559b4fd27d56ab 100644 --- a/src/inspector_profiler.cc +++ b/src/inspector_profiler.cc @@ -548,6 +548,30 @@ static void SetSourceMapCacheGetter(const FunctionCallbackInfo& args) { env->set_source_map_cache_getter(args[0].As()); } +static void StartCoverage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Debug(env, + DebugCategory::INSPECTOR_PROFILER, + "StartCoverage, connection %s nullptr\n", + env->coverage_connection() == nullptr ? "==" : "!="); + + if (env->coverage_connection() != nullptr) { + return; + } + + // The parent of `--test --test-isolation=process` intentionally has no + // inspector (see Environment::should_create_inspector); workers handle + // coverage themselves. Without an inspector, V8CoverageConnection would + // get a null session and crash on the first DispatchMessage. + if (!env->should_create_inspector()) { + return; + } + + env->set_coverage_connection(std::make_unique(env)); + env->coverage_connection()->Start(); +} + static void TakeCoverage(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); V8CoverageConnection* connection = env->coverage_connection(); @@ -601,6 +625,7 @@ static void Initialize(Local target, SetMethod(context, target, "setCoverageDirectory", SetCoverageDirectory); SetMethod( context, target, "setSourceMapCacheGetter", SetSourceMapCacheGetter); + SetMethod(context, target, "startCoverage", StartCoverage); SetMethod(context, target, "takeCoverage", TakeCoverage); SetMethod(context, target, "stopCoverage", StopCoverage); SetMethod(context, target, "endCoverage", EndCoverage); @@ -609,6 +634,7 @@ static void Initialize(Local target, void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(SetCoverageDirectory); registry->Register(SetSourceMapCacheGetter); + registry->Register(StartCoverage); registry->Register(TakeCoverage); registry->Register(StopCoverage); registry->Register(EndCoverage); diff --git a/test/fixtures/test-runner/coverage-isolation-none/runner.mjs b/test/fixtures/test-runner/coverage-isolation-none/runner.mjs new file mode 100644 index 00000000000000..dffe2f5cd27713 --- /dev/null +++ b/test/fixtures/test-runner/coverage-isolation-none/runner.mjs @@ -0,0 +1,18 @@ +import { run } from 'node:test'; +import { join } from 'node:path'; + +const stream = run({ + files: [join(import.meta.dirname, 'tests', 'foo.test.mjs')], + coverage: true, + isolation: 'none', + cwd: import.meta.dirname, +}); +stream.on('test:fail', () => process.exit(10)); +let summary; +stream.on('test:coverage', (event) => { summary = event.summary; }); +for await (const _ of stream); +if (!summary || summary.files.length === 0) process.exit(11); +const hasSrc = summary.files.some((f) => f.path.endsWith('foo.mjs') && !f.path.endsWith('foo.test.mjs')); +const hasTest = summary.files.some((f) => f.path.endsWith('foo.test.mjs')); +if (!hasSrc) process.exit(12); +if (hasTest) process.exit(13); \ No newline at end of file diff --git a/test/fixtures/test-runner/coverage-isolation-none/src/foo.mjs b/test/fixtures/test-runner/coverage-isolation-none/src/foo.mjs new file mode 100644 index 00000000000000..f6a50e85a9d412 --- /dev/null +++ b/test/fixtures/test-runner/coverage-isolation-none/src/foo.mjs @@ -0,0 +1,11 @@ +export function add(a, b) { + return a + b; +} + +export function sub(a, b) { + return a - b; +} + +export function unused() { + return 'unused'; +} diff --git a/test/fixtures/test-runner/coverage-isolation-none/tests/foo.test.mjs b/test/fixtures/test-runner/coverage-isolation-none/tests/foo.test.mjs new file mode 100644 index 00000000000000..efbccddc87412b --- /dev/null +++ b/test/fixtures/test-runner/coverage-isolation-none/tests/foo.test.mjs @@ -0,0 +1,11 @@ +import test from 'node:test'; +import assert from 'node:assert'; +import { add, sub } from '../src/foo.mjs'; + +test('add', () => { + assert.strictEqual(add(2, 3), 5); +}); + +test('sub', () => { + assert.strictEqual(sub(5, 3), 2); +}); diff --git a/test/parallel/test-runner-coverage-isolation-none-api.mjs b/test/parallel/test-runner-coverage-isolation-none-api.mjs new file mode 100644 index 00000000000000..950af9f3111c3a --- /dev/null +++ b/test/parallel/test-runner-coverage-isolation-none-api.mjs @@ -0,0 +1,71 @@ +import * as common from '../common/index.mjs'; +import { before, describe, it, run } from 'node:test'; +import assert from 'node:assert'; +import { spawnSync } from 'node:child_process'; +import { cp } from 'node:fs/promises'; +import { join, sep } from 'node:path'; +import tmpdir from '../common/tmpdir.js'; +import fixtures from '../common/fixtures.js'; + +const skipIfNoInspector = { + skip: !process.features.inspector ? 'inspector disabled' : false, +}; + +tmpdir.refresh(); + +async function setupFixtures() { + const fixtureDir = fixtures.path('test-runner', 'coverage-isolation-none'); + await cp(fixtureDir, tmpdir.path, { recursive: true }); +} + +describe('run() coverage with isolation: none', skipIfNoInspector, () => { + before(async () => { + await setupFixtures(); + }); + + for (const isolation of ['none', 'process']) { + it(`reports src coverage and excludes test files by default (isolation=${isolation})`, async () => { + const stream = run({ + files: [join(tmpdir.path, 'tests', 'foo.test.mjs')], + coverage: true, + isolation, + cwd: tmpdir.path, + }); + stream.on('test:fail', common.mustNotCall()); + + let summary; + stream.on('test:coverage', common.mustCall(({ summary: s }) => { + summary = s; + })); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + + assert.ok(summary, 'test:coverage event must fire'); + const paths = summary.files.map((f) => f.path); + assert.ok( + paths.length > 0, + `coverage files must be reported (isolation=${isolation}); got ${JSON.stringify(paths)}`, + ); + assert.ok( + paths.some((p) => p.endsWith(`src${sep}foo.mjs`)), + `expected src/foo.mjs to be present (isolation=${isolation}); got ${JSON.stringify(paths)}`, + ); + assert.ok( + paths.every((p) => !p.endsWith('foo.test.mjs')), + `expected foo.test.mjs to be excluded by default (isolation=${isolation}); got ${JSON.stringify(paths)}`, + ); + }); + } + + it('is idempotent when --experimental-test-coverage is also passed', async () => { + const result = spawnSync(process.execPath, [ + '--experimental-test-coverage', + join(tmpdir.path, 'runner.mjs'), + ], { cwd: tmpdir.path }); + assert.strictEqual( + result.status, + 0, + `exited with ${result.status}\nstderr: ${result.stderr}\nstdout: ${result.stdout}`, + ); + }); +}); diff --git a/test/parallel/test-runner-run-coverage.mjs b/test/parallel/test-runner-run-coverage.mjs index 15fcfef5238843..89a9da2a179e44 100644 --- a/test/parallel/test-runner-run-coverage.mjs +++ b/test/parallel/test-runner-run-coverage.mjs @@ -123,6 +123,7 @@ describe('require(\'node:test\').run coverage settings', { concurrency: true }, const stream = run({ files, coverage: true, + coverageExcludeGlobs: '!test/**', coverageIncludeGlobs: ['test/fixtures/test-runner/coverage.js', 'test/*/v8-coverage/throw.js'], }); stream.on('test:fail', common.mustNotCall()); @@ -157,7 +158,14 @@ describe('require(\'node:test\').run coverage settings', { concurrency: true }, const thresholdErrors = []; const originalExitCode = process.exitCode; assert.notStrictEqual(originalExitCode, 1); - const stream = run({ files, coverage: true, lineCoverage: 99, branchCoverage: 99, functionCoverage: 99 }); + const stream = run({ + files, + coverage: true, + coverageExcludeGlobs: '!test/**', + lineCoverage: 99, + branchCoverage: 99, + functionCoverage: 99, + }); stream.on('test:fail', common.mustNotCall()); stream.on('test:pass', common.mustCall(1)); stream.on('test:diagnostic', ({ message }) => { From c9562ddb82dde83eb6e9c62aa8055b345a46b575 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 23 May 2026 20:43:31 +0200 Subject: [PATCH 06/89] vfs: add minimal node:vfs subsystem Adds the node:vfs builtin module with VirtualFileSystem and provider classes. No integration with fs, modules, or SEA. Assisted-by: Claude-Opus4.7 Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/63115 Reviewed-By: James M Snell Reviewed-By: Paolo Insogna Reviewed-By: Robert Nagy Reviewed-By: Stephen Belanger --- doc/api/cli.md | 12 + doc/api/index.md | 1 + doc/api/vfs.md | 310 +++++ doc/node.1 | 7 + lib/internal/bootstrap/realm.js | 11 +- lib/internal/modules/cjs/loader.js | 3 + lib/internal/process/pre_execution.js | 10 + lib/internal/vfs/dir.js | 104 ++ lib/internal/vfs/errors.js | 193 +++ lib/internal/vfs/fd.js | 87 ++ lib/internal/vfs/file_handle.js | 720 +++++++++++ lib/internal/vfs/file_system.js | 1147 +++++++++++++++++ lib/internal/vfs/provider.js | 618 +++++++++ lib/internal/vfs/providers/memory.js | 1023 +++++++++++++++ lib/internal/vfs/providers/real.js | 477 +++++++ lib/internal/vfs/stats.js | 300 +++++ lib/internal/vfs/streams.js | 353 +++++ lib/internal/vfs/watcher.js | 688 ++++++++++ lib/vfs.js | 37 + src/node_builtins.cc | 1 + src/node_options.cc | 4 + src/node_options.h | 1 + test/parallel/test-process-get-builtin.mjs | 2 + test/parallel/test-require-resolve.js | 2 + test/parallel/test-vfs-access-modes.js | 41 + test/parallel/test-vfs-append-write.js | 19 + test/parallel/test-vfs-bigint-position.js | 18 + test/parallel/test-vfs-callback-api.js | 154 +++ test/parallel/test-vfs-copyfile-mode.js | 52 + test/parallel/test-vfs-create.js | 65 + test/parallel/test-vfs-ctime-update.js | 49 + test/parallel/test-vfs-dir-handle.js | 114 ++ test/parallel/test-vfs-fd.js | 319 +++++ test/parallel/test-vfs-file-handle.js | 205 +++ test/parallel/test-vfs-flag.js | 59 + test/parallel/test-vfs-hardlink-nlink.js | 32 + test/parallel/test-vfs-link.js | 24 + test/parallel/test-vfs-memory-file-handle.js | 15 + .../test-vfs-memory-provider-dynamic.js | 127 ++ .../test-vfs-memory-provider-flags.js | 42 + test/parallel/test-vfs-memory-provider.js | 664 ++++++++++ test/parallel/test-vfs-mkdir.js | 49 + test/parallel/test-vfs-mkdtemp.js | 38 + test/parallel/test-vfs-parent-timestamps.js | 25 + test/parallel/test-vfs-promises-open.js | 17 + test/parallel/test-vfs-promises.js | 483 +++++++ .../test-vfs-readdir-symlink-recursive.js | 54 + test/parallel/test-vfs-readfile-async.js | 24 + test/parallel/test-vfs-readfile-encoding.js | 21 + test/parallel/test-vfs-readfile-flag.js | 39 + .../parallel/test-vfs-real-provider-handle.js | 104 ++ .../test-vfs-real-provider-promises.js | 55 + .../test-vfs-real-provider-symlinks.js | 111 ++ test/parallel/test-vfs-real-provider-watch.js | 40 + test/parallel/test-vfs-real-provider.js | 148 +++ test/parallel/test-vfs-rename.js | 46 + test/parallel/test-vfs-rm-edge-cases.js | 70 + test/parallel/test-vfs-rmdir-symlink.js | 31 + test/parallel/test-vfs-stats-bigint.js | 36 + test/parallel/test-vfs-stats-helpers.js | 80 ++ test/parallel/test-vfs-stats-ino-dev.js | 24 + test/parallel/test-vfs-stream-errors.js | 65 + test/parallel/test-vfs-stream-explicit-fd.js | 57 + test/parallel/test-vfs-stream-properties.js | 40 + test/parallel/test-vfs-stream-validation.js | 29 + test/parallel/test-vfs-streams.js | 302 +++++ test/parallel/test-vfs-symlinks.js | 56 + test/parallel/test-vfs-truncate-negative.js | 15 + test/parallel/test-vfs-utimes.js | 27 + test/parallel/test-vfs-virtual-file-handle.js | 88 ++ test/parallel/test-vfs-virtual-provider.js | 109 ++ test/parallel/test-vfs-watch-abort-signal.js | 53 + test/parallel/test-vfs-watch-directory.js | 62 + test/parallel/test-vfs-watch-encoding.js | 21 + test/parallel/test-vfs-watch-promises.js | 84 ++ test/parallel/test-vfs-watch-recursive.js | 36 + test/parallel/test-vfs-watch.js | 75 ++ test/parallel/test-vfs-watchfile.js | 103 ++ test/parallel/test-vfs-write-options.js | 33 + 79 files changed, 10859 insertions(+), 1 deletion(-) create mode 100644 doc/api/vfs.md create mode 100644 lib/internal/vfs/dir.js create mode 100644 lib/internal/vfs/errors.js create mode 100644 lib/internal/vfs/fd.js create mode 100644 lib/internal/vfs/file_handle.js create mode 100644 lib/internal/vfs/file_system.js create mode 100644 lib/internal/vfs/provider.js create mode 100644 lib/internal/vfs/providers/memory.js create mode 100644 lib/internal/vfs/providers/real.js create mode 100644 lib/internal/vfs/stats.js create mode 100644 lib/internal/vfs/streams.js create mode 100644 lib/internal/vfs/watcher.js create mode 100644 lib/vfs.js create mode 100644 test/parallel/test-vfs-access-modes.js create mode 100644 test/parallel/test-vfs-append-write.js create mode 100644 test/parallel/test-vfs-bigint-position.js create mode 100644 test/parallel/test-vfs-callback-api.js create mode 100644 test/parallel/test-vfs-copyfile-mode.js create mode 100644 test/parallel/test-vfs-create.js create mode 100644 test/parallel/test-vfs-ctime-update.js create mode 100644 test/parallel/test-vfs-dir-handle.js create mode 100644 test/parallel/test-vfs-fd.js create mode 100644 test/parallel/test-vfs-file-handle.js create mode 100644 test/parallel/test-vfs-flag.js create mode 100644 test/parallel/test-vfs-hardlink-nlink.js create mode 100644 test/parallel/test-vfs-link.js create mode 100644 test/parallel/test-vfs-memory-file-handle.js create mode 100644 test/parallel/test-vfs-memory-provider-dynamic.js create mode 100644 test/parallel/test-vfs-memory-provider-flags.js create mode 100644 test/parallel/test-vfs-memory-provider.js create mode 100644 test/parallel/test-vfs-mkdir.js create mode 100644 test/parallel/test-vfs-mkdtemp.js create mode 100644 test/parallel/test-vfs-parent-timestamps.js create mode 100644 test/parallel/test-vfs-promises-open.js create mode 100644 test/parallel/test-vfs-promises.js create mode 100644 test/parallel/test-vfs-readdir-symlink-recursive.js create mode 100644 test/parallel/test-vfs-readfile-async.js create mode 100644 test/parallel/test-vfs-readfile-encoding.js create mode 100644 test/parallel/test-vfs-readfile-flag.js create mode 100644 test/parallel/test-vfs-real-provider-handle.js create mode 100644 test/parallel/test-vfs-real-provider-promises.js create mode 100644 test/parallel/test-vfs-real-provider-symlinks.js create mode 100644 test/parallel/test-vfs-real-provider-watch.js create mode 100644 test/parallel/test-vfs-real-provider.js create mode 100644 test/parallel/test-vfs-rename.js create mode 100644 test/parallel/test-vfs-rm-edge-cases.js create mode 100644 test/parallel/test-vfs-rmdir-symlink.js create mode 100644 test/parallel/test-vfs-stats-bigint.js create mode 100644 test/parallel/test-vfs-stats-helpers.js create mode 100644 test/parallel/test-vfs-stats-ino-dev.js create mode 100644 test/parallel/test-vfs-stream-errors.js create mode 100644 test/parallel/test-vfs-stream-explicit-fd.js create mode 100644 test/parallel/test-vfs-stream-properties.js create mode 100644 test/parallel/test-vfs-stream-validation.js create mode 100644 test/parallel/test-vfs-streams.js create mode 100644 test/parallel/test-vfs-symlinks.js create mode 100644 test/parallel/test-vfs-truncate-negative.js create mode 100644 test/parallel/test-vfs-utimes.js create mode 100644 test/parallel/test-vfs-virtual-file-handle.js create mode 100644 test/parallel/test-vfs-virtual-provider.js create mode 100644 test/parallel/test-vfs-watch-abort-signal.js create mode 100644 test/parallel/test-vfs-watch-directory.js create mode 100644 test/parallel/test-vfs-watch-encoding.js create mode 100644 test/parallel/test-vfs-watch-promises.js create mode 100644 test/parallel/test-vfs-watch-recursive.js create mode 100644 test/parallel/test-vfs-watch.js create mode 100644 test/parallel/test-vfs-watchfile.js create mode 100644 test/parallel/test-vfs-write-options.js diff --git a/doc/api/cli.md b/doc/api/cli.md index bc1f69483f3f7f..85ca8c8d8376a2 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1444,6 +1444,16 @@ The flag may be specified more than once; tests must contain **every** filter value to run. See [Test tags][] for details on declaring and inheriting tags. +### `--experimental-vfs` + + + +> Stability: 1 - Experimental + +Enable the experimental [`node:vfs`][] module. + ### `--experimental-vm-modules` + + + +> Stability: 1 - Experimental + + + +The `node:vfs` module provides an in-memory virtual file system with a +`node:fs`-like API. It is useful for tests, fixtures, embedded assets, and other +scenarios where you need a self-contained file system without touching the +actual file-system. + +To access it: + +```mjs +import vfs from 'node:vfs'; +``` + +```cjs +const vfs = require('node:vfs'); +``` + +This module is only available under the `node:` scheme, and only when Node.js +is started with the `--experimental-vfs` flag. + +## Basic usage + +```cjs +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.mkdirSync('/dir', { recursive: true }); +myVfs.writeFileSync('/dir/hello.txt', 'Hello, VFS!'); + +console.log(myVfs.readFileSync('/dir/hello.txt', 'utf8')); // 'Hello, VFS!' +``` + +`vfs.create()` returns a [`VirtualFileSystem`][] instance backed by a +[`MemoryProvider`][] by default. The instance exposes synchronous, +callback-based, and promise-based file system methods that mirror the +shape of the [`node:fs`][] API. All paths are POSIX-style and absolute +(starting with `/`). + +## `vfs.create([provider][, options])` + + + +* `provider` {VirtualProvider} The provider to use. **Default:** + `new MemoryProvider()`. +* `options` {Object} + * `emitExperimentalWarning` {boolean} Whether to emit the experimental + warning when the instance is created. **Default:** `true`. +* Returns: {VirtualFileSystem} + +Convenience factory equivalent to `new VirtualFileSystem(provider, options)`. + +```cjs +const vfs = require('node:vfs'); + +// Default in-memory provider +const memoryVfs = vfs.create(); + +// Explicit provider +const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox')); +``` + +## Class: `VirtualFileSystem` + + + +A `VirtualFileSystem` wraps a [`VirtualProvider`][] and exposes a +`node:fs`-like API. Each instance maintains its own file tree. + +### `new VirtualFileSystem([provider][, options])` + + + +* `provider` {VirtualProvider} The provider to use. **Default:** + `new MemoryProvider()`. +* `options` {Object} + * `emitExperimentalWarning` {boolean} Whether to emit the experimental + warning. **Default:** `true`. + +### `vfs.provider` + + + +* {VirtualProvider} + +The provider backing this VFS instance. + +### `vfs.readonly` + + + +* {boolean} + +`true` when the underlying provider is read-only. + +### APIs + +`VirtualFileSystem` implements the following methods, with the same +signatures as their [`node:fs`][] counterparts: + +#### Synchronous API + +* `existsSync(path)` +* `statSync(path[, options])` +* `lstatSync(path[, options])` +* `readFileSync(path[, options])` +* `writeFileSync(path, data[, options])` +* `appendFileSync(path, data[, options])` +* `readdirSync(path[, options])` +* `mkdirSync(path[, options])` +* `rmdirSync(path)` +* `unlinkSync(path)` +* `renameSync(oldPath, newPath)` +* `copyFileSync(src, dest[, mode])` +* `realpathSync(path[, options])` +* `readlinkSync(path[, options])` +* `symlinkSync(target, path[, type])` +* `accessSync(path[, mode])` +* `rmSync(path[, options])` +* `truncateSync(path[, len])` +* `ftruncateSync(fd[, len])` +* `linkSync(existingPath, newPath)` +* `chmodSync(path, mode)` +* `chownSync(path, uid, gid)` +* `utimesSync(path, atime, mtime)` +* `lutimesSync(path, atime, mtime)` +* `mkdtempSync(prefix)` +* `opendirSync(path[, options])` +* `openAsBlob(path[, options])` +* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`, + `fstatSync` +* Streams: `createReadStream`, `createWriteStream` +* Watchers: `watch`, `watchFile`, `unwatchFile` + +#### Callback API + +`readFile`, `writeFile`, `stat`, `lstat`, `readdir`, `realpath`, `readlink`, +`access`, `open`, `close`, `read`, `write`, `rm`, `fstat`, `truncate`, +`ftruncate`, `link`, `mkdtemp`, `opendir`. Each takes a Node.js-style +callback `(err, ...result) => {}`. + +#### Promise API + +`vfs.promises` exposes the promise-based variants: + +```cjs +const vfs = require('node:vfs'); + +async function example() { + const myVfs = vfs.create(); + await myVfs.promises.writeFile('/file.txt', 'hello'); + const data = await myVfs.promises.readFile('/file.txt', 'utf8'); + return data; +} +example(); +``` + +The promise namespace mirrors `fs.promises` and includes `readFile`, +`writeFile`, `appendFile`, `stat`, `lstat`, `readdir`, `mkdir`, `rmdir`, +`unlink`, `rename`, `copyFile`, `realpath`, `readlink`, `symlink`, +`access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`, +`utimes`, `lutimes`, `open`, `lchmod`, and `watch`. + +## Class: `VirtualProvider` + + + +The base class for all VFS providers. Subclasses implement the essential +primitives (`open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`, +`rename`, ...) and inherit default implementations of the derived +The base class for all VFS providers. Subclasses implement the essential +primitives (such as `open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`, +`rename`, etc.) and inherit default implementations of the derived +methods (such as `readFile`, `writeFile`, `exists`, `copyFile`, `access`, etc.). + +### Capability flags + +* `provider.readonly` {boolean} **Default:** `false`. +* `provider.supportsSymlinks` {boolean} **Default:** `false`. +* `provider.supportsWatch` {boolean} **Default:** `false`. + +### Creating custom providers + +```cjs +const { VirtualProvider } = require('node:vfs'); + +class StaticProvider extends VirtualProvider { + get readonly() { return true; } + + statSync(path) { /* ... */ } + openSync(path, flags) { /* ... */ } + readdirSync(path, options) { /* ... */ } + // ... +} +``` + +The base class throws `ERR_METHOD_NOT_IMPLEMENTED` for any primitive +that has not been overridden, and rejects writes from a `readonly` +provider with `EROFS`. + +## Class: `MemoryProvider` + + + +The default in-memory provider. Stores files, directories, and symbolic +links in a `Map`-backed tree, supports symlinks (`supportsSymlinks === +true`), and supports watching (`supportsWatch === true`). + +### `memoryProvider.setReadOnly()` + + + +Locks the provider into read-only mode. Subsequent writes through any +[`VirtualFileSystem`][] using this provider throw `EROFS`. There is no +way to revert the provider to writable. + +```cjs +const vfs = require('node:vfs'); + +const provider = new vfs.MemoryProvider(); +const myVfs = vfs.create(provider); +myVfs.writeFileSync('/seed.txt', 'initial'); + +provider.setReadOnly(); + +myVfs.writeFileSync('/x.txt', 'fail'); // throws EROFS +``` + +## Class: `RealFSProvider` + + + +A provider that wraps a directory (i.e. one on the actual file system) and exposes its +contents through the VFS API. All VFS paths are resolved relative to +the root and verified to stay inside it; symbolic links resolving +outside the root are rejected. + +### `new RealFSProvider(rootPath)` + + + +* `rootPath` {string} The absolute file-system path to use as the root. + Must be a non-empty string. + +```cjs +const vfs = require('node:vfs'); + +const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox')); +realVfs.writeFileSync('/file.txt', 'hello'); // writes /tmp/sandbox/file.txt +``` + +### `realFSProvider.rootPath` + + + +* {string} + +The resolved absolute path used as the root. + +## Implementation details + +### `Stats` objects + +VFS `Stats` objects are real instances of [`fs.Stats`][] (or +[`fs.BigIntStats`][] when `{ bigint: true }` is requested). Their +fields use synthetic but stable values: + +* `dev` is `4085` (the VFS device id). +* `ino` is monotonically increasing per process. +* `blksize` is `4096`. +* `blocks` is `Math.ceil(size / 512)`. +* Times default to the moment the entry was created/last modified. + +[`MemoryProvider`]: #class-memoryprovider +[`VirtualFileSystem`]: #class-virtualfilesystem +[`VirtualProvider`]: #class-virtualprovider +[`fs.BigIntStats`]: fs.md#class-fsbigintstats +[`fs.Stats`]: fs.md#class-fsstats +[`node:fs`]: fs.md diff --git a/doc/node.1 b/doc/node.1 index 2f847f91bd08ea..5934919ae2aa8b 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -758,6 +758,11 @@ Enable the experimental .Sy node:stream/iter module. . +.It Fl -experimental-vfs +Enable the experimental +.Sy node:vfs +module. +. .It Fl -experimental-sea-config Use this flag to generate a blob that can be injected into the Node.js binary to produce a single executable application. See the documentation @@ -1945,6 +1950,8 @@ one is included in the list below. .It \fB--experimental-top-level-await\fR .It +\fB--experimental-vfs\fR +.It \fB--experimental-vm-modules\fR .It \fB--experimental-wasi-unstable-preview1\fR diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index 0fa7a8c4c1bcb7..8a4d179806aa53 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -131,9 +131,18 @@ const schemelessBlockList = new SafeSet([ 'quic', 'test', 'test/reporters', + 'vfs', ]); // Modules that will only be enabled at run time. -const experimentalModuleList = new SafeSet(['dtls', 'ffi', 'sqlite', 'quic', 'stream/iter', 'zlib/iter']); +const experimentalModuleList = new SafeSet([ + 'dtls', + 'ffi', + 'quic', + 'sqlite', + 'stream/iter', + 'vfs', + 'zlib/iter', +]); // Set up process.binding() and process._linkedBinding(). { diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 824214b55a2cb5..801ab9caecc2aa 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -496,6 +496,9 @@ function initializeCJS() { if (!getOptionValue('--experimental-ffi')) { modules = modules.filter((i) => i !== 'node:ffi'); } + if (!getOptionValue('--experimental-vfs')) { + modules = modules.filter((i) => i !== 'node:vfs'); + } Module.builtinModules = ObjectFreeze(modules); initializeCjsConditions(); diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 394c18887f72ce..4e4e2bcbdd5523 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -118,6 +118,7 @@ function prepareExecution(options) { setupSQLite(); setupStreamIter(); setupDTLS(); + setupVfs(); setupQuic(); setupWebStorage(); setupWebsocket(); @@ -431,6 +432,15 @@ function setupQuic() { BuiltinModule.allowRequireByUsers('quic'); } +function setupVfs() { + if (!getOptionValue('--experimental-vfs')) { + return; + } + + const { BuiltinModule } = require('internal/bootstrap/realm'); + BuiltinModule.allowRequireByUsers('vfs'); +} + function setupWebStorage() { if (getEmbedderOptions().noBrowserGlobals || !getOptionValue('--experimental-webstorage')) { diff --git a/lib/internal/vfs/dir.js b/lib/internal/vfs/dir.js new file mode 100644 index 00000000000000..803aeb4045310d --- /dev/null +++ b/lib/internal/vfs/dir.js @@ -0,0 +1,104 @@ +'use strict'; + +const { + SymbolAsyncDispose, + SymbolAsyncIterator, + SymbolDispose, +} = primordials; + +const { + codes: { + ERR_DIR_CLOSED, + }, +} = require('internal/errors'); + +/** + * Virtual directory handle returned by VFS opendir/opendirSync. + * Mimics the subset of the native Dir interface used by Node.js internals + * (e.g. fs.cp, fs.promises.cp). + */ +class VirtualDir { + #path; + #entries; + #index; + #closed; + + constructor(dirPath, entries) { + this.#path = dirPath; + this.#entries = entries; + this.#index = 0; + this.#closed = false; + } + + get path() { + return this.#path; + } + + readSync() { + if (this.#closed) { + throw new ERR_DIR_CLOSED(); + } + if (this.#index >= this.#entries.length) { + return null; + } + return this.#entries[this.#index++]; + } + + async read(callback) { + if (typeof callback === 'function') { + try { + const result = this.readSync(); + process.nextTick(callback, null, result); + } catch (err) { + process.nextTick(callback, err); + } + return; + } + return this.readSync(); + } + + closeSync() { + if (this.#closed) { + throw new ERR_DIR_CLOSED(); + } + this.#closed = true; + } + + async close(callback) { + if (typeof callback === 'function') { + this.closeSync(); + process.nextTick(callback, null); + return; + } + this.closeSync(); + } + + async *entries() { + if (this.#closed) { + throw new ERR_DIR_CLOSED(); + } + try { + let entry; + while ((entry = this.readSync()) !== null) { + yield entry; + } + } finally { + if (!this.#closed) { + this.closeSync(); + } + } + } + + [SymbolDispose]() { + if (!this.#closed) { + this.closeSync(); + } + } +} + +VirtualDir.prototype[SymbolAsyncIterator] = VirtualDir.prototype.entries; +VirtualDir.prototype[SymbolAsyncDispose] = VirtualDir.prototype.close; + +module.exports = { + VirtualDir, +}; diff --git a/lib/internal/vfs/errors.js b/lib/internal/vfs/errors.js new file mode 100644 index 00000000000000..79e4a647d133b1 --- /dev/null +++ b/lib/internal/vfs/errors.js @@ -0,0 +1,193 @@ +'use strict'; + +const { + ErrorCaptureStackTrace, +} = primordials; + +const { + UVException, +} = require('internal/errors'); + +const { + UV_ENOENT, + UV_ENOTDIR, + UV_ENOTEMPTY, + UV_EISDIR, + UV_EBADF, + UV_EEXIST, + UV_EROFS, + UV_EINVAL, + UV_ELOOP, + UV_EACCES, +} = internalBinding('uv'); + +/** + * Creates an ENOENT error for virtual file system operations. + * @param {string} syscall The system call name + * @param {string} path The path that was not found + * @returns {Error} + */ +function createENOENT(syscall, path) { + const err = new UVException({ + errno: UV_ENOENT, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createENOENT); + return err; +} + +/** + * Creates an ENOTDIR error. + * @param {string} syscall The system call name + * @param {string} path The path that is not a directory + * @returns {Error} + */ +function createENOTDIR(syscall, path) { + const err = new UVException({ + errno: UV_ENOTDIR, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createENOTDIR); + return err; +} + +/** + * Creates an ENOTEMPTY error for non-empty directory. + * @param {string} syscall The system call name + * @param {string} path The path of the non-empty directory + * @returns {Error} + */ +function createENOTEMPTY(syscall, path) { + const err = new UVException({ + errno: UV_ENOTEMPTY, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createENOTEMPTY); + return err; +} + +/** + * Creates an EISDIR error. + * @param {string} syscall The system call name + * @param {string} path The path that is a directory + * @returns {Error} + */ +function createEISDIR(syscall, path) { + const err = new UVException({ + errno: UV_EISDIR, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEISDIR); + return err; +} + +/** + * Creates an EBADF error for invalid file descriptor operations. + * @param {string} syscall The system call name + * @returns {Error} + */ +function createEBADF(syscall) { + const err = new UVException({ + errno: UV_EBADF, + syscall, + }); + ErrorCaptureStackTrace(err, createEBADF); + return err; +} + +/** + * Creates an EEXIST error. + * @param {string} syscall The system call name + * @param {string} path The path that already exists + * @returns {Error} + */ +function createEEXIST(syscall, path) { + const err = new UVException({ + errno: UV_EEXIST, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEEXIST); + return err; +} + +/** + * Creates an EROFS error for read-only file system. + * @param {string} syscall The system call name + * @param {string} path The path + * @returns {Error} + */ +function createEROFS(syscall, path) { + const err = new UVException({ + errno: UV_EROFS, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEROFS); + return err; +} + +/** + * Creates an EINVAL error for invalid argument. + * @param {string} syscall The system call name + * @param {string} path The path + * @returns {Error} + */ +function createEINVAL(syscall, path) { + const err = new UVException({ + errno: UV_EINVAL, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEINVAL); + return err; +} + +/** + * Creates an ELOOP error for too many symbolic links. + * @param {string} syscall The system call name + * @param {string} path The path + * @returns {Error} + */ +function createELOOP(syscall, path) { + const err = new UVException({ + errno: UV_ELOOP, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createELOOP); + return err; +} + +/** + * Creates an EACCES error for permission denied. + * @param {string} syscall The system call name + * @param {string} path The path + * @returns {Error} + */ +function createEACCES(syscall, path) { + const err = new UVException({ + errno: UV_EACCES, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEACCES); + return err; +} + +module.exports = { + createENOENT, + createENOTDIR, + createENOTEMPTY, + createEISDIR, + createEBADF, + createEEXIST, + createEROFS, + createEINVAL, + createELOOP, + createEACCES, +}; diff --git a/lib/internal/vfs/fd.js b/lib/internal/vfs/fd.js new file mode 100644 index 00000000000000..bd36ad218f48b2 --- /dev/null +++ b/lib/internal/vfs/fd.js @@ -0,0 +1,87 @@ +'use strict'; + +const { + SafeMap, + Symbol, +} = primordials; + +// Private symbols +const kFd = Symbol('kFd'); +const kEntry = Symbol('kEntry'); + +// VFS FDs use bit 30 set to avoid conflicts with real OS fds. +// Real fds are small non-negative integers; VFS fds start at 0x40000000. +const VFS_FD_MASK = 0x40000000; +let nextFd = 0; + +// Global registry of open virtual file descriptors +const openFDs = new SafeMap(); + +/** + * Represents an open virtual file descriptor. + * Wraps a VirtualFileHandle from the provider. + */ +class VirtualFD { + /** + * @param {number} fd The file descriptor number + * @param {VirtualFileHandle} entry The virtual file handle + */ + constructor(fd, entry) { + this[kFd] = fd; + this[kEntry] = entry; + } + + /** + * Gets the file descriptor number. + * @returns {number} + */ + get fd() { + return this[kFd]; + } + + /** + * Gets the file handle. + * @returns {VirtualFileHandle} + */ + get entry() { + return this[kEntry]; + } +} + +/** + * Opens a virtual file and returns its file descriptor. + * @param {VirtualFileHandle} entry The virtual file handle + * @returns {number} The file descriptor + */ +function openVirtualFd(entry) { + const fd = VFS_FD_MASK | nextFd++; + const vfd = new VirtualFD(fd, entry); + openFDs.set(fd, vfd); + return fd; +} + +/** + * Gets a VirtualFD by its file descriptor number. + * @param {number} fd The file descriptor number + * @returns {VirtualFD|undefined} + */ +function getVirtualFd(fd) { + return openFDs.get(fd); +} + +/** + * Closes a virtual file descriptor. + * @param {number} fd The file descriptor number + * @returns {boolean} True if the fd was found and closed + */ +function closeVirtualFd(fd) { + return openFDs.delete(fd); +} + +module.exports = { + VFS_FD_MASK, + VirtualFD, + openVirtualFd, + getVirtualFd, + closeVirtualFd, +}; diff --git a/lib/internal/vfs/file_handle.js b/lib/internal/vfs/file_handle.js new file mode 100644 index 00000000000000..e8a37e07f26495 --- /dev/null +++ b/lib/internal/vfs/file_handle.js @@ -0,0 +1,720 @@ +'use strict'; + +const { + DateNow, + MathMax, + MathMin, + Number, + Symbol, + SymbolAsyncDispose, + SymbolDispose, +} = primordials; + +const { Buffer } = require('buffer'); +const { + codes: { + ERR_INVALID_STATE, + ERR_METHOD_NOT_IMPLEMENTED, + }, +} = require('internal/errors'); +const { + createEBADF, +} = require('internal/vfs/errors'); + +// Private symbols +const kPath = Symbol('kPath'); +const kFlags = Symbol('kFlags'); +const kMode = Symbol('kMode'); +const kPosition = Symbol('kPosition'); +const kClosed = Symbol('kClosed'); + +/** + * Base class for virtual file handles. + * Provides the interface that file handles must implement. + */ +class VirtualFileHandle { + /** + * @param {string} path The file path + * @param {string} flags The open flags + * @param {number} [mode] The file mode + */ + constructor(path, flags, mode) { + this[kPath] = path; + this[kFlags] = flags; + this[kMode] = mode ?? 0o644; + this[kPosition] = 0; + this[kClosed] = false; + } + + /** + * Gets the file path. + * @returns {string} + */ + get path() { + return this[kPath]; + } + + /** + * Gets the open flags. + * @returns {string} + */ + get flags() { + return this[kFlags]; + } + + /** + * Gets the file mode. + * @returns {number} + */ + get mode() { + return this[kMode]; + } + + /** + * Gets the current position. + * @returns {number} + */ + get position() { + return this[kPosition]; + } + + /** + * Sets the current position. + * @param {number} pos The new position + */ + set position(pos) { + this[kPosition] = pos; + } + + /** + * Returns true if the handle is closed. + * @returns {boolean} + */ + get closed() { + return this[kClosed]; + } + + /** + * Throws if the handle is closed. + * @param {string} syscall The syscall name for the error + */ + #checkClosed(syscall) { + if (this[kClosed]) { + throw createEBADF(syscall); + } + } + + /** + * Reads data from the file. + * @param {Buffer} buffer The buffer to read into + * @param {number} offset The offset in the buffer to start writing + * @param {number} length The number of bytes to read + * @param {number|null} position The position to read from (null uses current position) + * @returns {Promise<{ bytesRead: number, buffer: Buffer }>} + */ + async read(buffer, offset, length, position) { + this.#checkClosed('read'); + throw new ERR_METHOD_NOT_IMPLEMENTED('read'); + } + + /** + * Reads data from the file synchronously. + * @param {Buffer} buffer The buffer to read into + * @param {number} offset The offset in the buffer to start writing + * @param {number} length The number of bytes to read + * @param {number|null} position The position to read from (null uses current position) + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + readSync(buffer, offset, length, position) { + this.#checkClosed('read'); + throw new ERR_METHOD_NOT_IMPLEMENTED('readSync'); + } + + /** + * Writes data to the file. + * @param {Buffer} buffer The buffer to write from + * @param {number} offset The offset in the buffer to start reading + * @param {number} length The number of bytes to write + * @param {number|null} position The position to write to (null uses current position) + * @returns {Promise<{ bytesWritten: number, buffer: Buffer }>} + */ + async write(buffer, offset, length, position) { + this.#checkClosed('write'); + throw new ERR_METHOD_NOT_IMPLEMENTED('write'); + } + + /** + * Writes data to the file synchronously. + * @param {Buffer} buffer The buffer to write from + * @param {number} offset The offset in the buffer to start reading + * @param {number} length The number of bytes to write + * @param {number|null} position The position to write to (null uses current position) + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + writeSync(buffer, offset, length, position) { + this.#checkClosed('write'); + throw new ERR_METHOD_NOT_IMPLEMENTED('writeSync'); + } + + /** + * Reads the entire file. + * @param {object|string} [options] Options or encoding + * @returns {Promise} + */ + async readFile(options) { + this.#checkClosed('read'); + throw new ERR_METHOD_NOT_IMPLEMENTED('readFile'); + } + + /** + * Reads the entire file synchronously. + * @param {object|string} [options] Options or encoding + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + readFileSync(options) { + this.#checkClosed('read'); + throw new ERR_METHOD_NOT_IMPLEMENTED('readFileSync'); + } + + /** + * Writes data to the file (replacing content). + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + * @returns {Promise} + */ + async writeFile(data, options) { + this.#checkClosed('write'); + throw new ERR_METHOD_NOT_IMPLEMENTED('writeFile'); + } + + /** + * Writes data to the file synchronously (replacing content). + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + */ + writeFileSync(data, options) { + this.#checkClosed('write'); + throw new ERR_METHOD_NOT_IMPLEMENTED('writeFileSync'); + } + + /** + * Gets file stats. + * @param {object} [options] Options + * @returns {Promise} + */ + async stat(options) { + this.#checkClosed('fstat'); + throw new ERR_METHOD_NOT_IMPLEMENTED('stat'); + } + + /** + * Gets file stats synchronously. + * @param {object} [options] Options + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + statSync(options) { + this.#checkClosed('fstat'); + throw new ERR_METHOD_NOT_IMPLEMENTED('statSync'); + } + + /** + * Truncates the file. + * @param {number} [len] The new length + * @returns {Promise} + */ + async truncate(len) { + this.#checkClosed('ftruncate'); + throw new ERR_METHOD_NOT_IMPLEMENTED('truncate'); + } + + /** + * Truncates the file synchronously. + * @param {number} [len] The new length + */ + truncateSync(len) { + this.#checkClosed('ftruncate'); + throw new ERR_METHOD_NOT_IMPLEMENTED('truncateSync'); + } + + /** + * No-op chmod - VFS files don't have real permissions. + * @returns {Promise} + */ + async chmod() {} + + /** + * No-op chown - VFS files don't have real ownership. + * @returns {Promise} + */ + async chown() {} + + /** + * No-op utimes - timestamps are handled by the provider. + * @returns {Promise} + */ + async utimes() {} + + /** + * No-op datasync - VFS is in-memory. + * @returns {Promise} + */ + async datasync() {} + + /** + * No-op sync - VFS is in-memory. + * @returns {Promise} + */ + async sync() {} + + /** + * Reads data from the file into multiple buffers. + * @param {Buffer[]} buffers The buffers to read into + * @param {number|null} [position] The position to read from + * @returns {Promise<{ bytesRead: number, buffers: Buffer[] }>} + */ + async readv(buffers, position) { + this.#checkClosed('readv'); + let totalRead = 0; + for (let i = 0; i < buffers.length; i++) { + const buf = buffers[i]; + const pos = position != null ? position + totalRead : null; + const { bytesRead } = await this.read(buf, 0, buf.byteLength, pos); + totalRead += bytesRead; + if (bytesRead < buf.byteLength) break; + } + return { __proto__: null, bytesRead: totalRead, buffers }; + } + + /** + * Writes data from multiple buffers to the file. + * @param {Buffer[]} buffers The buffers to write from + * @param {number|null} [position] The position to write to + * @returns {Promise<{ bytesWritten: number, buffers: Buffer[] }>} + */ + async writev(buffers, position) { + this.#checkClosed('writev'); + let totalWritten = 0; + for (let i = 0; i < buffers.length; i++) { + const buf = buffers[i]; + const pos = position != null ? position + totalWritten : null; + const { bytesWritten } = await this.write(buf, 0, buf.byteLength, pos); + totalWritten += bytesWritten; + if (bytesWritten < buf.byteLength) break; + } + return { __proto__: null, bytesWritten: totalWritten, buffers }; + } + + /** + * Appends data to the file. + * @param {Buffer|string} data The data to append + * @param {object} [options] Options + * @returns {Promise} + */ + async appendFile(data, options) { + this.#checkClosed('appendFile'); + const buffer = typeof data === 'string' ? + Buffer.from(data, options?.encoding) : data; + await this.write(buffer, 0, buffer.length, null); + } + + readableWebStream() { + throw new ERR_METHOD_NOT_IMPLEMENTED('readableWebStream'); + } + + readLines() { + throw new ERR_METHOD_NOT_IMPLEMENTED('readLines'); + } + + createReadStream() { + throw new ERR_METHOD_NOT_IMPLEMENTED('createReadStream'); + } + + createWriteStream() { + throw new ERR_METHOD_NOT_IMPLEMENTED('createWriteStream'); + } + + /** + * Closes the file handle. + * @returns {Promise} + */ + async close() { + this[kClosed] = true; + } + + /** + * Closes the file handle synchronously. + */ + closeSync() { + this[kClosed] = true; + } +} + +VirtualFileHandle.prototype[SymbolAsyncDispose] = VirtualFileHandle.prototype.close; +VirtualFileHandle.prototype[SymbolDispose] = VirtualFileHandle.prototype.closeSync; + +/** + * A file handle for in-memory file content. + * Used by MemoryProvider and similar providers. + */ +class MemoryFileHandle extends VirtualFileHandle { + #content; + #size; + #entry; + #getStats; + + #checkClosed(syscall) { + if (this.closed) { + throw createEBADF(syscall); + } + } + + /** + * @param {string} path The file path + * @param {string} flags The open flags + * @param {number} [mode] The file mode + * @param {Buffer} content The initial file content + * @param {object} entry The entry object (for updating content) + * @param {Function} getStats Function to get updated stats + */ + constructor(path, flags, mode, content, entry, getStats) { + super(path, flags, mode); + this.#content = content; + this.#size = content.length; + this.#entry = entry; + this.#getStats = getStats; + + // Handle different open modes + if (flags === 'w' || flags === 'w+' || + flags === 'wx' || flags === 'wx+') { + // Write mode: truncate + this.#content = Buffer.alloc(0); + this.#size = 0; + if (entry) { + entry.content = this.#content; + } + } else if (flags === 'a' || flags === 'a+' || + flags === 'ax' || flags === 'ax+') { + // Append mode: position at end + this.position = this.#size; + } + } + + /** + * Throws EBADF if the handle was not opened for writing. + */ + #checkWritable() { + if (this.flags === 'r') { + throw createEBADF('write'); + } + } + + /** + * Throws EBADF if the handle was not opened for reading. + */ + #checkReadable() { + const f = this.flags; + if (f === 'w' || f === 'a' || f === 'wx' || f === 'ax') { + throw createEBADF('read'); + } + } + + /** + * Returns true if this handle was opened in append mode. + * @returns {boolean} + */ + #isAppend() { + const f = this.flags; + return f === 'a' || f === 'a+' || f === 'ax' || f === 'ax+'; + } + + /** + * Gets the current content synchronously. + * For dynamic content providers, this gets fresh content from the entry. + * @returns {Buffer} + */ + get content() { + // If entry has a dynamic content provider, get fresh content sync + if (this.#entry?.isDynamic && this.#entry.isDynamic()) { + return this.#entry.getContentSync(); + } + return this.#content.subarray(0, this.#size); + } + + /** + * Gets the current content asynchronously. + * For dynamic content providers, this gets fresh content from the entry. + * @returns {Promise} + */ + async getContentAsync() { + // If entry has a dynamic content provider, get fresh content async + if (this.#entry?.getContentAsync) { + return this.#entry.getContentAsync(); + } + return this.#content; + } + + /** + * Reads data from the file synchronously. + * @param {Buffer} buffer The buffer to read into + * @param {number} offset The offset in the buffer to start writing + * @param {number} length The number of bytes to read + * @param {number|null} position The position to read from (null uses current position) + * @returns {number} The number of bytes read + */ + readSync(buffer, offset, length, position) { + this.#checkClosed('read'); + this.#checkReadable(); + + // Get content (resolves dynamic content providers) + const content = this.content; + const readPos = position !== null && position !== undefined ? + Number(position) : this.position; + const available = content.length - readPos; + + if (available <= 0) { + return 0; + } + + const bytesToRead = MathMin(length, available); + content.copy(buffer, offset, readPos, readPos + bytesToRead); + + // Update position if not using explicit position + if (position === null || position === undefined) { + this.position = readPos + bytesToRead; + } + + return bytesToRead; + } + + /** + * Reads data from the file. + * @param {Buffer} buffer The buffer to read into + * @param {number} offset The offset in the buffer to start writing + * @param {number} length The number of bytes to read + * @param {number|null} position The position to read from (null uses current position) + * @returns {Promise<{ bytesRead: number, buffer: Buffer }>} + */ + async read(buffer, offset, length, position) { + const bytesRead = this.readSync(buffer, offset, length, position); + return { __proto__: null, bytesRead, buffer }; + } + + /** + * Writes data to the file synchronously. + * @param {Buffer} buffer The buffer to write from + * @param {number} offset The offset in the buffer to start reading + * @param {number} length The number of bytes to write + * @param {number|null} position The position to write to (null uses current position) + * @returns {number} The number of bytes written + */ + writeSync(buffer, offset, length, position) { + this.#checkClosed('write'); + this.#checkWritable(); + + // In append mode, always write at the end + const writePos = this.#isAppend() ? + this.#size : + (position !== null && position !== undefined ? + Number(position) : this.position); + const data = buffer.subarray(offset, offset + length); + + // Expand buffer if needed (geometric doubling for amortized O(1) appends) + const neededSize = writePos + length; + if (neededSize > this.#content.length) { + const newCapacity = MathMax(neededSize, this.#content.length * 2); + const newContent = Buffer.alloc(newCapacity); + this.#content.copy(newContent, 0, 0, this.#size); + this.#content = newContent; + } + + // Write the data + data.copy(this.#content, writePos); + + // Update actual content size + if (neededSize > this.#size) { + this.#size = neededSize; + } + + // Update the entry's content, mtime, and ctime + if (this.#entry) { + const now = DateNow(); + this.#entry.content = this.#content.subarray(0, this.#size); + this.#entry.mtime = now; + this.#entry.ctime = now; + } + + // Update position if not using explicit position + if (position === null || position === undefined) { + this.position = writePos + length; + } + + return length; + } + + /** + * Writes data to the file. + * @param {Buffer} buffer The buffer to write from + * @param {number} offset The offset in the buffer to start reading + * @param {number} length The number of bytes to write + * @param {number|null} position The position to write to (null uses current position) + * @returns {Promise<{ bytesWritten: number, buffer: Buffer }>} + */ + async write(buffer, offset, length, position) { + const bytesWritten = this.writeSync(buffer, offset, length, position); + return { __proto__: null, bytesWritten, buffer }; + } + + /** + * Reads the entire file synchronously. + * @param {object|string} [options] Options or encoding + * @returns {Buffer|string} + */ + readFileSync(options) { + this.#checkClosed('read'); + this.#checkReadable(); + + // Get content (resolves dynamic content providers) + const content = this.content; + const encoding = typeof options === 'string' ? options : options?.encoding; + if (encoding) { + return content.toString(encoding); + } + return Buffer.from(content); + } + + /** + * Reads the entire file. + * @param {object|string} [options] Options or encoding + * @returns {Promise} + */ + async readFile(options) { + this.#checkClosed('read'); + this.#checkReadable(); + + // Get content asynchronously (supports async content providers) + const content = await this.getContentAsync(); + const encoding = typeof options === 'string' ? options : options?.encoding; + if (encoding) { + return content.toString(encoding); + } + return Buffer.from(content); + } + + /** + * Writes data to the file synchronously. + * Replaces content in 'w' mode, appends in 'a' mode. + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + */ + writeFileSync(data, options) { + this.#checkClosed('write'); + this.#checkWritable(); + + const buffer = typeof data === 'string' ? Buffer.from(data, options?.encoding) : data; + + // In append mode, append to existing content + if (this.#isAppend()) { + const neededSize = this.#size + buffer.length; + if (neededSize > this.#content.length) { + const newCapacity = MathMax(neededSize, this.#content.length * 2); + const newContent = Buffer.alloc(newCapacity); + this.#content.copy(newContent, 0, 0, this.#size); + this.#content = newContent; + } + buffer.copy(this.#content, this.#size); + this.#size = neededSize; + } else { + this.#content = Buffer.from(buffer); + this.#size = buffer.length; + } + + // Update the entry's content, mtime, and ctime + if (this.#entry) { + const now = DateNow(); + this.#entry.content = this.#content.subarray(0, this.#size); + this.#entry.mtime = now; + this.#entry.ctime = now; + } + + this.position = this.#size; + } + + /** + * Writes data to the file (replacing content). + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + * @returns {Promise} + */ + async writeFile(data, options) { + this.writeFileSync(data, options); + } + + /** + * Gets file stats synchronously. + * @param {object} [options] Options + * @returns {Stats} + */ + statSync(options) { + this.#checkClosed('fstat'); + if (this.#getStats) { + return this.#getStats(this.#size); + } + throw new ERR_INVALID_STATE('stats not available'); + } + + /** + * Gets file stats. + * @param {object} [options] Options + * @returns {Promise} + */ + async stat(options) { + return this.statSync(options); + } + + /** + * Truncates the file synchronously. + * @param {number} [len] The new length + */ + truncateSync(len = 0) { + this.#checkClosed('ftruncate'); + this.#checkWritable(); + + if (len < this.#size) { + // Zero out truncated region to avoid stale data + this.#content.fill(0, len, this.#size); + this.#size = len; + } else if (len > this.#size) { + if (len > this.#content.length) { + const newContent = Buffer.alloc(len); + this.#content.copy(newContent, 0, 0, this.#size); + this.#content = newContent; + } else { + // Buffer has enough capacity, just zero-fill the extension + this.#content.fill(0, this.#size, len); + } + this.#size = len; + } + + // Update the entry's content, mtime, and ctime + if (this.#entry) { + const now = DateNow(); + this.#entry.content = this.#content.subarray(0, this.#size); + this.#entry.mtime = now; + this.#entry.ctime = now; + } + } + + /** + * Truncates the file. + * @param {number} [len] The new length + * @returns {Promise} + */ + async truncate(len) { + this.truncateSync(len); + } +} + +module.exports = { + VirtualFileHandle, + MemoryFileHandle, +}; diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js new file mode 100644 index 00000000000000..c48478ee85aa6c --- /dev/null +++ b/lib/internal/vfs/file_system.js @@ -0,0 +1,1147 @@ +'use strict'; + +const { + MathRandom, + ObjectFreeze, + Symbol, +} = primordials; + +const { validateBoolean } = require('internal/validators'); +const { MemoryProvider } = require('internal/vfs/providers/memory'); +const { posix: pathPosix } = require('path'); +const { join: joinPath } = pathPosix; +const { + openVirtualFd, + getVirtualFd, + closeVirtualFd, +} = require('internal/vfs/fd'); +const { + createEBADF, + createEISDIR, +} = require('internal/vfs/errors'); +const { VirtualReadStream, VirtualWriteStream } = require('internal/vfs/streams'); +const { VirtualDir } = require('internal/vfs/dir'); +const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); + +// Private symbols +const kProvider = Symbol('kProvider'); +const kPromises = Symbol('kPromises'); + +/** + * Virtual File System implementation using Provider architecture. + * Wraps a Provider and exposes an fs-like API operating on + * provider-relative paths. + */ +class VirtualFileSystem { + /** + * @param {VirtualProvider|object} [providerOrOptions] The provider to use, or options + * @param {object} [options] Configuration options + * @param {boolean} [options.emitExperimentalWarning] Emit the experimental warning (default: true) + */ + constructor(providerOrOptions, options = kEmptyObject) { + + // Handle case where first arg is options object (no provider) + let provider = null; + if (providerOrOptions !== undefined && providerOrOptions !== null) { + if (typeof providerOrOptions.openSync === 'function') { + // It's a provider + provider = providerOrOptions; + } else if (typeof providerOrOptions === 'object') { + // It's options (no provider specified) + options = providerOrOptions; + provider = null; + } + } + + if (options.emitExperimentalWarning !== undefined) { + validateBoolean(options.emitExperimentalWarning, 'options.emitExperimentalWarning'); + } + + if (options.emitExperimentalWarning !== false) { + emitExperimentalWarning('VirtualFileSystem'); + } + + this[kProvider] = provider ?? new MemoryProvider(); + this[kPromises] = null; // Lazy-initialized + } + + /** + * Gets the underlying provider. + * @returns {VirtualProvider} + */ + get provider() { + return this[kProvider]; + } + + /** + * Returns true if the provider is read-only. + * @returns {boolean} + */ + get readonly() { + return this[kProvider].readonly; + } + + // ==================== Path Resolution ==================== + + /** + * Normalizes a path to a provider-relative POSIX path. + * @param {string} inputPath The path to normalize + * @returns {string} + */ + #toProviderPath(inputPath) { + return pathPosix.normalize(inputPath); + } + + // ==================== FS Operations (Sync) ==================== + + /** + * Checks if a path exists synchronously. + * @param {string} filePath The path to check + * @returns {boolean} + */ + existsSync(filePath) { + try { + const providerPath = this.#toProviderPath(filePath); + return this[kProvider].existsSync(providerPath); + } catch { + return false; + } + } + + /** + * Gets stats for a path synchronously. + * @param {string} filePath The path to stat + * @param {object} [options] Options + * @returns {Stats} + */ + statSync(filePath, options) { + const providerPath = this.#toProviderPath(filePath); + return this[kProvider].statSync(providerPath, options); + } + + /** + * Gets stats for a path synchronously without following symlinks. + * @param {string} filePath The path to stat + * @param {object} [options] Options + * @returns {Stats} + */ + lstatSync(filePath, options) { + const providerPath = this.#toProviderPath(filePath); + return this[kProvider].lstatSync(providerPath, options); + } + + /** + * Reads a file synchronously. + * @param {string} filePath The path to read + * @param {object|string} [options] Options or encoding + * @returns {Buffer|string} + */ + readFileSync(filePath, options) { + const providerPath = this.#toProviderPath(filePath); + return this[kProvider].readFileSync(providerPath, options); + } + + /** + * Writes a file synchronously. + * @param {string} filePath The path to write + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + */ + writeFileSync(filePath, data, options) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].writeFileSync(providerPath, data, options); + } + + /** + * Appends to a file synchronously. + * @param {string} filePath The path to append to + * @param {Buffer|string} data The data to append + * @param {object} [options] Options + */ + appendFileSync(filePath, data, options) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].appendFileSync(providerPath, data, options); + } + + /** + * Reads directory contents synchronously. + * @param {string} dirPath The directory path + * @param {object} [options] Options + * @returns {string[]|Dirent[]} + */ + readdirSync(dirPath, options) { + const providerPath = this.#toProviderPath(dirPath); + const result = this[kProvider].readdirSync(providerPath, options); + + // Fix Dirent parentPath from provider-relative to actual VFS path + if (options?.withFileTypes === true) { + const recursive = options?.recursive === true; + for (let i = 0; i < result.length; i++) { + const dirent = result[i]; + if (recursive) { + // In recursive mode, name may contain slashes (e.g. 'a/b.txt'). + // Fix to basename only and set correct parentPath. + const slashIdx = dirent.name.lastIndexOf('/'); + if (slashIdx !== -1) { + const subdir = dirent.name.slice(0, slashIdx); + dirent.parentPath = joinPath(dirPath, subdir); + dirent.name = dirent.name.slice(slashIdx + 1); + } else { + dirent.parentPath = dirPath; + } + } else { + dirent.parentPath = dirPath; + } + } + } + + return result; + } + + /** + * Creates a directory synchronously. + * @param {string} dirPath The directory path + * @param {object} [options] Options + * @returns {string|undefined} + */ + mkdirSync(dirPath, options) { + const providerPath = this.#toProviderPath(dirPath); + return this[kProvider].mkdirSync(providerPath, options); + } + + /** + * Removes a directory synchronously. + * @param {string} dirPath The directory path + */ + rmdirSync(dirPath) { + const providerPath = this.#toProviderPath(dirPath); + this[kProvider].rmdirSync(providerPath); + } + + /** + * Removes a file synchronously. + * @param {string} filePath The file path + */ + unlinkSync(filePath) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].unlinkSync(providerPath); + } + + /** + * Renames a file or directory synchronously. + * @param {string} oldPath The old path + * @param {string} newPath The new path + */ + renameSync(oldPath, newPath) { + const oldProviderPath = this.#toProviderPath(oldPath); + const newProviderPath = this.#toProviderPath(newPath); + this[kProvider].renameSync(oldProviderPath, newProviderPath); + } + + /** + * Copies a file synchronously. + * @param {string} src Source path + * @param {string} dest Destination path + * @param {number} [mode] Copy mode flags + */ + copyFileSync(src, dest, mode) { + const srcProviderPath = this.#toProviderPath(src); + const destProviderPath = this.#toProviderPath(dest); + this[kProvider].copyFileSync(srcProviderPath, destProviderPath, mode); + } + + /** + * Gets the real path by resolving all symlinks. + * @param {string} filePath The path + * @param {object} [options] Options + * @returns {string} + */ + realpathSync(filePath, options) { + const providerPath = this.#toProviderPath(filePath); + return this[kProvider].realpathSync(providerPath, options); + } + + /** + * Reads the target of a symbolic link. + * @param {string} linkPath The symlink path + * @param {object} [options] Options + * @returns {string} + */ + readlinkSync(linkPath, options) { + const providerPath = this.#toProviderPath(linkPath); + return this[kProvider].readlinkSync(providerPath, options); + } + + /** + * Creates a symbolic link. + * @param {string} target The symlink target + * @param {string} path The symlink path + * @param {string} [type] The symlink type + */ + symlinkSync(target, path, type) { + const providerPath = this.#toProviderPath(path); + this[kProvider].symlinkSync(target, providerPath, type); + } + + /** + * Checks file accessibility synchronously. + * @param {string} filePath The path to check + * @param {number} [mode] Access mode + */ + accessSync(filePath, mode) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].accessSync(providerPath, mode); + } + + /** + * Removes a file or directory synchronously. + * @param {string} filePath The path to remove + * @param {object} [options] Options + * @param {boolean} [options.recursive] If true, remove directories recursively + * @param {boolean} [options.force] If true, ignore ENOENT errors + */ + rmSync(filePath, options) { + const recursive = options?.recursive === true; + const force = options?.force === true; + + let stats; + try { + stats = this.lstatSync(filePath); + } catch (err) { + if (force && err?.code === 'ENOENT') return; + throw err; + } + + // Symlinks should be unlinked directly, never recursed into + if (stats.isSymbolicLink()) { + this.unlinkSync(filePath); + return; + } + + if (stats.isDirectory()) { + if (!recursive) { + throw createEISDIR('rm', filePath); + } + const entries = this.readdirSync(filePath); + for (let i = 0; i < entries.length; i++) { + this.rmSync(joinPath(filePath, entries[i]), options); + } + this.rmdirSync(filePath); + } else { + this.unlinkSync(filePath); + } + } + + // ==================== Additional Sync Operations ==================== + + /** + * Truncates a file synchronously. + * @param {string} filePath The file path + * @param {number} [len] The new length + */ + truncateSync(filePath, len = 0) { + if (len < 0) len = 0; + const providerPath = this.#toProviderPath(filePath); + const handle = this[kProvider].openSync(providerPath, 'r+'); + try { + handle.truncateSync(len); + } finally { + handle.closeSync(); + } + } + + /** + * Truncates a file descriptor synchronously. + * @param {number} fd The file descriptor + * @param {number} [len] The new length + */ + ftruncateSync(fd, len = 0) { + const vfd = getVirtualFd(fd); + if (!vfd) { + throw createEBADF('ftruncate'); + } + vfd.entry.truncateSync(len); + } + + /** + * Creates a hard link synchronously. + * @param {string} existingPath The existing file path + * @param {string} newPath The new link path + */ + linkSync(existingPath, newPath) { + const existingProviderPath = this.#toProviderPath(existingPath); + const newProviderPath = this.#toProviderPath(newPath); + this[kProvider].linkSync(existingProviderPath, newProviderPath); + } + + chmodSync(filePath, mode) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].chmodSync(providerPath, mode); + } + + chownSync(filePath, uid, gid) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].chownSync(providerPath, uid, gid); + } + + utimesSync(filePath, atime, mtime) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].utimesSync(providerPath, atime, mtime); + } + + lutimesSync(filePath, atime, mtime) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].lutimesSync(providerPath, atime, mtime); + } + + /** + * Creates a unique temporary directory synchronously. + * @param {string} prefix The prefix for the temp directory + * @returns {string} The full path of the created directory + */ + mkdtempSync(prefix) { + const providerPrefix = this.#toProviderPath(prefix); + // Generate random 6-character suffix like Node does + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let suffix = ''; + for (let i = 0; i < 6; i++) { + suffix += chars[(MathRandom() * chars.length) | 0]; + } + const dirPath = providerPrefix + suffix; + this[kProvider].mkdirSync(dirPath); + return dirPath; + } + + /** + * Opens a directory synchronously. + * @param {string} dirPath The directory path + * @param {object} [options] Options + * @returns {VirtualDir} A directory handle + */ + opendirSync(dirPath, options) { + const entries = this.readdirSync(dirPath, { + withFileTypes: true, + recursive: options?.recursive, + }); + return new VirtualDir(dirPath, entries); + } + + /** + * Opens a file as a Blob. + * @param {string} filePath The file path + * @param {object} [options] Options + * @returns {Blob} The file content as a Blob + */ + openAsBlob(filePath, options) { + const { Blob } = require('buffer'); + const providerPath = this.#toProviderPath(filePath); + const content = this[kProvider].readFileSync(providerPath); + const type = options?.type || ''; + return new Blob([content], { type }); + } + + // ==================== File Descriptor Operations ==================== + + /** + * Opens a file synchronously and returns a file descriptor. + * @param {string} filePath The path to open + * @param {string} [flags] Open flags + * @param {number} [mode] File mode + * @returns {number} The file descriptor + */ + openSync(filePath, flags = 'r', mode) { + const providerPath = this.#toProviderPath(filePath); + const handle = this[kProvider].openSync(providerPath, flags, mode); + return openVirtualFd(handle); + } + + /** + * Closes a file descriptor synchronously. + * @param {number} fd The file descriptor + */ + closeSync(fd) { + const vfd = getVirtualFd(fd); + if (!vfd) { + throw createEBADF('close'); + } + vfd.entry.closeSync(); + closeVirtualFd(fd); + } + + /** + * Reads from a file descriptor synchronously. + * @param {number} fd The file descriptor + * @param {Buffer} buffer The buffer to read into + * @param {number} offset The offset in the buffer + * @param {number} length The number of bytes to read + * @param {number|null} position The position in the file + * @returns {number} The number of bytes read + */ + readSync(fd, buffer, offset, length, position) { + const vfd = getVirtualFd(fd); + if (!vfd) { + throw createEBADF('read'); + } + return vfd.entry.readSync(buffer, offset, length, position); + } + + /** + * Writes to a file descriptor synchronously. + * @param {number} fd The file descriptor + * @param {Buffer} buffer The buffer to write from + * @param {number} offset The offset in the buffer + * @param {number} length The number of bytes to write + * @param {number|null} position The position in the file + * @returns {number} The number of bytes written + */ + writeSync(fd, buffer, offset, length, position) { + const vfd = getVirtualFd(fd); + if (!vfd) { + throw createEBADF('write'); + } + return vfd.entry.writeSync(buffer, offset, length, position); + } + + /** + * Gets file stats from a file descriptor synchronously. + * @param {number} fd The file descriptor + * @param {object} [options] Options + * @returns {Stats} + */ + fstatSync(fd, options) { + const vfd = getVirtualFd(fd); + if (!vfd) { + throw createEBADF('fstat'); + } + return vfd.entry.statSync(options); + } + + // ==================== FS Operations (Async with Callbacks) ==================== + + /** + * Reads a file asynchronously. + * @param {string} filePath The path to read + * @param {object|string|Function} [options] Options, encoding, or callback + * @param {Function} [callback] Callback (err, data) + */ + readFile(filePath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].readFile(this.#toProviderPath(filePath), options) + .then((data) => callback(null, data), (err) => callback(err)); + } + + /** + * Writes a file asynchronously. + * @param {string} filePath The path to write + * @param {Buffer|string} data The data to write + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err) + */ + writeFile(filePath, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].writeFile(this.#toProviderPath(filePath), data, options) + .then(() => callback(null), (err) => callback(err)); + } + + /** + * Gets stats for a path asynchronously. + * @param {string} filePath The path to stat + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, stats) + */ + stat(filePath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].stat(this.#toProviderPath(filePath), options) + .then((stats) => callback(null, stats), (err) => callback(err)); + } + + /** + * Gets stats without following symlinks asynchronously. + * @param {string} filePath The path to stat + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, stats) + */ + lstat(filePath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].lstat(this.#toProviderPath(filePath), options) + .then((stats) => callback(null, stats), (err) => callback(err)); + } + + /** + * Reads directory contents asynchronously. + * @param {string} dirPath The directory path + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, entries) + */ + readdir(dirPath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].readdir(this.#toProviderPath(dirPath), options) + .then((entries) => callback(null, entries), (err) => callback(err)); + } + + /** + * Gets the real path asynchronously. + * @param {string} filePath The path + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, resolvedPath) + */ + realpath(filePath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].realpath(this.#toProviderPath(filePath), options) + .then((realPath) => callback(null, realPath), (err) => callback(err)); + } + + /** + * Reads symlink target asynchronously. + * @param {string} linkPath The symlink path + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, target) + */ + readlink(linkPath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + this[kProvider].readlink(this.#toProviderPath(linkPath), options) + .then((target) => callback(null, target), (err) => callback(err)); + } + + /** + * Checks file accessibility asynchronously. + * @param {string} filePath The path to check + * @param {number|Function} [mode] Access mode or callback + * @param {Function} [callback] Callback (err) + */ + access(filePath, mode, callback) { + if (typeof mode === 'function') { + callback = mode; + mode = undefined; + } + + this[kProvider].access(this.#toProviderPath(filePath), mode) + .then(() => callback(null), (err) => callback(err)); + } + + /** + * Opens a file asynchronously. + * @param {string} filePath The path to open + * @param {string|Function} [flags] Open flags or callback + * @param {number|Function} [mode] File mode or callback + * @param {Function} [callback] Callback (err, fd) + */ + open(filePath, flags, mode, callback) { + if (typeof flags === 'function') { + callback = flags; + flags = 'r'; + mode = undefined; + } else if (typeof mode === 'function') { + callback = mode; + mode = undefined; + } + + const providerPath = this.#toProviderPath(filePath); + this[kProvider].open(providerPath, flags, mode) + .then((handle) => { + const fd = openVirtualFd(handle); + callback(null, fd); + }, (err) => callback(err)); + } + + /** + * Closes a file descriptor asynchronously. + * @param {number} fd The file descriptor + * @param {Function} callback Callback (err) + */ + close(fd, callback) { + const vfd = getVirtualFd(fd); + if (!vfd) { + process.nextTick(callback, createEBADF('close')); + return; + } + + vfd.entry.close() + .then(() => { + closeVirtualFd(fd); + callback(null); + }, (err) => callback(err)); + } + + /** + * Reads from a file descriptor asynchronously. + * @param {number} fd The file descriptor + * @param {Buffer} buffer The buffer to read into + * @param {number} offset The offset in the buffer + * @param {number} length The number of bytes to read + * @param {number|null} position The position in the file + * @param {Function} callback Callback (err, bytesRead, buffer) + */ + read(fd, buffer, offset, length, position, callback) { + const vfd = getVirtualFd(fd); + if (!vfd) { + process.nextTick(callback, createEBADF('read')); + return; + } + + vfd.entry.read(buffer, offset, length, position) + .then(({ bytesRead }) => callback(null, bytesRead, buffer), (err) => callback(err)); + } + + /** + * Writes to a file descriptor asynchronously. + * @param {number} fd The file descriptor + * @param {Buffer} buffer The buffer to write from + * @param {number} offset The offset in the buffer + * @param {number} length The number of bytes to write + * @param {number|null} position The position in the file + * @param {Function} callback Callback (err, bytesWritten, buffer) + */ + write(fd, buffer, offset, length, position, callback) { + const vfd = getVirtualFd(fd); + if (!vfd) { + process.nextTick(callback, createEBADF('write')); + return; + } + + vfd.entry.write(buffer, offset, length, position) + .then(({ bytesWritten }) => callback(null, bytesWritten, buffer), (err) => callback(err)); + } + + /** + * Removes a file or directory asynchronously. + * @param {string} filePath The path to remove + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err) + */ + rm(filePath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + try { + this.rmSync(filePath, options); + process.nextTick(callback, null); + } catch (err) { + process.nextTick(callback, err); + } + } + + /** + * Gets file stats from a file descriptor asynchronously. + * @param {number} fd The file descriptor + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, stats) + */ + fstat(fd, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + const vfd = getVirtualFd(fd); + if (!vfd) { + process.nextTick(callback, createEBADF('fstat')); + return; + } + + vfd.entry.stat(options) + .then((stats) => callback(null, stats), (err) => callback(err)); + } + + /** + * Truncates a file asynchronously. + * @param {string} filePath The file path + * @param {number|Function} [len] The new length or callback + * @param {Function} [callback] Callback (err) + */ + truncate(filePath, len, callback) { + if (typeof len === 'function') { + callback = len; + len = 0; + } + try { + this.truncateSync(filePath, len); + process.nextTick(callback, null); + } catch (err) { + process.nextTick(callback, err); + } + } + + /** + * Truncates a file descriptor asynchronously. + * @param {number} fd The file descriptor + * @param {number|Function} [len] The new length or callback + * @param {Function} [callback] Callback (err) + */ + ftruncate(fd, len, callback) { + if (typeof len === 'function') { + callback = len; + len = 0; + } + try { + this.ftruncateSync(fd, len); + process.nextTick(callback, null); + } catch (err) { + process.nextTick(callback, err); + } + } + + /** + * Creates a hard link asynchronously. + * @param {string} existingPath The existing file path + * @param {string} newPath The new link path + * @param {Function} callback Callback (err) + */ + link(existingPath, newPath, callback) { + try { + this.linkSync(existingPath, newPath); + process.nextTick(callback, null); + } catch (err) { + process.nextTick(callback, err); + } + } + + /** + * Creates a unique temporary directory asynchronously. + * @param {string} prefix The prefix for the temp directory + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, dirPath) + */ + mkdtemp(prefix, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + try { + const dirPath = this.mkdtempSync(prefix); + process.nextTick(callback, null, dirPath); + } catch (err) { + process.nextTick(callback, err); + } + } + + /** + * Opens a directory asynchronously. + * @param {string} dirPath The directory path + * @param {object|Function} [options] Options or callback + * @param {Function} [callback] Callback (err, dir) + */ + opendir(dirPath, options, callback) { + if (typeof options === 'function') { + callback = options; + options = undefined; + } + try { + const dir = this.opendirSync(dirPath, options); + process.nextTick(callback, null, dir); + } catch (err) { + process.nextTick(callback, err); + } + } + + // ==================== Stream Operations ==================== + + /** + * Creates a readable stream for a virtual file. + * @param {string} filePath The path to the file + * @param {object} [options] Stream options + * @returns {ReadStream} + */ + createReadStream(filePath, options) { + return new VirtualReadStream(this, filePath, options); + } + + /** + * Creates a writable stream for a virtual file. + * @param {string} filePath The path to the file + * @param {object} [options] Stream options + * @returns {WriteStream} + */ + createWriteStream(filePath, options) { + return new VirtualWriteStream(this, filePath, options); + } + + // ==================== Watch Operations ==================== + + /** + * Watches a file or directory for changes. + * @param {string} filePath The path to watch + * @param {object|Function} [options] Watch options or listener + * @param {Function} [listener] Change listener + * @returns {EventEmitter} A watcher that emits 'change' events + */ + watch(filePath, options, listener) { + if (typeof options === 'function') { + listener = options; + options = {}; + } + + const providerPath = this.#toProviderPath(filePath); + const watcher = this[kProvider].watch(providerPath, options); + + if (listener) { + watcher.on('change', listener); + } + + return watcher; + } + + /** + * Watches a file for changes using stat polling. + * @param {string} filePath The path to watch + * @param {object|Function} [options] Watch options or listener + * @param {Function} [listener] Change listener + * @returns {EventEmitter} A stat watcher that emits 'change' events + */ + watchFile(filePath, options, listener) { + if (typeof options === 'function') { + listener = options; + options = {}; + } + + const providerPath = this.#toProviderPath(filePath); + return this[kProvider].watchFile(providerPath, options, listener); + } + + /** + * Stops watching a file for changes. + * @param {string} filePath The path to stop watching + * @param {Function} [listener] Optional listener to remove + */ + unwatchFile(filePath, listener) { + const providerPath = this.#toProviderPath(filePath); + this[kProvider].unwatchFile(providerPath, listener); + } + + // ==================== Promise API ==================== + + /** + * Gets the promises API for this VFS instance. + * @returns {object} Promise-based fs methods + */ + get promises() { + if (this[kPromises] === null) { + this[kPromises] = this.#createPromisesAPI(); + } + return this[kPromises]; + } + + /** + * Creates the promises API object for this VFS instance. + * @returns {object} Promise-based fs methods + */ + #createPromisesAPI() { + const provider = this[kProvider]; + + // Use arrow function to capture `this` for private method access + const toProviderPath = (p) => this.#toProviderPath(p); + + return ObjectFreeze({ + async readFile(filePath, options) { + const providerPath = toProviderPath(filePath); + return provider.readFile(providerPath, options); + }, + + async writeFile(filePath, data, options) { + const providerPath = toProviderPath(filePath); + return provider.writeFile(providerPath, data, options); + }, + + async appendFile(filePath, data, options) { + const providerPath = toProviderPath(filePath); + return provider.appendFile(providerPath, data, options); + }, + + async stat(filePath, options) { + const providerPath = toProviderPath(filePath); + return provider.stat(providerPath, options); + }, + + async lstat(filePath, options) { + const providerPath = toProviderPath(filePath); + return provider.lstat(providerPath, options); + }, + + async readdir(dirPath, options) { + const providerPath = toProviderPath(dirPath); + return provider.readdir(providerPath, options); + }, + + async mkdir(dirPath, options) { + const providerPath = toProviderPath(dirPath); + return provider.mkdir(providerPath, options); + }, + + async rmdir(dirPath) { + const providerPath = toProviderPath(dirPath); + return provider.rmdir(providerPath); + }, + + async unlink(filePath) { + const providerPath = toProviderPath(filePath); + return provider.unlink(providerPath); + }, + + async rename(oldPath, newPath) { + const oldProviderPath = toProviderPath(oldPath); + const newProviderPath = toProviderPath(newPath); + return provider.rename(oldProviderPath, newProviderPath); + }, + + async copyFile(src, dest, mode) { + const srcProviderPath = toProviderPath(src); + const destProviderPath = toProviderPath(dest); + return provider.copyFile(srcProviderPath, destProviderPath, mode); + }, + + async realpath(filePath, options) { + const providerPath = toProviderPath(filePath); + return provider.realpath(providerPath, options); + }, + + async readlink(linkPath, options) { + const providerPath = toProviderPath(linkPath); + return provider.readlink(providerPath, options); + }, + + async symlink(target, path, type) { + const providerPath = toProviderPath(path); + return provider.symlink(target, providerPath, type); + }, + + async access(filePath, mode) { + const providerPath = toProviderPath(filePath); + return provider.access(providerPath, mode); + }, + + async rm(filePath, options) { + const recursive = options?.recursive === true; + const force = options?.force === true; + + let stats; + try { + stats = await provider.lstat(toProviderPath(filePath)); + } catch (err) { + if (force && err?.code === 'ENOENT') return; + throw err; + } + + // Symlinks should be unlinked directly, never recursed into + if (stats.isSymbolicLink()) { + await provider.unlink(toProviderPath(filePath)); + return; + } + + if (stats.isDirectory()) { + if (!recursive) { + throw createEISDIR('rm', filePath); + } + const entries = await provider.readdir(toProviderPath(filePath)); + for (let i = 0; i < entries.length; i++) { + await this.rm(joinPath(filePath, entries[i]), options); + } + await provider.rmdir(toProviderPath(filePath)); + } else { + await provider.unlink(toProviderPath(filePath)); + } + }, + + async truncate(filePath, len = 0) { + const providerPath = toProviderPath(filePath); + const handle = await provider.open(providerPath, 'r+'); + try { + await handle.truncate(len); + } finally { + await handle.close(); + } + }, + + async link(existingPath, newPath) { + const existingProviderPath = toProviderPath(existingPath); + const newProviderPath = toProviderPath(newPath); + return provider.link(existingProviderPath, newProviderPath); + }, + + async mkdtemp(prefix) { + const providerPrefix = toProviderPath(prefix); + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let suffix = ''; + for (let i = 0; i < 6; i++) { + suffix += chars[(MathRandom() * chars.length) | 0]; + } + const dirPath = providerPrefix + suffix; + await provider.mkdir(dirPath); + return dirPath; + }, + + async chmod(filePath, mode) { + const providerPath = toProviderPath(filePath); + provider.chmodSync(providerPath, mode); + }, + + async chown(filePath, uid, gid) { + const providerPath = toProviderPath(filePath); + provider.chownSync(providerPath, uid, gid); + }, + + async lchown(filePath, uid, gid) { + const providerPath = toProviderPath(filePath); + provider.chownSync(providerPath, uid, gid); + }, + + async utimes(filePath, atime, mtime) { + const providerPath = toProviderPath(filePath); + provider.utimesSync(providerPath, atime, mtime); + }, + + async lutimes(filePath, atime, mtime) { + const providerPath = toProviderPath(filePath); + provider.lutimesSync(providerPath, atime, mtime); + }, + + async open(filePath, flags, mode) { + const providerPath = toProviderPath(filePath); + const handle = provider.openSync(providerPath, flags, mode); + return openVirtualFd(handle); + }, + + async lchmod(filePath, mode) { + const providerPath = toProviderPath(filePath); + provider.chmodSync(providerPath, mode); + }, + + watch(filePath, options) { + const providerPath = toProviderPath(filePath); + return provider.watchAsync(providerPath, options); + }, + }); + } +} + +module.exports = { + VirtualFileSystem, +}; diff --git a/lib/internal/vfs/provider.js b/lib/internal/vfs/provider.js new file mode 100644 index 00000000000000..32c238a23fe510 --- /dev/null +++ b/lib/internal/vfs/provider.js @@ -0,0 +1,618 @@ +'use strict'; + +const { + ERR_METHOD_NOT_IMPLEMENTED, +} = require('internal/errors').codes; + +const { + createEROFS, + createEEXIST, + createEACCES, +} = require('internal/vfs/errors'); + +const { + fs: { + R_OK, + W_OK, + X_OK, + COPYFILE_EXCL, + }, +} = internalBinding('constants'); + +/** + * Base class for VFS providers. + * Providers implement the essential primitives that the VFS delegates to. + * + * Implementations must override the essential primitives (open, stat, readdir, etc.) + * Default implementations for derived methods (readFile, writeFile, etc.) are provided. + */ +class VirtualProvider { + // === CAPABILITY FLAGS === + + /** + * Returns true if this provider is read-only. + * @returns {boolean} + */ + get readonly() { + return false; + } + + /** + * Returns true if this provider supports symbolic links. + * @returns {boolean} + */ + get supportsSymlinks() { + return false; + } + + /** + * Returns true if this provider supports file watching. + * @returns {boolean} + */ + get supportsWatch() { + return false; + } + + // === ESSENTIAL PRIMITIVES (must be implemented by subclasses) === + + /** + * Opens a file and returns a file handle. + * @param {string} path The file path (relative to provider root) + * @param {string} flags The open flags ('r', 'r+', 'w', 'w+', 'a', 'a+') + * @param {number} [mode] The file mode (for creating files) + * @returns {Promise} + */ + async open(path, flags, mode) { + throw new ERR_METHOD_NOT_IMPLEMENTED('open'); + } + + /** + * Opens a file synchronously and returns a file handle. + * @param {string} path The file path (relative to provider root) + * @param {string} flags The open flags ('r', 'r+', 'w', 'w+', 'a', 'a+') + * @param {number} [mode] The file mode (for creating files) + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + openSync(path, flags, mode) { + throw new ERR_METHOD_NOT_IMPLEMENTED('openSync'); + } + + /** + * Gets stats for a path. + * @param {string} path The path to stat + * @param {object} [options] Options + * @returns {Promise} + */ + async stat(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('stat'); + } + + /** + * Gets stats for a path synchronously. + * @param {string} path The path to stat + * @param {object} [options] Options + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + statSync(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('statSync'); + } + + /** + * Gets stats for a path without following symlinks. + * @param {string} path The path to stat + * @param {object} [options] Options + * @returns {Promise} + */ + async lstat(path, options) { + // Default: same as stat (for providers that don't support symlinks) + return this.stat(path, options); + } + + /** + * Gets stats for a path synchronously without following symlinks. + * @param {string} path The path to stat + * @param {object} [options] Options + * @returns {Stats} + */ + lstatSync(path, options) { + // Default: same as statSync (for providers that don't support symlinks) + return this.statSync(path, options); + } + + /** + * Reads directory contents. + * @param {string} path The directory path + * @param {object} [options] Options + * @returns {Promise} + */ + async readdir(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('readdir'); + } + + /** + * Reads directory contents synchronously. + * @param {string} path The directory path + * @param {object} [options] Options + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + readdirSync(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('readdirSync'); + } + + /** + * Creates a directory. + * @param {string} path The directory path + * @param {object} [options] Options + * @returns {Promise} + */ + async mkdir(path, options) { + if (this.readonly) { + throw createEROFS('mkdir', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('mkdir'); + } + + /** + * Creates a directory synchronously. + * @param {string} path The directory path + * @param {object} [options] Options + */ + mkdirSync(path, options) { + if (this.readonly) { + throw createEROFS('mkdir', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('mkdirSync'); + } + + /** + * Removes a directory. + * @param {string} path The directory path + * @returns {Promise} + */ + async rmdir(path) { + if (this.readonly) { + throw createEROFS('rmdir', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('rmdir'); + } + + /** + * Removes a directory synchronously. + * @param {string} path The directory path + */ + rmdirSync(path) { + if (this.readonly) { + throw createEROFS('rmdir', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('rmdirSync'); + } + + /** + * Removes a file. + * @param {string} path The file path + * @returns {Promise} + */ + async unlink(path) { + if (this.readonly) { + throw createEROFS('unlink', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('unlink'); + } + + /** + * Removes a file synchronously. + * @param {string} path The file path + */ + unlinkSync(path) { + if (this.readonly) { + throw createEROFS('unlink', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('unlinkSync'); + } + + /** + * Renames a file or directory. + * @param {string} oldPath The old path + * @param {string} newPath The new path + * @returns {Promise} + */ + async rename(oldPath, newPath) { + if (this.readonly) { + throw createEROFS('rename', oldPath); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('rename'); + } + + /** + * Renames a file or directory synchronously. + * @param {string} oldPath The old path + * @param {string} newPath The new path + */ + renameSync(oldPath, newPath) { + if (this.readonly) { + throw createEROFS('rename', oldPath); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('renameSync'); + } + + // === DEFAULT IMPLEMENTATIONS (built on primitives) === + + /** + * Reads a file. + * @param {string} path The file path + * @param {object|string} [options] Options or encoding + * @returns {Promise} + */ + async readFile(path, options) { + const flag = (typeof options === 'object' && options !== null) ? + (options.flag ?? 'r') : 'r'; + const handle = await this.open(path, flag); + try { + return await handle.readFile(options); + } finally { + await handle.close(); + } + } + + /** + * Reads a file synchronously. + * @param {string} path The file path + * @param {object|string} [options] Options or encoding + * @returns {Buffer|string} + */ + readFileSync(path, options) { + const flag = (typeof options === 'object' && options !== null) ? + (options.flag ?? 'r') : 'r'; + const handle = this.openSync(path, flag); + try { + return handle.readFileSync(options); + } finally { + handle.closeSync(); + } + } + + /** + * Writes a file. + * @param {string} path The file path + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + * @returns {Promise} + */ + async writeFile(path, data, options) { + if (this.readonly) { + throw createEROFS('open', path); + } + const flag = options?.flag ?? 'w'; + const handle = await this.open(path, flag, options?.mode); + try { + await handle.writeFile(data, options); + } finally { + await handle.close(); + } + } + + /** + * Writes a file synchronously. + * @param {string} path The file path + * @param {Buffer|string} data The data to write + * @param {object} [options] Options + */ + writeFileSync(path, data, options) { + if (this.readonly) { + throw createEROFS('open', path); + } + const flag = options?.flag ?? 'w'; + const handle = this.openSync(path, flag, options?.mode); + try { + handle.writeFileSync(data, options); + } finally { + handle.closeSync(); + } + } + + /** + * Appends to a file. + * @param {string} path The file path + * @param {Buffer|string} data The data to append + * @param {object} [options] Options + * @returns {Promise} + */ + async appendFile(path, data, options) { + if (this.readonly) { + throw createEROFS('open', path); + } + const flag = options?.flag ?? 'a'; + const handle = await this.open(path, flag, options?.mode); + try { + await handle.writeFile(data, options); + } finally { + await handle.close(); + } + } + + /** + * Appends to a file synchronously. + * @param {string} path The file path + * @param {Buffer|string} data The data to append + * @param {object} [options] Options + */ + appendFileSync(path, data, options) { + if (this.readonly) { + throw createEROFS('open', path); + } + const flag = options?.flag ?? 'a'; + const handle = this.openSync(path, flag, options?.mode); + try { + handle.writeFileSync(data, options); + } finally { + handle.closeSync(); + } + } + + /** + * Checks if a path exists. + * @param {string} path The path to check + * @returns {Promise} + */ + async exists(path) { + try { + await this.stat(path); + return true; + } catch { + return false; + } + } + + /** + * Checks if a path exists synchronously. + * @param {string} path The path to check + * @returns {boolean} + */ + existsSync(path) { + try { + this.statSync(path); + return true; + } catch { + return false; + } + } + + /** + * Copies a file. + * @param {string} src Source path + * @param {string} dest Destination path + * @param {number} [mode] Copy mode flags + * @returns {Promise} + */ + async copyFile(src, dest, mode) { + if (this.readonly) { + throw createEROFS('copyfile', dest); + } + if ((mode & COPYFILE_EXCL) !== 0) { + if (await this.exists(dest)) { + throw createEEXIST('copyfile', dest); + } + } + const content = await this.readFile(src); + await this.writeFile(dest, content); + } + + /** + * Copies a file synchronously. + * @param {string} src Source path + * @param {string} dest Destination path + * @param {number} [mode] Copy mode flags + */ + copyFileSync(src, dest, mode) { + if (this.readonly) { + throw createEROFS('copyfile', dest); + } + if ((mode & COPYFILE_EXCL) !== 0) { + if (this.existsSync(dest)) { + throw createEEXIST('copyfile', dest); + } + } + const content = this.readFileSync(src); + this.writeFileSync(dest, content); + } + + /** + * Gets the real path by resolving symlinks. + * @param {string} path The path + * @param {object} [options] Options + * @returns {Promise} + */ + async realpath(path, options) { + // Default: return the path as-is (for providers without symlinks) + // First verify the path exists + await this.stat(path); + return path; + } + + /** + * Gets the real path synchronously. + * @param {string} path The path + * @param {object} [options] Options + * @returns {string} + */ + realpathSync(path, options) { + // Default: return the path as-is (for providers without symlinks) + // First verify the path exists + this.statSync(path); + return path; + } + + /** + * Checks file accessibility. + * @param {string} path The path to check + * @param {number} [mode] Access mode + * @returns {Promise} + */ + async access(path, mode) { + const stats = await this.stat(path); + this.#checkAccessMode(path, stats, mode); + } + + /** + * Checks file accessibility synchronously. + * @param {string} path The path to check + * @param {number} [mode] Access mode + */ + accessSync(path, mode) { + const stats = this.statSync(path); + this.#checkAccessMode(path, stats, mode); + } + + /** + * Checks access mode bits against file stats. + * @param {string} path The path (for error messages) + * @param {Stats} stats The file stats + * @param {number} mode The requested access mode + */ + #checkAccessMode(path, stats, mode) { + if (mode == null || mode === 0) return; // F_OK = 0, existence-only check + + const fileMode = stats.mode & 0o777; // Permission bits + // Check owner permissions (simplified: treat VFS user as owner) + if ((mode & R_OK) !== 0 && (fileMode & 0o400) === 0) { + throw createEACCES('access', path); + } + if ((mode & W_OK) !== 0 && (fileMode & 0o200) === 0) { + throw createEACCES('access', path); + } + if ((mode & X_OK) !== 0 && (fileMode & 0o100) === 0) { + throw createEACCES('access', path); + } + } + + // === HARD LINK OPERATIONS (optional) === + + /** + * Creates a hard link. + * @param {string} existingPath The existing file path + * @param {string} newPath The new link path + * @returns {Promise} + */ + async link(existingPath, newPath) { + if (this.readonly) { + throw createEROFS('link', newPath); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('link'); + } + + /** + * Creates a hard link synchronously. + * @param {string} existingPath The existing file path + * @param {string} newPath The new link path + */ + linkSync(existingPath, newPath) { + if (this.readonly) { + throw createEROFS('link', newPath); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('linkSync'); + } + + // === SYMLINK OPERATIONS (optional, throw ENOENT by default) === + + /** + * Reads the target of a symbolic link. + * @param {string} path The symlink path + * @param {object} [options] Options + * @returns {Promise} + */ + async readlink(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('readlink'); + } + + /** + * Reads the target of a symbolic link synchronously. + * @param {string} path The symlink path + * @param {object} [options] Options + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass + */ + readlinkSync(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('readlinkSync'); + } + + /** + * Creates a symbolic link. + * @param {string} target The symlink target + * @param {string} path The symlink path + * @param {string} [type] The symlink type (file, dir, junction) + * @returns {Promise} + */ + async symlink(target, path, type) { + if (this.readonly) { + throw createEROFS('symlink', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('symlink'); + } + + /** + * Creates a symbolic link synchronously. + * @param {string} target The symlink target + * @param {string} path The symlink path + * @param {string} [type] The symlink type (file, dir, junction) + */ + symlinkSync(target, path, type) { + if (this.readonly) { + throw createEROFS('symlink', path); + } + throw new ERR_METHOD_NOT_IMPLEMENTED('symlinkSync'); + } + + // === WATCH OPERATIONS (optional, polling-based) === + + /** + * Watches a file or directory for changes. + * Returns an EventEmitter-like object that emits 'change' and 'close' events. + * @param {string} path The path to watch + * @param {object} [options] Watch options + * @param {number} [options.interval] Polling interval in ms (default: 100) + * @param {boolean} [options.recursive] Watch subdirectories (default: false) + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass + */ + watch(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('watch'); + } + + /** + * Watches a file or directory for changes (async iterable version). + * Used by fs.promises.watch(). + * @param {string} path The path to watch + * @param {object} [options] Watch options + * @param {number} [options.interval] Polling interval in ms (default: 100) + * @param {boolean} [options.recursive] Watch subdirectories (default: false) + * @param {AbortSignal} [options.signal] AbortSignal for cancellation + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass + */ + watchAsync(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('watchAsync'); + } + + /** + * Watches a file for changes using stat polling. + * Returns a StatWatcher-like object that emits 'change' events with stats. + * @param {string} path The path to watch + * @param {object} [options] Watch options + * @param {number} [options.interval] Polling interval in ms (default: 5007) + * @param {boolean} [options.persistent] Whether the watcher should prevent exit + * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass + */ + watchFile(path, options) { + throw new ERR_METHOD_NOT_IMPLEMENTED('watchFile'); + } + + /** + * Stops watching a file for changes. + * @param {string} path The path to stop watching + * @param {Function} [listener] Optional listener to remove + */ + unwatchFile(path, listener) { + throw new ERR_METHOD_NOT_IMPLEMENTED('unwatchFile'); + } +} + +module.exports = { + VirtualProvider, +}; diff --git a/lib/internal/vfs/providers/memory.js b/lib/internal/vfs/providers/memory.js new file mode 100644 index 00000000000000..5fc18ccdd2b517 --- /dev/null +++ b/lib/internal/vfs/providers/memory.js @@ -0,0 +1,1023 @@ +'use strict'; + +const { + ArrayFrom, + ArrayPrototypePush, + DateNow, + SafeMap, + StringPrototypeReplaceAll, + Symbol, +} = primordials; + +const { Buffer } = require('buffer'); +const { isPromise } = require('util/types'); +const { posix: pathPosix } = require('path'); +const { VirtualProvider } = require('internal/vfs/provider'); +const { MemoryFileHandle } = require('internal/vfs/file_handle'); +const { + VFSWatcher, + VFSStatWatcher, + VFSWatchAsyncIterable, +} = require('internal/vfs/watcher'); +const { + codes: { + ERR_INVALID_STATE, + }, +} = require('internal/errors'); +const { + createENOENT, + createENOTDIR, + createENOTEMPTY, + createEISDIR, + createEEXIST, + createEINVAL, + createELOOP, + createEROFS, +} = require('internal/vfs/errors'); +const { + createFileStats, + createDirectoryStats, + createSymlinkStats, +} = require('internal/vfs/stats'); +const { Dirent } = require('internal/fs/utils'); +const { kEmptyObject } = require('internal/util'); +const { + fs: { + O_APPEND, + O_CREAT, + O_EXCL, + O_RDWR, + O_TRUNC, + O_WRONLY, + UV_DIRENT_FILE, + UV_DIRENT_DIR, + UV_DIRENT_LINK, + }, +} = internalBinding('constants'); + +/** + * Converts numeric flags to a string representation. + * If already a string, returns as-is. + * @param {string|number} flags The flags to normalize + * @returns {string} Normalized string flags + */ +function normalizeFlags(flags) { + if (typeof flags === 'string') return flags; + if (typeof flags !== 'number') return 'r'; + + const rdwr = (flags & O_RDWR) !== 0; + const append = (flags & O_APPEND) !== 0; + const excl = (flags & O_EXCL) !== 0; + const write = (flags & O_WRONLY) !== 0 || + (flags & O_CREAT) !== 0 || + (flags & O_TRUNC) !== 0; + + if (append) { + return 'a' + (excl ? 'x' : '') + (rdwr ? '+' : ''); + } + if (write) { + return 'w' + (excl ? 'x' : '') + (rdwr ? '+' : ''); + } + if (rdwr) return 'r+'; + return 'r'; +} + +/** + * Converts a time argument (Date, number, or string) to milliseconds. + * Numbers are treated as seconds (matching Node.js utimes convention). + * @param {Date|number|string} time The time value + * @returns {number} Milliseconds since epoch + */ +function toMs(time) { + if (typeof time === 'number') return time * 1000; + if (typeof time === 'string') return DateNow(); // Fallback for string timestamps + if (typeof time === 'object' && time !== null) return +time; + return time; +} + +// Private symbols +const kRoot = Symbol('kRoot'); +const kReadonly = Symbol('kReadonly'); +const kStatWatchers = Symbol('kStatWatchers'); + +// Entry types +const TYPE_FILE = 0; +const TYPE_DIR = 1; +const TYPE_SYMLINK = 2; + +// Maximum symlink resolution depth +const kMaxSymlinkDepth = 40; + +/** + * Internal entry representation for MemoryProvider. + */ +class MemoryEntry { + constructor(type, options = kEmptyObject) { + this.type = type; + this.mode = options.mode ?? (type === TYPE_DIR ? 0o755 : 0o644); + this.content = null; // For files - static Buffer content + this.contentProvider = null; // For files - dynamic content function + this.target = null; // For symlinks + this.children = null; // For directories + this.populate = null; // For directories - lazy population callback + this.populated = true; // For directories - has populate been called? + this.nlink = 1; + this.uid = 0; + this.gid = 0; + const now = DateNow(); + this.atime = now; + this.mtime = now; + this.ctime = now; + this.birthtime = now; + } + + /** + * Gets the file content synchronously. + * Throws if the content provider returns a Promise. + * @returns {Buffer} The file content + */ + getContentSync() { + if (this.contentProvider !== null) { + const result = this.contentProvider(); + if (isPromise(result)) { + // It's a Promise - can't use sync API + throw new ERR_INVALID_STATE('cannot use sync API with async content provider'); + } + return typeof result === 'string' ? Buffer.from(result) : result; + } + return this.content; + } + + /** + * Gets the file content asynchronously. + * @returns {Promise} The file content + */ + async getContentAsync() { + if (this.contentProvider !== null) { + const result = await this.contentProvider(); + return typeof result === 'string' ? Buffer.from(result) : result; + } + return this.content; + } + + /** + * Returns true if this file has a dynamic content provider. + * @returns {boolean} + */ + isDynamic() { + return this.contentProvider !== null; + } + + isFile() { + return this.type === TYPE_FILE; + } + + isDirectory() { + return this.type === TYPE_DIR; + } + + isSymbolicLink() { + return this.type === TYPE_SYMLINK; + } +} + +/** + * In-memory filesystem provider. + * Supports full read/write operations. + */ +class MemoryProvider extends VirtualProvider { + constructor() { + super(); + // Root directory + this[kRoot] = new MemoryEntry(TYPE_DIR); + this[kRoot].children = new SafeMap(); + this[kReadonly] = false; + // Map of path -> VFSStatWatcher for watchFile + this[kStatWatchers] = new SafeMap(); + } + + get readonly() { + return this[kReadonly]; + } + + get supportsWatch() { + return true; + } + + /** + * Sets the provider to read-only mode. + * Once set to read-only, the provider cannot be changed back to writable. + * This is useful for finalizing a VFS after initial population. + */ + setReadOnly() { + this[kReadonly] = true; + } + + get supportsSymlinks() { + return true; + } + + /** + * Normalizes a path to use forward slashes, removes trailing slash, + * and resolves . and .. components. + * @param {string} path The path to normalize + * @returns {string} Normalized path + */ + #normalizePath(path) { + // Convert backslashes to forward slashes + let normalized = StringPrototypeReplaceAll(path, '\\', '/'); + // Ensure absolute path + if (normalized[0] !== '/') { + normalized = '/' + normalized; + } + // Use path.posix.normalize to resolve . and .. + return pathPosix.normalize(normalized); + } + + /** + * Splits a path into segments. + * @param {string} path Normalized path + * @returns {string[]} Path segments + */ + #splitPath(path) { + if (path === '/') { + return []; + } + return path.slice(1).split('/'); + } + + + /** + * Resolves a symlink target to an absolute path. + * @param {string} symlinkPath The path of the symlink + * @param {string} target The symlink target + * @returns {string} Resolved absolute path + */ + #resolveSymlinkTarget(symlinkPath, target) { + if (target.startsWith('/')) { + return this.#normalizePath(target); + } + // Relative target: resolve against symlink's parent directory + const parentPath = pathPosix.dirname(symlinkPath); + return this.#normalizePath(pathPosix.join(parentPath, target)); + } + + /** + * Looks up an entry by path, optionally following symlinks. + * @param {string} path The path to look up + * @param {boolean} followSymlinks Whether to follow symlinks + * @param {number} depth Current symlink resolution depth + * @returns {{ entry: MemoryEntry|null, resolvedPath: string|null, eloop?: boolean }} + */ + #lookupEntry(path, followSymlinks = true, depth = 0) { + const normalized = this.#normalizePath(path); + + if (normalized === '/') { + return { entry: this[kRoot], resolvedPath: '/' }; + } + + const segments = this.#splitPath(normalized); + let current = this[kRoot]; + let currentPath = '/'; + + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + + // Always follow symlinks for intermediate path components + if (current.isSymbolicLink()) { + if (depth >= kMaxSymlinkDepth) { + return { entry: null, resolvedPath: null, eloop: true }; + } + const targetPath = this.#resolveSymlinkTarget(currentPath, current.target); + const result = this.#lookupEntry(targetPath, true, depth + 1); + if (result.eloop) { + return result; + } + if (!result.entry) { + return { entry: null, resolvedPath: null }; + } + current = result.entry; + currentPath = result.resolvedPath; + } + + if (!current.isDirectory()) { + return { entry: null, resolvedPath: null }; + } + + // Ensure directory is populated before accessing children + this.#ensurePopulated(current, currentPath); + + const entry = current.children.get(segment); + if (!entry) { + return { entry: null, resolvedPath: null }; + } + + currentPath = pathPosix.join(currentPath, segment); + current = entry; + } + + // Follow symlink at the end if requested + if (current.isSymbolicLink() && followSymlinks) { + if (depth >= kMaxSymlinkDepth) { + return { entry: null, resolvedPath: null, eloop: true }; + } + const targetPath = this.#resolveSymlinkTarget(currentPath, current.target); + return this.#lookupEntry(targetPath, true, depth + 1); + } + + return { entry: current, resolvedPath: currentPath }; + } + + /** + * Gets an entry by path, throwing if not found. + * @param {string} path The path + * @param {string} syscall The syscall name for error + * @param {boolean} followSymlinks Whether to follow symlinks + * @returns {MemoryEntry} + */ + #getEntry(path, syscall, followSymlinks = true) { + const result = this.#lookupEntry(path, followSymlinks); + if (result.eloop) { + throw createELOOP(syscall, path); + } + if (!result.entry) { + throw createENOENT(syscall, path); + } + return result.entry; + } + + /** + * Ensures parent directories exist, optionally creating them. + * @param {string} path The full path + * @param {boolean} create Whether to create missing directories + * @param {string} syscall The syscall name for errors + * @returns {MemoryEntry} The parent directory entry + */ + #ensureParent(path, create, syscall) { + if (path === '/') { + return this[kRoot]; + } + const parentPath = pathPosix.dirname(path); + + const segments = this.#splitPath(parentPath); + let current = this[kRoot]; + + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + const currentPath = pathPosix.join('/', ...segments.slice(0, i)); + + // Follow symlinks in parent path + if (current.isSymbolicLink()) { + const targetPath = this.#resolveSymlinkTarget(currentPath, current.target); + const result = this.#lookupEntry(targetPath, true, 0); + if (!result.entry) { + throw createENOENT(syscall, path); + } + current = result.entry; + } + + if (!current.isDirectory()) { + throw createENOTDIR(syscall, path); + } + + // Ensure directory is populated before accessing children + this.#ensurePopulated(current, currentPath); + + let entry = current.children.get(segment); + if (!entry) { + if (create) { + entry = new MemoryEntry(TYPE_DIR); + entry.children = new SafeMap(); + current.children.set(segment, entry); + } else { + throw createENOENT(syscall, path); + } + } + current = entry; + } + + // Follow symlinks on the final parent entry + if (current.isSymbolicLink()) { + const targetPath = this.#resolveSymlinkTarget(parentPath, current.target); + const result = this.#lookupEntry(targetPath, true, 0); + if (!result.entry) { + throw createENOENT(syscall, path); + } + current = result.entry; + } + + if (!current.isDirectory()) { + throw createENOTDIR(syscall, path); + } + + // Ensure final directory is populated + this.#ensurePopulated(current, parentPath); + + return current; + } + + /** + * Creates stats for an entry. + * @param {MemoryEntry} entry The entry + * @param {number} [size] Override size for files + * @returns {Stats} + */ + #createStats(entry, size, bigint) { + const options = { + mode: entry.mode, + nlink: entry.nlink, + uid: entry.uid, + gid: entry.gid, + atimeMs: entry.atime, + mtimeMs: entry.mtime, + ctimeMs: entry.ctime, + birthtimeMs: entry.birthtime, + bigint, + }; + + if (entry.isFile()) { + let fileSize = size; + if (fileSize === undefined) { + fileSize = entry.isDynamic() ? + entry.getContentSync().length : + entry.content.length; + } + return createFileStats(fileSize, options); + } else if (entry.isDirectory()) { + return createDirectoryStats(options); + } else if (entry.isSymbolicLink()) { + return createSymlinkStats(entry.target.length, options); + } + + throw new ERR_INVALID_STATE('Unknown entry type'); + } + + /** + * Ensures a directory is populated by calling its populate callback if needed. + * @param {MemoryEntry} entry The directory entry + * @param {string} path The directory path (for error messages and scoped VFS) + */ + #ensurePopulated(entry, path) { + if (entry.isDirectory() && !entry.populated && entry.populate) { + // Create a scoped VFS for the populate callback + const scopedVfs = { + addFile: (name, content, opts) => { + const fileEntry = new MemoryEntry(TYPE_FILE, opts); + if (typeof content === 'function') { + fileEntry.content = Buffer.alloc(0); + fileEntry.contentProvider = content; + } else { + fileEntry.content = typeof content === 'string' ? Buffer.from(content) : content; + } + entry.children.set(name, fileEntry); + }, + addDirectory: (name, populate, opts) => { + const dirEntry = new MemoryEntry(TYPE_DIR, opts); + dirEntry.children = new SafeMap(); + if (typeof populate === 'function') { + dirEntry.populate = populate; + dirEntry.populated = false; + } + entry.children.set(name, dirEntry); + }, + addSymlink: (name, target, opts) => { + const symlinkEntry = new MemoryEntry(TYPE_SYMLINK, opts); + symlinkEntry.target = target; + entry.children.set(name, symlinkEntry); + }, + }; + entry.populate(scopedVfs); + entry.populated = true; + } + } + + openSync(path, flags, mode) { + const normalized = this.#normalizePath(path); + + // Normalize numeric flags to string + flags = normalizeFlags(flags); + + // Handle create and exclusive modes + const isCreate = flags === 'w' || flags === 'w+' || + flags === 'a' || flags === 'a+' || + flags === 'wx' || flags === 'wx+' || + flags === 'ax' || flags === 'ax+'; + const isExclusive = flags === 'wx' || flags === 'wx+' || + flags === 'ax' || flags === 'ax+'; + const isWritable = flags !== 'r'; + + // Check readonly for any writable mode + if (this.readonly && isWritable) { + throw createEROFS('open', path); + } + + let entry; + try { + entry = this.#getEntry(normalized, 'open'); + // Exclusive flag: file must not exist + if (isExclusive) { + throw createEEXIST('open', path); + } + } catch (err) { + if (err.code !== 'ENOENT' || !isCreate) throw err; + // Create the file + const parent = this.#ensureParent(normalized, false, 'open'); + const name = pathPosix.basename(normalized); + entry = new MemoryEntry(TYPE_FILE, { mode }); + entry.content = Buffer.alloc(0); + parent.children.set(name, entry); + const now = DateNow(); + parent.mtime = now; + parent.ctime = now; + } + + if (entry.isDirectory()) { + throw createEISDIR('open', path); + } + + if (entry.isSymbolicLink()) { + // Should have been resolved already, but just in case + throw createEINVAL('open', path); + } + + const getStats = (size) => this.#createStats(entry, size); + return new MemoryFileHandle(normalized, flags, mode ?? entry.mode, entry.content, entry, getStats); + } + + async open(path, flags, mode) { + return this.openSync(path, flags, mode); + } + + statSync(path, options) { + const entry = this.#getEntry(path, 'stat', true); + return this.#createStats(entry, undefined, options?.bigint); + } + + async stat(path, options) { + return this.statSync(path, options); + } + + lstatSync(path, options) { + const entry = this.#getEntry(path, 'lstat', false); + return this.#createStats(entry, undefined, options?.bigint); + } + + async lstat(path, options) { + return this.lstatSync(path, options); + } + + readdirSync(path, options) { + const entry = this.#getEntry(path, 'scandir', true); + if (!entry.isDirectory()) { + throw createENOTDIR('scandir', path); + } + + // Ensure directory is populated (for lazy population) + this.#ensurePopulated(entry, path); + + const normalized = this.#normalizePath(path); + const withFileTypes = options?.withFileTypes === true; + const recursive = options?.recursive === true; + + if (recursive) { + return this.#readdirRecursive(entry, normalized, withFileTypes); + } + + if (withFileTypes) { + const dirents = []; + for (const { 0: name, 1: childEntry } of entry.children) { + let type; + if (childEntry.isSymbolicLink()) { + type = UV_DIRENT_LINK; + } else if (childEntry.isDirectory()) { + type = UV_DIRENT_DIR; + } else { + type = UV_DIRENT_FILE; + } + ArrayPrototypePush(dirents, new Dirent(name, type, normalized)); + } + return dirents; + } + + return ArrayFrom(entry.children.keys()); + } + + /** + * Recursively reads directory contents. + * @param {MemoryEntry} dirEntry The directory entry + * @param {string} dirPath The normalized directory path + * @param {boolean} withFileTypes Whether to return Dirent objects + * @returns {string[]|Dirent[]} + */ + #readdirRecursive(dirEntry, dirPath, withFileTypes) { + const results = []; + + const walk = (entry, currentPath, relativePath) => { + this.#ensurePopulated(entry, currentPath); + + for (const { 0: name, 1: childEntry } of entry.children) { + const childRelative = relativePath ? + relativePath + '/' + name : name; + + if (withFileTypes) { + let type; + if (childEntry.isSymbolicLink()) { + type = UV_DIRENT_LINK; + } else if (childEntry.isDirectory()) { + type = UV_DIRENT_DIR; + } else { + type = UV_DIRENT_FILE; + } + ArrayPrototypePush(results, + new Dirent(childRelative, type, dirPath)); + } else { + ArrayPrototypePush(results, childRelative); + } + + // Follow symlinks to directories for recursive traversal + let resolvedChild = childEntry; + if (childEntry.isSymbolicLink()) { + const targetPath = this.#resolveSymlinkTarget( + pathPosix.join(currentPath, name), childEntry.target, + ); + const result = this.#lookupEntry(targetPath, true, 0); + if (result.entry) { + resolvedChild = result.entry; + } + } + if (resolvedChild.isDirectory()) { + const childPath = pathPosix.join(currentPath, name); + walk(resolvedChild, childPath, childRelative); + } + } + }; + + walk(dirEntry, dirPath, ''); + return results; + } + + async readdir(path, options) { + return this.readdirSync(path, options); + } + + mkdirSync(path, options) { + if (this.readonly) { + throw createEROFS('mkdir', path); + } + + const normalized = this.#normalizePath(path); + const recursive = options?.recursive === true; + + // Check if already exists + const existing = this.#lookupEntry(normalized, true); + if (existing.entry) { + if (existing.entry.isDirectory() && recursive) { + // Already exists, that's ok for recursive + return undefined; + } + throw createEEXIST('mkdir', path); + } + + if (recursive) { + // Create all parent directories + const segments = this.#splitPath(normalized); + let current = this[kRoot]; + let currentPath = '/'; + let firstCreated; + + for (const segment of segments) { + currentPath = pathPosix.join(currentPath, segment); + let entry = current.children.get(segment); + if (!entry) { + entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode }); + entry.children = new SafeMap(); + current.children.set(segment, entry); + if (firstCreated === undefined) { + firstCreated = currentPath; + } + } else if (!entry.isDirectory()) { + throw createENOTDIR('mkdir', path); + } + current = entry; + } + return firstCreated; + } + + const parent = this.#ensureParent(normalized, false, 'mkdir'); + const name = pathPosix.basename(normalized); + const entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode }); + entry.children = new SafeMap(); + parent.children.set(name, entry); + const now = DateNow(); + parent.mtime = now; + parent.ctime = now; + return undefined; + } + + async mkdir(path, options) { + return this.mkdirSync(path, options); + } + + rmdirSync(path) { + if (this.readonly) { + throw createEROFS('rmdir', path); + } + + const normalized = this.#normalizePath(path); + const entry = this.#getEntry(normalized, 'rmdir', false); + + if (!entry.isDirectory()) { + throw createENOTDIR('rmdir', path); + } + + if (entry.children.size > 0) { + throw createENOTEMPTY('rmdir', path); + } + + const parent = this.#ensureParent(normalized, false, 'rmdir'); + const name = pathPosix.basename(normalized); + parent.children.delete(name); + const now = DateNow(); + parent.mtime = now; + parent.ctime = now; + } + + async rmdir(path) { + this.rmdirSync(path); + } + + unlinkSync(path) { + if (this.readonly) { + throw createEROFS('unlink', path); + } + + const normalized = this.#normalizePath(path); + const entry = this.#getEntry(normalized, 'unlink', false); + + if (entry.isDirectory()) { + throw createEISDIR('unlink', path); + } + + const parent = this.#ensureParent(normalized, false, 'unlink'); + const name = pathPosix.basename(normalized); + parent.children.delete(name); + entry.nlink--; + const now = DateNow(); + parent.mtime = now; + parent.ctime = now; + } + + async unlink(path) { + this.unlinkSync(path); + } + + renameSync(oldPath, newPath) { + if (this.readonly) { + throw createEROFS('rename', oldPath); + } + + const normalizedOld = this.#normalizePath(oldPath); + const normalizedNew = this.#normalizePath(newPath); + + // Get the entry (without following symlinks for the entry itself) + const entry = this.#getEntry(normalizedOld, 'rename', false); + + // Validate destination parent exists (do not auto-create) + const newParent = this.#ensureParent(normalizedNew, false, 'rename'); + const newName = pathPosix.basename(normalizedNew); + + // Check if destination exists + const existingDest = newParent.children.get(newName); + if (existingDest) { + // Cannot overwrite a directory with a non-directory + if (existingDest.isDirectory() && !entry.isDirectory()) { + throw createEISDIR('rename', newPath); + } + // Cannot overwrite a non-directory with a directory + if (!existingDest.isDirectory() && entry.isDirectory()) { + throw createENOTDIR('rename', newPath); + } + } + + // Remove from old location (after destination validation) + const oldParent = this.#ensureParent(normalizedOld, false, 'rename'); + const oldName = pathPosix.basename(normalizedOld); + oldParent.children.delete(oldName); + + // Add to new location + newParent.children.set(newName, entry); + + const now = DateNow(); + oldParent.mtime = now; + oldParent.ctime = now; + if (newParent !== oldParent) { + newParent.mtime = now; + newParent.ctime = now; + } + } + + async rename(oldPath, newPath) { + this.renameSync(oldPath, newPath); + } + + linkSync(existingPath, newPath) { + if (this.readonly) { + throw createEROFS('link', newPath); + } + + const normalizedExisting = this.#normalizePath(existingPath); + const normalizedNew = this.#normalizePath(newPath); + + const entry = this.#getEntry(normalizedExisting, 'link', true); + if (!entry.isFile()) { + // Hard links to directories are not supported + throw createEINVAL('link', existingPath); + } + + // Check if new path already exists + const existing = this.#lookupEntry(normalizedNew, false); + if (existing.entry) { + throw createEEXIST('link', newPath); + } + + const parent = this.#ensureParent(normalizedNew, false, 'link'); + const name = pathPosix.basename(normalizedNew); + // Hard link: same entry object referenced by both names + parent.children.set(name, entry); + entry.nlink++; + const now = DateNow(); + parent.mtime = now; + parent.ctime = now; + } + + async link(existingPath, newPath) { + this.linkSync(existingPath, newPath); + } + + readlinkSync(path, options) { + const normalized = this.#normalizePath(path); + const entry = this.#getEntry(normalized, 'readlink', false); + + if (!entry.isSymbolicLink()) { + throw createEINVAL('readlink', path); + } + + return entry.target; + } + + async readlink(path, options) { + return this.readlinkSync(path, options); + } + + symlinkSync(target, path, type) { + if (this.readonly) { + throw createEROFS('symlink', path); + } + + const normalized = this.#normalizePath(path); + + // Check if already exists + const existing = this.#lookupEntry(normalized, false); + if (existing.entry) { + throw createEEXIST('symlink', path); + } + + const parent = this.#ensureParent(normalized, false, 'symlink'); + const name = pathPosix.basename(normalized); + const entry = new MemoryEntry(TYPE_SYMLINK); + entry.target = target; + parent.children.set(name, entry); + const now = DateNow(); + parent.mtime = now; + parent.ctime = now; + } + + async symlink(target, path, type) { + this.symlinkSync(target, path, type); + } + + realpathSync(path, options) { + const result = this.#lookupEntry(path, true, 0); + if (result.eloop) { + throw createELOOP('realpath', path); + } + if (!result.entry) { + throw createENOENT('realpath', path); + } + return result.resolvedPath; + } + + async realpath(path, options) { + return this.realpathSync(path, options); + } + + // === METADATA OPERATIONS === + + chmodSync(path, mode) { + const entry = this.#getEntry(path, 'chmod', true); + // Preserve file type bits, update permission bits + entry.mode = (entry.mode & ~0o7777) | (mode & 0o7777); + entry.ctime = DateNow(); + } + + chownSync(path, uid, gid) { + const entry = this.#getEntry(path, 'chown', true); + if (uid >= 0) entry.uid = uid; + if (gid >= 0) entry.gid = gid; + entry.ctime = DateNow(); + } + + utimesSync(path, atime, mtime) { + const entry = this.#getEntry(path, 'utime', true); + entry.atime = toMs(atime); + entry.mtime = toMs(mtime); + entry.ctime = DateNow(); + } + + lutimesSync(path, atime, mtime) { + const entry = this.#getEntry(path, 'utime', false); + entry.atime = toMs(atime); + entry.mtime = toMs(mtime); + entry.ctime = DateNow(); + } + + // === WATCH OPERATIONS === + + /** + * Watches a file or directory for changes. + * @param {string} path The path to watch + * @param {object} [options] Watch options + * @returns {VFSWatcher} + */ + watch(path, options) { + const normalized = this.#normalizePath(path); + return new VFSWatcher(this, normalized, options); + } + + /** + * Watches a file or directory for changes (async iterable version). + * Used by fs.promises.watch(). + * @param {string} path The path to watch + * @param {object} [options] Watch options + * @returns {VFSWatchAsyncIterable} + */ + watchAsync(path, options) { + const normalized = this.#normalizePath(path); + return new VFSWatchAsyncIterable(this, normalized, options); + } + + /** + * Watches a file for changes using stat polling. + * @param {string} path The path to watch + * @param {object} [options] Watch options + * @param {Function} [listener] Change listener + * @returns {VFSStatWatcher} + */ + watchFile(path, options, listener) { + const normalized = this.#normalizePath(path); + + // Reuse existing watcher for the same path + let watcher = this[kStatWatchers].get(normalized); + if (!watcher) { + watcher = new VFSStatWatcher(this, normalized, options); + this[kStatWatchers].set(normalized, watcher); + } + + if (listener) { + watcher.addListener('change', listener); + } + + return watcher; + } + + /** + * Stops watching a file for changes. + * @param {string} path The path to stop watching + * @param {Function} [listener] Optional listener to remove + */ + unwatchFile(path, listener) { + const normalized = this.#normalizePath(path); + const watcher = this[kStatWatchers].get(normalized); + + if (!watcher) { + return; + } + + if (listener) { + watcher.removeListener('change', listener); + } else { + // Remove all listeners + watcher.removeAllListeners('change'); + } + + // If no more listeners, stop and remove the watcher + if (watcher.hasNoListeners()) { + watcher.stop(); + this[kStatWatchers].delete(normalized); + } + } +} + +module.exports = { + MemoryProvider, +}; diff --git a/lib/internal/vfs/providers/real.js b/lib/internal/vfs/providers/real.js new file mode 100644 index 00000000000000..fbcff25e39ccfe --- /dev/null +++ b/lib/internal/vfs/providers/real.js @@ -0,0 +1,477 @@ +'use strict'; + +const { + Promise, + StringPrototypeStartsWith, +} = primordials; + +const fs = require('fs'); +const path = require('path'); +const { VirtualProvider } = require('internal/vfs/provider'); +const { VirtualFileHandle } = require('internal/vfs/file_handle'); +const { getValidatedPath } = require('internal/fs/utils'); +const { setOwnProperty } = require('internal/util'); +const { + createEACCES, + createEBADF, + createENOENT, +} = require('internal/vfs/errors'); + +/** + * A file handle that wraps a real file descriptor. + */ +// TODO(mcollina): reuse FileHandle from internal/fs/promises for the async +// methods instead of manually wrapping fs.read/write/fstat/ftruncate/close in +// Promises. Blocked on a way to wrap an existing numeric fd in a FileHandle so +// sync-opened handles can still share one underlying handle for async ops. +class RealFileHandle extends VirtualFileHandle { + #fd; + #realPath; + + #checkClosed(syscall) { + if (this.closed) { + throw createEBADF(syscall); + } + } + + /** + * @param {string} path The VFS path + * @param {string} flags The open flags + * @param {number} mode The file mode + * @param {number} fd The real file descriptor + * @param {string} realPath The real filesystem path + */ + constructor(path, flags, mode, fd, realPath) { + super(path, flags, mode); + this.#fd = fd; + this.#realPath = realPath; + } + + readSync(buffer, offset, length, position) { + this.#checkClosed('read'); + return fs.readSync(this.#fd, buffer, offset, length, position); + } + + async read(buffer, offset, length, position) { + this.#checkClosed('read'); + return new Promise((resolve, reject) => { + fs.read(this.#fd, buffer, offset, length, position, (err, bytesRead) => { + if (err) reject(err); + else resolve({ __proto__: null, bytesRead, buffer }); + }); + }); + } + + writeSync(buffer, offset, length, position) { + this.#checkClosed('write'); + return fs.writeSync(this.#fd, buffer, offset, length, position); + } + + async write(buffer, offset, length, position) { + this.#checkClosed('write'); + return new Promise((resolve, reject) => { + fs.write(this.#fd, buffer, offset, length, position, (err, bytesWritten) => { + if (err) reject(err); + else resolve({ __proto__: null, bytesWritten, buffer }); + }); + }); + } + + readFileSync(options) { + this.#checkClosed('read'); + return fs.readFileSync(this.#realPath, options); + } + + async readFile(options) { + this.#checkClosed('read'); + return fs.promises.readFile(this.#realPath, options); + } + + writeFileSync(data, options) { + this.#checkClosed('write'); + fs.writeFileSync(this.#realPath, data, options); + } + + async writeFile(data, options) { + this.#checkClosed('write'); + return fs.promises.writeFile(this.#realPath, data, options); + } + + statSync(options) { + this.#checkClosed('fstat'); + return fs.fstatSync(this.#fd, options); + } + + async stat(options) { + this.#checkClosed('fstat'); + return new Promise((resolve, reject) => { + fs.fstat(this.#fd, options, (err, stats) => { + if (err) reject(err); + else resolve(stats); + }); + }); + } + + truncateSync(len = 0) { + this.#checkClosed('ftruncate'); + fs.ftruncateSync(this.#fd, len); + } + + async truncate(len = 0) { + this.#checkClosed('ftruncate'); + return new Promise((resolve, reject) => { + fs.ftruncate(this.#fd, len, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + closeSync() { + if (!this.closed) { + fs.closeSync(this.#fd); + super.closeSync(); + } + } + + async close() { + if (!this.closed) { + return new Promise((resolve, reject) => { + fs.close(this.#fd, (err) => { + if (err) reject(err); + else { + super.closeSync(); + resolve(); + } + }); + }); + } + } +} + +/** + * A provider that wraps a real filesystem directory. + * Allows mounting a real directory at a different VFS path. + */ +class RealFSProvider extends VirtualProvider { + #rootPath; + + /** + * @param {string} rootPath The real filesystem path to use as root + */ + constructor(rootPath) { + super(); + // Resolve to absolute path and normalize + this.#rootPath = path.resolve(getValidatedPath(rootPath, 'rootPath')); + setOwnProperty(this, 'readonly', false); + setOwnProperty(this, 'supportsSymlinks', true); + } + + /** + * Gets the root path of this provider. + * @returns {string} + */ + get rootPath() { + return this.#rootPath; + } + + /** + * Resolves a VFS path to a real filesystem path. + * Ensures the path doesn't escape the root directory. + * @param {string} vfsPath The VFS path (relative to provider root) + * @returns {string} The real filesystem path + * @private + */ + #resolvePath(vfsPath, followSymlinks = true) { + // Normalize the VFS path (remove leading slash, handle . and ..) + let normalized = vfsPath; + if (normalized.startsWith('/')) { + normalized = normalized.slice(1); + } + + // Join with root and resolve + const realPath = path.resolve(this.#rootPath, normalized); + + // Security check: ensure the resolved path is within rootPath + const rootWithSep = this.#rootPath.endsWith(path.sep) ? + this.#rootPath : + this.#rootPath + path.sep; + + if (realPath !== this.#rootPath && !StringPrototypeStartsWith(realPath, rootWithSep)) { + throw createENOENT('open', vfsPath); + } + + // Resolve symlinks to prevent escape via symbolic links + if (followSymlinks) { + try { + const resolved = fs.realpathSync(realPath); + if (resolved !== this.#rootPath && + !StringPrototypeStartsWith(resolved, rootWithSep)) { + throw createENOENT('open', vfsPath); + } + return resolved; + } catch (err) { + if (err?.code !== 'ENOENT') throw err; + // Path doesn't exist yet - verify deepest existing ancestor + this.#verifyAncestorInRoot(realPath, rootWithSep, vfsPath); + return realPath; + } + } + + // For lstat/readlink (no final symlink follow), check parent only + this.#verifyAncestorInRoot(realPath, rootWithSep, vfsPath); + return realPath; + } + + /** + * Verifies that the deepest existing ancestor of a path is within rootPath. + * @param {string} realPath The real filesystem path + * @param {string} rootWithSep The rootPath with trailing separator + * @param {string} vfsPath The original VFS path (for error messages) + */ + #verifyAncestorInRoot(realPath, rootWithSep, vfsPath) { + let current = path.dirname(realPath); + while (current.length >= this.#rootPath.length) { + try { + const resolved = fs.realpathSync(current); + if (resolved !== this.#rootPath && + !StringPrototypeStartsWith(resolved, rootWithSep)) { + throw createENOENT('open', vfsPath); + } + return; + } catch (err) { + if (err?.code !== 'ENOENT') throw err; + current = path.dirname(current); + } + } + } + + openSync(vfsPath, flags, mode) { + const realPath = this.#resolvePath(vfsPath); + const fd = fs.openSync(realPath, flags, mode); + return new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd, realPath); + } + + async open(vfsPath, flags, mode) { + const realPath = this.#resolvePath(vfsPath); + return new Promise((resolve, reject) => { + fs.open(realPath, flags, mode, (err, fd) => { + if (err) reject(err); + else resolve(new RealFileHandle(vfsPath, flags, mode ?? 0o644, fd, realPath)); + }); + }); + } + + statSync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.statSync(realPath, options); + } + + async stat(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.stat(realPath, options); + } + + lstatSync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath, false); + return fs.lstatSync(realPath, options); + } + + async lstat(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath, false); + return fs.promises.lstat(realPath, options); + } + + readdirSync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.readdirSync(realPath, options); + } + + async readdir(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.readdir(realPath, options); + } + + mkdirSync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.mkdirSync(realPath, options); + } + + async mkdir(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.mkdir(realPath, options); + } + + rmdirSync(vfsPath) { + const realPath = this.#resolvePath(vfsPath); + fs.rmdirSync(realPath); + } + + async rmdir(vfsPath) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.rmdir(realPath); + } + + unlinkSync(vfsPath) { + const realPath = this.#resolvePath(vfsPath); + fs.unlinkSync(realPath); + } + + async unlink(vfsPath) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.unlink(realPath); + } + + renameSync(oldVfsPath, newVfsPath) { + const oldRealPath = this.#resolvePath(oldVfsPath); + const newRealPath = this.#resolvePath(newVfsPath); + fs.renameSync(oldRealPath, newRealPath); + } + + async rename(oldVfsPath, newVfsPath) { + const oldRealPath = this.#resolvePath(oldVfsPath); + const newRealPath = this.#resolvePath(newVfsPath); + return fs.promises.rename(oldRealPath, newRealPath); + } + + readlinkSync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath, false); + const target = fs.readlinkSync(realPath, options); + // Translate absolute targets within rootPath to VFS-relative + if (path.isAbsolute(target)) { + const rootWithSep = this.#rootPath + path.sep; + if (target === this.#rootPath) { + return '/'; + } + if (StringPrototypeStartsWith(target, rootWithSep)) { + return '/' + target.slice(rootWithSep.length).replace(/\\/g, '/'); + } + } + return target; + } + + async readlink(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath, false); + const target = await fs.promises.readlink(realPath, options); + // Translate absolute targets within rootPath to VFS-relative + if (path.isAbsolute(target)) { + const rootWithSep = this.#rootPath + path.sep; + if (target === this.#rootPath) { + return '/'; + } + if (StringPrototypeStartsWith(target, rootWithSep)) { + return '/' + target.slice(rootWithSep.length).replace(/\\/g, '/'); + } + } + return target; + } + + symlinkSync(target, vfsPath, type) { + // Validate target resolves within rootPath + if (path.isAbsolute(target)) { + throw createEACCES('symlink', vfsPath); + } + const realPath = this.#resolvePath(vfsPath); + const resolvedTarget = path.resolve(path.dirname(realPath), target); + const rootWithSep = this.#rootPath.endsWith(path.sep) ? + this.#rootPath : this.#rootPath + path.sep; + if (resolvedTarget !== this.#rootPath && + !StringPrototypeStartsWith(resolvedTarget, rootWithSep)) { + throw createEACCES('symlink', vfsPath); + } + fs.symlinkSync(target, realPath, type); + } + + async symlink(target, vfsPath, type) { + // Validate target resolves within rootPath + if (path.isAbsolute(target)) { + throw createEACCES('symlink', vfsPath); + } + const realPath = this.#resolvePath(vfsPath); + const resolvedTarget = path.resolve(path.dirname(realPath), target); + const rootWithSep = this.#rootPath.endsWith(path.sep) ? + this.#rootPath : this.#rootPath + path.sep; + if (resolvedTarget !== this.#rootPath && + !StringPrototypeStartsWith(resolvedTarget, rootWithSep)) { + throw createEACCES('symlink', vfsPath); + } + return fs.promises.symlink(target, realPath, type); + } + + // path.relative handles case-insensitivity on Windows, which matters here + // because fs.realpathSync (a JS impl) preserves case but fs.promises.realpath + // (native) canonicalizes the drive letter and other components. + #resolvedToVfsPath(resolved, vfsPath, syscall) { + const rel = path.relative(this.#rootPath, resolved); + if (rel === '') return '/'; + if (rel === '..' || + StringPrototypeStartsWith(rel, '..' + path.sep) || + path.isAbsolute(rel)) { + throw createEACCES(syscall, vfsPath); + } + return '/' + rel.replace(/\\/g, '/'); + } + + realpathSync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + const resolved = fs.realpathSync(realPath, options); + return this.#resolvedToVfsPath(resolved, vfsPath, 'realpath'); + } + + async realpath(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + const resolved = await fs.promises.realpath(realPath, options); + return this.#resolvedToVfsPath(resolved, vfsPath, 'realpath'); + } + + accessSync(vfsPath, mode) { + const realPath = this.#resolvePath(vfsPath); + fs.accessSync(realPath, mode); + } + + async access(vfsPath, mode) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.access(realPath, mode); + } + + copyFileSync(srcVfsPath, destVfsPath, mode) { + const srcRealPath = this.#resolvePath(srcVfsPath); + const destRealPath = this.#resolvePath(destVfsPath); + fs.copyFileSync(srcRealPath, destRealPath, mode); + } + + async copyFile(srcVfsPath, destVfsPath, mode) { + const srcRealPath = this.#resolvePath(srcVfsPath); + const destRealPath = this.#resolvePath(destVfsPath); + return fs.promises.copyFile(srcRealPath, destRealPath, mode); + } + + get supportsWatch() { + return true; + } + + watch(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.watch(realPath, options); + } + + watchAsync(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.promises.watch(realPath, options); + } + + watchFile(vfsPath, options) { + const realPath = this.#resolvePath(vfsPath); + return fs.watchFile(realPath, options, () => {}); + } + + unwatchFile(vfsPath, listener) { + const realPath = this.#resolvePath(vfsPath); + fs.unwatchFile(realPath, listener); + } +} + +module.exports = { + RealFSProvider, + RealFileHandle, +}; diff --git a/lib/internal/vfs/stats.js b/lib/internal/vfs/stats.js new file mode 100644 index 00000000000000..fdec6fe87cad26 --- /dev/null +++ b/lib/internal/vfs/stats.js @@ -0,0 +1,300 @@ +'use strict'; + +const { + BigInt, + BigInt64Array, + DateNow, + Float64Array, + MathCeil, + MathFloor, +} = primordials; + +const { + fs: { + S_IFDIR, + S_IFREG, + S_IFLNK, + }, +} = internalBinding('constants'); + +const { getStatsFromBinding } = require('internal/fs/utils'); + +// Default block size for virtual files (4KB) +const kDefaultBlockSize = 4096; + +// Distinctive device number for VFS files (0xVF5 = 4085) +const kVfsDev = 4085; + +// Incrementing inode counter for unique ino values +let inoCounter = 1; + +// Reusable arrays for creating Stats objects. +// IMPORTANT: Safe only because getStatsFromBinding copies synchronously. +// Do not use in async paths. +// Format: dev, mode, nlink, uid, gid, rdev, blksize, ino, size, blocks, +// atime_sec, atime_nsec, mtime_sec, mtime_nsec, ctime_sec, ctime_nsec, +// birthtime_sec, birthtime_nsec +const statsArray = new Float64Array(18); +const bigintStatsArray = new BigInt64Array(18); + +/** + * Converts milliseconds to seconds and nanoseconds. + * @param {number} ms Milliseconds + * @returns {{ sec: number, nsec: number }} + */ +function msToTimeSpec(ms) { + const sec = MathFloor(ms / 1000); + const nsec = (ms % 1000) * 1_000_000; + return { sec, nsec }; +} + +/** + * Fills the bigint stats array with the given values. + * @returns {Stats} + */ +function fillBigIntStatsArray( + dev, mode, nlink, uid, gid, rdev, blksize, ino, + size, blocks, atime, mtime, ctime, birthtime, +) { + bigintStatsArray[0] = BigInt(dev); + bigintStatsArray[1] = BigInt(mode); + bigintStatsArray[2] = BigInt(nlink); + bigintStatsArray[3] = BigInt(uid); + bigintStatsArray[4] = BigInt(gid); + bigintStatsArray[5] = BigInt(rdev); + bigintStatsArray[6] = BigInt(blksize); + bigintStatsArray[7] = BigInt(ino); + bigintStatsArray[8] = BigInt(size); + bigintStatsArray[9] = BigInt(blocks); + bigintStatsArray[10] = BigInt(atime.sec); + bigintStatsArray[11] = BigInt(atime.nsec); + bigintStatsArray[12] = BigInt(mtime.sec); + bigintStatsArray[13] = BigInt(mtime.nsec); + bigintStatsArray[14] = BigInt(ctime.sec); + bigintStatsArray[15] = BigInt(ctime.nsec); + bigintStatsArray[16] = BigInt(birthtime.sec); + bigintStatsArray[17] = BigInt(birthtime.nsec); + return getStatsFromBinding(bigintStatsArray); +} + +/** + * Creates a Stats object for a virtual file. + * @param {number} size The file size in bytes + * @param {object} [options] Optional stat properties + * @param {number} [options.mode] File mode (default: 0o644) + * @param {number} [options.uid] User ID (default: process.getuid() or 0) + * @param {number} [options.gid] Group ID (default: process.getgid() or 0) + * @param {number} [options.atimeMs] Access time in ms + * @param {number} [options.mtimeMs] Modification time in ms + * @param {number} [options.ctimeMs] Change time in ms + * @param {number} [options.birthtimeMs] Birth time in ms + * @param {boolean} [options.bigint] Return BigIntStats + * @returns {Stats} + */ +function createFileStats(size, options = {}) { + const now = DateNow(); + const mode = (options.mode ?? 0o644) | S_IFREG; + const nlink = options.nlink ?? 1; + const uid = options.uid ?? (process.getuid?.() ?? 0); + const gid = options.gid ?? (process.getgid?.() ?? 0); + const atimeMs = options.atimeMs ?? now; + const mtimeMs = options.mtimeMs ?? now; + const ctimeMs = options.ctimeMs ?? now; + const birthtimeMs = options.birthtimeMs ?? now; + const blocks = MathCeil(size / 512); + const ino = inoCounter++; + + const atime = msToTimeSpec(atimeMs); + const mtime = msToTimeSpec(mtimeMs); + const ctime = msToTimeSpec(ctimeMs); + const birthtime = msToTimeSpec(birthtimeMs); + + if (options.bigint) { + return fillBigIntStatsArray( + kVfsDev, mode, nlink, uid, gid, 0, kDefaultBlockSize, ino, + size, blocks, atime, mtime, ctime, birthtime, + ); + } + + statsArray[0] = kVfsDev; // dev + statsArray[1] = mode; // mode + statsArray[2] = nlink; // nlink + statsArray[3] = uid; // uid + statsArray[4] = gid; // gid + statsArray[5] = 0; // rdev + statsArray[6] = kDefaultBlockSize; // blksize + statsArray[7] = ino; // ino + statsArray[8] = size; // size + statsArray[9] = blocks; // blocks + statsArray[10] = atime.sec; // atime_sec + statsArray[11] = atime.nsec; // atime_nsec + statsArray[12] = mtime.sec; // mtime_sec + statsArray[13] = mtime.nsec; // mtime_nsec + statsArray[14] = ctime.sec; // ctime_sec + statsArray[15] = ctime.nsec; // ctime_nsec + statsArray[16] = birthtime.sec; // birthtime_sec + statsArray[17] = birthtime.nsec; // birthtime_nsec + + return getStatsFromBinding(statsArray); +} + +/** + * Creates a Stats object for a virtual directory. + * @param {object} [options] Optional stat properties + * @param {number} [options.mode] Directory mode (default: 0o755) + * @param {number} [options.uid] User ID (default: process.getuid() or 0) + * @param {number} [options.gid] Group ID (default: process.getgid() or 0) + * @param {number} [options.atimeMs] Access time in ms + * @param {number} [options.mtimeMs] Modification time in ms + * @param {number} [options.ctimeMs] Change time in ms + * @param {number} [options.birthtimeMs] Birth time in ms + * @returns {Stats} + */ +function createDirectoryStats(options = {}) { + const now = DateNow(); + const mode = (options.mode ?? 0o755) | S_IFDIR; + const uid = options.uid ?? (process.getuid?.() ?? 0); + const gid = options.gid ?? (process.getgid?.() ?? 0); + const atimeMs = options.atimeMs ?? now; + const mtimeMs = options.mtimeMs ?? now; + const ctimeMs = options.ctimeMs ?? now; + const birthtimeMs = options.birthtimeMs ?? now; + const ino = inoCounter++; + + const atime = msToTimeSpec(atimeMs); + const mtime = msToTimeSpec(mtimeMs); + const ctime = msToTimeSpec(ctimeMs); + const birthtime = msToTimeSpec(birthtimeMs); + + if (options.bigint) { + return fillBigIntStatsArray( + kVfsDev, mode, 1, uid, gid, 0, kDefaultBlockSize, ino, + kDefaultBlockSize, 8, atime, mtime, ctime, birthtime, + ); + } + + statsArray[0] = kVfsDev; // dev + statsArray[1] = mode; // mode + statsArray[2] = 1; // nlink + statsArray[3] = uid; // uid + statsArray[4] = gid; // gid + statsArray[5] = 0; // rdev + statsArray[6] = kDefaultBlockSize; // blksize + statsArray[7] = ino; // ino + statsArray[8] = kDefaultBlockSize; // size (directory size) + statsArray[9] = 8; // blocks + statsArray[10] = atime.sec; // atime_sec + statsArray[11] = atime.nsec; // atime_nsec + statsArray[12] = mtime.sec; // mtime_sec + statsArray[13] = mtime.nsec; // mtime_nsec + statsArray[14] = ctime.sec; // ctime_sec + statsArray[15] = ctime.nsec; // ctime_nsec + statsArray[16] = birthtime.sec; // birthtime_sec + statsArray[17] = birthtime.nsec; // birthtime_nsec + + return getStatsFromBinding(statsArray); +} + +/** + * Creates a Stats object for a virtual symbolic link. + * @param {number} size The symlink size (length of target path) + * @param {object} [options] Optional stat properties + * @param {number} [options.mode] Symlink mode (default: 0o777) + * @param {number} [options.uid] User ID (default: process.getuid() or 0) + * @param {number} [options.gid] Group ID (default: process.getgid() or 0) + * @param {number} [options.atimeMs] Access time in ms + * @param {number} [options.mtimeMs] Modification time in ms + * @param {number} [options.ctimeMs] Change time in ms + * @param {number} [options.birthtimeMs] Birth time in ms + * @returns {Stats} + */ +function createSymlinkStats(size, options = {}) { + const now = DateNow(); + const mode = (options.mode ?? 0o777) | S_IFLNK; + const uid = options.uid ?? (process.getuid?.() ?? 0); + const gid = options.gid ?? (process.getgid?.() ?? 0); + const atimeMs = options.atimeMs ?? now; + const mtimeMs = options.mtimeMs ?? now; + const ctimeMs = options.ctimeMs ?? now; + const birthtimeMs = options.birthtimeMs ?? now; + const blocks = MathCeil(size / 512); + const ino = inoCounter++; + + const atime = msToTimeSpec(atimeMs); + const mtime = msToTimeSpec(mtimeMs); + const ctime = msToTimeSpec(ctimeMs); + const birthtime = msToTimeSpec(birthtimeMs); + + if (options.bigint) { + return fillBigIntStatsArray( + kVfsDev, mode, 1, uid, gid, 0, kDefaultBlockSize, ino, + size, blocks, atime, mtime, ctime, birthtime, + ); + } + + statsArray[0] = kVfsDev; // dev + statsArray[1] = mode; // mode + statsArray[2] = 1; // nlink + statsArray[3] = uid; // uid + statsArray[4] = gid; // gid + statsArray[5] = 0; // rdev + statsArray[6] = kDefaultBlockSize; // blksize + statsArray[7] = ino; // ino + statsArray[8] = size; // size + statsArray[9] = blocks; // blocks + statsArray[10] = atime.sec; // atime_sec + statsArray[11] = atime.nsec; // atime_nsec + statsArray[12] = mtime.sec; // mtime_sec + statsArray[13] = mtime.nsec; // mtime_nsec + statsArray[14] = ctime.sec; // ctime_sec + statsArray[15] = ctime.nsec; // ctime_nsec + statsArray[16] = birthtime.sec; // birthtime_sec + statsArray[17] = birthtime.nsec; // birthtime_nsec + + return getStatsFromBinding(statsArray); +} + +/** + * Creates a zeroed Stats object for non-existent files. + * All fields are zero, including mode (no S_IFREG bit set). + * This matches Node.js fs.watchFile() behavior for missing files. + * @returns {Stats} + */ +function createZeroStats(options) { + const zero = { sec: 0, nsec: 0 }; + + if (options?.bigint) { + return fillBigIntStatsArray( + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, zero, zero, zero, zero, + ); + } + + statsArray[0] = 0; // dev + statsArray[1] = 0; // mode (no file type bits) + statsArray[2] = 0; // nlink + statsArray[3] = 0; // uid + statsArray[4] = 0; // gid + statsArray[5] = 0; // rdev + statsArray[6] = 0; // blksize + statsArray[7] = 0; // ino + statsArray[8] = 0; // size + statsArray[9] = 0; // blocks + statsArray[10] = 0; // atime_sec + statsArray[11] = 0; // atime_nsec + statsArray[12] = 0; // mtime_sec + statsArray[13] = 0; // mtime_nsec + statsArray[14] = 0; // ctime_sec + statsArray[15] = 0; // ctime_nsec + statsArray[16] = 0; // birthtime_sec + statsArray[17] = 0; // birthtime_nsec + + return getStatsFromBinding(statsArray); +} + +module.exports = { + createFileStats, + createDirectoryStats, + createSymlinkStats, + createZeroStats, +}; diff --git a/lib/internal/vfs/streams.js b/lib/internal/vfs/streams.js new file mode 100644 index 00000000000000..79890491bb3556 --- /dev/null +++ b/lib/internal/vfs/streams.js @@ -0,0 +1,353 @@ +'use strict'; + +const { + MathMin, +} = primordials; + +const { Buffer } = require('buffer'); +const { Readable, Writable } = require('stream'); +const { createEBADF } = require('internal/vfs/errors'); +const { getVirtualFd } = require('internal/vfs/fd'); +const { kEmptyObject } = require('internal/util'); +const { validateInteger } = require('internal/validators'); +const { + codes: { + ERR_OUT_OF_RANGE, + }, +} = require('internal/errors'); + +/** + * A readable stream for virtual files. + */ +class VirtualReadStream extends Readable { + #vfs; + #path; + #fd = null; + #end; + #pos; + #content = null; + #autoClose; + + /** + * Number of bytes read so far. + * @type {number} + */ + bytesRead = 0; + + /** + * True until the first read completes. + * @type {boolean} + */ + pending = true; + + /** + * @param {VirtualFileSystem} vfs The VFS instance + * @param {string} filePath The path to the file + * @param {object} [options] Stream options + */ + constructor(vfs, filePath, options = kEmptyObject) { + const { + start, + end, + highWaterMark = 64 * 1024, + encoding, + fd, + ...streamOptions + } = options; + + // Validate start/end matching real ReadStream behavior + if (start !== undefined) { + validateInteger(start, 'start', 0); + } + if (end !== undefined && end !== Infinity) { + validateInteger(end, 'end', 0); + } + if (start !== undefined && end !== undefined && end !== Infinity && + start > end) { + throw new ERR_OUT_OF_RANGE( + 'start', + `<= "end" (here: ${end})`, + start, + ); + } + + super({ ...streamOptions, highWaterMark, encoding }); + + this.#vfs = vfs; + this.#path = filePath; + this.#end = end === undefined ? Infinity : end; + this.#pos = start === undefined ? 0 : start; + this.#autoClose = options.autoClose !== false; + + if (fd !== null && fd !== undefined) { + // Use the already-open file descriptor + this.#fd = fd; + process.nextTick(() => { + this.emit('open', this.#fd); + this.emit('ready'); + }); + } else { + // Open the file on next tick so listeners can be attached. + // Note: #openFile will not throw - if it fails, the stream is destroyed. + process.nextTick(() => this.#openFile()); + } + } + + /** + * Gets the file path. + * @returns {string} + */ + get path() { + return this.#path; + } + + /** + * Opens the virtual file. + * Events are emitted synchronously within this method, which runs + * asynchronously via process.nextTick - matching real fs behavior. + */ + #openFile() { + try { + this.#fd = this.#vfs.openSync(this.#path); + this.emit('open', this.#fd); + this.emit('ready'); + } catch (err) { + this.destroy(err); + } + } + + /** + * Implements the readable _read method. + * @param {number} size Number of bytes to read + */ + _read(size) { + if (this.destroyed || this.#fd === null) { + this.destroy(createEBADF('read')); + return; + } + + // Load content on first read (lazy loading) + if (this.#content === null) { + try { + const vfd = getVirtualFd(this.#fd); + if (!vfd) { + this.destroy(createEBADF('read')); + return; + } + // Use the file handle's readFileSync to get content + this.#content = vfd.entry.readFileSync(); + this.pending = false; + } catch (err) { + this.destroy(err); + return; + } + } + + // Calculate how much to read + // Note: end is inclusive, so we use end + 1 for the upper bound + const endPos = this.#end === Infinity ? this.#content.length : this.#end + 1; + const remaining = MathMin(endPos, this.#content.length) - this.#pos; + if (remaining <= 0) { + this.push(null); + return; + } + + const bytesToRead = MathMin(size, remaining); + const chunk = this.#content.subarray(this.#pos, this.#pos + bytesToRead); + this.#pos += bytesToRead; + this.bytesRead += bytesToRead; + + this.push(chunk); + + // Check if we've reached the end + if (this.#pos >= endPos || this.#pos >= this.#content.length) { + this.push(null); + } + } + + /** + * Closes the file descriptor. + * Note: Does not emit 'close' - the base Readable class handles that. + */ + #close() { + if (this.#fd !== null) { + try { + this.#vfs.closeSync(this.#fd); + } catch { + // Ignore close errors + } + this.#fd = null; + } + } + + /** + * Implements the readable _destroy method. + * @param {Error|null} err The error + * @param {Function} callback Callback + */ + _destroy(err, callback) { + if (this.#autoClose) { + this.#close(); + } + callback(err); + } +} + +/** + * A writable stream for virtual files. + */ +class VirtualWriteStream extends Writable { + #vfs; + #path; + #fd = null; + #autoClose; + #start; + + /** + * Number of bytes written so far. + * @type {number} + */ + bytesWritten = 0; + + /** + * True until the first write completes. + * @type {boolean} + */ + pending = true; + + /** + * @param {VirtualFileSystem} vfs The VFS instance + * @param {string} filePath The path to the file + * @param {object} [options] Stream options + */ + constructor(vfs, filePath, options = kEmptyObject) { + const { + highWaterMark = 64 * 1024, + ...streamOptions + } = options; + + // Validate start matching real WriteStream behavior + if (options.start !== undefined) { + validateInteger(options.start, 'start', 0); + } + + super({ ...streamOptions, highWaterMark }); + + this.#vfs = vfs; + this.#path = filePath; + this.#autoClose = options.autoClose !== false; + this.#start = options.start; + + const fd = options.fd; + if (fd !== null && fd !== undefined) { + // Use the already-open file descriptor + this.#fd = fd; + if (this.#start !== undefined) { + this.#setPosition(this.#start); + } + process.nextTick(() => { + this.emit('open', this.#fd); + this.emit('ready'); + }); + } else { + // Open file synchronously (VFS is in-memory) so writes can proceed + // immediately. Emit events on next tick for listener attachment. + const flags = options.flags || 'w'; + try { + this.#fd = this.#vfs.openSync(this.#path, flags); + if (this.#start !== undefined) { + this.#setPosition(this.#start); + } + } catch (err) { + process.nextTick(() => this.destroy(err)); + return; + } + process.nextTick(() => { + this.emit('open', this.#fd); + this.emit('ready'); + }); + } + } + + /** + * Sets the file handle position for the given fd. + * @param {number} pos The position to set + */ + #setPosition(pos) { + const vfd = getVirtualFd(this.#fd); + if (vfd) { + vfd.entry.position = pos; + } + } + + /** + * Gets the file path. + * @returns {string} + */ + get path() { + return this.#path; + } + + /** + * Implements the writable _write method. + * @param {Buffer|string} chunk Data to write + * @param {string} encoding Encoding + * @param {Function} callback Callback + */ + _write(chunk, encoding, callback) { + if (this.destroyed || this.#fd === null) { + callback(createEBADF('write')); + return; + } + + try { + const buffer = typeof chunk === 'string' ? + Buffer.from(chunk, encoding) : chunk; + this.#vfs.writeSync(this.#fd, buffer, 0, buffer.length, null); + this.bytesWritten += buffer.length; + this.pending = false; + callback(); + } catch (err) { + callback(err); + } + } + + /** + * Implements the writable _final method (flush before close). + * @param {Function} callback Callback + */ + _final(callback) { + callback(); + } + + /** + * Closes the file descriptor. + */ + #close() { + if (this.#fd !== null) { + try { + this.#vfs.closeSync(this.#fd); + } catch { + // Ignore close errors + } + this.#fd = null; + } + } + + /** + * Implements the writable _destroy method. + * @param {Error|null} err The error + * @param {Function} callback Callback + */ + _destroy(err, callback) { + if (this.#autoClose) { + this.#close(); + } + callback(err); + } +} + +module.exports = { + VirtualReadStream, + VirtualWriteStream, +}; diff --git a/lib/internal/vfs/watcher.js b/lib/internal/vfs/watcher.js new file mode 100644 index 00000000000000..f64ba91ba3c9a6 --- /dev/null +++ b/lib/internal/vfs/watcher.js @@ -0,0 +1,688 @@ +'use strict'; + +const { + ArrayPrototypePush, + ObjectAssign, + Promise, + PromiseResolve, + SafeMap, + SafeSet, + SymbolAsyncIterator, +} = primordials; + +const { AbortError } = require('internal/errors'); +const { Buffer } = require('buffer'); +const { EventEmitter } = require('events'); +const { basename, join } = require('path'); +const { + setInterval, + clearInterval, +} = require('timers'); + +/** + * VFSWatcher - Polling-based file/directory watcher for VFS. + * Emits 'change' events when the file content or stats change. + * Compatible with fs.watch() return value interface. + */ +class VFSWatcher extends EventEmitter { + #vfs; + #path; + #interval; + #timer = null; + #lastStats; + #closed = false; + #persistent; + #recursive; + #encoding; + #trackedFiles; + #signal; + #abortHandler = null; + + /** + * @param {VirtualProvider} provider The VFS provider + * @param {string} path The path to watch (provider-relative) + * @param {object} [options] Options + * @param {number} [options.interval] Polling interval in ms (default: 100) + * @param {boolean} [options.persistent] Keep process alive (default: true) + * @param {boolean} [options.recursive] Watch subdirectories (default: false) + * @param {AbortSignal} [options.signal] AbortSignal for cancellation + */ + constructor(provider, path, options = {}) { + super(); + + this.#vfs = provider; + this.#path = path; + this.#interval = options.interval ?? 100; + this.#persistent = options.persistent !== false; + this.#recursive = options.recursive === true; + this.#encoding = options.encoding; + this.#trackedFiles = new SafeMap(); // path -> { stats, relativePath } + this.#signal = options.signal; + + // Handle AbortSignal + if (this.#signal) { + if (this.#signal.aborted) { + this.close(); + return; + } + this.#abortHandler = () => this.close(); + this.#signal.addEventListener('abort', this.#abortHandler, { once: true }); + } + + // Get initial stats + this.#lastStats = this.#getStats(); + + // If watching a directory, build file list + if (this.#lastStats?.isDirectory()) { + if (this.#recursive) { + this.#buildFileList(this.#path, ''); + } else { + this.#buildChildList(this.#path); + } + } + + // Start polling + this.#startPolling(); + } + + /** + * Encodes a filename according to the watcher's encoding option. + * @param {string} filename The filename to encode + * @returns {string|Buffer} The encoded filename + */ + #encodeFilename(filename) { + if (this.#encoding === 'buffer') { + return Buffer.from(filename); + } + return filename; + } + + /** + * Gets stats for the watched path. + * @returns {Stats|null} The stats or null if file doesn't exist + */ + #getStats() { + try { + return this.#vfs.statSync(this.#path); + } catch { + return null; + } + } + + /** + * Starts the polling timer. + */ + #startPolling() { + if (this.#closed) return; + + this.#timer = setInterval(() => this.#poll(), this.#interval); + + // If not persistent, unref the timer to allow process to exit + if (!this.#persistent && this.#timer.unref) { + this.#timer.unref(); + } + } + + /** + * Polls for changes. + */ + #poll() { + if (this.#closed) return; + + // For directory watching, poll tracked children + if (this.#lastStats?.isDirectory()) { + this.#pollDirectory(); + return; + } + + // For single file watching + const newStats = this.#getStats(); + + if (this.#statsChanged(this.#lastStats, newStats)) { + const eventType = this.#determineEventType(this.#lastStats, newStats); + const filename = this.#encodeFilename(basename(this.#path)); + this.emit('change', eventType, filename); + } + + this.#lastStats = newStats; + } + + /** + * Polls directory children for changes, detecting new and deleted files. + */ + #pollDirectory() { + // Rescan for new files + if (this.#recursive) { + this.#rescanRecursive(this.#path, ''); + } else { + this.#rescanChildren(this.#path); + } + + // Check tracked files for changes/deletions + for (const { 0: filePath, 1: info } of this.#trackedFiles) { + const newStats = this.#getStatsFor(filePath); + if (newStats === null && info.stats !== null) { + // File was deleted + this.emit('change', 'rename', this.#encodeFilename(info.relativePath)); + this.#trackedFiles.delete(filePath); + } else if (this.#statsChanged(info.stats, newStats)) { + const eventType = this.#determineEventType(info.stats, newStats); + this.emit('change', eventType, this.#encodeFilename(info.relativePath)); + info.stats = newStats; + } + } + } + + /** + * Rescans direct children for new entries. + * @param {string} dirPath The directory path + */ + #rescanChildren(dirPath) { + try { + const entries = this.#vfs.readdirSync(dirPath); + for (const name of entries) { + const fullPath = join(dirPath, name); + if (!this.#trackedFiles.has(fullPath)) { + const stats = this.#getStatsFor(fullPath); + this.#trackedFiles.set(fullPath, { + stats, + relativePath: name, + }); + this.emit('change', 'rename', this.#encodeFilename(name)); + } + } + } catch { + // Directory might not exist or be readable + } + } + + /** + * Recursively rescans for new entries. + * @param {string} dirPath The directory path + * @param {string} relativePath The relative path from watched root + */ + #rescanRecursive(dirPath, relativePath) { + try { + const entries = this.#vfs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dirPath, entry.name); + const relPath = relativePath ? + join(relativePath, entry.name) : entry.name; + + if (entry.isDirectory()) { + this.#rescanRecursive(fullPath, relPath); + } else if (!this.#trackedFiles.has(fullPath)) { + const stats = this.#getStatsFor(fullPath); + this.#trackedFiles.set(fullPath, { + stats, + relativePath: relPath, + }); + this.emit('change', 'rename', this.#encodeFilename(relPath)); + } + } + } catch { + // Directory might not exist or be readable + } + } + + /** + * Gets stats for a specific path. + * @param {string} filePath The file path + * @returns {Stats|null} + */ + #getStatsFor(filePath) { + try { + return this.#vfs.statSync(filePath); + } catch { + return null; + } + } + + /** + * Builds the list of files to track for recursive watching. + * @param {string} dirPath The directory path + * @param {string} relativePath The relative path from the watched root + */ + #buildFileList(dirPath, relativePath) { + try { + const entries = this.#vfs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dirPath, entry.name); + const relPath = relativePath ? join(relativePath, entry.name) : entry.name; + + if (entry.isDirectory()) { + // Recurse into subdirectory + this.#buildFileList(fullPath, relPath); + } else { + // Track the file + const stats = this.#getStatsFor(fullPath); + this.#trackedFiles.set(fullPath, { + stats, + relativePath: relPath, + }); + } + } + } catch { + // Directory might not exist or be readable + } + } + + /** + * Builds a list of direct children to track for non-recursive watching. + * @param {string} dirPath The directory path + */ + #buildChildList(dirPath) { + try { + const entries = this.#vfs.readdirSync(dirPath); + for (const name of entries) { + const fullPath = join(dirPath, name); + const stats = this.#getStatsFor(fullPath); + this.#trackedFiles.set(fullPath, { + stats, + relativePath: name, + }); + } + } catch { + // Directory might not exist or be readable + } + } + + /** + * Checks if stats have changed. + * @param {Stats|null} oldStats Previous stats + * @param {Stats|null} newStats Current stats + * @returns {boolean} True if stats changed + */ + #statsChanged(oldStats, newStats) { + // File created or deleted + if ((oldStats === null) !== (newStats === null)) { + return true; + } + + // Both null - no change + if (oldStats === null && newStats === null) { + return false; + } + + // Compare mtime and size + if (oldStats.mtimeMs !== newStats.mtimeMs) { + return true; + } + if (oldStats.size !== newStats.size) { + return true; + } + + return false; + } + + /** + * Determines the event type based on stats change. + * @param {Stats|null} oldStats Previous stats + * @param {Stats|null} newStats Current stats + * @returns {string} 'rename' or 'change' + */ + #determineEventType(oldStats, newStats) { + // File was created or deleted + if ((oldStats === null) !== (newStats === null)) { + return 'rename'; + } + // Content changed + return 'change'; + } + + /** + * Closes the watcher and stops polling. + */ + close() { + if (this.#closed) return; + this.#closed = true; + + if (this.#timer) { + clearInterval(this.#timer); + this.#timer = null; + } + + // Clear tracked files + this.#trackedFiles.clear(); + + // Remove abort handler + if (this.#signal && this.#abortHandler) { + this.#signal.removeEventListener('abort', this.#abortHandler); + } + + this.emit('close'); + } + + /** + * Alias for close() - compatibility with FSWatcher. + * @returns {this} + */ + unref() { + this.#timer?.unref?.(); + return this; + } + + /** + * Makes the timer keep the process alive - compatibility with FSWatcher. + * @returns {this} + */ + ref() { + this.#timer?.ref?.(); + return this; + } +} + +/** + * VFSStatWatcher - Polling-based stat watcher for VFS. + * Emits 'change' events with current and previous stats. + * Compatible with fs.watchFile() return value interface. + */ +class VFSStatWatcher extends EventEmitter { + #vfs; + #path; + #interval; + #persistent; + #bigint; + #closed = false; + #timer = null; + #lastStats; + #listeners; + + /** + * @param {VirtualProvider} provider The VFS provider + * @param {string} path The path to watch (provider-relative) + * @param {object} [options] Options + * @param {number} [options.interval] Polling interval in ms (default: 5007) + * @param {boolean} [options.persistent] Keep process alive (default: true) + */ + constructor(provider, path, options = {}) { + super(); + + this.#vfs = provider; + this.#path = path; + this.#interval = options.interval ?? 5007; + this.#persistent = options.persistent !== false; + this.#bigint = options.bigint === true; + this.#listeners = new SafeSet(); + + // Get initial stats + this.#lastStats = this.#getStats(); + + // Start polling + this.#startPolling(); + } + + /** + * Gets stats for the watched path. + * @returns {Stats} The stats (with zeroed values if file doesn't exist) + */ + #getStats() { + try { + return this.#vfs.statSync(this.#path, { bigint: this.#bigint }); + } catch { + // Return a zeroed stats object for non-existent files + // This matches Node.js behavior + return this.#createZeroStats(); + } + } + + /** + * Creates a zeroed stats object for non-existent files. + * @returns {object} Zeroed stats + */ + #createZeroStats() { + const { createZeroStats } = require('internal/vfs/stats'); + return createZeroStats({ bigint: this.#bigint }); + } + + /** + * Starts the polling timer. + */ + #startPolling() { + if (this.#closed) return; + + this.#timer = setInterval(() => this.#poll(), this.#interval); + + // If not persistent, unref the timer to allow process to exit + if (!this.#persistent && this.#timer.unref) { + this.#timer.unref(); + } + } + + /** + * Polls for changes. + */ + #poll() { + if (this.#closed) return; + + const newStats = this.#getStats(); + + if (this.#statsChanged(this.#lastStats, newStats)) { + const prevStats = this.#lastStats; + this.#lastStats = newStats; + this.emit('change', newStats, prevStats); + } + } + + /** + * Checks if stats have changed. + * @param {Stats} oldStats Previous stats + * @param {Stats} newStats Current stats + * @returns {boolean} True if stats changed + */ + #statsChanged(oldStats, newStats) { + // Compare mtime and ctime + if (oldStats.mtimeMs !== newStats.mtimeMs) { + return true; + } + if (oldStats.ctimeMs !== newStats.ctimeMs) { + return true; + } + if (oldStats.size !== newStats.size) { + return true; + } + return false; + } + + /** + * Adds a listener for the given event. + * Tracks 'change' listeners for internal bookkeeping. + * @param {string} event The event name + * @param {Function} listener The listener function + * @returns {this} + */ + addListener(event, listener) { + if (event === 'change') { + this.#listeners.add(listener); + } + super.addListener(event, listener); + return this; + } + + /** + * Removes a listener for the given event. + * @param {string} event The event name + * @param {Function} listener The listener function + * @returns {this} + */ + removeListener(event, listener) { + if (event === 'change') { + this.#listeners.delete(listener); + } + super.removeListener(event, listener); + return this; + } + + /** + * Removes all listeners for an event. + * Overrides EventEmitter to also clear internal #listeners tracking. + * @param {string} eventName The event name + * @returns {this} + */ + removeAllListeners(eventName) { + if (eventName === 'change') { + this.#listeners.clear(); + } + super.removeAllListeners(eventName); + return this; + } + + /** + * Returns true if there are no listeners. + * @returns {boolean} + */ + hasNoListeners() { + return this.#listeners.size === 0; + } + + /** + * Stops the watcher. + */ + stop() { + if (this.#closed) return; + this.#closed = true; + + if (this.#timer) { + clearInterval(this.#timer); + this.#timer = null; + } + + this.emit('stop'); + } + + /** + * Makes the timer not keep the process alive. + * @returns {this} + */ + unref() { + this.#timer?.unref?.(); + return this; + } + + /** + * Makes the timer keep the process alive. + * @returns {this} + */ + ref() { + this.#timer?.ref?.(); + return this; + } +} + +/** + * VFSWatchAsyncIterable - Async iterable wrapper for VFSWatcher. + * Compatible with fs.promises.watch() return value interface. + */ +const kMaxPendingEvents = 1024; + +class VFSWatchAsyncIterable { + #watcher; + #closed = false; + #pendingEvents = []; + #pendingResolvers = []; + + /** + * @param {VirtualProvider} provider The VFS provider + * @param {string} path The path to watch (provider-relative) + * @param {object} [options] Options + */ + constructor(provider, path, options = {}) { + // Strip signal from options passed to VFSWatcher - we handle abort + // at the iterable level to reject pending next() with AbortError + // instead of resolving with done:true via the 'close' event. + const signal = options.signal; + const watcherOptions = ObjectAssign({ __proto__: null }, options); + delete watcherOptions.signal; + this.#watcher = new VFSWatcher(provider, path, watcherOptions); + + this.#watcher.on('change', (eventType, filename) => { + const event = { eventType, filename }; + if (this.#pendingResolvers.length > 0) { + const { resolve } = this.#pendingResolvers.shift(); + resolve({ value: event, done: false }); + } else if (this.#pendingEvents.length < kMaxPendingEvents) { + ArrayPrototypePush(this.#pendingEvents, event); + } + // Drop events when queue is full to prevent unbounded memory growth + }); + + this.#watcher.on('close', () => { + this.#closed = true; + // Resolve any pending iterators + while (this.#pendingResolvers.length > 0) { + const { resolve } = this.#pendingResolvers.shift(); + resolve({ value: undefined, done: true }); + } + }); + + // Handle abort signal - reject pending next() with AbortError + if (signal) { + const onAbort = () => { + this.#closed = true; + const err = new AbortError(undefined, { cause: signal.reason }); + while (this.#pendingResolvers.length > 0) { + const { reject } = this.#pendingResolvers.shift(); + reject(err); + } + this.#watcher.close(); + }; + if (signal.aborted) { + onAbort(); + } else { + signal.addEventListener('abort', onAbort, { once: true }); + } + } + } + + /** + * Returns the async iterator. + * @returns {AsyncIterator} + */ + [SymbolAsyncIterator]() { + return this; + } + + /** + * Gets the next event. + * @returns {Promise} + */ + next() { + if (this.#closed) { + return PromiseResolve({ value: undefined, done: true }); + } + + if (this.#pendingEvents.length > 0) { + const event = this.#pendingEvents.shift(); + return PromiseResolve({ value: event, done: false }); + } + + return new Promise((resolve, reject) => { + ArrayPrototypePush(this.#pendingResolvers, { resolve, reject }); + }); + } + + /** + * Closes the iterator and underlying watcher. + * @returns {Promise} + */ + return() { + this.#watcher.close(); + return PromiseResolve({ value: undefined, done: true }); + } + + /** + * Handles iterator throw. + * @param {Error} error The error to throw + * @returns {Promise} + */ + throw(error) { + this.#watcher.close(); + return PromiseResolve({ value: undefined, done: true }); + } +} + +module.exports = { + VFSWatcher, + VFSStatWatcher, + VFSWatchAsyncIterable, +}; diff --git a/lib/vfs.js b/lib/vfs.js new file mode 100644 index 00000000000000..0d12229aca72cd --- /dev/null +++ b/lib/vfs.js @@ -0,0 +1,37 @@ +'use strict'; + +const { + FunctionPrototypeSymbolHasInstance, +} = primordials; + +const { VirtualFileSystem } = require('internal/vfs/file_system'); +const { VirtualProvider } = require('internal/vfs/provider'); +const { MemoryProvider } = require('internal/vfs/providers/memory'); +const { RealFSProvider } = require('internal/vfs/providers/real'); + +/** + * Creates a new VirtualFileSystem instance. + * @param {VirtualProvider} [provider] The provider to use (defaults to MemoryProvider) + * @param {object} [options] Configuration options + * @param {boolean} [options.moduleHooks] Whether to enable require/import hooks (default: true) + * @param {boolean} [options.virtualCwd] Whether to enable virtual working directory + * @returns {VirtualFileSystem} + */ +function create(provider, options) { + // Handle case where first arg is options (no provider) + if (provider != null && + !FunctionPrototypeSymbolHasInstance(VirtualProvider, provider) && + typeof provider === 'object') { + options = provider; + provider = undefined; + } + return new VirtualFileSystem(provider, options); +} + +module.exports = { + create, + VirtualFileSystem, + VirtualProvider, + MemoryProvider, + RealFSProvider, +}; diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 63dde770cc0195..6eca24fdaa403d 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -153,6 +153,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "sqlite", // Experimental. "stream/iter", // Experimental. "sys", // Deprecated. + "vfs", // Experimental. "wasi", // Experimental. "zlib/iter", // Experimental. #if !HAVE_SQLITE diff --git a/src/node_options.cc b/src/node_options.cc index bcb3819a643ede..b7cb32f67cf614 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -619,6 +619,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { NoOp{}, #endif kAllowedInEnvvar); + AddOption("--experimental-vfs", + "experimental node:vfs module", + &EnvironmentOptions::experimental_vfs, + kAllowedInEnvvar); AddOption("--experimental-quic", #ifndef OPENSSL_NO_QUIC "experimental QUIC support", diff --git a/src/node_options.h b/src/node_options.h index 3e6ecc51c78fec..ab74dd31daab05 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -132,6 +132,7 @@ class EnvironmentOptions : public Options { bool experimental_websocket = true; bool experimental_sqlite = HAVE_SQLITE; bool experimental_stream_iter = EXPERIMENTALS_DEFAULT_VALUE; + bool experimental_vfs = EXPERIMENTALS_DEFAULT_VALUE; bool webstorage = HAVE_SQLITE; bool experimental_dtls = EXPERIMENTALS_DEFAULT_VALUE; bool experimental_quic = EXPERIMENTALS_DEFAULT_VALUE; diff --git a/test/parallel/test-process-get-builtin.mjs b/test/parallel/test-process-get-builtin.mjs index 2295c160a874ac..fa92dc52f96916 100644 --- a/test/parallel/test-process-get-builtin.mjs +++ b/test/parallel/test-process-get-builtin.mjs @@ -40,6 +40,8 @@ if (!hasIntl) { publicBuiltins.delete('node:dtls'); // TODO(@jasnell): Remove this once node:quic graduates from unflagged. publicBuiltins.delete('node:quic'); +// Remove this once node:vfs graduates from unflagged. +publicBuiltins.delete('node:vfs'); if (!hasInspector) { publicBuiltins.delete('inspector'); diff --git a/test/parallel/test-require-resolve.js b/test/parallel/test-require-resolve.js index 281b0cc6087ab9..44422706e186cc 100644 --- a/test/parallel/test-require-resolve.js +++ b/test/parallel/test-require-resolve.js @@ -64,6 +64,8 @@ require(fixtures.path('resolve-paths', 'default', 'verify-paths.js')); if (mod === 'node:quic') return; // TODO: Remove once node:ffi is no longer flagged if (mod === 'node:ffi') return; + // Remove once node:vfs is no longer flagged + if (mod === 'node:vfs') return; if (mod === 'node:sqlite' && !common.hasSQLite) return; assert.strictEqual(require.resolve.paths(mod), null); if (!mod.startsWith('node:')) { diff --git a/test/parallel/test-vfs-access-modes.js b/test/parallel/test-vfs-access-modes.js new file mode 100644 index 00000000000000..6b4854b204aebc --- /dev/null +++ b/test/parallel/test-vfs-access-modes.js @@ -0,0 +1,41 @@ +// Flags: --experimental-vfs +'use strict'; + +// access / accessSync honour the R_OK / W_OK / X_OK / F_OK mode bits and +// throw EACCES when the file's permission bits don't allow the request. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const vfs = require('node:vfs'); + +const { R_OK, W_OK, X_OK } = fs.constants; + +const myVfs = vfs.create(); + +// No read permission (mode 0o222 → owner has W only) +myVfs.writeFileSync('/no-r.txt', 'x'); +myVfs.chmodSync('/no-r.txt', 0o222); +assert.throws(() => myVfs.accessSync('/no-r.txt', R_OK), { code: 'EACCES' }); +assert.rejects(myVfs.promises.access('/no-r.txt', R_OK), + { code: 'EACCES' }).then(common.mustCall()); + +// No write permission (mode 0o444 → owner has R only) +myVfs.writeFileSync('/no-w.txt', 'x'); +myVfs.chmodSync('/no-w.txt', 0o444); +assert.throws(() => myVfs.accessSync('/no-w.txt', W_OK), { code: 'EACCES' }); +assert.rejects(myVfs.promises.access('/no-w.txt', W_OK), + { code: 'EACCES' }).then(common.mustCall()); + +// No execute permission (mode 0o644) +myVfs.writeFileSync('/no-x.txt', 'x'); +myVfs.chmodSync('/no-x.txt', 0o644); +assert.throws(() => myVfs.accessSync('/no-x.txt', X_OK), { code: 'EACCES' }); +assert.rejects(myVfs.promises.access('/no-x.txt', X_OK), + { code: 'EACCES' }).then(common.mustCall()); + +// F_OK (mode 0) is an existence-only check and does not require permission +myVfs.accessSync('/no-r.txt', 0); + +// Mode passed as null also exits early (existence-only) +myVfs.accessSync('/no-r.txt', null); diff --git a/test/parallel/test-vfs-append-write.js b/test/parallel/test-vfs-append-write.js new file mode 100644 index 00000000000000..fee8a137adee35 --- /dev/null +++ b/test/parallel/test-vfs-append-write.js @@ -0,0 +1,19 @@ +// Flags: --experimental-vfs +'use strict'; + +// writeSync in append mode must append, not overwrite. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/append.txt', 'init'); + +const fd = myVfs.openSync('/append.txt', 'a'); +const buf = Buffer.from(' more'); +myVfs.writeSync(fd, buf, 0, buf.length); +myVfs.closeSync(fd); + +const content = myVfs.readFileSync('/append.txt', 'utf8'); +assert.strictEqual(content, 'init more'); diff --git a/test/parallel/test-vfs-bigint-position.js b/test/parallel/test-vfs-bigint-position.js new file mode 100644 index 00000000000000..d869f93947f852 --- /dev/null +++ b/test/parallel/test-vfs-bigint-position.js @@ -0,0 +1,18 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS readSync should accept a BigInt position parameter. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'abcde'); + +const fd = myVfs.openSync('/file.txt', 'r'); +const buf = Buffer.alloc(1); +const bytesRead = myVfs.readSync(fd, buf, 0, 1, 1n); +assert.strictEqual(bytesRead, 1); +assert.strictEqual(buf.toString(), 'b'); +myVfs.closeSync(fd); diff --git a/test/parallel/test-vfs-callback-api.js b/test/parallel/test-vfs-callback-api.js new file mode 100644 index 00000000000000..dd95a5d75877e5 --- /dev/null +++ b/test/parallel/test-vfs-callback-api.js @@ -0,0 +1,154 @@ +// Flags: --experimental-vfs +'use strict'; + +// Exercise the VFS callback-style async API on every method. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.mkdirSync('/dir/sub', { recursive: true }); +myVfs.writeFileSync('/dir/file.txt', 'hello'); +myVfs.writeFileSync('/dir/other.txt', 'other'); + +// readFile (no options) +myVfs.readFile('/dir/file.txt', common.mustSucceed((data) => { + assert.ok(Buffer.isBuffer(data)); +})); + +// writeFile + appendFile (no options) -> readFile +myVfs.writeFile('/cb-write.txt', 'a', common.mustSucceed(() => { + myVfs.readFile('/cb-write.txt', 'utf8', common.mustSucceed((data) => { + assert.strictEqual(data, 'a'); + })); +})); + +// stat / lstat (with and without options) +myVfs.stat('/dir/file.txt', common.mustSucceed((st) => { + assert.strictEqual(st.size, 5); +})); +myVfs.stat('/dir/file.txt', { bigint: true }, common.mustSucceed((st) => { + assert.strictEqual(typeof st.size, 'bigint'); +})); +myVfs.lstat('/dir/file.txt', common.mustSucceed((st) => { + assert.ok(st.isFile()); +})); + +// readdir +myVfs.readdir('/dir', common.mustSucceed((names) => { + assert.ok(names.includes('file.txt')); +})); + +// realpath +myVfs.realpath('/dir/file.txt', common.mustSucceed((p) => { + assert.strictEqual(p, '/dir/file.txt'); +})); + +// access (with and without mode) +myVfs.access('/dir/file.txt', common.mustSucceed()); +myVfs.access('/dir/file.txt', 0, common.mustSucceed()); +myVfs.access('/missing.txt', common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); +})); + +// open / read / write / close cb chain +myVfs.open('/dir/file.txt', common.mustSucceed((fd) => { + const buf = Buffer.alloc(5); + myVfs.read(fd, buf, 0, 5, 0, common.mustSucceed((bytesRead) => { + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buf.toString(), 'hello'); + myVfs.close(fd, common.mustSucceed()); + })); +})); + +// Open with explicit flags / mode +myVfs.open('/dir/new1.txt', 'w', common.mustSucceed((fd) => { + const buf = Buffer.from('xyz'); + myVfs.write(fd, buf, 0, 3, 0, common.mustSucceed((bytesWritten) => { + assert.strictEqual(bytesWritten, 3); + myVfs.fstat(fd, common.mustSucceed((st) => { + assert.strictEqual(st.size, 3); + myVfs.ftruncate(fd, 1, common.mustSucceed(() => { + myVfs.close(fd, common.mustCall()); + })); + })); + })); +})); + +// Open with explicit flags, no mode arg form +myVfs.open('/dir/new2.txt', 'w', 0o644, common.mustSucceed((fd) => { + myVfs.close(fd, common.mustCall()); +})); + +// rm callback (file) +myVfs.writeFileSync('/cb-rm.txt', 'x'); +myVfs.rm('/cb-rm.txt', common.mustSucceed(() => { + assert.strictEqual(myVfs.existsSync('/cb-rm.txt'), false); +})); + +// Rm callback with options (recursive) +myVfs.mkdirSync('/cb-rm-dir/sub', { recursive: true }); +myVfs.writeFileSync('/cb-rm-dir/sub/f.txt', 'x'); +myVfs.rm('/cb-rm-dir', { recursive: true }, common.mustSucceed()); + +// Rm callback failure path +myVfs.rm('/missing-rm', common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); +})); + +// truncate / ftruncate cb +myVfs.writeFileSync('/cb-tr.txt', 'abcdef'); +myVfs.truncate('/cb-tr.txt', 3, common.mustSucceed(() => { + assert.strictEqual(myVfs.readFileSync('/cb-tr.txt', 'utf8'), 'abc'); +})); +myVfs.truncate('/missing-tr.txt', common.mustCall((err) => { + assert.ok(err); +})); +myVfs.ftruncate(0xFFFFFFF /* invalid fd */, common.mustCall((err) => { + assert.strictEqual(err.code, 'EBADF'); +})); + +// link cb +myVfs.writeFileSync('/cb-link-src.txt', 'x'); +myVfs.link('/cb-link-src.txt', '/cb-link-dst.txt', common.mustSucceed()); +myVfs.link('/missing-src.txt', '/cb-bad-link.txt', common.mustCall((err) => { + assert.ok(err); +})); + +// mkdtemp cb +myVfs.mkdtemp('/tmp-', common.mustSucceed((p) => { + assert.ok(p.startsWith('/tmp-')); +})); +myVfs.mkdtemp('/tmp-', {}, common.mustSucceed((p) => { + assert.ok(p.startsWith('/tmp-')); +})); + +// opendir cb +myVfs.opendir('/dir', common.mustSucceed((dir) => { + assert.strictEqual(dir.path, '/dir'); + dir.closeSync(); +})); +myVfs.opendir('/missing-dir', common.mustCall((err) => { + assert.ok(err); +})); + +// EBADF callback paths +myVfs.read(0xFFFFFFF, Buffer.alloc(1), 0, 1, 0, common.mustCall((err) => { + assert.strictEqual(err.code, 'EBADF'); +})); +myVfs.write(0xFFFFFFF, Buffer.alloc(1), 0, 1, 0, common.mustCall((err) => { + assert.strictEqual(err.code, 'EBADF'); +})); +myVfs.close(0xFFFFFFF, common.mustCall((err) => { + assert.strictEqual(err.code, 'EBADF'); +})); +myVfs.fstat(0xFFFFFFF, common.mustCall((err) => { + assert.strictEqual(err.code, 'EBADF'); +})); + +// readlink cb +myVfs.symlinkSync('/dir/file.txt', '/cb-link'); +myVfs.readlink('/cb-link', common.mustSucceed((target) => { + assert.strictEqual(target, '/dir/file.txt'); +})); diff --git a/test/parallel/test-vfs-copyfile-mode.js b/test/parallel/test-vfs-copyfile-mode.js new file mode 100644 index 00000000000000..2d701f4f105d27 --- /dev/null +++ b/test/parallel/test-vfs-copyfile-mode.js @@ -0,0 +1,52 @@ +// Flags: --experimental-vfs +'use strict'; + +// Tests for VFS copyFile mode support: +// - COPYFILE_EXCL throws when destination exists +// - Without COPYFILE_EXCL, copy overwrites destination + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const vfs = require('node:vfs'); + +const { COPYFILE_EXCL } = fs.constants; + +// copyFileSync with COPYFILE_EXCL throws when destination exists. +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/src.txt', 'src'); + myVfs.writeFileSync('/dst.txt', 'dst'); + + assert.throws( + () => myVfs.copyFileSync('/src.txt', '/dst.txt', COPYFILE_EXCL), + { code: 'EEXIST' }, + ); + + assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 'dst'); +} + +// copyFileSync without COPYFILE_EXCL succeeds and overwrites. +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/src.txt', 'new-content'); + myVfs.writeFileSync('/dst.txt', 'old'); + + myVfs.copyFileSync('/src.txt', '/dst.txt'); + assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 'new-content'); +} + +// promises.copyFile with COPYFILE_EXCL +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/src.txt', 's'); + myVfs.writeFileSync('/dst.txt', 'd'); + + await assert.rejects( + myVfs.promises.copyFile('/src.txt', '/dst.txt', COPYFILE_EXCL), + { code: 'EEXIST' }, + ); + + await myVfs.promises.copyFile('/src.txt', '/dst.txt'); + assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 's'); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-create.js b/test/parallel/test-vfs-create.js new file mode 100644 index 00000000000000..764e276f0e9148 --- /dev/null +++ b/test/parallel/test-vfs-create.js @@ -0,0 +1,65 @@ +// Flags: --experimental-vfs +'use strict'; + +// Constructor variants and option validation for vfs.create() and +// `new VirtualFileSystem(...)`. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// vfs.create() with no arguments uses the default MemoryProvider +{ + const myVfs = vfs.create(); + assert.ok(myVfs.provider instanceof vfs.MemoryProvider); +} + +// vfs.create with first arg as options object (no provider) +{ + const myVfs = vfs.create({ emitExperimentalWarning: false }); + assert.ok(myVfs); + assert.ok(myVfs.provider instanceof vfs.MemoryProvider); +} + +// vfs.create with explicit provider +{ + const provider = new vfs.MemoryProvider(); + const myVfs = vfs.create(provider); + assert.strictEqual(myVfs.provider, provider); +} + +// new VirtualFileSystem(options) directly +{ + const myVfs = new vfs.VirtualFileSystem({ emitExperimentalWarning: false }); + assert.ok(myVfs); +} + +// emitExperimentalWarning option must be a boolean +{ + assert.throws( + () => new vfs.VirtualFileSystem({ emitExperimentalWarning: 'not-bool' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// existsSync swallows ALL errors from the provider, not just ENOENT +{ + class ThrowingProvider extends vfs.VirtualProvider { + existsSync() { throw new Error('boom'); } + } + const myVfs = vfs.create(new ThrowingProvider()); + assert.strictEqual(myVfs.existsSync('/anything'), false); +} + +// Walking a path through a regular-file parent throws ENOTDIR +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'x'); + assert.throws(() => myVfs.writeFileSync('/file.txt/oops', 'y'), + { code: 'ENOTDIR' }); +} + +// statSync('/') returns the root directory +{ + const myVfs = vfs.create(); + assert.ok(myVfs.statSync('/').isDirectory()); +} diff --git a/test/parallel/test-vfs-ctime-update.js b/test/parallel/test-vfs-ctime-update.js new file mode 100644 index 00000000000000..227732ce74cde0 --- /dev/null +++ b/test/parallel/test-vfs-ctime-update.js @@ -0,0 +1,49 @@ +// Flags: --experimental-vfs +'use strict'; + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Test that writeFileSync updates both mtime and ctime +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'initial'); + + const stat1 = myVfs.statSync('/file.txt'); + const oldCtime = stat1.ctimeMs; + + myVfs.writeFileSync('/file.txt', 'updated'); + const stat2 = myVfs.statSync('/file.txt'); + assert.ok(stat2.mtimeMs >= oldCtime); + assert.ok(stat2.ctimeMs >= oldCtime); + // Ctime and mtime should be the same value (both set from same write) + assert.strictEqual(stat2.ctimeMs, stat2.mtimeMs); +} + +// Test that writeSync via file handle updates ctime +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'initial'); + + const fd = myVfs.openSync('/file.txt', 'r+'); + const buf = Buffer.from('X'); + myVfs.writeSync(fd, buf, 0, 1, 0); + myVfs.closeSync(fd); + + const stat = myVfs.statSync('/file.txt'); + assert.strictEqual(stat.ctimeMs, stat.mtimeMs); +} + +// Test that truncateSync updates ctime +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'some content'); + + const fd = myVfs.openSync('/file.txt', 'r+'); + myVfs.ftruncateSync(fd, 0); + myVfs.closeSync(fd); + + const stat = myVfs.statSync('/file.txt'); + assert.strictEqual(stat.ctimeMs, stat.mtimeMs); +} diff --git a/test/parallel/test-vfs-dir-handle.js b/test/parallel/test-vfs-dir-handle.js new file mode 100644 index 00000000000000..6a26a69ce2d485 --- /dev/null +++ b/test/parallel/test-vfs-dir-handle.js @@ -0,0 +1,114 @@ +// Flags: --experimental-vfs +'use strict'; + +// Exercise the VirtualDir handle returned by opendirSync. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.mkdirSync('/d'); +myVfs.writeFileSync('/d/a.txt', 'a'); +myVfs.writeFileSync('/d/b.txt', 'b'); +myVfs.mkdirSync('/d/sub'); + +// readSync iteration +{ + const dir = myVfs.opendirSync('/d'); + assert.strictEqual(dir.path, '/d'); + const names = []; + let entry; + while ((entry = dir.readSync()) !== null) { + names.push(entry.name); + } + assert.deepStrictEqual(names.sort(), ['a.txt', 'b.txt', 'sub']); + dir.closeSync(); + // Closing again must throw + assert.throws(() => dir.closeSync(), { code: 'ERR_DIR_CLOSED' }); + // Reading after close throws + assert.throws(() => dir.readSync(), { code: 'ERR_DIR_CLOSED' }); +} + +// for-await iteration +(async () => { + const dir = myVfs.opendirSync('/d'); + const names = []; + for await (const entry of dir) { + names.push(entry.name); + } + assert.deepStrictEqual(names.sort(), ['a.txt', 'b.txt', 'sub']); +})().then(common.mustCall()); + +// Async read with callback +(async () => { + const dir = myVfs.opendirSync('/d'); + await new Promise((resolve, reject) => { + dir.read((err, entry) => { + if (err) reject(err); + else resolve(entry); + }); + }); + await new Promise((resolve, reject) => { + dir.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); +})().then(common.mustCall()); + +// Async read without callback returns a promise +(async () => { + const dir = myVfs.opendirSync('/d'); + const entry = await dir.read(); + assert.ok(entry); + await dir.close(); +})().then(common.mustCall()); + +// using/explicit resource management +{ + const dir = myVfs.opendirSync('/d'); + dir[Symbol.dispose](); + assert.throws(() => dir.readSync(), { code: 'ERR_DIR_CLOSED' }); +} + +// opendir (callback) +myVfs.opendir('/d', common.mustSucceed((dir) => { + assert.strictEqual(dir.path, '/d'); + dir.closeSync(); +})); + +// read() callback on a closed dir delivers ERR_DIR_CLOSED +{ + const dir = myVfs.opendirSync('/d'); + dir.closeSync(); + dir.read(common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_DIR_CLOSED'); + })); + // entries() iteration on a closed dir rejects with ERR_DIR_CLOSED + (async () => { + await assert.rejects( + // eslint-disable-next-line no-unused-vars + (async () => { for await (const _ of dir.entries()); })(), + { code: 'ERR_DIR_CLOSED' }); + })().then(common.mustCall()); + // [Symbol.dispose] is a no-op on an already-closed dir (must not throw) + dir[Symbol.dispose](); +} + +// Async dir.close() returns a promise when invoked without a callback +(async () => { + const dir = myVfs.opendirSync('/d'); + await dir.close(); +})().then(common.mustCall()); + +// opendirSync without options object +{ + const dir = myVfs.opendirSync('/d'); + dir.closeSync(); +} + +// Opendir error path (missing directory) +myVfs.opendir('/missing-dir', common.mustCall((err) => { + assert.ok(err); +})); diff --git a/test/parallel/test-vfs-fd.js b/test/parallel/test-vfs-fd.js new file mode 100644 index 00000000000000..ec9145189da299 --- /dev/null +++ b/test/parallel/test-vfs-fd.js @@ -0,0 +1,319 @@ +// Flags: --experimental-vfs +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Test openSync and closeSync +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'hello world'); + + const fd = myVfs.openSync('/file.txt'); + assert.ok((fd & 0x40000000) !== 0, 'VFS fd should have bit 30 set'); + myVfs.closeSync(fd); +} + +// Test openSync with non-existent file +{ + const myVfs = vfs.create(); + + assert.throws(() => { + myVfs.openSync('/nonexistent.txt'); + }, { code: 'ENOENT' }); +} + +// Test openSync with directory +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/mydir', { recursive: true }); + + assert.throws(() => { + myVfs.openSync('/mydir'); + }, { code: 'EISDIR' }); +} + +// Test closeSync with invalid fd +{ + const myVfs = vfs.create(); + + assert.throws(() => { + myVfs.closeSync(12345); + }, { code: 'EBADF' }); +} + +// Test readSync +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'hello world'); + + const fd = myVfs.openSync('/file.txt'); + const buffer = Buffer.alloc(5); + + const bytesRead = myVfs.readSync(fd, buffer, 0, 5, 0); + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buffer.toString(), 'hello'); + + myVfs.closeSync(fd); +} + +// Test readSync with position tracking +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'hello world'); + + const fd = myVfs.openSync('/file.txt'); + const buffer1 = Buffer.alloc(5); + const buffer2 = Buffer.alloc(6); + + // Read first 5 bytes + let bytesRead = myVfs.readSync(fd, buffer1, 0, 5, null); + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buffer1.toString(), 'hello'); + + // Continue reading (position should advance) + bytesRead = myVfs.readSync(fd, buffer2, 0, 6, null); + assert.strictEqual(bytesRead, 6); + assert.strictEqual(buffer2.toString(), ' world'); + + myVfs.closeSync(fd); +} + +// Test readSync with explicit position +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'hello world'); + + const fd = myVfs.openSync('/file.txt'); + const buffer = Buffer.alloc(5); + + // Read from position 6 (start of "world") + const bytesRead = myVfs.readSync(fd, buffer, 0, 5, 6); + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buffer.toString(), 'world'); + + myVfs.closeSync(fd); +} + +// Test readSync at end of file +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'short'); + + const fd = myVfs.openSync('/file.txt'); + const buffer = Buffer.alloc(10); + + // Read from position beyond file + const bytesRead = myVfs.readSync(fd, buffer, 0, 10, 100); + assert.strictEqual(bytesRead, 0); + + myVfs.closeSync(fd); +} + +// Test readSync with invalid fd +{ + const myVfs = vfs.create(); + const buffer = Buffer.alloc(10); + + assert.throws(() => { + myVfs.readSync(99999, buffer, 0, 10, 0); + }, { code: 'EBADF' }); +} + +// Test fstatSync +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'hello world'); + + const fd = myVfs.openSync('/file.txt'); + const stats = myVfs.fstatSync(fd); + + assert.strictEqual(stats.isFile(), true); + assert.strictEqual(stats.isDirectory(), false); + assert.strictEqual(stats.size, 11); + + myVfs.closeSync(fd); +} + +// Test fstatSync with invalid fd +{ + const myVfs = vfs.create(); + + assert.throws(() => { + myVfs.fstatSync(99999); + }, { code: 'EBADF' }); +} + +// Test async open and close +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/async-file.txt', 'async content'); + + myVfs.open('/async-file.txt', common.mustCall((err, fd) => { + assert.strictEqual(err, null); + assert.ok((fd & 0x40000000) !== 0); + + myVfs.close(fd, common.mustCall((err) => { + assert.strictEqual(err, null); + })); + })); +} + +// Test async open with error +{ + const myVfs = vfs.create(); + + myVfs.open('/nonexistent.txt', common.mustCall((err, fd) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(fd, undefined); + })); +} + +// Test async close with invalid fd +{ + const myVfs = vfs.create(); + + myVfs.close(99999, common.mustCall((err) => { + assert.strictEqual(err.code, 'EBADF'); + })); +} + +// Test async read +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/read-test.txt', 'read content'); + + myVfs.open('/read-test.txt', common.mustCall((err, fd) => { + assert.strictEqual(err, null); + + const buffer = Buffer.alloc(4); + myVfs.read(fd, buffer, 0, 4, 0, common.mustCall((err, bytesRead, buf) => { + assert.strictEqual(err, null); + assert.strictEqual(bytesRead, 4); + assert.strictEqual(buf, buffer); + assert.strictEqual(buffer.toString(), 'read'); + + myVfs.close(fd, common.mustCall()); + })); + })); +} + +// Test async read with position tracking +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/track-test.txt', 'ABCDEFGHIJ'); + + myVfs.open('/track-test.txt', common.mustCall((err, fd) => { + assert.strictEqual(err, null); + + const buffer1 = Buffer.alloc(3); + const buffer2 = Buffer.alloc(3); + + myVfs.read(fd, buffer1, 0, 3, null, common.mustCall((err, bytesRead, buf) => { + assert.strictEqual(err, null); + assert.strictEqual(bytesRead, 3); + assert.strictEqual(buffer1.toString(), 'ABC'); + + // Continue reading without explicit position + myVfs.read(fd, buffer2, 0, 3, null, common.mustCall((err, bytesRead, buf) => { + assert.strictEqual(err, null); + assert.strictEqual(bytesRead, 3); + assert.strictEqual(buffer2.toString(), 'DEF'); + + myVfs.close(fd, common.mustCall()); + })); + })); + })); +} + +// Test async read with invalid fd +{ + const myVfs = vfs.create(); + const buffer = Buffer.alloc(10); + + myVfs.read(99999, buffer, 0, 10, 0, common.mustCall((err, bytesRead, buf) => { + assert.strictEqual(err.code, 'EBADF'); + })); +} + +// Test async fstat +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/fstat-test.txt', '12345'); + + myVfs.open('/fstat-test.txt', common.mustCall((err, fd) => { + assert.strictEqual(err, null); + + myVfs.fstat(fd, common.mustCall((err, stats) => { + assert.strictEqual(err, null); + assert.strictEqual(stats.isFile(), true); + assert.strictEqual(stats.size, 5); + + myVfs.close(fd, common.mustCall()); + })); + })); +} + +// Test async fstat with invalid fd +{ + const myVfs = vfs.create(); + + myVfs.fstat(99999, common.mustCall((err, stats) => { + assert.strictEqual(err.code, 'EBADF'); + })); +} + +// Test that separate VFS instances have separate fd spaces +{ + const vfs1 = vfs.create(); + const vfs2 = vfs.create(); + + vfs1.writeFileSync('/file1.txt', 'content1'); + vfs2.writeFileSync('/file2.txt', 'content2'); + + const fd1 = vfs1.openSync('/file1.txt'); + const fd2 = vfs2.openSync('/file2.txt'); + + // Both should get valid fds + assert.ok((fd1 & 0x40000000) !== 0); + assert.ok((fd2 & 0x40000000) !== 0); + + // Read from fd1 using vfs1 + const buf1 = Buffer.alloc(8); + const read1 = vfs1.readSync(fd1, buf1, 0, 8, 0); + assert.strictEqual(read1, 8); + assert.strictEqual(buf1.toString(), 'content1'); + + // Read from fd2 using vfs2 + const buf2 = Buffer.alloc(8); + const read2 = vfs2.readSync(fd2, buf2, 0, 8, 0); + assert.strictEqual(read2, 8); + assert.strictEqual(buf2.toString(), 'content2'); + + vfs1.closeSync(fd1); + vfs2.closeSync(fd2); +} + +// Test multiple opens of same file +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/multi.txt', 'multi content'); + + const fd1 = myVfs.openSync('/multi.txt'); + const fd2 = myVfs.openSync('/multi.txt'); + + assert.notStrictEqual(fd1, fd2); + + const buf1 = Buffer.alloc(5); + const buf2 = Buffer.alloc(5); + + myVfs.readSync(fd1, buf1, 0, 5, 0); + myVfs.readSync(fd2, buf2, 0, 5, 0); + + assert.strictEqual(buf1.toString(), 'multi'); + assert.strictEqual(buf2.toString(), 'multi'); + + myVfs.closeSync(fd1); + myVfs.closeSync(fd2); +} diff --git a/test/parallel/test-vfs-file-handle.js b/test/parallel/test-vfs-file-handle.js new file mode 100644 index 00000000000000..6c653593ce7ea2 --- /dev/null +++ b/test/parallel/test-vfs-file-handle.js @@ -0,0 +1,205 @@ +// Flags: --experimental-vfs +'use strict'; + +// Exercise VirtualFileHandle / MemoryFileHandle methods directly via +// the promises.open() handle returned by VFS. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'hello world'); + +(async () => { + // Open file via provider directly (returns a real FileHandle) + const handle = await myVfs.provider.open('/file.txt', 'r'); + assert.ok(handle); + assert.strictEqual(handle.path, '/file.txt'); + assert.strictEqual(handle.flags, 'r'); + assert.strictEqual(typeof handle.mode, 'number'); + assert.strictEqual(handle.closed, false); + + // readFile + const content = await handle.readFile('utf8'); + assert.strictEqual(content, 'hello world'); + + // stat + const stats = await handle.stat(); + assert.strictEqual(stats.size, 11); + + // read into buffer + const buf = Buffer.alloc(5); + const { bytesRead } = await handle.read(buf, 0, 5, 0); + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buf.toString(), 'hello'); + + // readv + const b1 = Buffer.alloc(5); + const b2 = Buffer.alloc(6); + const readvResult = await handle.readv([b1, b2], 0); + assert.strictEqual(readvResult.bytesRead, 11); + assert.strictEqual(b1.toString(), 'hello'); + assert.strictEqual(b2.toString(), ' world'); + + // no-op metadata methods + await handle.chmod(); + await handle.chown(); + await handle.utimes(); + await handle.datasync(); + await handle.sync(); + + await handle.close(); + assert.strictEqual(handle.closed, true); +})().then(common.mustCall()); + +// Write mode: truncate, write, writev, appendFile, truncate +(async () => { + const handle = await myVfs.provider.open('/out.txt', 'w+'); + + // No explicit position so the handle position advances naturally + await handle.write(Buffer.from('hello'), 0, 5); + await handle.writev([Buffer.from(' '), Buffer.from('world')]); + + const stats = await handle.stat(); + assert.strictEqual(stats.size, 11); + + await handle.appendFile('!'); + const content = await handle.readFile('utf8'); + assert.strictEqual(content, 'hello world!'); + + await handle.truncate(5); + const truncated = await handle.readFile('utf8'); + assert.strictEqual(truncated, 'hello'); + + await handle.close(); +})().then(common.mustCall()); + +// readSync / writeSync / readFileSync / writeFileSync / statSync / truncateSync / closeSync +{ + const fd = myVfs.openSync('/sync.txt', 'w'); + const buf = Buffer.from('abc'); + myVfs.writeSync(fd, buf, 0, 3, 0); + myVfs.closeSync(fd); + + const fd2 = myVfs.openSync('/sync.txt', 'r'); + const out = Buffer.alloc(3); + myVfs.readSync(fd2, out, 0, 3, 0); + assert.strictEqual(out.toString(), 'abc'); + const stats = myVfs.fstatSync(fd2); + assert.strictEqual(stats.size, 3); + myVfs.closeSync(fd2); +} + +// using-style explicit resource management for handles +(async () => { + const handle = await myVfs.provider.open('/file.txt', 'r'); + await handle[Symbol.asyncDispose](); + assert.strictEqual(handle.closed, true); +})().then(common.mustCall()); + +// readableWebStream / readLines / createReadStream / createWriteStream throw +(async () => { + const handle = await myVfs.provider.open('/file.txt', 'r'); + assert.throws(() => handle.readableWebStream(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); + assert.throws(() => handle.readLines(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); + assert.throws(() => handle.createReadStream(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); + assert.throws(() => handle.createWriteStream(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); + await handle.close(); +})().then(common.mustCall()); + +// Operations after close throw EBADF +(async () => { + const handle = await myVfs.provider.open('/file.txt', 'r'); + await handle.close(); + await assert.rejects(handle.read(Buffer.alloc(1), 0, 1, 0), + { code: 'EBADF' }); + assert.throws(() => handle.readSync(Buffer.alloc(1), 0, 1, 0), + { code: 'EBADF' }); + await assert.rejects(handle.stat(), { code: 'EBADF' }); +})().then(common.mustCall()); + +// Readv with a partial read at EOF (second buffer larger than remaining) +(async () => { + const handle = await myVfs.provider.open('/file.txt', 'r'); + const b1 = Buffer.alloc(5); + const b2 = Buffer.alloc(20); + const r = await handle.readv([b1, b2], 0); + assert.strictEqual(b1.toString(), 'hello'); + assert.strictEqual(r.bytesRead, 11); + await handle.close(); +})().then(common.mustCall()); + +// Writev with explicit position 0 +(async () => { + const wh = await myVfs.provider.open('/wv.txt', 'w+'); + await wh.writev([Buffer.from('AB'), Buffer.from('CD')], 0); + await wh.close(); + assert.strictEqual(myVfs.readFileSync('/wv.txt', 'utf8'), 'ABCD'); +})().then(common.mustCall()); + +// appendFile with string + encoding option +(async () => { + const ah = await myVfs.provider.open('/ap.txt', 'a+'); + await ah.appendFile('hello', { encoding: 'utf8' }); + await ah.close(); + assert.strictEqual(myVfs.readFileSync('/ap.txt', 'utf8'), 'hello'); +})().then(common.mustCall()); + +// 'w'-mode handle rejects all read ops with EBADF +(async () => { + const handle = await myVfs.provider.open('/wonly.txt', 'w'); + assert.throws(() => handle.readSync(Buffer.alloc(1), 0, 1, 0), + { code: 'EBADF' }); + await assert.rejects(handle.read(Buffer.alloc(1), 0, 1, 0), + { code: 'EBADF' }); + assert.throws(() => handle.readFileSync(), { code: 'EBADF' }); + await assert.rejects(handle.readFile(), { code: 'EBADF' }); + await handle.close(); +})().then(common.mustCall()); + +// 'r'-mode handle rejects all write ops with EBADF +(async () => { + myVfs.writeFileSync('/ronly.txt', 'x'); + const handle = await myVfs.provider.open('/ronly.txt', 'r'); + assert.throws(() => handle.writeSync(Buffer.from('y'), 0, 1, 0), + { code: 'EBADF' }); + await assert.rejects(handle.write(Buffer.from('y'), 0, 1, 0), + { code: 'EBADF' }); + assert.throws(() => handle.writeFileSync('y'), { code: 'EBADF' }); + await assert.rejects(handle.writeFile('y'), { code: 'EBADF' }); + assert.throws(() => handle.truncateSync(0), { code: 'EBADF' }); + await assert.rejects(handle.truncate(0), { code: 'EBADF' }); + await handle.close(); +})().then(common.mustCall()); + +// writeFile with string + encoding +(async () => { + const handle = await myVfs.provider.open('/se.txt', 'w+'); + await handle.writeFile('héllo', { encoding: 'utf8' }); + assert.strictEqual(await handle.readFile('utf8'), 'héllo'); + await handle.close(); +})().then(common.mustCall()); + +// Truncate extending past current size zero-fills +(async () => { + const handle = await myVfs.provider.open('/grow.txt', 'w+'); + await handle.writeFile('abc'); + await handle.truncate(10); + assert.strictEqual((await handle.stat()).size, 10); + assert.strictEqual((await handle.readFile()).length, 10); + await handle.close(); +})().then(common.mustCall()); + +// readv / writev / appendFile on a closed handle reject with EBADF +(async () => { + const handle = await myVfs.provider.open('/file.txt', 'r'); + await handle.close(); + await assert.rejects(handle.readv([Buffer.alloc(1)], 0), { code: 'EBADF' }); + await assert.rejects(handle.writev([Buffer.alloc(1)], 0), { code: 'EBADF' }); + await assert.rejects(handle.appendFile('x'), { code: 'EBADF' }); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-flag.js b/test/parallel/test-vfs-flag.js new file mode 100644 index 00000000000000..14e34c9e8be9f7 --- /dev/null +++ b/test/parallel/test-vfs-flag.js @@ -0,0 +1,59 @@ +'use strict'; + +// node:vfs is gated behind --experimental-vfs. Without the flag the +// module is not exposed; bare `vfs` (without the node: scheme) is also +// blocked. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +// Without the flag, requiring node:vfs throws ERR_UNKNOWN_BUILTIN_MODULE. +{ + spawnSyncAndAssert(process.execPath, [ + '-e', 'require("node:vfs")', + ], { status: 1, stderr: /ERR_UNKNOWN_BUILTIN_MODULE/ }); +} + +// Without the flag, importing node:vfs throws ERR_UNKNOWN_BUILTIN_MODULE. +{ + spawnSyncAndAssert(process.execPath, [ + '--input-type=module', + '-e', 'import("node:vfs").catch((e) => { console.error(e.code); process.exit(1); });', + ], { + status: 1, + stderr: /ERR_UNKNOWN_BUILTIN_MODULE/, + }); +} + +// With the flag, node:vfs loads and works. +{ + const script = + 'const v = require("node:vfs");' + + 'const x = v.create();' + + 'x.writeFileSync("/x", "hi");' + + 'console.log(x.readFileSync("/x", "utf8"));'; + spawnSyncAndAssert(process.execPath, ['--experimental-vfs', '-e', script], { + stdout: 'hi', + trim: true, + }); +} + +// Bare `vfs` (no node: scheme) is always blocked. +{ + spawnSyncAndAssert(process.execPath, [ + '--experimental-vfs', + '-e', "require('vfs')", + ], { status: 1, stderr: /Cannot find module 'vfs'/ }); +} + +// Module.builtinModules reflects whether --experimental-vfs is active. +for (const [flag, expected] of [ + ['--experimental-vfs', 'true\n'], + ['--no-experimental-vfs', 'false\n'], +]) { + spawnSyncAndAssert(process.execPath, [ + flag, + '-p', + 'require("node:module").builtinModules.includes("node:vfs")', + ], { stdout: expected, stderr: '' }); +} diff --git a/test/parallel/test-vfs-hardlink-nlink.js b/test/parallel/test-vfs-hardlink-nlink.js new file mode 100644 index 00000000000000..8a58d0e9d59081 --- /dev/null +++ b/test/parallel/test-vfs-hardlink-nlink.js @@ -0,0 +1,32 @@ +// Flags: --experimental-vfs +'use strict'; + +// Test that nlink count is updated correctly when creating/removing hard links. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/src.txt', 'content'); + +// Initially nlink should be 1 +assert.strictEqual(myVfs.statSync('/src.txt').nlink, 1); + +// After hard link, nlink should be 2 on both +myVfs.linkSync('/src.txt', '/link.txt'); +assert.strictEqual(myVfs.statSync('/src.txt').nlink, 2); +assert.strictEqual(myVfs.statSync('/link.txt').nlink, 2); + +// Removing one decrements nlink on the other +myVfs.unlinkSync('/link.txt'); +assert.strictEqual(myVfs.statSync('/src.txt').nlink, 1); + +// promises.link equivalent +(async () => { + const v = vfs.create(); + v.writeFileSync('/a', 'x'); + await v.promises.link('/a', '/b'); + assert.strictEqual(v.statSync('/a').nlink, 2); + assert.strictEqual(v.statSync('/b').nlink, 2); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-link.js b/test/parallel/test-vfs-link.js new file mode 100644 index 00000000000000..6925ad004fd966 --- /dev/null +++ b/test/parallel/test-vfs-link.js @@ -0,0 +1,24 @@ +// Flags: --experimental-vfs +'use strict'; + +// Hard-link error cases: creating a link to a directory or to an +// already-existing path. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Linking to a directory throws EINVAL +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/d'); + assert.throws(() => myVfs.linkSync('/d', '/d-link'), { code: 'EINVAL' }); +} + +// Linking to an existing target throws EEXIST +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'x'); + myVfs.writeFileSync('/b.txt', 'y'); + assert.throws(() => myVfs.linkSync('/a.txt', '/b.txt'), { code: 'EEXIST' }); +} diff --git a/test/parallel/test-vfs-memory-file-handle.js b/test/parallel/test-vfs-memory-file-handle.js new file mode 100644 index 00000000000000..5a00b437a3379e --- /dev/null +++ b/test/parallel/test-vfs-memory-file-handle.js @@ -0,0 +1,15 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// MemoryFileHandle internals: the "stats not available" path when there +// is no entry/getStats callback wired up. + +require('../common'); +const assert = require('assert'); +const { MemoryFileHandle } = require('internal/vfs/file_handle'); + +// MemoryFileHandle without a #getStats callback throws ERR_INVALID_STATE +{ + const h = new MemoryFileHandle('/x', 'r', 0o644, Buffer.alloc(0), null, undefined); + assert.throws(() => h.statSync(), { code: 'ERR_INVALID_STATE' }); +} diff --git a/test/parallel/test-vfs-memory-provider-dynamic.js b/test/parallel/test-vfs-memory-provider-dynamic.js new file mode 100644 index 00000000000000..9ddd105a5a5563 --- /dev/null +++ b/test/parallel/test-vfs-memory-provider-dynamic.js @@ -0,0 +1,127 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// MemoryProvider supports dynamic content providers and lazily-populated +// directories internally. These features have no public construction API, +// so we drive them directly through MemoryEntry / MemoryProvider. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); +const { MemoryProvider } = require('internal/vfs/providers/memory'); + +function getRoot(provider) { + const symbols = Object.getOwnPropertySymbols(provider); + const kRoot = symbols.find((s) => s.description === 'kRoot'); + assert.ok(kRoot, 'kRoot symbol expected on MemoryProvider'); + return provider[kRoot]; +} + +function makeFileEntry(prototypeFrom, contentProvider) { + const t = Date.now(); + const fileEntry = { __proto__: Object.getPrototypeOf(prototypeFrom) }; + Object.assign(fileEntry, { + type: 0, // TYPE_FILE + mode: 0o644, + content: Buffer.alloc(0), + contentProvider, + children: null, + target: null, + populate: null, + populated: true, + nlink: 1, + uid: 0, + gid: 0, + atime: t, + mtime: t, + ctime: t, + birthtime: t, + }); + fileEntry.isFile = prototypeFrom.isFile.bind(fileEntry); + fileEntry.isDirectory = prototypeFrom.isDirectory.bind(fileEntry); + fileEntry.isSymbolicLink = prototypeFrom.isSymbolicLink.bind(fileEntry); + fileEntry.isDynamic = prototypeFrom.isDynamic.bind(fileEntry); + fileEntry.getContentSync = prototypeFrom.getContentSync.bind(fileEntry); + fileEntry.getContentAsync = prototypeFrom.getContentAsync.bind(fileEntry); + return fileEntry; +} + +// ===== Lazy-populated directory ===== +{ + const provider = new MemoryProvider(); + const root = getRoot(provider); + + const dir = { + __proto__: Object.getPrototypeOf(root), + type: 1, // TYPE_DIR + mode: 0o755, + children: new Map(), + populate: (scoped) => { + scoped.addFile('hello.txt', 'lazy hello'); + scoped.addFile('dyn.txt', () => 'dynamic-string'); + scoped.addDirectory('subdir', null); + scoped.addSymlink('link.txt', '/lazy/hello.txt'); + }, + populated: false, + nlink: 1, + uid: 0, + gid: 0, + }; + const t = Date.now(); + dir.atime = t; dir.mtime = t; dir.ctime = t; dir.birthtime = t; + dir.isFile = root.isFile.bind(dir); + dir.isDirectory = root.isDirectory.bind(dir); + dir.isSymbolicLink = root.isSymbolicLink.bind(dir); + dir.isDynamic = root.isDynamic.bind(dir); + dir.getContentSync = root.getContentSync.bind(dir); + dir.getContentAsync = root.getContentAsync.bind(dir); + root.children.set('lazy', dir); + + const myVfs = vfs.create(provider); + + // Reading the lazy directory triggers populate + const entries = myVfs.readdirSync('/lazy'); + assert.deepStrictEqual(entries.sort(), + ['dyn.txt', 'hello.txt', 'link.txt', 'subdir']); + + // Static file in the lazy directory + assert.strictEqual(myVfs.readFileSync('/lazy/hello.txt', 'utf8'), + 'lazy hello'); + + // Dynamic content provider returning a string (sync read) + assert.strictEqual(myVfs.readFileSync('/lazy/dyn.txt', 'utf8'), + 'dynamic-string'); + + // Dynamic content provider via promises.readFile + myVfs.promises.readFile('/lazy/dyn.txt', 'utf8').then(common.mustCall((s) => { + assert.strictEqual(s, 'dynamic-string'); + })); +} + +// ===== Dynamic content provider returning a Buffer ===== +{ + const provider = new MemoryProvider(); + const root = getRoot(provider); + root.children.set('buf-dyn.txt', + makeFileEntry(root, () => Buffer.from('buffer-content'))); + + const myVfs = vfs.create(provider); + assert.strictEqual(myVfs.readFileSync('/buf-dyn.txt', 'utf8'), + 'buffer-content'); +} + +// ===== Async-only content provider: sync API throws ERR_INVALID_STATE ===== +{ + const provider = new MemoryProvider(); + const root = getRoot(provider); + root.children.set('async-only.txt', + makeFileEntry(root, async () => 'async-only')); + + const myVfs = vfs.create(provider); + assert.throws(() => myVfs.readFileSync('/async-only.txt'), + { code: 'ERR_INVALID_STATE' }); + + myVfs.promises.readFile('/async-only.txt', 'utf8').then(common.mustCall((s) => { + assert.strictEqual(s, 'async-only'); + })); +} diff --git a/test/parallel/test-vfs-memory-provider-flags.js b/test/parallel/test-vfs-memory-provider-flags.js new file mode 100644 index 00000000000000..08963208278b7a --- /dev/null +++ b/test/parallel/test-vfs-memory-provider-flags.js @@ -0,0 +1,42 @@ +// Flags: --experimental-vfs +'use strict'; + +// MemoryProvider: numeric open flags (mirroring fs.constants.O_*) must be +// normalised to their string equivalents. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const vfs = require('node:vfs'); + +const { O_RDONLY, O_RDWR, O_WRONLY, O_CREAT, O_TRUNC, O_EXCL, O_APPEND } = fs.constants; + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'orig'); + +// O_RDONLY (0) +myVfs.closeSync(myVfs.openSync('/file.txt', O_RDONLY)); + +// O_RDWR ('r+') +myVfs.closeSync(myVfs.openSync('/file.txt', O_RDWR)); + +// 'w' = O_WRONLY | O_CREAT | O_TRUNC +myVfs.closeSync(myVfs.openSync('/created.txt', O_WRONLY | O_CREAT | O_TRUNC)); + +// 'wx' = O_WRONLY | O_CREAT | O_EXCL +myVfs.closeSync(myVfs.openSync('/excl.txt', O_WRONLY | O_CREAT | O_EXCL)); + +// 'wx' on an existing file throws EEXIST +assert.throws( + () => myVfs.openSync('/file.txt', O_WRONLY | O_CREAT | O_EXCL), + { code: 'EEXIST' }); + +// 'a' = O_APPEND | O_RDWR | O_CREAT (mapped to 'a+') +myVfs.closeSync(myVfs.openSync('/app.txt', O_APPEND | O_RDWR | O_CREAT)); + +// 'ax+' = O_APPEND | O_EXCL | O_RDWR | O_CREAT +myVfs.closeSync(myVfs.openSync('/axplus.txt', + O_APPEND | O_EXCL | O_RDWR | O_CREAT)); + +// Bogus non-string non-number defaults to 'r' +myVfs.closeSync(myVfs.openSync('/file.txt', null)); diff --git a/test/parallel/test-vfs-memory-provider.js b/test/parallel/test-vfs-memory-provider.js new file mode 100644 index 00000000000000..c45ee69f679b1c --- /dev/null +++ b/test/parallel/test-vfs-memory-provider.js @@ -0,0 +1,664 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Test MemoryProvider can be instantiated directly +{ + const provider = new vfs.MemoryProvider(); + assert.strictEqual(provider.readonly, false); + assert.strictEqual(provider.supportsSymlinks, true); +} + +// Test creating VFS with MemoryProvider (default) +{ + const myVfs = vfs.create(); + assert.ok(myVfs); + assert.strictEqual(myVfs.readonly, false); + assert.ok(myVfs.provider instanceof vfs.MemoryProvider); +} + +// Test creating VFS with explicit MemoryProvider +{ + const myVfs = vfs.create(new vfs.MemoryProvider()); + assert.ok(myVfs); + assert.strictEqual(myVfs.readonly, false); +} + +// Test reading and writing files +{ + const myVfs = vfs.create(); + + // Write a file + myVfs.writeFileSync('/hello.txt', 'Hello from VFS!'); + + // Read it back + assert.strictEqual(myVfs.readFileSync('/hello.txt', 'utf8'), 'Hello from VFS!'); + + // Read as Buffer + const buf = myVfs.readFileSync('/hello.txt'); + assert.ok(Buffer.isBuffer(buf)); + assert.strictEqual(buf.toString(), 'Hello from VFS!'); + + // Overwrite + myVfs.writeFileSync('/hello.txt', 'Overwritten'); + assert.strictEqual(myVfs.readFileSync('/hello.txt', 'utf8'), 'Overwritten'); +} + +// Test appendFile +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/append.txt', 'start'); + myVfs.appendFileSync('/append.txt', '-end'); + assert.strictEqual(myVfs.readFileSync('/append.txt', 'utf8'), 'start-end'); + + // Append to non-existent file creates it + myVfs.appendFileSync('/new-append.txt', 'new content'); + assert.strictEqual(myVfs.readFileSync('/new-append.txt', 'utf8'), 'new content'); +} + +// Test stat operations +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/stat-test.txt', 'content'); + myVfs.mkdirSync('/stat-dir', { recursive: true }); + + const fileStat = myVfs.statSync('/stat-test.txt'); + assert.strictEqual(fileStat.isFile(), true); + assert.strictEqual(fileStat.isDirectory(), false); + assert.strictEqual(fileStat.size, 7); + + const dirStat = myVfs.statSync('/stat-dir'); + assert.strictEqual(dirStat.isFile(), false); + assert.strictEqual(dirStat.isDirectory(), true); + + // Test ENOENT + assert.throws(() => { + myVfs.statSync('/nonexistent'); + }, { code: 'ENOENT' }); +} + +// Test lstatSync (same as statSync for memory provider since no real symlink following) +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/lstat.txt', 'lstat test'); + + const stat = myVfs.lstatSync('/lstat.txt'); + assert.strictEqual(stat.isFile(), true); +} + +// Test readdirSync +{ + const myVfs = vfs.create(); + + myVfs.mkdirSync('/readdir-test/subdir', { recursive: true }); + myVfs.writeFileSync('/readdir-test/a.txt', 'a'); + myVfs.writeFileSync('/readdir-test/b.txt', 'b'); + + const entries = myVfs.readdirSync('/readdir-test'); + assert.deepStrictEqual(entries.sort(), ['a.txt', 'b.txt', 'subdir']); + + // With file types + const dirents = myVfs.readdirSync('/readdir-test', { withFileTypes: true }); + assert.strictEqual(dirents.length, 3); + + const fileEntry = dirents.find((d) => d.name === 'a.txt'); + assert.ok(fileEntry); + assert.strictEqual(fileEntry.isFile(), true); + + const dirEntry = dirents.find((d) => d.name === 'subdir'); + assert.ok(dirEntry); + assert.strictEqual(dirEntry.isDirectory(), true); + + // ENOENT for non-existent directory + assert.throws(() => { + myVfs.readdirSync('/nonexistent'); + }, { code: 'ENOENT' }); + + // ENOTDIR for file + assert.throws(() => { + myVfs.readdirSync('/readdir-test/a.txt'); + }, { code: 'ENOTDIR' }); +} + +// Test mkdir and rmdir +{ + const myVfs = vfs.create(); + + myVfs.mkdirSync('/new-dir'); + assert.strictEqual(myVfs.existsSync('/new-dir'), true); + assert.strictEqual(myVfs.statSync('/new-dir').isDirectory(), true); + + myVfs.rmdirSync('/new-dir'); + assert.strictEqual(myVfs.existsSync('/new-dir'), false); + + // EEXIST for existing directory + myVfs.mkdirSync('/exists'); + assert.throws(() => { + myVfs.mkdirSync('/exists'); + }, { code: 'EEXIST' }); + + // ENOTEMPTY for non-empty directory + myVfs.mkdirSync('/nonempty'); + myVfs.writeFileSync('/nonempty/file.txt', 'content'); + assert.throws(() => { + myVfs.rmdirSync('/nonempty'); + }, { code: 'ENOTEMPTY' }); +} + +// Test recursive mkdir +{ + const myVfs = vfs.create(); + + myVfs.mkdirSync('/deep/nested/dir', { recursive: true }); + assert.strictEqual(myVfs.existsSync('/deep/nested/dir'), true); + assert.strictEqual(myVfs.statSync('/deep/nested/dir').isDirectory(), true); + + // Recursive on existing is OK + myVfs.mkdirSync('/deep/nested/dir', { recursive: true }); + assert.strictEqual(myVfs.existsSync('/deep/nested/dir'), true); +} + +// Test unlink +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/to-delete.txt', 'delete me'); + assert.strictEqual(myVfs.existsSync('/to-delete.txt'), true); + + myVfs.unlinkSync('/to-delete.txt'); + assert.strictEqual(myVfs.existsSync('/to-delete.txt'), false); + + // ENOENT for non-existent file + assert.throws(() => { + myVfs.unlinkSync('/nonexistent.txt'); + }, { code: 'ENOENT' }); + + // EISDIR for directory + myVfs.mkdirSync('/dir-to-unlink'); + assert.throws(() => { + myVfs.unlinkSync('/dir-to-unlink'); + }, { code: 'EISDIR' }); +} + +// Test rename +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/old-name.txt', 'rename me'); + myVfs.renameSync('/old-name.txt', '/new-name.txt'); + + assert.strictEqual(myVfs.existsSync('/old-name.txt'), false); + assert.strictEqual(myVfs.existsSync('/new-name.txt'), true); + assert.strictEqual(myVfs.readFileSync('/new-name.txt', 'utf8'), 'rename me'); + + // ENOENT for non-existent source + assert.throws(() => { + myVfs.renameSync('/nonexistent.txt', '/dest.txt'); + }, { code: 'ENOENT' }); +} + +// Test copyFile +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/source.txt', 'copy me'); + myVfs.copyFileSync('/source.txt', '/dest.txt'); + + assert.strictEqual(myVfs.existsSync('/source.txt'), true); + assert.strictEqual(myVfs.existsSync('/dest.txt'), true); + assert.strictEqual(myVfs.readFileSync('/dest.txt', 'utf8'), 'copy me'); + + // ENOENT for non-existent source + assert.throws(() => { + myVfs.copyFileSync('/nonexistent.txt', '/fail.txt'); + }, { code: 'ENOENT' }); +} + +// Test realpathSync +{ + const myVfs = vfs.create(); + + myVfs.mkdirSync('/path/to', { recursive: true }); + myVfs.writeFileSync('/path/to/real.txt', 'content'); + + const resolved = myVfs.realpathSync('/path/to/real.txt'); + assert.strictEqual(resolved, '/path/to/real.txt'); + + // With .. components + const normalized = myVfs.realpathSync('/path/to/../to/real.txt'); + assert.strictEqual(normalized, '/path/to/real.txt'); + + // ENOENT for non-existent + assert.throws(() => { + myVfs.realpathSync('/nonexistent'); + }, { code: 'ENOENT' }); +} + +// Test existsSync +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/exists.txt', 'content'); + assert.strictEqual(myVfs.existsSync('/exists.txt'), true); + assert.strictEqual(myVfs.existsSync('/not-exists.txt'), false); +} + +// Test accessSync +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/accessible.txt', 'content'); + + // Should not throw for existing file + myVfs.accessSync('/accessible.txt'); + + // Should throw ENOENT for non-existent + assert.throws(() => { + myVfs.accessSync('/nonexistent.txt'); + }, { code: 'ENOENT' }); +} + +// Test file handle operations via openSync +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/handle-test.txt', 'hello world'); + + const fd = myVfs.openSync('/handle-test.txt', 'r'); + assert.ok((fd & 0x40000000) !== 0); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + // Read via file handle + const buffer = Buffer.alloc(5); + const bytesRead = handle.entry.readSync(buffer, 0, 5, 0); + assert.strictEqual(bytesRead, 5); + assert.strictEqual(buffer.toString(), 'hello'); + + myVfs.closeSync(fd); +} + +// Test file handle write operations +{ + const myVfs = vfs.create(); + + const fd = myVfs.openSync('/write-handle.txt', 'w'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + const buffer = Buffer.from('written via handle'); + const bytesWritten = handle.entry.writeSync(buffer, 0, buffer.length, 0); + assert.strictEqual(bytesWritten, buffer.length); + + myVfs.closeSync(fd); + + // Verify content + assert.strictEqual(myVfs.readFileSync('/write-handle.txt', 'utf8'), 'written via handle'); +} + +// Test file handle readFile and writeFile +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/handle-rw.txt', 'original'); + + const fd = myVfs.openSync('/handle-rw.txt', 'r+'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + // Read via readFile + const content = handle.entry.readFileSync('utf8'); + assert.strictEqual(content, 'original'); + + // Write via writeFile + handle.entry.writeFileSync('replaced'); + myVfs.closeSync(fd); + + assert.strictEqual(myVfs.readFileSync('/handle-rw.txt', 'utf8'), 'replaced'); +} + +// Test symlink operations +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/target.txt', 'target content'); + myVfs.symlinkSync('/target.txt', '/link.txt'); + + // Reading through symlink should work + assert.strictEqual(myVfs.readFileSync('/link.txt', 'utf8'), 'target content'); + + // ReadlinkSync should return target + assert.strictEqual(myVfs.readlinkSync('/link.txt'), '/target.txt'); + + // Lstat on symlink should show it's a symlink + const lstat = myVfs.lstatSync('/link.txt'); + assert.strictEqual(lstat.isSymbolicLink(), true); +} + +// Test reading directory as file should fail +{ + const myVfs = vfs.create(); + + myVfs.mkdirSync('/mydir', { recursive: true }); + assert.throws(() => { + myVfs.readFileSync('/mydir'); + }, { code: 'EISDIR' }); +} + +// Test that readFileSync returns independent buffer copies +{ + const myVfs = vfs.create(); + + myVfs.writeFileSync('/independent.txt', 'original content'); + + const buf1 = myVfs.readFileSync('/independent.txt'); + const buf2 = myVfs.readFileSync('/independent.txt'); + + // Both should have the same content + assert.deepStrictEqual(buf1, buf2); + + // Mutating one should not affect the other + buf1[0] = 0xFF; + assert.notDeepStrictEqual(buf1, buf2); + assert.strictEqual(buf2.toString(), 'original content'); + + // A third read should still return the original content + const buf3 = myVfs.readFileSync('/independent.txt'); + assert.strictEqual(buf3.toString(), 'original content'); +} + +// ==================== Async Operations ==================== + +// Test async read and write operations +(async () => { + const myVfs = vfs.create(); + + await myVfs.promises.writeFile('/async-test.txt', 'async content'); + const content = await myVfs.promises.readFile('/async-test.txt', 'utf8'); + assert.strictEqual(content, 'async content'); + + const stat = await myVfs.promises.stat('/async-test.txt'); + assert.strictEqual(stat.isFile(), true); + + await myVfs.promises.unlink('/async-test.txt'); + assert.strictEqual(myVfs.existsSync('/async-test.txt'), false); +})().then(common.mustCall()); + +// Test async lstat +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-lstat.txt', 'async lstat'); + + const stat = await myVfs.promises.lstat('/async-lstat.txt'); + assert.strictEqual(stat.isFile(), true); +})().then(common.mustCall()); + +// Test async copyFile +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-src.txt', 'async copy'); + + await myVfs.promises.copyFile('/async-src.txt', '/async-dest.txt'); + + assert.strictEqual(myVfs.existsSync('/async-dest.txt'), true); + assert.strictEqual(myVfs.readFileSync('/async-dest.txt', 'utf8'), 'async copy'); +})().then(common.mustCall()); + +// Test async mkdir and rmdir +(async () => { + const myVfs = vfs.create(); + + await myVfs.promises.mkdir('/async-dir'); + assert.strictEqual(myVfs.existsSync('/async-dir'), true); + + await myVfs.promises.rmdir('/async-dir'); + assert.strictEqual(myVfs.existsSync('/async-dir'), false); +})().then(common.mustCall()); + +// Test async rename +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-old.txt', 'async rename'); + + await myVfs.promises.rename('/async-old.txt', '/async-new.txt'); + + assert.strictEqual(myVfs.existsSync('/async-old.txt'), false); + assert.strictEqual(myVfs.existsSync('/async-new.txt'), true); +})().then(common.mustCall()); + +// Test async readdir +(async () => { + const myVfs = vfs.create(); + + myVfs.mkdirSync('/async-readdir', { recursive: true }); + myVfs.writeFileSync('/async-readdir/file.txt', 'content'); + + const entries = await myVfs.promises.readdir('/async-readdir'); + assert.deepStrictEqual(entries, ['file.txt']); +})().then(common.mustCall()); + +// Test async appendFile +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-append.txt', 'start'); + + await myVfs.promises.appendFile('/async-append.txt', '-end'); + assert.strictEqual(myVfs.readFileSync('/async-append.txt', 'utf8'), 'start-end'); +})().then(common.mustCall()); + +// Test async access +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-access.txt', 'content'); + + await myVfs.promises.access('/async-access.txt'); + + await assert.rejects( + myVfs.promises.access('/nonexistent.txt'), + { code: 'ENOENT' } + ); +})().then(common.mustCall()); + +// Test async realpath +(async () => { + const myVfs = vfs.create(); + + myVfs.mkdirSync('/async-real/path', { recursive: true }); + myVfs.writeFileSync('/async-real/path/file.txt', 'content'); + + const resolved = await myVfs.promises.realpath('/async-real/path/file.txt'); + assert.strictEqual(resolved, '/async-real/path/file.txt'); +})().then(common.mustCall()); + +// Test async file handle read +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-handle.txt', 'async read test'); + + const fd = myVfs.openSync('/async-handle.txt', 'r'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + const buffer = Buffer.alloc(10); + const result = await handle.entry.read(buffer, 0, 10, 0); + assert.strictEqual(result.bytesRead, 10); + assert.strictEqual(buffer.toString(), 'async read'); + + myVfs.closeSync(fd); +})().then(common.mustCall()); + +// Test async file handle write +(async () => { + const myVfs = vfs.create(); + + const fd = myVfs.openSync('/async-write.txt', 'w'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + const buffer = Buffer.from('async write'); + const result = await handle.entry.write(buffer, 0, buffer.length, 0); + assert.strictEqual(result.bytesWritten, buffer.length); + + myVfs.closeSync(fd); + + // Verify content + assert.strictEqual(myVfs.readFileSync('/async-write.txt', 'utf8'), 'async write'); +})().then(common.mustCall()); + +// Test async file handle stat +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/stat-handle.txt', 'stat test'); + + const fd = myVfs.openSync('/stat-handle.txt', 'r'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + const stat = await handle.entry.stat(); + assert.strictEqual(stat.isFile(), true); + + myVfs.closeSync(fd); +})().then(common.mustCall()); + +// Test async file handle truncate +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/truncate-handle.txt', 'truncate this'); + + const fd = myVfs.openSync('/truncate-handle.txt', 'r+'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + await handle.entry.truncate(8); + myVfs.closeSync(fd); + + // Verify content was truncated + assert.strictEqual(myVfs.readFileSync('/truncate-handle.txt', 'utf8'), 'truncate'); +})().then(common.mustCall()); + +// Test async file handle close +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/close-handle.txt', 'close test'); + + const fd = myVfs.openSync('/close-handle.txt', 'r'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + await handle.entry.close(); + assert.strictEqual(handle.entry.closed, true); +})().then(common.mustCall()); + +// Test async readFile and writeFile on handle +(async () => { + const myVfs = vfs.create(); + + myVfs.writeFileSync('/async-rw.txt', 'async original'); + + const fd = myVfs.openSync('/async-rw.txt', 'r+'); + const handle = require('internal/vfs/fd').getVirtualFd(fd); + + // Async read + const content = await handle.entry.readFile('utf8'); + assert.strictEqual(content, 'async original'); + + // Async write + await handle.entry.writeFile('async replaced'); + myVfs.closeSync(fd); + + assert.strictEqual(myVfs.readFileSync('/async-rw.txt', 'utf8'), 'async replaced'); +})().then(common.mustCall()); + +// ==================== Readonly Mode ==================== + +// Test MemoryProvider readonly mode +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'content'); + myVfs.mkdirSync('/dir', { recursive: true }); + + // Set to readonly + myVfs.provider.setReadOnly(); + assert.strictEqual(myVfs.provider.readonly, true); + + // Read operations should still work + assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'content'); + assert.strictEqual(myVfs.existsSync('/file.txt'), true); + assert.ok(myVfs.statSync('/file.txt')); + + // Write operations should throw EROFS + assert.throws(() => { + myVfs.writeFileSync('/file.txt', 'new content'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.writeFileSync('/new.txt', 'content'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.appendFileSync('/file.txt', 'more'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.mkdirSync('/newdir'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.unlinkSync('/file.txt'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.rmdirSync('/dir'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.renameSync('/file.txt', '/renamed.txt'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.copyFileSync('/file.txt', '/copy.txt'); + }, { code: 'EROFS' }); + + assert.throws(() => { + myVfs.symlinkSync('/file.txt', '/link'); + }, { code: 'EROFS' }); +} + +// Test async operations on readonly MemoryProvider +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/readonly.txt', 'content'); + myVfs.provider.setReadOnly(); + + await assert.rejects( + myVfs.promises.writeFile('/readonly.txt', 'new'), + { code: 'EROFS' } + ); + + await assert.rejects( + myVfs.promises.appendFile('/readonly.txt', 'more'), + { code: 'EROFS' } + ); + + await assert.rejects( + myVfs.promises.mkdir('/newdir'), + { code: 'EROFS' } + ); + + await assert.rejects( + myVfs.promises.unlink('/readonly.txt'), + { code: 'EROFS' } + ); + + await assert.rejects( + myVfs.promises.copyFile('/readonly.txt', '/copy.txt'), + { code: 'EROFS' } + ); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-mkdir.js b/test/parallel/test-vfs-mkdir.js new file mode 100644 index 00000000000000..87a823b77d87ca --- /dev/null +++ b/test/parallel/test-vfs-mkdir.js @@ -0,0 +1,49 @@ +// Flags: --experimental-vfs +'use strict'; + +// mkdirSync / rmdirSync behaviour: return value, recursive option, mode +// option, error cases. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// mkdirSync({ recursive: true }) returns the path of the first newly- +// created directory when some parents already exist. +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/a'); + const result = myVfs.mkdirSync('/a/b/c', { recursive: true }); + assert.strictEqual(result, '/a/b'); +} + +// mkdirSync with explicit mode (non-recursive) +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/d-mode', { mode: 0o700 }); + assert.strictEqual(myVfs.statSync('/d-mode').mode & 0o777, 0o700); +} + +// mkdirSync with explicit mode + recursive +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/r-mode/sub/deep', { recursive: true, mode: 0o700 }); + assert.strictEqual(myVfs.statSync('/r-mode/sub/deep').mode & 0o777, 0o700); +} + +// Recursive mkdir through a regular-file blocker throws ENOTDIR +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/blocker', 'x'); + assert.throws( + () => myVfs.mkdirSync('/blocker/sub', { recursive: true }), + { code: 'ENOTDIR' }); +} + +// Rmdir on a non-empty directory throws ENOTEMPTY +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/d'); + myVfs.writeFileSync('/d/x', ''); + assert.throws(() => myVfs.rmdirSync('/d'), { code: 'ENOTEMPTY' }); +} diff --git a/test/parallel/test-vfs-mkdtemp.js b/test/parallel/test-vfs-mkdtemp.js new file mode 100644 index 00000000000000..f2140e2bc7df51 --- /dev/null +++ b/test/parallel/test-vfs-mkdtemp.js @@ -0,0 +1,38 @@ +// Flags: --experimental-vfs +'use strict'; + +// mkdtemp / mkdtempSync behaviour. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// mkdtempSync returns the created directory path (with random suffix) +{ + const myVfs = vfs.create(); + const dir = myVfs.mkdtempSync('/tmp-'); + assert.ok(dir.startsWith('/tmp-')); + assert.ok(myVfs.statSync(dir).isDirectory()); +} + +// Mkdtemp callback variant - success +{ + const myVfs = vfs.create(); + myVfs.mkdtemp('/tmp-', common.mustSucceed((dir) => { + assert.ok(dir.startsWith('/tmp-')); + })); +} + +// Mkdtemp callback variant - with options object +{ + const myVfs = vfs.create(); + myVfs.mkdtemp('/tmp-', {}, common.mustSucceed((dir) => { + assert.ok(dir.startsWith('/tmp-')); + })); +} + +// Mkdtemp callback variant — error path (parent doesn't exist) +{ + const myVfs = vfs.create(); + myVfs.mkdtemp('/missing/prefix-', common.expectsError()); +} diff --git a/test/parallel/test-vfs-parent-timestamps.js b/test/parallel/test-vfs-parent-timestamps.js new file mode 100644 index 00000000000000..d12b2e7ec54182 --- /dev/null +++ b/test/parallel/test-vfs-parent-timestamps.js @@ -0,0 +1,25 @@ +// Flags: --experimental-vfs +'use strict'; + +// Operations that modify a directory should bump its mtime/ctime. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.mkdirSync('/dir'); + +function getTimestamps(p) { + const st = myVfs.statSync(p); + return { mtimeMs: st.mtimeMs, ctimeMs: st.ctimeMs }; +} + +const before = getTimestamps('/dir'); +// Wait long enough for ms-resolution mtime to differ +setTimeout(common.mustCall(() => { + myVfs.writeFileSync('/dir/file.txt', 'hello'); + const after = getTimestamps('/dir'); + assert.ok(after.mtimeMs >= before.mtimeMs); + assert.ok(after.ctimeMs >= before.ctimeMs); +}), 5); diff --git a/test/parallel/test-vfs-promises-open.js b/test/parallel/test-vfs-promises-open.js new file mode 100644 index 00000000000000..280627ce73bb39 --- /dev/null +++ b/test/parallel/test-vfs-promises-open.js @@ -0,0 +1,17 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS promises.open returns a usable handle. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/hello.txt', 'hello from vfs'); + +(async () => { + const fh = await myVfs.promises.open('/hello.txt', 'r'); + assert.ok(fh); + assert.ok(typeof fh === 'number'); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-promises.js b/test/parallel/test-vfs-promises.js new file mode 100644 index 00000000000000..85b1b68b1a176f --- /dev/null +++ b/test/parallel/test-vfs-promises.js @@ -0,0 +1,483 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Test callback-based readFile +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/test.txt', 'hello world'); + + myVfs.readFile('/test.txt', common.mustSucceed((data) => { + assert.ok(Buffer.isBuffer(data)); + assert.strictEqual(data.toString(), 'hello world'); + })); + + myVfs.readFile('/test.txt', 'utf8', common.mustSucceed((data) => { + assert.strictEqual(data, 'hello world'); + })); + + myVfs.readFile('/test.txt', { encoding: 'utf8' }, common.mustSucceed((data) => { + assert.strictEqual(data, 'hello world'); + })); +} + +// Test callback-based readFile with non-existent file +{ + const myVfs = vfs.create(); + + myVfs.readFile('/nonexistent.txt', common.expectsError({ + code: 'ENOENT', + })); +} + +// Test callback-based readFile with directory +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/mydir', { recursive: true }); + + myVfs.readFile('/mydir', common.expectsError({ + code: 'EISDIR', + })); +} + +// Test callback-based stat +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir', { recursive: true }); + myVfs.writeFileSync('/file.txt', 'content'); + + myVfs.stat('/file.txt', common.mustSucceed((stats) => { + assert.strictEqual(stats.isFile(), true); + assert.strictEqual(stats.isDirectory(), false); + assert.strictEqual(stats.size, 7); + })); + + myVfs.stat('/dir', common.mustSucceed((stats) => { + assert.strictEqual(stats.isFile(), false); + assert.strictEqual(stats.isDirectory(), true); + })); + + myVfs.stat('/nonexistent', common.expectsError({ + code: 'ENOENT', + })); +} + +// Test callback-based lstat (same as stat for VFS) +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'content'); + + myVfs.lstat('/file.txt', common.mustCall((err, stats) => { + assert.strictEqual(err, null); + assert.strictEqual(stats.isFile(), true); + })); +} + +// Test callback-based readdir +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir/subdir', { recursive: true }); + myVfs.writeFileSync('/dir/file1.txt', 'a'); + myVfs.writeFileSync('/dir/file2.txt', 'b'); + + myVfs.readdir('/dir', common.mustCall((err, entries) => { + assert.strictEqual(err, null); + assert.deepStrictEqual(entries.sort(), ['file1.txt', 'file2.txt', 'subdir']); + })); + + myVfs.readdir('/dir', { withFileTypes: true }, common.mustCall((err, entries) => { + assert.strictEqual(err, null); + assert.strictEqual(entries.length, 3); + + const file1 = entries.find((e) => e.name === 'file1.txt'); + assert.strictEqual(file1.isFile(), true); + assert.strictEqual(file1.isDirectory(), false); + + const subdir = entries.find((e) => e.name === 'subdir'); + assert.strictEqual(subdir.isFile(), false); + assert.strictEqual(subdir.isDirectory(), true); + })); + + myVfs.readdir('/nonexistent', common.mustCall((err, entries) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(entries, undefined); + })); + + myVfs.readdir('/dir/file1.txt', common.mustCall((err, entries) => { + assert.strictEqual(err.code, 'ENOTDIR'); + assert.strictEqual(entries, undefined); + })); +} + +// Test callback-based realpath +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/path/to', { recursive: true }); + myVfs.writeFileSync('/path/to/file.txt', 'content'); + + myVfs.realpath('/path/to/file.txt', common.mustCall((err, resolved) => { + assert.strictEqual(err, null); + assert.strictEqual(resolved, '/path/to/file.txt'); + })); + + myVfs.realpath('/path/to/../to/file.txt', common.mustCall((err, resolved) => { + assert.strictEqual(err, null); + assert.strictEqual(resolved, '/path/to/file.txt'); + })); + + myVfs.realpath('/nonexistent', common.mustCall((err, resolved) => { + assert.strictEqual(err.code, 'ENOENT'); + assert.strictEqual(resolved, undefined); + })); +} + +// Test callback-based access +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/accessible.txt', 'content'); + + myVfs.access('/accessible.txt', common.mustCall((err) => { + assert.strictEqual(err, null); + })); + + myVfs.access('/nonexistent.txt', common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + })); +} + +// Test callback-based writeFile +{ + const myVfs = vfs.create(); + + myVfs.writeFile('/cb-write.txt', 'callback written', common.mustCall((err) => { + assert.strictEqual(err, null); + assert.strictEqual(myVfs.readFileSync('/cb-write.txt', 'utf8'), 'callback written'); + })); + + // Overwrite existing + myVfs.writeFileSync('/cb-overwrite.txt', 'old'); + myVfs.writeFile('/cb-overwrite.txt', 'new', common.mustCall((err) => { + assert.strictEqual(err, null); + assert.strictEqual(myVfs.readFileSync('/cb-overwrite.txt', 'utf8'), 'new'); + })); + + // Write with Buffer + myVfs.writeFile('/cb-buf.txt', Buffer.from('buf data'), common.mustCall((err) => { + assert.strictEqual(err, null); + assert.strictEqual(myVfs.readFileSync('/cb-buf.txt', 'utf8'), 'buf data'); + })); +} + +// Test callback-based readlink +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/link-target.txt', 'content'); + myVfs.symlinkSync('/link-target.txt', '/my-link.txt'); + + myVfs.readlink('/my-link.txt', common.mustCall((err, target) => { + assert.strictEqual(err, null); + assert.strictEqual(target, '/link-target.txt'); + })); + + myVfs.readlink('/link-target.txt', common.mustCall((err) => { + assert.strictEqual(err.code, 'EINVAL'); + })); +} + +// Test callback-based open, read, fstat, close +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/fd-test.txt', 'fd content'); + + myVfs.open('/fd-test.txt', 'r', common.mustCall((err, fd) => { + assert.strictEqual(err, null); + assert.strictEqual(typeof fd, 'number'); + + // fstat + myVfs.fstat(fd, common.mustCall((err, stats) => { + assert.strictEqual(err, null); + assert.strictEqual(stats.isFile(), true); + assert.strictEqual(stats.size, 10); + })); + + // read + const buf = Buffer.alloc(10); + myVfs.read(fd, buf, 0, 10, 0, common.mustCall((err, bytesRead, buffer) => { + assert.strictEqual(err, null); + assert.strictEqual(bytesRead, 10); + assert.strictEqual(buffer.toString(), 'fd content'); + })); + + // close + myVfs.close(fd, common.mustCall((err) => { + assert.strictEqual(err, null); + })); + })); + + // open non-existent + myVfs.open('/nonexistent.txt', 'r', common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + })); +} + +// ==================== Promise API Tests ==================== + +// Test promises.readFile +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/promise-test.txt', 'promise content'); + + const bufferData = await myVfs.promises.readFile('/promise-test.txt'); + assert.ok(Buffer.isBuffer(bufferData)); + assert.strictEqual(bufferData.toString(), 'promise content'); + + const stringData = await myVfs.promises.readFile('/promise-test.txt', 'utf8'); + assert.strictEqual(stringData, 'promise content'); + + const stringData2 = await myVfs.promises.readFile('/promise-test.txt', { encoding: 'utf8' }); + assert.strictEqual(stringData2, 'promise content'); + + await assert.rejects( + myVfs.promises.readFile('/nonexistent.txt'), + { code: 'ENOENT' } + ); + + myVfs.mkdirSync('/promisedir', { recursive: true }); + await assert.rejects( + myVfs.promises.readFile('/promisedir'), + { code: 'EISDIR' } + ); +})().then(common.mustCall()); + +// Test promises.stat +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/stat-dir', { recursive: true }); + myVfs.writeFileSync('/stat-file.txt', 'hello'); + + const fileStats = await myVfs.promises.stat('/stat-file.txt'); + assert.strictEqual(fileStats.isFile(), true); + assert.strictEqual(fileStats.size, 5); + + const dirStats = await myVfs.promises.stat('/stat-dir'); + assert.strictEqual(dirStats.isDirectory(), true); + + await assert.rejects( + myVfs.promises.stat('/nonexistent'), + { code: 'ENOENT' } + ); +})().then(common.mustCall()); + +// Test promises.lstat +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/lstat-file.txt', 'content'); + + const stats = await myVfs.promises.lstat('/lstat-file.txt'); + assert.strictEqual(stats.isFile(), true); +})().then(common.mustCall()); + +// Test promises.readdir +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/pdir/sub', { recursive: true }); + myVfs.writeFileSync('/pdir/a.txt', 'a'); + myVfs.writeFileSync('/pdir/b.txt', 'b'); + + const names = await myVfs.promises.readdir('/pdir'); + assert.deepStrictEqual(names.sort(), ['a.txt', 'b.txt', 'sub']); + + const dirents = await myVfs.promises.readdir('/pdir', { withFileTypes: true }); + assert.strictEqual(dirents.length, 3); + const aFile = dirents.find((e) => e.name === 'a.txt'); + assert.strictEqual(aFile.isFile(), true); + + await assert.rejects( + myVfs.promises.readdir('/nonexistent'), + { code: 'ENOENT' } + ); + + await assert.rejects( + myVfs.promises.readdir('/pdir/a.txt'), + { code: 'ENOTDIR' } + ); +})().then(common.mustCall()); + +// Test promises.realpath +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/real/path', { recursive: true }); + myVfs.writeFileSync('/real/path/file.txt', 'content'); + + const resolved = await myVfs.promises.realpath('/real/path/file.txt'); + assert.strictEqual(resolved, '/real/path/file.txt'); + + const normalized = await myVfs.promises.realpath('/real/path/../path/file.txt'); + assert.strictEqual(normalized, '/real/path/file.txt'); + + await assert.rejects( + myVfs.promises.realpath('/nonexistent'), + { code: 'ENOENT' } + ); +})().then(common.mustCall()); + +// Test promises.access +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/access-test.txt', 'content'); + + await myVfs.promises.access('/access-test.txt'); + + await assert.rejects( + myVfs.promises.access('/nonexistent'), + { code: 'ENOENT' } + ); +})().then(common.mustCall()); + +// Test promises.writeFile +(async () => { + const myVfs = vfs.create(); + + await myVfs.promises.writeFile('/write-test.txt', 'async written'); + assert.strictEqual(myVfs.readFileSync('/write-test.txt', 'utf8'), 'async written'); + + // Overwrite existing file + await myVfs.promises.writeFile('/write-test.txt', 'overwritten'); + assert.strictEqual(myVfs.readFileSync('/write-test.txt', 'utf8'), 'overwritten'); + + // Write with Buffer + await myVfs.promises.writeFile('/buffer-write.txt', Buffer.from('buffer data')); + assert.strictEqual(myVfs.readFileSync('/buffer-write.txt', 'utf8'), 'buffer data'); +})().then(common.mustCall()); + +// Test promises.appendFile +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/append-test.txt', 'start'); + + await myVfs.promises.appendFile('/append-test.txt', '-end'); + assert.strictEqual(myVfs.readFileSync('/append-test.txt', 'utf8'), 'start-end'); + + // Append to non-existent file creates it + await myVfs.promises.appendFile('/new-append.txt', 'new content'); + assert.strictEqual(myVfs.readFileSync('/new-append.txt', 'utf8'), 'new content'); +})().then(common.mustCall()); + +// Test promises.mkdir +(async () => { + const myVfs = vfs.create(); + + await myVfs.promises.mkdir('/async-dir'); + const stat = myVfs.statSync('/async-dir'); + assert.strictEqual(stat.isDirectory(), true); + + // Recursive mkdir + await myVfs.promises.mkdir('/async-dir/nested/deep', { recursive: true }); + assert.strictEqual(myVfs.statSync('/async-dir/nested/deep').isDirectory(), true); + + // Mkdir on existing directory throws without recursive + await assert.rejects( + myVfs.promises.mkdir('/async-dir'), + { code: 'EEXIST' } + ); +})().then(common.mustCall()); + +// Test promises.unlink +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/unlink-test.txt', 'to delete'); + + await myVfs.promises.unlink('/unlink-test.txt'); + assert.strictEqual(myVfs.existsSync('/unlink-test.txt'), false); + + await assert.rejects( + myVfs.promises.unlink('/nonexistent.txt'), + { code: 'ENOENT' } + ); +})().then(common.mustCall()); + +// Test promises.rmdir +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/rmdir-test'); + + await myVfs.promises.rmdir('/rmdir-test'); + assert.strictEqual(myVfs.existsSync('/rmdir-test'), false); + + // Rmdir on non-empty directory throws + myVfs.mkdirSync('/nonempty'); + myVfs.writeFileSync('/nonempty/file.txt', 'content'); + await assert.rejects( + myVfs.promises.rmdir('/nonempty'), + { code: 'ENOTEMPTY' } + ); +})().then(common.mustCall()); + +// Test promises.rename +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/rename-src.txt', 'rename me'); + + await myVfs.promises.rename('/rename-src.txt', '/rename-dest.txt'); + assert.strictEqual(myVfs.existsSync('/rename-src.txt'), false); + assert.strictEqual(myVfs.readFileSync('/rename-dest.txt', 'utf8'), 'rename me'); +})().then(common.mustCall()); + +// Test promises.copyFile +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/copy-src.txt', 'copy me'); + + await myVfs.promises.copyFile('/copy-src.txt', '/copy-dest.txt'); + assert.strictEqual(myVfs.readFileSync('/copy-dest.txt', 'utf8'), 'copy me'); + // Source still exists + assert.strictEqual(myVfs.existsSync('/copy-src.txt'), true); + + await assert.rejects( + myVfs.promises.copyFile('/nonexistent.txt', '/fail.txt'), + { code: 'ENOENT' } + ); +})().then(common.mustCall()); + +// Test promises.symlink and promises.readlink +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/symlink-target.txt', 'symlink content'); + + await myVfs.promises.symlink('/symlink-target.txt', '/symlink-link.txt'); + + // Verify symlink was created + const lstat = myVfs.lstatSync('/symlink-link.txt'); + assert.strictEqual(lstat.isSymbolicLink(), true); + + // Read through symlink + const content = await myVfs.promises.readFile('/symlink-link.txt', 'utf8'); + assert.strictEqual(content, 'symlink content'); + + // Readlink should return target + const target = await myVfs.promises.readlink('/symlink-link.txt'); + assert.strictEqual(target, '/symlink-target.txt'); + + // Readlink on non-symlink should error + await assert.rejects( + myVfs.promises.readlink('/symlink-target.txt'), + { code: 'EINVAL' } + ); +})().then(common.mustCall()); + +// Test async truncate (via file handle) +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/truncate-test.txt', 'async content'); + + const fd = myVfs.openSync('/truncate-test.txt', 'r+'); + const { getVirtualFd } = require('internal/vfs/fd'); + const handle = getVirtualFd(fd); + + await handle.entry.truncate(5); + myVfs.closeSync(fd); + assert.strictEqual(myVfs.readFileSync('/truncate-test.txt', 'utf8'), 'async'); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-readdir-symlink-recursive.js b/test/parallel/test-vfs-readdir-symlink-recursive.js new file mode 100644 index 00000000000000..1f9661947c7444 --- /dev/null +++ b/test/parallel/test-vfs-readdir-symlink-recursive.js @@ -0,0 +1,54 @@ +// Flags: --experimental-vfs +'use strict'; + +// Recursive readdir must follow symlinks to directories. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.mkdirSync('/real-dir'); +myVfs.writeFileSync('/real-dir/nested.txt', 'nested'); + +myVfs.mkdirSync('/root'); +myVfs.symlinkSync('/real-dir', '/root/symdir'); + +const entries = myVfs.readdirSync('/root', { recursive: true }); +assert.ok(entries.includes('symdir')); +assert.ok( + entries.includes('symdir/nested.txt'), + `Expected 'symdir/nested.txt' in entries: ${entries}`, +); + +// Recursive readdir with withFileTypes:true returns Dirent objects whose +// parentPath reflects the actual location of the entry (not the entry's +// stringified relative path). +{ + const v = vfs.create(); + v.mkdirSync('/r/a/b', { recursive: true }); + v.writeFileSync('/r/top.txt', 'x'); + v.writeFileSync('/r/a/b/leaf.txt', 'y'); + + const dirents = v.readdirSync('/r', { withFileTypes: true, recursive: true }); + const leaf = dirents.find((d) => d.name === 'leaf.txt'); + assert.ok(leaf); + assert.strictEqual(leaf.parentPath, '/r/a/b'); + + const top = dirents.find((d) => d.name === 'top.txt'); + assert.ok(top); + assert.strictEqual(top.parentPath, '/r'); +} + +// Non-recursive readdir with withFileTypes returns mixed entry types +{ + const v = vfs.create(); + v.mkdirSync('/d'); + v.writeFileSync('/d/a.txt', 'x'); + v.mkdirSync('/d/sub'); + v.symlinkSync('a.txt', '/d/lnk'); + const dirents = v.readdirSync('/d', { withFileTypes: true }); + assert.ok(dirents.some((d) => d.name === 'a.txt' && d.isFile())); + assert.ok(dirents.some((d) => d.name === 'sub' && d.isDirectory())); + assert.ok(dirents.some((d) => d.name === 'lnk' && d.isSymbolicLink())); +} diff --git a/test/parallel/test-vfs-readfile-async.js b/test/parallel/test-vfs-readfile-async.js new file mode 100644 index 00000000000000..79580a915973fd --- /dev/null +++ b/test/parallel/test-vfs-readfile-async.js @@ -0,0 +1,24 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS readFile callback API. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/async-read.txt', 'async content'); + +myVfs.readFile('/async-read.txt', 'utf8', common.mustSucceed((data) => { + assert.strictEqual(data, 'async content'); +})); + +myVfs.readFile('/async-read.txt', common.mustSucceed((data) => { + assert.ok(Buffer.isBuffer(data)); + assert.strictEqual(data.toString(), 'async content'); +})); + +myVfs.readFile('/missing.txt', common.expectsError({ + code: 'ENOENT', +})); diff --git a/test/parallel/test-vfs-readfile-encoding.js b/test/parallel/test-vfs-readfile-encoding.js new file mode 100644 index 00000000000000..c80a56cce35bc8 --- /dev/null +++ b/test/parallel/test-vfs-readfile-encoding.js @@ -0,0 +1,21 @@ +// Flags: --experimental-vfs +'use strict'; + +// readFileSync with invalid encoding must throw. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'x'); + +assert.throws( + () => myVfs.readFileSync('/file.txt', { encoding: 'bogus' }), + /encoding/i, +); + +// Valid encodings should work +assert.strictEqual(myVfs.readFileSync('/file.txt', 'utf8'), 'x'); +assert.strictEqual(myVfs.readFileSync('/file.txt', { encoding: 'utf8' }), 'x'); +assert.deepStrictEqual(myVfs.readFileSync('/file.txt'), Buffer.from('x')); diff --git a/test/parallel/test-vfs-readfile-flag.js b/test/parallel/test-vfs-readfile-flag.js new file mode 100644 index 00000000000000..5fcfc902b79188 --- /dev/null +++ b/test/parallel/test-vfs-readfile-flag.js @@ -0,0 +1,39 @@ +// Flags: --experimental-vfs +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Test readFileSync with flag: 'w+' truncates existing file +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'original content'); + + // Reading with 'w+' flag should truncate then read (empty result) + const result = myVfs.readFileSync('/file.txt', { flag: 'w+' }); + assert.strictEqual(result.length, 0); +} + +// Test readFileSync with flag: 'a+' on new file +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir', { recursive: true }); + + // Reading with 'a+' flag should create file if missing and return empty + const result = myVfs.readFileSync('/dir/new.txt', { flag: 'a+' }); + assert.strictEqual(result.length, 0); + + // File should now exist + assert.strictEqual(myVfs.existsSync('/dir/new.txt'), true); +} + +// Test async readFile with flag: 'w+' truncates existing file +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file2.txt', 'some data'); + + myVfs.promises.readFile('/file2.txt', { flag: 'w+' }).then(common.mustCall((result) => { + assert.strictEqual(result.length, 0); + })); +} diff --git a/test/parallel/test-vfs-real-provider-handle.js b/test/parallel/test-vfs-real-provider-handle.js new file mode 100644 index 00000000000000..5246e28e3206c5 --- /dev/null +++ b/test/parallel/test-vfs-real-provider-handle.js @@ -0,0 +1,104 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// RealFileHandle: sync and async file-handle operations, plus EBADF +// behaviour after close. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); +const { getVirtualFd } = require('internal/vfs/fd'); + +tmpdir.refresh(); +const root = path.join(tmpdir.path, 'real-handle'); +fs.mkdirSync(root, { recursive: true }); +const myVfs = vfs.create(new vfs.RealFSProvider(root)); + +(async () => { + // ===== Sync read/write/stat/truncate via openSync + getVirtualFd ===== + { + fs.writeFileSync(path.join(root, 'sync-rw.txt'), 'hello world'); + const fd = myVfs.openSync('/sync-rw.txt', 'r+'); + const handle = getVirtualFd(fd).entry; + + const buf = Buffer.alloc(5); + assert.strictEqual(handle.readSync(buf, 0, 5, 0), 5); + assert.strictEqual(buf.toString(), 'hello'); + + const wbuf = Buffer.from('zz'); + assert.strictEqual(handle.writeSync(wbuf, 0, 2, 0), 2); + + assert.strictEqual(handle.statSync().isFile(), true); + assert.strictEqual(handle.readFileSync('utf8'), 'zzllo world'); + + handle.writeFileSync('replaced'); + assert.strictEqual(handle.readFileSync('utf8'), 'replaced'); + + myVfs.closeSync(fd); + } + + // ===== Async read/write/stat/truncate via provider.open ===== + { + await myVfs.promises.writeFile('/h2.txt', 'abcdef'); + const handle = await myVfs.provider.open('/h2.txt', 'r+'); + + const buf = Buffer.alloc(3); + assert.strictEqual(handle.readSync(buf, 0, 3, 0), 3); + assert.strictEqual(buf.toString(), 'abc'); + + const r = await handle.read(Buffer.alloc(3), 0, 3, 3); + assert.strictEqual(r.bytesRead, 3); + assert.strictEqual(r.buffer.toString(), 'def'); + + handle.writeSync(Buffer.from('ZZ'), 0, 2, 0); + const w = await handle.write(Buffer.from('YY'), 0, 2, 4); + assert.strictEqual(w.bytesWritten, 2); + + const s1 = handle.statSync(); + const s2 = await handle.stat(); + assert.strictEqual(s1.size, s2.size); + + assert.ok(handle.readFileSync().length > 0); + assert.ok((await handle.readFile()).length > 0); + + handle.writeFileSync('OVERWRITTEN'); + assert.strictEqual(handle.readFileSync('utf8'), 'OVERWRITTEN'); + await handle.writeFile('async-overwrite'); + assert.strictEqual(await handle.readFile('utf8'), 'async-overwrite'); + + handle.truncateSync(3); + await handle.truncate(2); + + await handle.close(); + } + + // ===== EBADF after close ===== + { + await myVfs.promises.writeFile('/h.txt', 'hello'); + const handle = await myVfs.provider.open('/h.txt', 'r'); + await handle.close(); + assert.strictEqual(handle.closed, true); + assert.throws(() => handle.readSync(Buffer.alloc(1), 0, 1, 0), + { code: 'EBADF' }); + await assert.rejects(handle.read(Buffer.alloc(1), 0, 1, 0), + { code: 'EBADF' }); + assert.throws(() => handle.writeSync(Buffer.from('x'), 0, 1, 0), + { code: 'EBADF' }); + await assert.rejects(handle.write(Buffer.from('x'), 0, 1, 0), + { code: 'EBADF' }); + assert.throws(() => handle.readFileSync(), { code: 'EBADF' }); + await assert.rejects(handle.readFile(), { code: 'EBADF' }); + assert.throws(() => handle.writeFileSync('x'), { code: 'EBADF' }); + await assert.rejects(handle.writeFile('x'), { code: 'EBADF' }); + assert.throws(() => handle.statSync(), { code: 'EBADF' }); + await assert.rejects(handle.stat(), { code: 'EBADF' }); + assert.throws(() => handle.truncateSync(), { code: 'EBADF' }); + await assert.rejects(handle.truncate(), { code: 'EBADF' }); + // Subsequent close is a no-op + handle.closeSync(); + await handle.close(); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-real-provider-promises.js b/test/parallel/test-vfs-real-provider-promises.js new file mode 100644 index 00000000000000..932a30fd086dab --- /dev/null +++ b/test/parallel/test-vfs-real-provider-promises.js @@ -0,0 +1,55 @@ +// Flags: --experimental-vfs +'use strict'; + +// Promises API for RealFSProvider: every async/promises method, +// plus access() existing/missing. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +tmpdir.refresh(); +const root = path.join(tmpdir.path, 'real-provider-promises'); +fs.mkdirSync(root, { recursive: true }); +const myVfs = vfs.create(new vfs.RealFSProvider(root)); + +(async () => { + // writeFile + readFile + await myVfs.promises.writeFile('/a.txt', 'hello'); + assert.strictEqual(await myVfs.promises.readFile('/a.txt', 'utf8'), 'hello'); + + // stat / lstat / access + const st = await myVfs.promises.stat('/a.txt'); + assert.strictEqual(st.size, 5); + assert.strictEqual((await myVfs.promises.lstat('/a.txt')).isFile(), true); + await myVfs.promises.access('/a.txt'); + await assert.rejects(myVfs.promises.access('/missing.txt'), + { code: 'ENOENT' }); + + // mkdir / readdir / rmdir + await myVfs.promises.mkdir('/d/sub', { recursive: true }); + const entries = await myVfs.promises.readdir('/d'); + assert.deepStrictEqual(entries.sort(), ['sub']); + await myVfs.promises.rmdir('/d/sub'); + + // rename + await myVfs.promises.writeFile('/old.txt', 'x'); + await myVfs.promises.rename('/old.txt', '/new.txt'); + assert.strictEqual(myVfs.existsSync('/old.txt'), false); + assert.strictEqual(myVfs.existsSync('/new.txt'), true); + + // unlink + await myVfs.promises.unlink('/new.txt'); + assert.strictEqual(myVfs.existsSync('/new.txt'), false); + + // copyFile + await myVfs.promises.copyFile('/a.txt', '/copy.txt'); + assert.strictEqual(await myVfs.promises.readFile('/copy.txt', 'utf8'), 'hello'); + + // open async error + await assert.rejects(myVfs.provider.open('/missing.txt', 'r'), + { code: 'ENOENT' }); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-real-provider-symlinks.js b/test/parallel/test-vfs-real-provider-symlinks.js new file mode 100644 index 00000000000000..d84c17ecd8490d --- /dev/null +++ b/test/parallel/test-vfs-real-provider-symlinks.js @@ -0,0 +1,111 @@ +// Flags: --experimental-vfs +'use strict'; + +// Symlink and path-escape behaviour for RealFSProvider. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +tmpdir.refresh(); +const root = path.join(tmpdir.path, 'real-symlinks'); +fs.mkdirSync(root, { recursive: true }); +const myVfs = vfs.create(new vfs.RealFSProvider(root)); + +(async () => { + // .. traversal in VFS paths can't escape root + { + const subDir = path.join(root, 'sandbox'); + fs.mkdirSync(subDir, { recursive: true }); + const subVfs = vfs.create(new vfs.RealFSProvider(subDir)); + assert.throws(() => subVfs.statSync('/../hello.txt'), { code: 'ENOENT' }); + assert.throws(() => subVfs.readFileSync('/../../../etc/passwd'), + { code: 'ENOENT' }); + fs.rmdirSync(subDir); + } + + // Path traversal via a non-leading-slash relative path + { + const escapeProvider = new vfs.RealFSProvider(root); + assert.throws(() => escapeProvider.statSync('../etc/passwd'), + { code: 'ENOENT' }); + } + + // Symlinks: absolute target rejected with EACCES + { + assert.throws(() => myVfs.symlinkSync('/etc/passwd', '/escape'), + { code: 'EACCES' }); + await assert.rejects(myVfs.promises.symlink('/etc/passwd', '/escape2'), + { code: 'EACCES' }); + } + + // Symlinks: relative target outside root rejected with EACCES + { + assert.throws(() => myVfs.symlinkSync('../../escape', '/bad-link'), + { code: 'EACCES' }); + await assert.rejects( + myVfs.promises.symlink('../../escape', '/bad-link2'), + { code: 'EACCES' }); + } + + // Symlink with relative target inside root — readlink returns target as-is + { + fs.writeFileSync(path.join(root, 'rel-target.txt'), 'ok'); + fs.symlinkSync('rel-target.txt', path.join(root, 'rel-link')); + assert.strictEqual(myVfs.readlinkSync('/rel-link'), 'rel-target.txt'); + assert.strictEqual(await myVfs.promises.readlink('/rel-link'), + 'rel-target.txt'); + } + + // Symlink whose absolute target is inside root → translated to VFS path + { + fs.writeFileSync(path.join(root, 'target.txt'), 'x'); + fs.symlinkSync(path.join(root, 'target.txt'), + path.join(root, 'abs-link')); + assert.strictEqual(myVfs.readlinkSync('/abs-link'), '/target.txt'); + assert.strictEqual(await myVfs.promises.readlink('/abs-link'), + '/target.txt'); + } + + // Symlink whose absolute target equals root → '/' + { + fs.symlinkSync(root, path.join(root, 'root-link')); + assert.strictEqual(myVfs.readlinkSync('/root-link'), '/'); + assert.strictEqual(await myVfs.promises.readlink('/root-link'), '/'); + } + + // Symlink that points outside root: realpath rejects with EACCES + { + fs.writeFileSync(path.join(tmpdir.path, 'outside.txt'), 'forbidden'); + fs.symlinkSync(path.join(tmpdir.path, 'outside.txt'), + path.join(root, 'esc-link')); + + // Readlink returns the absolute target as-is (no translation since it's + // outside the root) + const target = myVfs.readlinkSync('/esc-link'); + assert.strictEqual(target, path.join(tmpdir.path, 'outside.txt')); + + // Realpath rejects: the resolved target escapes root + assert.throws(() => myVfs.realpathSync('/esc-link'), { code: 'EACCES' }); + await assert.rejects(myVfs.promises.realpath('/esc-link'), + { code: 'EACCES' }); + } + + // Realpath on root and on a subdir + { + fs.mkdirSync(path.join(root, 'sub2'), { recursive: true }); + assert.strictEqual(myVfs.realpathSync('/'), '/'); + assert.strictEqual(myVfs.realpathSync('/sub2'), '/sub2'); + assert.strictEqual(await myVfs.promises.realpath('/sub2'), '/sub2'); + } + + // RealFSProvider with a rootPath that already ends in path.sep + { + fs.writeFileSync(path.join(root, 'tr.txt'), 'tr'); + const trailingProvider = new vfs.RealFSProvider(root + path.sep); + assert.strictEqual(trailingProvider.readFileSync('/tr.txt', 'utf8'), 'tr'); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-real-provider-watch.js b/test/parallel/test-vfs-real-provider-watch.js new file mode 100644 index 00000000000000..f4218fa408ee4d --- /dev/null +++ b/test/parallel/test-vfs-real-provider-watch.js @@ -0,0 +1,40 @@ +// Flags: --experimental-vfs +'use strict'; + +// watch / promises.watch / watchFile through RealFSProvider. + +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +tmpdir.refresh(); +const root = path.join(tmpdir.path, 'real-provider-watch'); +fs.mkdirSync(root, { recursive: true }); +const myVfs = vfs.create(new vfs.RealFSProvider(root)); + +assert.strictEqual(myVfs.provider.supportsWatch, true); + +// fs.watch wrapper +{ + fs.writeFileSync(path.join(root, 'watch-me.txt'), 'a'); + const watcher = myVfs.watch('/watch-me.txt', { persistent: false }); + watcher.close(); +} + +// promises.watch wrapper +(async () => { + fs.writeFileSync(path.join(root, 'pwatch.txt'), 'a'); + const iter = myVfs.promises.watch('/pwatch.txt', { persistent: false }); + await iter.return(); +})().then(common.mustCall()); + +// watchFile / unwatchFile +{ + fs.writeFileSync(path.join(root, 'wf.txt'), 'a'); + const listener = () => {}; + myVfs.watchFile('/wf.txt', { persistent: false }, listener); + myVfs.unwatchFile('/wf.txt', listener); +} diff --git a/test/parallel/test-vfs-real-provider.js b/test/parallel/test-vfs-real-provider.js new file mode 100644 index 00000000000000..a54181d8f6dc4a --- /dev/null +++ b/test/parallel/test-vfs-real-provider.js @@ -0,0 +1,148 @@ +// Flags: --experimental-vfs +'use strict'; + +// Synchronous API for RealFSProvider: construction, basic file ops, +// stats, directories, copy, realpath. Async/promises live in +// test-vfs-real-provider-promises.js, file-handle ops live in +// test-vfs-real-provider-handle.js, and symlinks/path-escape live in +// test-vfs-real-provider-symlinks.js. + +require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +tmpdir.refresh(); + +const testDir = path.join(tmpdir.path, 'vfs-real-provider'); +fs.mkdirSync(testDir, { recursive: true }); + +// Capability flags + construction +{ + const provider = new vfs.RealFSProvider(testDir); + assert.ok(provider); + assert.strictEqual(provider.rootPath, testDir); + assert.strictEqual(provider.readonly, false); + assert.strictEqual(provider.supportsSymlinks, true); + assert.strictEqual(provider.supportsWatch, true); +} + +// Invalid rootPath +{ + assert.throws(() => new vfs.RealFSProvider(123), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.throws(() => new vfs.RealFSProvider({}), + { code: 'ERR_INVALID_ARG_TYPE' }); +} + +// vfs.create(provider) wires it up +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + assert.ok(realVfs); + assert.strictEqual(realVfs.readonly, false); +} + +// readFile / writeFile sync +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + realVfs.writeFileSync('/hello.txt', 'Hello from VFS!'); + const realPath = path.join(testDir, 'hello.txt'); + assert.strictEqual(fs.existsSync(realPath), true); + assert.strictEqual(fs.readFileSync(realPath, 'utf8'), 'Hello from VFS!'); + assert.strictEqual(realVfs.readFileSync('/hello.txt', 'utf8'), 'Hello from VFS!'); + fs.unlinkSync(realPath); +} + +// statSync / lstatSync +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + fs.writeFileSync(path.join(testDir, 'stat-test.txt'), 'content'); + fs.mkdirSync(path.join(testDir, 'stat-dir'), { recursive: true }); + + assert.strictEqual(realVfs.statSync('/stat-test.txt').isFile(), true); + assert.strictEqual(realVfs.statSync('/stat-dir').isDirectory(), true); + assert.strictEqual(realVfs.lstatSync('/stat-test.txt').isFile(), true); + + assert.throws(() => realVfs.statSync('/nonexistent'), + { code: 'ENOENT' }); + + fs.unlinkSync(path.join(testDir, 'stat-test.txt')); + fs.rmdirSync(path.join(testDir, 'stat-dir')); +} + +// readdirSync +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + fs.mkdirSync(path.join(testDir, 'readdir-test', 'subdir'), { recursive: true }); + fs.writeFileSync(path.join(testDir, 'readdir-test', 'a.txt'), 'a'); + fs.writeFileSync(path.join(testDir, 'readdir-test', 'b.txt'), 'b'); + + const entries = realVfs.readdirSync('/readdir-test'); + assert.deepStrictEqual(entries.sort(), ['a.txt', 'b.txt', 'subdir']); + + const dirents = realVfs.readdirSync('/readdir-test', { withFileTypes: true }); + assert.strictEqual(dirents.length, 3); + assert.ok(dirents.some((d) => d.name === 'a.txt' && d.isFile())); + assert.ok(dirents.some((d) => d.name === 'subdir' && d.isDirectory())); + + fs.unlinkSync(path.join(testDir, 'readdir-test', 'a.txt')); + fs.unlinkSync(path.join(testDir, 'readdir-test', 'b.txt')); + fs.rmdirSync(path.join(testDir, 'readdir-test', 'subdir')); + fs.rmdirSync(path.join(testDir, 'readdir-test')); +} + +// mkdir / rmdir / recursive mkdir +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + realVfs.mkdirSync('/new-dir'); + assert.strictEqual(fs.existsSync(path.join(testDir, 'new-dir')), true); + realVfs.rmdirSync('/new-dir'); + assert.strictEqual(fs.existsSync(path.join(testDir, 'new-dir')), false); + + realVfs.mkdirSync('/deep/nested/dir', { recursive: true }); + assert.strictEqual(fs.existsSync(path.join(testDir, 'deep/nested/dir')), true); + fs.rmdirSync(path.join(testDir, 'deep/nested/dir')); + fs.rmdirSync(path.join(testDir, 'deep/nested')); + fs.rmdirSync(path.join(testDir, 'deep')); +} + +// unlink +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + fs.writeFileSync(path.join(testDir, 'to-delete.txt'), 'delete me'); + assert.strictEqual(realVfs.existsSync('/to-delete.txt'), true); + realVfs.unlinkSync('/to-delete.txt'); + assert.strictEqual(fs.existsSync(path.join(testDir, 'to-delete.txt')), false); +} + +// rename +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + fs.writeFileSync(path.join(testDir, 'old-name.txt'), 'rename me'); + realVfs.renameSync('/old-name.txt', '/new-name.txt'); + assert.strictEqual(fs.existsSync(path.join(testDir, 'old-name.txt')), false); + assert.strictEqual(fs.readFileSync(path.join(testDir, 'new-name.txt'), 'utf8'), + 'rename me'); + fs.unlinkSync(path.join(testDir, 'new-name.txt')); +} + +// copyFile +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + fs.writeFileSync(path.join(testDir, 'source.txt'), 'copy me'); + realVfs.copyFileSync('/source.txt', '/dest.txt'); + assert.strictEqual(fs.readFileSync(path.join(testDir, 'dest.txt'), 'utf8'), + 'copy me'); + fs.unlinkSync(path.join(testDir, 'source.txt')); + fs.unlinkSync(path.join(testDir, 'dest.txt')); +} + +// realpathSync (non-symlink) +{ + const realVfs = vfs.create(new vfs.RealFSProvider(testDir)); + fs.writeFileSync(path.join(testDir, 'real.txt'), 'content'); + assert.strictEqual(realVfs.realpathSync('/real.txt'), '/real.txt'); + fs.unlinkSync(path.join(testDir, 'real.txt')); +} diff --git a/test/parallel/test-vfs-rename.js b/test/parallel/test-vfs-rename.js new file mode 100644 index 00000000000000..26506f01dd7338 --- /dev/null +++ b/test/parallel/test-vfs-rename.js @@ -0,0 +1,46 @@ +// Flags: --experimental-vfs +'use strict'; + +// Rename behaviour: overwrite, type mismatches, same-parent rename. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Renaming a file onto a directory throws EISDIR +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'x'); + myVfs.mkdirSync('/dir'); + assert.throws(() => myVfs.renameSync('/file.txt', '/dir'), + { code: 'EISDIR' }); +} + +// Renaming a directory onto a file throws ENOTDIR +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'x'); + myVfs.mkdirSync('/dir'); + assert.throws(() => myVfs.renameSync('/dir', '/file.txt'), + { code: 'ENOTDIR' }); +} + +// Renaming a file onto another file overwrites +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'a'); + myVfs.writeFileSync('/b.txt', 'b'); + myVfs.renameSync('/a.txt', '/b.txt'); + assert.strictEqual(myVfs.readFileSync('/b.txt', 'utf8'), 'a'); + assert.strictEqual(myVfs.existsSync('/a.txt'), false); +} + +// Renaming within the same parent directory +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/d'); + myVfs.writeFileSync('/d/a.txt', 'x'); + myVfs.renameSync('/d/a.txt', '/d/b.txt'); + assert.strictEqual(myVfs.existsSync('/d/a.txt'), false); + assert.strictEqual(myVfs.existsSync('/d/b.txt'), true); +} diff --git a/test/parallel/test-vfs-rm-edge-cases.js b/test/parallel/test-vfs-rm-edge-cases.js new file mode 100644 index 00000000000000..9961940aea830e --- /dev/null +++ b/test/parallel/test-vfs-rm-edge-cases.js @@ -0,0 +1,70 @@ +// Flags: --experimental-vfs +'use strict'; + +// Tests for VFS rmSync edge cases: +// - rmSync on a directory without recursive must throw EISDIR +// - rmSync on a symlink must not recurse into the target directory + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// rmSync on a directory without { recursive: true } must throw EISDIR. +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir'); + + assert.throws(() => myVfs.rmSync('/dir'), { code: 'EISDIR' }); + // Directory should still exist after the failed rmSync + assert.strictEqual(myVfs.existsSync('/dir'), true); +} + +// rmSync(link, { recursive: true }) removes symlink without recursing +// into the target directory. +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir/sub', { recursive: true }); + myVfs.writeFileSync('/dir/sub/file.txt', 'x'); + myVfs.symlinkSync('/dir', '/link'); + + myVfs.rmSync('/link', { recursive: true }); + + // Symlink should be removed + assert.strictEqual(myVfs.existsSync('/link'), false); + // Target directory and its contents should still exist + assert.strictEqual(myVfs.existsSync('/dir/sub/file.txt'), true); +} + +// rmSync with force: true ignores ENOENT +{ + const myVfs = vfs.create(); + myVfs.rmSync('/missing.txt', { force: true }); +} + +// rmSync without force on missing path throws ENOENT +{ + const myVfs = vfs.create(); + assert.throws(() => myVfs.rmSync('/missing.txt'), { code: 'ENOENT' }); +} + +// promises.rm equivalents +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/d/sub', { recursive: true }); + myVfs.writeFileSync('/d/sub/f.txt', 'x'); + + await assert.rejects(myVfs.promises.rm('/d'), { code: 'EISDIR' }); + await myVfs.promises.rm('/d', { recursive: true }); + assert.strictEqual(myVfs.existsSync('/d'), false); + + await myVfs.promises.rm('/missing', { force: true }); + await assert.rejects(myVfs.promises.rm('/missing'), { code: 'ENOENT' }); + + // promises.rm on symlink unlinks without recursion + myVfs.mkdirSync('/d2/sub', { recursive: true }); + myVfs.writeFileSync('/d2/sub/file.txt', 'x'); + myVfs.symlinkSync('/d2', '/link2'); + await myVfs.promises.rm('/link2', { recursive: true }); + assert.strictEqual(myVfs.existsSync('/link2'), false); + assert.strictEqual(myVfs.existsSync('/d2/sub/file.txt'), true); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-rmdir-symlink.js b/test/parallel/test-vfs-rmdir-symlink.js new file mode 100644 index 00000000000000..fba1981322f559 --- /dev/null +++ b/test/parallel/test-vfs-rmdir-symlink.js @@ -0,0 +1,31 @@ +// Flags: --experimental-vfs +'use strict'; + +// rmdirSync on a symlink to a directory should throw ENOTDIR + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir'); + myVfs.symlinkSync('/dir', '/link'); + + assert.throws(() => myVfs.rmdirSync('/link'), + { code: 'ENOTDIR' }); + + // Both the symlink and directory should still exist + assert.ok(myVfs.existsSync('/link')); + assert.ok(myVfs.existsSync('/dir')); +} + +// promises.rmdir equivalent +(async () => { + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir'); + myVfs.symlinkSync('/dir', '/link'); + + await assert.rejects(myVfs.promises.rmdir('/link'), + { code: 'ENOTDIR' }); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-stats-bigint.js b/test/parallel/test-vfs-stats-bigint.js new file mode 100644 index 00000000000000..074ac9fa379f88 --- /dev/null +++ b/test/parallel/test-vfs-stats-bigint.js @@ -0,0 +1,36 @@ +// Flags: --experimental-vfs +'use strict'; + +// Verify { bigint: true } returns BigInt values for VFS stats. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'x'); + +const st = myVfs.statSync('/file.txt', { bigint: true }); +assert.strictEqual(typeof st.size, 'bigint'); +assert.strictEqual(st.size, 1n); +assert.strictEqual(typeof st.ino, 'bigint'); +assert.strictEqual(typeof st.mode, 'bigint'); + +// Bigint stats for directories +{ + const v = vfs.create(); + v.mkdirSync('/dir'); + const st = v.statSync('/dir', { bigint: true }); + assert.strictEqual(typeof st.size, 'bigint'); + assert.strictEqual(st.isDirectory(), true); +} + +// Bigint stats for symlinks via lstat +{ + const v = vfs.create(); + v.mkdirSync('/dir'); + v.symlinkSync('/dir', '/link'); + const st = v.lstatSync('/link', { bigint: true }); + assert.strictEqual(typeof st.size, 'bigint'); + assert.strictEqual(st.isSymbolicLink(), true); +} diff --git a/test/parallel/test-vfs-stats-helpers.js b/test/parallel/test-vfs-stats-helpers.js new file mode 100644 index 00000000000000..3df0701138ce9c --- /dev/null +++ b/test/parallel/test-vfs-stats-helpers.js @@ -0,0 +1,80 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// Exercise the default-option paths in createFileStats / createDirectoryStats +// / createSymlinkStats / createZeroStats. These defaults aren't taken when +// MemoryProvider populates every option from the entry, so we drive the +// helpers directly. + +require('../common'); +const assert = require('assert'); +const { + createFileStats, + createDirectoryStats, + createSymlinkStats, + createZeroStats, +} = require('internal/vfs/stats'); + +// All defaults — no options object at all +{ + const st = createFileStats(42); + assert.strictEqual(st.size, 42); + assert.strictEqual((st.mode & 0o777), 0o644); + assert.strictEqual(st.nlink, 1); + assert.ok(st.isFile()); + + const dirSt = createDirectoryStats(); + assert.ok(dirSt.isDirectory()); + assert.strictEqual((dirSt.mode & 0o777), 0o755); + + const linkSt = createSymlinkStats(7); + assert.ok(linkSt.isSymbolicLink()); + assert.strictEqual((linkSt.mode & 0o777), 0o777); + assert.strictEqual(linkSt.size, 7); +} + +// Empty options object exercises the `?? default` right-hand side. +{ + const st = createFileStats(1, {}); + assert.ok(st.isFile()); + const dirSt = createDirectoryStats({}); + assert.ok(dirSt.isDirectory()); + const linkSt = createSymlinkStats(0, {}); + assert.ok(linkSt.isSymbolicLink()); +} + +// Bigint variant of zero-stats +{ + const z = createZeroStats({ bigint: true }); + assert.strictEqual(typeof z.size, 'bigint'); + assert.strictEqual(z.size, 0n); + assert.strictEqual(z.mode, 0n); +} + +// Non-bigint zero-stats with no options +{ + const z = createZeroStats(); + assert.strictEqual(z.size, 0); + assert.strictEqual(z.mode, 0); +} + +// Cover the `process.getuid?.() ?? 0` fallback (Windows-like environment). +// We stub the optional methods to simulate their absence. +{ + const realUid = process.getuid; + const realGid = process.getgid; + process.getuid = undefined; + process.getgid = undefined; + try { + const fs = createFileStats(0); + assert.strictEqual(fs.uid, 0); + assert.strictEqual(fs.gid, 0); + const ds = createDirectoryStats(); + assert.strictEqual(ds.uid, 0); + const ls = createSymlinkStats(0); + assert.strictEqual(ls.uid, 0); + } finally { + process.getuid = realUid; + process.getgid = realGid; + } +} diff --git a/test/parallel/test-vfs-stats-ino-dev.js b/test/parallel/test-vfs-stats-ino-dev.js new file mode 100644 index 00000000000000..9a5e69d603bddf --- /dev/null +++ b/test/parallel/test-vfs-stats-ino-dev.js @@ -0,0 +1,24 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS stats must have non-zero, unique ino and a distinctive dev. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/f1.txt', 'a'); +myVfs.writeFileSync('/f2.txt', 'b'); + +const s1 = myVfs.statSync('/f1.txt'); +const s2 = myVfs.statSync('/f2.txt'); + +// Dev should be distinctive VFS value (4085 = 0xVF5) +assert.strictEqual(s1.dev, 4085); +assert.strictEqual(s2.dev, 4085); + +// Ino should be unique per call +assert.notStrictEqual(s1.ino, 0); +assert.notStrictEqual(s2.ino, 0); +assert.notStrictEqual(s1.ino, s2.ino); diff --git a/test/parallel/test-vfs-stream-errors.js b/test/parallel/test-vfs-stream-errors.js new file mode 100644 index 00000000000000..df006d5cf3d9fb --- /dev/null +++ b/test/parallel/test-vfs-stream-errors.js @@ -0,0 +1,65 @@ +// Flags: --experimental-vfs +'use strict'; + +// Error paths in VFS streams: missing files, EBADF on closed fds, +// destroyed streams, and write to a path under a missing parent. + +const common = require('../common'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); + +// Read of a nonexistent file emits 'error' +{ + const stream = myVfs.createReadStream('/missing.txt'); + stream.on('error', common.expectsError({ + code: 'ENOENT', + })); +} + +// Read on a stream whose fd has been pre-closed → EBADF on first _read +{ + myVfs.writeFileSync('/x.txt', 'hi'); + const fd = myVfs.openSync('/x.txt'); + const rs = myVfs.createReadStream('/x.txt', { fd, autoClose: false }); + myVfs.closeSync(fd); + rs.on('error', common.expectsError({ + code: 'EBADF', + })); + rs.resume(); +} + +// Read stream with autoClose:true after the fd is invalidated — covers the +// close-error swallow path inside the stream's #close. +{ + myVfs.writeFileSync('/cl.txt', 'data'); + const fd = myVfs.openSync('/cl.txt'); + const rs = myVfs.createReadStream('/cl.txt', { fd, autoClose: true }); + myVfs.closeSync(fd); + rs.on('error', common.expectsError()); + rs.resume(); +} + +// WriteStream synchronously failing to open → destroys on next tick +{ + const ws = myVfs.createWriteStream('/missing-dir/foo.txt', { flags: 'wx' }); + ws.on('error', common.expectsError()); +} + +// WriteStream destroyed before write() — covers the destroyed-true branch +// in _write. +{ + const ws = myVfs.createWriteStream('/wd.txt'); + ws.on('error', () => {}); + ws.destroy(new Error('boom')); +} + +// _write errors when writeSync throws (closed fd) +{ + myVfs.writeFileSync('/wfd.txt', ''); + const fd = myVfs.openSync('/wfd.txt', 'w'); + const ws = myVfs.createWriteStream('/wfd.txt', { fd, autoClose: false }); + myVfs.closeSync(fd); + ws.on('error', common.expectsError()); + ws.write('x'); +} diff --git a/test/parallel/test-vfs-stream-explicit-fd.js b/test/parallel/test-vfs-stream-explicit-fd.js new file mode 100644 index 00000000000000..d439e441a8699d --- /dev/null +++ b/test/parallel/test-vfs-stream-explicit-fd.js @@ -0,0 +1,57 @@ +// Flags: --experimental-vfs +'use strict'; + +// Test createReadStream / createWriteStream with an explicit `fd` option. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'hello world'); + +function readStream(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on('data', (c) => chunks.push(c)); + stream.on('end', () => resolve(Buffer.concat(chunks).toString())); + stream.on('error', reject); + }); +} + +// Read using an existing fd; autoClose:false leaves fd open +{ + const fd = myVfs.openSync('/file.txt', 'r'); + const stream = myVfs.createReadStream('/file.txt', { fd, autoClose: false }); + let opened = false; + stream.on('open', () => { opened = true; }); + readStream(stream).then(common.mustCall((s) => { + assert.strictEqual(s, 'hello world'); + assert.strictEqual(opened, true); + myVfs.closeSync(fd); + })); +} + +// WriteStream with explicit fd; autoClose:false leaves the fd open +(async () => { + const fd = myVfs.openSync('/fd-write.txt', 'w'); + const stream = myVfs.createWriteStream('/fd-write.txt', { fd, autoClose: false }); + await new Promise((resolve) => stream.on('ready', resolve)); + await new Promise((resolve, reject) => + stream.end('via-fd', (err) => (err ? reject(err) : resolve()))); + myVfs.closeSync(fd); + assert.strictEqual(myVfs.readFileSync('/fd-write.txt', 'utf8'), 'via-fd'); +})().then(common.mustCall()); + +// WriteStream with explicit fd + start position +(async () => { + myVfs.writeFileSync('/pad.txt', 'AAAAAAAAAA'); + const fd = myVfs.openSync('/pad.txt', 'r+'); + const ws = myVfs.createWriteStream('/pad.txt', { fd, start: 5, autoClose: false }); + await new Promise((resolve) => ws.on('ready', resolve)); + await new Promise((resolve, reject) => + ws.end('XX', (err) => (err ? reject(err) : resolve()))); + myVfs.closeSync(fd); + // Position 5 → "AAAAAXXAAA" + assert.strictEqual(myVfs.readFileSync('/pad.txt', 'utf8'), 'AAAAAXXAAA'); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-stream-properties.js b/test/parallel/test-vfs-stream-properties.js new file mode 100644 index 00000000000000..98691c4961ed21 --- /dev/null +++ b/test/parallel/test-vfs-stream-properties.js @@ -0,0 +1,40 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS streams must expose bytesRead, bytesWritten, and pending. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// ReadStream: bytesRead and pending +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/stream.txt', 'stream data'); + + const rs = myVfs.createReadStream('/stream.txt'); + assert.strictEqual(rs.pending, true); + + rs.on('data', common.mustCall(() => { + assert.strictEqual(rs.pending, false); + assert.ok(rs.bytesRead > 0); + })); + + rs.on('end', common.mustCall()); +} + +// WriteStream: bytesWritten and pending +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/out.txt', ''); + + const ws = myVfs.createWriteStream('/out.txt'); + assert.strictEqual(ws.pending, true); + assert.strictEqual(ws.bytesWritten, 0); + + ws.write('hello', common.mustCall(() => { + assert.strictEqual(ws.pending, false); + assert.strictEqual(ws.bytesWritten, 5); + ws.end(); + })); +} diff --git a/test/parallel/test-vfs-stream-validation.js b/test/parallel/test-vfs-stream-validation.js new file mode 100644 index 00000000000000..7e599fccf9acd0 --- /dev/null +++ b/test/parallel/test-vfs-stream-validation.js @@ -0,0 +1,29 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS stream constructors must validate start/end synchronously. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'abc'); + +// ReadStream: start > end must throw ERR_OUT_OF_RANGE synchronously +assert.throws( + () => myVfs.createReadStream('/file.txt', { start: 2, end: 1 }), + { code: 'ERR_OUT_OF_RANGE' }, +); + +// ReadStream: negative start +assert.throws( + () => myVfs.createReadStream('/file.txt', { start: -1 }), + { code: 'ERR_OUT_OF_RANGE' }, +); + +// ReadStream: negative end +assert.throws( + () => myVfs.createReadStream('/file.txt', { end: -1 }), + { code: 'ERR_OUT_OF_RANGE' }, +); diff --git a/test/parallel/test-vfs-streams.js b/test/parallel/test-vfs-streams.js new file mode 100644 index 00000000000000..83625d0072daaa --- /dev/null +++ b/test/parallel/test-vfs-streams.js @@ -0,0 +1,302 @@ +// Flags: --experimental-vfs +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Test basic createReadStream +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'hello world'); + + const stream = myVfs.createReadStream('/file.txt'); + let data = ''; + + stream.on('open', common.mustCall((fd) => { + assert.notStrictEqual(fd & 0x40000000, 0); + })); + + stream.on('ready', common.mustCall()); + + stream.on('data', (chunk) => { + data += chunk; + }); + + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'hello world'); + })); + + stream.on('close', common.mustCall()); +} + +// Test createReadStream with encoding +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/encoded.txt', 'encoded content'); + + const stream = myVfs.createReadStream('/encoded.txt', { encoding: 'utf8' }); + let data = ''; + let receivedString = false; + + stream.on('data', (chunk) => { + if (typeof chunk === 'string') { + receivedString = true; + } + data += chunk; + }); + + stream.on('end', common.mustCall(() => { + assert.strictEqual(receivedString, true); + assert.strictEqual(data, 'encoded content'); + })); +} + +// Test createReadStream with start and end +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/range.txt', '0123456789'); + + const stream = myVfs.createReadStream('/range.txt', { + start: 2, + end: 5, + }); + let data = ''; + + stream.on('data', (chunk) => { + data += chunk; + }); + + stream.on('end', common.mustCall(() => { + // End is inclusive, so positions 2, 3, 4, 5 = "2345" (4 chars) + assert.strictEqual(data, '2345'); + })); +} + +// Test createReadStream with start only +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/start.txt', 'abcdefghij'); + + const stream = myVfs.createReadStream('/start.txt', { start: 5 }); + let data = ''; + + stream.on('data', (chunk) => { + data += chunk; + }); + + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'fghij'); + })); +} + +// Test createReadStream with non-existent file +{ + const myVfs = vfs.create(); + + const stream = myVfs.createReadStream('/nonexistent.txt'); + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ENOENT'); + })); +} + +// Test createReadStream path property +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/path-test.txt', 'test'); + + const stream = myVfs.createReadStream('/path-test.txt'); + assert.strictEqual(stream.path, '/path-test.txt'); + + stream.on('data', () => {}); // Consume data + stream.on('end', common.mustCall()); +} + +// Test createReadStream with small highWaterMark +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/small-hwm.txt', 'AAAABBBBCCCCDDDD'); + + const stream = myVfs.createReadStream('/small-hwm.txt', { + highWaterMark: 4, + }); + + const chunks = []; + stream.on('data', (chunk) => { + chunks.push(chunk.toString()); + }); + + stream.on('end', common.mustCall(() => { + // With highWaterMark of 4, we should get multiple chunks + assert.ok(chunks.length >= 1); + assert.strictEqual(chunks.join(''), 'AAAABBBBCCCCDDDD'); + })); +} + +// Test createReadStream destroy +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/destroy.txt', 'content to destroy'); + + const stream = myVfs.createReadStream('/destroy.txt'); + + stream.on('open', common.mustCall(() => { + stream.destroy(); + })); + + stream.on('close', common.mustCall()); +} + +// Test createReadStream with large file +{ + const myVfs = vfs.create(); + const largeContent = 'X'.repeat(100000); + myVfs.writeFileSync('/large.txt', largeContent); + + const stream = myVfs.createReadStream('/large.txt'); + let receivedLength = 0; + + stream.on('data', (chunk) => { + receivedLength += chunk.length; + }); + + stream.on('end', common.mustCall(() => { + assert.strictEqual(receivedLength, 100000); + })); +} + +// Test createReadStream pipe to another stream +{ + const myVfs = vfs.create(); + const { Writable } = require('stream'); + + myVfs.writeFileSync('/pipe-source.txt', 'pipe this content'); + + const stream = myVfs.createReadStream('/pipe-source.txt'); + let collected = ''; + + const writable = new Writable({ + write(chunk, encoding, callback) { + collected += chunk; + callback(); + }, + }); + + stream.pipe(writable); + + writable.on('finish', common.mustCall(() => { + assert.strictEqual(collected, 'pipe this content'); + })); +} + +// Test autoClose: false +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/no-auto-close.txt', 'content'); + + const stream = myVfs.createReadStream('/no-auto-close.txt', { + autoClose: false, + }); + + stream.on('close', common.mustCall()); + + // Start flowing the stream + stream.resume(); + stream.on('end', common.mustCall(() => { + // With autoClose: false, close should not be emitted automatically + // We need to manually destroy the stream + setImmediate(() => { + stream.destroy(); + }); + })); +} + +// ==================== Additional coverage ==================== + +const { Readable } = require('stream'); +const { pipeline } = require('stream/promises'); + +// Slicing read stream with start/end +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/slice.txt', 'hello world'); + const rs = myVfs.createReadStream('/slice.txt', { start: 6, end: 10 }); + const chunks = []; + rs.on('data', (c) => chunks.push(c)); + rs.on('end', common.mustCall(() => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'world'); + })); +} + +// start: beyond file size → empty stream +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/sm.txt', 'abc'); + const rs = myVfs.createReadStream('/sm.txt', { start: 10 }); + rs.on('data', common.mustNotCall('no data expected')); + rs.on('end', common.mustCall()); +} + +// Empty file → end immediately +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/empty.txt', ''); + const rs = myVfs.createReadStream('/empty.txt'); + rs.on('data', common.mustNotCall('no data expected')); + rs.on('end', common.mustCall()); +} + +// Pipeline write +(async () => { + const myVfs = vfs.create(); + await pipeline( + Readable.from([Buffer.from('hello'), Buffer.from(' world')]), + myVfs.createWriteStream('/out.txt'), + ); + assert.strictEqual(myVfs.readFileSync('/out.txt', 'utf8'), 'hello world'); +})().then(common.mustCall()); + +// Pipeline write with start position +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/pad.txt', 'AAAAAAAAAA'); + await pipeline( + Readable.from([Buffer.from('XX')]), + myVfs.createWriteStream('/pad.txt', { start: 3, flags: 'r+' }), + ); + assert.strictEqual(myVfs.readFileSync('/pad.txt', 'utf8'), 'AAAXXAAAAA'); +})().then(common.mustCall()); + +// Write string chunk + encoding callback +(async () => { + const myVfs = vfs.create(); + const stream = myVfs.createWriteStream('/str.txt'); + await new Promise((resolve, reject) => { + stream.write('hello', 'utf8', (err) => (err ? reject(err) : resolve())); + }); + await new Promise((resolve) => stream.end(resolve)); + assert.strictEqual(myVfs.readFileSync('/str.txt', 'utf8'), 'hello'); +})().then(common.mustCall()); + +// path getter +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/p.txt', 'x'); + const rs = myVfs.createReadStream('/p.txt'); + assert.strictEqual(rs.path, '/p.txt'); + rs.destroy(); + + const ws = myVfs.createWriteStream('/p2.txt'); + assert.strictEqual(ws.path, '/p2.txt'); + ws.destroy(); +} + +// destroy() before any data triggers _destroy + close cleanup +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/d.txt', 'data'); + const rs = myVfs.createReadStream('/d.txt'); + rs.on('error', () => {}); + rs.destroy(); +} diff --git a/test/parallel/test-vfs-symlinks.js b/test/parallel/test-vfs-symlinks.js new file mode 100644 index 00000000000000..f251c08b6b9e33 --- /dev/null +++ b/test/parallel/test-vfs-symlinks.js @@ -0,0 +1,56 @@ +// Flags: --experimental-vfs +'use strict'; + +// Symlink behaviour in the default (memory) VFS: +// - Loop detection (ELOOP) +// - Absolute and relative targets +// - Symlinked parent directories transparently follow + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Direct symlink loop: /a -> /b -> /a +{ + const myVfs = vfs.create(); + myVfs.symlinkSync('/b', '/a'); + myVfs.symlinkSync('/a', '/b'); + assert.throws(() => myVfs.statSync('/a'), { code: 'ELOOP' }); + assert.throws(() => myVfs.realpathSync('/a'), { code: 'ELOOP' }); +} + +// Symlink loop in an intermediate path component +{ + const myVfs = vfs.create(); + myVfs.symlinkSync('/loop2', '/loop1'); + myVfs.symlinkSync('/loop1', '/loop2'); + assert.throws(() => myVfs.statSync('/loop1/sub'), { code: 'ELOOP' }); +} + +// Absolute symlink target inside the VFS +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/dir'); + myVfs.writeFileSync('/dir/file.txt', 'hi'); + myVfs.symlinkSync('/dir', '/abs-link'); + assert.strictEqual(myVfs.readFileSync('/abs-link/file.txt', 'utf8'), 'hi'); +} + +// Symlinked parent directory transparently follows on read+write +{ + const myVfs = vfs.create(); + myVfs.mkdirSync('/real-dir'); + myVfs.writeFileSync('/real-dir/file.txt', 'hello'); + myVfs.symlinkSync('/real-dir', '/link-dir'); + + assert.strictEqual(myVfs.readFileSync('/link-dir/file.txt', 'utf8'), 'hello'); + myVfs.writeFileSync('/link-dir/new.txt', 'new'); + assert.strictEqual(myVfs.readFileSync('/real-dir/new.txt', 'utf8'), 'new'); +} + +// Symlink onto an existing path throws EEXIST +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'x'); + assert.throws(() => myVfs.symlinkSync('/a.txt', '/a.txt'), { code: 'EEXIST' }); +} diff --git a/test/parallel/test-vfs-truncate-negative.js b/test/parallel/test-vfs-truncate-negative.js new file mode 100644 index 00000000000000..d051d00de8ac9f --- /dev/null +++ b/test/parallel/test-vfs-truncate-negative.js @@ -0,0 +1,15 @@ +// Flags: --experimental-vfs +'use strict'; + +// truncateSync with negative length must clamp to 0, not throw. + +require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/file.txt', 'abc'); + +myVfs.truncateSync('/file.txt', -1); +const content = myVfs.readFileSync('/file.txt', 'utf8'); +assert.strictEqual(content, ''); diff --git a/test/parallel/test-vfs-utimes.js b/test/parallel/test-vfs-utimes.js new file mode 100644 index 00000000000000..861b59aaff91d0 --- /dev/null +++ b/test/parallel/test-vfs-utimes.js @@ -0,0 +1,27 @@ +// Flags: --experimental-vfs +'use strict'; + +// utimes / lutimes accept Date instances, numeric seconds, strings, +// and other values (which fall through to a no-op time value). + +require('../common'); +const vfs = require('node:vfs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/u.txt', 'x'); +myVfs.symlinkSync('/u.txt', '/lk'); + +// Numeric seconds branch +myVfs.utimesSync('/u.txt', 1000, 2000); + +// Date object branch +myVfs.utimesSync('/u.txt', new Date(3000000), new Date(4000000)); + +// String time → DateNow() fallback +myVfs.utimesSync('/u.txt', 'now', 'now'); + +// null/undefined → fallthrough (returns the value as-is) +myVfs.utimesSync('/u.txt', null, undefined); + +// lutimes for symlinks +myVfs.lutimesSync('/lk', new Date(0), new Date(0)); diff --git a/test/parallel/test-vfs-virtual-file-handle.js b/test/parallel/test-vfs-virtual-file-handle.js new file mode 100644 index 00000000000000..21f93709ab65ea --- /dev/null +++ b/test/parallel/test-vfs-virtual-file-handle.js @@ -0,0 +1,88 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// Cover the VirtualFileHandle base class — all primitives must throw +// ERR_METHOD_NOT_IMPLEMENTED until a subclass overrides them. + +const common = require('../common'); +const assert = require('assert'); +const { VirtualFileHandle } = require('internal/vfs/file_handle'); + +const handle = new VirtualFileHandle('/x.txt', 'r', 0o600); +assert.strictEqual(handle.path, '/x.txt'); +assert.strictEqual(handle.flags, 'r'); +assert.strictEqual(handle.mode, 0o600); +assert.strictEqual(handle.position, 0); +assert.strictEqual(handle.closed, false); + +// Sync stubs throw ERR_METHOD_NOT_IMPLEMENTED +for (const m of ['readSync', 'writeSync', 'readFileSync', 'writeFileSync', + 'statSync', 'truncateSync']) { + assert.throws(() => handle[m](), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }, + `${m} should throw`); +} + +// Async stubs reject +for (const m of ['read', 'write', 'readFile', 'writeFile', 'stat', 'truncate']) { + assert.rejects(handle[m](), { code: 'ERR_METHOD_NOT_IMPLEMENTED' }, + `${m} should reject`).then(common.mustCall()); +} + +// readv/writev base stubs throw +assert.rejects(handle.readv([Buffer.alloc(1)], 0), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); +assert.rejects(handle.writev([Buffer.alloc(1)], 0), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); + +// appendFile uses write under the hood — same not-implemented +assert.rejects(handle.appendFile('x'), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); + +// readableWebStream / readLines / createReadStream / createWriteStream throw +assert.throws(() => handle.readableWebStream(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); +assert.throws(() => handle.readLines(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); +assert.throws(() => handle.createReadStream(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); +assert.throws(() => handle.createWriteStream(), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); + +// No-op metadata +(async () => { + await handle.chmod(); + await handle.chown(); + await handle.utimes(); + await handle.datasync(); + await handle.sync(); +})().then(common.mustCall()); + +// close() / closeSync() / dispose +{ + const h = new VirtualFileHandle('/y', 'r'); + h.closeSync(); + assert.strictEqual(h.closed, true); + + // Operations after close throw EBADF (via #checkClosed) before NOT_IMPL + assert.throws(() => h.readSync(), { code: 'EBADF' }); + assert.rejects(h.read(), { code: 'EBADF' }).then(common.mustCall()); +} + +// Close via async + Symbol.asyncDispose +(async () => { + const h = new VirtualFileHandle('/z', 'r'); + await h.close(); + assert.strictEqual(h.closed, true); + + const h2 = new VirtualFileHandle('/z2', 'r'); + await h2[Symbol.asyncDispose](); + assert.strictEqual(h2.closed, true); +})().then(common.mustCall()); + +// truncateSync default len = 0 path requires close-check too +{ + const h = new VirtualFileHandle('/a', 'r'); + h.closeSync(); + assert.throws(() => h.truncateSync(), { code: 'EBADF' }); + assert.rejects(h.truncate(), { code: 'EBADF' }).then(common.mustCall()); +} diff --git a/test/parallel/test-vfs-virtual-provider.js b/test/parallel/test-vfs-virtual-provider.js new file mode 100644 index 00000000000000..467f860bc8ce1d --- /dev/null +++ b/test/parallel/test-vfs-virtual-provider.js @@ -0,0 +1,109 @@ +// Flags: --experimental-vfs +'use strict'; + +// Exercise the VirtualProvider base class — its capability flags, +// readonly stubs, and the default implementations built on primitives. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// Bare base provider: every primitive throws ERR_METHOD_NOT_IMPLEMENTED. +{ + const p = new vfs.VirtualProvider(); + assert.strictEqual(p.readonly, false); + assert.strictEqual(p.supportsSymlinks, false); + assert.strictEqual(p.supportsWatch, false); + + for (const method of ['openSync', 'statSync', 'readdirSync', 'mkdirSync', + 'rmdirSync', 'unlinkSync', 'renameSync', + 'linkSync', 'readlinkSync', 'symlinkSync', + 'watch', 'watchAsync', 'watchFile', 'unwatchFile']) { + assert.throws(() => p[method]('/x', '/y'), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }, + `${method} must throw`); + } + + // Async primitives reject with the same error + for (const method of ['open', 'stat', 'readdir', 'mkdir', 'rmdir', 'unlink', + 'rename', 'link', 'readlink', 'symlink']) { + assert.rejects(p[method]('/x', '/y'), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }, + `${method} must reject`).then(common.mustCall()); + } + + // lstat/lstatSync default to stat — should propagate the not-impl error + assert.throws(() => p.lstatSync('/x'), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }); + assert.rejects(p.lstat('/x'), + { code: 'ERR_METHOD_NOT_IMPLEMENTED' }).then(common.mustCall()); + + // existsSync / exists default impl: stat throws → false + assert.strictEqual(p.existsSync('/x'), false); + p.exists('/x').then(common.mustCall((r) => { + assert.strictEqual(r, false); + })); +} + +// Read-only provider: write primitives throw EROFS instead of NOT_IMPL. +{ + const p = new class extends vfs.VirtualProvider { + get readonly() { return true; } + }(); + for (const method of ['mkdirSync', 'rmdirSync', 'unlinkSync', + 'renameSync', 'linkSync', 'symlinkSync']) { + assert.throws(() => p[method]('/x', '/y'), + { code: 'EROFS' }, + `${method} must throw EROFS`); + } + for (const method of ['mkdir', 'rmdir', 'unlink', 'rename', 'link', 'symlink']) { + assert.rejects(p[method]('/x', '/y'), + { code: 'EROFS' }).then(common.mustCall()); + } + + // copyFile / writeFile / appendFile reject with EROFS + assert.rejects(p.copyFile('/a', '/b'), + { code: 'EROFS' }).then(common.mustCall()); + assert.throws(() => p.copyFileSync('/a', '/b'), + { code: 'EROFS' }); + assert.rejects(p.writeFile('/a', 'x'), + { code: 'EROFS' }).then(common.mustCall()); + assert.throws(() => p.writeFileSync('/a', 'x'), + { code: 'EROFS' }); + assert.rejects(p.appendFile('/a', 'x'), + { code: 'EROFS' }).then(common.mustCall()); + assert.throws(() => p.appendFileSync('/a', 'x'), + { code: 'EROFS' }); +} + +// Default access / realpath / copyFile delegate to stat + read/write +{ + // Use MemoryProvider with the public API to verify delegation paths, + // since the base class needs working primitives. + const myVfs = vfs.create(); + myVfs.writeFileSync('/src.txt', 'hello'); + + // Realpath default: returns path as-is after stat — covered by myVfs.realpathSync + assert.strictEqual(myVfs.realpathSync('/src.txt'), '/src.txt'); + + // exists default impl + assert.strictEqual(myVfs.provider.existsSync('src.txt'), true); + assert.strictEqual(myVfs.provider.existsSync('missing.txt'), false); + + // copyFile via base class default path (MemoryProvider doesn't override) + myVfs.provider.copyFileSync('src.txt', 'dst.txt'); + assert.strictEqual(myVfs.readFileSync('/dst.txt', 'utf8'), 'hello'); + + // copyFile with COPYFILE_EXCL and existing dest must throw + const COPYFILE_EXCL = 1; + assert.throws(() => + myVfs.provider.copyFileSync('src.txt', 'dst.txt', COPYFILE_EXCL), + { code: 'EEXIST' }); + + // accessSync with mode=0 (existence-only) + myVfs.provider.accessSync('src.txt', 0); + + // accessSync R_OK on a readable file should pass + const R_OK = 4; + myVfs.provider.accessSync('src.txt', R_OK); +} diff --git a/test/parallel/test-vfs-watch-abort-signal.js b/test/parallel/test-vfs-watch-abort-signal.js new file mode 100644 index 00000000000000..4ceb9f78744567 --- /dev/null +++ b/test/parallel/test-vfs-watch-abort-signal.js @@ -0,0 +1,53 @@ +// Flags: --experimental-vfs +'use strict'; + +// AbortSignal handling for watch() and promises.watch(). + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +(async () => { + // Pre-aborted signal closes the watcher at construction + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'a'); + const watcher = myVfs.watch('/file.txt', { signal: AbortSignal.abort() }); + watcher.on('change', common.mustNotCall()); + setImmediate(() => myVfs.writeFileSync('/file.txt', 'b')); + } + + // Aborting after construction triggers close + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'a'); + const ac = new AbortController(); + const watcher = myVfs.watch('/file.txt', { signal: ac.signal }); + watcher.on('change', common.mustNotCall()); + ac.abort(); + setImmediate(() => myVfs.writeFileSync('/file.txt', 'b')); + } + + // promises.watch with pre-aborted signal resolves done immediately + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/p.txt', 'a'); + const iter = myVfs.promises.watch('/p.txt', { signal: AbortSignal.abort() }); + const r = await iter.next(); + assert.strictEqual(r.done, true); + } + + // promises.watch with mid-flight abort rejects pending next() with AbortError + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/p2.txt', 'a'); + const ac = new AbortController(); + const iter = myVfs.promises.watch('/p2.txt', { + signal: ac.signal, + interval: 1000, + }); + const pending = iter.next(); + queueMicrotask(() => ac.abort()); + await assert.rejects(pending, { name: 'AbortError' }); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-watch-directory.js b/test/parallel/test-vfs-watch-directory.js new file mode 100644 index 00000000000000..cc6e6ce2d3683d --- /dev/null +++ b/test/parallel/test-vfs-watch-directory.js @@ -0,0 +1,62 @@ +// Flags: --experimental-vfs +'use strict'; + +// Tests for VFS directory watching: +// - watch() on directories reports child changes +// - File creation / deletion events +// - Listing failures during a poll are swallowed + +const common = require('../common'); +const assert = require('assert'); +const { once } = require('events'); +const vfs = require('node:vfs'); + +(async () => { + // Modifying a child file emits a change event. + { + const myVfs = vfs.create(); + myVfs.mkdirSync('/parent', { recursive: true }); + myVfs.writeFileSync('/parent/file.txt', 'x'); + const watcher = myVfs.watch('/parent', { interval: 25 }); + const changed = once(watcher, 'change'); + myVfs.writeFileSync('/parent/file.txt', 'longer-content-changed'); + assert.deepStrictEqual(await changed, ['change', 'file.txt']); + watcher.close(); + } + + // Non-recursive directory watch: file creation + { + const myVfs = vfs.create(); + myVfs.mkdirSync('/d'); + myVfs.writeFileSync('/d/a.txt', 'x'); + const watcher = myVfs.watch('/d', { interval: 25 }); + const changed = once(watcher, 'change'); + myVfs.writeFileSync('/d/new.txt', 'x'); + assert.deepStrictEqual(await changed, ['rename', 'new.txt']); + watcher.close(); + } + + // Non-recursive directory watch: file deletion of a tracked child + { + const myVfs = vfs.create(); + myVfs.mkdirSync('/dd'); + myVfs.writeFileSync('/dd/keep.txt', 'a'); + myVfs.writeFileSync('/dd/goes.txt', 'b'); + const watcher = myVfs.watch('/dd', { interval: 25 }); + const evt = once(watcher, 'change'); + myVfs.unlinkSync('/dd/goes.txt'); + assert.deepStrictEqual(await evt, ['rename', 'goes.txt']); + watcher.close(); + } + + // The watched directory is removed mid-poll: readdirSync inside the + // poll throws and the watcher swallows the error. + { + const myVfs = vfs.create(); + myVfs.mkdirSync('/gone'); + myVfs.writeFileSync('/gone/f.txt', 'x'); + const watcher = myVfs.watch('/gone', { interval: 25 }); + myVfs.rmSync('/gone', { recursive: true }); + watcher.close(); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-watch-encoding.js b/test/parallel/test-vfs-watch-encoding.js new file mode 100644 index 00000000000000..2a5cc652cbca37 --- /dev/null +++ b/test/parallel/test-vfs-watch-encoding.js @@ -0,0 +1,21 @@ +// Flags: --experimental-vfs +'use strict'; + +// Buffer encoding for watch(): filename arrives as a Buffer. + +const common = require('../common'); +const assert = require('assert'); +const { once } = require('events'); +const vfs = require('node:vfs'); + +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/bf.txt', 'a'); + const watcher = myVfs.watch('/bf.txt', { interval: 25, encoding: 'buffer' }); + const changed = once(watcher, 'change'); + myVfs.writeFileSync('/bf.txt', 'longer-content-changed'); + const [eventType, filename] = await changed; + assert.strictEqual(eventType, 'change'); + assert.deepStrictEqual(filename, Buffer.from('bf.txt')); + watcher.close(); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-watch-promises.js b/test/parallel/test-vfs-watch-promises.js new file mode 100644 index 00000000000000..9de8ebd4022c39 --- /dev/null +++ b/test/parallel/test-vfs-watch-promises.js @@ -0,0 +1,84 @@ +// Flags: --experimental-vfs +'use strict'; + +// promises.watch() returns an async iterable. Cover its event queue, +// next/return/throw, and close-while-pending behaviour. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +(async () => { + // Basic for-await iteration receives a change event + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/file.txt', 'a'); + const iter = myVfs.promises.watch('/file.txt', { interval: 25 }); + queueMicrotask(() => myVfs.writeFileSync('/file.txt', 'longer-changed')); + for await (const evt of iter) { + assert.strictEqual(evt.eventType, 'change'); + break; + } + } + + // Events queued before next() drain via the next call + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/q.txt', 'a'); + const iter = myVfs.promises.watch('/q.txt', { interval: 25 }); + myVfs.writeFileSync('/q.txt', 'longer-changed'); + assert.partialDeepStrictEqual(await iter.next(), { + done: false, + value: { + eventType: 'change', + } + }); + assert.deepStrictEqual(await iter.return(), { + done: true, + value: undefined, + }); + } + + // A change while a next() is pending shifts the resolver + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/q2.txt', 'a'); + const iter = myVfs.promises.watch('/q2.txt', { interval: 25 }); + const pending = iter.next(); + myVfs.writeFileSync('/q2.txt', 'longer-changed'); + const r = await pending; + if (!r.done) assert.strictEqual(r.value.eventType, 'change'); + assert.partialDeepStrictEqual(await pending, { + done: false, + value: { + eventType: 'change', + } + }); + assert.deepStrictEqual(await iter.return(), { + done: true, + value: undefined, + }); + } + + // throw() closes the watcher and resolves with done:true + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/q3.txt', 'a'); + const iter = myVfs.promises.watch('/q3.txt', { interval: 25 }); + const r = await iter.throw(new Error('go away')); + assert.deepStrictEqual(r, { done: true, value: undefined }); + myVfs.writeFileSync('/q3.txt', 'b'); + assert.deepStrictEqual(await iter.next(), { done: true, value: undefined }); + } + + // Close while a resolver is pending — drains via the 'close' handler + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/q4.txt', 'a'); + const iter = myVfs.promises.watch('/q4.txt', { interval: 1000 }); + const pending = iter.next(); + queueMicrotask(() => iter.return()); + const r = await pending; + assert.strictEqual(r.done, true); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-watch-recursive.js b/test/parallel/test-vfs-watch-recursive.js new file mode 100644 index 00000000000000..9f678db3069d98 --- /dev/null +++ b/test/parallel/test-vfs-watch-recursive.js @@ -0,0 +1,36 @@ +// Flags: --experimental-vfs +'use strict'; + +// Recursive directory watching: descendant changes trigger events. + +const common = require('../common'); +const { once } = require('events'); +const vfs = require('node:vfs'); + +(async () => { + // Recursive watch detects creation in a subdirectory + { + const myVfs = vfs.create(); + myVfs.mkdirSync('/d/sub', { recursive: true }); + myVfs.writeFileSync('/d/sub/a.txt', 'x'); + const watcher = myVfs.watch('/d', { interval: 25, recursive: true }); + watcher.on('change', common.mustCall(1)); // Making sure the event listener is called only once + const changedPromise = once(watcher, 'change'); + myVfs.writeFileSync('/d/sub/b.txt', 'new'); + await changedPromise; + watcher.close(); + } + + // Recursive watch detects modification of a pre-existing descendant + { + const myVfs = vfs.create(); + myVfs.mkdirSync('/r/sub', { recursive: true }); + myVfs.writeFileSync('/r/sub/file.txt', 'x'); + const watcher = myVfs.watch('/r', { interval: 25, recursive: true }); + watcher.on('change', common.mustCall(1)); // Making sure the event listener is called only once + const changedPromise = once(watcher, 'change'); + myVfs.writeFileSync('/r/sub/file.txt', 'longer-content-changed'); + await changedPromise; + watcher.close(); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-watch.js b/test/parallel/test-vfs-watch.js new file mode 100644 index 00000000000000..ed5d36d1d0687d --- /dev/null +++ b/test/parallel/test-vfs-watch.js @@ -0,0 +1,75 @@ +// Flags: --experimental-vfs +'use strict'; + +// Tests for VFS watch() on a single file: change detection, listener +// registration, ref/unref, and the watch-then-create flow. + +const common = require('../common'); +const assert = require('assert'); +const { once } = require('events'); +const vfs = require('node:vfs'); + +(async () => { + // Listener as 2nd argument + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/lf.txt', 'a'); + const w = myVfs.watch('/lf.txt', common.mustNotCall()); + w.close(); + } + + // Listener add/remove + ref/unref smoke + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/r.txt', 'a'); + const w = myVfs.watch('/r.txt'); + const fn = common.mustNotCall(); + w.on('change', fn); + w.removeListener('change', fn); + w.on('change', fn); + w.removeAllListeners('change'); + w.ref(); + w.unref(); + w.close(); + } + + // Double close is a no-op + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/x.txt', 'a'); + const watcher = myVfs.watch('/x.txt'); + watcher.close(); + watcher.close(); + } + + // persistent: false reaches the unref branch + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/p.txt', 'a'); + const watcher = myVfs.watch('/p.txt', { persistent: false }); + watcher.close(); + } + + // Watching a missing path then creating it + { + const myVfs = vfs.create(); + const watcher = myVfs.watch('/late.txt', { interval: 25 }); + const changed = once(watcher, 'change'); + myVfs.writeFileSync('/late.txt', 'now'); + await changed; + watcher.close(); + } + + // Modifying the watched file emits change with the basename as filename + { + const myVfs = vfs.create(); + myVfs.writeFileSync('/single.txt', 'a'); + const watcher = myVfs.watch('/single.txt', { interval: 25 }); + const evt = once(watcher, 'change'); + myVfs.writeFileSync('/single.txt', 'longer-content-changed'); + const [eventType, filename] = await evt; + assert.strictEqual(eventType, 'change'); + assert.strictEqual(filename, 'single.txt'); + watcher.close(); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-watchfile.js b/test/parallel/test-vfs-watchfile.js new file mode 100644 index 00000000000000..6a6e08e5f92491 --- /dev/null +++ b/test/parallel/test-vfs-watchfile.js @@ -0,0 +1,103 @@ +// Flags: --experimental-vfs +'use strict'; + +// Tests for VFS watchFile/unwatchFile. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// unwatchFile(path) without a specific listener cleans up the timer. +// If the timer leaks, the process would hang. +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'x'); + myVfs.watchFile('/a.txt', { interval: 50, persistent: false }, common.mustNotCall()); + myVfs.unwatchFile('/a.txt'); +} + +// Default options: no interval option provided +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/dw.txt', 'a'); + const listener = common.mustNotCall(); + myVfs.watchFile('/dw.txt', listener); + myVfs.unwatchFile('/dw.txt', listener); + myVfs.writeFileSync('/dw.txt', 'b'); +} + +// Double unwatch is a no-op +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/sw.txt', 'a'); + const listener = common.mustNotCall(); + myVfs.watchFile('/sw.txt', { interval: 25 }, listener); + myVfs.unwatchFile('/sw.txt', listener); + myVfs.unwatchFile('/sw.txt', listener); +} + +// Zero stats for a missing file: prev.isFile() is false and prev.mode is 0 +{ + const myVfs = vfs.create(); + function listener(curr, prev) { + assert.strictEqual(prev.isFile(), false); + assert.strictEqual(prev.mode, 0); + myVfs.unwatchFile('/missing.txt', listener); + } + myVfs.watchFile('/missing.txt', { interval: 50, persistent: false }, common.mustCall(listener)); + setImmediate(() => myVfs.writeFileSync('/missing.txt', 'x')); +} + +// Content change fires the listener with curr/prev stats +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/sw.txt', 'a'); + const listener = common.mustCallAtLeast((curr, prev) => { + assert.strictEqual(typeof curr.size, 'number'); + assert.strictEqual(typeof prev.size, 'number'); + }, 1); + const fired = new Promise((resolve) => { + myVfs.watchFile('/sw.txt', { interval: 25 }, (curr, prev) => { + listener(curr, prev); + resolve(); + }); + }); + myVfs.writeFileSync('/sw.txt', 'longer-content-changed'); + await fired; + myVfs.unwatchFile('/sw.txt'); +})().then(common.mustCall()); + +// bigint: true returns BigInt fields in both curr and prev stats, plus +// the bigint createZeroStats path when watching an initially-missing file. +(async () => { + const myVfs = vfs.create(); + myVfs.writeFileSync('/bi.txt', 'a'); + const listener = common.mustCallAtLeast((curr, prev) => { + assert.strictEqual(typeof curr.size, 'bigint'); + assert.strictEqual(typeof prev.size, 'bigint'); + }, 1); + const fired = new Promise((resolve) => { + myVfs.watchFile('/bi.txt', { interval: 25, bigint: true }, (curr, prev) => { + listener(curr, prev); + resolve(); + }); + }); + myVfs.writeFileSync('/bi.txt', 'longer-content-changed'); + await fired; + myVfs.unwatchFile('/bi.txt'); +})().then(common.mustCall()); + +// bigint: true on a missing file emits BigInt prev.size = 0n +{ + const myVfs = vfs.create(); + const watcher = myVfs.watchFile('/missing-b.txt', + { interval: 50, persistent: false, bigint: true }, + common.mustCallAtLeast((curr, prev) => { + assert.strictEqual(typeof curr.size, 'bigint'); + assert.strictEqual(typeof prev.size, 'bigint'); + myVfs.unwatchFile('/missing-b.txt'); + }, 1)); + setTimeout(() => myVfs.writeFileSync('/missing-b.txt', 'now-here'), 80); + setTimeout(() => myVfs.unwatchFile('/missing-b.txt'), 500); + if (watcher?.unref) watcher.unref(); +} diff --git a/test/parallel/test-vfs-write-options.js b/test/parallel/test-vfs-write-options.js new file mode 100644 index 00000000000000..476d00801bdd06 --- /dev/null +++ b/test/parallel/test-vfs-write-options.js @@ -0,0 +1,33 @@ +// Flags: --experimental-vfs +'use strict'; + +// writeFile / appendFile accept explicit { flag, mode } options. + +const common = require('../common'); +const assert = require('assert'); +const vfs = require('node:vfs'); + +// writeFileSync / promises.writeFile with explicit flag and mode +{ + const myVfs = vfs.create(); + myVfs.writeFileSync('/a.txt', 'hello', { flag: 'w', mode: 0o600 }); + assert.strictEqual(myVfs.readFileSync('/a.txt', 'utf8'), 'hello'); + + myVfs.promises.writeFile('/b.txt', 'world', { flag: 'w', mode: 0o600 }) + .then(common.mustCall(() => { + assert.strictEqual(myVfs.readFileSync('/b.txt', 'utf8'), 'world'); + })); +} + +// appendFileSync / promises.appendFile with explicit flag and mode +{ + const myVfs = vfs.create(); + myVfs.appendFileSync('/a.txt', 'first', { flag: 'a', mode: 0o600 }); + myVfs.appendFileSync('/a.txt', '-second', { flag: 'a' }); + assert.strictEqual(myVfs.readFileSync('/a.txt', 'utf8'), 'first-second'); + + myVfs.promises.appendFile('/b.txt', 'go', { flag: 'a', mode: 0o600 }) + .then(common.mustCall(() => { + assert.strictEqual(myVfs.readFileSync('/b.txt', 'utf8'), 'go'); + })); +} From 985b6087301a61215bf13e114dc720545f5092c0 Mon Sep 17 00:00:00 2001 From: Daijiro Wachi Date: Sun, 24 May 2026 05:10:43 +0900 Subject: [PATCH 07/89] doc: fix double space in modules.md Signed-off-by: Daijiro Wachi PR-URL: https://github.com/nodejs/node/pull/63512 Reviewed-By: Antoine du Hamel Reviewed-By: Marco Ippolito Reviewed-By: Luigi Pinca --- doc/api/modules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/modules.md b/doc/api/modules.md index 5f920ec324bc21..5b001026d02f8b 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -286,7 +286,7 @@ console.log(distance); // undefined ``` Notice in the example above, when the `module.exports` export name is used, named exports -will be lost to CommonJS consumers. To allow CommonJS consumers to continue accessing +will be lost to CommonJS consumers. To allow CommonJS consumers to continue accessing named exports, the module can make sure that the default export is an object with the named exports attached to it as properties. For example with the example above, `distance` can be attached to the default export, the `Point` class, as a static method. From 837910d298b0e1bd0c93491d51c0022f13e830f1 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sat, 23 May 2026 14:43:57 -0700 Subject: [PATCH 08/89] stream: flush each fused stateless transform Ensure consecutive stateless stream/iter transforms each receive a final null flush after upstream flush output has been processed. Fixes: https://github.com/nodejs/node/issues/63467 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/63468 Fixes: https://github.com/nodejs/node/issues/63467 Reviewed-By: Matteo Collina Reviewed-By: James M Snell --- lib/internal/streams/iter/pull.js | 77 ++++++++++++-------- test/parallel/test-stream-iter-pull-async.js | 14 ++++ test/parallel/test-stream-iter-pull-sync.js | 15 ++++ 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/lib/internal/streams/iter/pull.js b/lib/internal/streams/iter/pull.js index 3ff88b251d182a..95871e99037d58 100644 --- a/lib/internal/streams/iter/pull.js +++ b/lib/internal/streams/iter/pull.js @@ -273,6 +273,17 @@ function* processTransformResultSync(result) { result); } +/** + * Append normalized transform result batches to an array (sync). + * @param {Array} target + * @param {*} result + */ +function appendTransformResultSync(target, result) { + for (const batch of processTransformResultSync(result)) { + ArrayPrototypePush(target, batch); + } +} + /** * Process transform result (async). * @yields {Uint8Array[]} @@ -356,6 +367,18 @@ async function* processTransformResultAsync(result) { result); } +/** + * Append normalized transform result batches to an array (async). + * @param {Array} target + * @param {*} result + * @returns {Promise} + */ +async function appendTransformResultAsync(target, result) { + for await (const batch of processTransformResultAsync(result)) { + ArrayPrototypePush(target, batch); + } +} + // ============================================================================= // Sync Pipeline Implementation // ============================================================================= @@ -398,18 +421,19 @@ function* applyFusedStatelessSyncTransforms(source, run) { yield* processTransformResultSync(current); } } - // Flush - let current = null; + // Flush each transform after all upstream data, including data emitted by + // earlier flushes, has been processed by that transform. + let pending = []; for (let i = 0; i < run.length; i++) { - const result = run[i](current); - if (result === null) { - current = null; - continue; + const next = []; + for (let j = 0; j < pending.length; j++) { + appendTransformResultSync(next, run[i](pending[j])); } - current = result; + appendTransformResultSync(next, run[i](null)); + pending = next; } - if (current != null) { - yield* processTransformResultSync(current); + for (let i = 0; i < pending.length; i++) { + yield pending[i]; } } @@ -522,30 +546,23 @@ async function* applyFusedStatelessAsyncTransforms(source, run, signal) { yield* processTransformResultAsync(current); } } - // Flush: send null through each transform in order - let current = null; + // Flush each transform after all upstream data, including data emitted by + // earlier flushes, has been processed by that transform. + let pending = []; for (let i = 0; i < run.length; i++) { - const result = run[i](current, { __proto__: null, signal }); - if (result === null) { - current = null; - continue; - } - if (isPromise(result)) { - current = await result; - } else { - current = result; + const next = []; + for (let j = 0; j < pending.length; j++) { + await appendTransformResultAsync( + next, + run[i](pending[j], { __proto__: null, signal })); } + await appendTransformResultAsync( + next, + run[i](null, { __proto__: null, signal })); + pending = next; } - if (current !== null) { - if (isUint8ArrayBatch(current)) { - if (current.length > 0) yield current; - } else if (isUint8Array(current)) { - yield [current]; - } else if (typeof current === 'string') { - yield [toUint8Array(current)]; - } else { - yield* processTransformResultAsync(current); - } + for (let i = 0; i < pending.length; i++) { + yield pending[i]; } } diff --git a/test/parallel/test-stream-iter-pull-async.js b/test/parallel/test-stream-iter-pull-async.js index 157cc5e265ea34..1dbe7c98878ccc 100644 --- a/test/parallel/test-stream-iter-pull-async.js +++ b/test/parallel/test-stream-iter-pull-async.js @@ -233,6 +233,19 @@ async function testPullStatelessTransformFlush() { assert.strictEqual(data, 'data-TRAILER'); } +// Consecutive stateless transforms each receive a final flush signal after +// upstream flush output has been processed. +async function testPullConsecutiveStatelessTransformFlush() { + const enc = new TextEncoder(); + const addAOnFlush = (chunks) => (chunks === null ? + [enc.encode('-A')] : chunks); + const addBOnFlush = (chunks) => (chunks === null ? + [enc.encode('-B')] : chunks); + + const data = await text(pull(from('x'), addAOnFlush, addBOnFlush)); + assert.strictEqual(data, 'x-A-B'); +} + // Stateless transform flush error propagates async function testPullStatelessTransformFlushError() { const badFlush = (chunks) => { @@ -357,6 +370,7 @@ async function testTransformOptionsNotShared() { testPullStatelessTransformError(), testPullStatefulTransformError(), testPullStatelessTransformFlush(), + testPullConsecutiveStatelessTransformFlush(), testPullStatelessTransformFlushError(), testPullWithSyncSource(), testPullStringSource(), diff --git a/test/parallel/test-stream-iter-pull-sync.js b/test/parallel/test-stream-iter-pull-sync.js index 35679ac102d512..c47a6b3f92330d 100644 --- a/test/parallel/test-stream-iter-pull-sync.js +++ b/test/parallel/test-stream-iter-pull-sync.js @@ -127,6 +127,20 @@ function testPullSyncStatelessTransformFlush() { assert.strictEqual(data, 'data-TRAILER'); } +// Consecutive stateless transforms each receive a final flush signal after +// upstream flush output has been processed. +function testPullSyncConsecutiveStatelessTransformFlush() { + const enc = new TextEncoder(); + const addAOnFlush = (chunks) => (chunks === null ? + [enc.encode('-A')] : chunks); + const addBOnFlush = (chunks) => (chunks === null ? + [enc.encode('-B')] : chunks); + + const data = new TextDecoder().decode(bytesSync( + pullSync(fromSync('x'), addAOnFlush, addBOnFlush))); + assert.strictEqual(data, 'x-A-B'); +} + // Stateless transform flush error propagates function testPullSyncStatelessTransformFlushError() { const badFlush = (chunks) => { @@ -173,6 +187,7 @@ Promise.all([ testPullSyncStatelessTransformError(), testPullSyncStatefulTransformError(), testPullSyncStatelessTransformFlush(), + testPullSyncConsecutiveStatelessTransformFlush(), testPullSyncStatelessTransformFlushError(), testPullSyncInvalidTransform(), ]).then(common.mustCall()); From 74ccf384a9bd1cd1aba5e95fd829dca9356a2083 Mon Sep 17 00:00:00 2001 From: atlowChemi Date: Tue, 19 May 2026 14:34:34 +0300 Subject: [PATCH 09/89] test_runner: fix --test-rerun-failures swallowing failures on retry Three independent bugs interacted to let a real failure-on-retry be reported as a pass: 1. The runner's disambiguator stored the counter against the suffixed identifier (after mutation) instead of the base key, so the counter never advanced past 1 and every 3rd+ same-loc registration collided on :(1). 2. The reporter had the same off-by-one when writing the state file. 3. The reporter only bumped its counter on `test:pass`, so any failing test at a shared source location desynchronised the writer and runner counters - on retry, the surviving failing sibling would inherit a slot that in the previous attempt belonged to a different (passing) sibling. Node matched by that slot, replaced `this.fn` with a synthetic noop replay, and reported the failure as a pass. Track the base identifier separately in the runner, bump the counter against the base key in both the runner and the reporter, and bump the reporter's counter on `test:fail` in addition to `test:pass`. Fixes: https://github.com/nodejs/node/issues/63424 Signed-off-by: atlowChemi PR-URL: https://github.com/nodejs/node/pull/63431 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Moshe Atlow --- lib/internal/test_runner/reporter/rerun.js | 31 ++++---- lib/internal/test_runner/test.js | 9 ++- .../rerun-shared-helper-swallows-failure.mjs | 51 ++++++++++++ .../test-runner-test-rerun-failures.js | 77 +++++++++++++++++++ 4 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 test/fixtures/test-runner/rerun-shared-helper-swallows-failure.mjs diff --git a/lib/internal/test_runner/reporter/rerun.js b/lib/internal/test_runner/reporter/rerun.js index ecdd53243e887a..3f9ae102fea33e 100644 --- a/lib/internal/test_runner/reporter/rerun.js +++ b/lib/internal/test_runner/reporter/rerun.js @@ -48,22 +48,25 @@ function reportReruns(previousRuns, globalOptions) { } - if (type === 'test:pass') { - let identifier = getTestId(data); - if (disambiguator[identifier] !== undefined) { - identifier += `:(${disambiguator[identifier]})`; - disambiguator[identifier] += 1; + if (type === 'test:pass' || type === 'test:fail') { + const baseIdentifier = getTestId(data); + let identifier = baseIdentifier; + if (disambiguator[baseIdentifier] !== undefined) { + identifier += `:(${disambiguator[baseIdentifier]})`; + disambiguator[baseIdentifier] += 1; } else { - disambiguator[identifier] = 1; + disambiguator[baseIdentifier] = 1; + } + if (type === 'test:pass') { + const children = ArrayPrototypeMap(currentTest.children, (child) => child.data); + obj[identifier] = { + __proto__: null, + name: data.name, + children, + passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt, + duration_ms: data.details.duration_ms, + }; } - const children = ArrayPrototypeMap(currentTest.children, (child) => child.data); - obj[identifier] = { - __proto__: null, - 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 7210ff38f2b518..006172f5b82b72 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -793,13 +793,14 @@ class Test extends AsyncResource { } if (this.loc != null && this.root.harness.previousRuns != null) { - let testIdentifier = `${relative(this.config.cwd, this.loc.file)}:${this.loc.line}:${this.loc.column}`; - const disambiguator = this.root.testDisambiguator.get(testIdentifier); + const baseIdentifier = `${relative(this.config.cwd, this.loc.file)}:${this.loc.line}:${this.loc.column}`; + let testIdentifier = baseIdentifier; + const disambiguator = this.root.testDisambiguator.get(baseIdentifier); if (disambiguator !== undefined) { testIdentifier += `:(${disambiguator})`; - this.root.testDisambiguator.set(testIdentifier, disambiguator + 1); + this.root.testDisambiguator.set(baseIdentifier, disambiguator + 1); } else { - this.root.testDisambiguator.set(testIdentifier, 1); + this.root.testDisambiguator.set(baseIdentifier, 1); } this.attempt = this.root.harness.previousRuns.length; const previousAttempt = this.root.harness.previousRuns[this.attempt - 1]?.[testIdentifier]; diff --git a/test/fixtures/test-runner/rerun-shared-helper-swallows-failure.mjs b/test/fixtures/test-runner/rerun-shared-helper-swallows-failure.mjs new file mode 100644 index 00000000000000..22eaaa321c01b5 --- /dev/null +++ b/test/fixtures/test-runner/rerun-shared-helper-swallows-failure.mjs @@ -0,0 +1,51 @@ +// Regression coverage for https://github.com/nodejs/node/issues/63424: +// `--test-rerun-failures` could mark a failing test as passing on retry +// without executing its body. +// +// The runtime disambiguator at lib/internal/test_runner/test.js keys tests +// by `file:line:column`. A `t.test()` registered inside a factory function +// gets the same source location regardless of which parent invoked the +// factory. Three independent bugs interacted to make the failure-on-retry +// vanish: +// 1. Runner counter was set against the suffixed key, so it never +// advanced past 1 - every 3rd+ same-loc registration collided on :(1). +// 2. Reporter had the same off-by-one against the suffixed key. +// 3. Reporter only bumped its counter on test:pass, so failing tests at +// a shared location desynchronised the writer and runner counters. +// On retry, the failing sibling could inherit a counter slot that, in +// attempt 0, belonged to a different (passing) sibling. Node matched by +// that slot, replaced `this.fn` with a synthetic noop replay, and reported +// the failure as a pass. + +import { describe, it } from 'node:test'; +import { strict as assert } from 'node:assert'; + +function makeSuite(shouldPass, label) { + return async (t) => { + await t.test('inner', async () => { + if (!shouldPass) assert.fail(`${label} should fail`); + }); + }; +} + +// Four siblings with the failure in the middle, placed first so that the +// passing sibling at global position 2 (E) ends up recorded at base:(1). +// With the runner-side off-by-one (bug 1), the buggy counter on retry would +// alias F's id onto base:(1), match F against E's recorded "passed" entry, +// replace F's assert.fail with a synthetic noop, and swallow F. +describe('parents (middle failure)', { concurrency: false }, () => { + it('D passes', makeSuite(true, 'D')); + it('E passes', makeSuite(true, 'E')); + it('F fails', makeSuite(false, 'F')); // the only real failure in this group + it('G passes', makeSuite(true, 'G')); +}); + +// Three-sibling case from the issue, kept verbatim to exercise the writer +// bugs (off-by-one storing the counter against the suffixed key; reporter +// ignoring test:fail when bumping the counter). Either bug shifts C's +// recorded slot from base:(6) to a position B would inherit on retry. +describe('parents', { concurrency: false }, () => { + it('A passes', makeSuite(true, 'A')); + it('B fails', makeSuite(false, 'B')); // the only real failure in this group + it('C passes', makeSuite(true, 'C')); +}); diff --git a/test/parallel/test-runner-test-rerun-failures.js b/test/parallel/test-runner-test-rerun-failures.js index a4e8d96410346d..0af3722fad706e 100644 --- a/test/parallel/test-runner-test-rerun-failures.js +++ b/test/parallel/test-runner-test-rerun-failures.js @@ -5,6 +5,7 @@ const tmpdir = require('../common/tmpdir'); const fixtures = require('../common/fixtures'); const assert = require('node:assert'); const { rm, readFile } = require('node:fs/promises'); +const { relative } = require('node:path'); const { setTimeout } = require('node:timers/promises'); const { test, beforeEach, afterEach, run } = require('node:test'); @@ -155,6 +156,82 @@ test('test should pass on third rerun with `--test`', async () => { assert.deepStrictEqual(await getStateFile(), expectedStateFile); }); +test('failing test is not swallowed when siblings share its source location', async () => { + // Regression coverage for https://github.com/nodejs/node/issues/63424. + // + // The fixture runs the 4-sibling group (D, E pass; F fails; G passes) + // before the 3-sibling group from the issue (A pass, B fails, C pass) so + // that the passing sibling at global position 2 (E) ends up recorded at + // base:(1). With the runner-side off-by-one reintroduced, the buggy + // counter on retry collides every sibling after the first onto base:(1) + // and matches the failing F (and B) against E's "passed" entry, replacing + // their assert.fail with a synthetic noop and reporting exit 0. + // + // The state-file shape additionally pins down the writer-side bugs: the + // writer off-by-one or its prior pass-only counting both shift the + // recorded slots away from the expected base:(N) layout. + const fixturePath = fixtures.path('test-runner', 'rerun-shared-helper-swallows-failure.mjs'); + const args = ['--test-rerun-failures', stateFile, fixturePath]; + + // getStateFile() normalises backslashes to '/'; match that on Windows. + const fixtureKey = relative(process.cwd(), fixturePath).replaceAll('\\', '/'); + const innerLoc = `${fixtureKey}:25:13`; + const passingInner = { passed_on_attempt: 0, name: 'inner' }; + const expectedInnerSlots = { + [innerLoc]: passingInner, // D + [`${innerLoc}:(1)`]: passingInner, // E - fills the slot a buggy runner aliases F/B onto + [`${innerLoc}:(3)`]: passingInner, // G - :(2) reserved for F's failure + [`${innerLoc}:(4)`]: passingInner, // A + [`${innerLoc}:(6)`]: passingInner, // C - :(5) reserved for B's failure + }; + const passingParents = { + [`${fixtureKey}:37:3`]: 'D passes', + [`${fixtureKey}:38:3`]: 'E passes', + [`${fixtureKey}:40:3`]: 'G passes', + [`${fixtureKey}:48:3`]: 'A passes', + [`${fixtureKey}:50:3`]: 'C passes', + }; + const failingParentSlots = [ + `${fixtureKey}:39:3`, // F fails + `${fixtureKey}:49:3`, // B fails + ]; + const failingInnerSlots = [`${innerLoc}:(2)`, `${innerLoc}:(5)`]; + + function assertAttempt(state, attemptIndex) { + for (const [key, expected] of Object.entries(expectedInnerSlots)) { + assert.deepStrictEqual(state[attemptIndex][key], expected, `attempt ${attemptIndex} missing or wrong entry for ${key}`); + } + for (const [key, name] of Object.entries(passingParents)) { + assert.strictEqual(state[attemptIndex][key]?.name, name, `attempt ${attemptIndex} missing parent ${name} at ${key}`); + assert.strictEqual(state[attemptIndex][key]?.passed_on_attempt, 0); + } + for (const key of failingInnerSlots) { + assert.strictEqual(state[attemptIndex][key], undefined, `attempt ${attemptIndex} should not record failing inner at ${key}`); + } + for (const key of failingParentSlots) { + assert.strictEqual(state[attemptIndex][key], undefined, `attempt ${attemptIndex} should not record failing parent at ${key}`); + } + } + + // Attempt 0: F and B fail. + let { code, signal } = await common.spawnPromisified(process.execPath, args); + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + let state = await getStateFile(); + assert.strictEqual(state.length, 1); + assertAttempt(state, 0); + + // Attempt 1: F and B must STILL fail. Before the fix this run reported + // exit 0 because the failing tests were matched to passing siblings' + // previous-attempt slots and replaced with synthetic noops. + ({ code, signal } = await common.spawnPromisified(process.execPath, args)); + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + state = await getStateFile(); + assert.strictEqual(state.length, 2); + assertAttempt(state, 1); +}); + 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]; From df09b2a48c2d7ec805edb24ddc71d444509a1fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Sun, 24 May 2026 13:39:53 +0100 Subject: [PATCH 10/89] ffi: remove function signature property aliases Signed-off-by: Renegade334 PR-URL: https://github.com/nodejs/node/pull/63482 Refs: https://github.com/nodejs/node/pull/62072/changes#r3067834658 Reviewed-By: Anna Henningsen Reviewed-By: Paolo Insogna --- doc/api/ffi.md | 39 +++--- lib/internal/ffi-shared-buffer.js | 120 +++++++----------- src/env_properties.h | 8 +- src/ffi/types.cc | 48 +------ src/node_ffi.cc | 26 ++-- test/ffi/ffi-test-common.js | 94 +++++++------- test/ffi/test-ffi-calls.js | 28 ++--- test/ffi/test-ffi-dynamic-library.js | 50 +++----- test/ffi/test-ffi-shared-buffer.js | 180 +++++++++++++-------------- test/ffi/test-ffi-weakref-calls.js | 4 +- 10 files changed, 254 insertions(+), 343 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 37ccf2a719bc1a..01abdc0b6bbdb1 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -124,18 +124,18 @@ such as `0` and `1`; JavaScript `true` and `false` are not accepted. Functions and callbacks are described with signature objects. -Supported fields: +Signature objects may contain the following properties, both of which are +optional: -* `result`, `return`, or `returns` for the return type. -* `parameters` or `arguments` for the parameter type list. +* `return` {string} A [type name][type names] specifying the return type of the + function or callback. **Default:** `'void'`. +* `arguments` {string\[]} An array of [type names][] specifying the argument + type list of the function or callback. **Default:** `[]`. -Only one return-type field and one parameter-list field may be present in a -single signature object. - -```cjs +```js const signature = { - result: 'i32', - parameters: ['i32', 'i32'], + return: 'i32', + arguments: ['i32', 'i32'], }; ``` @@ -193,7 +193,7 @@ import { dlopen } from 'node:ffi'; { using handle = dlopen('./mylib.so', { - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, }); console.log(handle.functions.add_i32(20, 22)); } // handle.lib.close() is invoked automatically here. @@ -203,8 +203,8 @@ import { dlopen } from 'node:ffi'; import { dlopen } from 'node:ffi'; const { lib, functions } = dlopen('./mylib.so', { - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, - string_length: { parameters: ['pointer'], result: 'u64' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, + string_length: { arguments: ['pointer'], return: 'u64' }, }); console.log(functions.add_i32(20, 22)); @@ -214,8 +214,8 @@ console.log(functions.add_i32(20, 22)); const { dlopen } = require('node:ffi'); const { lib, functions } = dlopen('./mylib.so', { - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, - string_length: { parameters: ['pointer'], result: 'u64' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, + string_length: { arguments: ['pointer'], return: 'u64' }, }); console.log(functions.add_i32(20, 22)); @@ -356,8 +356,8 @@ const { DynamicLibrary } = require('node:ffi'); const lib = new DynamicLibrary('./mylib.so'); const add = lib.getFunction('add_i32', { - parameters: ['i32', 'i32'], - result: 'i32', + arguments: ['i32', 'i32'], + return: 'i32', }); console.log(add(20, 22)); @@ -407,7 +407,7 @@ const { DynamicLibrary } = require('node:ffi'); const lib = new DynamicLibrary('./mylib.so'); const callback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, (value) => value * 2, ); ``` @@ -417,7 +417,7 @@ Callbacks are subject to the following restrictions: * They must be invoked on the same system thread where they were created. * They must not throw exceptions. * They must not return promises. -* They must return a value compatible with the declared result type. +* They must return a value compatible with the declared return type. * They must not call `library.close()` on their owning library while running. * They must not unregister themselves while running. @@ -465,7 +465,7 @@ JavaScript `number` values that match the declared type. For 64-bit integer types (`i64` and `u64`), pass JavaScript `bigint` values. -For pointer-like parameters: +For pointer-like arguments: * `null` and `undefined` are passed as null pointers. * `string` values are copied to temporary NUL-terminated UTF-8 strings for the @@ -727,3 +727,4 @@ and keep callback and pointer lifetimes explicit on the native side. [`--allow-ffi`]: cli.md#--allow-ffi [`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy [`using`]: https://tc39.es/proposal-explicit-resource-management/#sec-using-declarations +[type names]: #type-names diff --git a/lib/internal/ffi-shared-buffer.js b/lib/internal/ffi-shared-buffer.js index bce51fd79959dd..c5a769c394f11a 100644 --- a/lib/internal/ffi-shared-buffer.js +++ b/lib/internal/ffi-shared-buffer.js @@ -31,18 +31,14 @@ const { TypeError, } = primordials; -const { - codes: { - ERR_INTERNAL_ASSERTION, - }, -} = require('internal/errors'); +const assert = require('internal/assert'); const { DynamicLibrary, charIsSigned, + kSbArguments, kSbInvokeSlow, - kSbParams, - kSbResult, + kSbReturn, kSbSharedBuffer, uintptrMax, } = internalBinding('ffi'); @@ -159,11 +155,9 @@ function writeNumericArg(view, info, offset, arg, index) { return; } - /* c8 ignore start */ // Unreachable: caller filters out non-numeric kinds. - throw new ERR_INTERNAL_ASSERTION( - `FFI: writeNumericArg reached with unexpected kind="${kind}"`); - /* c8 ignore stop */ + /* c8 ignore next */ + assert.fail(`FFI: writeNumericArg reached with unexpected kind="${kind}"`); } // Returns true on fast-path success, false when the caller must fall back @@ -208,51 +202,46 @@ function inheritMetadata(wrapper, rawFn, nargs) { // arguments out of it into invocation-local storage before `ffi_call` and // reads the return value back only after, so nested/reentrant calls into // the same function are safe. -function wrapWithSharedBuffer(rawFn, parameters, resultType) { - if (rawFn === undefined || rawFn === null) return rawFn; +function wrapWithSharedBuffer(rawFn, signature) { + if (rawFn == null) return rawFn; const buffer = rawFn[kSbSharedBuffer]; if (buffer === undefined) return rawFn; // Callers without explicit signature info (the `functions` accessor - // patch below) rely on the `kSbParams` / `kSbResult` metadata attached + // patch below) rely on the `kSbArguments` / `kSbReturn` metadata attached // by the native `CreateFunction`. - if (parameters === undefined) parameters = rawFn[kSbParams]; - if (resultType === undefined) resultType = rawFn[kSbResult]; - // `CreateFunction` always attaches these for SB-eligible functions. - // Missing here means the native side and this wrapper are out of sync. - /* c8 ignore start */ - if (parameters === undefined || resultType === undefined) { - throw new ERR_INTERNAL_ASSERTION( - 'FFI: shared-buffer raw function is missing kSbParams or kSbResult'); + let argumentTypes, returnType; + if (signature === undefined) { + argumentTypes = rawFn[kSbArguments]; + returnType = rawFn[kSbReturn]; + + // `CreateFunction` always attaches these for SB-eligible functions. + // Missing here means the native side and this wrapper are out of sync. + assert(argumentTypes !== undefined && returnType !== undefined, + 'FFI: shared-buffer raw function is missing kSbArguments or kSbReturn'); + } else { + argumentTypes = signature.arguments ?? []; + returnType = signature.return ?? 'void'; } - /* c8 ignore stop */ const slowInvoke = rawFn[kSbInvokeSlow]; const view = new DataView(buffer); let retGetter = null; - if (resultType !== 'void') { - const retInfo = sbTypeInfo[resultType]; - /* c8 ignore start */ - if (retInfo === undefined) { - throw new ERR_INTERNAL_ASSERTION( - `FFI: shared-buffer type table missing entry for result type "${resultType}"`); - } - /* c8 ignore stop */ + if (returnType !== 'void') { + const retInfo = sbTypeInfo[returnType]; + assert(retInfo !== undefined, + `FFI: shared-buffer type table missing entry for return type "${returnType}"`); retGetter = retInfo.get; } - const nargs = parameters.length; + const nargs = argumentTypes.length; const argInfos = []; const argOffsets = []; let anyPointer = false; for (let i = 0; i < nargs; i++) { - const info = sbTypeInfo[parameters[i]]; - /* c8 ignore start */ - if (info === undefined) { - throw new ERR_INTERNAL_ASSERTION( - `FFI: shared-buffer type table missing entry for parameter type "${parameters[i]}"`); - } - /* c8 ignore stop */ + const info = sbTypeInfo[argumentTypes[i]]; + assert(info !== undefined, + `FFI: shared-buffer type table missing entry for argument type "${argumentTypes[i]}"`); // Push the `sbTypeInfo` entry directly (entries with the same `kind` // share a shape, keeping `writeNumericArg`'s call sites // low-polymorphism) and store offsets in a parallel array to avoid @@ -267,13 +256,8 @@ function wrapWithSharedBuffer(rawFn, parameters, resultType) { // Pointer signatures need a per-arg runtime type check and fall back // to the native slow-path invoker for non-BigInt pointer arguments, // so arity specialization wouldn't buy much here. - /* c8 ignore start */ - if (slowInvoke === undefined) { - throw new ERR_INTERNAL_ASSERTION( - 'FFI: shared-buffer raw function with pointer arguments is ' + - 'missing kSbInvokeSlow'); - } - /* c8 ignore stop */ + assert(slowInvoke !== undefined, + 'FFI: shared-buffer raw function with pointer arguments is missing kSbInvokeSlow'); wrapper = function(...args) { if (args.length !== nargs) { throwFFIArgCountError(nargs, args.length); @@ -542,19 +526,6 @@ function buildNumericWrapper( }; } -// Accept-set mirrors the native `ParseFunctionSignature` in -// `src/ffi/types.cc`. `ParseFunctionSignature` additionally throws when -// multiple aliases are set at once. The wrapper runs before the native -// call, so those conflicts still surface from the native side regardless -// of which alias we happen to read here. -function sigParams(sig) { - return sig.parameters ?? sig.arguments ?? []; -} - -function sigResult(sig) { - return sig.result ?? sig.return ?? sig.returns ?? 'void'; -} - // The native invoker for SB-eligible symbols is `InvokeFunctionSB`, which // reads arguments from the shared buffer populated by // `wrapWithSharedBuffer`. These patches make sure every path that surfaces @@ -563,11 +534,11 @@ function sigResult(sig) { const rawGetFunction = DynamicLibrary.prototype.getFunction; const rawGetFunctions = DynamicLibrary.prototype.getFunctions; -DynamicLibrary.prototype.getFunction = function getFunction(name, sig) { - // Native `DynamicLibrary::GetFunction` validates `sig`, so by the time - // we have `raw` we know `sig` is a valid object. - const raw = FunctionPrototypeCall(rawGetFunction, this, name, sig); - return wrapWithSharedBuffer(raw, sigParams(sig), sigResult(sig)); +DynamicLibrary.prototype.getFunction = function getFunction(name, signature) { + // Native `DynamicLibrary::GetFunction` validates `signature`, so by the time + // we have `raw` we know `signature` is a valid object. + const raw = FunctionPrototypeCall(rawGetFunction, this, name, signature); + return wrapWithSharedBuffer(raw, signature); }; DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { @@ -583,13 +554,12 @@ DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { for (let i = 0; i < keys.length; i++) { const name = keys[i]; // No `definitions`: native side returned every cached function, so we - // wrap using each function's own `kSbParams` / `kSbResult` metadata + // wrap using each function's own `kSbArguments` / `kSbReturn` metadata // (same fallback as the `functions` accessor). if (definitions === undefined) { out[name] = wrapWithSharedBuffer(raw[name]); } else { - const sig = definitions[name]; - out[name] = wrapWithSharedBuffer(raw[name], sigParams(sig), sigResult(sig)); + out[name] = wrapWithSharedBuffer(raw[name], definitions[name]); } } return out; @@ -602,16 +572,12 @@ DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) { // uninitialized buffer. const functionsDescriptor = ObjectGetOwnPropertyDescriptor(DynamicLibrary.prototype, 'functions'); - /* c8 ignore start */ - if (functionsDescriptor === undefined || !functionsDescriptor.get) { - // Missing getter means the native and JS sides are out of sync; silently - // skipping the patch would expose the fast-path-against-uninitialized-buffer - // footgun this whole block exists to prevent. - throw new ERR_INTERNAL_ASSERTION( - 'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter'); - } - /* c8 ignore stop */ - const origGetter = functionsDescriptor.get; + const origGetter = functionsDescriptor?.get; + // Missing getter means the native and JS sides are out of sync; silently + // skipping the patch would expose the fast-path-against-uninitialized-buffer + // footgun this whole block exists to prevent. + assert(origGetter !== undefined, + 'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter'); ObjectDefineProperty(DynamicLibrary.prototype, 'functions', { __proto__: null, configurable: true, diff --git a/src/env_properties.h b/src/env_properties.h index 113cc066ab2c5d..896b664b64eea2 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -48,8 +48,8 @@ V(async_id_symbol, "async_id_symbol") \ V(ffi_sb_shared_buffer_symbol, "ffi_sb_shared_buffer_symbol") \ V(ffi_sb_invoke_slow_symbol, "ffi_sb_invoke_slow_symbol") \ - V(ffi_sb_params_symbol, "ffi_sb_params_symbol") \ - V(ffi_sb_result_symbol, "ffi_sb_result_symbol") \ + V(ffi_sb_arguments_symbol, "ffi_sb_arguments_symbol") \ + V(ffi_sb_return_symbol, "ffi_sb_return_symbol") \ V(constructor_key_symbol, "constructor_key_symbol") \ V(handle_onclose_symbol, "handle_onclose") \ V(no_message_symbol, "no_message_symbol") \ @@ -293,7 +293,6 @@ V(password_string, "password") \ V(path_string, "path") \ V(pathname_string, "pathname") \ - V(parameters_string, "parameters") \ V(pending_handle_string, "pendingHandle") \ V(permission_string, "permission") \ V(phase_string, "phase") \ @@ -329,9 +328,8 @@ V(require_string, "require") \ V(resource_string, "resource") \ V(result_string, "result") \ - V(return_string, "return") \ - V(returns_string, "returns") \ V(return_arrays_string, "returnArrays") \ + V(return_string, "return") \ V(salt_length_string, "saltLength") \ V(search_string, "search") \ V(servername_string, "servername") \ diff --git a/src/ffi/types.cc b/src/ffi/types.cc index e8469ebc0bbcc6..7f31845a06736c 100644 --- a/src/ffi/types.cc +++ b/src/ffi/types.cc @@ -83,63 +83,26 @@ Maybe ParseFunctionSignature(Environment* env, std::string_view name, Local signature) { Local context = env->context(); - Local returns_key = env->returns_string(); Local return_key = env->return_string(); - Local result_key = env->result_string(); - Local parameters_key = env->parameters_string(); Local arguments_key = env->arguments_string(); - bool has_returns; bool has_return; - bool has_result; - bool has_parameters; bool has_arguments; - if (!signature->Has(context, returns_key).To(&has_returns) || - !signature->Has(context, return_key).To(&has_return) || - !signature->Has(context, result_key).To(&has_result) || - !signature->Has(context, parameters_key).To(&has_parameters) || + if (!signature->Has(context, return_key).To(&has_return) || !signature->Has(context, arguments_key).To(&has_arguments)) { return {}; } - if (has_returns + has_return + has_result > 1) { - THROW_ERR_INVALID_ARG_VALUE( - env, - "Function signature of %s" - " must have either 'returns', 'return' or 'result' " - "property", - name); - return {}; - } - - if (has_arguments && has_parameters) { - THROW_ERR_INVALID_ARG_VALUE(env, - "Function signature of %s" - " must have either 'parameters' or 'arguments' " - "property", - name); - return {}; - } - ffi_type* return_type = &ffi_type_void; std::vector args; std::string return_type_name = "void"; std::vector arg_type_names; Isolate* isolate = env->isolate(); - if (has_returns || has_return || has_result) { - Local return_type_key; - if (has_returns) { - return_type_key = returns_key; - } else if (has_return) { - return_type_key = return_key; - } else { - return_type_key = result_key; - } - + if (has_return) { Local return_type_val; - if (!signature->Get(context, return_type_key).ToLocal(&return_type_val)) { + if (!signature->Get(context, return_key).ToLocal(&return_type_val)) { return {}; } @@ -162,10 +125,9 @@ Maybe ParseFunctionSignature(Environment* env, return_type_name = return_type_str.ToString(); } - if (has_arguments || has_parameters) { + if (has_arguments) { Local arguments_val; - if (!signature->Get(context, has_arguments ? arguments_key : parameters_key) - .ToLocal(&arguments_val)) { + if (!signature->Get(context, arguments_key).ToLocal(&arguments_val)) { return {}; } diff --git a/src/node_ffi.cc b/src/node_ffi.cc index c8197827ac47ae..b8e6df7d29eb6c 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -312,28 +312,28 @@ MaybeLocal DynamicLibrary::CreateFunction( // Attach the original signature type names so the JS wrapper can // rebuild the signature from a raw function when the caller did not - // pass parameters and result explicitly. The `lib.functions` accessor + // pass arguments and return explicitly. The `lib.functions` accessor // path relies on this. - Local params_arr; - if (!ToV8Value(context, fn->arg_type_names, isolate).ToLocal(¶ms_arr)) { + Local args_arr; + if (!ToV8Value(context, fn->arg_type_names, isolate).ToLocal(&args_arr)) { return MaybeLocal(); } if (!ret->DefineOwnProperty(context, - env->ffi_sb_params_symbol(), - params_arr, + env->ffi_sb_arguments_symbol(), + args_arr, internal_attrs) .FromMaybe(false)) { return MaybeLocal(); } - Local result_name; + Local return_name; if (!ToV8Value(context, fn->return_type_name, isolate) - .ToLocal(&result_name)) { + .ToLocal(&return_name)) { return MaybeLocal(); } if (!ret->DefineOwnProperty(context, - env->ffi_sb_result_symbol(), - result_name, + env->ffi_sb_return_symbol(), + return_name, internal_attrs) .FromMaybe(false)) { return MaybeLocal(); @@ -1197,13 +1197,13 @@ static void Initialize(Local target, .Check(); target ->Set(context, - FIXED_ONE_BYTE_STRING(isolate, "kSbParams"), - env->ffi_sb_params_symbol()) + FIXED_ONE_BYTE_STRING(isolate, "kSbArguments"), + env->ffi_sb_arguments_symbol()) .Check(); target ->Set(context, - FIXED_ONE_BYTE_STRING(isolate, "kSbResult"), - env->ffi_sb_result_symbol()) + FIXED_ONE_BYTE_STRING(isolate, "kSbReturn"), + env->ffi_sb_return_symbol()) .Check(); } diff --git a/test/ffi/ffi-test-common.js b/test/ffi/ffi-test-common.js index 7cc64eb00cda2e..86e56de8ec2163 100644 --- a/test/ffi/ffi-test-common.js +++ b/test/ffi/ffi-test-common.js @@ -31,56 +31,56 @@ function ensureFixtureLibrary() { ensureFixtureLibrary(); const fixtureSymbols = { - add_i8: { parameters: ['i8', 'i8'], result: 'i8' }, - add_u8: { parameters: ['u8', 'u8'], result: 'u8' }, - add_i16: { parameters: ['i16', 'i16'], result: 'i16' }, - add_u16: { parameters: ['u16', 'u16'], result: 'u16' }, - add_i32: { parameters: ['i32', 'i32'], result: 'i32' }, - add_u32: { parameters: ['u32', 'u32'], result: 'u32' }, - add_i64: { parameters: ['i64', 'i64'], result: 'i64' }, - add_u64: { parameters: ['u64', 'u64'], result: 'u64' }, - identity_char: { parameters: ['char'], result: 'char' }, - char_is_signed: { parameters: [], result: 'i32' }, - add_f32: { parameters: ['f32', 'f32'], result: 'f32' }, - multiply_f64: { parameters: ['f64', 'f64'], result: 'f64' }, - identity_pointer: { parameters: ['pointer'], result: 'pointer' }, - pointer_to_usize: { parameters: ['pointer'], result: 'u64' }, - usize_to_pointer: { parameters: ['u64'], result: 'pointer' }, - string_length: { parameters: ['pointer'], result: 'u64' }, - string_concat: { parameters: ['pointer', 'pointer'], result: 'pointer' }, - string_duplicate: { parameters: ['pointer'], result: 'pointer' }, - free_string: { parameters: ['pointer'], result: 'void' }, - fill_buffer: { parameters: ['pointer', 'u64', 'u32'], result: 'void' }, - sum_buffer: { parameters: ['pointer', 'u64'], result: 'u64' }, - reverse_buffer: { parameters: ['pointer', 'u64'], result: 'void' }, - logical_and: { parameters: ['i32', 'i32'], result: 'i32' }, - logical_or: { parameters: ['i32', 'i32'], result: 'i32' }, - logical_not: { parameters: ['i32'], result: 'i32' }, - increment_counter: { parameters: [], result: 'void' }, - get_counter: { parameters: [], result: 'i32' }, - reset_counter: { parameters: [], result: 'void' }, - call_int_callback: { parameters: ['pointer', 'i32'], result: 'i32' }, - call_int8_callback: { parameters: ['pointer', 'i8'], result: 'i8' }, - call_pointer_callback_is_null: { parameters: ['pointer'], result: 'i32' }, - call_void_callback: { parameters: ['pointer'], result: 'void' }, - call_string_callback: { parameters: ['function', 'pointer'], result: 'void' }, - call_binary_int_callback: { parameters: ['function', 'i32', 'i32'], result: 'i32' }, - call_callback_multiple_times: { parameters: ['pointer', 'i32'], result: 'void' }, - divide_i32: { parameters: ['i32', 'i32'], result: 'i32' }, - safe_strlen: { parameters: ['pointer'], result: 'i32' }, - sum_five_i32: { parameters: ['i32', 'i32', 'i32', 'i32', 'i32'], result: 'i32' }, - sum_five_f64: { parameters: ['f64', 'f64', 'f64', 'f64', 'f64'], result: 'f64' }, - mixed_operation: { parameters: ['i32', 'f32', 'f64', 'u32'], result: 'f64' }, - allocate_memory: { parameters: ['u64'], result: 'pointer' }, - deallocate_memory: { parameters: ['pointer'], result: 'void' }, - array_get_i32: { parameters: ['pointer', 'u64'], result: 'i32' }, - array_set_i32: { parameters: ['pointer', 'u64', 'i32'], result: 'void' }, - array_get_f64: { parameters: ['pointer', 'u64'], result: 'f64' }, - array_set_f64: { parameters: ['pointer', 'u64', 'f64'], result: 'void' }, + add_i8: { arguments: ['i8', 'i8'], return: 'i8' }, + add_u8: { arguments: ['u8', 'u8'], return: 'u8' }, + add_i16: { arguments: ['i16', 'i16'], return: 'i16' }, + add_u16: { arguments: ['u16', 'u16'], return: 'u16' }, + add_i32: { arguments: ['i32', 'i32'], return: 'i32' }, + add_u32: { arguments: ['u32', 'u32'], return: 'u32' }, + add_i64: { arguments: ['i64', 'i64'], return: 'i64' }, + add_u64: { arguments: ['u64', 'u64'], return: 'u64' }, + identity_char: { arguments: ['char'], return: 'char' }, + char_is_signed: { arguments: [], return: 'i32' }, + add_f32: { arguments: ['f32', 'f32'], return: 'f32' }, + multiply_f64: { arguments: ['f64', 'f64'], return: 'f64' }, + identity_pointer: { arguments: ['pointer'], return: 'pointer' }, + pointer_to_usize: { arguments: ['pointer'], return: 'u64' }, + usize_to_pointer: { arguments: ['u64'], return: 'pointer' }, + string_length: { arguments: ['pointer'], return: 'u64' }, + string_concat: { arguments: ['pointer', 'pointer'], return: 'pointer' }, + string_duplicate: { arguments: ['pointer'], return: 'pointer' }, + free_string: { arguments: ['pointer'], return: 'void' }, + fill_buffer: { arguments: ['pointer', 'u64', 'u32'], return: 'void' }, + sum_buffer: { arguments: ['pointer', 'u64'], return: 'u64' }, + reverse_buffer: { arguments: ['pointer', 'u64'], return: 'void' }, + logical_and: { arguments: ['i32', 'i32'], return: 'i32' }, + logical_or: { arguments: ['i32', 'i32'], return: 'i32' }, + logical_not: { arguments: ['i32'], return: 'i32' }, + increment_counter: { arguments: [], return: 'void' }, + get_counter: { arguments: [], return: 'i32' }, + reset_counter: { arguments: [], return: 'void' }, + call_int_callback: { arguments: ['pointer', 'i32'], return: 'i32' }, + call_int8_callback: { arguments: ['pointer', 'i8'], return: 'i8' }, + call_pointer_callback_is_null: { arguments: ['pointer'], return: 'i32' }, + call_void_callback: { arguments: ['pointer'], return: 'void' }, + call_string_callback: { arguments: ['function', 'pointer'], return: 'void' }, + call_binary_int_callback: { arguments: ['function', 'i32', 'i32'], return: 'i32' }, + call_callback_multiple_times: { arguments: ['pointer', 'i32'], return: 'void' }, + divide_i32: { arguments: ['i32', 'i32'], return: 'i32' }, + safe_strlen: { arguments: ['pointer'], return: 'i32' }, + sum_five_i32: { arguments: ['i32', 'i32', 'i32', 'i32', 'i32'], return: 'i32' }, + sum_five_f64: { arguments: ['f64', 'f64', 'f64', 'f64', 'f64'], return: 'f64' }, + mixed_operation: { arguments: ['i32', 'f32', 'f64', 'u32'], return: 'f64' }, + allocate_memory: { arguments: ['u64'], return: 'pointer' }, + deallocate_memory: { arguments: ['pointer'], return: 'void' }, + array_get_i32: { arguments: ['pointer', 'u64'], return: 'i32' }, + array_set_i32: { arguments: ['pointer', 'u64', 'i32'], return: 'void' }, + array_get_f64: { arguments: ['pointer', 'u64'], return: 'f64' }, + array_set_f64: { arguments: ['pointer', 'u64', 'f64'], return: 'void' }, }; if (!common.isWindows) { - fixtureSymbols.readonly_memory = { parameters: [], result: 'pointer' }; + fixtureSymbols.readonly_memory = { arguments: [], return: 'pointer' }; } function cString(value) { diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js index ef43fb0a6f7274..14020c5a6c78ae 100644 --- a/test/ffi/test-ffi-calls.js +++ b/test/ffi/test-ffi-calls.js @@ -59,8 +59,8 @@ test('ffi bool signatures use uint8 values', () => { assert.strictEqual(symbols.logical_not(0), 1); const boolAdder = lib.getFunction('add_u8', { - parameters: ['bool', 'bool'], - result: 'bool', + arguments: ['bool', 'bool'], + return: 'bool', }); assert.strictEqual(boolAdder(1, 0), 1); assert.throws(() => boolAdder(true, false), /Argument 0 must be a uint8/); @@ -148,15 +148,15 @@ test('ffi callbacks can be registered and invoked', () => { const { lib, functions: symbols } = getLibrary(); const seen = []; const intCallback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, (value) => value * 2, ); const stringCallback = lib.registerCallback( - { parameters: ['pointer'], result: 'void' }, + { arguments: ['pointer'], return: 'void' }, (ptr) => seen.push(ffi.toString(ptr)), ); const binaryCallback = lib.registerCallback( - { arguments: ['i32', 'i32'], returns: 'i32' }, + { arguments: ['i32', 'i32'], return: 'i32' }, (a, b) => a + b, ); @@ -166,8 +166,8 @@ test('ffi callbacks can be registered and invoked', () => { assert.deepStrictEqual(seen, ['hello callback']); assert.strictEqual(symbols.call_binary_int_callback(binaryCallback, 19, 23), 42); - const nullPointerCallback = lib.registerCallback({ result: 'pointer' }, () => null); - const undefinedPointerCallback = lib.registerCallback({ result: 'pointer' }, () => undefined); + const nullPointerCallback = lib.registerCallback({ return: 'pointer' }, () => null); + const undefinedPointerCallback = lib.registerCallback({ return: 'pointer' }, () => undefined); try { assert.strictEqual(symbols.call_pointer_callback_is_null(nullPointerCallback), 1); assert.strictEqual(symbols.call_pointer_callback_is_null(undefinedPointerCallback), 1); @@ -191,7 +191,7 @@ test('ffi callback ref and unref APIs work', () => { called = true; }); const countingCallback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, (value) => { values.push(value); return 0; @@ -271,7 +271,7 @@ const ffi = require('node:ffi'); const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))}); const { lib, functions } = ffi.dlopen(libraryPath, fixtureSymbols); const callback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, () => (${returnExpression}), ); functions.call_int_callback(callback, 21);`, @@ -294,7 +294,7 @@ const ffi = require('node:ffi'); const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))}); const { lib, functions } = ffi.dlopen(libraryPath, fixtureSymbols); const callback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, () => { ${callbackBody} }, ); functions.call_int_callback(callback, 21);`, @@ -325,7 +325,7 @@ const ffi = require('node:ffi'); const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))}); const { lib } = ffi.dlopen(libraryPath, fixtureSymbols); const callback = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, (value) => value * 2, ); new Worker(${JSON.stringify(workerSource)}, { eval: true, workerData: callback });`, @@ -359,7 +359,7 @@ test('ffi unrefCallback releases callback function', async () => { let callback = () => 1; const ref = new WeakRef(callback); const pointer = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, callback, ); @@ -383,7 +383,7 @@ test('ffi unrefCallback zero-fills narrow callback return', async () => { let callback = () => 1; const ref = new WeakRef(callback); const pointer = lib.registerCallback( - { parameters: ['i8'], result: 'i8' }, + { arguments: ['i8'], return: 'i8' }, callback, ); @@ -406,7 +406,7 @@ test('ffi refCallback retains callback function', async () => { try { let callback = () => 1; const ref = new WeakRef(callback); - const pointer = lib.registerCallback({ result: 'i32' }, callback); + const pointer = lib.registerCallback({ return: 'i32' }, callback); lib.unrefCallback(pointer); lib.refCallback(pointer); diff --git a/test/ffi/test-ffi-dynamic-library.js b/test/ffi/test-ffi-dynamic-library.js index e3171b57124250..ba0f8a383ffd8b 100644 --- a/test/ffi/test-ffi-dynamic-library.js +++ b/test/ffi/test-ffi-dynamic-library.js @@ -27,7 +27,7 @@ test('dlopen resolves symbols from the current process with null path', { skip: common.isWindows, }, () => { const { lib, functions } = ffi.dlopen(null, { - uv_os_getpid: { result: 'i32', parameters: [] }, + uv_os_getpid: { return: 'i32', arguments: [] }, }); try { @@ -41,8 +41,8 @@ test('dlopen resolves symbols from the current process with null path', { test('dlopen resolves functions from definitions', () => { const { lib, functions } = ffi.dlopen(libraryPath, { add_i32: fixtureSymbols.add_i32, - add_f32: { returns: 'f32', arguments: ['f32', 'f32'] }, - add_u64: { return: 'u64', parameters: ['u64', 'u64'] }, + add_f32: { return: 'f32', arguments: ['f32', 'f32'] }, + add_u64: { return: 'u64', arguments: ['u64', 'u64'] }, }); try { @@ -73,7 +73,7 @@ test('DynamicLibrary exposes functions and symbols', () => { try { const addI32 = lib.getFunction('add_i32', fixtureSymbols.add_i32); const addU64 = lib.getFunction('add_u64', { - returns: 'u64', + return: 'u64', arguments: ['u64', 'u64'], }); const addI32Ptr = lib.getSymbol('add_i32'); @@ -84,7 +84,7 @@ test('DynamicLibrary exposes functions and symbols', () => { assert.strictEqual(addI32.pointer, addI32Ptr); const functions = lib.getFunctions({ - add_f32: { result: 'f32', parameters: ['f32', 'f32'] }, + add_f32: { return: 'f32', arguments: ['f32', 'f32'] }, add_i64: { return: 'i64', arguments: ['i64', 'i64'] }, }); @@ -115,7 +115,7 @@ test('getFunction caches signatures consistently', () => { ); assert.throws(() => { - lib.getFunction('add_i32', { parameters: ['u32', 'u32'], result: 'u32' }); + lib.getFunction('add_i32', { arguments: ['u32', 'u32'], return: 'u32' }); }, /already requested with a different signature/); } finally { lib.close(); @@ -226,7 +226,7 @@ test('dynamic library APIs validate failures and bad signatures', () => { assert.throws(() => { ffi.dlopen(libraryPath, { add_i32: fixtureSymbols.add_i32, - missing_symbol: { result: 'void', parameters: [] }, + missing_symbol: { return: 'void', arguments: [] }, }); }, /dlsym failed:/); @@ -245,7 +245,7 @@ test('dynamic library APIs validate failures and bad signatures', () => { try { assert.throws(() => { - lib.getFunction('missing_symbol', { result: 'void', parameters: [] }); + lib.getFunction('missing_symbol', { return: 'void', arguments: [] }); }, /dlsym failed:/); assert.throws(() => { @@ -259,21 +259,21 @@ test('dynamic library APIs validate failures and bad signatures', () => { assert.throws(() => { lib.getFunctions({ add_i32: fixtureSymbols.add_i32, - missing_symbol: { result: 'void', parameters: [] }, + missing_symbol: { return: 'void', arguments: [] }, }); }, /dlsym failed:/); assert.strictEqual(lib.getFunction('add_i32', { - result: 'pointer', - parameters: ['pointer'], + return: 'pointer', + arguments: ['pointer'], }).pointer, lib.getSymbol('add_i32')); assert.throws(() => { - lib.getFunction('add_i32', { result: 'i32\0bad', parameters: [] }); + lib.getFunction('add_i32', { return: 'i32\0bad', arguments: [] }); }, /Return value type of function add_i32 must not contain null bytes/); assert.throws(() => { - lib.getFunction('add_i32', { result: 'i32', parameters: ['i32\0bad'] }); + lib.getFunction('add_i32', { return: 'i32', arguments: ['i32\0bad'] }); }, /Argument 0 of function add_i32 must not contain null bytes/); assert.throws(() => { @@ -305,30 +305,14 @@ test('dynamic library APIs validate failures and bad signatures', () => { }); assert.throws(() => { - lib.getFunction('add_i32', { - result: 'i32', - return: 'i32', - parameters: ['i32', 'i32'], - }); - }, /must have either 'returns', 'return' or 'result' property/); - - assert.throws(() => { - lib.getFunction('add_i32', { - result: 'i32', - parameters: ['i32', 'i32'], - arguments: ['i32', 'i32'], - }); - }, /must have either 'parameters' or 'arguments' property/); - - assert.throws(() => { - lib.getFunction('add_i32', { result: 'bogus', parameters: [] }); + lib.getFunction('add_i32', { return: 'bogus', arguments: [] }); }, /Unsupported FFI type: bogus/); const hasTrapError = new Error('signature has trap'); assert.throws(() => { lib.getFunction('add_i32', new Proxy({}, { has(target, key) { - if (key === 'result') { + if (key === 'return') { throw hasTrapError; } return Reflect.has(target, key); @@ -339,8 +323,8 @@ test('dynamic library APIs validate failures and bad signatures', () => { const getterError = new Error('signature getter'); assert.throws(() => { lib.getFunction('add_i32', { - result: 'i32', - get parameters() { + return: 'i32', + get arguments() { throw getterError; }, }); diff --git a/test/ffi/test-ffi-shared-buffer.js b/test/ffi/test-ffi-shared-buffer.js index 944b4021abc47a..429faf6439faf0 100644 --- a/test/ffi/test-ffi-shared-buffer.js +++ b/test/ffi/test-ffi-shared-buffer.js @@ -19,8 +19,8 @@ const { internalBinding } = require('internal/test/binding'); const ffiBinding = internalBinding('ffi'); const { kSbInvokeSlow, - kSbParams, - kSbResult, + kSbArguments, + kSbReturn, kSbSharedBuffer, } = ffiBinding; const rawGetFunctionUnpatched = ffiBinding.DynamicLibrary.prototype.getFunction; @@ -30,7 +30,7 @@ const { libraryPath } = require('./ffi-test-common'); test('numeric-only i32 function uses SB path', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, }); try { assert.strictEqual(functions.add_i32(20, 22), 42); @@ -44,10 +44,10 @@ test('numeric-only i32 function uses SB path', () => { test('i8/u8/i16/u16 round-trip', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i8: { result: 'i8', parameters: ['i8', 'i8'] }, - add_u8: { result: 'u8', parameters: ['u8', 'u8'] }, - add_i16: { result: 'i16', parameters: ['i16', 'i16'] }, - add_u16: { result: 'u16', parameters: ['u16', 'u16'] }, + add_i8: { return: 'i8', arguments: ['i8', 'i8'] }, + add_u8: { return: 'u8', arguments: ['u8', 'u8'] }, + add_i16: { return: 'i16', arguments: ['i16', 'i16'] }, + add_u16: { return: 'u16', arguments: ['u16', 'u16'] }, }); try { assert.strictEqual(functions.add_i8(10, 20), 30); @@ -61,8 +61,8 @@ test('i8/u8/i16/u16 round-trip', () => { test('f32/f64 round-trip', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_f32: { result: 'f32', parameters: ['f32', 'f32'] }, - add_f64: { result: 'f64', parameters: ['f64', 'f64'] }, + add_f32: { return: 'f32', arguments: ['f32', 'f32'] }, + add_f64: { return: 'f64', arguments: ['f64', 'f64'] }, }); try { // 1.25 and 2.75 are exactly representable in float32, so the sum is exact. @@ -75,8 +75,8 @@ test('f32/f64 round-trip', () => { test('i64/u64 BigInt round-trip', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i64: { result: 'i64', parameters: ['i64', 'i64'] }, - add_u64: { result: 'u64', parameters: ['u64', 'u64'] }, + add_i64: { return: 'i64', arguments: ['i64', 'i64'] }, + add_u64: { return: 'u64', arguments: ['u64', 'u64'] }, }); try { assert.strictEqual(functions.add_i64(10n, 20n), 30n); @@ -88,7 +88,7 @@ test('i64/u64 BigInt round-trip', () => { test('zero-arg function', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - char_is_signed: { result: 'i32', parameters: [] }, + char_is_signed: { return: 'i32', arguments: [] }, }); try { const result = functions.char_is_signed(); @@ -101,7 +101,7 @@ test('zero-arg function', () => { test('6-arg numeric function', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - sum_6_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'] }, + sum_6_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'] }, }); try { assert.strictEqual(functions.sum_6_i32(1, 2, 3, 4, 5, 6), 21); @@ -112,8 +112,8 @@ test('6-arg numeric function', () => { test('pointer args: fast path (BigInt/null) and slow-path fallback (Buffer/ArrayBuffer)', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - identity_pointer: { result: 'pointer', parameters: ['pointer'] }, - pointer_to_usize: { result: 'u64', parameters: ['pointer'] }, + identity_pointer: { return: 'pointer', arguments: ['pointer'] }, + pointer_to_usize: { return: 'u64', arguments: ['pointer'] }, }); try { assert.strictEqual(functions.identity_pointer(0n), 0n); @@ -137,7 +137,7 @@ test('pointer args: fast path (BigInt/null) and slow-path fallback (Buffer/Array test('string pointer uses slow-path fallback', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - string_length: { result: 'u64', parameters: ['pointer'] }, + string_length: { return: 'u64', arguments: ['pointer'] }, }); try { assert.strictEqual(functions.string_length('hello'), 5n); @@ -150,8 +150,8 @@ test('string pointer uses slow-path fallback', () => { test('non-SB-eligible signature falls back to raw function', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - string_duplicate: { result: 'pointer', parameters: ['pointer'] }, - free_string: { result: 'void', parameters: ['pointer'] }, + string_duplicate: { return: 'pointer', arguments: ['pointer'] }, + free_string: { return: 'void', arguments: ['pointer'] }, }); try { const dup = functions.string_duplicate('round-trip'); @@ -167,14 +167,14 @@ test('reentrancy across two FFI symbols', () => { // A JS callback invoked by one FFI function reenters a different FFI // function. Each has its own ArrayBuffer; neither may clobber the other. const { lib, functions } = ffi.dlopen(libraryPath, { - call_int_callback: { result: 'i32', parameters: ['pointer', 'i32'] }, - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, + call_int_callback: { return: 'i32', arguments: ['pointer', 'i32'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, }); let callDepth = 0; let innerResult = -1; const callback = lib.registerCallback( - { result: 'i32', parameters: ['i32'] }, + { return: 'i32', arguments: ['i32'] }, (x) => { callDepth++; if (callDepth === 1) innerResult = functions.add_i32(x, 100); @@ -194,7 +194,7 @@ test('reentrancy across two FFI symbols', () => { test('arity mismatch throws ERR_INVALID_ARG_VALUE', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, }); try { assert.throws(() => functions.add_i32(1), { @@ -213,8 +213,8 @@ test('arity mismatch throws ERR_INVALID_ARG_VALUE', () => { test('arity 7+ uses the generic rest-params branch', () => { const { lib, functions } = ffi.dlopen(libraryPath, { sum_7_i32: { - result: 'i32', - parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], + return: 'i32', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], }, }); try { @@ -230,8 +230,8 @@ test('arity 7+ uses the generic rest-params branch', () => { test('wrappers preserve name/length/pointer and the functions accessor returns wrappers', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, - identity_pointer: { result: 'pointer', parameters: ['pointer'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, + identity_pointer: { return: 'pointer', arguments: ['pointer'] }, }); try { assert.strictEqual(functions.add_i32.name, 'add_i32'); @@ -253,12 +253,12 @@ test('wrappers preserve name/length/pointer and the functions accessor returns w test('integer boundaries for i8/u8/i16/u16/i32/u32', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i8: { result: 'i8', parameters: ['i8', 'i8'] }, - add_u8: { result: 'u8', parameters: ['u8', 'u8'] }, - add_i16: { result: 'i16', parameters: ['i16', 'i16'] }, - add_u16: { result: 'u16', parameters: ['u16', 'u16'] }, - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, - add_u32: { result: 'u32', parameters: ['u32', 'u32'] }, + add_i8: { return: 'i8', arguments: ['i8', 'i8'] }, + add_u8: { return: 'u8', arguments: ['u8', 'u8'] }, + add_i16: { return: 'i16', arguments: ['i16', 'i16'] }, + add_u16: { return: 'u16', arguments: ['u16', 'u16'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, + add_u32: { return: 'u32', arguments: ['u32', 'u32'] }, }); try { @@ -299,8 +299,8 @@ test('integer boundaries for i8/u8/i16/u16/i32/u32', () => { test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_i64: { result: 'i64', parameters: ['i64', 'i64'] }, - add_u64: { result: 'u64', parameters: ['u64', 'u64'] }, + add_i64: { return: 'i64', arguments: ['i64', 'i64'] }, + add_u64: { return: 'u64', arguments: ['u64', 'u64'] }, }); try { @@ -328,8 +328,8 @@ test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => { test('char type picks signed/unsigned range based on host ABI', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - char_is_signed: { result: 'i32', parameters: [] }, - identity_char: { result: 'char', parameters: ['char'] }, + char_is_signed: { return: 'i32', arguments: [] }, + identity_char: { return: 'char', arguments: ['char'] }, }); try { @@ -358,13 +358,13 @@ test('SB metadata is Symbol-keyed, attribute-hardened, and not leaked onto the w const rawLib = new ffiBinding.DynamicLibrary(libraryPath); try { const rawFn = rawGetFunctionUnpatched.call( - rawLib, 'add_i32', { result: 'i32', parameters: ['i32', 'i32'] }); + rawLib, 'add_i32', { return: 'i32', arguments: ['i32', 'i32'] }); for (const [name, sym] of [ ['kSbSharedBuffer', kSbSharedBuffer], ['kSbInvokeSlow', kSbInvokeSlow], - ['kSbParams', kSbParams], - ['kSbResult', kSbResult], + ['kSbArguments', kSbArguments], + ['kSbReturn', kSbReturn], ]) { assert.strictEqual(typeof sym, 'symbol', `${name} must be a Symbol`); } @@ -372,8 +372,8 @@ test('SB metadata is Symbol-keyed, attribute-hardened, and not leaked onto the w // Numeric-only signature: kSbInvokeSlow absent; the rest present and hardened. for (const [name, sym] of [ ['kSbSharedBuffer', kSbSharedBuffer], - ['kSbParams', kSbParams], - ['kSbResult', kSbResult], + ['kSbArguments', kSbArguments], + ['kSbReturn', kSbReturn], ]) { const desc = Object.getOwnPropertyDescriptor(rawFn, sym); assert.ok(desc !== undefined, `${name} missing on pure-numeric SB function`); @@ -386,7 +386,7 @@ test('SB metadata is Symbol-keyed, attribute-hardened, and not leaked onto the w // Pointer signature: kSbInvokeSlow must exist (and be hardened). const rawPtrFn = rawGetFunctionUnpatched.call( - rawLib, 'identity_pointer', { result: 'pointer', parameters: ['pointer'] }); + rawLib, 'identity_pointer', { return: 'pointer', arguments: ['pointer'] }); const slowDesc = Object.getOwnPropertyDescriptor(rawPtrFn, kSbInvokeSlow); assert.ok(slowDesc !== undefined); assert.strictEqual(slowDesc.enumerable, false); @@ -396,18 +396,18 @@ test('SB metadata is Symbol-keyed, attribute-hardened, and not leaked onto the w assert.deepStrictEqual(Object.keys(rawFn), ['pointer']); const ownSyms = Object.getOwnPropertySymbols(rawFn); assert.ok(ownSyms.includes(kSbSharedBuffer)); - assert.ok(ownSyms.includes(kSbParams)); - assert.ok(ownSyms.includes(kSbResult)); + assert.ok(ownSyms.includes(kSbArguments)); + assert.ok(ownSyms.includes(kSbReturn)); // Internals must not be forwarded by `inheritMetadata`. const { lib, functions } = ffi.dlopen(libraryPath, { - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, }); try { assert.strictEqual(functions.add_i32[kSbSharedBuffer], undefined); assert.strictEqual(functions.add_i32[kSbInvokeSlow], undefined); - assert.strictEqual(functions.add_i32[kSbParams], undefined); - assert.strictEqual(functions.add_i32[kSbResult], undefined); + assert.strictEqual(functions.add_i32[kSbArguments], undefined); + assert.strictEqual(functions.add_i32[kSbReturn], undefined); } finally { lib.close(); } @@ -418,7 +418,7 @@ test('SB metadata is Symbol-keyed, attribute-hardened, and not leaked onto the w test('pointer fast-path range check: [0, 2^64 - 1]', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - identity_pointer: { result: 'pointer', parameters: ['pointer'] }, + identity_pointer: { return: 'pointer', arguments: ['pointer'] }, }); try { assert.strictEqual(functions.identity_pointer(0n), 0n); @@ -438,15 +438,15 @@ test('self-recursive reentrancy: a single function\'s ArrayBuffer survives a nes // call can reuse the same buffer without clobbering the outer frame. const { lib, functions } = ffi.dlopen(libraryPath, { call_binary_int_callback: { - result: 'i32', - parameters: ['function', 'i32', 'i32'], + return: 'i32', + arguments: ['function', 'i32', 'i32'], }, }); try { let depth = 0; const callback = lib.registerCallback( - { result: 'i32', parameters: ['i32', 'i32'] }, + { return: 'i32', arguments: ['i32', 'i32'] }, common.mustCall((a, b) => { depth++; if (depth === 1) { @@ -469,9 +469,9 @@ test('self-recursive reentrancy: a single function\'s ArrayBuffer survives a nes test('void-return 0-arg wrapper branch', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - reset_counter: { result: 'void', parameters: [] }, - increment_counter: { result: 'void', parameters: [] }, - get_counter: { result: 'i32', parameters: [] }, + reset_counter: { return: 'void', arguments: [] }, + increment_counter: { return: 'void', arguments: [] }, + get_counter: { return: 'i32', arguments: [] }, }); try { assert.strictEqual(functions.reset_counter(), undefined); @@ -496,26 +496,26 @@ test('void-return wrapper at every specialized arity observes side effects', () // at every arity the ladder specializes (1..6) plus the 7+ rest-params // fallback. const { lib, functions } = ffi.dlopen(libraryPath, { - store_i32: { result: 'void', parameters: ['i32'] }, - store_sum_2_i32: { result: 'void', parameters: ['i32', 'i32'] }, - store_sum_3_i32: { result: 'void', parameters: ['i32', 'i32', 'i32'] }, + store_i32: { return: 'void', arguments: ['i32'] }, + store_sum_2_i32: { return: 'void', arguments: ['i32', 'i32'] }, + store_sum_3_i32: { return: 'void', arguments: ['i32', 'i32', 'i32'] }, store_sum_4_i32: { - result: 'void', - parameters: ['i32', 'i32', 'i32', 'i32'], + return: 'void', + arguments: ['i32', 'i32', 'i32', 'i32'], }, store_sum_5_i32: { - result: 'void', - parameters: ['i32', 'i32', 'i32', 'i32', 'i32'], + return: 'void', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32'], }, store_sum_6_i32: { - result: 'void', - parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], + return: 'void', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], }, store_sum_8_i32: { - result: 'void', - parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], + return: 'void', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], }, - get_scratch: { result: 'i32', parameters: [] }, + get_scratch: { return: 'i32', arguments: [] }, }); try { // Powers-of-two summands detect a dropped or duplicated slot at each @@ -598,17 +598,17 @@ test('value-return wrapper arity mismatch hits every specialized branch', () => // value-return closures for arities 1..6 so each specialization's // argument-count guard runs at least once. const { lib, functions } = ffi.dlopen(libraryPath, { - logical_not: { result: 'i32', parameters: ['i32'] }, - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, - sum_3_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32'] }, - sum_4_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32', 'i32'] }, + logical_not: { return: 'i32', arguments: ['i32'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, + sum_3_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32'] }, + sum_4_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32', 'i32'] }, sum_five_i32: { - result: 'i32', - parameters: ['i32', 'i32', 'i32', 'i32', 'i32'], + return: 'i32', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32'], }, sum_6_i32: { - result: 'i32', - parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], + return: 'i32', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'], }, }); try { @@ -647,7 +647,7 @@ test('pointer-dispatch wrapper rejects wrong-arity calls', () => { // per-arity ladder, but it still has its own `throwFFIArgCountError` // branch that needs to be exercised. const { lib, functions } = ffi.dlopen(libraryPath, { - identity_pointer: { result: 'pointer', parameters: ['pointer'] }, + identity_pointer: { return: 'pointer', arguments: ['pointer'] }, }); try { assert.throws( @@ -669,10 +669,10 @@ test('pointer-dispatch wrapper rejects wrong-arity calls', () => { test('mid-arity wrappers (1, 3, 4, 5)', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - logical_not: { result: 'i32', parameters: ['i32'] }, - sum_3_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32'] }, - sum_4_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32', 'i32'] }, - sum_five_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32', 'i32', 'i32'] }, + logical_not: { return: 'i32', arguments: ['i32'] }, + sum_3_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32'] }, + sum_4_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32', 'i32'] }, + sum_five_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32', 'i32', 'i32'] }, }); try { assert.strictEqual(functions.logical_not(0), 1); @@ -689,8 +689,8 @@ test('mid-arity wrappers (1, 3, 4, 5)', () => { test('float specials: NaN, ±Infinity, -0 round-trip bit-exact', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - add_f64: { result: 'f64', parameters: ['f64', 'f64'] }, - multiply_f64: { result: 'f64', parameters: ['f64', 'f64'] }, + add_f64: { return: 'f64', arguments: ['f64', 'f64'] }, + multiply_f64: { return: 'f64', arguments: ['f64', 'f64'] }, }); try { assert.ok(Number.isNaN(functions.add_f64(NaN, 1.0))); @@ -704,7 +704,7 @@ test('float specials: NaN, ±Infinity, -0 round-trip bit-exact', () => { test('arity-7+ branch still runs per-arg validation', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - sum_7_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'] }, + sum_7_i32: { return: 'i32', arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'] }, }); try { assert.throws( @@ -720,7 +720,7 @@ test('mixed-kind signature (i32, f32, f64, u32) dispatches the right writer per // Four distinct `sbTypeInfo.kind` values (int, float, float, int) — a // wiring bug that reused one writer across slots would surface here. const { lib, functions } = ffi.dlopen(libraryPath, { - mixed_operation: { parameters: ['i32', 'f32', 'f64', 'u32'], result: 'f64' }, + mixed_operation: { arguments: ['i32', 'f32', 'f64', 'u32'], return: 'f64' }, }); try { @@ -748,11 +748,11 @@ test('lib.getFunctions() with no arguments wraps every cached function', () => { // the early-return path in `wrapWithSharedBuffer` alongside the wrapped // branch. const { lib } = ffi.dlopen(libraryPath, { - add_i32: { result: 'i32', parameters: ['i32', 'i32'] }, - add_f64: { result: 'f64', parameters: ['f64', 'f64'] }, - mixed_operation: { parameters: ['i32', 'f32', 'f64', 'u32'], result: 'f64' }, - identity_pointer: { result: 'pointer', parameters: ['pointer'] }, - string_length: { result: 'u64', parameters: ['string'] }, + add_i32: { return: 'i32', arguments: ['i32', 'i32'] }, + add_f64: { return: 'f64', arguments: ['f64', 'f64'] }, + mixed_operation: { arguments: ['i32', 'f32', 'f64', 'u32'], return: 'f64' }, + identity_pointer: { return: 'pointer', arguments: ['pointer'] }, + string_length: { return: 'u64', arguments: ['string'] }, }); try { @@ -789,12 +789,12 @@ test('lib.getFunctions() with no arguments wraps every cached function', () => { test('mixed pointer + numeric signature uses the pointer-dispatch wrapper', () => { const { lib, functions } = ffi.dlopen(libraryPath, { - call_int_callback: { result: 'i32', parameters: ['pointer', 'i32'] }, + call_int_callback: { return: 'i32', arguments: ['pointer', 'i32'] }, }); try { const cb = lib.registerCallback( - { result: 'i32', parameters: ['i32'] }, + { return: 'i32', arguments: ['i32'] }, (x) => x * 2, ); try { diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index d29ca1051fa450..1a2a2eaeab87cd 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -15,7 +15,7 @@ test('ffi unrefCallback releases callback function', async (t) => { let callback = () => 1; const ref = new WeakRef(callback); const pointer = lib.registerCallback( - { parameters: ['i32'], result: 'i32' }, + { arguments: ['i32'], return: 'i32' }, callback, ); @@ -37,7 +37,7 @@ test('ffi refCallback retains callback function', async (t) => { let callback = () => 1; const ref = new WeakRef(callback); - const pointer = lib.registerCallback({ result: 'i32' }, callback); + const pointer = lib.registerCallback({ return: 'i32' }, callback); lib.unrefCallback(pointer); lib.refCallback(pointer); From 7a9feef584982a9d7b31c5bb0e67f0bf14419f0d Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 24 May 2026 17:20:48 +0200 Subject: [PATCH 11/89] doc: edit Rust toolchain general install instructions Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/63488 Reviewed-By: Luigi Pinca Reviewed-By: Chemi Atlow --- BUILDING.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index d10c1b685ac24e..de50ddd53b8673 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -237,23 +237,11 @@ tarball and/or browse the git repository checked out at the relevant tag. ### Prerequisites * [A supported version of Python][Python versions] for building and testing. -* A Rust toolchain if [building Node.js with Temporal support](#building-nodejs-with-temporal-support) - is required (enabled by default starting in Node.js 26). +* A Rust toolchain if [building Node.js with Temporal support](#building-nodejs-with-temporal-support). * Memory: at least 8GB of RAM is typically required when compiling with 4 parallel jobs (e.g: `make -j4`). ### Unix and macOS -Consult the official [Install Rust](https://rust-lang.org/tools/install/) -instructions to install a Rust toolchain, required for Temporal support introduced in Node.js 25.4.0. -Individual packages such as `rust` and `cargo` in some operating system distributions may be considered -as an alternative, for example in CI environments. -Consult with relevant operating system documentation to ensure that packages -meet the minimum version specified in the -[Building Node.js with Temporal support](#building-nodejs-with-temporal-support) section, -as packaged versions may lag behind the `stable` version installed by the official instructions. -Avoid mixing `rustup` together with `rust` and `cargo` package installations, due to -potential version conflicts. - #### Unix prerequisites * `gcc` and `g++` >= 13.2 or `clang` and `clang++` >= 19.1 @@ -1062,14 +1050,19 @@ enable FIPS support in Node.js. Node.js supports the [Temporal](https://github.com/tc39/proposal-temporal) APIs, when linking statically or dynamically with a version of [temporal\_rs](https://github.com/boa-dev/temporal). - -Temporal support is enabled by default starting in Node.js 26. Building it -requires a Rust toolchain: +Building it requires a Rust toolchain: * rustc >= 1.82 (with LLVM >= 19) * cargo >= 1.82 Refer to [Install Rust](https://rust-lang.org/tools/install/) for instructions. +Individual packages such as `rust` and `cargo` in some operating system distributions may be considered +as an alternative, for example in CI environments. +Consult with relevant operating system documentation to ensure that packages +meet the minimum version specified above, +as packaged versions may lag behind the `stable` version installed by the official instructions. +Avoid mixing `rustup` together with `rust` and `cargo` package installations, due to +potential version conflicts. If `--v8-enable-temporal-support` and `--v8-disable-temporal-support` are both omitted, `configure.py` probes for `cargo` and `rustc`. If either is missing, From 8c3e9bd96706e3b21e7064c709d36ec584e817aa Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 24 May 2026 17:20:58 +0200 Subject: [PATCH 12/89] test: get rid of unnecessary `AbortController` instanciations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/63489 Reviewed-By: Aviv Keller Reviewed-By: Chengzhong Wu Reviewed-By: René Reviewed-By: Trivikram Kamat Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Luigi Pinca Reviewed-By: Chemi Atlow Reviewed-By: James M Snell --- .../test-fs-promises-file-handle-writer.js | 15 +++------------ .../parallel/test-runner-mock-timers-scheduler.js | 4 +--- test/parallel/test-runner-mock-timers.js | 8 ++------ test/parallel/test-stream-iter-broadcast-from.js | 5 +---- test/parallel/test-stream-iter-consumers-bytes.js | 4 +--- test/parallel/test-stream-iter-consumers-merge.js | 5 +---- test/parallel/test-stream-iter-consumers-text.js | 4 +--- test/parallel/test-stream-iter-pull-async.js | 5 +---- test/parallel/test-stream-iter-push-basic.js | 4 +--- test/parallel/test-stream-iter-push-writer.js | 5 +---- test/parallel/test-stream-iter-share-async.js | 5 +---- test/parallel/test-stream-iter-to-readable.js | 4 +--- 12 files changed, 15 insertions(+), 53 deletions(-) diff --git a/test/parallel/test-fs-promises-file-handle-writer.js b/test/parallel/test-fs-promises-file-handle-writer.js index 95ba8756fbe3cd..ff90716400ef37 100644 --- a/test/parallel/test-fs-promises-file-handle-writer.js +++ b/test/parallel/test-fs-promises-file-handle-writer.js @@ -458,11 +458,8 @@ async function testWriteWithAbortedSignalRejects() { const fh = await open(filePath, 'w'); const w = fh.writer(); - const ac = new AbortController(); - ac.abort(); - await assert.rejects( - w.write(Buffer.from('data'), { signal: ac.signal }), + w.write(Buffer.from('data'), { signal: AbortSignal.abort() }), { name: 'AbortError' }, ); @@ -479,11 +476,8 @@ async function testWritevWithAbortedSignalRejects() { const fh = await open(filePath, 'w'); const w = fh.writer(); - const ac = new AbortController(); - ac.abort(); - await assert.rejects( - w.writev([Buffer.from('a'), Buffer.from('b')], { signal: ac.signal }), + w.writev([Buffer.from('a'), Buffer.from('b')], { signal: AbortSignal.abort() }), { name: 'AbortError' }, ); @@ -501,11 +495,8 @@ async function testEndWithAbortedSignalRejects() { await w.write(Buffer.from('data')); - const ac = new AbortController(); - ac.abort(); - await assert.rejects( - w.end({ signal: ac.signal }), + w.end({ signal: AbortSignal.abort() }), { name: 'AbortError' }, ); diff --git a/test/parallel/test-runner-mock-timers-scheduler.js b/test/parallel/test-runner-mock-timers-scheduler.js index 72a69b5d675b39..fa019221fc88fe 100644 --- a/test/parallel/test-runner-mock-timers-scheduler.js +++ b/test/parallel/test-runner-mock-timers-scheduler.js @@ -97,11 +97,9 @@ describe('Mock Timers Scheduler Test Suite', () => { it('should abort operation when .abort is called before calling setInterval', async (t) => { t.mock.timers.enable({ apis: ['scheduler.wait'] }); - const controller = new AbortController(); - controller.abort(); const p = nodeTimersPromises.scheduler.wait(2000, { ref: true, - signal: controller.signal, + signal: AbortSignal.abort(), }); await assert.rejects(() => p, { diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 09075cf3a58fa8..25510fe023d0ed 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -551,11 +551,9 @@ describe('Mock Timers Test Suite', () => { it('should abort operation when .abort is called before calling setInterval', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }); const expectedResult = 'result'; - const controller = new AbortController(); - controller.abort(); const p = nodeTimersPromises.setTimeout(2000, expectedResult, { ref: true, - signal: controller.signal, + signal: AbortSignal.abort(), }); await assert.rejects(() => p, { @@ -778,10 +776,8 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.enable({ apis: ['setInterval'] }); const interval = 100; - const abortController = new AbortController(); - abortController.abort(); const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), { - signal: abortController.signal, + signal: AbortSignal.abort(), }); const first = intervalIterator.next(); diff --git a/test/parallel/test-stream-iter-broadcast-from.js b/test/parallel/test-stream-iter-broadcast-from.js index c7458dee19ad64..0203252f26229a 100644 --- a/test/parallel/test-stream-iter-broadcast-from.js +++ b/test/parallel/test-stream-iter-broadcast-from.js @@ -79,10 +79,7 @@ async function testAbortSignal() { } async function testAlreadyAbortedSignal() { - const ac = new AbortController(); - ac.abort(); - - const { broadcast: bc } = broadcast({ signal: ac.signal }); + const { broadcast: bc } = broadcast({ signal: AbortSignal.abort() }); const consumer = bc.push(); await assert.rejects(async () => { diff --git a/test/parallel/test-stream-iter-consumers-bytes.js b/test/parallel/test-stream-iter-consumers-bytes.js index ebb5dae0ac636e..e45ee991d587fd 100644 --- a/test/parallel/test-stream-iter-consumers-bytes.js +++ b/test/parallel/test-stream-iter-consumers-bytes.js @@ -45,10 +45,8 @@ async function testBytesAsyncLimit() { } async function testBytesAsyncAbort() { - const ac = new AbortController(); - ac.abort(); await assert.rejects( - () => bytes(from('data'), { signal: ac.signal }), + () => bytes(from('data'), { signal: AbortSignal.abort() }), { name: 'AbortError' }, ); } diff --git a/test/parallel/test-stream-iter-consumers-merge.js b/test/parallel/test-stream-iter-consumers-merge.js index c5b18be042d874..b777a3ee205fad 100644 --- a/test/parallel/test-stream-iter-consumers-merge.js +++ b/test/parallel/test-stream-iter-consumers-merge.js @@ -55,10 +55,7 @@ async function testMergeEmpty() { } async function testMergeWithAbortSignal() { - const ac = new AbortController(); - ac.abort(); - - const merged = merge(from('data'), { signal: ac.signal }); + const merged = merge(from('data'), { signal: AbortSignal.abort() }); await assert.rejects( async () => { diff --git a/test/parallel/test-stream-iter-consumers-text.js b/test/parallel/test-stream-iter-consumers-text.js index 8bfa7c3320981c..16fca64142a442 100644 --- a/test/parallel/test-stream-iter-consumers-text.js +++ b/test/parallel/test-stream-iter-consumers-text.js @@ -96,10 +96,8 @@ async function testTextEmpty() { // text() with abort signal async function testTextWithSignal() { - const ac = new AbortController(); - ac.abort(); await assert.rejects( - () => text(from('data'), { signal: ac.signal }), + () => text(from('data'), { signal: AbortSignal.abort() }), { name: 'AbortError' }, ); } diff --git a/test/parallel/test-stream-iter-pull-async.js b/test/parallel/test-stream-iter-pull-async.js index 1dbe7c98878ccc..db25a339848011 100644 --- a/test/parallel/test-stream-iter-pull-async.js +++ b/test/parallel/test-stream-iter-pull-async.js @@ -41,14 +41,11 @@ async function testPullStatefulTransform() { } async function testPullWithAbortSignal() { - const ac = new AbortController(); - ac.abort(); - async function* gen() { yield [new Uint8Array([1])]; } - const result = pull(gen(), { signal: ac.signal }); + const result = pull(gen(), { signal: AbortSignal.abort() }); await assert.rejects( async () => { // eslint-disable-next-line no-unused-vars diff --git a/test/parallel/test-stream-iter-push-basic.js b/test/parallel/test-stream-iter-push-basic.js index 22d5b26c830a47..bd6944e9492575 100644 --- a/test/parallel/test-stream-iter-push-basic.js +++ b/test/parallel/test-stream-iter-push-basic.js @@ -109,9 +109,7 @@ async function testAbortSignal() { } async function testPreAbortedSignal() { - const ac = new AbortController(); - ac.abort(); - const { readable } = push({ signal: ac.signal }); + const { readable } = push({ signal: AbortSignal.abort() }); await assert.rejects(async () => { // eslint-disable-next-line no-unused-vars for await (const _ of readable) { diff --git a/test/parallel/test-stream-iter-push-writer.js b/test/parallel/test-stream-iter-push-writer.js index e7e783d7b74a9c..7a9307c5c2a9f6 100644 --- a/test/parallel/test-stream-iter-push-writer.js +++ b/test/parallel/test-stream-iter-push-writer.js @@ -61,12 +61,9 @@ async function testWriteWithSignalRejects() { async function testWriteWithPreAbortedSignal() { const { writer, readable } = push({ highWaterMark: 1 }); - const ac = new AbortController(); - ac.abort(); - // Pre-aborted signal should reject immediately await assert.rejects( - writer.write('data', { signal: ac.signal }), + writer.write('data', { signal: AbortSignal.abort() }), { name: 'AbortError' }, ); diff --git a/test/parallel/test-stream-iter-share-async.js b/test/parallel/test-stream-iter-share-async.js index 076fe0a4037aa0..c10ac71a7b3ead 100644 --- a/test/parallel/test-stream-iter-share-async.js +++ b/test/parallel/test-stream-iter-share-async.js @@ -197,10 +197,7 @@ async function testShareAbortSignalWhileSourcePullPending() { } async function testShareAlreadyAborted() { - const ac = new AbortController(); - ac.abort(); - - const shared = share(from('data'), { signal: ac.signal }); + const shared = share(from('data'), { signal: AbortSignal.abort() }); const consumer = shared.pull(); await assert.rejects(async () => { diff --git a/test/parallel/test-stream-iter-to-readable.js b/test/parallel/test-stream-iter-to-readable.js index 3f03090e30960c..d8287036e7e7fe 100644 --- a/test/parallel/test-stream-iter-to-readable.js +++ b/test/parallel/test-stream-iter-to-readable.js @@ -311,9 +311,7 @@ async function testSignalAlreadyAborted() { yield [Buffer.from('should not reach')]; } - const ac = new AbortController(); - ac.abort(); - const readable = toReadable(gen(), { signal: ac.signal }); + const readable = toReadable(gen(), { signal: AbortSignal.abort() }); await assert.rejects(async () => { // eslint-disable-next-line no-unused-vars From d79aad8cd714363af021b56d7ee9e63f1fdb77fe Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 24 May 2026 19:57:19 +0200 Subject: [PATCH 13/89] tools: fix skip of `test-internet` on forks Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/63492 Reviewed-By: Filip Skokan Reviewed-By: Aviv Keller Reviewed-By: Chemi Atlow --- .github/workflows/test-internet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml index 6471391171b0c5..47fadf9a3e113c 100644 --- a/.github/workflows/test-internet.yml +++ b/.github/workflows/test-internet.yml @@ -44,7 +44,7 @@ permissions: jobs: test-internet: - if: github.event_name == 'schedule' && github.repository == 'nodejs/node' || github.event.pull_request.draft == false + if: (github.event_name == 'schedule' && github.repository == 'nodejs/node') || (github.event.pull_request && github.event.pull_request.draft == false) runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 8bc95474a122d0237b960155c2948ddb82c7621c Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sun, 24 May 2026 14:46:55 -0400 Subject: [PATCH 14/89] doc: move hyperlinks outside of text blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: avivkeller PR-URL: https://github.com/nodejs/node/pull/63493 Reviewed-By: Antoine du Hamel Reviewed-By: René --- doc/api/modules.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/api/modules.md b/doc/api/modules.md index 5b001026d02f8b..ca11cae333bc7a 100644 --- a/doc/api/modules.md +++ b/doc/api/modules.md @@ -359,8 +359,7 @@ require(X) from module at path Y MAYBE_DETECT_AND_LOAD(X) 1. If X parses as a CommonJS module, load X as a CommonJS module. STOP. 2. Else, if the source code of X can be parsed as ECMAScript module using - DETECT_MODULE_SYNTAX defined in - the ESM resolver, + DETECT_MODULE_SYNTAX defined in the ESM resolver, a. Load X as an ECMAScript module. STOP. 3. THROW the SyntaxError from attempting to parse X as CommonJS in 1. STOP. @@ -424,7 +423,7 @@ LOAD_PACKAGE_IMPORTS(X, DIR) a. let CONDITIONS = ["node", "require", "module-sync"] b. Else, let CONDITIONS = ["node", "require"] 5. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), - CONDITIONS) defined in the ESM resolver. + CONDITIONS) defined in the ESM resolver. 6. RESOLVE_ESM_MATCH(MATCH). LOAD_PACKAGE_EXPORTS(X, DIR) @@ -438,7 +437,7 @@ LOAD_PACKAGE_EXPORTS(X, DIR) a. let CONDITIONS = ["node", "require", "module-sync"] b. Else, let CONDITIONS = ["node", "require"] 6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH, - `package.json` "exports", CONDITIONS) defined in the ESM resolver. + `package.json` "exports", CONDITIONS) defined in the ESM resolver. 7. RESOLVE_ESM_MATCH(MATCH) LOAD_PACKAGE_SELF(X, DIR) @@ -448,7 +447,7 @@ LOAD_PACKAGE_SELF(X, DIR) 4. If the SCOPE/package.json "name" is not the first segment of X, return. 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE), "." + X.slice("name".length), `package.json` "exports", ["node", "require"]) - defined in the ESM resolver. + defined in the ESM resolver. 6. RESOLVE_ESM_MATCH(MATCH) RESOLVE_ESM_MATCH(MATCH) @@ -458,6 +457,8 @@ RESOLVE_ESM_MATCH(MATCH) 3. THROW "not found" ``` +The "ESM resolver" is defined [in the ESM documentation](esm.md#resolver-algorithm-specification). + ## Caching From 1ddb754d392da419f3c4a96a1520ee84e1019699 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sun, 24 May 2026 22:48:42 +0300 Subject: [PATCH 15/89] test_runner: dont buffer unordered events in process isolation mode Signed-off-by: Moshe Atlow PR-URL: https://github.com/nodejs/node/pull/63432 Reviewed-By: Chemi Atlow Reviewed-By: Benjamin Gruenbaum --- lib/internal/test_runner/runner.js | 9 +++ .../execution-ordered-bypass/fast-fail.mjs | 6 ++ .../execution-ordered-bypass/slow.mjs | 9 +++ .../test-runner-execution-ordered-bypass.mjs | 59 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs create mode 100644 test/fixtures/test-runner/execution-ordered-bypass/slow.mjs create mode 100644 test/parallel/test-runner-execution-ordered-bypass.mjs diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 92b963cd72bcf3..d150943783e975 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -128,6 +128,11 @@ const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', const kCanceledTests = new SafeSet() .add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure); +// Execution-ordered events are forwarded immediately, bypassing the +// per-file declaration-order buffer. +const kExecutionOrderedEvents = new SafeSet() + .add('test:enqueue').add('test:dequeue').add('test:complete'); + let kResistStopPropagation; // Worker ID pool management for concurrent test execution @@ -331,6 +336,10 @@ class FileTest extends Test { } } addToReport(item) { + if (kExecutionOrderedEvents.has(item.type)) { + this.#handleReportItem(item); + return; + } this.#accumulateReportItem(item); if (!this.isClearToSend()) { ArrayPrototypePush(this.#reportBuffer, item); diff --git a/test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs b/test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs new file mode 100644 index 00000000000000..74b77682b6821d --- /dev/null +++ b/test/fixtures/test-runner/execution-ordered-bypass/fast-fail.mjs @@ -0,0 +1,6 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test('fast-fail', () => { + assert.fail('fast'); +}); diff --git a/test/fixtures/test-runner/execution-ordered-bypass/slow.mjs b/test/fixtures/test-runner/execution-ordered-bypass/slow.mjs new file mode 100644 index 00000000000000..4ee60ffe8537e7 --- /dev/null +++ b/test/fixtures/test-runner/execution-ordered-bypass/slow.mjs @@ -0,0 +1,9 @@ +import { test } from 'node:test'; +import { setTimeout as sleep } from 'node:timers/promises'; + +test('slow', async () => { + // Long enough that fast-fail's process can spawn, run, and round-trip its + // bypassed test:complete to the host on slow CI, but short enough that the + // test does not waste much time when the bypass is working. + await sleep(30_000); +}); diff --git a/test/parallel/test-runner-execution-ordered-bypass.mjs b/test/parallel/test-runner-execution-ordered-bypass.mjs new file mode 100644 index 00000000000000..85b468cae2a7e1 --- /dev/null +++ b/test/parallel/test-runner-execution-ordered-bypass.mjs @@ -0,0 +1,59 @@ +// Flags: --no-warnings + +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; +import { test, run } from 'node:test'; + +const files = [ + fixtures.path('test-runner', 'execution-ordered-bypass', 'slow.mjs'), + fixtures.path('test-runner', 'execution-ordered-bypass', 'fast-fail.mjs'), +]; + +test('execution-ordered events bypass FileTest declaration-order buffer', async () => { + // Concurrency must be a number so the runner does not collapse it to 1 on + // single-core CI runners (where `concurrency: true` resolves to + // `availableParallelism() - 1`). Without two slots the runner spawns the + // files sequentially and fast-fail never starts while slow is sleeping. + const stream = run({ + files, + isolation: 'process', + concurrency: 2, + }); + + const events = []; + + stream.on('test:complete', (data) => { + if (data.name === 'slow' || data.name === 'fast-fail') { + events.push(`complete:${data.name}`); + } + }); + + stream.on('test:fail', (data) => { + if (data.name === 'fast-fail') { + events.push(`fail:${data.name}`); + } + }); + + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + + const completeFast = events.indexOf('complete:fast-fail'); + const completeSlow = events.indexOf('complete:slow'); + const failFast = events.indexOf('fail:fast-fail'); + + assert.notStrictEqual(completeFast, -1); + assert.notStrictEqual(completeSlow, -1); + assert.notStrictEqual(failFast, -1); + + assert.ok( + completeFast < completeSlow, + `test:complete for fast-fail should arrive before slow; events=${events.join(', ')}`, + ); + + // test:fail is declaration-ordered, so the bypass must not affect it. + assert.ok( + failFast > completeSlow, + `test:fail for fast-fail should arrive after test:complete for slow; events=${events.join(', ')}`, + ); +}); From c58e5a1d4f2216e922c2171a35537ae1d7a89211 Mon Sep 17 00:00:00 2001 From: Marco Date: Sun, 24 May 2026 23:51:52 +0200 Subject: [PATCH 16/89] tools: skip commit-lint on backport pull requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcopiraccini PR-URL: https://github.com/nodejs/node/pull/63378 Fixes: https://github.com/nodejs/node/issues/63192 Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Luigi Pinca --- .github/workflows/commit-lint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 8f652a91782aea..75ace50d2a071f 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -4,7 +4,6 @@ on: pull_request: branches: - main - - v[0-9]+.x-staging env: NODE_VERSION: lts/* From e98f2ba5a5c082eeb7589683a3c61a9e9c2509bd Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 24 May 2026 16:57:31 -0700 Subject: [PATCH 17/89] test: reduce watch mode restart flakiness Start waiting for each watch restart before writing the file, then wait for a platform-scaled settling period before mutating it again. This gives watch mode time to process filesystem events and dependency reporting from the child process on slower CI machines. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/63390 Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-05-16.md#jstest-failure Reviewed-By: Antoine du Hamel --- .../test-watch-mode-restart-esm-loading-error.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/sequential/test-watch-mode-restart-esm-loading-error.mjs b/test/sequential/test-watch-mode-restart-esm-loading-error.mjs index 42618adaffd386..95a62423160af9 100644 --- a/test/sequential/test-watch-mode-restart-esm-loading-error.mjs +++ b/test/sequential/test-watch-mode-restart-esm-loading-error.mjs @@ -7,6 +7,7 @@ import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; import { inspect } from 'node:util'; import { createInterface } from 'node:readline'; +import { setTimeout as sleep } from 'node:timers/promises'; if (common.isIBMi) common.skip('IBMi does not support `fs.watch()`'); @@ -112,19 +113,21 @@ try { // Update file with syntax error const syntaxErrorContent = `console.log('hello, wor`; + const failedRestart = restart(common.platformTimeout(10_000)); writeFileSync(file, syntaxErrorContent); - + await sleep(common.platformTimeout(1000)); // Wait for the failed restart - const { stderr: stderr2, stdout: stdout2 } = await restart(); + const { stderr: stderr2, stdout: stdout2 } = await failedRestart; assert.match(stderr2, /SyntaxError: Invalid or unexpected token/); assert.deepStrictEqual(stdout2, [ `Restarting ${inspect(file)}`, `Failed running ${inspect(file)}. Waiting for file changes before restarting...`, ]); + const successfulRestart = restart(common.platformTimeout(10_000)); writeFileSync(file, `console.log('hello again, world');`); - - const { stderr: stderr3, stdout: stdout3 } = await restart(); + await sleep(common.platformTimeout(1000)); + const { stderr: stderr3, stdout: stdout3 } = await successfulRestart; // Verify it recovered and ran successfully assert.strictEqual(stderr3, ''); From 1dbea7a20de2c9f77a20488bcec8720a256c7739 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 24 May 2026 16:57:41 -0700 Subject: [PATCH 18/89] test: avoid test_runner watch restart in spec snapshot Pass explicit test files to the watch-mode spec reporter fixture. This prevents setup writes from triggering a watch restart. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/63392 Refs: https://github.com/nodejs/node/actions/runs/25983032903/job/76375266688 Reviewed-By: Antoine du Hamel --- test/fixtures/test-runner/output/test-runner-watch-spec.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/fixtures/test-runner/output/test-runner-watch-spec.mjs b/test/fixtures/test-runner/output/test-runner-watch-spec.mjs index 6c9b575a164dc7..518fcb56526149 100644 --- a/test/fixtures/test-runner/output/test-runner-watch-spec.mjs +++ b/test/fixtures/test-runner/output/test-runner-watch-spec.mjs @@ -33,6 +33,10 @@ const { signal } = controller; const stream = run({ watch: true, cwd: tmpdir.path, + files: [ + fixturePaths['failing-test.js'], + fixturePaths['test.js'], + ], signal, }); From c55b126f09892d636f5dc0fad00d99fd6732a6be Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sun, 24 May 2026 18:29:01 -0700 Subject: [PATCH 19/89] test: deflake async-hooks statwatcher test Wait for the initial missing-file poll before writing watched files. Otherwise uv_fs_poll can observe the created file as its baseline and never emit the expected change event. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/63396 Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-05-15.md#jstest-failure Reviewed-By: Antoine du Hamel --- test/async-hooks/test-statwatcher.js | 33 ++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/async-hooks/test-statwatcher.js b/test/async-hooks/test-statwatcher.js index 70b3d64ba4afe8..d24aab584029d7 100644 --- a/test/async-hooks/test-statwatcher.js +++ b/test/async-hooks/test-statwatcher.js @@ -59,10 +59,32 @@ checkInvocations(statwatcher1, { init: 1 }, 'watcher1: when started to watch second file'); checkWatcherStart('watcher2', statwatcher2); -setTimeout(() => fs.writeFileSync(file1, 'foo++'), - common.platformTimeout(100)); +let w2Initialized = false; +let writeFile2AfterW1 = false; + +const onW2Initialized = (curr, prev) => { + if (curr.nlink !== 0 || prev.nlink !== 0) + return; + + w2.removeListener('change', onW2Initialized); + w2Initialized = true; + if (writeFile2AfterW1) { + setTimeout(() => fs.writeFileSync(file2, 'bar++'), + common.platformTimeout(100)); + } +}; +w2.on('change', onW2Initialized); + w1.on('change', common.mustCallAtLeast((curr, prev) => { console.log('w1 change to', curr, 'from', prev); + // Wait for the initial ENOENT poll before creating the file. Otherwise the + // first stat can race with the write and use the created file as the baseline. + if (curr.nlink === 0 && prev.nlink === 0) { + setTimeout(() => fs.writeFileSync(file1, 'foo++'), + common.platformTimeout(100)); + return; + } + // Wait until we get the write above. if (prev.size !== 0 || curr.size !== 5) return; @@ -74,8 +96,11 @@ w1.on('change', common.mustCallAtLeast((curr, prev) => { checkInvocations(statwatcher2, { init: 1 }, 'watcher2: when unwatched first file'); - setTimeout(() => fs.writeFileSync(file2, 'bar++'), - common.platformTimeout(100)); + writeFile2AfterW1 = true; + if (w2Initialized) { + setTimeout(() => fs.writeFileSync(file2, 'bar++'), + common.platformTimeout(100)); + } w2.on('change', common.mustCallAtLeast((curr, prev) => { console.log('w2 change to', curr, 'from', prev); // Wait until we get the write above. From 052017445bbcf339ab670c4bd27d2732d4234c03 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 16 May 2026 21:41:09 -0700 Subject: [PATCH 20/89] quic: add doc note about certificate size limitations Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/63483 Reviewed-By: Stephen Belanger Reviewed-By: Matteo Collina --- doc/api/quic.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/doc/api/quic.md b/doc/api/quic.md index ad400bf225b974..eb53f0e55a7e5b 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -104,6 +104,42 @@ to negotiate the application protocol (via ALPN), authenticate the server (and optionally the client), exchange transport parameters, and establish shared keys for encryption. +#### Certificate size and handshake performance + +QUIC includes an anti-amplification limit ([RFC 9000 Section 8.1][]) that +restricts the server to sending at most three times the data received from +the client before the client's address is validated. Because the client's +Initial packet is typically around 1200 bytes, the server can send at most +approximately 3600 bytes before it must wait for the client to acknowledge. + +The server's initial response is dominated by its TLS certificate chain. If +the certificate chain exceeds the amplification limit, the handshake requires +an additional round trip — the server must pause, wait for the client's +acknowledgement, and then continue sending the remainder of the certificate. +This eliminates QUIC's 1-RTT handshake advantage over TCP+TLS and can add +50–100 ms or more of latency on the first connection, depending on the network +path. + +To avoid this, servers should use compact certificate chains: + +* **Use ECDSA certificates** (P-256 or P-384) rather than RSA. ECDSA keys and + signatures are significantly smaller. A typical ECDSA P-256 certificate chain + with one intermediate is approximately 1.5–2 KB, well within the amplification + limit. An equivalent RSA-2048 chain is often 3–5 KB, which may exceed it. + +* **Minimize the certificate chain.** Include only the leaf certificate and + the necessary intermediate(s). Do not include the root certificate (clients + already have it in their trust store). Avoid cross-signed intermediates when + the self-signed root is already widely trusted. + +* **Prefer certificate authorities with short chains.** Some CAs issue + certificates with a single small intermediate, while others require multiple + large RSA intermediates. The choice of CA directly affects handshake latency. + +Certificate compression ([RFC 8879][]) can also address this issue by +compressing the certificate chain during the handshake. However, Node.js does +not currently support TLS certificate compression. + ### Applications Every `QuicSession` is associated with a single application protocol, negotiated @@ -4063,8 +4099,10 @@ throughput issues caused by flow control. [JSON-SEQ]: https://www.rfc-editor.org/rfc/rfc7464 [NSS Key Log Format]: https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format [Permission Model]: permissions.md#permission-model +[RFC 8879]: https://www.rfc-editor.org/rfc/rfc8879 [RFC 8999]: https://www.rfc-editor.org/rfc/rfc8999 [RFC 9000]: https://www.rfc-editor.org/rfc/rfc9000 +[RFC 9000 Section 8.1]: https://www.rfc-editor.org/rfc/rfc9000#section-8.1 [RFC 9001]: https://www.rfc-editor.org/rfc/rfc9001 [RFC 9002]: https://www.rfc-editor.org/rfc/rfc9002 [RFC 9114]: https://www.rfc-editor.org/rfc/rfc9114 From 444ba160e3581b28762d1d456dd6d7fc86de5d18 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 16 May 2026 22:05:16 -0700 Subject: [PATCH 21/89] quic: flip preferred address policy default to 'ignore' Signed-off-by: James M Snell Assisted-by: Opencode:Opus 4.6 PR-URL: https://github.com/nodejs/node/pull/63483 Reviewed-By: Stephen Belanger Reviewed-By: Matteo Collina --- doc/api/quic.md | 8 +++++++- lib/internal/quic/quic.js | 2 +- src/quic/preferredaddress.cc | 2 +- .../test-quic-callback-error-onpathvalidation.mjs | 1 + test/parallel/test-quic-diagnostics-channel-path.mjs | 1 + .../parallel/test-quic-session-preferred-address-ipv6.mjs | 1 + test/parallel/test-quic-session-preferred-address.mjs | 1 + 7 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index eb53f0e55a7e5b..e14d82fd482556 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -2782,9 +2782,15 @@ added: v23.8.0 --> * Type: {string} One of `'use'`, `'ignore'`, or `'default'`. +* **Default:** `'ignore'` When the remote peer advertises a preferred address, this option specifies whether -to use it or ignore it. +to use it or ignore it. The default is `'ignore'` because honoring a server's +preferred address causes the client to migrate its connection to a different IP +address, which can be exploited for data exfiltration attacks that are +indistinguishable from legitimate QUIC connection migration at the network level. +Set to `'use'` only when connecting to trusted servers that require preferred +address migration. #### `sessionOptions.qlog` diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index a137f04a417a73..d346078b79c07f 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -4919,7 +4919,7 @@ function processSessionOptions(options, config = kEmptyObject) { reuseEndpoint = true, version, minVersion, - preferredAddressPolicy = 'default', + preferredAddressPolicy = 'ignore', transportParams = kEmptyObject, qlog = false, sessionTicket, diff --git a/src/quic/preferredaddress.cc b/src/quic/preferredaddress.cc index ca7908dda35c59..7ce27798a63423 100644 --- a/src/quic/preferredaddress.cc +++ b/src/quic/preferredaddress.cc @@ -140,7 +140,7 @@ void PreferredAddress::Initialize(Environment* env, Local target) { static constexpr auto PREFERRED_ADDRESS_IGNORE = static_cast(Policy::IGNORE_PREFERRED); static constexpr auto DEFAULT_PREFERRED_ADDRESS_POLICY = - static_cast(Policy::USE_PREFERRED); + static_cast(Policy::IGNORE_PREFERRED); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_IGNORE); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_USE); diff --git a/test/parallel/test-quic-callback-error-onpathvalidation.mjs b/test/parallel/test-quic-callback-error-onpathvalidation.mjs index e4f4cb4de8b14f..d60033e3efe304 100644 --- a/test/parallel/test-quic-callback-error-onpathvalidation.mjs +++ b/test/parallel/test-quic-callback-error-onpathvalidation.mjs @@ -36,6 +36,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, + preferredAddressPolicy: 'use', onpathvalidation() { throw testError; }, diff --git a/test/parallel/test-quic-diagnostics-channel-path.mjs b/test/parallel/test-quic-diagnostics-channel-path.mjs index a5464c07076101..63305025e6efbf 100644 --- a/test/parallel/test-quic-diagnostics-channel-path.mjs +++ b/test/parallel/test-quic-diagnostics-channel-path.mjs @@ -47,6 +47,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, + preferredAddressPolicy: 'use', // The onpathvalidation must be set for the JS handler to fire, // which in turn publishes to the diagnostics channel. onpathvalidation: mustCall(), diff --git a/test/parallel/test-quic-session-preferred-address-ipv6.mjs b/test/parallel/test-quic-session-preferred-address-ipv6.mjs index 3eda4a0b04a678..e9f23c3bf554d5 100644 --- a/test/parallel/test-quic-session-preferred-address-ipv6.mjs +++ b/test/parallel/test-quic-session-preferred-address-ipv6.mjs @@ -81,6 +81,7 @@ console.log(serverEndpoint.address); const clientSession = await connect(serverEndpoint.address, { // We don't want this endpoint to reuse either of the two listening endpoints. reuseEndpoint: false, + preferredAddressPolicy: 'use', transportParams: { maxDatagramFrameSize: 1200 }, ondatagramstatus: mustCall((id, status) => { if (++statusCount >= 4) allStatusDone.resolve(); diff --git a/test/parallel/test-quic-session-preferred-address.mjs b/test/parallel/test-quic-session-preferred-address.mjs index 59858649bec29a..c4a55ee4d42b74 100644 --- a/test/parallel/test-quic-session-preferred-address.mjs +++ b/test/parallel/test-quic-session-preferred-address.mjs @@ -65,6 +65,7 @@ const serverEndpoint = await listen(handleSession, { const clientSession = await connect(serverEndpoint.address, { // We don't want this endpoint to reuse either of the two listening endpoints. reuseEndpoint: false, + preferredAddressPolicy: 'use', transportParams: { maxDatagramFrameSize: 1200 }, ondatagramstatus: mustCall((id, status) => { if (++statusCount >= 4) allStatusDone.resolve(); From e88346666ff07044c61575c43f0576cee66c6e69 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 17 May 2026 04:28:46 -0700 Subject: [PATCH 22/89] quic: refine rate limiting Signed-off-by: James M Snell Assisted-by: Opencode:Opus 4.6 PR-URL: https://github.com/nodejs/node/pull/63483 Reviewed-By: Stephen Belanger Reviewed-By: Matteo Collina --- doc/api/quic.md | 100 +++++++++-- lib/internal/quic/quic.js | 30 +++- lib/internal/quic/stats.js | 48 +++++ node.gyp | 1 + src/quic/bindingdata.h | 10 +- src/quic/defs.h | 18 ++ src/quic/endpoint.cc | 165 +++++++++--------- src/quic/endpoint.h | 93 ++++------ test/cctest/test_quic_tokenbucket.cc | 77 ++++++++ .../test-quic-internal-endpoint-options.mjs | 48 +++-- ...est-quic-internal-endpoint-stats-state.mjs | 8 + test/parallel/test-quic-stateless-reset.mjs | 18 +- 12 files changed, 433 insertions(+), 183 deletions(-) create mode 100644 test/cctest/test_quic_tokenbucket.cc diff --git a/doc/api/quic.md b/doc/api/quic.md index e14d82fd482556..5d2f118c4721d0 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -746,7 +746,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only. +* Type: {bigint} The total number of retry packets sent by this endpoint. Read only. + +### `endpointStats.retryRateLimited` + +* Type: {bigint} The total number of retry packets dropped by the global rate + limiter. Read only. A non-zero value indicates the endpoint is under retry + flood pressure. ### `endpointStats.versionNegotiationCount` @@ -754,7 +760,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only. +* Type: {bigint} The total number of version negotiation packets sent by this + endpoint. Read only. + +### `endpointStats.versionNegotiationRateLimited` + +* Type: {bigint} The total number of version negotiation packets dropped by + the global rate limiter. Read only. ### `endpointStats.statelessResetCount` @@ -762,7 +774,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only. +* Type: {bigint} The total number of stateless reset packets sent by this + endpoint. Read only. + +### `endpointStats.statelessResetRateLimited` + +* Type: {bigint} The total number of stateless reset packets dropped by the + global rate limiter. Read only. ### `endpointStats.immediateCloseCount` @@ -770,7 +788,13 @@ added: v23.8.0 added: v23.8.0 --> -* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only. +* Type: {bigint} The total number of immediate connection close packets sent + by this endpoint. Read only. + +### `endpointStats.immediateCloseRateLimited` + +* Type: {bigint} The total number of immediate connection close packets + dropped by the global rate limiter. Read only. ## Class: `QuicSession` @@ -2475,25 +2499,69 @@ addresses. When the limit is reached, new connections are refused with This limit can also be changed dynamically after construction via [`endpoint.maxConnectionsTotal`][]. -#### `endpointOptions.maxRetries` +#### `endpointOptions.retryRate` - +* Type: {number} +* **Default:** `100` -* Type: {bigint|number} +The maximum number of QUIC retry packets the endpoint will send per second. +This is a global rate limit (not per-host) that caps the total server-wide +retry response rate, preventing spoofed-source floods from consuming unbounded +resources. -Specifies the maximum number of QUIC retry attempts allowed per remote peer address. +#### `endpointOptions.retryBurst` -#### `endpointOptions.maxStatelessResetsPerHost` +* Type: {number} +* **Default:** `200` - +The maximum burst of retry packets allowed before rate limiting takes effect. -* Type: {bigint|number} +#### `endpointOptions.statelessResetRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of stateless reset packets the endpoint will send per second. + +#### `endpointOptions.statelessResetBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of stateless reset packets allowed before rate limiting +takes effect. + +#### `endpointOptions.versionNegotiationRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of version negotiation packets the endpoint will send per +second. + +#### `endpointOptions.versionNegotiationBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of version negotiation packets allowed before rate limiting +takes effect. + +#### `endpointOptions.immediateCloseRate` + +* Type: {number} +* **Default:** `100` + +The maximum number of immediate connection close packets the endpoint will +send per second. + +#### `endpointOptions.immediateCloseBurst` + +* Type: {number} +* **Default:** `200` -Specifies the maximum number of stateless resets that are allowed per remote peer address. +The maximum burst of immediate connection close packets allowed before rate +limiting takes effect. #### `endpointOptions.retryTokenExpiration` diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index d346078b79c07f..2117ea9c58bf2f 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet(); * @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing * @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections - * @property {bigint|number} [maxRetries] The maximum number of retries - * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host + * @property {number} [retryRate] Global rate limit for retry packets (per second) + * @property {number} [retryBurst] Burst capacity for retry rate limiter + * @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second) + * @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter + * @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second) + * @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter + * @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second) + * @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter * @property {ArrayBufferView} [resetTokenSecret] The reset token secret * @property {bigint|number} [retryTokenExpiration] The retry token expiration * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) @@ -3997,10 +4003,16 @@ class QuicEndpoint { tokenExpiration, maxConnectionsPerHost = 100, maxConnectionsTotal = 10_000, - maxStatelessResetsPerHost, disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, @@ -4034,10 +4046,16 @@ class QuicEndpoint { // Connection limits are set on the state buffer, not passed to C++. maxConnectionsPerHost, maxConnectionsTotal, - maxStatelessResetsPerHost, disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, diff --git a/lib/internal/quic/stats.js b/lib/internal/quic/stats.js index b6500e6700713e..39e5f51f465717 100644 --- a/lib/internal/quic/stats.js +++ b/lib/internal/quic/stats.js @@ -60,9 +60,13 @@ const { IDX_STATS_ENDPOINT_CLIENT_SESSIONS, IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, IDX_STATS_ENDPOINT_RETRY_COUNT, + IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED, IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, + IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED, IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, + IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED, IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, + IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED, IDX_STATS_SESSION_CREATED_AT, IDX_STATS_SESSION_DESTROYED_AT, @@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined); assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS !== undefined); assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT !== undefined); assert(IDX_STATS_ENDPOINT_RETRY_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED !== undefined); assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined); +assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED !== undefined); assert(IDX_STATS_SESSION_CREATED_AT !== undefined); assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined); assert(IDX_STATS_SESSION_CLOSING_AT !== undefined); @@ -280,24 +288,48 @@ class QuicEndpointStats { return this.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT]; } + /** @type {bigint} */ + get retryRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED]; + } + /** @type {bigint} */ get versionNegotiationCount() { assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT]; } + /** @type {bigint} */ + get versionNegotiationRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED]; + } + /** @type {bigint} */ get statelessResetCount() { assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT]; } + /** @type {bigint} */ + get statelessResetRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED]; + } + /** @type {bigint} */ get immediateCloseCount() { assertIsQuicEndpointStats(this); return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT]; } + /** @type {bigint} */ + get immediateCloseRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED]; + } + toString() { return JSONStringify(this.toJSON()); } @@ -315,9 +347,13 @@ class QuicEndpointStats { clientSessions, serverBusyCount, retryCount, + retryRateLimited, versionNegotiationCount, + versionNegotiationRateLimited, statelessResetCount, + statelessResetRateLimited, immediateCloseCount, + immediateCloseRateLimited, } = this; return { __proto__: null, @@ -334,9 +370,13 @@ class QuicEndpointStats { clientSessions: `${clientSessions}`, serverBusyCount: `${serverBusyCount}`, retryCount: `${retryCount}`, + retryRateLimited: `${retryRateLimited}`, versionNegotiationCount: `${versionNegotiationCount}`, + versionNegotiationRateLimited: `${versionNegotiationRateLimited}`, statelessResetCount: `${statelessResetCount}`, + statelessResetRateLimited: `${statelessResetRateLimited}`, immediateCloseCount: `${immediateCloseCount}`, + immediateCloseRateLimited: `${immediateCloseRateLimited}`, }; } @@ -363,9 +403,13 @@ class QuicEndpointStats { clientSessions, serverBusyCount, retryCount, + retryRateLimited, versionNegotiationCount, + versionNegotiationRateLimited, statelessResetCount, + statelessResetRateLimited, immediateCloseCount, + immediateCloseRateLimited, } = this; return `QuicEndpointStats ${inspect({ @@ -380,9 +424,13 @@ class QuicEndpointStats { clientSessions, serverBusyCount, retryCount, + retryRateLimited, versionNegotiationCount, + versionNegotiationRateLimited, statelessResetCount, + statelessResetRateLimited, immediateCloseCount, + immediateCloseRateLimited, }, opts)}`; } diff --git a/node.gyp b/node.gyp index 1a724ccb771342..9c6d44d90bbba1 100644 --- a/node.gyp +++ b/node.gyp @@ -463,6 +463,7 @@ 'test/cctest/test_quic_cid.cc', 'test/cctest/test_quic_error.cc', 'test/cctest/test_quic_preferredaddress.cc', + 'test/cctest/test_quic_tokenbucket.cc', 'test/cctest/test_quic_tokens.cc', ], 'node_cctest_inspector_sources': [ diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index c3c5e9fc7834df..dc14ad8ce960d4 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -117,8 +117,14 @@ class SessionManager; V(idle_timeout, "idleTimeout") \ V(max_idle_timeout, "maxIdleTimeout") \ V(max_payload_size, "maxPayloadSize") \ - V(max_retries, "maxRetries") \ - V(max_stateless_resets, "maxStatelessResetsPerHost") \ + V(retry_rate, "retryRate") \ + V(retry_burst, "retryBurst") \ + V(stateless_reset_rate, "statelessResetRate") \ + V(stateless_reset_burst, "statelessResetBurst") \ + V(version_negotiation_rate, "versionNegotiationRate") \ + V(version_negotiation_burst, "versionNegotiationBurst") \ + V(immediate_close_rate, "immediateCloseRate") \ + V(immediate_close_burst, "immediateCloseBurst") \ V(max_stream_window, "maxStreamWindow") \ V(max_window, "maxWindow") \ V(min_version, "minVersion") \ diff --git a/src/quic/defs.h b/src/quic/defs.h index 6b18c19f4c3c6d..8e4bce73337239 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS; constexpr size_t kMaxVectorCount = 16; constexpr stream_id kMaxStreamId = std::numeric_limits::max(); +// A token bucket rate limiter using lazy refill. No timer needed — tokens +// are computed on demand from the elapsed time since the last check. +// Used to cap the total rate of stateless responses (retry, reset, +// version negotiation, immediate close) regardless of source address, +// preventing spoofed-source floods from bypassing per-host limits. +struct TokenBucket final { + double rate; // tokens per second (refill rate) + double burst; // maximum tokens (bucket capacity) + double tokens; // current token count + uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime) + + TokenBucket(double rate, double burst); + + // Try to consume one token. Refills based on elapsed time, then + // attempts to consume. Returns true if the request is allowed. + bool consume(); +}; + class DebugIndentScope final { public: inline DebugIndentScope() { ++indent_; } diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 1a72113dc7bea4..615de3b01ebeb9 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -73,9 +73,13 @@ namespace quic { V(CLIENT_SESSIONS, client_sessions) \ V(SERVER_BUSY_COUNT, server_busy_count) \ V(RETRY_COUNT, retry_count) \ + V(RETRY_RATE_LIMITED, retry_rate_limited) \ V(VERSION_NEGOTIATION_COUNT, version_negotiation_count) \ + V(VERSION_NEGOTIATION_RATE_LIMITED, version_negotiation_rate_limited) \ V(STATELESS_RESET_COUNT, stateless_reset_count) \ - V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) + V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \ + V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \ + V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) struct Endpoint::State { #define V(_, name, type) type name; @@ -85,6 +89,23 @@ struct Endpoint::State { STAT_STRUCT(Endpoint, ENDPOINT) +TokenBucket::TokenBucket(double rate, double burst) + : rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {} + +// Try to consume one token. Refills based on elapsed time, then +// attempts to consume. Returns true if the request is allowed. +bool TokenBucket::consume() { + uint64_t now = uv_hrtime(); + double elapsed = static_cast(now - last_ts) / 1e9; // seconds + last_ts = now; + tokens = std::min(burst, tokens + elapsed * rate); + if (tokens >= 1.0) { + tokens -= 1.0; + return true; + } + return false; +} + // ============================================================================ // Endpoint::Options namespace { @@ -97,6 +118,7 @@ bool is_diagnostic_packet_loss(double probability) { CHECK(ncrypto::CSPRNG(&c, 1)); return (static_cast(c) / 255) < probability; } +#endif // DEBUG template bool SetOption(Environment* env, @@ -106,18 +128,24 @@ bool SetOption(Environment* env, Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - Local num; - if (!value->ToNumber(env->context()).ToLocal(&num)) { + if (!value->IsNumber()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be a number", nameStr); return false; } - options->*member = num->Value(); + Local num = value.As(); + double dbl = num->Value(); + if (dbl < 0) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be a non-negative number", nameStr); + return false; + } + options->*member = dbl; } return true; } -#endif // DEBUG template bool SetOption(Environment* env, @@ -133,15 +161,15 @@ bool SetOption(Environment* env, env, "The %s option must be an uint8", nameStr); return false; } - Local num; - if (!value->ToUint32(env->context()).ToLocal(&num) || - num->Value() > std::numeric_limits::max()) { + Local num = value.As(); + uint32_t val = num->Value(); + if (val > std::numeric_limits::max()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an uint8", nameStr); return false; } - options->*member = num->Value(); + options->*member = val; } return true; } @@ -195,9 +223,12 @@ Maybe Endpoint::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(retry_token_expiration) || !SET(token_expiration) || - !SET(max_stateless_resets) || !SET(address_lru_size) || - !SET(max_retries) || !SET(validate_address) || + !SET(address_lru_size) || !SET(validate_address) || !SET(disable_stateless_reset) || !SET(ipv6_only) || !SET(reuse_port) || + !SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) || + !SET(stateless_reset_burst) || !SET(version_negotiation_rate) || + !SET(version_negotiation_burst) || !SET(immediate_close_rate) || + !SET(immediate_close_burst) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif @@ -251,10 +282,21 @@ std::string Endpoint::Options::ToString() const { " seconds"; res += prefix + "token expiration: " + std::to_string(token_expiration) + " seconds"; - res += - prefix + "max stateless resets: " + std::to_string(max_stateless_resets); res += prefix + "address lru size: " + std::to_string(address_lru_size); - res += prefix + "max retries: " + std::to_string(max_retries); + res += prefix + "retry rate: " + std::to_string(retry_rate) + "/s"; + res += prefix + "retry burst: " + std::to_string(retry_burst); + res += prefix + + "stateless reset rate: " + std::to_string(stateless_reset_rate) + "/s"; + res += prefix + + "stateless reset burst: " + std::to_string(stateless_reset_burst); + res += prefix + "version negotiation rate: " + + std::to_string(version_negotiation_rate) + "/s"; + res += prefix + "version negotiation burst: " + + std::to_string(version_negotiation_burst); + res += prefix + + "immediate close rate: " + std::to_string(immediate_close_rate) + "/s"; + res += prefix + + "immediate close burst: " + std::to_string(immediate_close_burst); res += prefix + "validate address: " + boolToString(validate_address); res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); @@ -580,8 +622,6 @@ void Endpoint::InitPerContext(Realm* realm, Local target) { #undef V NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS); - NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT); static constexpr auto DEFAULT_RETRYTOKEN_EXPIRATION = RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; @@ -643,7 +683,14 @@ Endpoint::Endpoint(Environment* env, HandleScope scope(this->env()->isolate()); Destroy(); }), - addr_validation_lru_(options_.address_lru_size) { + addr_validation_lru_(options_.address_lru_size), + retry_bucket_(options_.retry_rate, options_.retry_burst), + stateless_reset_bucket_(options_.stateless_reset_rate, + options_.stateless_reset_burst), + version_negotiation_bucket_(options_.version_negotiation_rate, + options_.version_negotiation_burst), + immediate_close_bucket_(options_.immediate_close_rate, + options_.immediate_close_burst) { MakeWeak(); udp_.Unref(); idle_timer_.Unref(); @@ -964,55 +1011,31 @@ void Endpoint::SendBatch(Packet::Ptr* packets, size_t count) { } void Endpoint::SendRetry(const PathDescriptor& options) { - // Generating and sending retry packets does consume some system resources, - // and it is possible for a malicious peer to trigger sending a large number - // of retry packets, resulting in a potential DOS vector. To help ward that - // off, we track how many retry packets we send to a particular host and - // enforce limits. Note that since we are using an LRU cache these limits - // aren't strict. If a retry is sent, we increment the retry_count statistic - // to give application code a means of detecting and responding to abuse on - // its own. What this count does not give is the rate of retry, so it is still - // somewhat limited. Debug(this, "Sending retry on path %s", options); - auto info = addr_validation_lru_.Upsert(options.remote_address); - if (++(info->retry_count) <= options_.max_retries) { - auto packet = - Packet::CreateRetryPacket(*this, options, options_.token_secret); - if (packet) { - STAT_INCREMENT(Stats, retry_count); - Send(std::move(packet)); - } + if (!retry_bucket_.consume()) { + Debug(this, "Retry rate limit exceeded (global)"); + STAT_INCREMENT(Stats, retry_rate_limited); + return; + } - // If creating the retry is unsuccessful, we just drop things on the floor. - // It's not worth committing any further resources to this one packet. We - // might want to log the failure at some point tho. + auto packet = + Packet::CreateRetryPacket(*this, options, options_.token_secret); + if (packet) { + STAT_INCREMENT(Stats, retry_count); + Send(std::move(packet)); } } void Endpoint::SendVersionNegotiation(const PathDescriptor& options) { Debug(this, "Sending version negotiation on path %s", options); - // A malicious peer can trivially force version negotiation packets by - // sending packets with unsupported QUIC versions, potentially from - // spoofed source addresses. Rate-limit per remote host to prevent - // amplification attacks. - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addr_validation_lru_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->version_negotiation_count : 0; - return count >= kMaxVersionNegotiations; - }; - - if (exceeds_limits()) { - Debug(this, - "Version negotiation rate limit exceeded for %s", - options.remote_address); + if (!version_negotiation_bucket_.consume()) { + Debug(this, "Version negotiation rate limit exceeded (global)"); + STAT_INCREMENT(Stats, version_negotiation_rate_limited); return; } auto packet = Packet::CreateVersionNegotiationPacket(*this, options); if (packet) { - addr_validation_lru_.Upsert(options.remote_address) - ->version_negotiation_count++; STAT_INCREMENT(Stats, version_negotiation_count); Send(std::move(packet)); } @@ -1028,17 +1051,9 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, options, source_len); - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addr_validation_lru_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->reset_count : 0; - return count >= options_.max_stateless_resets; - }; - - // Per the QUIC spec, we need to protect against sending too many stateless - // reset tokens to an endpoint to prevent endless looping. - if (exceeds_limits()) { - Debug(this, "Stateless reset rate limit exceeded"); + if (!stateless_reset_bucket_.consume()) { + Debug(this, "Stateless reset rate limit exceeded (global)"); + STAT_INCREMENT(Stats, stateless_reset_rate_limited); return false; } @@ -1047,7 +1062,6 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, if (packet) { Debug(this, "Sending stateless reset packet (%zu bytes)", packet->length()); - addr_validation_lru_.Upsert(options.remote_address)->reset_count++; STAT_INCREMENT(Stats, stateless_reset_count); Send(std::move(packet)); return true; @@ -1062,28 +1076,15 @@ void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, "Sending immediate connection close on path %s with reason %s", options, reason); - // A malicious peer can trigger immediate connection close packets by - // sending Initial packets with invalid tokens or when the server is - // busy. Rate-limit per remote host to prevent amplification attacks. - const auto exceeds_limits = [&] { - SocketAddressInfoTraits::Type* counts = - addr_validation_lru_.Peek(options.remote_address); - auto count = counts != nullptr ? counts->immediate_close_count : 0; - return count >= kMaxImmediateCloses; - }; - - if (exceeds_limits()) { - Debug(this, - "Immediate connection close rate limit exceeded for %s", - options.remote_address); + if (!immediate_close_bucket_.consume()) { + Debug(this, "Immediate connection close rate limit exceeded (global)"); + STAT_INCREMENT(Stats, immediate_close_rate_limited); return; } auto packet = Packet::CreateImmediateConnectionClosePacket(*this, options, reason); if (packet) { - addr_validation_lru_.Upsert(options.remote_address) - ->immediate_close_count++; STAT_INCREMENT(Stats, immediate_close_count); Send(std::move(packet)); } diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index da2ea253dd8d88..a7bc69f1403bea 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -29,37 +29,20 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // The socket address LRU is used for tracking validated remote addresses. static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = 1024; - // The max stateless resets is the maximum number of stateless reset packets - // that the Endpoint will generate for a given remote host within a window of - // time (while tracking that host in the socket address LRU). This is not - // mandated by QUIC, and the limit is arbitrary. We can set it to whatever - // we'd like. The purpose is to prevent a malicious peer from intentionally - // triggering generation of a large number of stateless resets. Once the - // limit is reached, packets that would have otherwise triggered generation - // of a stateless reset will simply be dropped instead. - static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10; - - // Similar to stateless resets, the max retry limit is the maximum number of - // retry packets that the Endpoint will generate for a given remote host - // within a window of time (while tracking that host in the socket address - // LRU). This is not mandated by QUIC, and the limit is arbitrary. We can set - // it to whatever we'd like. The purpose is to prevent a malicious peer from - // intentionally triggering generation of a large number of retries. - static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10; - - // Maximum number of version negotiation packets that will be sent to a - // given remote host within the LRU tracking window. Version negotiation - // packets are cheap to generate but can be used as an amplification - // vector with spoofed source addresses. - // TODO(@jasnell): Consider making this configurable via Endpoint::Options. - static constexpr uint64_t kMaxVersionNegotiations = 10; - - // Maximum number of immediate connection close packets that will be sent - // to a given remote host within the LRU tracking window. These are sent - // when the server is busy or a token is invalid — a malicious peer could - // trigger a large number of them. - // TODO(@jasnell): Consider making this configurable via Endpoint::Options. - static constexpr uint64_t kMaxImmediateCloses = 10; + // Default rate limits for stateless responses. These are global token + // bucket limits that cap the total rate of each response type regardless + // of source address. This prevents spoofed-source floods from bypassing + // per-host limits (which are keyed by source IP and trivially defeated + // by rotating spoofed addresses). The rate is in responses per second + // and the burst is the maximum tokens the bucket can hold. + static constexpr double DEFAULT_RETRY_RATE = 100; + static constexpr double DEFAULT_RETRY_BURST = 200; + static constexpr double DEFAULT_STATELESS_RESET_RATE = 100; + static constexpr double DEFAULT_STATELESS_RESET_BURST = 200; + static constexpr double DEFAULT_VERSION_NEGOTIATION_RATE = 100; + static constexpr double DEFAULT_VERSION_NEGOTIATION_BURST = 200; + static constexpr double DEFAULT_IMMEDIATE_CLOSE_RATE = 100; + static constexpr double DEFAULT_IMMEDIATE_CLOSE_BURST = 200; // Endpoint configuration options struct Options final : public MemoryRetainer { @@ -83,30 +66,22 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { uint64_t token_expiration = RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS; - // A stateless reset in QUIC is a discrete mechanism that one endpoint can - // use to communicate to a peer that it has lost whatever state it - // previously held about a session. Because generating a stateless reset - // consumes resources (even very modestly), they can be a DOS vector in - // which a malicious peer intentionally sends a large number of stateless - // reset eliciting packets. To protect against that risk, we limit the - // number of stateless resets that may be generated for a given remote host - // within a window of time. This is not mandated by QUIC, and the limit is - // arbitrary. We can set it to whatever we'd like. - uint64_t max_stateless_resets = DEFAULT_MAX_STATELESS_RESETS; - - // For tracking the number of connections per host, the number of stateless - // resets that have been sent, and tracking the path verification status of - // a remote host, we maintain an LRU cache of the most recently seen hosts. - // The address_lru_size parameter determines the size of that cache. The - // default is set modestly at 10 times the default max connections per host. + // For tracking the path verification status of remote hosts, we maintain + // an LRU cache of the most recently seen hosts. uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE; - // Similar to stateless resets, we enforce a limit on the number of retry - // packets that can be generated and sent for a remote host. Generating - // retry packets consumes a modest amount of resources and it's fairly - // trivial for a malicious peer to trigger generation of a large number of - // retries, so limiting them helps prevent a DOS vector. - uint64_t max_retries = DEFAULT_MAX_RETRY_LIMIT; + // Global token bucket rate limits for stateless responses. These cap + // the total rate of each response type regardless of source address, + // preventing spoofed-source floods. Rate is in responses per second, + // burst is the maximum number of responses that can be sent in a burst. + double retry_rate = DEFAULT_RETRY_RATE; + double retry_burst = DEFAULT_RETRY_BURST; + double stateless_reset_rate = DEFAULT_STATELESS_RESET_RATE; + double stateless_reset_burst = DEFAULT_STATELESS_RESET_BURST; + double version_negotiation_rate = DEFAULT_VERSION_NEGOTIATION_RATE; + double version_negotiation_burst = DEFAULT_VERSION_NEGOTIATION_BURST; + double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE; + double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST; // The validate_address parameter instructs the Endpoint to perform explicit // address validation using retry tokens. This is strongly recommended and @@ -475,10 +450,6 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { struct SocketAddressInfoTraits final { struct Type final { - size_t reset_count; - size_t retry_count; - size_t version_negotiation_count; - size_t immediate_close_count; uint64_t timestamp; bool validated; }; @@ -489,6 +460,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { SocketAddressLRU addr_validation_lru_; + // Global token buckets for stateless response rate limiting. + // These cap the total server-wide rate of each response type, + // regardless of source address. + TokenBucket retry_bucket_; + TokenBucket stateless_reset_bucket_; + TokenBucket version_negotiation_bucket_; + TokenBucket immediate_close_bucket_; + // Per-IP connection counts for maxConnectionsPerHost enforcement. // Only populated when max_connections_per_host > 0. Entries are // added in AddSession and removed when the count reaches 0 in diff --git a/test/cctest/test_quic_tokenbucket.cc b/test/cctest/test_quic_tokenbucket.cc new file mode 100644 index 00000000000000..aef3d9fa418da6 --- /dev/null +++ b/test/cctest/test_quic_tokenbucket.cc @@ -0,0 +1,77 @@ +#if HAVE_OPENSSL && HAVE_QUIC +#include "quic/guard.h" +#ifndef OPENSSL_NO_QUIC +#include +#include +#include +#include + +namespace node::quic { +namespace { + +TEST(QuicTokenBucket, AllowsBurst) { + TokenBucket bucket(10, 5); // 10/sec rate, burst of 5 + + // Should allow up to burst count immediately + for (int i = 0; i < 5; i++) { + EXPECT_TRUE(bucket.consume()) << "consume " << i << " should succeed"; + } + + // Bucket should be empty now + EXPECT_FALSE(bucket.consume()); +} + +TEST(QuicTokenBucket, RefillsOverTime) { + TokenBucket bucket(1000, 1); // 1000/sec rate, burst of 1 + + // Drain the bucket + EXPECT_TRUE(bucket.consume()); + EXPECT_FALSE(bucket.consume()); + + // Sleep 2ms — at 1000/sec that's ~2 tokens, but burst is 1 + uv_sleep(2); + + // Should have refilled to at least 1 (capped by burst) + EXPECT_TRUE(bucket.consume()); +} + +TEST(QuicTokenBucket, BurstCapacity) { + TokenBucket bucket(10000, 3); // high rate, burst of 3 + + // Drain + EXPECT_TRUE(bucket.consume()); + EXPECT_TRUE(bucket.consume()); + EXPECT_TRUE(bucket.consume()); + EXPECT_FALSE(bucket.consume()); + + // Sleep enough to fully refill + uv_sleep(10); + + // Should be capped at burst (3), not more + int count = 0; + while (bucket.consume()) count++; + EXPECT_EQ(count, 3); +} + +TEST(QuicTokenBucket, ZeroRateAlwaysDenies) { + TokenBucket bucket(0, 0); + EXPECT_FALSE(bucket.consume()); + EXPECT_FALSE(bucket.consume()); +} + +TEST(QuicTokenBucket, HighRateAllowsRapidConsume) { + TokenBucket bucket(1000000, 1000); // 1M/sec, burst 1000 + + // Should be able to consume many quickly + int count = 0; + for (int i = 0; i < 1000; i++) { + if (bucket.consume()) count++; + } + EXPECT_EQ(count, 1000); +} + +} // namespace +} // namespace node::quic + +#endif // OPENSSL_NO_QUIC +#endif // HAVE_OPENSSL && HAVE_QUIC diff --git a/test/parallel/test-quic-internal-endpoint-options.mjs b/test/parallel/test-quic-internal-endpoint-options.mjs index 306d0c523f4611..b6ac6d25c414c2 100644 --- a/test/parallel/test-quic-internal-endpoint-options.mjs +++ b/test/parallel/test-quic-internal-endpoint-options.mjs @@ -53,25 +53,51 @@ const cases = [ invalid: [-1, 65536, 1.5, 'a', null, false, true, {}, [], () => {}] }, { - key: 'maxStatelessResetsPerHost', + key: 'addressLRUSize', valid: [ 1, 10, 100, 1000, 10000, 10000n, ], invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] }, { - key: 'addressLRUSize', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + key: 'retryRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] }, { - key: 'maxRetries', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + key: 'retryBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'statelessResetRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'statelessResetBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'versionNegotiationRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'versionNegotiationBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'immediateCloseRate', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'immediateCloseBurst', + valid: [0, 1, 10, 100.5, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] }, { key: 'validateAddress', diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.mjs b/test/parallel/test-quic-internal-endpoint-stats-state.mjs index c8e6cb89beaaef..bbe7a833c3cc8d 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.mjs +++ b/test/parallel/test-quic-internal-endpoint-stats-state.mjs @@ -88,9 +88,13 @@ const { strictEqual(typeof endpoint.stats.clientSessions, 'bigint'); strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint'); strictEqual(typeof endpoint.stats.retryCount, 'bigint'); + strictEqual(typeof endpoint.stats.retryRateLimited, 'bigint'); strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint'); + strictEqual(typeof endpoint.stats.versionNegotiationRateLimited, 'bigint'); strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint'); + strictEqual(typeof endpoint.stats.statelessResetRateLimited, 'bigint'); strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); + strictEqual(typeof endpoint.stats.immediateCloseRateLimited, 'bigint'); deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ 'connected', @@ -104,9 +108,13 @@ const { 'clientSessions', 'serverBusyCount', 'retryCount', + 'retryRateLimited', 'versionNegotiationCount', + 'versionNegotiationRateLimited', 'statelessResetCount', + 'statelessResetRateLimited', 'immediateCloseCount', + 'immediateCloseRateLimited', ]); strictEqual(typeof inspect(endpoint.stats), 'string'); } diff --git a/test/parallel/test-quic-stateless-reset.mjs b/test/parallel/test-quic-stateless-reset.mjs index b9fdb397e00c6f..5c19d465e20040 100644 --- a/test/parallel/test-quic-stateless-reset.mjs +++ b/test/parallel/test-quic-stateless-reset.mjs @@ -6,8 +6,7 @@ // session closes. // When disableStatelessReset is true, the server does NOT // send a stateless reset. -// maxStatelessResetsPerHost rate limits the number of resets -// sent to a single remote address. +// Global token bucket rate limits the total number of resets. import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; @@ -138,9 +137,8 @@ const encoder = new TextEncoder(); await serverEndpoint.close(); } -// maxStatelessResetsPerHost rate limits resets per remote address. -// The LRU tracks resets per IP+port, so both sessions must share a -// client endpoint to have the same source address. +// Global token bucket rate limits stateless resets. +// With burst=1 and rate=0, only one reset can be sent. { let sessionCount = 0; const serverDestroyed1 = Promise.withResolvers(); @@ -161,12 +159,12 @@ const encoder = new TextEncoder(); deferred.resolve(); }); }, 2), { - endpoint: { maxStatelessResetsPerHost: 1 }, + endpoint: { statelessResetBurst: 1, statelessResetRate: 0 }, onerror(err) { ok(err); }, }); - // Both clients share an endpoint so the server sees the same - // remote IP+port for both, making the rate limiter apply. + // The global token bucket rate limiter applies regardless of + // client source address. const { QuicEndpoint } = await import('node:quic'); const clientEndpoint = new QuicEndpoint(); @@ -215,7 +213,7 @@ const encoder = new TextEncoder(); await serverDestroyed2.promise; // Send a packet — the server would normally send a stateless reset, - // but the rate limit (1 per host) is already exhausted. + // but the global rate limit (burst of 1) is already exhausted. // eslint-disable-next-line no-unused-vars const s2b = await client2.createBidirectionalStream({ body: encoder.encode('after destroy 2'), @@ -226,6 +224,8 @@ const encoder = new TextEncoder(); strictEqual(serverEndpoint.stats.statelessResetCount, 1n, 'Second reset should have been rate-limited'); + ok(serverEndpoint.stats.statelessResetRateLimited > 0n, + 'Rate-limited counter should be non-zero'); await clientEndpoint.close(); await serverEndpoint.close(); From 15ad3a606b39ad261a48238b572b5dd23474bda4 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 17 May 2026 04:54:23 -0700 Subject: [PATCH 23/89] quic: add session creation rate limiting Signed-off-by: James M Snell Assisted-by: Opencode/Opus 4.6 PR-URL: https://github.com/nodejs/node/pull/63483 Reviewed-By: Stephen Belanger Reviewed-By: Matteo Collina --- doc/api/quic.md | 26 +++++++++++++++ lib/internal/quic/quic.js | 6 ++++ lib/internal/quic/stats.js | 12 +++++++ src/quic/bindingdata.h | 2 ++ src/quic/defs.h | 7 ++++ src/quic/endpoint.cc | 33 +++++++++++++++++-- src/quic/endpoint.h | 15 +++++++++ .../test-quic-internal-endpoint-options.mjs | 10 ++++++ ...est-quic-internal-endpoint-stats-state.mjs | 2 ++ 9 files changed, 111 insertions(+), 2 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index 5d2f118c4721d0..3c785b652f1c06 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -796,6 +796,12 @@ added: v23.8.0 * Type: {bigint} The total number of immediate connection close packets dropped by the global rate limiter. Read only. +### `endpointStats.sessionCreationRateLimited` + +* Type: {bigint} The total number of session creation attempts dropped by the + per-host rate limiter. Read only. A non-zero value indicates one or more + remote addresses are creating sessions faster than the configured rate allows. + ## Class: `QuicSession` -* `unwrapAlgo` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} -* `unwrappedKeyAlgo` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams|KmacImportParams} +* `unwrapAlgorithm` {string|Algorithm|RsaOaepParams|AesCtrParams|AesCbcParams|AeadParams} +* `unwrappedKeyAlgorithm` {string|Algorithm|RsaHashedImportParams|EcKeyImportParams|HmacImportParams|KmacImportParams} @@ -1452,8 +1452,8 @@ In cryptography, "wrapping a key" refers to exporting and then encrypting the keying material. This method attempts to decrypt a wrapped key and create a {CryptoKey} instance. It is equivalent to calling [`subtle.decrypt()`][] first on the encrypted key data (using the `wrappedKey`, -`unwrapAlgo`, and `unwrappingKey` arguments as input) then passing the results -to the [`subtle.importKey()`][] method using the `unwrappedKeyAlgo`, +`unwrapAlgorithm`, and `unwrappingKey` arguments as input) then passing the results +to the [`subtle.importKey()`][] method using the `unwrappedKeyAlgorithm`, `extractable`, and `keyUsages` arguments as inputs. If successful, the returned promise is resolved with a {CryptoKey} object. @@ -1541,7 +1541,7 @@ The algorithms currently supported include: * `'RSA-PSS'` * `'RSASSA-PKCS1-v1_5'` -### `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` +### `subtle.wrapKey(format, key, wrappingKey, wrapAlgorithm)` @@ -1568,10 +1568,10 @@ changes: In cryptography, "wrapping a key" refers to exporting and then encrypting the keying material. This method exports the keying material into the format identified by `format`, then encrypts it using the method and -parameters specified by `wrapAlgo` and the keying material provided by +parameters specified by `wrapAlgorithm` and the keying material provided by `wrappingKey`. It is the equivalent to calling [`subtle.exportKey()`][] using `format` and `key` as the arguments, then passing the result to the -[`subtle.encrypt()`][] method using `wrappingKey` and `wrapAlgo` as inputs. If +[`subtle.encrypt()`][] method using `wrappingKey` and `wrapAlgorithm` as inputs. If successful, the returned promise will be resolved with an {ArrayBuffer} containing the encrypted key data. @@ -2815,19 +2815,19 @@ added: [Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/ [`SubtleCrypto.supports()`]: #static-method-subtlecryptosupportsoperation-algorithm-lengthoradditionalalgorithm [`subtle.decapsulateBits()`]: #subtledecapsulatebitsdecapsulationalgorithm-decapsulationkey-ciphertext -[`subtle.decapsulateKey()`]: #subtledecapsulatekeydecapsulationalgorithm-decapsulationkey-ciphertext-sharedkeyalgorithm-extractable-usages +[`subtle.decapsulateKey()`]: #subtledecapsulatekeydecapsulationalgorithm-decapsulationkey-ciphertext-sharedkeyalgorithm-extractable-keyusages [`subtle.decrypt()`]: #subtledecryptalgorithm-key-data [`subtle.deriveBits()`]: #subtlederivebitsalgorithm-basekey-length -[`subtle.deriveKey()`]: #subtlederivekeyalgorithm-basekey-derivedkeyalgorithm-extractable-keyusages +[`subtle.deriveKey()`]: #subtlederivekeyalgorithm-basekey-derivedkeytype-extractable-keyusages [`subtle.digest()`]: #subtledigestalgorithm-data [`subtle.encapsulateBits()`]: #subtleencapsulatebitsencapsulationalgorithm-encapsulationkey -[`subtle.encapsulateKey()`]: #subtleencapsulatekeyencapsulationalgorithm-encapsulationkey-sharedkeyalgorithm-extractable-usages +[`subtle.encapsulateKey()`]: #subtleencapsulatekeyencapsulationalgorithm-encapsulationkey-sharedkeyalgorithm-extractable-keyusages [`subtle.encrypt()`]: #subtleencryptalgorithm-key-data [`subtle.exportKey()`]: #subtleexportkeyformat-key [`subtle.generateKey()`]: #subtlegeneratekeyalgorithm-extractable-keyusages [`subtle.getPublicKey()`]: #subtlegetpublickeykey-keyusages [`subtle.importKey()`]: #subtleimportkeyformat-keydata-algorithm-extractable-keyusages [`subtle.sign()`]: #subtlesignalgorithm-key-data -[`subtle.unwrapKey()`]: #subtleunwrapkeyformat-wrappedkey-unwrappingkey-unwrapalgo-unwrappedkeyalgo-extractable-keyusages +[`subtle.unwrapKey()`]: #subtleunwrapkeyformat-wrappedkey-unwrappingkey-unwrapalgorithm-unwrappedkeyalgorithm-extractable-keyusages [`subtle.verify()`]: #subtleverifyalgorithm-key-signature-data -[`subtle.wrapKey()`]: #subtlewrapkeyformat-key-wrappingkey-wrapalgo +[`subtle.wrapKey()`]: #subtlewrapkeyformat-key-wrappingkey-wrapalgorithm diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 2ed6c69f43e3d4..981502c51700be 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -175,14 +175,14 @@ function aesCipher(mode, key, data, algorithm) { } } -function aesGenerateKey(algorithm, extractable, keyUsages) { +function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; if (name !== 'AES-KW') ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( 'Unsupported key usage for an AES key', @@ -207,13 +207,13 @@ function aesImportKey( format, keyData, extractable, - keyUsages) { + usages) { const { name } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; if (name !== 'AES-KW') ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( 'Unsupported key usage for an AES key', diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 98b1862b7ad1f8..3e6152b1f55501 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -73,10 +73,10 @@ function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { } } -function cfrgGenerateKey(algorithm, extractable, keyUsages) { +function cfrgGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'Ed25519': // Fall through @@ -170,11 +170,11 @@ function cfrgImportKey( keyData, algorithm, extractable, - keyUsages) { + usages) { const { name } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableCfrgKeyUse( diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 9d4606090af8ee..689cab59f3fbf2 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -47,12 +47,12 @@ function c20pCipher(mode, key, data, algorithm) { algorithm.additionalData)); } -function c20pGenerateKey(algorithm, extractable, keyUsages) { +function c20pGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( `Unsupported key usage for a ${algorithm.name} key`, @@ -77,11 +77,11 @@ function c20pImportKey( format, keyData, extractable, - keyUsages) { + usages) { const { name } = algorithm; const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, checkUsages)) { throw lazyDOMException( `Unsupported key usage for a ${algorithm.name} key`, diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 212ba75e0a9b11..d102b3fe05a29c 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -77,10 +77,10 @@ function verifyAcceptableEcKeyUse(name, isPublic, usages) { } } -function ecGenerateKey(algorithm, extractable, keyUsages) { +function ecGenerateKey(algorithm, extractable, usages) { const { name, namedCurve } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'ECDSA': if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { @@ -178,12 +178,12 @@ function ecImportKey( keyData, algorithm, extractable, - keyUsages, + usages, ) { const { name, namedCurve } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableEcKeyUse( diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index c3418231650b02..724b2104d4b8c8 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -40,14 +40,14 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -function hmacGenerateKey(algorithm, extractable, keyUsages) { +function hmacGenerateKey(algorithm, extractable, usages) { const { hash, name, length = getBlockSize(hash.name), } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { throw lazyDOMException( 'Unsupported key usage for an HMAC key', @@ -67,7 +67,7 @@ function hmacGenerateKey(algorithm, extractable, keyUsages) { extractable)); } -function kmacGenerateKey(algorithm, extractable, keyUsages) { +function kmacGenerateKey(algorithm, extractable, usages) { const { name, length = { @@ -77,7 +77,7 @@ function kmacGenerateKey(algorithm, extractable, keyUsages) { }[name], } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${name} key`, @@ -102,10 +102,10 @@ function macImportKey( keyData, algorithm, extractable, - keyUsages, + usages, ) { const isHmac = algorithm.name === 'HMAC'; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${algorithm.name} key`, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 5a08291562bcf2..e2497a2b722b97 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -59,10 +59,10 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { } } -function mlDsaGenerateKey(algorithm, extractable, keyUsages) { +function mlDsaGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for an ${name} key`, @@ -136,11 +136,11 @@ function mlDsaImportKey( keyData, algorithm, extractable, - keyUsages) { + usages) { const { name } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableMlDsaKeyUse( diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 530507be4e340d..2dea4d00af052f 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -49,10 +49,10 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -function mlKemGenerateKey(algorithm, extractable, keyUsages) { +function mlKemGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) { throw lazyDOMException( `Unsupported key usage for an ${name} key`, @@ -137,11 +137,11 @@ function mlKemImportKey( keyData, algorithm, extractable, - keyUsages) { + usages) { const { name } = algorithm; let handle; - const usagesSet = new SafeSet(keyUsages); + const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObject': { verifyAcceptableMlKemKeyUse( diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index d72d55c2bbff42..a09dd7b9f0fda9 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -108,7 +108,7 @@ function rsaOaepCipher(mode, key, data, algorithm) { function rsaKeyGenerate( algorithm, extractable, - keyUsages, + usages, ) { const publicExponentConverted = bigIntArrayToUnsignedInt(algorithm.publicExponent); if (publicExponentConverted === undefined) { @@ -123,7 +123,7 @@ function rsaKeyGenerate( hash, } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'RSA-OAEP': @@ -213,8 +213,8 @@ function rsaImportKey( keyData, algorithm, extractable, - keyUsages) { - const usagesSet = new SafeSet(keyUsages); + usages) { + const usagesSet = new SafeSet(usages); let handle; switch (format) { case 'KeyObject': { diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 996bcb1a729275..05c337d3262229 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -128,9 +128,9 @@ function digestImpl(algorithm, data) { context: '2nd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'digest'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); - return FunctionPrototypeCall(asyncDigest, this, algorithm, data); + return FunctionPrototypeCall(asyncDigest, this, normalizedAlgorithm, data); } function randomUUID() { @@ -162,20 +162,20 @@ function generateKeyImpl( prefix, context: '2nd argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'generateKey'); - switch (algorithm.name) { + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'generateKey'); + switch (normalizedAlgorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': return require('internal/crypto/rsa') - .rsaKeyGenerate(algorithm, extractable, keyUsages); + .rsaKeyGenerate(normalizedAlgorithm, extractable, usages); case 'Ed25519': // Fall through case 'Ed448': @@ -184,15 +184,15 @@ function generateKeyImpl( // Fall through case 'X448': return require('internal/crypto/cfrg') - .cfrgGenerateKey(algorithm, extractable, keyUsages); + .cfrgGenerateKey(normalizedAlgorithm, extractable, usages); case 'ECDSA': // Fall through case 'ECDH': return require('internal/crypto/ec') - .ecGenerateKey(algorithm, extractable, keyUsages); + .ecGenerateKey(normalizedAlgorithm, extractable, usages); case 'HMAC': return require('internal/crypto/mac') - .hmacGenerateKey(algorithm, extractable, keyUsages); + .hmacGenerateKey(normalizedAlgorithm, extractable, usages); case 'AES-CTR': // Fall through case 'AES-CBC': @@ -203,29 +203,29 @@ function generateKeyImpl( // Fall through case 'AES-KW': return require('internal/crypto/aes') - .aesGenerateKey(algorithm, extractable, keyUsages); + .aesGenerateKey(normalizedAlgorithm, extractable, usages); case 'ChaCha20-Poly1305': return require('internal/crypto/chacha20_poly1305') - .c20pGenerateKey(algorithm, extractable, keyUsages); + .c20pGenerateKey(normalizedAlgorithm, extractable, usages); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': return require('internal/crypto/ml_dsa') - .mlDsaGenerateKey(algorithm, extractable, keyUsages); + .mlDsaGenerateKey(normalizedAlgorithm, extractable, usages); case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': return require('internal/crypto/ml_kem') - .mlKemGenerateKey(algorithm, extractable, keyUsages); + .mlKemGenerateKey(normalizedAlgorithm, extractable, usages); case 'KMAC128': // Fall through case 'KMAC256': return require('internal/crypto/mac') - .kmacGenerateKey(algorithm, extractable, keyUsages); + .kmacGenerateKey(normalizedAlgorithm, extractable, usages); default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } @@ -256,35 +256,35 @@ function deriveBitsImpl(algorithm, baseKey, length = null) { }); } - algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); if (!hasCryptoKeyUsage(baseKey, 'deriveBits')) { throw lazyDOMException( 'baseKey does not have deriveBits usage', 'InvalidAccessError'); } - if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== normalizedAlgorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - switch (algorithm.name) { + switch (normalizedAlgorithm.name) { case 'X25519': // Fall through case 'X448': // Fall through case 'ECDH': return require('internal/crypto/diffiehellman') - .ecdhDeriveBits(algorithm, baseKey, length); + .ecdhDeriveBits(normalizedAlgorithm, baseKey, length); case 'HKDF': return require('internal/crypto/hkdf') - .hkdfDeriveBits(algorithm, baseKey, length); + .hkdfDeriveBits(normalizedAlgorithm, baseKey, length); case 'PBKDF2': return require('internal/crypto/pbkdf2') - .pbkdf2DeriveBits(algorithm, baseKey, length); + .pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length); case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': return require('internal/crypto/argon2') - .argon2DeriveBits(algorithm, baseKey, length); + .argon2DeriveBits(normalizedAlgorithm, baseKey, length); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } @@ -331,7 +331,7 @@ function getKeyLength({ name, length, hash }) { function deriveKey( algorithm, baseKey, - derivedKeyAlgorithm, + derivedKeyType, extractable, keyUsages) { return callSubtleCryptoMethod(deriveKeyImpl, this, arguments); @@ -340,7 +340,7 @@ function deriveKey( function deriveKeyImpl( algorithm, baseKey, - derivedKeyAlgorithm, + derivedKeyType, extractable, keyUsages) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -356,7 +356,7 @@ function deriveKeyImpl( prefix, context: '2nd argument', }); - derivedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(derivedKeyAlgorithm, { + derivedKeyType = webidl.converters.AlgorithmIdentifier(derivedKeyType, { prefix, context: '3rd argument', }); @@ -364,56 +364,63 @@ function deriveKeyImpl( prefix, context: '4th argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '5th argument', }); - algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); - derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); + const normalizedDerivedKeyAlgorithmImport = + normalizeAlgorithm(derivedKeyType, 'importKey'); + const normalizedDerivedKeyAlgorithmLength = + normalizeAlgorithm(derivedKeyType, 'get key length'); if (!hasCryptoKeyUsage(baseKey, 'deriveKey')) { throw lazyDOMException( 'baseKey does not have deriveKey usage', 'InvalidAccessError'); } - if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== normalizedAlgorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length')); - let bits; - switch (algorithm.name) { + const length = getKeyLength(normalizedDerivedKeyAlgorithmLength); + let secret; + switch (normalizedAlgorithm.name) { case 'X25519': // Fall through case 'X448': // Fall through case 'ECDH': - bits = require('internal/crypto/diffiehellman') - .ecdhDeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/diffiehellman') + .ecdhDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'HKDF': - bits = require('internal/crypto/hkdf') - .hkdfDeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/hkdf') + .hkdfDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'PBKDF2': - bits = require('internal/crypto/pbkdf2') - .pbkdf2DeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/pbkdf2') + .pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length); break; case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': - bits = require('internal/crypto/argon2') - .argon2DeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/argon2') + .argon2DeriveBits(normalizedAlgorithm, baseKey, length); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return jobPromiseThen(bits, (bits) => FunctionPrototypeCall( + return jobPromiseThen(secret, (secret) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages, + 'raw-secret', + secret, + normalizedDerivedKeyAlgorithmImport, + extractable, + usages, )); } @@ -805,14 +812,14 @@ function detachFromUserPrototypes(value) { } // Parse wrapped JWK bytes according to WebCrypto's "parse a JWK" procedure. -function parseJwk(keyData) { +function parseJwk(data) { let key; try { // WebCrypto parses JWKs in a fresh global. Detach parsed JSON values // from user-mutated prototypes before WebIDL dictionary conversion. // Wrapped JWKs may be produced outside WebCrypto, so parse using the // spec-required UTF-8. - const json = decodeUTF8(keyData, false, true); + const json = decodeUTF8(data, false, true); const result = JSONParse(json); detachFromUserPrototypes(result); key = webidl.converters.JsonWebKey(result); @@ -841,7 +848,7 @@ function aliasKeyFormat(format) { } } -function importKeySync(format, keyData, algorithm, extractable, keyUsages) { +function importKeySync(format, keyData, algorithm, extractable, usages) { let result; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': @@ -851,14 +858,24 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { case 'RSA-OAEP': format = aliasKeyFormat(format); result = require('internal/crypto/rsa') - .rsaImportKey(format, keyData, algorithm, extractable, keyUsages); + .rsaImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'ECDSA': // Fall through case 'ECDH': format = aliasKeyFormat(format); result = require('internal/crypto/ec') - .ecImportKey(format, keyData, algorithm, extractable, keyUsages); + .ecImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'Ed25519': // Fall through @@ -869,7 +886,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { case 'X448': format = aliasKeyFormat(format); result = require('internal/crypto/cfrg') - .cfrgImportKey(format, keyData, algorithm, extractable, keyUsages); + .cfrgImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'HMAC': // Fall through @@ -877,7 +899,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'KMAC256': result = require('internal/crypto/mac') - .macImportKey(format, keyData, algorithm, extractable, keyUsages); + .macImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'AES-CTR': // Fall through @@ -889,11 +916,21 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'AES-OCB': result = require('internal/crypto/aes') - .aesImportKey(algorithm, format, keyData, extractable, keyUsages); + .aesImportKey( + algorithm, + format, + keyData, + extractable, + usages); break; case 'ChaCha20-Poly1305': result = require('internal/crypto/chacha20_poly1305') - .c20pImportKey(algorithm, format, keyData, extractable, keyUsages); + .c20pImportKey( + algorithm, + format, + keyData, + extractable, + usages); break; case 'HKDF': // Fall through @@ -904,7 +941,7 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { format, keyData, extractable, - keyUsages); + usages); break; case 'Argon2d': // Fall through @@ -917,7 +954,7 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { format, keyData, extractable, - keyUsages); + usages); } break; case 'ML-DSA-44': @@ -926,7 +963,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'ML-DSA-87': result = require('internal/crypto/ml_dsa') - .mlDsaImportKey(format, keyData, algorithm, extractable, keyUsages); + .mlDsaImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; case 'ML-KEM-512': // Fall through @@ -934,7 +976,12 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { // Fall through case 'ML-KEM-1024': result = require('internal/crypto/ml_kem') - .mlKemImportKey(format, keyData, algorithm, extractable, keyUsages); + .mlKemImportKey( + format, + keyData, + algorithm, + extractable, + usages); break; } @@ -991,27 +1038,31 @@ function importKeyImpl( prefix, context: '4th argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '5th argument', }); - algorithm = normalizeAlgorithm(algorithm, 'importKey'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); return FunctionPrototypeCall( importKeySync, this, - format, keyData, algorithm, extractable, keyUsages, + format, + keyData, + normalizedAlgorithm, + extractable, + usages, ); } // subtle.wrapKey() is essentially a subtle.exportKey() followed // by a subtle.encrypt(). -function wrapKey(format, key, wrappingKey, algorithm) { +function wrapKey(format, key, wrappingKey, wrapAlgorithm) { return callSubtleCryptoMethod(wrapKeyImpl, this, arguments); } -function wrapKeyImpl(format, key, wrappingKey, algorithm) { +function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1029,50 +1080,52 @@ function wrapKeyImpl(format, key, wrappingKey, algorithm) { prefix, context: '3rd argument', }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + const algorithm = webidl.converters.AlgorithmIdentifier(wrapAlgorithm, { prefix, context: '4th argument', }); + let normalizedAlgorithm; try { - algorithm = normalizeAlgorithm(algorithm, 'wrapKey'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'wrapKey'); } catch { - algorithm = normalizeAlgorithm(algorithm, 'encrypt'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); } - if (algorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(wrappingKey, 'wrapKey')) throw lazyDOMException( 'Unable to use this key to wrapKey', 'InvalidAccessError'); - let keyData = exportKeySync(format, key); + const exportedKey = exportKeySync(format, key); + let bytes = exportedKey; if (format === 'jwk') { // The WebCrypto spec stringifies JWKs in a new global object. Rather // than create a new realm here, detach this internally generated JWK from // user-mutated prototypes so JSON.stringify cannot read inherited toJSON // hooks from the current global. - detachFromUserPrototypes(keyData); - const json = JSONStringify(keyData); + detachFromUserPrototypes(exportedKey); + const json = JSONStringify(exportedKey); // As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey // we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes // in length // The spec then UTF-8 encodes json. - if (algorithm.name === 'AES-KW' && json.length % 8 !== 0) { - keyData = encodeUtf8String( + if (normalizedAlgorithm.name === 'AES-KW' && json.length % 8 !== 0) { + bytes = encodeUtf8String( json + StringPrototypeRepeat(' ', 8 - (json.length % 8))); } else { - keyData = encodeUtf8String(json); + bytes = encodeUtf8String(json); } } return cipherOrWrap( kWebCryptoCipherEncrypt, - algorithm, + normalizedAlgorithm, wrappingKey, - keyData, + bytes, 'wrapKey'); } @@ -1082,8 +1135,8 @@ function unwrapKey( format, wrappedKey, unwrappingKey, - unwrapAlgo, - unwrappedKeyAlgo, + unwrapAlgorithm, + unwrappedKeyAlgorithm, extractable, keyUsages) { return callSubtleCryptoMethod(unwrapKeyImpl, this, arguments); @@ -1093,8 +1146,8 @@ function unwrapKeyImpl( format, wrappedKey, unwrappingKey, - unwrapAlgo, - unwrappedKeyAlgo, + unwrapAlgorithm, + unwrappedKeyAlgorithm, extractable, keyUsages) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1114,12 +1167,12 @@ function unwrapKeyImpl( prefix, context: '3rd argument', }); - unwrapAlgo = webidl.converters.AlgorithmIdentifier(unwrapAlgo, { + const algorithm = webidl.converters.AlgorithmIdentifier(unwrapAlgorithm, { prefix, context: '4th argument', }); - unwrappedKeyAlgo = webidl.converters.AlgorithmIdentifier( - unwrappedKeyAlgo, + unwrappedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( + unwrappedKeyAlgorithm, { prefix, context: '5th argument', @@ -1129,87 +1182,94 @@ function unwrapKeyImpl( prefix, context: '6th argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '7th argument', }); + let normalizedAlgorithm; try { - unwrapAlgo = normalizeAlgorithm(unwrapAlgo, 'unwrapKey'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'unwrapKey'); } catch { - unwrapAlgo = normalizeAlgorithm(unwrapAlgo, 'decrypt'); + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); } - unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey'); + const normalizedKeyAlgorithm = + normalizeAlgorithm(unwrappedKeyAlgorithm, 'importKey'); - if (unwrapAlgo.name !== getCryptoKeyAlgorithm(unwrappingKey).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(unwrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(unwrappingKey, 'unwrapKey')) throw lazyDOMException( 'Unable to use this key to unwrapKey', 'InvalidAccessError'); - const keyData = cipherOrWrap( + const bytes = cipherOrWrap( kWebCryptoCipherDecrypt, - unwrapAlgo, + normalizedAlgorithm, unwrappingKey, wrappedKey, 'unwrapKey'); - return jobPromiseThen(keyData, (keyData) => { + return jobPromiseThen(bytes, (bytes) => { + let keyData = bytes; if (format === 'jwk') { - keyData = parseJwk(keyData); + keyData = parseJwk(bytes); } return FunctionPrototypeCall( importKeySync, this, - format, keyData, unwrappedKeyAlgo, extractable, keyUsages, + format, + keyData, + normalizedKeyAlgorithm, + extractable, + usages, ); }); } function signVerify(algorithm, key, data, signature) { - const op = signature !== undefined ? 'verify' : 'sign'; // This is also usage - algorithm = normalizeAlgorithm(algorithm, op); + const operation = signature !== undefined ? 'verify' : 'sign'; // This is also usage + const normalizedAlgorithm = normalizeAlgorithm(algorithm, operation); - if (algorithm.name !== getCryptoKeyAlgorithm(key).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!hasCryptoKeyUsage(key, op)) + if (!hasCryptoKeyUsage(key, operation)) throw lazyDOMException( - `Unable to use this key to ${op}`, 'InvalidAccessError'); + `Unable to use this key to ${operation}`, 'InvalidAccessError'); - switch (algorithm.name) { + switch (normalizedAlgorithm.name) { case 'RSA-PSS': // Fall through case 'RSASSA-PKCS1-v1_5': return require('internal/crypto/rsa') - .rsaSignVerify(key, data, algorithm, signature); + .rsaSignVerify(key, data, normalizedAlgorithm, signature); case 'ECDSA': return require('internal/crypto/ec') - .ecdsaSignVerify(key, data, algorithm, signature); + .ecdsaSignVerify(key, data, normalizedAlgorithm, signature); case 'Ed25519': // Fall through case 'Ed448': // Fall through return require('internal/crypto/cfrg') - .eddsaSignVerify(key, data, algorithm, signature); + .eddsaSignVerify(key, data, normalizedAlgorithm, signature); case 'HMAC': return require('internal/crypto/mac') - .hmacSignVerify(key, data, algorithm, signature); + .hmacSignVerify(key, data, normalizedAlgorithm, signature); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': return require('internal/crypto/ml_dsa') - .mlDsaSignVerify(key, data, algorithm, signature); + .mlDsaSignVerify(key, data, normalizedAlgorithm, signature); case 'KMAC128': // Fall through case 'KMAC256': return require('internal/crypto/mac') - .kmacSignVerify(key, data, algorithm, signature); + .kmacSignVerify(key, data, normalizedAlgorithm, signature); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } @@ -1270,16 +1330,16 @@ function verifyImpl(algorithm, key, signature, data) { return signVerify(algorithm, key, data, signature); } -function cipherOrWrap(mode, algorithm, key, data, op) { +function cipherOrWrap(mode, normalizedAlgorithm, key, data, operation) { // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. validateMaxBufferLength(data, 'data'); - switch (algorithm.name) { + switch (normalizedAlgorithm.name) { case 'RSA-OAEP': return require('internal/crypto/rsa') - .rsaCipher(mode, key, data, algorithm); + .rsaCipher(mode, key, data, normalizedAlgorithm); case 'AES-CTR': // Fall through case 'AES-CBC': @@ -1288,14 +1348,14 @@ function cipherOrWrap(mode, algorithm, key, data, op) { // Fall through case 'AES-OCB': return require('internal/crypto/aes') - .aesCipher(mode, key, data, algorithm); + .aesCipher(mode, key, data, normalizedAlgorithm); case 'ChaCha20-Poly1305': return require('internal/crypto/chacha20_poly1305') - .c20pCipher(mode, key, data, algorithm); + .c20pCipher(mode, key, data, normalizedAlgorithm); case 'AES-KW': - if (op === 'wrapKey' || op === 'unwrapKey') { + if (operation === 'wrapKey' || operation === 'unwrapKey') { return require('internal/crypto/aes') - .aesCipher(mode, key, data, algorithm); + .aesCipher(mode, key, data, normalizedAlgorithm); } } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); @@ -1324,9 +1384,9 @@ function encryptImpl(algorithm, key, data) { context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'encrypt'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); - if (algorithm.name !== getCryptoKeyAlgorithm(key).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(key, 'encrypt')) @@ -1335,7 +1395,7 @@ function encryptImpl(algorithm, key, data) { return cipherOrWrap( kWebCryptoCipherEncrypt, - algorithm, + normalizedAlgorithm, key, data, 'encrypt', @@ -1365,9 +1425,9 @@ function decryptImpl(algorithm, key, data) { context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'decrypt'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); - if (algorithm.name !== getCryptoKeyAlgorithm(key).name) + if (normalizedAlgorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); if (!hasCryptoKeyUsage(key, 'decrypt')) @@ -1376,7 +1436,7 @@ function decryptImpl(algorithm, key, data) { return cipherOrWrap( kWebCryptoCipherDecrypt, - algorithm, + normalizedAlgorithm, key, data, 'decrypt', @@ -1399,7 +1459,7 @@ function getPublicKeyImpl(key, keyUsages) { prefix, context: '1st argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '2nd argument', }); @@ -1412,7 +1472,7 @@ function getPublicKeyImpl(key, keyUsages) { // TODO(panva): this is by no means a hot path, but let's still follow up to get // rid of this awkwardness const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key))); - return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, keyUsages); + return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, usages); } function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { @@ -1426,10 +1486,13 @@ function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'encapsulateBits' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 2, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(encapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + encapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { prefix, context: '2nd argument', @@ -1467,7 +1530,7 @@ function encapsulateKey( encapsulationKey, sharedKeyAlgorithm, extractable, - usages) { + keyUsages) { return callSubtleCryptoMethod(encapsulateKeyImpl, this, arguments); } @@ -1476,30 +1539,36 @@ function encapsulateKeyImpl( encapsulationKey, sharedKeyAlgorithm, extractable, - usages) { + keyUsages) { emitExperimentalWarning('The encapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'encapsulateKey' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 5, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(encapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + encapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { prefix, context: '2nd argument', }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(sharedKeyAlgorithm, { - prefix, - context: '3rd argument', - }); + sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( + sharedKeyAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); extractable = webidl.converters.boolean(extractable, { prefix, context: '4th argument', }); - usages = webidl.converters['sequence'](usages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '5th argument', }); @@ -1522,28 +1591,31 @@ function encapsulateKeyImpl( 'InvalidAccessError'); } - let encapsulateBits; + let encapsulatedBits; switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - encapsulateBits = require('internal/crypto/ml_kem') + encapsulatedBits = require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return jobPromiseThen(encapsulateBits, (encapsulateBits) => { + return jobPromiseThen(encapsulatedBits, (encapsulatedBits) => { const sharedKey = FunctionPrototypeCall( importKeySync, this, - 'raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, - extractable, usages, + 'raw-secret', + encapsulatedBits.sharedKey, + normalizedSharedKeyAlgorithm, + extractable, + usages, ); return { - ciphertext: encapsulateBits.ciphertext, + ciphertext: encapsulatedBits.ciphertext, sharedKey, }; }); @@ -1560,10 +1632,13 @@ function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertex webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'decapsulateBits' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 3, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(decapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + decapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { prefix, context: '2nd argument', @@ -1601,8 +1676,12 @@ function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertex } function decapsulateKey( - decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages, -) { + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + keyUsages) { return callSubtleCryptoMethod(decapsulateKeyImpl, this, arguments); } @@ -1612,17 +1691,20 @@ function decapsulateKeyImpl( ciphertext, sharedKeyAlgorithm, extractable, - usages) { + keyUsages) { emitExperimentalWarning('The decapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); const prefix = "Failed to execute 'decapsulateKey' on 'SubtleCrypto'"; webidl.requiredArguments(arguments.length, 6, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier(decapsulationAlgorithm, { - prefix, - context: '1st argument', - }); + decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( + decapsulationAlgorithm, + { + prefix, + context: '1st argument', + }, + ); decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { prefix, context: '2nd argument', @@ -1631,15 +1713,18 @@ function decapsulateKeyImpl( prefix, context: '3rd argument', }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(sharedKeyAlgorithm, { - prefix, - context: '4th argument', - }); + sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( + sharedKeyAlgorithm, + { + prefix, + context: '4th argument', + }, + ); extractable = webidl.converters.boolean(extractable, { prefix, context: '5th argument', }); - usages = webidl.converters['sequence'](usages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '6th argument', }); @@ -1677,7 +1762,10 @@ function decapsulateKeyImpl( return jobPromiseThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, + 'raw-secret', + decapsulatedBits, + normalizedSharedKeyAlgorithm, + extractable, usages, )); } @@ -1733,10 +1821,13 @@ class SubtleCrypto { let length; let additionalAlgorithm; if (operation === 'deriveKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); if (!check('importKey', additionalAlgorithm)) { return false; @@ -1750,19 +1841,25 @@ class SubtleCrypto { operation = 'deriveBits'; } else if (operation === 'wrapKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); if (!check('exportKey', additionalAlgorithm)) { return false; } } else if (operation === 'unwrapKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); if (!check('importKey', additionalAlgorithm)) { return false; @@ -1796,10 +1893,13 @@ class SubtleCrypto { return false; } } else if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { - additionalAlgorithm = webidl.converters.AlgorithmIdentifier(lengthOrAdditionalAlgorithm, { - prefix, - context: '3rd argument', - }); + additionalAlgorithm = webidl.converters.AlgorithmIdentifier( + lengthOrAdditionalAlgorithm, + { + prefix, + context: '3rd argument', + }, + ); let normalizedAdditionalAlgorithm; try { @@ -1824,7 +1924,8 @@ class SubtleCrypto { case 'HMAC': case 'KMAC128': case 'KMAC256': - if (normalizedAdditionalAlgorithm.length === undefined || normalizedAdditionalAlgorithm.length === 256) { + if (normalizedAdditionalAlgorithm.length === undefined || + normalizedAdditionalAlgorithm.length === 256) { break; } return false; From 742849dca7200dcf442ff04e32c2c27a56ef1efd Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 18:36:32 +0200 Subject: [PATCH 37/89] test: cover webcrypto prototype pollution systematically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drive the regression test from the WebCrypto algorithm registry so all supported algorithms and operations must add explicit coverage regardless of whether they are native-job backed or js-based. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63520 Reviewed-By: René Reviewed-By: Antoine du Hamel Reviewed-By: James M Snell --- lib/internal/crypto/util.js | 1 + ...-webcrypto-promise-prototype-pollution.mjs | 1158 +++++++++++------ 2 files changed, 796 insertions(+), 363 deletions(-) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 046efc4554ca36..74d86de3f1b9e1 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -960,6 +960,7 @@ module.exports = { toBuf, kNamedCurveAliases, + kSupportedAlgorithms, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index 17cc5c97716df0..5c13561dc26063 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -1,23 +1,39 @@ +// Flags: --expose-internals + import * as common from '../common/index.mjs'; import assert from 'node:assert'; +import { createRequire } from 'node:module'; if (!common.hasCrypto) common.skip('missing crypto'); -// WebCrypto subtle methods must not leak intermediate values -// through Promise.prototype.then or constructor pollution. +// WebCrypto subtle methods must not leak intermediate values through +// Promise.prototype.then, constructor/species, thenable assimilation, or +// inherited accessors on internally-created JWK/result objects. // Regression test for https://github.com/nodejs/node/pull/61492 // and https://github.com/nodejs/node/issues/59699. +// +// When adding WebCrypto algorithms: +// - Add a fixture with addFixture() below. Prefer the shared fixture builders +// unless an algorithm needs operation-specific parameters. +// - Add new operation names to operationOrder and implement that operation on +// every affected fixture. +// - Add new "get key length" algorithms to keyLengthTargets, unless the +// algorithm is itself a KDF whose getKeyLength() result is null; those belong +// in nullKeyLengthAlgorithms. +// The registry assertions at the bottom make missing updates fail loudly. -import { hasOpenSSL } from '../common/crypto.js'; - +const require = createRequire(import.meta.url); +const { kSupportedAlgorithms } = require('internal/crypto/util'); const { subtle } = globalThis.crypto; Promise.prototype.then = common.mustNotCall('Promise.prototype.then'); +const data = new TextEncoder().encode('prototype pollution'); + // WebCrypto methods return native promises. Re-wrapping a promise with // PromiseResolve() or chaining it with Promise.prototype.then can read // user-mutated constructor/species accessors. -async function assertNoPromiseConstructorAccess(name, fn) { +function assertNoPromiseConstructorAccess(name, fn) { const constructorDescriptor = Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor'); const speciesDescriptor = @@ -43,7 +59,7 @@ async function assertNoPromiseConstructorAccess(name, fn) { constructorDescriptor); Object.defineProperty(Promise, Symbol.species, speciesDescriptor); } - return await promise; + return promise; } // Exercise each export format through the same promise-constructor guard. @@ -96,6 +112,11 @@ function assertNoInheritedObjectThenAccess(name, fn) { fn); } +function assertCryptoKeyResult(name, fn) { + return assertNoInheritedCryptoKeyThenAccess(name, () => + assertNoPromiseConstructorAccess(name, fn)); +} + // wrapKey('jwk') stringifies an internally exported JWK. The spec does this // in a fresh global, so inherited toJSON hooks from the current global must // not observe or replace key material. @@ -131,7 +152,9 @@ async function assertNoInheritedToJSONAccess(name, fn) { } // JWK export creates and fills a result object. The exported members must be -// own data properties, not writes that can observe inherited accessors. +// own data properties, not writes that can observe inherited accessors. Keep +// this list complete: if exportKey('jwk') starts returning a new JWK member, +// this helper must poison that member on Object.prototype too. async function assertNoInheritedJwkPropertyAccess(name, fn) { const properties = [ 'alg', @@ -153,6 +176,7 @@ async function assertNoInheritedJwkPropertyAccess(name, fn) { 'x', 'y', ]; + const handledProperties = new Set(properties); const descriptors = new Map(); for (const property of properties) { descriptors.set( @@ -165,8 +189,9 @@ async function assertNoInheritedJwkPropertyAccess(name, fn) { set: common.mustNotCall(`${name} Object.prototype.${property} setter`), }); } + let result; try { - return await fn(); + result = await fn(); } finally { for (const property of properties) { const descriptor = descriptors.get(property); @@ -177,6 +202,16 @@ async function assertNoInheritedJwkPropertyAccess(name, fn) { } } } + + if (result !== null && typeof result === 'object') { + for (const property of Object.keys(result)) { + assert( + handledProperties.has(property), + `${name} returned unhandled JWK property ${property}`); + } + } + + return result; } // unwrapKey('jwk') parses a JWK and then converts it to the JsonWebKey IDL @@ -292,389 +327,786 @@ async function assertNoRawSharedKeyObjectThenAccess(name, fn) { } } -await assertNoPromiseConstructorAccess('digest', () => - subtle.digest('SHA-256', new Uint8Array([1, 2, 3]))); +function algorithm(name, params = {}) { + return { name, ...params }; +} -const secretKey = await assertNoPromiseConstructorAccess( - 'generateKey secret', - () => subtle.generateKey( - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); +function rsaAlgorithm(name) { + return algorithm(name, { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }); +} -const extractableKeyPair = await assertNoPromiseConstructorAccess('generateKey pair', () => - subtle.generateKey( - { name: 'ECDSA', namedCurve: 'P-256' }, - true, - ['sign', 'verify'])); - -const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); - -const importedKey = await assertNoPromiseConstructorAccess('importKey', () => - subtle.importKey( - 'raw', - rawKey, - { name: 'AES-CBC', length: 256 }, - false, - ['encrypt', 'decrypt'])); - -await assertNoInheritedCryptoKeyThenAccess('importKey', () => - subtle.importKey( - 'raw', - rawKey, - { name: 'AES-CBC', length: 256 }, - false, - ['encrypt', 'decrypt'])); - -await assertNoInheritedJwkPropertyAccess('exportKey jwk secret', () => - assertExportKeyNoPromiseConstructorAccess( - 'jwk secret', - 'jwk', - secretKey)); -await assertNoInheritedObjectThenAccess('exportKey jwk secret', () => - subtle.exportKey('jwk', secretKey)); -await assertNoInheritedJwkPropertyAccess('exportKey jwk public', () => - assertExportKeyNoPromiseConstructorAccess( - 'jwk public', - 'jwk', - extractableKeyPair.publicKey)); -await assertNoInheritedObjectThenAccess('exportKey jwk public', () => - subtle.exportKey('jwk', extractableKeyPair.publicKey)); -await assertNoInheritedJwkPropertyAccess('exportKey jwk private', () => - assertExportKeyNoPromiseConstructorAccess( - 'jwk private', - 'jwk', - extractableKeyPair.privateKey)); -await assertNoInheritedObjectThenAccess('exportKey jwk private', () => - subtle.exportKey('jwk', extractableKeyPair.privateKey)); -await assertNoInheritedArrayBufferThenAccess('exportKey raw secret', () => - subtle.exportKey('raw', secretKey)); -await assertExportKeyNoPromiseConstructorAccess( - 'raw secret', - 'raw', - secretKey); -await assertNoInheritedArrayBufferThenAccess('exportKey spki', () => - subtle.exportKey('spki', extractableKeyPair.publicKey)); -await assertExportKeyNoPromiseConstructorAccess( - 'spki', - 'spki', - extractableKeyPair.publicKey); -await assertNoInheritedArrayBufferThenAccess('exportKey pkcs8', () => - subtle.exportKey('pkcs8', extractableKeyPair.privateKey)); -await assertExportKeyNoPromiseConstructorAccess( - 'pkcs8', - 'pkcs8', - extractableKeyPair.privateKey); -await assertNoInheritedArrayBufferThenAccess('exportKey raw-public', () => - subtle.exportKey('raw-public', extractableKeyPair.publicKey)); -await assertExportKeyNoPromiseConstructorAccess( - 'raw-public', - 'raw-public', - extractableKeyPair.publicKey); - -const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); -const plaintext = new TextEncoder().encode('Hello, world!'); - -const ciphertext = await assertNoPromiseConstructorAccess('encrypt', () => - subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext)); - -await assertNoPromiseConstructorAccess('decrypt', () => - subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext)); - -const signingKey = await subtle.generateKey( - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign', 'verify']); +function importRsaAlgorithm(name) { + return algorithm(name, { hash: 'SHA-256' }); +} -const data = new TextEncoder().encode('test data'); +function exportArrayBuffer(name, format, key) { + return assertNoInheritedArrayBufferThenAccess(`exportKey ${format} ${name}`, () => + assertExportKeyNoPromiseConstructorAccess(`${format} ${name}`, format, key)); +} -const signature = await assertNoPromiseConstructorAccess('sign', () => - subtle.sign('HMAC', signingKey, data)); +function exportJwk(name, key) { + return assertNoInheritedJwkPropertyAccess(`exportKey jwk ${name}`, () => + assertNoInheritedObjectThenAccess(`exportKey jwk ${name}`, () => + assertExportKeyNoPromiseConstructorAccess(`jwk ${name}`, 'jwk', key))); +} -await assertNoPromiseConstructorAccess('verify', () => - subtle.verify('HMAC', signingKey, signature, data)); +function importCryptoKey(name, format, keyData, importAlgorithm, extractable, usages) { + return assertCryptoKeyResult(`importKey ${format} ${name}`, () => + subtle.importKey(format, keyData, importAlgorithm, extractable, usages)); +} -const pbkdf2Key = await subtle.importKey( - 'raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']); - -await assertNoPromiseConstructorAccess('deriveBits', () => - subtle.deriveBits( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - 256)); - -await assertNoPromiseConstructorAccess('deriveBits PBKDF2 zero-length', () => - subtle.deriveBits( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - 0)); - -const hkdfKey = await subtle.importKey( - 'raw', rawKey, 'HKDF', false, ['deriveBits']); - -await assertNoPromiseConstructorAccess('deriveBits HKDF zero-length', () => - subtle.deriveBits( - { name: 'HKDF', hash: 'SHA-256', salt: rawKey, info: rawKey }, - hkdfKey, - 0)); - -const ecdhKeyPair = await subtle.generateKey( - { name: 'ECDH', namedCurve: 'P-256' }, - false, - ['deriveBits']); - -await assertNoPromiseConstructorAccess('deriveBits ECDH', () => - subtle.deriveBits( - { name: 'ECDH', public: ecdhKeyPair.publicKey }, - ecdhKeyPair.privateKey, - 256)); - -await assertNoPromiseConstructorAccess('deriveKey', () => - subtle.deriveKey( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); -await assertNoInheritedArrayBufferThenAccess('deriveKey', () => - subtle.deriveKey( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); -await assertNoInheritedCryptoKeyThenAccess('deriveKey result', () => - subtle.deriveKey( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); - -const wrappingKey = await subtle.generateKey( - { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); - -const keyToWrap = await subtle.generateKey( - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); - -const wrapped = await assertNoPromiseConstructorAccess('wrapKey', () => - subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW')); - -const wrappedJwk = await assertNoInheritedJwkPropertyAccess('wrapKey jwk', () => - assertNoInheritedToJSONAccess('wrapKey jwk', () => - assertNoUserMutableEncodeAccess('wrapKey jwk', () => - assertNoPromiseConstructorAccess('wrapKey jwk', () => - subtle.wrapKey('jwk', keyToWrap, wrappingKey, 'AES-KW'))))); - -await assertNoPromiseConstructorAccess('unwrapKey', () => - subtle.unwrapKey( - 'raw', - wrapped, - wrappingKey, - 'AES-KW', - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); -await assertNoInheritedArrayBufferThenAccess('unwrapKey', () => - subtle.unwrapKey( - 'raw', - wrapped, - wrappingKey, - 'AES-KW', - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); -await assertNoInheritedCryptoKeyThenAccess('unwrapKey result', () => - subtle.unwrapKey( - 'raw', - wrapped, - wrappingKey, - 'AES-KW', - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); +async function getKeyToWrap() { + if (getKeyToWrap.key === undefined) { + getKeyToWrap.key = await subtle.generateKey( + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt']); + } + return getKeyToWrap.key; +} -await assertNoUserMutableDecodeAccess('unwrapKey jwk', () => - assertNoPromiseConstructorAccess('unwrapKey jwk', () => - subtle.unwrapKey( +function addCommonKeyExportTests(fixture) { + fixture.exportKey = async (ctx) => { + if (fixture.rawFormat !== undefined) + ctx.raw = await exportArrayBuffer(fixture.name, fixture.rawFormat, ctx.key); + ctx.jwk = await exportJwk(fixture.name, ctx.key); + }; + fixture.importKey = async (ctx) => { + if (fixture.rawFormat !== undefined) { + ctx.importedRaw = await importCryptoKey( + fixture.name, + fixture.rawFormat, + ctx.raw, + fixture.importAlgorithm, + true, + fixture.usages); + } + ctx.importedJwk = await importCryptoKey( + fixture.name, 'jwk', - wrappedJwk, - wrappingKey, - 'AES-KW', - { name: 'AES-CBC', length: 256 }, + ctx.jwk, + fixture.importAlgorithm, true, - ['encrypt', 'decrypt']))); -await assertNoInheritedArrayBufferThenAccess('unwrapKey jwk', () => - subtle.unwrapKey( - 'jwk', - wrappedJwk, - wrappingKey, - 'AES-KW', - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); -await assertNoInheritedCryptoKeyThenAccess('unwrapKey jwk result', () => - subtle.unwrapKey( - 'jwk', - wrappedJwk, - wrappingKey, - 'AES-KW', - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt'])); + fixture.usages); + }; +} -{ - const jwkUnwrappingKey = await subtle.generateKey( - { name: 'AES-CBC', length: 128 }, - true, - ['encrypt', 'unwrapKey']); +function secretKeyFixture(options) { + const fixture = { + ...options, + generateKey: async (ctx) => { + ctx.key = await assertNoPromiseConstructorAccess(`generateKey ${options.name}`, () => + subtle.generateKey(options.generateAlgorithm, true, options.usages)); + }, + }; - { - const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); - const validWrappedJwk = await subtle.encrypt( - { name: 'AES-CBC', iv }, - jwkUnwrappingKey, - Buffer.from('{"kty":"oct","k":"AAAAAAAAAAAAAAAAAAAAAA"}')); - - await assertNoUserMutableDecodeAccess('unwrapKey jwk AES-CBC', () => - assertNoPromiseConstructorAccess('unwrapKey jwk AES-CBC', () => - subtle.unwrapKey( - 'jwk', - validWrappedJwk, - jwkUnwrappingKey, - { name: 'AES-CBC', iv }, - { name: 'AES-CBC', length: 128 }, + addCommonKeyExportTests(fixture); + + if (options.encryptAlgorithm !== undefined) { + fixture.encrypt = async (ctx) => { + ctx.ciphertext = await assertNoPromiseConstructorAccess(`encrypt ${options.name}`, () => + subtle.encrypt(options.encryptAlgorithm, ctx.key, data)); + }; + fixture.decrypt = async (ctx) => { + await assertNoPromiseConstructorAccess(`decrypt ${options.name}`, () => + subtle.decrypt(options.encryptAlgorithm, ctx.key, ctx.ciphertext)); + }; + } + + if (options.signAlgorithm !== undefined) { + fixture.sign = async (ctx) => { + ctx.signature = await assertNoPromiseConstructorAccess(`sign ${options.name}`, () => + subtle.sign(options.signAlgorithm, ctx.key, data)); + }; + fixture.verify = async (ctx) => { + await assertNoPromiseConstructorAccess(`verify ${options.name}`, () => + subtle.verify(options.signAlgorithm, ctx.key, ctx.signature, data)); + }; + } + + if (options.wrapAlgorithm !== undefined) { + fixture.wrapKey = async (ctx) => { + const keyToWrap = await getKeyToWrap(); + ctx.wrappedRawSecret = await assertNoPromiseConstructorAccess( + `wrapKey raw-secret ${options.name}`, + () => subtle.wrapKey('raw-secret', keyToWrap, ctx.key, options.wrapAlgorithm)); + ctx.wrappedJwk = await assertNoInheritedJwkPropertyAccess( + `wrapKey jwk ${options.name}`, + () => assertNoInheritedToJSONAccess( + `wrapKey jwk ${options.name}`, + () => assertNoUserMutableEncodeAccess( + `wrapKey jwk ${options.name}`, () => + assertNoPromiseConstructorAccess(`wrapKey jwk ${options.name}`, () => + subtle.wrapKey('jwk', keyToWrap, ctx.key, options.wrapAlgorithm))))); + }; + fixture.unwrapKey = async (ctx) => { + await assertNoInheritedArrayBufferThenAccess(`unwrapKey raw-secret ${options.name}`, () => + assertCryptoKeyResult(`unwrapKey raw-secret ${options.name} result`, () => + subtle.unwrapKey( + 'raw-secret', + ctx.wrappedRawSecret, + ctx.key, + options.wrapAlgorithm, + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt']))); + await assertNoUserMutableDecodeAccess(`unwrapKey jwk ${options.name}`, () => + assertNoInheritedArrayBufferThenAccess(`unwrapKey jwk ${options.name}`, () => + assertCryptoKeyResult(`unwrapKey jwk ${options.name} result`, () => + subtle.unwrapKey( + 'jwk', + ctx.wrappedJwk, + ctx.key, + options.wrapAlgorithm, + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt'])))); + }; + } + + return fixture; +} + +function pairKeyFixture(options) { + const fixture = { + ...options, + generateKey: async (ctx) => { + ctx.keyPair = await assertNoPromiseConstructorAccess(`generateKey ${options.name}`, () => + subtle.generateKey(options.generateAlgorithm, true, options.usages)); + }, + exportKey: async (ctx) => { + if (options.spki !== false) { + ctx.spki = await exportArrayBuffer(`${options.name} public`, 'spki', ctx.keyPair.publicKey); + ctx.pkcs8 = await exportArrayBuffer(`${options.name} private`, 'pkcs8', ctx.keyPair.privateKey); + } + if (options.rawPublic === true) { + ctx.rawPublic = await exportArrayBuffer( + `${options.name} public`, + 'raw-public', + ctx.keyPair.publicKey); + } + if (options.rawSeed === true) { + ctx.rawSeed = await exportArrayBuffer( + `${options.name} private`, + 'raw-seed', + ctx.keyPair.privateKey); + } + ctx.publicJwk = await exportJwk(`${options.name} public`, ctx.keyPair.publicKey); + ctx.privateJwk = await exportJwk(`${options.name} private`, ctx.keyPair.privateKey); + }, + importKey: async (ctx) => { + if (options.spki !== false) { + ctx.importedSpki = await importCryptoKey( + `${options.name} public`, + 'spki', + ctx.spki, + options.importAlgorithm, + true, + options.publicUsages); + ctx.importedPkcs8 = await importCryptoKey( + `${options.name} private`, + 'pkcs8', + ctx.pkcs8, + options.importAlgorithm, + true, + options.privateUsages); + } + if (options.rawPublic === true) { + ctx.importedRawPublic = await importCryptoKey( + `${options.name} public`, + 'raw-public', + ctx.rawPublic, + options.importAlgorithm, true, - ['encrypt']))); - await assertNoInheritedCryptoKeyThenAccess( - 'unwrapKey jwk AES-CBC result', - () => subtle.unwrapKey( + options.publicUsages); + } + if (options.rawSeed === true) { + ctx.importedRawSeed = await importCryptoKey( + `${options.name} private`, + 'raw-seed', + ctx.rawSeed, + options.importAlgorithm, + true, + options.privateUsages); + } + ctx.importedPublicJwk = await importCryptoKey( + `${options.name} public`, + 'jwk', + ctx.publicJwk, + options.importAlgorithm, + true, + options.publicUsages); + ctx.importedPrivateJwk = await importCryptoKey( + `${options.name} private`, 'jwk', - validWrappedJwk, - jwkUnwrappingKey, - { name: 'AES-CBC', iv }, - { name: 'AES-CBC', length: 128 }, + ctx.privateJwk, + options.importAlgorithm, true, - ['encrypt'])); + options.privateUsages); + }, + }; + + if (options.signAlgorithm !== undefined) { + fixture.sign = async (ctx) => { + ctx.signature = await assertNoPromiseConstructorAccess(`sign ${options.name}`, () => + subtle.sign(options.signAlgorithm, ctx.keyPair.privateKey, data)); + }; + fixture.verify = async (ctx) => { + await assertNoPromiseConstructorAccess(`verify ${options.name}`, () => + subtle.verify(options.signAlgorithm, ctx.keyPair.publicKey, ctx.signature, data)); + }; } - { - const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); - const wrappedRawKey = await subtle.encrypt( - { name: 'AES-CBC', iv }, - jwkUnwrappingKey, - rawKey); - - await assertNoPromiseConstructorAccess('unwrapKey raw AES-CBC', () => - subtle.unwrapKey( - 'raw', - wrappedRawKey, - jwkUnwrappingKey, - { name: 'AES-CBC', iv }, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt'])); - await assertNoInheritedArrayBufferThenAccess('unwrapKey raw AES-CBC', () => - subtle.unwrapKey( - 'raw', - wrappedRawKey, - jwkUnwrappingKey, - { name: 'AES-CBC', iv }, - { name: 'AES-CBC', length: 256 }, + if (options.encryptAlgorithm !== undefined) { + fixture.encrypt = async (ctx) => { + ctx.ciphertext = await assertNoPromiseConstructorAccess(`encrypt ${options.name}`, () => + subtle.encrypt(options.encryptAlgorithm, ctx.keyPair.publicKey, data)); + }; + fixture.decrypt = async (ctx) => { + await assertNoPromiseConstructorAccess(`decrypt ${options.name}`, () => + subtle.decrypt(options.encryptAlgorithm, ctx.keyPair.privateKey, ctx.ciphertext)); + }; + } + + if (options.deriveAlgorithm !== undefined) { + fixture.deriveBits = async (ctx) => { + ctx.peerKeyPair ??= await subtle.generateKey( + options.generateAlgorithm, true, - ['encrypt'])); - await assertNoInheritedCryptoKeyThenAccess( - 'unwrapKey raw AES-CBC result', - () => subtle.unwrapKey( - 'raw', - wrappedRawKey, - jwkUnwrappingKey, - { name: 'AES-CBC', iv }, - { name: 'AES-CBC', length: 256 }, + options.usages); + ctx.derivedBits = await assertNoPromiseConstructorAccess(`deriveBits ${options.name}`, () => + subtle.deriveBits( + options.deriveAlgorithm(ctx.peerKeyPair.publicKey), + ctx.keyPair.privateKey, + options.deriveLength)); + }; + fixture.extra = async (ctx) => { + ctx.peerKeyPair ??= await subtle.generateKey( + options.generateAlgorithm, true, - ['encrypt'])); + options.usages); + await assertNoInheritedArrayBufferThenAccess(`deriveKey ${options.name}`, () => + assertCryptoKeyResult(`deriveKey ${options.name} result`, () => + subtle.deriveKey( + options.deriveAlgorithm(ctx.peerKeyPair.publicKey), + ctx.keyPair.privateKey, + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt']))); + }; } - { - const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); - const missingKtyWrappedJwk = await subtle.encrypt( - { name: 'AES-CBC', iv }, - jwkUnwrappingKey, - Buffer.from('{"k":"AAAAAAAAAAAAAAAAAAAAAA"}')); + fixture.getPublicKey = async (ctx) => { + await assertCryptoKeyResult(`getPublicKey ${options.name}`, () => + subtle.getPublicKey(ctx.keyPair.privateKey, options.publicUsages)); + }; - await assertMissingJwkKtyIgnoresPrototype(() => - subtle.unwrapKey( - 'jwk', - missingKtyWrappedJwk, - jwkUnwrappingKey, - { name: 'AES-CBC', iv }, - { name: 'AES-CBC', length: 128 }, - true, - ['encrypt'])); - } + return fixture; } -await assertNoPromiseConstructorAccess('getPublicKey', () => - subtle.getPublicKey(extractableKeyPair.privateKey, ['verify'])); -await assertNoInheritedCryptoKeyThenAccess('getPublicKey', () => - subtle.getPublicKey(extractableKeyPair.privateKey, ['verify'])); - -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { - const kemPair = await subtle.generateKey( - { name: 'ML-KEM-768' }, true, - ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); +function kdfFixture(options) { + return { + ...options, + importKey: async (ctx) => { + ctx.key = await importCryptoKey( + options.name, + options.rawFormat, + new Uint8Array(32).fill(1), + options.importAlgorithm, + false, + ['deriveBits', 'deriveKey']); + }, + deriveBits: async (ctx) => { + await assertNoPromiseConstructorAccess(`deriveBits ${options.name}`, () => + subtle.deriveBits(options.deriveAlgorithm, ctx.key, 256)); + }, + extra: async (ctx) => { + await assertNoInheritedArrayBufferThenAccess(`deriveKey ${options.name}`, () => + assertCryptoKeyResult(`deriveKey ${options.name} result`, () => + subtle.deriveKey( + options.deriveAlgorithm, + ctx.key, + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt']))); + }, + }; +} - await assertNoInheritedArrayBufferThenAccess('exportKey raw-seed', () => - subtle.exportKey('raw-seed', kemPair.privateKey)); - await assertExportKeyNoPromiseConstructorAccess( - 'raw-seed', - 'raw-seed', - kemPair.privateKey); +function kemFixture(options) { + const fixture = pairKeyFixture({ + ...options, + usages: [ + 'encapsulateKey', + 'encapsulateBits', + 'decapsulateKey', + 'decapsulateBits', + ], + publicUsages: ['encapsulateKey', 'encapsulateBits'], + privateUsages: ['decapsulateKey', 'decapsulateBits'], + rawPublic: true, + rawSeed: true, + }); - const { ciphertext: ct1 } = - await assertNoRawSharedKeyObjectThenAccess('encapsulateKey', () => - assertNoPromiseConstructorAccess('encapsulateKey', () => + fixture.encapsulate = async (ctx) => { + ctx.encapsulatedBits = await assertNoPromiseConstructorAccess( + `encapsulateBits ${options.name}`, () => + subtle.encapsulateBits(algorithm(options.name), ctx.keyPair.publicKey)); + ctx.encapsulatedKey = await assertNoRawSharedKeyObjectThenAccess( + `encapsulateKey ${options.name}`, + () => assertNoPromiseConstructorAccess(`encapsulateKey ${options.name}`, () => subtle.encapsulateKey( - { name: 'ML-KEM-768' }, - kemPair.publicKey, + algorithm(options.name), + ctx.keyPair.publicKey, 'HKDF', false, ['deriveBits']))); + }; + fixture.decapsulate = async (ctx) => { + await assertNoPromiseConstructorAccess(`decapsulateBits ${options.name}`, () => + subtle.decapsulateBits( + algorithm(options.name), + ctx.keyPair.privateKey, + ctx.encapsulatedBits.ciphertext)); + await assertNoInheritedArrayBufferThenAccess(`decapsulateKey ${options.name}`, () => + assertCryptoKeyResult(`decapsulateKey ${options.name} result`, () => + subtle.decapsulateKey( + algorithm(options.name), + ctx.keyPair.privateKey, + ctx.encapsulatedKey.ciphertext, + 'HKDF', + false, + ['deriveBits']))); + }; + + return fixture; +} + +// The fixture registry mirrors kSupportedAlgorithms by algorithm name. Each +// fixture supplies the public SubtleCrypto calls needed to exercise the +// registered operations for that algorithm. +const fixtures = new Map(); + +function addFixture(name, fixture) { + fixtures.set(name, fixture); +} + +for (const name of ['AES-CBC', 'AES-CTR', 'AES-GCM', 'AES-OCB']) { + const encryptAlgorithms = { + 'AES-CBC': algorithm(name, { iv: new Uint8Array(16) }), + 'AES-CTR': algorithm(name, { counter: new Uint8Array(16), length: 64 }), + 'AES-GCM': algorithm(name, { + iv: new Uint8Array(12), + additionalData: new Uint8Array(1), + tagLength: 128, + }), + 'AES-OCB': algorithm(name, { + iv: new Uint8Array(15), + additionalData: new Uint8Array(1), + tagLength: 128, + }), + }; + addFixture(name, secretKeyFixture({ + name, + generateAlgorithm: algorithm(name, { length: 128 }), + importAlgorithm: algorithm(name), + usages: ['encrypt', 'decrypt'], + rawFormat: 'raw-secret', + encryptAlgorithm: encryptAlgorithms[name], + })); +} + +addFixture('AES-KW', secretKeyFixture({ + name: 'AES-KW', + generateAlgorithm: algorithm('AES-KW', { length: 128 }), + importAlgorithm: algorithm('AES-KW'), + usages: ['wrapKey', 'unwrapKey'], + rawFormat: 'raw-secret', + wrapAlgorithm: 'AES-KW', +})); + +addFixture('ChaCha20-Poly1305', secretKeyFixture({ + name: 'ChaCha20-Poly1305', + generateAlgorithm: algorithm('ChaCha20-Poly1305'), + importAlgorithm: algorithm('ChaCha20-Poly1305'), + usages: ['encrypt', 'decrypt'], + rawFormat: 'raw-secret', + encryptAlgorithm: algorithm('ChaCha20-Poly1305', { + iv: new Uint8Array(12), + additionalData: new Uint8Array(1), + tagLength: 128, + }), +})); + +addFixture('HMAC', secretKeyFixture({ + name: 'HMAC', + generateAlgorithm: algorithm('HMAC', { hash: 'SHA-256', length: 256 }), + importAlgorithm: algorithm('HMAC', { hash: 'SHA-256' }), + usages: ['sign', 'verify'], + rawFormat: 'raw-secret', + signAlgorithm: 'HMAC', +})); + +for (const name of ['KMAC128', 'KMAC256']) { + addFixture(name, secretKeyFixture({ + name, + generateAlgorithm: algorithm(name, { + length: name === 'KMAC128' ? 128 : 256, + }), + importAlgorithm: algorithm(name), + usages: ['sign', 'verify'], + rawFormat: 'raw-secret', + signAlgorithm: algorithm(name, { outputLength: 256 }), + })); +} + +for (const name of ['RSASSA-PKCS1-v1_5', 'RSA-PSS']) { + addFixture(name, pairKeyFixture({ + name, + generateAlgorithm: rsaAlgorithm(name), + importAlgorithm: importRsaAlgorithm(name), + usages: ['sign', 'verify'], + publicUsages: ['verify'], + privateUsages: ['sign'], + signAlgorithm: name === 'RSA-PSS' ? + algorithm(name, { saltLength: 32 }) : + algorithm(name), + })); +} + +addFixture('RSA-OAEP', pairKeyFixture({ + name: 'RSA-OAEP', + generateAlgorithm: rsaAlgorithm('RSA-OAEP'), + importAlgorithm: importRsaAlgorithm('RSA-OAEP'), + usages: ['encrypt', 'decrypt'], + publicUsages: ['encrypt'], + privateUsages: ['decrypt'], + encryptAlgorithm: algorithm('RSA-OAEP', { label: new Uint8Array(1) }), +})); + +addFixture('ECDSA', pairKeyFixture({ + name: 'ECDSA', + generateAlgorithm: algorithm('ECDSA', { namedCurve: 'P-256' }), + importAlgorithm: algorithm('ECDSA', { namedCurve: 'P-256' }), + usages: ['sign', 'verify'], + publicUsages: ['verify'], + privateUsages: ['sign'], + rawPublic: true, + signAlgorithm: algorithm('ECDSA', { hash: 'SHA-256' }), +})); + +addFixture('ECDH', pairKeyFixture({ + name: 'ECDH', + generateAlgorithm: algorithm('ECDH', { namedCurve: 'P-256' }), + importAlgorithm: algorithm('ECDH', { namedCurve: 'P-256' }), + usages: ['deriveBits', 'deriveKey'], + publicUsages: [], + privateUsages: ['deriveBits', 'deriveKey'], + rawPublic: true, + deriveAlgorithm: (publicKey) => algorithm('ECDH', { public: publicKey }), + deriveLength: 256, +})); + +for (const name of ['Ed25519', 'Ed448']) { + addFixture(name, pairKeyFixture({ + name, + generateAlgorithm: algorithm(name), + importAlgorithm: algorithm(name), + usages: ['sign', 'verify'], + publicUsages: ['verify'], + privateUsages: ['sign'], + rawPublic: true, + signAlgorithm: algorithm(name), + })); +} + +for (const name of ['X25519', 'X448']) { + addFixture(name, pairKeyFixture({ + name, + generateAlgorithm: algorithm(name), + importAlgorithm: algorithm(name), + usages: ['deriveBits', 'deriveKey'], + publicUsages: [], + privateUsages: ['deriveBits', 'deriveKey'], + rawPublic: true, + deriveAlgorithm: (publicKey) => algorithm(name, { public: publicKey }), + deriveLength: name === 'X25519' ? 256 : 448, + })); +} + +for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + addFixture(name, pairKeyFixture({ + name, + generateAlgorithm: algorithm(name), + importAlgorithm: algorithm(name), + usages: ['sign', 'verify'], + publicUsages: ['verify'], + privateUsages: ['sign'], + rawPublic: true, + rawSeed: true, + signAlgorithm: algorithm(name), + })); +} + +for (const name of [ + 'ML-KEM-512', + 'ML-KEM-768', + 'ML-KEM-1024', +]) { + addFixture(name, kemFixture({ + name, + generateAlgorithm: algorithm(name), + importAlgorithm: algorithm(name), + })); +} + +for (const name of ['HKDF', 'PBKDF2']) { + addFixture(name, kdfFixture({ + name, + importAlgorithm: name, + rawFormat: 'raw-secret', + deriveAlgorithm: name === 'HKDF' ? + algorithm(name, { + hash: 'SHA-256', + salt: new Uint8Array(8), + info: new Uint8Array(8), + }) : + algorithm(name, { + hash: 'SHA-256', + salt: new Uint8Array(8), + iterations: 1, + }), + })); +} + +for (const name of ['Argon2d', 'Argon2i', 'Argon2id']) { + addFixture(name, kdfFixture({ + name, + importAlgorithm: name, + rawFormat: 'raw-secret', + deriveAlgorithm: algorithm(name, { + memory: 32, + passes: 1, + parallelism: 1, + nonce: new Uint8Array(16), + }), + })); +} + +function digestAlgorithm(name) { + if (name === 'cSHAKE128') + return algorithm(name, { outputLength: 256 }); + if (name === 'cSHAKE256') + return algorithm(name, { outputLength: 512 }); + if (name === 'TurboSHAKE128') + return algorithm(name, { outputLength: 256 }); + if (name === 'TurboSHAKE256') + return algorithm(name, { outputLength: 512 }); + if (name === 'KT128') + return algorithm(name, { outputLength: 256 }); + if (name === 'KT256') + return algorithm(name, { outputLength: 512 }); + return name; +} + +for (const name of [ + 'SHA-1', + 'SHA-256', + 'SHA-384', + 'SHA-512', + 'SHA3-256', + 'SHA3-384', + 'SHA3-512', + 'cSHAKE128', + 'cSHAKE256', + 'TurboSHAKE128', + 'TurboSHAKE256', + 'KT128', + 'KT256', +]) { + addFixture(name, { + name, + digest: async () => { + await assertNoPromiseConstructorAccess(`digest ${name}`, () => + subtle.digest(digestAlgorithm(name), data)); + }, + }); +} + +// deriveKey() is the only public API that performs the "get key length" +// registry operation. Keep this table in sync with algorithms that can be a +// concrete derived-key target. +const keyLengthTargets = { + 'AES-CBC': { + algorithm: algorithm('AES-CBC', { length: 128 }), + usages: ['encrypt'], + }, + 'AES-CTR': { + algorithm: algorithm('AES-CTR', { length: 128 }), + usages: ['encrypt'], + }, + 'AES-GCM': { + algorithm: algorithm('AES-GCM', { length: 128 }), + usages: ['encrypt'], + }, + 'AES-KW': { + algorithm: algorithm('AES-KW', { length: 128 }), + usages: ['wrapKey'], + }, + 'AES-OCB': { + algorithm: algorithm('AES-OCB', { length: 128 }), + usages: ['encrypt'], + }, + 'ChaCha20-Poly1305': { + algorithm: algorithm('ChaCha20-Poly1305'), + usages: ['encrypt'], + }, + 'HMAC': { + algorithm: algorithm('HMAC', { hash: 'SHA-256', length: 256 }), + usages: ['sign'], + }, + 'KMAC128': { + algorithm: algorithm('KMAC128', { length: 128 }), + usages: ['sign'], + }, + 'KMAC256': { + algorithm: algorithm('KMAC256', { length: 256 }), + usages: ['sign'], + }, +}; + +function getSupportedAlgorithmOperations() { + const algorithms = new Map(); + for (const operation of Object.keys(kSupportedAlgorithms)) { + if (operation === 'get key length') + continue; + for (const name of Object.keys(kSupportedAlgorithms[operation])) { + if (!algorithms.has(name)) + algorithms.set(name, new Set()); + algorithms.get(name).add(operation); + } + } + return algorithms; +} + +// This is the list of supported public registry operations that this file +// exercises. A new operation name must be added here before the registry +// assertion below will pass. +const operationOrder = [ + 'digest', + 'generateKey', + 'exportKey', + 'importKey', + 'encrypt', + 'decrypt', + 'sign', + 'verify', + 'deriveBits', + 'encapsulate', + 'decapsulate', + 'wrapKey', + 'unwrapKey', +]; + +const coveredOperations = new Set([ + ...operationOrder, + 'get key length', +]); + +for (const operation of Object.keys(kSupportedAlgorithms)) { + assert( + coveredOperations.has(operation), + `missing prototype pollution operation coverage for ${operation}`); +} + +const supportedAlgorithms = getSupportedAlgorithmOperations(); +for (const [name, operations] of supportedAlgorithms) { + const fixture = fixtures.get(name); + assert(fixture, `missing prototype pollution fixture for ${name}`); + + const ctx = { __proto__: null }; + for (const operation of operationOrder) { + if (!operations.has(operation)) + continue; + assert.strictEqual( + typeof fixture[operation], + 'function', + `missing prototype pollution coverage for ${name} ${operation}`); + await fixture[operation](ctx); + } + + if (typeof fixture.getPublicKey === 'function' && + ctx.keyPair?.privateKey !== undefined) { + await fixture.getPublicKey(ctx); + } + if (typeof fixture.extra === 'function') + await fixture.extra(ctx); +} + +const getKeyLengthAlgorithms = + Object.keys(kSupportedAlgorithms['get key length'] ?? {}); +// KDF base algorithms return null from getKeyLength(). They still need to be +// listed explicitly so a newly registered get-key-length algorithm does not +// silently skip prototype-pollution coverage. +const nullKeyLengthAlgorithms = [ + 'HKDF', + 'PBKDF2', + 'Argon2d', + 'Argon2i', + 'Argon2id', +]; +const pbkdf2Key = await subtle.importKey( + 'raw-secret', + new Uint8Array(32).fill(1), + 'PBKDF2', + false, + ['deriveKey']); +for (const name of getKeyLengthAlgorithms) { + const target = keyLengthTargets[name]; + if (target === undefined) { + assert( + nullKeyLengthAlgorithms.includes(name), + `missing get key length coverage for ${name}`); + continue; + } + + await assertCryptoKeyResult(`get key length ${name}`, () => + subtle.deriveKey( + algorithm('PBKDF2', { + hash: 'SHA-256', + salt: new Uint8Array(8), + iterations: 1, + }), + pbkdf2Key, + target.algorithm, + true, + target.usages)); +} + +// Keep one explicit unwrapKey('jwk') negative case: the parsed object must not +// inherit kty from Object.prototype when the wrapped JSON does not have it. +{ + const jwkUnwrappingKey = await subtle.generateKey( + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt', 'unwrapKey']); + const iv = new Uint8Array(16); + const missingKtyWrappedJwk = await subtle.encrypt( + algorithm('AES-CBC', { iv }), + jwkUnwrappingKey, + new TextEncoder().encode('{"k":"AAAAAAAAAAAAAAAAAAAAAA"}')); - await assertNoPromiseConstructorAccess('decapsulateKey', () => - subtle.decapsulateKey( - { name: 'ML-KEM-768' }, - kemPair.privateKey, - ct1, - 'HKDF', - false, - ['deriveBits'])); - await assertNoInheritedArrayBufferThenAccess('decapsulateKey', () => - subtle.decapsulateKey( - { name: 'ML-KEM-768' }, - kemPair.privateKey, - ct1, - 'HKDF', - false, - ['deriveBits'])); - await assertNoInheritedCryptoKeyThenAccess('decapsulateKey result', () => - subtle.decapsulateKey( - { name: 'ML-KEM-768' }, - kemPair.privateKey, - ct1, - 'HKDF', - false, - ['deriveBits'])); - - const { ciphertext: ct2 } = - await assertNoPromiseConstructorAccess('encapsulateBits', () => - subtle.encapsulateBits( - { name: 'ML-KEM-768' }, - kemPair.publicKey)); - - await assertNoPromiseConstructorAccess('decapsulateBits', () => - subtle.decapsulateBits( - { name: 'ML-KEM-768' }, - kemPair.privateKey, - ct2)); + await assertMissingJwkKtyIgnoresPrototype(() => + subtle.unwrapKey( + 'jwk', + missingKtyWrappedJwk, + jwkUnwrappingKey, + algorithm('AES-CBC', { iv }), + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt'])); } From b9203ee79fbf6d8800e968ce095c8431619779f4 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 25 May 2026 19:20:10 +0200 Subject: [PATCH 38/89] src,sqlite: only pass `xFilter` when user provided a callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antoine du Hamel PR-URL: https://github.com/nodejs/node/pull/63516 Reviewed-By: Colin Ihrig Reviewed-By: René Reviewed-By: Edy Silva --- src/node_sqlite.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index f23f25ba0d58fe..522d3c24cfba70 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -2237,7 +2237,6 @@ static int xConflict(void* pCtx, int eConflict, sqlite3_changeset_iter* pIter) { static int xFilter(void* pCtx, const char* zTab) { auto ctx = static_cast(pCtx); - if (!ctx->filterCallback) return 1; return ctx->filterCallback(zTab) ? 1 : 0; } @@ -2348,7 +2347,7 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo& args) { db->connection_, buf.length(), const_cast(static_cast(buf.data())), - xFilter, + context.filterCallback ? xFilter : nullptr, xConflict, static_cast(&context)); if (r == SQLITE_OK) { From e2ad744e5f9f4711bfbc5c0a08f963ada958d870 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Mon, 25 May 2026 23:52:00 +0200 Subject: [PATCH 39/89] test: remove test-node-output-v8-warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit asm.js validation is deprecated and disabled by default in V8, so there's no longer a stable user-reachable trigger for `v8::Isolate::kMessageWarning`. Remove the test. Refs: https://chromium-review.googlesource.com/c/v8/v8/+/7832184 Signed-off-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/63469 Fixes: https://github.com/nodejs/node-v8/issues/310 Refs: https://issues.chromium.org/issues/510487707 Refs: https://github.com/nodejs/node/pull/24365 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/7832184 Reviewed-By: Michaël Zasso Reviewed-By: Antoine du Hamel Reviewed-By: Richard Lau Reviewed-By: Luigi Pinca Reviewed-By: Chengzhong Wu --- test/fixtures/v8/v8_warning.js | 19 ------------------- test/fixtures/v8/v8_warning.snapshot | 2 -- test/parallel/test-node-output-v8-warning.mjs | 15 --------------- 3 files changed, 36 deletions(-) delete mode 100644 test/fixtures/v8/v8_warning.js delete mode 100644 test/fixtures/v8/v8_warning.snapshot delete mode 100644 test/parallel/test-node-output-v8-warning.mjs diff --git a/test/fixtures/v8/v8_warning.js b/test/fixtures/v8/v8_warning.js deleted file mode 100644 index ab4d2bf305823f..00000000000000 --- a/test/fixtures/v8/v8_warning.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -require('../../common'); - -function AsmModule() { - 'use asm'; - - function add(a, b) { - a = a | 0; - b = b | 0; - - // Should be `return (a + b) | 0;` - return a + b; - } - - return { add: add }; -} - -AsmModule(); diff --git a/test/fixtures/v8/v8_warning.snapshot b/test/fixtures/v8/v8_warning.snapshot deleted file mode 100644 index 87c7c86b4fedeb..00000000000000 --- a/test/fixtures/v8/v8_warning.snapshot +++ /dev/null @@ -1,2 +0,0 @@ -(node:) V8: /test/fixtures/v8/v8_warning.js:13 Invalid asm.js: Invalid return type -(Use ` --trace-warnings ...` to show where the warning was created) diff --git a/test/parallel/test-node-output-v8-warning.mjs b/test/parallel/test-node-output-v8-warning.mjs deleted file mode 100644 index 43f6709dc248c7..00000000000000 --- a/test/parallel/test-node-output-v8-warning.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import '../common/index.mjs'; -import * as fixtures from '../common/fixtures.mjs'; -import * as snapshot from '../common/assertSnapshot.js'; -import { describe, it } from 'node:test'; - -describe('v8 output', { concurrency: !process.env.TEST_PARALLEL }, () => { - const tests = [ - { name: 'v8/v8_warning.js' }, - ]; - for (const { name } of tests) { - it(name, async () => { - await snapshot.spawnAndAssert(fixtures.path(name), snapshot.defaultTransform); - }); - } -}); From 73c592be2f0ed69d2a7804ca5aad14dcb1816950 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Mon, 25 May 2026 22:03:07 -0400 Subject: [PATCH 40/89] tools: update nixpkgs-unstable to 3d8f0f3f72a6cd4d93d0ad13203f2ea1cb7 PR-URL: https://github.com/nodejs/node/pull/63528 Reviewed-By: Filip Skokan Reviewed-By: Colin Ihrig --- tools/nix/pkgs.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/nix/pkgs.nix b/tools/nix/pkgs.nix index 66f809d8b23382..512a184420a267 100644 --- a/tools/nix/pkgs.nix +++ b/tools/nix/pkgs.nix @@ -1,10 +1,10 @@ arg: let repo = "https://github.com/NixOS/nixpkgs"; - rev = "d233902339c02a9c334e7e593de68855ad26c4cb"; + rev = "3d8f0f3f72a6cd4d93d0ad13203f2ea1cb7e1456"; nixpkgs = import (builtins.fetchTarball { url = "${repo}/archive/${rev}.tar.gz"; - sha256 = "1485vqhb8cwym1m75v61i10j427vazszaklkwj2wmm80k8sijjyz"; + sha256 = "1vq4c8nfn16zcz9sa3rjy4bhabdmnwy4pq3pdj20gzmgd3iwbrxb"; }) arg; in nixpkgs From 339339986d9ae27cdcde5780d6395712cb834010 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 24 May 2026 10:37:02 +0200 Subject: [PATCH 41/89] crypto: coerce -0 keylen to +0 in pbkdf2 and scrypt `validateInt32(keylen, 'keylen', 0)` lets `-0` through: `typeof -0` is `'number'`, `Number.isInteger(-0)` is `true`, and `-0 < 0` is `false`. The value then reaches the PBKDF2Job binding, whose `IsInt32()` check fails (V8 boxes `-0` as a HeapNumber rather than a tagged SMI) and aborts the process with SIGABRT. Coerce `keylen` to `+0` after validation so the binding sees a true Int32. Reachable from any caller that forwards a JSON-parsed value, since `JSON.parse('{"keylen":-0}').keylen` preserves the sign. Mirror of the prior pbkdf2 fix. `validateInt32(keylen, 'keylen', 0)` lets `-0` through (since `-0 < 0` is `false`), and the ScryptJob binding's `IsInt32()` check at `crypto_scrypt.cc` aborts the process with SIGABRT because V8 boxes `-0` as a HeapNumber rather than a tagged SMI. Coerce `keylen` to `+0` after validation. Signed-off-by: Jordan Harband PR-URL: https://github.com/nodejs/node/pull/63531 Reviewed-By: Filip Skokan Reviewed-By: James M Snell Reviewed-By: Luigi Pinca --- lib/internal/crypto/pbkdf2.js | 2 ++ lib/internal/crypto/scrypt.js | 2 ++ test/parallel/test-crypto-pbkdf2.js | 29 +++++++++++++++++++++++++++++ test/parallel/test-crypto-scrypt.js | 27 +++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 87f763da9ba8a3..f42ce3bbd133d5 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -92,6 +92,8 @@ function check(password, salt, iterations, keylen, digest) { // to the 31-bit range here (which is plenty). validateInt32(iterations, 'iterations', 1); validateInt32(keylen, 'keylen', 0); + // Coerce -0 to +0. + keylen += 0; return { password, salt, iterations, keylen, digest }; } diff --git a/lib/internal/crypto/scrypt.js b/lib/internal/crypto/scrypt.js index ce170dff3b8344..14e5f0ac68ce74 100644 --- a/lib/internal/crypto/scrypt.js +++ b/lib/internal/crypto/scrypt.js @@ -83,6 +83,8 @@ function check(password, salt, keylen, options) { password = getArrayBufferOrView(password, 'password'); salt = getArrayBufferOrView(salt, 'salt'); validateInt32(keylen, 'keylen', 0); + // Coerce -0 to +0. + keylen += 0; let { N, r, p, maxmem } = defaults; if (options && options !== defaults) { diff --git a/test/parallel/test-crypto-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js index efd8d6eaf0d640..78b73ed6c4e0d0 100644 --- a/test/parallel/test-crypto-pbkdf2.js +++ b/test/parallel/test-crypto-pbkdf2.js @@ -110,6 +110,35 @@ for (const iterations of [-1, 0, 2147483648]) { }); }); +// `-0` keylen must not abort the process via the native binding's +// IsInt32() assertion. Behavior of `keylen=0` itself varies by OpenSSL +// build (bundled returns an empty buffer; some shared OpenSSL builds +// throw); the requirement here is only that `-0` produces the same +// outcome as `+0`. +{ + let posError; + let posResult; + try { + posResult = crypto.pbkdf2Sync('password', 'salt', 1, 0, 'sha256'); + } catch (err) { + posError = err; + } + let negError; + let negResult; + try { + negResult = crypto.pbkdf2Sync('password', 'salt', 1, -0, 'sha256'); + } catch (err) { + negError = err; + } + if (posError !== undefined) { + assert.strictEqual(negError?.message, posError.message); + } else { + assert.deepStrictEqual(negResult, posResult); + } + + crypto.pbkdf2('password', 'salt', 1, -0, 'sha256', common.mustCall()); +} + // Should not get FATAL ERROR with empty password and salt // https://github.com/nodejs/node/issues/8571 crypto.pbkdf2('', '', 1, 32, 'sha256', common.mustSucceed()); diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index 5effc083cda11a..421ee4ce8f3490 100644 --- a/test/parallel/test-crypto-scrypt.js +++ b/test/parallel/test-crypto-scrypt.js @@ -274,3 +274,30 @@ for (const { args, expected } of badargs) { ['p', 1], ['parallelization', 1], ].forEach((arg) => testParameter(...arg)); } + +// `-0` keylen must not abort the process via the native binding's +// IsInt32() assertion. Assert that `-0` produces the same outcome as +// `+0` (which differs by OpenSSL build). +{ + let posError; + let posResult; + try { + posResult = crypto.scryptSync('', '', 0); + } catch (err) { + posError = err; + } + let negError; + let negResult; + try { + negResult = crypto.scryptSync('', '', -0); + } catch (err) { + negError = err; + } + if (posError !== undefined) { + assert.strictEqual(negError?.message, posError.message); + } else { + assert.deepStrictEqual(negResult, posResult); + } + + crypto.scrypt('', '', -0, common.mustCall()); +} From 8bb63edee771faf5d16ba2831536c3d978f75afe Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 26 May 2026 06:14:18 -0400 Subject: [PATCH 42/89] crypto: update root certificates to NSS 3.123.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the [`certdata.txt`][0] from NSS 3.123.1. This is the version of NSS that shipped in Firefox 151.0.1 on 2026-05-21 Certificates removed: - QuoVadis Root CA 2 - QuoVadis Root CA 3 - DigiCert Assured ID Root CA - DigiCert Global Root CA - DigiCert High Assurance EV Root CA - SwissSign Gold CA - G2 - SecureTrust CA - Secure Global CA - COMODO Certification Authority - Certigna - certSIGN ROOT CA - Izenpe.com - AffirmTrust Commercial - AffirmTrust Networking - AffirmTrust Premium - AffirmTrust Premium ECC - TeliaSonera Root CA v1 - Entrust Root Certification Authority - G2 - Entrust Root Certification Authority - EC1 - Trustwave Global Certification Authority - Trustwave Global ECC P256 Certification Authority - Trustwave Global ECC P384 Certification Authority - GLOBALTRUST 2020 - GTS Root R2 - FIRMAPROFESIONAL CA ROOT-A WEB [0]: https://raw.githubusercontent.com/nss-dev/nss/refs/tags/NSS_3_123_1_RTM/lib/ckfw/builtins/certdata.txt PR-URL: https://github.com/nodejs/node/pull/63527 Reviewed-By: Luigi Pinca Reviewed-By: Gürgün Dayıoğlu --- src/node_root_certs.h | 608 ------------ tools/certdata.txt | 2059 +---------------------------------------- 2 files changed, 39 insertions(+), 2628 deletions(-) diff --git a/src/node_root_certs.h b/src/node_root_certs.h index e3c77a175f9ccf..53ac2034c8a810 100644 --- a/src/node_root_certs.h +++ b/src/node_root_certs.h @@ -26,240 +26,6 @@ "j2A781q0tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8\n" "-----END CERTIFICATE-----", -/* QuoVadis Root CA 2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNV\n" -"BAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0w\n" -"NjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBR\n" -"dW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqG\n" -"SIb3DQEBAQUAA4ICDwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4Gt\n" -"Mh6QRr+jhiYaHv5+HBg6XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp\n" -"3MJGF/hd/aTa/55JWpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsR\n" -"E8Scd3bBrrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp\n" -"+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI\n" -"0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2\n" -"BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIizPtGo/KPaHbDRsSNU30R2be1B\n" -"2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOhD7osFRXql7PSorW+8oyWHhqPHWyk\n" -"YTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyP\n" -"ZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQAB\n" -"o4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwz\n" -"JQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL\n" -"MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1Zh\n" -"ZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUvZ+YT\n" -"RYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3\n" -"UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgt\n" -"JodmVjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q8\n" -"0m/DShcK+JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W\n" -"6ZM/57Es3zrWIozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQj\n" -"rLhVoQPRTUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD\n" -"mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6y\n" -"hhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO\n" -"1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAF\n" -"ZdWCEOrCMc0u\n" -"-----END CERTIFICATE-----", - -/* QuoVadis Root CA 3 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNV\n" -"BAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0w\n" -"NjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBR\n" -"dW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqG\n" -"SIb3DQEBAQUAA4ICDwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTP\n" -"krgEQK0CSzGrvI2RaNggDhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZ\n" -"z3HmDyl2/7FWeUUrH556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2Objyj\n" -"Ptr7guXd8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv\n" -"vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mta\n" -"a7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJ\n" -"k8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1\n" -"ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEXMJPpGovgc2PZapKUSU60rUqFxKMi\n" -"MPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArl\n" -"zW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQAB\n" -"o4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMw\n" -"gcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0\n" -"aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0aWZpY2F0\n" -"ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYBBQUH\n" -"AgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD\n" -"VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1\n" -"XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEb\n" -"MBkGA1UEAxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62g\n" -"LEz6wPJv92ZVqyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon\n" -"24QRiSemd1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd\n" -"+LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hR\n" -"OJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j5\n" -"6hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6l\n" -"i92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8S\n" -"h17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7\n" -"j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEo\n" -"kt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7\n" -"zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto=\n" -"-----END CERTIFICATE-----", - -/* DigiCert Assured ID Root CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYD\n" -"VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n" -"Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAw\n" -"MDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQg\n" -"SW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1\n" -"cmVkIElEIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOA\n" -"XLGH87dg+XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT\n" -"XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\n" -"wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/l\n" -"bQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcX\n" -"xH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQE\n" -"AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAf\n" -"BgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog68\n" -"3+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqo\n" -"R+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+\n" -"fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\n" -"H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe+o0bJW1s\n" -"j6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n" -"-----END CERTIFICATE-----", - -/* DigiCert Global Root CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYD\n" -"VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n" -"Y29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa\n" -"Fw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx\n" -"GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBS\n" -"b290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKP\n" -"C3eQyaKl7hLOllsBCSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscF\n" -"s3YnFo97nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" -"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6g\n" -"SzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSii\n" -"cNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYD\n" -"VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm8KPiGxvDl7I90VUwHwYDVR0jBBgw\n" -"FoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1E\n" -"nE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDi\n" -"qw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBA\n" -"I+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" -"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQkCAUw7C29\n" -"C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" -"-----END CERTIFICATE-----", - -/* DigiCert High Assurance EV Root CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYD\n" -"VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu\n" -"Y29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2\n" -"MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp\n" -"Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNl\n" -"cnQgSGlnaCBBc3N1cmFuY2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" -"AQoCggEBAMbM5XPm+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlB\n" -"WTrT3JTWPNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n" -"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeB\n" -"QVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5\n" -"OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsgEsxBu24LUTi4S8sCAwEAAaNj\n" -"MGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9H\n" -"AdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3\n" -"DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1\n" -"ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VH\n" -"MWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n" -"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCevEsXCS+0\n" -"yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K\n" -"-----END CERTIFICATE-----", - -/* SwissSign Gold CA - G2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNI\n" -"MRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0g\n" -"RzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMG\n" -"A1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIIC\n" -"IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJC\n" -"Eyq8ZVeCQD5XJM1QiyUqt2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcf\n" -"DmJlD909Vopz2q5+bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpi\n" -"kJKVyh+c6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE\n" -"emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT\n" -"28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdV\n" -"xVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02yMszYF9rNt85mndT9Xv+9lz4p\n" -"ded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkOpeUDDniOJihC8AcLYiAQZzlG+qkD\n" -"zAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR7ySArqpWl2/5rX3aYT+Ydzyl\n" -"kbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+Zr\n" -"zsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E\n" -"FgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn\n" -"8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDovL3JlcG9z\n" -"aXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm5djV\n" -"9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr\n" -"44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8\n" -"AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0V\n" -"qbe/vd6mGu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9Qkvfsywe\n" -"xcZdylU6oJxpmo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/Eb\n" -"MFYOkrCChdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3\n" -"92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG\n" -"2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/Y\n" -"YPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkk\n" -"W8mw0FfB+j564ZfJ\n" -"-----END CERTIFICATE-----", - -/* SecureTrust CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYD\n" -"VQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNl\n" -"Y3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UE\n" -"BhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1\n" -"cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7C\n" -"T8rU4niVWJxB4Q2ZQCQXOZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29\n" -"vo6pQT64lO0pGtSO0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZ\n" -"bf2IzIaowW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n" -"7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xH\n" -"CzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIE\n" -"Bh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE\n" -"/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2NybC5zZWN1cmV0cnVz\n" -"dC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDt\n" -"T0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQ\n" -"f2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cp\n" -"rp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\n" -"CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR3ItHuuG5\n" -"1WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n" -"-----END CERTIFICATE-----", - -/* Secure Global CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYD\n" -"VQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNl\n" -"Y3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYD\n" -"VQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNl\n" -"Y3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxV\n" -"aQZx5RNoJLNP2MwhR/jxYDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6Mpjh\n" -"HZevj8fcyTiW89sa/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ\n" -"/kG5VacJjnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\n" -"HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPi\n" -"XB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGC\n" -"NxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9E\n" -"BMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJl\n" -"dHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IB\n" -"AQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQV\n" -"DpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895\n" -"P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\n" -"iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xcf8LDmBxr\n" -"ThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n" -"-----END CERTIFICATE-----", - -/* COMODO Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkG\n" -"A1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9y\n" -"ZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZp\n" -"Y2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQsw\n" -"CQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm\n" -"b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRp\n" -"ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECL\n" -"i3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI\n" -"2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7eu\n" -"NJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC\n" -"8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQF\n" -"ZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4GOMIGLMB0GA1Ud\n" -"DgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw\n" -"AwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9D\n" -"ZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5\n" -"t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv\n" -"IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/RxdMosIG\n" -"lgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmcIGfE\n" -"7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN\n" -"+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ==\n" -"-----END CERTIFICATE-----", - /* COMODO ECC Certification Authority */ "-----BEGIN CERTIFICATE-----\n" "MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UE\n" @@ -277,28 +43,6 @@ "V9mSOdY=\n" "-----END CERTIFICATE-----", -/* Certigna */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZS\n" -"MRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMw\n" -"NVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczER\n" -"MA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ\n" -"1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lI\n" -"zw7sebYs5zRLcAglozyHGxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxr\n" -"yIRWijOp5yIVUxbwzBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJb\n" -"zg4ij02Q130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2\n" -"JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0T\n" -"AQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AU\n" -"Gu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlt\n" -"eW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEG\n" -"CWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl\n" -"1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxA\n" -"GYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9q\n" -"cEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w\n" -"t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/QwWyH8EZE0\n" -"vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==\n" -"-----END CERTIFICATE-----", - /* ePKI Root Certification Authority */ "-----BEGIN CERTIFICATE-----\n" "MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYD\n" @@ -331,26 +75,6 @@ "EZw=\n" "-----END CERTIFICATE-----", -/* certSIGN ROOT CA */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREw\n" -"DwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQx\n" -"NzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lH\n" -"TjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" -"AQoCggEBALczuX7IJUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oq\n" -"rl0Hj0rDKH/v+yv6efHHrfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsA\n" -"fsT8AzNXDe3i+s5dRdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUo\n" -"Se1b16kQOA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv\n" -"JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNC\n" -"MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPx\n" -"fIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJLjX8+HXd5n9liPRyTMks1zJO\n" -"890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6\n" -"IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KT\n" -"afcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI\n" -"0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5V\n" -"aZVDADlN9u6wWk5JRFRYX0KD\n" -"-----END CERTIFICATE-----", - /* NetLock Arany (Class Gold) Főtanúsítvány */ "-----BEGIN CERTIFICATE-----\n" "MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTER\n" @@ -420,39 +144,6 @@ "WD9f\n" "-----END CERTIFICATE-----", -/* Izenpe.com */ -"-----BEGIN CERTIFICATE-----\n" -"MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYD\n" -"VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcN\n" -"MDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwL\n" -"SVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC\n" -"DwAwggIKAoICAQDJ03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5Tz\n" -"cqQsRNiekpsUOqHnJJAKClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpz\n" -"bm3benhB6QiIEn6HLmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJ\n" -"GjMxCrFXuaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD\n" -"yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8\n" -"hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG7\n" -"0t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyNBjNaooXlkDWgYlwWTvDjovoD\n" -"GrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+0rnq49qlw0dpEuDb8PYZi+17cNcC\n" -"1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQD\n" -"fo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNV\n" -"HREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4g\n" -"LSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB\n" -"BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAxMCBWaXRv\n" -"cmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\n" -"FB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l\n" -"Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9\n" -"fbgakEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJO\n" -"ubv5vr8qhT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m\n" -"5hzkQiCeR7Csg1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Py\n" -"e6kfLqCTVyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk\n" -"LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqt\n" -"ujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZ\n" -"pR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6i\n" -"SNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE4\n" -"1V4tC5h9Pmzb/CaIxw==\n" -"-----END CERTIFICATE-----", - /* Go Daddy Root Certificate Authority - G2 */ "-----BEGIN CERTIFICATE-----\n" "MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNV\n" @@ -521,90 +212,6 @@ "/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6\n" "-----END CERTIFICATE-----", -/* AffirmTrust Commercial */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMx\n" -"FDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFs\n" -"MB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNV\n" -"BAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjAN\n" -"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTW\n" -"zsO3qyxPxkEylFf6EqdbDuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U\n" -"6Mje+SJIZMblq8Yrba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNA\n" -"FxHUdPALMeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1\n" -"yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1J\n" -"dX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8w\n" -"DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAFis\n" -"9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M\n" -"06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1Ua\n" -"ADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjip\n" -"M1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclN\n" -"msxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=\n" -"-----END CERTIFICATE-----", - -/* AffirmTrust Networking */ -"-----BEGIN CERTIFICATE-----\n" -"MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMx\n" -"FDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5n\n" -"MB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNV\n" -"BAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjAN\n" -"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWRE\n" -"ZY9nZOIG41w3SfYvm4SEHi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ\n" -"/Ls6rnla1fTWcbuakCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXL\n" -"viRmVSRLQESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp\n" -"6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKB\n" -"Nv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0w\n" -"DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAIlX\n" -"shZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t\n" -"3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA\n" -"3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzek\n" -"ujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfx\n" -"ojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=\n" -"-----END CERTIFICATE-----", - -/* AffirmTrust Premium */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMx\n" -"FDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4X\n" -"DTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoM\n" -"C0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG\n" -"9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64t\n" -"b+eT2TZwamjPjlGjhVtnBKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/\n" -"0qRY7iZNyaqoe5rZ+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/\n" -"K+k8rNrSs8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5\n" -"HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua\n" -"2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/\n" -"9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+SqHZGnEJlPqQewQcDWkYtuJfz\n" -"t9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m\n" -"6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKP\n" -"KrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNC\n" -"MEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYD\n" -"VR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2\n" -"KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMgNt58D2kT\n" -"iKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC6C1Y\n" -"91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S\n" -"L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQ\n" -"wUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFo\n" -"oC8k4gmVBtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5Yw\n" -"H2AG7hsj/oFgIxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/\n" -"qzWaVYa8GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO\n" -"RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAlo\n" -"GRwYQw==\n" -"-----END CERTIFICATE-----", - -/* AffirmTrust Premium ECC */ -"-----BEGIN CERTIFICATE-----\n" -"MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDAS\n" -"BgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAe\n" -"Fw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQK\n" -"DAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcq\n" -"hkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQU\n" -"X+iOGasvLkjmrBhDeKzQN8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR\n" -"4ptlKymjQjBAMB0GA1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTAD\n" -"AQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs\n" -"aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9C\n" -"a/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==\n" -"-----END CERTIFICATE-----", - /* Certum Trusted Network CA */ "-----BEGIN CERTIFICATE-----\n" "MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYD\n" @@ -933,35 +540,6 @@ "aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0=\n" "-----END CERTIFICATE-----", -/* TeliaSonera Root CA v1 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIG\n" -"A1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcN\n" -"MDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEf\n" -"MB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIP\n" -"ADCCAgoCggIBAMK+6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3\n" -"t+XmfHnqjLWCi65ItqwA3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq\n" -"/t75rH2D+1665I+XZ75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1\n" -"jF3oI7x+/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs\n" -"81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAg\n" -"HNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzT\n" -"jU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMusDor8zagrC/kb2HCUQk5PotT\n" -"ubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7Rc\n" -"We/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUB\n" -"iJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB\n" -"/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjAN\n" -"BgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl\n" -"dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx0GtnLLCo\n" -"4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfWpb/I\n" -"mWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV\n" -"G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KF\n" -"dSpcc41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrE\n" -"gUy7onOTJsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQ\n" -"mz1wHiRszYd2qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfuj\n" -"uLpwQMcnHL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx\n" -"SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY=\n" -"-----END CERTIFICATE-----", - /* T-TeleSec GlobalRoot Class 2 */ "-----BEGIN CERTIFICATE-----\n" "MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNV\n" @@ -1355,50 +933,6 @@ "BOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c\n" "-----END CERTIFICATE-----", -/* Entrust Root Certification Authority - G2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAU\n" -"BgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn\n" -"YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9y\n" -"aXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0\n" -"aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UE\n" -"BhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVz\n" -"dC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBm\n" -"b3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj\n" -"YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6\n" -"hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3\n" -"gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWNcCG0szLni6LVhjkCsbjSR87k\n" -"yUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKUs/Ja5CeanyTXxuzQmyWC48zCxEXF\n" -"jJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+\n" -"tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1Ud\n" -"DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2f\n" -"kBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/\n" -"jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZRkfz6/dj\n" -"wUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDginWyT\n" -"msQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+\n" -"vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ\n" -"19xOe4pIb4tF9g==\n" -"-----END CERTIFICATE-----", - -/* Entrust Root Certification Authority - EC1 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMC\n" -"VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5u\n" -"ZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3Ig\n" -"YXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRp\n" -"b24gQXV0aG9yaXR5IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8x\n" -"CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3\n" -"LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJ\n" -"bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD\n" -"ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQT\n" -"ydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9\n" -"ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/\n" -"BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLdj5xrdjekIplWDpOBqUEFlEUJJ\n" -"MAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHv\n" -"AvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZB\n" -"WyVgrtBIGu4G\n" -"-----END CERTIFICATE-----", - /* CFCA EV ROOT */ "-----BEGIN CERTIFICATE-----\n" "MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4G\n" @@ -2187,71 +1721,6 @@ "hVdJIgc=\n" "-----END CERTIFICATE-----", -/* Trustwave Global Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQG\n" -"EwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRy\n" -"dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0\n" -"aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGI\n" -"MQswCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAf\n" -"BgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEds\n" -"b2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC\n" -"AgoCggIBALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn\n" -"swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogD\n" -"nXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXo\n" -"LG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ\n" -"9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+\n" -"VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqI\n" -"yE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m\n" -"4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm9\n" -"43xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n\n" -"twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1UdEwEB/wQF\n" -"MAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIBBjAN\n" -"BgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H\n" -"PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1K\n" -"aA0HZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgim\n" -"QlRXtpla4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0W\n" -"BpanI5ojSP5RvbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92Y\n" -"HJtZuSPTMaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe\n" -"qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVy\n" -"QYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8\n" -"AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzL\n" -"J8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTKyeC2nOnOcXHebD8WpHk=\n" -"-----END CERTIFICATE-----", - -/* Trustwave Global ECC P256 Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJV\n" -"UzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\n" -"d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1\n" -"NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1\n" -"MTBaMIGRMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNh\n" -"Z28xITAfBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\n" -"YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49\n" -"AgEGCCqGSM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN\n" -"FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0P\n" -"AQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwID\n" -"RwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyG\n" -"bbOcNEhjhAnFjXca4syc4XR7\n" -"-----END CERTIFICATE-----", - -/* Trustwave Global ECC P384 Certification Authority */ -"-----BEGIN CERTIFICATE-----\n" -"MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJV\n" -"UzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0\n" -"d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4\n" -"NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2\n" -"NDNaMIGRMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNh\n" -"Z28xITAfBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3\n" -"YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49\n" -"AgEGBSuBBAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ\n" -"j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhl\n" -"oKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ\n" -"0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMD\n" -"Er5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3\n" -"g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw==\n" -"-----END CERTIFICATE-----", - /* NAVER Global Root Certification Authority */ "-----BEGIN CERTIFICATE-----\n" "MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTEL\n" @@ -2343,37 +1812,6 @@ "wWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=\n" "-----END CERTIFICATE-----", -/* GLOBALTRUST 2020 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMC\n" -"QVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9C\n" -"QUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UE\n" -"BhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBH\n" -"TE9CQUxUUlVTVCAyMDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc\n" -"7/aVj6B3GyvTY4+ETUWiD59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4\n" -"UeDLgztzOG53ig9ZYybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7M\n" -"potQsjj3QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw\n" -"yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQf\n" -"Es4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiK\n" -"weR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkHr96i5OTUawuzXnzUJIBHKWk7\n" -"buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlGDfV0OoIu0G4skaMxXDtG6nsEEFZe\n" -"gB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfK\n" -"N0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQ\n" -"jJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B\n" -"Af8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu\n" -"H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jAVC/f7GLD\n" -"w56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw4Lx0\n" -"SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9\n" -"iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ\n" -"0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPP\n" -"m2eggAe2HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQ\n" -"Sa9+pTeAsRxSvTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCE\n" -"uGwyEn6CMUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn\n" -"4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlx\n" -"fv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8\n" -"vTmR9W0Nv3vXkg==\n" -"-----END CERTIFICATE-----", - /* ANF Secure Server Root CA */ "-----BEGIN CERTIFICATE-----\n" "MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2\n" @@ -2699,36 +2137,6 @@ "NQzcmRk13NfIRmPVNnGuV/u3gm3c\n" "-----END CERTIFICATE-----", -/* GTS Root R2 */ -"-----BEGIN CERTIFICATE-----\n" -"MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG\n" -"EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RT\n" -"IFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJV\n" -"UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv\n" -"b3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3\n" -"GTXd98GdVarTzTukk3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfg\n" -"LFuv5AS/T3KgGjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/B\n" -"W9BuXvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k\n" -"RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FL\n" -"PD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66H\n" -"jucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8YzodDqs5xoic4DSMPclQsciOzsS\n" -"rZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvG\n" -"eJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9Om\n" -"TN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGF\n" -"PP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAd\n" -"BgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H\n" -"vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM80mJhwQTt\n" -"zuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyCB19m3H0Q/gxhswWV\n" -"7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2uNmSRXbBoGOqKYcl3qJfEycel\n" -"/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMgyALOWr7Z6v2yTcQvG99fevX4i8buMTol\n" -"UVVnjWQye+mew4K6Ki3pHrTgSAai/GevHyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFe\n" -"nTgCR2y59PYjJbigapordwj6xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGo\n" -"o7z7GJa7Um8M7YNRTOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCM\n" -"Elv924SgJPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV\n" -"7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl6WLAYv7Y\n" -"TVWW4tAR+kg0Eeye7QUd5MjWHYbL\n" -"-----END CERTIFICATE-----", - /* GTS Root R3 */ "-----BEGIN CERTIFICATE-----\n" "MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV\n" @@ -3202,22 +2610,6 @@ "NSWSs1A=\n" "-----END CERTIFICATE-----", -/* FIRMAPROFESIONAL CA ROOT-A WEB */ -"-----BEGIN CERTIFICATE-----\n" -"MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQswCQYDVQQG\n" -"EwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYy\n" -"NjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwHhcNMjIw\n" -"NDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQswCQYDVQQGEwJFUzEcMBoGA1UECgwTRmly\n" -"bWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5G\n" -"SVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARH\n" -"U+osEaR3xyrq89Zfe9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K\n" -"6k84Si6CcyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB\n" -"/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0OBBYEFJPh\n" -"Q2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjAd\n" -"fKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFwhVmpHqTm6iMxoAACMQD94viz\n" -"rxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dGXSaQpYXFuXqUPoeovQA=\n" -"-----END CERTIFICATE-----", - /* TWCA CYBER Root CA */ "-----BEGIN CERTIFICATE-----\n" "MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQMQswCQYD\n" diff --git a/tools/certdata.txt b/tools/certdata.txt index 150f746b62d376..97b118f6879758 100644 --- a/tools/certdata.txt +++ b/tools/certdata.txt @@ -979,7 +979,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\002\005\011 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -1161,163 +1161,6 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\002\005\306 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "XRamp Global CA Root" -# -# Issuer: CN=XRamp Global Certification Authority,O=XRamp Security Services Inc,OU=www.xrampsecurity.com,C=US -# Serial Number:50:94:6c:ec:18:ea:d5:9c:4d:d5:97:ef:75:8f:a0:ad -# Subject: CN=XRamp Global Certification Authority,O=XRamp Security Services Inc,OU=www.xrampsecurity.com,C=US -# Not Valid Before: Mon Nov 01 17:14:04 2004 -# Not Valid After : Mon Jan 01 05:37:19 2035 -# Fingerprint (SHA-256): CE:CD:DC:90:50:99:D8:DA:DF:C5:B1:D2:09:B7:37:CB:E2:C1:8C:FB:2C:10:C0:FF:0B:CF:0D:32:86:FC:1A:A2 -# Fingerprint (SHA1): B8:01:86:D1:EB:9C:86:A5:41:04:CF:30:54:F3:4C:52:B7:E5:58:C6 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "XRamp Global CA Root" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\201\202\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\036\060\034\006\003\125\004\013\023\025\167\167\167\056\170 -\162\141\155\160\163\145\143\165\162\151\164\171\056\143\157\155 -\061\044\060\042\006\003\125\004\012\023\033\130\122\141\155\160 -\040\123\145\143\165\162\151\164\171\040\123\145\162\166\151\143 -\145\163\040\111\156\143\061\055\060\053\006\003\125\004\003\023 -\044\130\122\141\155\160\040\107\154\157\142\141\154\040\103\145 -\162\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150 -\157\162\151\164\171 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\201\202\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\036\060\034\006\003\125\004\013\023\025\167\167\167\056\170 -\162\141\155\160\163\145\143\165\162\151\164\171\056\143\157\155 -\061\044\060\042\006\003\125\004\012\023\033\130\122\141\155\160 -\040\123\145\143\165\162\151\164\171\040\123\145\162\166\151\143 -\145\163\040\111\156\143\061\055\060\053\006\003\125\004\003\023 -\044\130\122\141\155\160\040\107\154\157\142\141\154\040\103\145 -\162\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150 -\157\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\120\224\154\354\030\352\325\234\115\325\227\357\165\217 -\240\255 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\004\060\060\202\003\030\240\003\002\001\002\002\020\120 -\224\154\354\030\352\325\234\115\325\227\357\165\217\240\255\060 -\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060\201 -\202\061\013\060\011\006\003\125\004\006\023\002\125\123\061\036 -\060\034\006\003\125\004\013\023\025\167\167\167\056\170\162\141 -\155\160\163\145\143\165\162\151\164\171\056\143\157\155\061\044 -\060\042\006\003\125\004\012\023\033\130\122\141\155\160\040\123 -\145\143\165\162\151\164\171\040\123\145\162\166\151\143\145\163 -\040\111\156\143\061\055\060\053\006\003\125\004\003\023\044\130 -\122\141\155\160\040\107\154\157\142\141\154\040\103\145\162\164 -\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157\162 -\151\164\171\060\036\027\015\060\064\061\061\060\061\061\067\061 -\064\060\064\132\027\015\063\065\060\061\060\061\060\065\063\067 -\061\071\132\060\201\202\061\013\060\011\006\003\125\004\006\023 -\002\125\123\061\036\060\034\006\003\125\004\013\023\025\167\167 -\167\056\170\162\141\155\160\163\145\143\165\162\151\164\171\056 -\143\157\155\061\044\060\042\006\003\125\004\012\023\033\130\122 -\141\155\160\040\123\145\143\165\162\151\164\171\040\123\145\162 -\166\151\143\145\163\040\111\156\143\061\055\060\053\006\003\125 -\004\003\023\044\130\122\141\155\160\040\107\154\157\142\141\154 -\040\103\145\162\164\151\146\151\143\141\164\151\157\156\040\101 -\165\164\150\157\162\151\164\171\060\202\001\042\060\015\006\011 -\052\206\110\206\367\015\001\001\001\005\000\003\202\001\017\000 -\060\202\001\012\002\202\001\001\000\230\044\036\275\025\264\272 -\337\307\214\245\047\266\070\013\151\363\266\116\250\054\056\041 -\035\134\104\337\041\135\176\043\164\376\136\176\264\112\267\246 -\255\037\256\340\006\026\342\233\133\331\147\164\153\135\200\217 -\051\235\206\033\331\234\015\230\155\166\020\050\130\344\145\260 -\177\112\230\171\237\340\303\061\176\200\053\265\214\300\100\073 -\021\206\320\313\242\206\066\140\244\325\060\202\155\331\156\320 -\017\022\004\063\227\137\117\141\132\360\344\371\221\253\347\035 -\073\274\350\317\364\153\055\064\174\342\110\141\034\216\363\141 -\104\314\157\240\112\251\224\260\115\332\347\251\064\172\162\070 -\250\101\314\074\224\021\175\353\310\246\214\267\206\313\312\063 -\073\331\075\067\213\373\172\076\206\054\347\163\327\012\127\254 -\144\233\031\353\364\017\004\010\212\254\003\027\031\144\364\132 -\045\042\215\064\054\262\366\150\035\022\155\323\212\036\024\332 -\304\217\246\342\043\205\325\172\015\275\152\340\351\354\354\027 -\273\102\033\147\252\045\355\105\203\041\374\301\311\174\325\142 -\076\372\362\305\055\323\375\324\145\002\003\001\000\001\243\201 -\237\060\201\234\060\023\006\011\053\006\001\004\001\202\067\024 -\002\004\006\036\004\000\103\000\101\060\013\006\003\125\035\017 -\004\004\003\002\001\206\060\017\006\003\125\035\023\001\001\377 -\004\005\060\003\001\001\377\060\035\006\003\125\035\016\004\026 -\004\024\306\117\242\075\006\143\204\011\234\316\142\344\004\254 -\215\134\265\351\266\033\060\066\006\003\125\035\037\004\057\060 -\055\060\053\240\051\240\047\206\045\150\164\164\160\072\057\057 -\143\162\154\056\170\162\141\155\160\163\145\143\165\162\151\164 -\171\056\143\157\155\057\130\107\103\101\056\143\162\154\060\020 -\006\011\053\006\001\004\001\202\067\025\001\004\003\002\001\001 -\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000\003 -\202\001\001\000\221\025\071\003\001\033\147\373\112\034\371\012 -\140\133\241\332\115\227\142\371\044\123\047\327\202\144\116\220 -\056\303\111\033\053\232\334\374\250\170\147\065\361\035\360\021 -\275\267\110\343\020\366\015\337\077\322\311\266\252\125\244\110 -\272\002\333\336\131\056\025\133\073\235\026\175\107\327\067\352 -\137\115\166\022\066\273\037\327\241\201\004\106\040\243\054\155 -\251\236\001\176\077\051\316\000\223\337\375\311\222\163\211\211 -\144\236\347\053\344\034\221\054\322\271\316\175\316\157\061\231 -\323\346\276\322\036\220\360\011\024\171\134\043\253\115\322\332 -\041\037\115\231\171\235\341\317\047\237\020\233\034\210\015\260 -\212\144\101\061\270\016\154\220\044\244\233\134\161\217\272\273 -\176\034\033\333\152\200\017\041\274\351\333\246\267\100\364\262 -\213\251\261\344\357\232\032\320\075\151\231\356\250\050\243\341 -\074\263\360\262\021\234\317\174\100\346\335\347\103\175\242\330 -\072\265\251\215\362\064\231\304\324\020\341\006\375\011\204\020 -\073\356\304\114\364\354\047\174\102\302\164\174\202\212\011\311 -\264\003\045\274 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "XRamp Global CA Root" -# Issuer: CN=XRamp Global Certification Authority,O=XRamp Security Services Inc,OU=www.xrampsecurity.com,C=US -# Serial Number:50:94:6c:ec:18:ea:d5:9c:4d:d5:97:ef:75:8f:a0:ad -# Subject: CN=XRamp Global Certification Authority,O=XRamp Security Services Inc,OU=www.xrampsecurity.com,C=US -# Not Valid Before: Mon Nov 01 17:14:04 2004 -# Not Valid After : Mon Jan 01 05:37:19 2035 -# Fingerprint (SHA-256): CE:CD:DC:90:50:99:D8:DA:DF:C5:B1:D2:09:B7:37:CB:E2:C1:8C:FB:2C:10:C0:FF:0B:CF:0D:32:86:FC:1A:A2 -# Fingerprint (SHA1): B8:01:86:D1:EB:9C:86:A5:41:04:CF:30:54:F3:4C:52:B7:E5:58:C6 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "XRamp Global CA Root" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\270\001\206\321\353\234\206\245\101\004\317\060\124\363\114\122 -\267\345\130\306 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\241\013\104\263\312\020\330\000\156\235\017\330\017\222\012\321 -END -CKA_ISSUER MULTILINE_OCTAL -\060\201\202\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\036\060\034\006\003\125\004\013\023\025\167\167\167\056\170 -\162\141\155\160\163\145\143\165\162\151\164\171\056\143\157\155 -\061\044\060\042\006\003\125\004\012\023\033\130\122\141\155\160 -\040\123\145\143\165\162\151\164\171\040\123\145\162\166\151\143 -\145\163\040\111\156\143\061\055\060\053\006\003\125\004\003\023 -\044\130\122\141\155\160\040\107\154\157\142\141\154\040\103\145 -\162\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150 -\157\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\120\224\154\354\030\352\325\234\115\325\227\357\165\217 -\240\255 -END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST @@ -1754,7 +1597,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\020\014\347\340\345\027\330\106\376\217\345\140\374\033\360 \060\071 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -1897,7 +1740,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\020\010\073\340\126\220\102\106\261\241\165\152\311\131\221 \307\112 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -2041,7 +1884,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\020\002\254\134\046\152\013\100\233\217\013\171\362\256\106 \045\167 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -2208,281 +2051,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\011\000\273\100\034\103\365\136\117\260 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "SecureTrust CA" -# -# Issuer: CN=SecureTrust CA,O=SecureTrust Corporation,C=US -# Serial Number:0c:f0:8e:5c:08:16:a5:ad:42:7f:f0:eb:27:18:59:d0 -# Subject: CN=SecureTrust CA,O=SecureTrust Corporation,C=US -# Not Valid Before: Tue Nov 07 19:31:18 2006 -# Not Valid After : Mon Dec 31 19:40:55 2029 -# Fingerprint (SHA-256): F1:C1:B5:0A:E5:A2:0D:D8:03:0E:C9:F6:BC:24:82:3D:D3:67:B5:25:57:59:B4:E7:1B:61:FC:E9:F7:37:5D:73 -# Fingerprint (SHA1): 87:82:C6:C3:04:35:3B:CF:D2:96:92:D2:59:3E:7D:44:D9:34:FF:11 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "SecureTrust CA" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\110\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\040\060\036\006\003\125\004\012\023\027\123\145\143\165\162\145 -\124\162\165\163\164\040\103\157\162\160\157\162\141\164\151\157 -\156\061\027\060\025\006\003\125\004\003\023\016\123\145\143\165 -\162\145\124\162\165\163\164\040\103\101 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\110\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\040\060\036\006\003\125\004\012\023\027\123\145\143\165\162\145 -\124\162\165\163\164\040\103\157\162\160\157\162\141\164\151\157 -\156\061\027\060\025\006\003\125\004\003\023\016\123\145\143\165 -\162\145\124\162\165\163\164\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\014\360\216\134\010\026\245\255\102\177\360\353\047\030 -\131\320 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\003\270\060\202\002\240\240\003\002\001\002\002\020\014 -\360\216\134\010\026\245\255\102\177\360\353\047\030\131\320\060 -\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060\110 -\061\013\060\011\006\003\125\004\006\023\002\125\123\061\040\060 -\036\006\003\125\004\012\023\027\123\145\143\165\162\145\124\162 -\165\163\164\040\103\157\162\160\157\162\141\164\151\157\156\061 -\027\060\025\006\003\125\004\003\023\016\123\145\143\165\162\145 -\124\162\165\163\164\040\103\101\060\036\027\015\060\066\061\061 -\060\067\061\071\063\061\061\070\132\027\015\062\071\061\062\063 -\061\061\071\064\060\065\065\132\060\110\061\013\060\011\006\003 -\125\004\006\023\002\125\123\061\040\060\036\006\003\125\004\012 -\023\027\123\145\143\165\162\145\124\162\165\163\164\040\103\157 -\162\160\157\162\141\164\151\157\156\061\027\060\025\006\003\125 -\004\003\023\016\123\145\143\165\162\145\124\162\165\163\164\040 -\103\101\060\202\001\042\060\015\006\011\052\206\110\206\367\015 -\001\001\001\005\000\003\202\001\017\000\060\202\001\012\002\202 -\001\001\000\253\244\201\345\225\315\365\366\024\216\302\117\312 -\324\342\170\225\130\234\101\341\015\231\100\044\027\071\221\063 -\146\351\276\341\203\257\142\134\211\321\374\044\133\141\263\340 -\021\021\101\034\035\156\360\270\273\370\336\247\201\272\246\110 -\306\237\035\275\276\216\251\101\076\270\224\355\051\032\324\216 -\322\003\035\003\357\155\015\147\034\127\327\006\255\312\310\365 -\376\016\257\146\045\110\004\226\013\135\243\272\026\303\010\117 -\321\106\370\024\134\362\310\136\001\231\155\375\210\314\206\250 -\301\157\061\102\154\122\076\150\313\363\031\064\337\273\207\030 -\126\200\046\304\320\334\300\157\337\336\240\302\221\026\240\144 -\021\113\104\274\036\366\347\372\143\336\146\254\166\244\161\243 -\354\066\224\150\172\167\244\261\347\016\057\201\172\342\265\162 -\206\357\242\153\213\360\017\333\323\131\077\272\162\274\104\044 -\234\343\163\263\367\257\127\057\102\046\235\251\164\272\000\122 -\362\113\315\123\174\107\013\066\205\016\146\251\010\227\026\064 -\127\301\146\367\200\343\355\160\124\307\223\340\056\050\025\131 -\207\272\273\002\003\001\000\001\243\201\235\060\201\232\060\023 -\006\011\053\006\001\004\001\202\067\024\002\004\006\036\004\000 -\103\000\101\060\013\006\003\125\035\017\004\004\003\002\001\206 -\060\017\006\003\125\035\023\001\001\377\004\005\060\003\001\001 -\377\060\035\006\003\125\035\016\004\026\004\024\102\062\266\026 -\372\004\375\376\135\113\172\303\375\367\114\100\035\132\103\257 -\060\064\006\003\125\035\037\004\055\060\053\060\051\240\047\240 -\045\206\043\150\164\164\160\072\057\057\143\162\154\056\163\145 -\143\165\162\145\164\162\165\163\164\056\143\157\155\057\123\124 -\103\101\056\143\162\154\060\020\006\011\053\006\001\004\001\202 -\067\025\001\004\003\002\001\000\060\015\006\011\052\206\110\206 -\367\015\001\001\005\005\000\003\202\001\001\000\060\355\117\112 -\341\130\072\122\162\133\265\246\243\145\030\246\273\121\073\167 -\351\235\352\323\237\134\340\105\145\173\015\312\133\342\160\120 -\262\224\005\024\256\111\307\215\101\007\022\163\224\176\014\043 -\041\375\274\020\177\140\020\132\162\365\230\016\254\354\271\177 -\335\172\157\135\323\034\364\377\210\005\151\102\251\005\161\310 -\267\254\046\350\056\264\214\152\377\161\334\270\261\337\231\274 -\174\041\124\053\344\130\242\273\127\051\256\236\251\243\031\046 -\017\231\056\010\260\357\375\151\317\231\032\011\215\343\247\237 -\053\311\066\064\173\044\263\170\114\225\027\244\006\046\036\266 -\144\122\066\137\140\147\331\234\305\005\164\013\347\147\043\322 -\010\374\210\351\256\213\177\341\060\364\067\176\375\306\062\332 -\055\236\104\060\060\154\356\007\336\322\064\374\322\377\100\366 -\113\364\146\106\006\124\246\362\062\012\143\046\060\153\233\321 -\334\213\107\272\341\271\325\142\320\242\240\364\147\005\170\051 -\143\032\157\004\326\370\306\114\243\232\261\067\264\215\345\050 -\113\035\236\054\302\270\150\274\355\002\356\061 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "SecureTrust CA" -# Issuer: CN=SecureTrust CA,O=SecureTrust Corporation,C=US -# Serial Number:0c:f0:8e:5c:08:16:a5:ad:42:7f:f0:eb:27:18:59:d0 -# Subject: CN=SecureTrust CA,O=SecureTrust Corporation,C=US -# Not Valid Before: Tue Nov 07 19:31:18 2006 -# Not Valid After : Mon Dec 31 19:40:55 2029 -# Fingerprint (SHA-256): F1:C1:B5:0A:E5:A2:0D:D8:03:0E:C9:F6:BC:24:82:3D:D3:67:B5:25:57:59:B4:E7:1B:61:FC:E9:F7:37:5D:73 -# Fingerprint (SHA1): 87:82:C6:C3:04:35:3B:CF:D2:96:92:D2:59:3E:7D:44:D9:34:FF:11 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "SecureTrust CA" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\207\202\306\303\004\065\073\317\322\226\222\322\131\076\175\104 -\331\064\377\021 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\334\062\303\247\155\045\127\307\150\011\235\352\055\251\242\321 -END -CKA_ISSUER MULTILINE_OCTAL -\060\110\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\040\060\036\006\003\125\004\012\023\027\123\145\143\165\162\145 -\124\162\165\163\164\040\103\157\162\160\157\162\141\164\151\157 -\156\061\027\060\025\006\003\125\004\003\023\016\123\145\143\165 -\162\145\124\162\165\163\164\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\014\360\216\134\010\026\245\255\102\177\360\353\047\030 -\131\320 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "Secure Global CA" -# -# Issuer: CN=Secure Global CA,O=SecureTrust Corporation,C=US -# Serial Number:07:56:22:a4:e8:d4:8a:89:4d:f4:13:c8:f0:f8:ea:a5 -# Subject: CN=Secure Global CA,O=SecureTrust Corporation,C=US -# Not Valid Before: Tue Nov 07 19:42:28 2006 -# Not Valid After : Mon Dec 31 19:52:06 2029 -# Fingerprint (SHA-256): 42:00:F5:04:3A:C8:59:0E:BB:52:7D:20:9E:D1:50:30:29:FB:CB:D4:1C:A1:B5:06:EC:27:F1:5A:DE:7D:AC:69 -# Fingerprint (SHA1): 3A:44:73:5A:E5:81:90:1F:24:86:61:46:1E:3B:9C:C4:5F:F5:3A:1B -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Secure Global CA" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\112\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\040\060\036\006\003\125\004\012\023\027\123\145\143\165\162\145 -\124\162\165\163\164\040\103\157\162\160\157\162\141\164\151\157 -\156\061\031\060\027\006\003\125\004\003\023\020\123\145\143\165 -\162\145\040\107\154\157\142\141\154\040\103\101 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\112\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\040\060\036\006\003\125\004\012\023\027\123\145\143\165\162\145 -\124\162\165\163\164\040\103\157\162\160\157\162\141\164\151\157 -\156\061\031\060\027\006\003\125\004\003\023\020\123\145\143\165 -\162\145\040\107\154\157\142\141\154\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\007\126\042\244\350\324\212\211\115\364\023\310\360\370 -\352\245 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\003\274\060\202\002\244\240\003\002\001\002\002\020\007 -\126\042\244\350\324\212\211\115\364\023\310\360\370\352\245\060 -\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060\112 -\061\013\060\011\006\003\125\004\006\023\002\125\123\061\040\060 -\036\006\003\125\004\012\023\027\123\145\143\165\162\145\124\162 -\165\163\164\040\103\157\162\160\157\162\141\164\151\157\156\061 -\031\060\027\006\003\125\004\003\023\020\123\145\143\165\162\145 -\040\107\154\157\142\141\154\040\103\101\060\036\027\015\060\066 -\061\061\060\067\061\071\064\062\062\070\132\027\015\062\071\061 -\062\063\061\061\071\065\062\060\066\132\060\112\061\013\060\011 -\006\003\125\004\006\023\002\125\123\061\040\060\036\006\003\125 -\004\012\023\027\123\145\143\165\162\145\124\162\165\163\164\040 -\103\157\162\160\157\162\141\164\151\157\156\061\031\060\027\006 -\003\125\004\003\023\020\123\145\143\165\162\145\040\107\154\157 -\142\141\154\040\103\101\060\202\001\042\060\015\006\011\052\206 -\110\206\367\015\001\001\001\005\000\003\202\001\017\000\060\202 -\001\012\002\202\001\001\000\257\065\056\330\254\154\125\151\006 -\161\345\023\150\044\263\117\330\314\041\107\370\361\140\070\211 -\211\003\351\275\352\136\106\123\011\334\134\365\132\350\367\105 -\052\002\353\061\141\327\051\063\114\316\307\174\012\067\176\017 -\272\062\230\341\035\227\257\217\307\334\311\070\226\363\333\032 -\374\121\355\150\306\320\156\244\174\044\321\256\102\310\226\120 -\143\056\340\376\165\376\230\247\137\111\056\225\343\071\063\144 -\216\036\244\137\220\322\147\074\262\331\376\101\271\125\247\011 -\216\162\005\036\213\335\104\205\202\102\320\111\300\035\140\360 -\321\027\054\225\353\366\245\301\222\243\305\302\247\010\140\015 -\140\004\020\226\171\236\026\064\346\251\266\372\045\105\071\310 -\036\145\371\223\365\252\361\122\334\231\230\075\245\206\032\014 -\065\063\372\113\245\004\006\025\034\061\200\357\252\030\153\302 -\173\327\332\316\371\063\040\325\365\275\152\063\055\201\004\373 -\260\134\324\234\243\342\134\035\343\251\102\165\136\173\324\167 -\357\071\124\272\311\012\030\033\022\231\111\057\210\113\375\120 -\142\321\163\347\217\172\103\002\003\001\000\001\243\201\235\060 -\201\232\060\023\006\011\053\006\001\004\001\202\067\024\002\004 -\006\036\004\000\103\000\101\060\013\006\003\125\035\017\004\004 -\003\002\001\206\060\017\006\003\125\035\023\001\001\377\004\005 -\060\003\001\001\377\060\035\006\003\125\035\016\004\026\004\024 -\257\104\004\302\101\176\110\203\333\116\071\002\354\354\204\172 -\346\316\311\244\060\064\006\003\125\035\037\004\055\060\053\060 -\051\240\047\240\045\206\043\150\164\164\160\072\057\057\143\162 -\154\056\163\145\143\165\162\145\164\162\165\163\164\056\143\157 -\155\057\123\107\103\101\056\143\162\154\060\020\006\011\053\006 -\001\004\001\202\067\025\001\004\003\002\001\000\060\015\006\011 -\052\206\110\206\367\015\001\001\005\005\000\003\202\001\001\000 -\143\032\010\100\175\244\136\123\015\167\330\172\256\037\015\013 -\121\026\003\357\030\174\310\343\257\152\130\223\024\140\221\262 -\204\334\210\116\276\071\212\072\363\346\202\211\135\001\067\263 -\253\044\244\025\016\222\065\132\112\104\136\116\127\372\165\316 -\037\110\316\146\364\074\100\046\222\230\154\033\356\044\106\014 -\027\263\122\245\333\245\221\221\317\067\323\157\347\047\010\072 -\116\031\037\072\247\130\134\027\317\171\077\213\344\247\323\046 -\043\235\046\017\130\151\374\107\176\262\320\215\213\223\277\051 -\117\103\151\164\166\147\113\317\007\214\346\002\367\265\341\264 -\103\265\113\055\024\237\371\334\046\015\277\246\107\164\006\330 -\210\321\072\051\060\204\316\322\071\200\142\033\250\307\127\111 -\274\152\125\121\147\025\112\276\065\007\344\325\165\230\067\171 -\060\024\333\051\235\154\305\151\314\107\125\242\060\367\314\134 -\177\302\303\230\034\153\116\026\200\353\172\170\145\105\242\000 -\032\257\014\015\125\144\064\110\270\222\271\361\264\120\051\362 -\117\043\037\332\154\254\037\104\341\335\043\170\121\133\307\026 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "Secure Global CA" -# Issuer: CN=Secure Global CA,O=SecureTrust Corporation,C=US -# Serial Number:07:56:22:a4:e8:d4:8a:89:4d:f4:13:c8:f0:f8:ea:a5 -# Subject: CN=Secure Global CA,O=SecureTrust Corporation,C=US -# Not Valid Before: Tue Nov 07 19:42:28 2006 -# Not Valid After : Mon Dec 31 19:52:06 2029 -# Fingerprint (SHA-256): 42:00:F5:04:3A:C8:59:0E:BB:52:7D:20:9E:D1:50:30:29:FB:CB:D4:1C:A1:B5:06:EC:27:F1:5A:DE:7D:AC:69 -# Fingerprint (SHA1): 3A:44:73:5A:E5:81:90:1F:24:86:61:46:1E:3B:9C:C4:5F:F5:3A:1B -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Secure Global CA" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\072\104\163\132\345\201\220\037\044\206\141\106\036\073\234\304 -\137\365\072\033 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\317\364\047\015\324\355\334\145\026\111\155\075\332\277\156\336 -END -CKA_ISSUER MULTILINE_OCTAL -\060\112\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\040\060\036\006\003\125\004\012\023\027\123\145\143\165\162\145 -\124\162\165\163\164\040\103\157\162\160\157\162\141\164\151\157 -\156\061\031\060\027\006\003\125\004\003\023\020\123\145\143\165 -\162\145\040\107\154\157\142\141\154\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\007\126\042\244\350\324\212\211\115\364\023\310\360\370 -\352\245 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -2638,7 +2207,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\020\116\201\055\212\202\145\340\013\002\356\076\065\002\106 \345\075 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -3052,7 +2621,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\011\000\376\334\343\001\017\311\110\377 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -3232,130 +2801,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE -# -# Certificate "certSIGN ROOT CA" -# -# Issuer: OU=certSIGN ROOT CA,O=certSIGN,C=RO -# Serial Number:20:06:05:16:70:02 -# Subject: OU=certSIGN ROOT CA,O=certSIGN,C=RO -# Not Valid Before: Tue Jul 04 17:20:04 2006 -# Not Valid After : Fri Jul 04 17:20:04 2031 -# Fingerprint (SHA-256): EA:A9:62:C4:FA:4A:6B:AF:EB:E4:15:19:6D:35:1C:CD:88:8D:4F:53:F3:FA:8A:E6:D7:C4:66:A9:4E:60:42:BB -# Fingerprint (SHA1): FA:B7:EE:36:97:26:62:FB:2D:B0:2A:F6:BF:03:FD:E8:7C:4B:2F:9B -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "certSIGN ROOT CA" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\073\061\013\060\011\006\003\125\004\006\023\002\122\117\061 -\021\060\017\006\003\125\004\012\023\010\143\145\162\164\123\111 -\107\116\061\031\060\027\006\003\125\004\013\023\020\143\145\162 -\164\123\111\107\116\040\122\117\117\124\040\103\101 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\073\061\013\060\011\006\003\125\004\006\023\002\122\117\061 -\021\060\017\006\003\125\004\012\023\010\143\145\162\164\123\111 -\107\116\061\031\060\027\006\003\125\004\013\023\020\143\145\162 -\164\123\111\107\116\040\122\117\117\124\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\006\040\006\005\026\160\002 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\003\070\060\202\002\040\240\003\002\001\002\002\006\040 -\006\005\026\160\002\060\015\006\011\052\206\110\206\367\015\001 -\001\005\005\000\060\073\061\013\060\011\006\003\125\004\006\023 -\002\122\117\061\021\060\017\006\003\125\004\012\023\010\143\145 -\162\164\123\111\107\116\061\031\060\027\006\003\125\004\013\023 -\020\143\145\162\164\123\111\107\116\040\122\117\117\124\040\103 -\101\060\036\027\015\060\066\060\067\060\064\061\067\062\060\060 -\064\132\027\015\063\061\060\067\060\064\061\067\062\060\060\064 -\132\060\073\061\013\060\011\006\003\125\004\006\023\002\122\117 -\061\021\060\017\006\003\125\004\012\023\010\143\145\162\164\123 -\111\107\116\061\031\060\027\006\003\125\004\013\023\020\143\145 -\162\164\123\111\107\116\040\122\117\117\124\040\103\101\060\202 -\001\042\060\015\006\011\052\206\110\206\367\015\001\001\001\005 -\000\003\202\001\017\000\060\202\001\012\002\202\001\001\000\267 -\063\271\176\310\045\112\216\265\333\264\050\033\252\127\220\350 -\321\042\323\144\272\323\223\350\324\254\206\141\100\152\140\127 -\150\124\204\115\274\152\124\002\005\377\337\233\232\052\256\135 -\007\217\112\303\050\177\357\373\053\372\171\361\307\255\360\020 -\123\044\220\213\146\311\250\210\253\257\132\243\000\351\276\272 -\106\356\133\163\173\054\027\202\201\136\142\054\241\002\145\263 -\275\305\053\000\176\304\374\003\063\127\015\355\342\372\316\135 -\105\326\070\315\065\266\262\301\320\234\201\112\252\344\262\001 -\134\035\217\137\231\304\261\255\333\210\041\353\220\010\202\200 -\363\060\243\103\346\220\202\256\125\050\111\355\133\327\251\020 -\070\016\376\217\114\133\233\106\352\101\365\260\010\164\303\320 -\210\063\266\174\327\164\337\334\204\321\103\016\165\071\241\045 -\100\050\352\170\313\016\054\056\071\235\214\213\156\026\034\057 -\046\202\020\342\343\145\224\012\004\300\136\367\135\133\370\020 -\342\320\272\172\113\373\336\067\000\000\032\133\050\343\322\234 -\163\076\062\207\230\241\311\121\057\327\336\254\063\263\117\002 -\003\001\000\001\243\102\060\100\060\017\006\003\125\035\023\001 -\001\377\004\005\060\003\001\001\377\060\016\006\003\125\035\017 -\001\001\377\004\004\003\002\001\306\060\035\006\003\125\035\016 -\004\026\004\024\340\214\233\333\045\111\263\361\174\206\326\262 -\102\207\013\320\153\240\331\344\060\015\006\011\052\206\110\206 -\367\015\001\001\005\005\000\003\202\001\001\000\076\322\034\211 -\056\065\374\370\165\335\346\177\145\210\364\162\114\311\054\327 -\062\116\363\335\031\171\107\275\216\073\133\223\017\120\111\044 -\023\153\024\006\162\357\011\323\241\241\343\100\204\311\347\030 -\062\164\074\110\156\017\237\113\324\367\036\323\223\206\144\124 -\227\143\162\120\325\125\317\372\040\223\002\242\233\303\043\223 -\116\026\125\166\240\160\171\155\315\041\037\317\057\055\274\031 -\343\210\061\370\131\032\201\011\310\227\246\164\307\140\304\133 -\314\127\216\262\165\375\033\002\011\333\131\157\162\223\151\367 -\061\101\326\210\070\277\207\262\275\026\171\371\252\344\276\210 -\045\335\141\047\043\034\265\061\007\004\066\264\032\220\275\240 -\164\161\120\211\155\274\024\343\017\206\256\361\253\076\307\240 -\011\314\243\110\321\340\333\144\347\222\265\317\257\162\103\160 -\213\371\303\204\074\023\252\176\222\233\127\123\223\372\160\302 -\221\016\061\371\233\147\135\351\226\070\136\137\263\163\116\210 -\025\147\336\236\166\020\142\040\276\125\151\225\103\000\071\115 -\366\356\260\132\116\111\104\124\130\137\102\203 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "certSIGN ROOT CA" -# Issuer: OU=certSIGN ROOT CA,O=certSIGN,C=RO -# Serial Number:20:06:05:16:70:02 -# Subject: OU=certSIGN ROOT CA,O=certSIGN,C=RO -# Not Valid Before: Tue Jul 04 17:20:04 2006 -# Not Valid After : Fri Jul 04 17:20:04 2031 -# Fingerprint (SHA-256): EA:A9:62:C4:FA:4A:6B:AF:EB:E4:15:19:6D:35:1C:CD:88:8D:4F:53:F3:FA:8A:E6:D7:C4:66:A9:4E:60:42:BB -# Fingerprint (SHA1): FA:B7:EE:36:97:26:62:FB:2D:B0:2A:F6:BF:03:FD:E8:7C:4B:2F:9B -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "certSIGN ROOT CA" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\372\267\356\066\227\046\142\373\055\260\052\366\277\003\375\350 -\174\113\057\233 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\030\230\300\326\351\072\374\371\260\365\014\367\113\001\104\027 -END -CKA_ISSUER MULTILINE_OCTAL -\060\073\061\013\060\011\006\003\125\004\006\023\002\122\117\061 -\021\060\017\006\003\125\004\012\023\010\143\145\162\164\123\111 -\107\116\061\031\060\027\006\003\125\004\013\023\020\143\145\162 -\164\123\111\107\116\040\122\117\117\124\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\006\040\006\005\026\160\002 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - # # Certificate "NetLock Arany (Class Gold) Főtanúsítvány" # @@ -3962,6 +3407,10 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\020\000\260\267\132\026\110\137\277\341\313\365\213\327\031 \346\175 END +# For Server Distrust After: Wed Apr 15 23:59:59 2026 +CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL +\062\066\060\064\061\065\062\063\065\071\065\071\132 +END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST @@ -4422,542 +3871,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE -# -# Certificate "AffirmTrust Commercial" -# -# Issuer: CN=AffirmTrust Commercial,O=AffirmTrust,C=US -# Serial Number:77:77:06:27:26:a9:b1:7c -# Subject: CN=AffirmTrust Commercial,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:06:06 2010 -# Not Valid After : Tue Dec 31 14:06:06 2030 -# Fingerprint (SHA-256): 03:76:AB:1D:54:C5:F9:80:3C:E4:B2:E2:01:A0:EE:7E:EF:7B:57:B6:36:E8:A9:3C:9B:8D:48:60:C9:6F:5F:A7 -# Fingerprint (SHA1): F9:B5:B6:32:45:5F:9C:BE:EC:57:5F:80:DC:E9:6E:2C:C7:B2:78:B7 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Commercial" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\104\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\037\060\035\006\003\125\004\003\014\026 -\101\146\146\151\162\155\124\162\165\163\164\040\103\157\155\155 -\145\162\143\151\141\154 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\104\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\037\060\035\006\003\125\004\003\014\026 -\101\146\146\151\162\155\124\162\165\163\164\040\103\157\155\155 -\145\162\143\151\141\154 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\167\167\006\047\046\251\261\174 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\003\114\060\202\002\064\240\003\002\001\002\002\010\167 -\167\006\047\046\251\261\174\060\015\006\011\052\206\110\206\367 -\015\001\001\013\005\000\060\104\061\013\060\011\006\003\125\004 -\006\023\002\125\123\061\024\060\022\006\003\125\004\012\014\013 -\101\146\146\151\162\155\124\162\165\163\164\061\037\060\035\006 -\003\125\004\003\014\026\101\146\146\151\162\155\124\162\165\163 -\164\040\103\157\155\155\145\162\143\151\141\154\060\036\027\015 -\061\060\060\061\062\071\061\064\060\066\060\066\132\027\015\063 -\060\061\062\063\061\061\064\060\066\060\066\132\060\104\061\013 -\060\011\006\003\125\004\006\023\002\125\123\061\024\060\022\006 -\003\125\004\012\014\013\101\146\146\151\162\155\124\162\165\163 -\164\061\037\060\035\006\003\125\004\003\014\026\101\146\146\151 -\162\155\124\162\165\163\164\040\103\157\155\155\145\162\143\151 -\141\154\060\202\001\042\060\015\006\011\052\206\110\206\367\015 -\001\001\001\005\000\003\202\001\017\000\060\202\001\012\002\202 -\001\001\000\366\033\117\147\007\053\241\025\365\006\042\313\037 -\001\262\343\163\105\006\104\111\054\273\111\045\024\326\316\303 -\267\253\054\117\306\101\062\224\127\372\022\247\133\016\342\217 -\037\036\206\031\247\252\265\055\271\137\015\212\302\257\205\065 -\171\062\055\273\034\142\067\362\261\133\112\075\312\315\161\137 -\351\102\276\224\350\310\336\371\042\110\144\306\345\253\306\053 -\155\255\005\360\372\325\013\317\232\345\360\120\244\213\073\107 -\245\043\133\172\172\370\063\077\270\357\231\227\343\040\301\326 -\050\211\317\224\373\271\105\355\343\100\027\021\324\164\360\013 -\061\342\053\046\152\233\114\127\256\254\040\076\272\105\172\005 -\363\275\233\151\025\256\175\116\040\143\304\065\166\072\007\002 -\311\067\375\307\107\356\350\361\166\035\163\025\362\227\244\265 -\310\172\171\331\102\252\053\177\134\376\316\046\117\243\146\201 -\065\257\104\272\124\036\034\060\062\145\235\346\074\223\136\120 -\116\172\343\072\324\156\314\032\373\371\322\067\256\044\052\253 -\127\003\042\050\015\111\165\177\267\050\332\165\277\216\343\334 -\016\171\061\002\003\001\000\001\243\102\060\100\060\035\006\003 -\125\035\016\004\026\004\024\235\223\306\123\213\136\312\257\077 -\237\036\017\345\231\225\274\044\366\224\217\060\017\006\003\125 -\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006\003 -\125\035\017\001\001\377\004\004\003\002\001\006\060\015\006\011 -\052\206\110\206\367\015\001\001\013\005\000\003\202\001\001\000 -\130\254\364\004\016\315\300\015\377\012\375\324\272\026\137\051 -\275\173\150\231\130\111\322\264\035\067\115\177\047\175\106\006 -\135\103\306\206\056\076\163\262\046\175\117\223\251\266\304\052 -\232\253\041\227\024\261\336\214\323\253\211\025\330\153\044\324 -\361\026\256\330\244\134\324\177\121\216\355\030\001\261\223\143 -\275\274\370\141\200\232\236\261\316\102\160\342\251\175\006\045 -\175\047\241\376\157\354\263\036\044\332\343\113\125\032\000\073 -\065\264\073\331\327\135\060\375\201\023\211\362\302\006\053\355 -\147\304\216\311\103\262\134\153\025\211\002\274\142\374\116\362 -\265\063\252\262\157\323\012\242\120\343\366\073\350\056\104\302 -\333\146\070\251\063\126\110\361\155\033\063\215\015\214\077\140 -\067\235\323\312\155\176\064\176\015\237\162\166\213\033\237\162 -\375\122\065\101\105\002\226\057\034\262\232\163\111\041\261\111 -\107\105\107\264\357\152\064\021\311\115\232\314\131\267\326\002 -\236\132\116\145\265\224\256\033\337\051\260\026\361\277\000\236 -\007\072\027\144\265\004\265\043\041\231\012\225\073\227\174\357 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -# For Server Distrust After: Sat Nov 30 23:59:59 2024 -CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL -\062\064\061\061\063\060\062\063\065\071\065\071\132 -END -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "AffirmTrust Commercial" -# Issuer: CN=AffirmTrust Commercial,O=AffirmTrust,C=US -# Serial Number:77:77:06:27:26:a9:b1:7c -# Subject: CN=AffirmTrust Commercial,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:06:06 2010 -# Not Valid After : Tue Dec 31 14:06:06 2030 -# Fingerprint (SHA-256): 03:76:AB:1D:54:C5:F9:80:3C:E4:B2:E2:01:A0:EE:7E:EF:7B:57:B6:36:E8:A9:3C:9B:8D:48:60:C9:6F:5F:A7 -# Fingerprint (SHA1): F9:B5:B6:32:45:5F:9C:BE:EC:57:5F:80:DC:E9:6E:2C:C7:B2:78:B7 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Commercial" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\371\265\266\062\105\137\234\276\354\127\137\200\334\351\156\054 -\307\262\170\267 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\202\222\272\133\357\315\212\157\246\075\125\371\204\366\326\267 -END -CKA_ISSUER MULTILINE_OCTAL -\060\104\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\037\060\035\006\003\125\004\003\014\026 -\101\146\146\151\162\155\124\162\165\163\164\040\103\157\155\155 -\145\162\143\151\141\154 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\167\167\006\047\046\251\261\174 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "AffirmTrust Networking" -# -# Issuer: CN=AffirmTrust Networking,O=AffirmTrust,C=US -# Serial Number:7c:4f:04:39:1c:d4:99:2d -# Subject: CN=AffirmTrust Networking,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:08:24 2010 -# Not Valid After : Tue Dec 31 14:08:24 2030 -# Fingerprint (SHA-256): 0A:81:EC:5A:92:97:77:F1:45:90:4A:F3:8D:5D:50:9F:66:B5:E2:C5:8F:CD:B5:31:05:8B:0E:17:F3:F0:B4:1B -# Fingerprint (SHA1): 29:36:21:02:8B:20:ED:02:F5:66:C5:32:D1:D6:ED:90:9F:45:00:2F -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Networking" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\104\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\037\060\035\006\003\125\004\003\014\026 -\101\146\146\151\162\155\124\162\165\163\164\040\116\145\164\167 -\157\162\153\151\156\147 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\104\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\037\060\035\006\003\125\004\003\014\026 -\101\146\146\151\162\155\124\162\165\163\164\040\116\145\164\167 -\157\162\153\151\156\147 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\174\117\004\071\034\324\231\055 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\003\114\060\202\002\064\240\003\002\001\002\002\010\174 -\117\004\071\034\324\231\055\060\015\006\011\052\206\110\206\367 -\015\001\001\005\005\000\060\104\061\013\060\011\006\003\125\004 -\006\023\002\125\123\061\024\060\022\006\003\125\004\012\014\013 -\101\146\146\151\162\155\124\162\165\163\164\061\037\060\035\006 -\003\125\004\003\014\026\101\146\146\151\162\155\124\162\165\163 -\164\040\116\145\164\167\157\162\153\151\156\147\060\036\027\015 -\061\060\060\061\062\071\061\064\060\070\062\064\132\027\015\063 -\060\061\062\063\061\061\064\060\070\062\064\132\060\104\061\013 -\060\011\006\003\125\004\006\023\002\125\123\061\024\060\022\006 -\003\125\004\012\014\013\101\146\146\151\162\155\124\162\165\163 -\164\061\037\060\035\006\003\125\004\003\014\026\101\146\146\151 -\162\155\124\162\165\163\164\040\116\145\164\167\157\162\153\151 -\156\147\060\202\001\042\060\015\006\011\052\206\110\206\367\015 -\001\001\001\005\000\003\202\001\017\000\060\202\001\012\002\202 -\001\001\000\264\204\314\063\027\056\153\224\154\153\141\122\240 -\353\243\317\171\224\114\345\224\200\231\313\125\144\104\145\217 -\147\144\342\006\343\134\067\111\366\057\233\204\204\036\055\362 -\140\235\060\116\314\204\205\342\054\317\036\236\376\066\253\063 -\167\065\104\330\065\226\032\075\066\350\172\016\330\325\107\241 -\152\151\213\331\374\273\072\256\171\132\325\364\326\161\273\232 -\220\043\153\232\267\210\164\207\014\036\137\271\236\055\372\253 -\123\053\334\273\166\076\223\114\010\010\214\036\242\043\034\324 -\152\255\042\272\231\001\056\155\145\313\276\044\146\125\044\113 -\100\104\261\033\327\341\302\205\300\336\020\077\075\355\270\374 -\361\361\043\123\334\277\145\227\157\331\371\100\161\215\175\275 -\225\324\316\276\240\136\047\043\336\375\246\320\046\016\000\051 -\353\074\106\360\075\140\277\077\120\322\334\046\101\121\236\024 -\067\102\004\243\160\127\250\033\207\355\055\372\173\356\214\012 -\343\251\146\211\031\313\101\371\335\104\066\141\317\342\167\106 -\310\175\366\364\222\201\066\375\333\064\361\162\176\363\014\026 -\275\264\025\002\003\001\000\001\243\102\060\100\060\035\006\003 -\125\035\016\004\026\004\024\007\037\322\347\234\332\302\156\242 -\100\264\260\172\120\020\120\164\304\310\275\060\017\006\003\125 -\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006\003 -\125\035\017\001\001\377\004\004\003\002\001\006\060\015\006\011 -\052\206\110\206\367\015\001\001\005\005\000\003\202\001\001\000 -\211\127\262\026\172\250\302\375\326\331\233\233\064\302\234\264 -\062\024\115\247\244\337\354\276\247\276\370\103\333\221\067\316 -\264\062\056\120\125\032\065\116\166\103\161\040\357\223\167\116 -\025\160\056\207\303\301\035\155\334\313\265\047\324\054\126\321 -\122\123\072\104\322\163\310\304\033\005\145\132\142\222\234\356 -\101\215\061\333\347\064\352\131\041\325\001\172\327\144\270\144 -\071\315\311\355\257\355\113\003\110\247\240\231\001\200\334\145 -\243\066\256\145\131\110\117\202\113\310\145\361\127\035\345\131 -\056\012\077\154\330\321\365\345\011\264\154\124\000\012\340\025 -\115\207\165\155\267\130\226\132\335\155\322\000\240\364\233\110 -\276\303\067\244\272\066\340\174\207\205\227\032\025\242\336\056 -\242\133\275\257\030\371\220\120\315\160\131\370\047\147\107\313 -\307\240\007\072\175\321\054\135\154\031\072\146\265\175\375\221 -\157\202\261\276\010\223\333\024\107\361\242\067\307\105\236\074 -\307\167\257\144\250\223\337\366\151\203\202\140\362\111\102\064 -\355\132\000\124\205\034\026\066\222\014\134\372\246\255\277\333 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -# For Server Distrust After: Sat Nov 30 23:59:59 2024 -CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL -\062\064\061\061\063\060\062\063\065\071\065\071\132 -END -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "AffirmTrust Networking" -# Issuer: CN=AffirmTrust Networking,O=AffirmTrust,C=US -# Serial Number:7c:4f:04:39:1c:d4:99:2d -# Subject: CN=AffirmTrust Networking,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:08:24 2010 -# Not Valid After : Tue Dec 31 14:08:24 2030 -# Fingerprint (SHA-256): 0A:81:EC:5A:92:97:77:F1:45:90:4A:F3:8D:5D:50:9F:66:B5:E2:C5:8F:CD:B5:31:05:8B:0E:17:F3:F0:B4:1B -# Fingerprint (SHA1): 29:36:21:02:8B:20:ED:02:F5:66:C5:32:D1:D6:ED:90:9F:45:00:2F -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Networking" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\051\066\041\002\213\040\355\002\365\146\305\062\321\326\355\220 -\237\105\000\057 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\102\145\312\276\001\232\232\114\251\214\101\111\315\300\325\177 -END -CKA_ISSUER MULTILINE_OCTAL -\060\104\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\037\060\035\006\003\125\004\003\014\026 -\101\146\146\151\162\155\124\162\165\163\164\040\116\145\164\167 -\157\162\153\151\156\147 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\174\117\004\071\034\324\231\055 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "AffirmTrust Premium" -# -# Issuer: CN=AffirmTrust Premium,O=AffirmTrust,C=US -# Serial Number:6d:8c:14:46:b1:a6:0a:ee -# Subject: CN=AffirmTrust Premium,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:10:36 2010 -# Not Valid After : Mon Dec 31 14:10:36 2040 -# Fingerprint (SHA-256): 70:A7:3F:7F:37:6B:60:07:42:48:90:45:34:B1:14:82:D5:BF:0E:69:8E:CC:49:8D:F5:25:77:EB:F2:E9:3B:9A -# Fingerprint (SHA1): D8:A6:33:2C:E0:03:6F:B1:85:F6:63:4F:7D:6A:06:65:26:32:28:27 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Premium" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\101\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\034\060\032\006\003\125\004\003\014\023 -\101\146\146\151\162\155\124\162\165\163\164\040\120\162\145\155 -\151\165\155 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\101\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\034\060\032\006\003\125\004\003\014\023 -\101\146\146\151\162\155\124\162\165\163\164\040\120\162\145\155 -\151\165\155 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\155\214\024\106\261\246\012\356 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\005\106\060\202\003\056\240\003\002\001\002\002\010\155 -\214\024\106\261\246\012\356\060\015\006\011\052\206\110\206\367 -\015\001\001\014\005\000\060\101\061\013\060\011\006\003\125\004 -\006\023\002\125\123\061\024\060\022\006\003\125\004\012\014\013 -\101\146\146\151\162\155\124\162\165\163\164\061\034\060\032\006 -\003\125\004\003\014\023\101\146\146\151\162\155\124\162\165\163 -\164\040\120\162\145\155\151\165\155\060\036\027\015\061\060\060 -\061\062\071\061\064\061\060\063\066\132\027\015\064\060\061\062 -\063\061\061\064\061\060\063\066\132\060\101\061\013\060\011\006 -\003\125\004\006\023\002\125\123\061\024\060\022\006\003\125\004 -\012\014\013\101\146\146\151\162\155\124\162\165\163\164\061\034 -\060\032\006\003\125\004\003\014\023\101\146\146\151\162\155\124 -\162\165\163\164\040\120\162\145\155\151\165\155\060\202\002\042 -\060\015\006\011\052\206\110\206\367\015\001\001\001\005\000\003 -\202\002\017\000\060\202\002\012\002\202\002\001\000\304\022\337 -\251\137\376\101\335\335\365\237\212\343\366\254\341\074\170\232 -\274\330\360\177\172\240\063\052\334\215\040\133\256\055\157\347 -\223\331\066\160\152\150\317\216\121\243\205\133\147\004\240\020 -\044\157\135\050\202\301\227\127\330\110\051\023\266\341\276\221 -\115\337\205\014\123\030\232\036\044\242\117\217\360\242\205\013 -\313\364\051\177\322\244\130\356\046\115\311\252\250\173\232\331 -\372\070\336\104\127\025\345\370\214\310\331\110\342\015\026\047 -\035\036\310\203\205\045\267\272\252\125\101\314\003\042\113\055 -\221\215\213\346\211\257\146\307\351\377\053\351\074\254\332\322 -\263\303\341\150\234\211\370\172\000\126\336\364\125\225\154\373 -\272\144\335\142\213\337\013\167\062\353\142\314\046\232\233\273 -\252\142\203\114\264\006\172\060\310\051\277\355\006\115\227\271 -\034\304\061\053\325\137\274\123\022\027\234\231\127\051\146\167 -\141\041\061\007\056\045\111\235\030\362\356\363\053\161\214\265 -\272\071\007\111\167\374\357\056\222\220\005\215\055\057\167\173 -\357\103\277\065\273\232\330\371\163\247\054\362\320\127\356\050 -\116\046\137\217\220\150\011\057\270\370\334\006\351\056\232\076 -\121\247\321\042\304\012\247\070\110\154\263\371\377\175\253\206 -\127\343\272\326\205\170\167\272\103\352\110\177\366\330\276\043 -\155\036\277\321\066\154\130\134\361\356\244\031\124\032\365\003 -\322\166\346\341\214\275\074\263\323\110\113\342\310\370\177\222 -\250\166\106\234\102\145\076\244\036\301\007\003\132\106\055\270 -\227\363\267\325\262\125\041\357\272\334\114\000\227\373\024\225 -\047\063\277\350\103\107\106\322\010\231\026\140\073\232\176\322 -\346\355\070\352\354\001\036\074\110\126\111\011\307\114\067\000 -\236\210\016\300\163\341\157\146\351\162\107\060\076\020\345\013 -\003\311\232\102\000\154\305\224\176\141\304\212\337\177\202\032 -\013\131\304\131\062\167\263\274\140\151\126\071\375\264\006\173 -\054\326\144\066\331\275\110\355\204\037\176\245\042\217\052\270 -\102\364\202\267\324\123\220\170\116\055\032\375\201\157\104\327 -\073\001\164\226\102\340\000\342\056\153\352\305\356\162\254\273 -\277\376\352\252\250\370\334\366\262\171\212\266\147\002\003\001 -\000\001\243\102\060\100\060\035\006\003\125\035\016\004\026\004 -\024\235\300\147\246\014\042\331\046\365\105\253\246\145\122\021 -\047\330\105\254\143\060\017\006\003\125\035\023\001\001\377\004 -\005\060\003\001\001\377\060\016\006\003\125\035\017\001\001\377 -\004\004\003\002\001\006\060\015\006\011\052\206\110\206\367\015 -\001\001\014\005\000\003\202\002\001\000\263\127\115\020\142\116 -\072\344\254\352\270\034\257\062\043\310\263\111\132\121\234\166 -\050\215\171\252\127\106\027\325\365\122\366\267\104\350\010\104 -\277\030\204\322\013\200\315\305\022\375\000\125\005\141\207\101 -\334\265\044\236\074\304\330\310\373\160\236\057\170\226\203\040 -\066\336\174\017\151\023\210\245\165\066\230\010\246\306\337\254 -\316\343\130\326\267\076\336\272\363\353\064\100\330\242\201\365 -\170\077\057\325\245\374\331\242\324\136\004\016\027\255\376\101 -\360\345\262\162\372\104\202\063\102\350\055\130\367\126\214\142 -\077\272\102\260\234\014\134\176\056\145\046\134\123\117\000\262 -\170\176\241\015\231\055\215\270\035\216\242\304\260\375\140\320 -\060\244\216\310\004\142\251\304\355\065\336\172\227\355\016\070 -\136\222\057\223\160\245\251\234\157\247\175\023\035\176\306\010 -\110\261\136\147\353\121\010\045\351\346\045\153\122\051\221\234 -\322\071\163\010\127\336\231\006\264\133\235\020\006\341\302\000 -\250\270\034\112\002\012\024\320\301\101\312\373\214\065\041\175 -\202\070\362\251\124\221\031\065\223\224\155\152\072\305\262\320 -\273\211\206\223\350\233\311\017\072\247\172\270\241\360\170\106 -\372\374\067\057\345\212\204\363\337\376\004\331\241\150\240\057 -\044\342\011\225\006\325\225\312\341\044\226\353\174\366\223\005 -\273\355\163\351\055\321\165\071\327\347\044\333\330\116\137\103 -\217\236\320\024\071\277\125\160\110\231\127\061\264\234\356\112 -\230\003\226\060\037\140\006\356\033\043\376\201\140\043\032\107 -\142\205\245\314\031\064\200\157\263\254\032\343\237\360\173\110 -\255\325\001\331\147\266\251\162\223\352\055\146\265\262\270\344 -\075\074\262\357\114\214\352\353\007\277\253\065\232\125\206\274 -\030\246\265\250\136\264\203\154\153\151\100\323\237\334\361\303 -\151\153\271\341\155\011\364\361\252\120\166\012\172\175\172\027 -\241\125\226\102\231\061\011\335\140\021\215\005\060\176\346\216 -\106\321\235\024\332\307\027\344\005\226\214\304\044\265\033\317 -\024\007\262\100\370\243\236\101\206\274\004\320\153\226\310\052 -\200\064\375\277\357\006\243\335\130\305\205\075\076\217\376\236 -\051\340\266\270\011\150\031\034\030\103 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -# For Server Distrust After: Sat Nov 30 23:59:59 2024 -CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL -\062\064\061\061\063\060\062\063\065\071\065\071\132 -END -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "AffirmTrust Premium" -# Issuer: CN=AffirmTrust Premium,O=AffirmTrust,C=US -# Serial Number:6d:8c:14:46:b1:a6:0a:ee -# Subject: CN=AffirmTrust Premium,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:10:36 2010 -# Not Valid After : Mon Dec 31 14:10:36 2040 -# Fingerprint (SHA-256): 70:A7:3F:7F:37:6B:60:07:42:48:90:45:34:B1:14:82:D5:BF:0E:69:8E:CC:49:8D:F5:25:77:EB:F2:E9:3B:9A -# Fingerprint (SHA1): D8:A6:33:2C:E0:03:6F:B1:85:F6:63:4F:7D:6A:06:65:26:32:28:27 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Premium" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\330\246\063\054\340\003\157\261\205\366\143\117\175\152\006\145 -\046\062\050\047 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\304\135\016\110\266\254\050\060\116\012\274\371\070\026\207\127 -END -CKA_ISSUER MULTILINE_OCTAL -\060\101\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\034\060\032\006\003\125\004\003\014\023 -\101\146\146\151\162\155\124\162\165\163\164\040\120\162\145\155 -\151\165\155 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\155\214\024\106\261\246\012\356 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "AffirmTrust Premium ECC" -# -# Issuer: CN=AffirmTrust Premium ECC,O=AffirmTrust,C=US -# Serial Number:74:97:25:8a:c7:3f:7a:54 -# Subject: CN=AffirmTrust Premium ECC,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:20:24 2010 -# Not Valid After : Mon Dec 31 14:20:24 2040 -# Fingerprint (SHA-256): BD:71:FD:F6:DA:97:E4:CF:62:D1:64:7A:DD:25:81:B0:7D:79:AD:F8:39:7E:B4:EC:BA:9C:5E:84:88:82:14:23 -# Fingerprint (SHA1): B8:23:6B:00:2F:1D:16:86:53:01:55:6C:11:A4:37:CA:EB:FF:C3:BB -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Premium ECC" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\105\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\040\060\036\006\003\125\004\003\014\027 -\101\146\146\151\162\155\124\162\165\163\164\040\120\162\145\155 -\151\165\155\040\105\103\103 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\105\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\040\060\036\006\003\125\004\003\014\027 -\101\146\146\151\162\155\124\162\165\163\164\040\120\162\145\155 -\151\165\155\040\105\103\103 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\164\227\045\212\307\077\172\124 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\001\376\060\202\001\205\240\003\002\001\002\002\010\164 -\227\045\212\307\077\172\124\060\012\006\010\052\206\110\316\075 -\004\003\003\060\105\061\013\060\011\006\003\125\004\006\023\002 -\125\123\061\024\060\022\006\003\125\004\012\014\013\101\146\146 -\151\162\155\124\162\165\163\164\061\040\060\036\006\003\125\004 -\003\014\027\101\146\146\151\162\155\124\162\165\163\164\040\120 -\162\145\155\151\165\155\040\105\103\103\060\036\027\015\061\060 -\060\061\062\071\061\064\062\060\062\064\132\027\015\064\060\061 -\062\063\061\061\064\062\060\062\064\132\060\105\061\013\060\011 -\006\003\125\004\006\023\002\125\123\061\024\060\022\006\003\125 -\004\012\014\013\101\146\146\151\162\155\124\162\165\163\164\061 -\040\060\036\006\003\125\004\003\014\027\101\146\146\151\162\155 -\124\162\165\163\164\040\120\162\145\155\151\165\155\040\105\103 -\103\060\166\060\020\006\007\052\206\110\316\075\002\001\006\005 -\053\201\004\000\042\003\142\000\004\015\060\136\033\025\235\003 -\320\241\171\065\267\072\074\222\172\312\025\034\315\142\363\234 -\046\134\007\075\345\124\372\243\326\314\022\352\364\024\137\350 -\216\031\253\057\056\110\346\254\030\103\170\254\320\067\303\275 -\262\315\054\346\107\342\032\346\143\270\075\056\057\170\304\117 -\333\364\017\244\150\114\125\162\153\225\035\116\030\102\225\170 -\314\067\074\221\342\233\145\053\051\243\102\060\100\060\035\006 -\003\125\035\016\004\026\004\024\232\257\051\172\300\021\065\065 -\046\121\060\000\303\152\376\100\325\256\326\074\060\017\006\003 -\125\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006 -\003\125\035\017\001\001\377\004\004\003\002\001\006\060\012\006 -\010\052\206\110\316\075\004\003\003\003\147\000\060\144\002\060 -\027\011\363\207\210\120\132\257\310\300\102\277\107\137\365\154 -\152\206\340\304\047\164\344\070\123\327\005\177\033\064\343\306 -\057\263\312\011\074\067\235\327\347\270\106\361\375\241\342\161 -\002\060\102\131\207\103\324\121\337\272\323\011\062\132\316\210 -\176\127\075\234\137\102\153\365\007\055\265\360\202\223\371\131 -\157\256\144\372\130\345\213\036\343\143\276\265\201\315\157\002 -\214\171 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -# For Server Distrust After: Sat Nov 30 23:59:59 2024 -CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL -\062\064\061\061\063\060\062\063\065\071\065\071\132 -END -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "AffirmTrust Premium ECC" -# Issuer: CN=AffirmTrust Premium ECC,O=AffirmTrust,C=US -# Serial Number:74:97:25:8a:c7:3f:7a:54 -# Subject: CN=AffirmTrust Premium ECC,O=AffirmTrust,C=US -# Not Valid Before: Fri Jan 29 14:20:24 2010 -# Not Valid After : Mon Dec 31 14:20:24 2040 -# Fingerprint (SHA-256): BD:71:FD:F6:DA:97:E4:CF:62:D1:64:7A:DD:25:81:B0:7D:79:AD:F8:39:7E:B4:EC:BA:9C:5E:84:88:82:14:23 -# Fingerprint (SHA1): B8:23:6B:00:2F:1D:16:86:53:01:55:6C:11:A4:37:CA:EB:FF:C3:BB -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "AffirmTrust Premium ECC" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\270\043\153\000\057\035\026\206\123\001\125\154\021\244\067\312 -\353\377\303\273 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\144\260\011\125\317\261\325\231\342\276\023\253\246\135\352\115 -END -CKA_ISSUER MULTILINE_OCTAL -\060\105\061\013\060\011\006\003\125\004\006\023\002\125\123\061 -\024\060\022\006\003\125\004\012\014\013\101\146\146\151\162\155 -\124\162\165\163\164\061\040\060\036\006\003\125\004\003\014\027 -\101\146\146\151\162\155\124\162\165\163\164\040\120\162\145\155 -\151\165\155\040\105\103\103 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\010\164\227\045\212\307\077\172\124 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - # # Certificate "Certum Trusted Network CA" # @@ -6801,192 +5714,34 @@ CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE # Trust for "TWCA Global Root CA" # Issuer: CN=TWCA Global Root CA,OU=Root CA,O=TAIWAN-CA,C=TW -# Serial Number: 3262 (0xcbe) -# Subject: CN=TWCA Global Root CA,OU=Root CA,O=TAIWAN-CA,C=TW -# Not Valid Before: Wed Jun 27 06:28:33 2012 -# Not Valid After : Tue Dec 31 15:59:59 2030 -# Fingerprint (SHA-256): 59:76:90:07:F7:68:5D:0F:CD:50:87:2F:9F:95:D5:75:5A:5B:2B:45:7D:81:F3:69:2B:61:0A:98:67:2F:0E:1B -# Fingerprint (SHA1): 9C:BB:48:53:F6:A4:F6:D3:52:A4:E8:32:52:55:60:13:F5:AD:AF:65 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "TWCA Global Root CA" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\234\273\110\123\366\244\366\323\122\244\350\062\122\125\140\023 -\365\255\257\145 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\371\003\176\317\346\236\074\163\172\052\220\007\151\377\053\226 -END -CKA_ISSUER MULTILINE_OCTAL -\060\121\061\013\060\011\006\003\125\004\006\023\002\124\127\061 -\022\060\020\006\003\125\004\012\023\011\124\101\111\127\101\116 -\055\103\101\061\020\060\016\006\003\125\004\013\023\007\122\157 -\157\164\040\103\101\061\034\060\032\006\003\125\004\003\023\023 -\124\127\103\101\040\107\154\157\142\141\154\040\122\157\157\164 -\040\103\101 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\002\014\276 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "TeliaSonera Root CA v1" -# -# Issuer: CN=TeliaSonera Root CA v1,O=TeliaSonera -# Serial Number:00:95:be:16:a0:f7:2e:46:f1:7b:39:82:72:fa:8b:cd:96 -# Subject: CN=TeliaSonera Root CA v1,O=TeliaSonera -# Not Valid Before: Thu Oct 18 12:00:50 2007 -# Not Valid After : Mon Oct 18 12:00:50 2032 -# Fingerprint (SHA-256): DD:69:36:FE:21:F8:F0:77:C1:23:A1:A5:21:C1:22:24:F7:22:55:B7:3E:03:A7:26:06:93:E8:A2:4B:0F:A3:89 -# Fingerprint (SHA1): 43:13:BB:96:F1:D5:86:9B:C1:4E:6A:92:F6:CF:F6:34:69:87:82:37 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "TeliaSonera Root CA v1" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\067\061\024\060\022\006\003\125\004\012\014\013\124\145\154 -\151\141\123\157\156\145\162\141\061\037\060\035\006\003\125\004 -\003\014\026\124\145\154\151\141\123\157\156\145\162\141\040\122 -\157\157\164\040\103\101\040\166\061 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\067\061\024\060\022\006\003\125\004\012\014\013\124\145\154 -\151\141\123\157\156\145\162\141\061\037\060\035\006\003\125\004 -\003\014\026\124\145\154\151\141\123\157\156\145\162\141\040\122 -\157\157\164\040\103\101\040\166\061 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\021\000\225\276\026\240\367\056\106\361\173\071\202\162\372 -\213\315\226 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\005\070\060\202\003\040\240\003\002\001\002\002\021\000 -\225\276\026\240\367\056\106\361\173\071\202\162\372\213\315\226 -\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060 -\067\061\024\060\022\006\003\125\004\012\014\013\124\145\154\151 -\141\123\157\156\145\162\141\061\037\060\035\006\003\125\004\003 -\014\026\124\145\154\151\141\123\157\156\145\162\141\040\122\157 -\157\164\040\103\101\040\166\061\060\036\027\015\060\067\061\060 -\061\070\061\062\060\060\065\060\132\027\015\063\062\061\060\061 -\070\061\062\060\060\065\060\132\060\067\061\024\060\022\006\003 -\125\004\012\014\013\124\145\154\151\141\123\157\156\145\162\141 -\061\037\060\035\006\003\125\004\003\014\026\124\145\154\151\141 -\123\157\156\145\162\141\040\122\157\157\164\040\103\101\040\166 -\061\060\202\002\042\060\015\006\011\052\206\110\206\367\015\001 -\001\001\005\000\003\202\002\017\000\060\202\002\012\002\202\002 -\001\000\302\276\353\047\360\041\243\363\151\046\125\176\235\305 -\125\026\221\134\375\357\041\277\123\200\172\055\322\221\214\143 -\061\360\354\044\360\303\245\322\162\174\020\155\364\067\267\345 -\346\174\171\352\214\265\202\213\256\110\266\254\000\334\145\165 -\354\052\115\137\301\207\365\040\145\053\201\250\107\076\211\043 -\225\060\026\220\177\350\127\007\110\347\031\256\277\105\147\261 -\067\033\006\052\376\336\371\254\175\203\373\136\272\344\217\227 -\147\276\113\216\215\144\007\127\070\125\151\064\066\075\023\110 -\357\117\342\323\146\036\244\317\032\267\136\066\063\324\264\006 -\275\030\001\375\167\204\120\000\105\365\214\135\350\043\274\176 -\376\065\341\355\120\173\251\060\215\031\323\011\216\150\147\135 -\277\074\227\030\123\273\051\142\305\312\136\162\301\307\226\324 -\333\055\240\264\037\151\003\354\352\342\120\361\014\074\360\254 -\363\123\055\360\034\365\355\154\071\071\163\200\026\310\122\260 -\043\315\340\076\334\335\074\107\240\273\065\212\342\230\150\213 -\276\345\277\162\356\322\372\245\355\022\355\374\230\030\251\046 -\166\334\050\113\020\040\034\323\177\026\167\055\355\157\200\367 -\111\273\123\005\273\135\150\307\324\310\165\026\077\211\132\213 -\367\027\107\324\114\361\322\211\171\076\115\075\230\250\141\336 -\072\036\322\370\136\003\340\301\311\034\214\323\215\115\323\225 -\066\263\067\137\143\143\233\063\024\360\055\046\153\123\174\211 -\214\062\302\156\354\075\041\000\071\311\241\150\342\120\203\056 -\260\072\053\363\066\240\254\057\344\157\141\302\121\011\071\076 -\213\123\271\273\147\332\334\123\271\166\131\066\235\103\345\040 -\340\075\062\140\205\042\121\267\307\063\273\335\025\057\244\170 -\246\007\173\201\106\066\004\206\335\171\065\307\225\054\073\260 -\243\027\065\345\163\037\264\134\131\357\332\352\020\145\173\172 -\320\177\237\263\264\052\067\073\160\213\233\133\271\053\267\354 -\262\121\022\227\123\051\132\324\360\022\020\334\117\002\273\022 -\222\057\142\324\077\151\103\174\015\326\374\130\165\001\210\235 -\130\026\113\336\272\220\377\107\001\211\006\152\366\137\262\220 -\152\263\002\246\002\210\277\263\107\176\052\331\325\372\150\170 -\065\115\002\003\001\000\001\243\077\060\075\060\017\006\003\125 -\035\023\001\001\377\004\005\060\003\001\001\377\060\013\006\003 -\125\035\017\004\004\003\002\001\006\060\035\006\003\125\035\016 -\004\026\004\024\360\217\131\070\000\263\365\217\232\226\014\325 -\353\372\173\252\027\350\023\022\060\015\006\011\052\206\110\206 -\367\015\001\001\005\005\000\003\202\002\001\000\276\344\134\142 -\116\044\364\014\010\377\360\323\014\150\344\223\111\042\077\104 -\047\157\273\155\336\203\146\316\250\314\015\374\365\232\006\345 -\167\024\221\353\235\101\173\231\052\204\345\377\374\041\301\135 -\360\344\037\127\267\165\251\241\137\002\046\377\327\307\367\116 -\336\117\370\367\034\106\300\172\117\100\054\042\065\360\031\261 -\320\153\147\054\260\250\340\300\100\067\065\366\204\134\134\343 -\257\102\170\376\247\311\015\120\352\015\204\166\366\121\357\203 -\123\306\172\377\016\126\111\056\217\172\326\014\346\047\124\343 -\115\012\140\162\142\315\221\007\326\245\277\310\231\153\355\304 -\031\346\253\114\021\070\305\157\061\342\156\111\310\077\166\200 -\046\003\046\051\340\066\366\366\040\123\343\027\160\064\027\235 -\143\150\036\153\354\303\115\206\270\023\060\057\135\106\015\107 -\103\325\033\252\131\016\271\134\215\006\110\255\164\207\137\307 -\374\061\124\101\023\342\307\041\016\236\340\036\015\341\300\173 -\103\205\220\305\212\130\306\145\012\170\127\362\306\043\017\001 -\331\040\113\336\017\373\222\205\165\052\134\163\215\155\173\045 -\221\312\356\105\256\006\113\000\314\323\261\131\120\332\072\210 -\073\051\103\106\136\227\053\124\316\123\157\215\112\347\226\372 -\277\161\016\102\213\174\375\050\240\320\110\312\332\304\201\114 -\273\242\163\223\046\310\353\014\326\046\210\266\300\044\317\273 -\275\133\353\165\175\351\010\216\206\063\054\171\167\011\151\245 -\211\374\263\160\220\207\166\217\323\042\273\102\316\275\163\013 -\040\046\052\320\233\075\160\036\044\154\315\207\166\251\027\226 -\267\317\015\222\373\216\030\251\230\111\321\236\376\140\104\162 -\041\271\031\355\302\365\061\361\071\110\210\220\044\165\124\026 -\255\316\364\370\151\024\144\071\373\243\270\272\160\100\307\047 -\034\277\304\126\123\372\143\145\320\363\034\016\026\365\153\206 -\130\115\030\324\344\015\216\245\235\133\221\334\166\044\120\077 -\306\052\373\331\267\234\265\326\346\320\331\350\031\213\025\161 -\110\255\267\352\330\131\210\324\220\277\026\263\331\351\254\131 -\141\124\310\034\272\312\301\312\341\271\040\114\217\072\223\211 -\245\240\314\277\323\366\165\244\165\226\155\126 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "TeliaSonera Root CA v1" -# Issuer: CN=TeliaSonera Root CA v1,O=TeliaSonera -# Serial Number:00:95:be:16:a0:f7:2e:46:f1:7b:39:82:72:fa:8b:cd:96 -# Subject: CN=TeliaSonera Root CA v1,O=TeliaSonera -# Not Valid Before: Thu Oct 18 12:00:50 2007 -# Not Valid After : Mon Oct 18 12:00:50 2032 -# Fingerprint (SHA-256): DD:69:36:FE:21:F8:F0:77:C1:23:A1:A5:21:C1:22:24:F7:22:55:B7:3E:03:A7:26:06:93:E8:A2:4B:0F:A3:89 -# Fingerprint (SHA1): 43:13:BB:96:F1:D5:86:9B:C1:4E:6A:92:F6:CF:F6:34:69:87:82:37 +# Serial Number: 3262 (0xcbe) +# Subject: CN=TWCA Global Root CA,OU=Root CA,O=TAIWAN-CA,C=TW +# Not Valid Before: Wed Jun 27 06:28:33 2012 +# Not Valid After : Tue Dec 31 15:59:59 2030 +# Fingerprint (SHA-256): 59:76:90:07:F7:68:5D:0F:CD:50:87:2F:9F:95:D5:75:5A:5B:2B:45:7D:81:F3:69:2B:61:0A:98:67:2F:0E:1B +# Fingerprint (SHA1): 9C:BB:48:53:F6:A4:F6:D3:52:A4:E8:32:52:55:60:13:F5:AD:AF:65 CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST CKA_TOKEN CK_BBOOL CK_TRUE CKA_PRIVATE CK_BBOOL CK_FALSE CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "TeliaSonera Root CA v1" +CKA_LABEL UTF8 "TWCA Global Root CA" CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\103\023\273\226\361\325\206\233\301\116\152\222\366\317\366\064 -\151\207\202\067 +\234\273\110\123\366\244\366\323\122\244\350\062\122\125\140\023 +\365\255\257\145 END CKA_CERT_MD5_HASH MULTILINE_OCTAL -\067\101\111\033\030\126\232\046\365\255\302\146\373\100\245\114 +\371\003\176\317\346\236\074\163\172\052\220\007\151\377\053\226 END CKA_ISSUER MULTILINE_OCTAL -\060\067\061\024\060\022\006\003\125\004\012\014\013\124\145\154 -\151\141\123\157\156\145\162\141\061\037\060\035\006\003\125\004 -\003\014\026\124\145\154\151\141\123\157\156\145\162\141\040\122 -\157\157\164\040\103\101\040\166\061 +\060\121\061\013\060\011\006\003\125\004\006\023\002\124\127\061 +\022\060\020\006\003\125\004\012\023\011\124\101\111\127\101\116 +\055\103\101\061\020\060\016\006\003\125\004\013\023\007\122\157 +\157\164\040\103\101\061\034\060\032\006\003\125\004\003\023\023 +\124\127\103\101\040\107\154\157\142\141\154\040\122\157\157\164 +\040\103\101 END CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\021\000\225\276\026\240\367\056\106\361\173\071\202\162\372 -\213\315\226 +\002\002\014\276 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR @@ -9729,7 +8484,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\004\112\123\214\050 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -9879,7 +8634,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\015\000\246\213\171\051\000\000\000\000\120\320\221\371 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -11295,7 +10050,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \346\226\066\133\312 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -11454,7 +10209,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \133\046\273\212\067 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -11556,7 +10311,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \236\166\003\362\112 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -11662,7 +10417,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \054\310\032\301\016 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -15017,449 +13772,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE -# -# Certificate "Trustwave Global Certification Authority" -# -# Issuer: CN=Trustwave Global Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Serial Number:05:f7:0e:86:da:49:f3:46:35:2e:ba:b2 -# Subject: CN=Trustwave Global Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Not Valid Before: Wed Aug 23 19:34:12 2017 -# Not Valid After : Sat Aug 23 19:34:12 2042 -# Fingerprint (SHA-256): 97:55:20:15:F5:DD:FC:3C:87:88:C0:06:94:45:55:40:88:94:45:00:84:F1:00:86:70:86:BC:1A:2B:B5:8D:C8 -# Fingerprint (SHA1): 2F:8F:36:4F:E1:58:97:44:21:59:87:A5:2A:9A:D0:69:95:26:7F:B5 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Trustwave Global Certification Authority" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\201\210\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\014\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\014\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\014\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\061\060\057\006\003\125\004 -\003\014\050\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\103\145\162\164\151\146\151\143\141\164\151\157 -\156\040\101\165\164\150\157\162\151\164\171 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\201\210\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\014\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\014\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\014\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\061\060\057\006\003\125\004 -\003\014\050\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\103\145\162\164\151\146\151\143\141\164\151\157 -\156\040\101\165\164\150\157\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\014\005\367\016\206\332\111\363\106\065\056\272\262 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\005\332\060\202\003\302\240\003\002\001\002\002\014\005 -\367\016\206\332\111\363\106\065\056\272\262\060\015\006\011\052 -\206\110\206\367\015\001\001\013\005\000\060\201\210\061\013\060 -\011\006\003\125\004\006\023\002\125\123\061\021\060\017\006\003 -\125\004\010\014\010\111\154\154\151\156\157\151\163\061\020\060 -\016\006\003\125\004\007\014\007\103\150\151\143\141\147\157\061 -\041\060\037\006\003\125\004\012\014\030\124\162\165\163\164\167 -\141\166\145\040\110\157\154\144\151\156\147\163\054\040\111\156 -\143\056\061\061\060\057\006\003\125\004\003\014\050\124\162\165 -\163\164\167\141\166\145\040\107\154\157\142\141\154\040\103\145 -\162\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150 -\157\162\151\164\171\060\036\027\015\061\067\060\070\062\063\061 -\071\063\064\061\062\132\027\015\064\062\060\070\062\063\061\071 -\063\064\061\062\132\060\201\210\061\013\060\011\006\003\125\004 -\006\023\002\125\123\061\021\060\017\006\003\125\004\010\014\010 -\111\154\154\151\156\157\151\163\061\020\060\016\006\003\125\004 -\007\014\007\103\150\151\143\141\147\157\061\041\060\037\006\003 -\125\004\012\014\030\124\162\165\163\164\167\141\166\145\040\110 -\157\154\144\151\156\147\163\054\040\111\156\143\056\061\061\060 -\057\006\003\125\004\003\014\050\124\162\165\163\164\167\141\166 -\145\040\107\154\157\142\141\154\040\103\145\162\164\151\146\151 -\143\141\164\151\157\156\040\101\165\164\150\157\162\151\164\171 -\060\202\002\042\060\015\006\011\052\206\110\206\367\015\001\001 -\001\005\000\003\202\002\017\000\060\202\002\012\002\202\002\001 -\000\271\135\121\050\113\074\067\222\321\202\316\275\035\275\315 -\335\270\253\317\012\076\341\135\345\334\252\011\271\127\002\076 -\346\143\141\337\362\017\202\143\256\243\367\254\163\321\174\347 -\263\013\257\010\000\011\131\177\315\051\052\210\223\207\027\030 -\200\355\210\262\264\266\020\037\055\326\137\125\242\023\135\321 -\306\353\006\126\211\210\376\254\062\235\375\134\303\005\307\156 -\356\206\211\272\210\003\235\162\041\206\220\256\217\003\245\334 -\237\210\050\313\243\222\111\017\354\320\017\342\155\104\117\200 -\152\262\324\347\240\012\123\001\272\216\227\221\166\156\274\374 -\325\153\066\346\100\210\326\173\057\137\005\350\054\155\021\363 -\347\262\276\222\104\114\322\227\244\376\322\162\201\103\007\234 -\351\021\076\365\213\032\131\175\037\150\130\335\004\000\054\226 -\363\103\263\176\230\031\164\331\234\163\331\030\276\101\307\064 -\171\331\364\142\302\103\271\263\047\260\042\313\371\075\122\307 -\060\107\263\311\076\270\152\342\347\350\201\160\136\102\213\117 -\046\245\376\072\302\040\156\273\370\026\216\315\014\251\264\033 -\154\166\020\341\130\171\106\076\124\316\200\250\127\011\067\051 -\033\231\023\217\014\310\326\054\034\373\005\350\010\225\075\145 -\106\334\356\315\151\342\115\217\207\050\116\064\013\076\317\024 -\331\273\335\266\120\232\255\167\324\031\326\332\032\210\310\116 -\033\047\165\330\262\010\361\256\203\060\271\021\016\315\207\360 -\204\215\025\162\174\241\357\314\362\210\141\272\364\151\273\014 -\214\013\165\127\004\270\116\052\024\056\075\017\034\036\062\246 -\142\066\356\146\342\042\270\005\100\143\020\042\363\063\035\164 -\162\212\054\365\071\051\240\323\347\033\200\204\055\305\075\343 -\115\261\375\032\157\272\145\007\073\130\354\102\105\046\373\330 -\332\045\162\304\366\000\261\042\171\275\343\174\131\142\112\234 -\005\157\075\316\346\326\107\143\231\306\044\157\162\022\310\254 -\177\220\264\013\221\160\350\267\346\026\020\161\027\316\336\006 -\117\110\101\175\065\112\243\211\362\311\113\173\101\021\155\147 -\267\010\230\114\345\021\031\256\102\200\334\373\220\005\324\370 -\120\312\276\344\255\307\302\224\327\026\235\346\027\217\257\066 -\373\002\003\001\000\001\243\102\060\100\060\017\006\003\125\035 -\023\001\001\377\004\005\060\003\001\001\377\060\035\006\003\125 -\035\016\004\026\004\024\231\340\031\147\015\142\333\166\263\332 -\075\270\133\350\375\102\322\061\016\207\060\016\006\003\125\035 -\017\001\001\377\004\004\003\002\001\006\060\015\006\011\052\206 -\110\206\367\015\001\001\013\005\000\003\202\002\001\000\230\163 -\160\342\260\323\355\071\354\114\140\331\251\022\206\027\036\226 -\320\350\124\050\073\144\055\041\246\370\235\126\023\152\110\075 -\117\307\076\051\333\155\130\203\124\075\207\175\043\005\324\344 -\034\334\350\070\145\206\305\165\247\132\333\065\005\275\167\336 -\273\051\067\100\005\007\303\224\122\237\312\144\335\361\033\053 -\334\106\012\020\002\061\375\112\150\015\007\144\220\346\036\365 -\052\241\250\273\074\135\371\243\010\013\021\014\361\077\055\020 -\224\157\376\342\064\207\203\326\317\345\033\065\155\322\003\341 -\260\015\250\240\252\106\047\202\066\247\025\266\010\246\102\124 -\127\266\231\132\342\013\171\220\327\127\022\121\065\031\210\101 -\150\045\324\067\027\204\025\373\001\162\334\225\336\122\046\040 -\230\046\342\166\365\047\157\372\000\073\112\141\331\015\313\121 -\223\052\375\026\006\226\247\043\232\043\110\376\121\275\266\304 -\260\261\124\316\336\154\101\255\026\147\176\333\375\070\315\271 -\070\116\262\301\140\313\235\027\337\130\236\172\142\262\046\217 -\164\225\233\344\133\035\322\017\335\230\034\233\131\271\043\323 -\061\240\246\377\070\335\317\040\117\351\130\126\072\147\303\321 -\366\231\231\235\272\066\266\200\057\210\107\117\206\277\104\072 -\200\344\067\034\246\272\352\227\230\021\320\204\142\107\144\036 -\252\356\100\277\064\261\234\217\116\341\362\222\117\037\216\363 -\236\227\336\363\246\171\152\211\161\117\113\047\027\110\376\354 -\364\120\017\117\111\175\314\105\343\275\172\100\305\101\334\141 -\126\047\006\151\345\162\101\201\323\266\001\211\240\057\072\162 -\171\376\072\060\277\101\354\307\142\076\221\113\307\331\061\166 -\102\371\367\074\143\354\046\214\163\014\175\032\035\352\250\174 -\207\250\302\047\174\341\063\101\017\317\317\374\000\240\042\200 -\236\112\247\157\000\260\101\105\267\042\312\150\110\305\102\242 -\256\335\035\362\340\156\116\005\130\261\300\220\026\052\244\075 -\020\100\276\217\142\143\203\251\234\202\175\055\002\351\203\060 -\174\313\047\311\375\036\146\000\260\056\323\041\057\216\063\026 -\154\230\355\020\250\007\326\314\223\317\333\321\151\034\344\312 -\311\340\266\234\351\316\161\161\336\154\077\026\244\171 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "Trustwave Global Certification Authority" -# Issuer: CN=Trustwave Global Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Serial Number:05:f7:0e:86:da:49:f3:46:35:2e:ba:b2 -# Subject: CN=Trustwave Global Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Not Valid Before: Wed Aug 23 19:34:12 2017 -# Not Valid After : Sat Aug 23 19:34:12 2042 -# Fingerprint (SHA-256): 97:55:20:15:F5:DD:FC:3C:87:88:C0:06:94:45:55:40:88:94:45:00:84:F1:00:86:70:86:BC:1A:2B:B5:8D:C8 -# Fingerprint (SHA1): 2F:8F:36:4F:E1:58:97:44:21:59:87:A5:2A:9A:D0:69:95:26:7F:B5 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Trustwave Global Certification Authority" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\057\217\066\117\341\130\227\104\041\131\207\245\052\232\320\151 -\225\046\177\265 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\370\034\030\055\057\272\137\155\241\154\274\307\253\221\307\016 -END -CKA_ISSUER MULTILINE_OCTAL -\060\201\210\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\014\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\014\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\014\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\061\060\057\006\003\125\004 -\003\014\050\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\103\145\162\164\151\146\151\143\141\164\151\157 -\156\040\101\165\164\150\157\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\014\005\367\016\206\332\111\363\106\065\056\272\262 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "Trustwave Global ECC P256 Certification Authority" -# -# Issuer: CN=Trustwave Global ECC P256 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Serial Number:0d:6a:5f:08:3f:28:5c:3e:51:95:df:5d -# Subject: CN=Trustwave Global ECC P256 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Not Valid Before: Wed Aug 23 19:35:10 2017 -# Not Valid After : Sat Aug 23 19:35:10 2042 -# Fingerprint (SHA-256): 94:5B:BC:82:5E:A5:54:F4:89:D1:FD:51:A7:3D:DF:2E:A6:24:AC:70:19:A0:52:05:22:5C:22:A7:8C:CF:A8:B4 -# Fingerprint (SHA1): B4:90:82:DD:45:0C:BE:8B:5B:B1:66:D3:E2:A4:08:26:CD:ED:42:CF -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Trustwave Global ECC P256 Certification Authority" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\201\221\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\023\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\023\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\023\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\072\060\070\006\003\125\004 -\003\023\061\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\105\103\103\040\120\062\065\066\040\103\145\162 -\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157 -\162\151\164\171 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\201\221\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\023\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\023\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\023\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\072\060\070\006\003\125\004 -\003\023\061\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\105\103\103\040\120\062\065\066\040\103\145\162 -\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157 -\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\014\015\152\137\010\077\050\134\076\121\225\337\135 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\002\140\060\202\002\007\240\003\002\001\002\002\014\015 -\152\137\010\077\050\134\076\121\225\337\135\060\012\006\010\052 -\206\110\316\075\004\003\002\060\201\221\061\013\060\011\006\003 -\125\004\006\023\002\125\123\061\021\060\017\006\003\125\004\010 -\023\010\111\154\154\151\156\157\151\163\061\020\060\016\006\003 -\125\004\007\023\007\103\150\151\143\141\147\157\061\041\060\037 -\006\003\125\004\012\023\030\124\162\165\163\164\167\141\166\145 -\040\110\157\154\144\151\156\147\163\054\040\111\156\143\056\061 -\072\060\070\006\003\125\004\003\023\061\124\162\165\163\164\167 -\141\166\145\040\107\154\157\142\141\154\040\105\103\103\040\120 -\062\065\066\040\103\145\162\164\151\146\151\143\141\164\151\157 -\156\040\101\165\164\150\157\162\151\164\171\060\036\027\015\061 -\067\060\070\062\063\061\071\063\065\061\060\132\027\015\064\062 -\060\070\062\063\061\071\063\065\061\060\132\060\201\221\061\013 -\060\011\006\003\125\004\006\023\002\125\123\061\021\060\017\006 -\003\125\004\010\023\010\111\154\154\151\156\157\151\163\061\020 -\060\016\006\003\125\004\007\023\007\103\150\151\143\141\147\157 -\061\041\060\037\006\003\125\004\012\023\030\124\162\165\163\164 -\167\141\166\145\040\110\157\154\144\151\156\147\163\054\040\111 -\156\143\056\061\072\060\070\006\003\125\004\003\023\061\124\162 -\165\163\164\167\141\166\145\040\107\154\157\142\141\154\040\105 -\103\103\040\120\062\065\066\040\103\145\162\164\151\146\151\143 -\141\164\151\157\156\040\101\165\164\150\157\162\151\164\171\060 -\131\060\023\006\007\052\206\110\316\075\002\001\006\010\052\206 -\110\316\075\003\001\007\003\102\000\004\176\373\154\346\043\343 -\163\062\010\312\140\346\123\234\272\164\215\030\260\170\220\122 -\200\335\070\300\112\035\321\250\314\223\244\227\006\070\312\015 -\025\142\306\216\001\052\145\235\252\337\064\221\056\201\301\344 -\063\222\061\304\375\011\072\246\077\255\243\103\060\101\060\017 -\006\003\125\035\023\001\001\377\004\005\060\003\001\001\377\060 -\017\006\003\125\035\017\001\001\377\004\005\003\003\007\006\000 -\060\035\006\003\125\035\016\004\026\004\024\243\101\006\254\220 -\155\321\112\353\165\245\112\020\231\263\261\241\213\112\367\060 -\012\006\010\052\206\110\316\075\004\003\002\003\107\000\060\104 -\002\040\007\346\124\332\016\240\132\262\256\021\237\207\305\266 -\377\151\336\045\276\370\240\267\010\363\104\316\052\337\010\041 -\014\067\002\040\055\046\003\240\005\275\153\321\366\134\370\145 -\314\206\155\263\234\064\110\143\204\011\305\215\167\032\342\314 -\234\341\164\173 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "Trustwave Global ECC P256 Certification Authority" -# Issuer: CN=Trustwave Global ECC P256 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Serial Number:0d:6a:5f:08:3f:28:5c:3e:51:95:df:5d -# Subject: CN=Trustwave Global ECC P256 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Not Valid Before: Wed Aug 23 19:35:10 2017 -# Not Valid After : Sat Aug 23 19:35:10 2042 -# Fingerprint (SHA-256): 94:5B:BC:82:5E:A5:54:F4:89:D1:FD:51:A7:3D:DF:2E:A6:24:AC:70:19:A0:52:05:22:5C:22:A7:8C:CF:A8:B4 -# Fingerprint (SHA1): B4:90:82:DD:45:0C:BE:8B:5B:B1:66:D3:E2:A4:08:26:CD:ED:42:CF -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Trustwave Global ECC P256 Certification Authority" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\264\220\202\335\105\014\276\213\133\261\146\323\342\244\010\046 -\315\355\102\317 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\133\104\343\215\135\066\206\046\350\015\005\322\131\247\203\124 -END -CKA_ISSUER MULTILINE_OCTAL -\060\201\221\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\023\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\023\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\023\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\072\060\070\006\003\125\004 -\003\023\061\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\105\103\103\040\120\062\065\066\040\103\145\162 -\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157 -\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\014\015\152\137\010\077\050\134\076\121\225\337\135 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - -# -# Certificate "Trustwave Global ECC P384 Certification Authority" -# -# Issuer: CN=Trustwave Global ECC P384 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Serial Number:08:bd:85:97:6c:99:27:a4:80:68:47:3b -# Subject: CN=Trustwave Global ECC P384 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Not Valid Before: Wed Aug 23 19:36:43 2017 -# Not Valid After : Sat Aug 23 19:36:43 2042 -# Fingerprint (SHA-256): 55:90:38:59:C8:C0:C3:EB:B8:75:9E:CE:4E:25:57:22:5F:F5:75:8B:BD:38:EB:D4:82:76:60:1E:1B:D5:80:97 -# Fingerprint (SHA1): E7:F3:A3:C8:CF:6F:C3:04:2E:6D:0E:67:32:C5:9E:68:95:0D:5E:D2 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Trustwave Global ECC P384 Certification Authority" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\201\221\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\023\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\023\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\023\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\072\060\070\006\003\125\004 -\003\023\061\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\105\103\103\040\120\063\070\064\040\103\145\162 -\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157 -\162\151\164\171 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\201\221\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\023\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\023\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\023\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\072\060\070\006\003\125\004 -\003\023\061\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\105\103\103\040\120\063\070\064\040\103\145\162 -\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157 -\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\014\010\275\205\227\154\231\047\244\200\150\107\073 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\002\235\060\202\002\044\240\003\002\001\002\002\014\010 -\275\205\227\154\231\047\244\200\150\107\073\060\012\006\010\052 -\206\110\316\075\004\003\003\060\201\221\061\013\060\011\006\003 -\125\004\006\023\002\125\123\061\021\060\017\006\003\125\004\010 -\023\010\111\154\154\151\156\157\151\163\061\020\060\016\006\003 -\125\004\007\023\007\103\150\151\143\141\147\157\061\041\060\037 -\006\003\125\004\012\023\030\124\162\165\163\164\167\141\166\145 -\040\110\157\154\144\151\156\147\163\054\040\111\156\143\056\061 -\072\060\070\006\003\125\004\003\023\061\124\162\165\163\164\167 -\141\166\145\040\107\154\157\142\141\154\040\105\103\103\040\120 -\063\070\064\040\103\145\162\164\151\146\151\143\141\164\151\157 -\156\040\101\165\164\150\157\162\151\164\171\060\036\027\015\061 -\067\060\070\062\063\061\071\063\066\064\063\132\027\015\064\062 -\060\070\062\063\061\071\063\066\064\063\132\060\201\221\061\013 -\060\011\006\003\125\004\006\023\002\125\123\061\021\060\017\006 -\003\125\004\010\023\010\111\154\154\151\156\157\151\163\061\020 -\060\016\006\003\125\004\007\023\007\103\150\151\143\141\147\157 -\061\041\060\037\006\003\125\004\012\023\030\124\162\165\163\164 -\167\141\166\145\040\110\157\154\144\151\156\147\163\054\040\111 -\156\143\056\061\072\060\070\006\003\125\004\003\023\061\124\162 -\165\163\164\167\141\166\145\040\107\154\157\142\141\154\040\105 -\103\103\040\120\063\070\064\040\103\145\162\164\151\146\151\143 -\141\164\151\157\156\040\101\165\164\150\157\162\151\164\171\060 -\166\060\020\006\007\052\206\110\316\075\002\001\006\005\053\201 -\004\000\042\003\142\000\004\153\332\015\165\065\010\061\107\005 -\256\105\231\125\361\021\023\056\112\370\020\061\043\243\176\203 -\323\177\050\010\072\046\032\072\317\227\202\037\200\267\047\011 -\217\321\216\060\304\012\233\016\254\130\004\253\367\066\175\224 -\043\244\233\012\212\213\253\353\375\071\045\146\361\136\376\214 -\256\215\101\171\235\011\140\316\050\251\323\212\155\363\326\105 -\324\362\230\204\070\145\240\243\103\060\101\060\017\006\003\125 -\035\023\001\001\377\004\005\060\003\001\001\377\060\017\006\003 -\125\035\017\001\001\377\004\005\003\003\007\006\000\060\035\006 -\003\125\035\016\004\026\004\024\125\251\204\211\322\301\062\275 -\030\313\154\246\007\116\310\347\235\276\202\220\060\012\006\010 -\052\206\110\316\075\004\003\003\003\147\000\060\144\002\060\067 -\001\222\227\105\022\176\240\363\076\255\031\072\162\335\364\120 -\223\003\022\276\104\322\117\101\244\214\234\235\037\243\366\302 -\222\347\110\024\376\116\233\245\221\127\256\306\067\162\273\002 -\060\147\045\012\261\014\136\356\251\143\222\157\345\220\013\376 -\146\042\312\107\375\212\061\367\203\376\172\277\020\276\030\053 -\036\217\366\051\036\224\131\357\216\041\067\313\121\230\245\156 -\113 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "Trustwave Global ECC P384 Certification Authority" -# Issuer: CN=Trustwave Global ECC P384 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Serial Number:08:bd:85:97:6c:99:27:a4:80:68:47:3b -# Subject: CN=Trustwave Global ECC P384 Certification Authority,O="Trustwave Holdings, Inc.",L=Chicago,ST=Illinois,C=US -# Not Valid Before: Wed Aug 23 19:36:43 2017 -# Not Valid After : Sat Aug 23 19:36:43 2042 -# Fingerprint (SHA-256): 55:90:38:59:C8:C0:C3:EB:B8:75:9E:CE:4E:25:57:22:5F:F5:75:8B:BD:38:EB:D4:82:76:60:1E:1B:D5:80:97 -# Fingerprint (SHA1): E7:F3:A3:C8:CF:6F:C3:04:2E:6D:0E:67:32:C5:9E:68:95:0D:5E:D2 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "Trustwave Global ECC P384 Certification Authority" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\347\363\243\310\317\157\303\004\056\155\016\147\062\305\236\150 -\225\015\136\322 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\352\317\140\304\073\271\025\051\100\241\227\355\170\047\223\326 -END -CKA_ISSUER MULTILINE_OCTAL -\060\201\221\061\013\060\011\006\003\125\004\006\023\002\125\123 -\061\021\060\017\006\003\125\004\010\023\010\111\154\154\151\156 -\157\151\163\061\020\060\016\006\003\125\004\007\023\007\103\150 -\151\143\141\147\157\061\041\060\037\006\003\125\004\012\023\030 -\124\162\165\163\164\167\141\166\145\040\110\157\154\144\151\156 -\147\163\054\040\111\156\143\056\061\072\060\070\006\003\125\004 -\003\023\061\124\162\165\163\164\167\141\166\145\040\107\154\157 -\142\141\154\040\105\103\103\040\120\063\070\064\040\103\145\162 -\164\151\146\151\143\141\164\151\157\156\040\101\165\164\150\157 -\162\151\164\171 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\014\010\275\205\227\154\231\047\244\200\150\107\073 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - # # Certificate "NAVER Global Root Certification Authority" # @@ -16316,176 +14628,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE -# -# Certificate "GLOBALTRUST 2020" -# -# Issuer: CN=GLOBALTRUST 2020,O=e-commerce monitoring GmbH,C=AT -# Serial Number:5a:4b:bd:5a:fb:4f:8a:5b:fa:65:e5 -# Subject: CN=GLOBALTRUST 2020,O=e-commerce monitoring GmbH,C=AT -# Not Valid Before: Mon Feb 10 00:00:00 2020 -# Not Valid After : Sun Jun 10 00:00:00 2040 -# Fingerprint (SHA-256): 9A:29:6A:51:82:D1:D4:51:A2:E3:7F:43:9B:74:DA:AF:A2:67:52:33:29:F9:0F:9A:0D:20:07:C3:34:E2:3C:9A -# Fingerprint (SHA1): D0:67:C1:13:51:01:0C:AA:D0:C7:6A:65:37:31:16:26:4F:53:71:A2 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "GLOBALTRUST 2020" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\115\061\013\060\011\006\003\125\004\006\023\002\101\124\061 -\043\060\041\006\003\125\004\012\023\032\145\055\143\157\155\155 -\145\162\143\145\040\155\157\156\151\164\157\162\151\156\147\040 -\107\155\142\110\061\031\060\027\006\003\125\004\003\023\020\107 -\114\117\102\101\114\124\122\125\123\124\040\062\060\062\060 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\115\061\013\060\011\006\003\125\004\006\023\002\101\124\061 -\043\060\041\006\003\125\004\012\023\032\145\055\143\157\155\155 -\145\162\143\145\040\155\157\156\151\164\157\162\151\156\147\040 -\107\155\142\110\061\031\060\027\006\003\125\004\003\023\020\107 -\114\117\102\101\114\124\122\125\123\124\040\062\060\062\060 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\013\132\113\275\132\373\117\212\133\372\145\345 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\005\202\060\202\003\152\240\003\002\001\002\002\013\132 -\113\275\132\373\117\212\133\372\145\345\060\015\006\011\052\206 -\110\206\367\015\001\001\013\005\000\060\115\061\013\060\011\006 -\003\125\004\006\023\002\101\124\061\043\060\041\006\003\125\004 -\012\023\032\145\055\143\157\155\155\145\162\143\145\040\155\157 -\156\151\164\157\162\151\156\147\040\107\155\142\110\061\031\060 -\027\006\003\125\004\003\023\020\107\114\117\102\101\114\124\122 -\125\123\124\040\062\060\062\060\060\036\027\015\062\060\060\062 -\061\060\060\060\060\060\060\060\132\027\015\064\060\060\066\061 -\060\060\060\060\060\060\060\132\060\115\061\013\060\011\006\003 -\125\004\006\023\002\101\124\061\043\060\041\006\003\125\004\012 -\023\032\145\055\143\157\155\155\145\162\143\145\040\155\157\156 -\151\164\157\162\151\156\147\040\107\155\142\110\061\031\060\027 -\006\003\125\004\003\023\020\107\114\117\102\101\114\124\122\125 -\123\124\040\062\060\062\060\060\202\002\042\060\015\006\011\052 -\206\110\206\367\015\001\001\001\005\000\003\202\002\017\000\060 -\202\002\012\002\202\002\001\000\256\056\126\255\033\034\357\366 -\225\217\240\167\033\053\323\143\217\204\115\105\242\017\237\133 -\105\253\131\173\121\064\371\354\213\212\170\305\335\153\257\275 -\304\337\223\105\036\277\221\070\013\256\016\026\347\101\163\370 -\333\273\321\270\121\340\313\203\073\163\070\156\167\212\017\131 -\143\046\315\247\052\316\124\373\270\342\300\174\107\316\140\174 -\077\262\163\362\300\031\266\212\222\207\065\015\220\050\242\344 -\025\004\143\076\272\257\356\174\136\314\246\213\120\262\070\367 -\101\143\312\316\377\151\217\150\016\225\066\345\314\271\214\011 -\312\113\335\061\220\226\310\314\037\375\126\226\064\333\216\034 -\352\054\276\205\056\143\335\252\251\225\323\375\051\225\023\360 -\310\230\223\331\055\026\107\220\021\203\242\072\042\242\050\127 -\242\353\376\300\214\050\240\246\175\347\052\102\073\202\200\143 -\245\143\037\031\314\174\262\146\250\302\323\155\067\157\342\176 -\006\121\331\105\204\037\022\316\044\122\144\205\013\110\200\116 -\207\261\042\042\060\252\353\256\276\340\002\340\100\350\260\102 -\200\003\121\252\264\176\252\104\327\103\141\363\242\153\026\211 -\111\244\243\244\053\212\002\304\170\364\150\212\301\344\172\066 -\261\157\033\226\033\167\111\215\324\311\006\162\217\317\123\343 -\334\027\205\040\112\334\230\047\323\221\046\053\107\036\151\007 -\257\336\242\344\344\324\153\013\263\136\174\324\044\200\107\051 -\151\073\156\350\254\375\100\353\330\355\161\161\053\362\350\130 -\035\353\101\227\042\305\037\324\071\320\047\217\207\343\030\364 -\340\251\106\015\365\164\072\202\056\320\156\054\221\243\061\134 -\073\106\352\173\004\020\126\136\200\035\365\245\145\350\202\374 -\342\007\214\142\105\365\040\336\106\160\206\241\274\223\323\036 -\164\246\154\260\054\367\003\014\210\014\313\324\162\123\206\274 -\140\106\363\230\152\302\361\277\103\371\160\040\167\312\067\101 -\171\125\122\143\215\133\022\237\305\150\304\210\235\254\362\060 -\253\267\243\061\227\147\255\217\027\017\154\307\163\355\044\224 -\153\310\203\232\320\232\067\111\004\253\261\026\310\154\111\111 -\055\253\241\320\214\222\362\101\112\171\041\045\333\143\327\266 -\234\247\176\102\151\373\072\143\002\003\001\000\001\243\143\060 -\141\060\017\006\003\125\035\023\001\001\377\004\005\060\003\001 -\001\377\060\016\006\003\125\035\017\001\001\377\004\004\003\002 -\001\006\060\035\006\003\125\035\016\004\026\004\024\334\056\037 -\321\141\067\171\344\253\325\325\263\022\161\150\075\152\150\234 -\042\060\037\006\003\125\035\043\004\030\060\026\200\024\334\056 -\037\321\141\067\171\344\253\325\325\263\022\161\150\075\152\150 -\234\042\060\015\006\011\052\206\110\206\367\015\001\001\013\005 -\000\003\202\002\001\000\221\360\102\002\150\100\356\303\150\300 -\124\057\337\354\142\303\303\236\212\240\061\050\252\203\216\244 -\126\226\022\020\206\126\272\227\162\322\124\060\174\255\031\325 -\035\150\157\373\024\102\330\215\016\363\265\321\245\343\002\102 -\136\334\350\106\130\007\065\002\060\340\274\164\112\301\103\052 -\377\333\032\320\260\257\154\303\375\313\263\365\177\155\003\056 -\131\126\235\055\055\065\214\262\326\103\027\054\222\012\313\135 -\350\214\017\113\160\103\320\202\377\250\314\277\244\224\300\276 -\207\275\212\343\223\173\306\217\233\026\235\047\145\274\172\305 -\102\202\154\134\007\320\251\301\210\140\104\351\230\205\026\137 -\370\217\312\001\020\316\045\303\371\140\033\240\305\227\303\323 -\054\210\061\242\275\060\354\320\320\300\022\361\301\071\343\345 -\365\370\326\112\335\064\315\373\157\301\117\343\000\213\126\342 -\222\367\050\262\102\167\162\043\147\307\077\021\025\262\304\003 -\005\276\273\021\173\012\277\250\156\347\377\130\103\317\233\147 -\240\200\007\266\035\312\255\155\352\101\021\176\055\164\223\373 -\302\274\276\121\104\305\357\150\045\047\200\343\310\240\324\022 -\354\331\245\067\035\067\174\264\221\312\332\324\261\226\201\357 -\150\134\166\020\111\257\176\245\067\200\261\034\122\275\063\201 -\114\217\371\335\145\331\024\315\212\045\130\364\342\305\203\245 -\011\220\324\154\024\143\265\100\337\353\300\374\304\130\176\015 -\024\026\207\124\047\156\126\344\160\204\270\154\062\022\176\202 -\061\103\276\327\335\174\241\255\256\326\253\040\022\357\012\303 -\020\214\111\226\065\334\013\165\136\261\117\325\117\064\016\021 -\040\007\165\103\105\351\243\021\332\254\243\231\302\266\171\047 -\342\271\357\310\342\366\065\051\172\164\372\305\177\202\005\142 -\246\012\352\150\262\171\107\006\156\362\127\250\025\063\306\367 -\170\112\075\102\173\153\176\376\367\106\352\321\353\216\357\210 -\150\133\350\301\331\161\176\375\144\357\377\147\107\210\130\045 -\057\076\206\007\275\373\250\345\202\250\254\245\323\151\103\315 -\061\210\111\204\123\222\300\261\071\033\071\203\001\060\304\362 -\251\372\320\003\275\162\067\140\126\037\066\174\275\071\221\365 -\155\015\277\173\327\222 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -# For Server Distrust After: Sun Jun 30 00:00:00 2024 -CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL -\062\064\060\066\063\060\060\060\060\060\060\060\132 -END -# For Email Distrust After: Sun Jun 30 00:00:00 2024 -CKA_NSS_EMAIL_DISTRUST_AFTER MULTILINE_OCTAL -\062\064\060\066\063\060\060\060\060\060\060\060\132 -END - -# Trust for "GLOBALTRUST 2020" -# Issuer: CN=GLOBALTRUST 2020,O=e-commerce monitoring GmbH,C=AT -# Serial Number:5a:4b:bd:5a:fb:4f:8a:5b:fa:65:e5 -# Subject: CN=GLOBALTRUST 2020,O=e-commerce monitoring GmbH,C=AT -# Not Valid Before: Mon Feb 10 00:00:00 2020 -# Not Valid After : Sun Jun 10 00:00:00 2040 -# Fingerprint (SHA-256): 9A:29:6A:51:82:D1:D4:51:A2:E3:7F:43:9B:74:DA:AF:A2:67:52:33:29:F9:0F:9A:0D:20:07:C3:34:E2:3C:9A -# Fingerprint (SHA1): D0:67:C1:13:51:01:0C:AA:D0:C7:6A:65:37:31:16:26:4F:53:71:A2 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "GLOBALTRUST 2020" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\320\147\301\023\121\001\014\252\320\307\152\145\067\061\026\046 -\117\123\161\242 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\212\307\157\313\155\343\314\242\361\174\203\372\016\170\327\350 -END -CKA_ISSUER MULTILINE_OCTAL -\060\115\061\013\060\011\006\003\125\004\006\023\002\101\124\061 -\043\060\041\006\003\125\004\012\023\032\145\055\143\157\155\155 -\145\162\143\145\040\155\157\156\151\164\157\162\151\156\147\040 -\107\155\142\110\061\031\060\027\006\003\125\004\003\023\020\107 -\114\117\102\101\114\124\122\125\123\124\040\062\060\062\060 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\013\132\113\275\132\373\117\212\133\372\145\345 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - # # Certificate "ANF Secure Server Root CA" # @@ -18579,7 +16721,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\015\002\003\345\176\365\077\223\375\245\011\041\262\246 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -18740,7 +16882,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\015\002\003\345\223\157\061\260\023\111\210\153\242\027 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -18900,7 +17042,7 @@ END CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\015\002\003\345\256\305\215\004\045\032\253\021\045\252 END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -19009,7 +17151,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\015\002\003\345\270\202\353\040\370\045\047\155\075\146 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -19117,7 +17259,7 @@ CKA_SERIAL_NUMBER MULTILINE_OCTAL \002\015\002\003\345\300\150\357\143\032\234\162\220\120\122 END CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR +CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE @@ -24070,129 +22212,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE -# -# Certificate "FIRMAPROFESIONAL CA ROOT-A WEB" -# -# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB,OID.2.5.4.97=VATES-A62634068,O=Firmaprofesional SA,C=ES -# Serial Number:31:97:21:ed:af:89:42:7f:35:41:87:a1:67:56:4c:6d -# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB,OID.2.5.4.97=VATES-A62634068,O=Firmaprofesional SA,C=ES -# Not Valid Before: Wed Apr 06 09:01:36 2022 -# Not Valid After : Sun Mar 31 09:01:36 2047 -# Fingerprint (SHA-256): BE:F2:56:DA:F2:6E:9C:69:BD:EC:16:02:35:97:98:F3:CA:F7:18:21:A0:3E:01:82:57:C5:3C:65:61:7F:3D:4A -# Fingerprint (SHA1): A8:31:11:74:A6:14:15:0D:CA:77:DD:0E:E4:0C:5D:58:FC:A0:72:A5 -CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "FIRMAPROFESIONAL CA ROOT-A WEB" -CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509 -CKA_SUBJECT MULTILINE_OCTAL -\060\156\061\013\060\011\006\003\125\004\006\023\002\105\123\061 -\034\060\032\006\003\125\004\012\014\023\106\151\162\155\141\160 -\162\157\146\145\163\151\157\156\141\154\040\123\101\061\030\060 -\026\006\003\125\004\141\014\017\126\101\124\105\123\055\101\066 -\062\066\063\064\060\066\070\061\047\060\045\006\003\125\004\003 -\014\036\106\111\122\115\101\120\122\117\106\105\123\111\117\116 -\101\114\040\103\101\040\122\117\117\124\055\101\040\127\105\102 -END -CKA_ID UTF8 "0" -CKA_ISSUER MULTILINE_OCTAL -\060\156\061\013\060\011\006\003\125\004\006\023\002\105\123\061 -\034\060\032\006\003\125\004\012\014\023\106\151\162\155\141\160 -\162\157\146\145\163\151\157\156\141\154\040\123\101\061\030\060 -\026\006\003\125\004\141\014\017\126\101\124\105\123\055\101\066 -\062\066\063\064\060\066\070\061\047\060\045\006\003\125\004\003 -\014\036\106\111\122\115\101\120\122\117\106\105\123\111\117\116 -\101\114\040\103\101\040\122\117\117\124\055\101\040\127\105\102 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\061\227\041\355\257\211\102\177\065\101\207\241\147\126 -\114\155 -END -CKA_VALUE MULTILINE_OCTAL -\060\202\002\172\060\202\002\000\240\003\002\001\002\002\020\061 -\227\041\355\257\211\102\177\065\101\207\241\147\126\114\155\060 -\012\006\010\052\206\110\316\075\004\003\003\060\156\061\013\060 -\011\006\003\125\004\006\023\002\105\123\061\034\060\032\006\003 -\125\004\012\014\023\106\151\162\155\141\160\162\157\146\145\163 -\151\157\156\141\154\040\123\101\061\030\060\026\006\003\125\004 -\141\014\017\126\101\124\105\123\055\101\066\062\066\063\064\060 -\066\070\061\047\060\045\006\003\125\004\003\014\036\106\111\122 -\115\101\120\122\117\106\105\123\111\117\116\101\114\040\103\101 -\040\122\117\117\124\055\101\040\127\105\102\060\036\027\015\062 -\062\060\064\060\066\060\071\060\061\063\066\132\027\015\064\067 -\060\063\063\061\060\071\060\061\063\066\132\060\156\061\013\060 -\011\006\003\125\004\006\023\002\105\123\061\034\060\032\006\003 -\125\004\012\014\023\106\151\162\155\141\160\162\157\146\145\163 -\151\157\156\141\154\040\123\101\061\030\060\026\006\003\125\004 -\141\014\017\126\101\124\105\123\055\101\066\062\066\063\064\060 -\066\070\061\047\060\045\006\003\125\004\003\014\036\106\111\122 -\115\101\120\122\117\106\105\123\111\117\116\101\114\040\103\101 -\040\122\117\117\124\055\101\040\127\105\102\060\166\060\020\006 -\007\052\206\110\316\075\002\001\006\005\053\201\004\000\042\003 -\142\000\004\107\123\352\054\021\244\167\307\052\352\363\326\137 -\173\323\004\221\134\372\210\306\042\271\203\020\142\167\204\063 -\055\351\003\210\324\340\063\367\355\167\054\112\140\352\344\157 -\255\155\264\370\114\212\244\344\037\312\352\117\070\112\056\202 -\163\053\307\146\233\012\214\100\234\174\212\366\362\071\140\262 -\336\313\354\270\344\157\352\233\135\267\123\220\030\062\125\305 -\040\267\224\243\143\060\141\060\017\006\003\125\035\023\001\001 -\377\004\005\060\003\001\001\377\060\037\006\003\125\035\043\004 -\030\060\026\200\024\223\341\103\143\134\074\235\326\047\363\122 -\354\027\262\251\257\054\367\166\370\060\035\006\003\125\035\016 -\004\026\004\024\223\341\103\143\134\074\235\326\047\363\122\354 -\027\262\251\257\054\367\166\370\060\016\006\003\125\035\017\001 -\001\377\004\004\003\002\001\006\060\012\006\010\052\206\110\316 -\075\004\003\003\003\150\000\060\145\002\060\035\174\244\173\303 -\211\165\063\341\073\251\105\277\106\351\351\241\335\311\042\026 -\267\107\021\013\330\232\272\361\310\013\160\120\123\002\221\160 -\205\131\251\036\244\346\352\043\061\240\000\002\061\000\375\342 -\370\263\257\026\271\036\163\304\226\343\301\060\031\330\176\346 -\303\227\336\034\117\270\211\057\063\353\110\017\031\367\207\106 -\135\046\220\245\205\305\271\172\224\076\207\250\275\000 -END -CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE -CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE -CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE - -# Trust for "FIRMAPROFESIONAL CA ROOT-A WEB" -# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB,OID.2.5.4.97=VATES-A62634068,O=Firmaprofesional SA,C=ES -# Serial Number:31:97:21:ed:af:89:42:7f:35:41:87:a1:67:56:4c:6d -# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB,OID.2.5.4.97=VATES-A62634068,O=Firmaprofesional SA,C=ES -# Not Valid Before: Wed Apr 06 09:01:36 2022 -# Not Valid After : Sun Mar 31 09:01:36 2047 -# Fingerprint (SHA-256): BE:F2:56:DA:F2:6E:9C:69:BD:EC:16:02:35:97:98:F3:CA:F7:18:21:A0:3E:01:82:57:C5:3C:65:61:7F:3D:4A -# Fingerprint (SHA1): A8:31:11:74:A6:14:15:0D:CA:77:DD:0E:E4:0C:5D:58:FC:A0:72:A5 -CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST -CKA_TOKEN CK_BBOOL CK_TRUE -CKA_PRIVATE CK_BBOOL CK_FALSE -CKA_MODIFIABLE CK_BBOOL CK_FALSE -CKA_LABEL UTF8 "FIRMAPROFESIONAL CA ROOT-A WEB" -CKA_CERT_SHA1_HASH MULTILINE_OCTAL -\250\061\021\164\246\024\025\015\312\167\335\016\344\014\135\130 -\374\240\162\245 -END -CKA_CERT_MD5_HASH MULTILINE_OCTAL -\202\262\255\105\000\202\260\146\143\370\137\303\147\116\316\243 -END -CKA_ISSUER MULTILINE_OCTAL -\060\156\061\013\060\011\006\003\125\004\006\023\002\105\123\061 -\034\060\032\006\003\125\004\012\014\023\106\151\162\155\141\160 -\162\157\146\145\163\151\157\156\141\154\040\123\101\061\030\060 -\026\006\003\125\004\141\014\017\126\101\124\105\123\055\101\066 -\062\066\063\064\060\066\070\061\047\060\045\006\003\125\004\003 -\014\036\106\111\122\115\101\120\122\117\106\105\123\111\117\116 -\101\114\040\103\101\040\122\117\117\124\055\101\040\127\105\102 -END -CKA_SERIAL_NUMBER MULTILINE_OCTAL -\002\020\061\227\041\355\257\211\102\177\065\101\207\241\147\126 -\114\155 -END -CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR -CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST -CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE - # # Certificate "TWCA CYBER Root CA" # From 460c35085f6c47c11134b75ef8ac92a50bda77f4 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 26 May 2026 12:20:15 +0200 Subject: [PATCH 43/89] debugger,test: deflake resume failure test and add debug logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On slow CI, the outer Debugger.resume can be picked up in the same drain pass as the Debugger.evaluateOnCallFrame, while V8 still considers the context paused. In this case both resume calls may succeed and the process can continue running from the setInterval until the timeout. Accept both probe failure and timeout as valid to accommodate this flakiness. This patch also adds more debug logs to the probe mode to show more information in case it flakes again in the CI Signed-off-by: Joyee Cheung PR-URL: https://github.com/nodejs/node/pull/63524 Fixes: https://github.com/nodejs/node/issues/63505 Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Chemi Atlow Reviewed-By: Chengzhong Wu --- lib/internal/debugger/inspect_probe.js | 20 ++++++++++- test/common/debugger-probe.js | 2 +- .../test-debugger-probe-failure-resume.js | 35 +++++++++++++++---- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/lib/internal/debugger/inspect_probe.js b/lib/internal/debugger/inspect_probe.js index 14f7413fea8ef1..fc9f3056f52341 100644 --- a/lib/internal/debugger/inspect_probe.js +++ b/lib/internal/debugger/inspect_probe.js @@ -25,6 +25,7 @@ const { const { clearTimeout, setTimeout } = require('timers'); const { SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util'); +const debug = require('internal/util/debuglog').debuglog('inspect_probe'); const InspectClient = require('internal/debugger/inspect_client'); const { @@ -477,6 +478,7 @@ class ProbeInspectorSession { finish(exitCode, terminal) { if (this.finished) { return; } + debug('finish: exitCode=%d, terminal=%s', exitCode, terminal?.event); this.finished = true; if (this.timeout !== null) { clearTimeout(this.timeout); @@ -523,6 +525,8 @@ class ProbeInspectorSession { } onChildExit(code, signal) { + debug('child exit: code=%s signal=%s connected=%s started=%s finished=%s inFlight=%j', + code, signal, this.connected, this.started, this.finished, this.inFlight); // Pre-connect exits are deliberately silent: the target never reached // a state where probes could be set, so any report would be empty. if (!this.connected) { return; } @@ -543,6 +547,8 @@ class ProbeInspectorSession { } onClientClose() { + debug('client close: disconnectRequested=%s finished=%s inFlight=%j', + this.disconnectRequested, this.finished, this.inFlight); if (!this.connected) { return; } if (this.disconnectRequested) { return; } if (this.finished) { return; } @@ -664,13 +670,21 @@ class ProbeInspectorSession { async callCdp(method, params, probe = null) { if (this.finished) { throw kInspectorFailedSentinel; } this.inFlight = { __proto__: null, method, probe }; + debug('CDP -> %s%s', method, probe !== null ? `, probe=${probe.index}` : ''); try { const result = await this.client.callMethod(method, params); // A timeout or process exit can finish the report while the CDP request // is still outstanding. Ignore the late reply in that case. - if (this.finished) { throw kInspectorFailedSentinel; } + if (this.finished) { + debug('CDP <- %s discarded (already finished)', method); + throw kInspectorFailedSentinel; + } + debug('CDP <- %s (success)', method); return result; } catch (err) { + if (err !== kInspectorFailedSentinel) { // Already handled. + debug('CDP <- %s error: %s', method, err?.code); + } if (this.disconnectRequested) { // Only the in-flight evaluation gets attribution. Other rejections // under disconnect are downstream noise. @@ -718,6 +732,8 @@ class ProbeInspectorSession { // Records the first inspector-side terminal for the session, later callers are ignored. recordInspectorFailure({ reason, advice, cdpError, internalError }) { if (this.finished) { return; } + debug('recordInspectorFailure "%s": inFlight=%j, lastProbeIndex=%s, cdpError=%j', + reason, this.inFlight, this.lastProbeIndex, cdpError); const child = this.child; const exitedAbnormally = child !== null && (child.signalCode !== null || (child.exitCode !== null && child.exitCode !== 0)); @@ -785,6 +801,8 @@ class ProbeInspectorSession { startTimeout() { this.timeout = setTimeout(() => { + debug('timeout fired: finished=%s, inFlight=%j, lastProbeIndex=%s', + this.finished, this.inFlight, this.lastProbeIndex); if (this.finished) { return; } if (this.inFlight !== null) { const hasProbeAttribution = diff --git a/test/common/debugger-probe.js b/test/common/debugger-probe.js index bc5c206bb78926..c2ed825a26064b 100644 --- a/test/common/debugger-probe.js +++ b/test/common/debugger-probe.js @@ -52,7 +52,7 @@ function normalizeProbeReport(value) { } function assertProbeJson(output, expected) { - const normalized = JSON.parse(output); + const normalized = typeof output === 'string' ? JSON.parse(output) : output; const lastResult = normalized.results?.[normalized.results.length - 1]; if (isProbeSegvTeardown(lastResult)) { diff --git a/test/parallel/test-debugger-probe-failure-resume.js b/test/parallel/test-debugger-probe-failure-resume.js index e5578ec452e1e7..8305ea9c8004b5 100644 --- a/test/parallel/test-debugger-probe-failure-resume.js +++ b/test/parallel/test-debugger-probe-failure-resume.js @@ -1,5 +1,7 @@ // This tests that a probe expression resuming the target through its own -// inspector.Session surfaces as probe_failure. +// inspector.Session is surfaced as a probe-side failure. The terminal event +// can be either probe_failure or probe_timeout depending on a race in V8's +// nested pause-loop drain. 'use strict'; const common = require('../common'); @@ -21,11 +23,11 @@ spawnSyncAndExit(process.execPath, [ 'inspect', '--json', '--probe', `${fixture}:12`, '--expr', probes[0].expr, fixture, -], { cwd }, { +], { cwd, env: { ...process.env, NODE_DEBUG: 'inspect_probe' } }, { status: 1, signal: null, stdout(output) { - assertProbeJson(output, { + const expected = { v: 2, probes, results: [{ @@ -34,7 +36,14 @@ spawnSyncAndExit(process.execPath, [ hit: 1, location, result: { type: 'number', value: 1, description: '1' }, - }, { + }] + }; + + const actual = JSON.parse(output); + + const code = actual.results.at(-1)?.error?.code; + if (code === 'probe_failure') { + expected.results.push({ event: 'error', pending: [], error: { @@ -50,8 +59,22 @@ spawnSyncAndExit(process.execPath, [ protocolError: { message: 'Can only perform operation while paused.', code: -32000 }, }, }, - }], - }); + }); + } else if (code === 'probe_timeout') { + // On slow CI, the outer Debugger.resume can be picked up in the same drain pass as + // the Debugger.evaluateOnCallFrame, while V8 still considers the context paused. + // In this case both resume calls may succeed and the process can continue running from + // the setInterval until the timeout. + expected.results.push({ + event: 'timeout', + pending: [], + error: { + code: 'probe_timeout', + message: 'Timed out after 30000ms waiting for target completion' + }, + }); + } + assertProbeJson(actual, expected); }, trim: true, }); From 956e2a2b86a9e51aca6ac0471a3b5034f77b7bdf Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Tue, 26 May 2026 10:40:14 -0300 Subject: [PATCH 44/89] lib,permission: add permission.drop Signed-off-by: RafaelGSS PR-URL: https://github.com/nodejs/node/pull/62672 Refs: https://github.com/nodejs/node/issues/62223 Reviewed-By: Matteo Collina --- doc/api/permissions.md | 37 +++- doc/api/process.md | 60 +++++++ lib/internal/process/permission.js | 12 ++ lib/internal/process/pre_execution.js | 3 +- src/permission/addon_permission.cc | 6 + src/permission/addon_permission.h | 3 + src/permission/child_process_permission.cc | 6 + src/permission/child_process_permission.h | 3 + src/permission/ffi_permission.cc | 6 + src/permission/ffi_permission.h | 3 + src/permission/fs_permission.cc | 91 ++++++++++ src/permission/fs_permission.h | 9 + src/permission/inspector_permission.cc | 6 + src/permission/inspector_permission.h | 3 + src/permission/net_permission.cc | 6 + src/permission/net_permission.h | 3 + src/permission/permission.cc | 67 ++++++++ src/permission/permission.h | 4 + src/permission/permission_base.h | 3 + src/permission/wasi_permission.cc | 6 + src/permission/wasi_permission.h | 3 + src/permission/worker_permission.cc | 6 + src/permission/worker_permission.h | 3 + .../test-permission-drop-child-process.js | 38 +++++ ...est-permission-drop-diagnostics-channel.js | 34 ++++ test/parallel/test-permission-drop-errors.js | 45 +++++ test/parallel/test-permission-drop-ffi.js | 40 +++++ test/parallel/test-permission-drop-fs-all.js | 38 +++++ .../test-permission-drop-fs-granted-path.js | 158 ++++++++++++++++++ test/parallel/test-permission-drop-fs-read.js | 40 +++++ .../parallel/test-permission-drop-fs-scope.js | 37 ++++ .../test-permission-drop-fs-specific-path.js | 60 +++++++ .../parallel/test-permission-drop-fs-write.js | 31 ++++ test/parallel/test-permission-drop-net.js | 20 +++ .../test-permission-drop-not-enabled.js | 14 ++ test/parallel/test-permission-drop-worker.js | 26 +++ 36 files changed, 928 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-permission-drop-child-process.js create mode 100644 test/parallel/test-permission-drop-diagnostics-channel.js create mode 100644 test/parallel/test-permission-drop-errors.js create mode 100644 test/parallel/test-permission-drop-ffi.js create mode 100644 test/parallel/test-permission-drop-fs-all.js create mode 100644 test/parallel/test-permission-drop-fs-granted-path.js create mode 100644 test/parallel/test-permission-drop-fs-read.js create mode 100644 test/parallel/test-permission-drop-fs-scope.js create mode 100644 test/parallel/test-permission-drop-fs-specific-path.js create mode 100644 test/parallel/test-permission-drop-fs-write.js create mode 100644 test/parallel/test-permission-drop-net.js create mode 100644 test/parallel/test-permission-drop-not-enabled.js create mode 100644 test/parallel/test-permission-drop-worker.js diff --git a/doc/api/permissions.md b/doc/api/permissions.md index 5af6fbb398f53a..3f2a6411d3f678 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -78,7 +78,7 @@ flag. For WASI, use the [`--allow-wasi`][] flag. For FFI, use the When enabling the Permission Model through the [`--permission`][] flag a new property `permission` is added to the `process` object. -This property contains one function: +This property contains the following functions: ##### `permission.has(scope[, reference])` @@ -92,6 +92,41 @@ process.permission.has('fs.read'); // true process.permission.has('fs.read', '/home/rafaelgss/protected-folder'); // false ``` +##### `permission.drop(scope[, reference])` + +API call to drop permissions at runtime. This operation is **irreversible**. + +When called without a reference, the entire scope is dropped. When called +with a reference, only the permission for that specific resource is revoked. +Dropping a permission only affects future access checks. It does not close or +revoke access to resources that are already open, such as file descriptors, +network sockets, child processes, or worker threads. Applications are +responsible for closing or terminating those resources when they are no longer +needed. + +You can only drop the exact resource that was explicitly granted. The +reference passed to `drop()` must match the original grant. If a permission +was granted using a wildcard (`*`), only the entire scope can be dropped +(by calling `drop()` without a reference). If a directory was granted +(e.g. `--allow-fs-read=/my/folder`), you cannot drop individual files +inside it - you must drop the same directory that was originally granted. + +```js +const fs = require('node:fs'); + +// Read config at startup while we still have permission +const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); + +// Drop read access to /etc/myapp after initialization +process.permission.drop('fs.read', '/etc/myapp'); + +// This will now throw ERR_ACCESS_DENIED +process.permission.has('fs.read', '/etc/myapp/config.json'); // false + +// Drop child process permission entirely +process.permission.drop('child'); +``` + #### File System Permissions The Permission Model, by default, restricts access to the file system through the `node:fs` module. diff --git a/doc/api/process.md b/doc/api/process.md index 28b60d93836429..c054b7336a5cb6 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -3168,6 +3168,65 @@ process.permission.has('fs.read', './README.md'); process.permission.has('fs.read'); ``` +### `process.permission.drop(scope[, reference])` + + + +> Stability: 1.1 - Active Development + +* `scope` {string} +* `reference` {string} + +Drops the specified permission from the current process. This operation is +**irreversible** — once a permission is dropped, it cannot be restored through +any Node.js API. + +If no reference is provided, the entire scope is dropped. For example, +`process.permission.drop('fs.read')` will revoke ALL file system read +permissions. + +When a reference is provided, only the permission for that specific resource +is dropped. For example, `process.permission.drop('fs.read', '/etc/myapp')` +will revoke read access to that directory while keeping other read +permissions intact. + +**Important:** You can only drop the exact resource that was explicitly +granted. The reference passed to `drop()` must match the original grant: + +* If a permission was granted using a wildcard (`*`), such as + `--allow-fs-read=*`, individual paths cannot be dropped - only the entire + scope can be dropped (by calling `drop()` without a reference). +* If a directory was granted (e.g. `--allow-fs-read=/my/folder`), you cannot + drop access to individual files inside it. You must drop the same directory + that was granted. Any remaining grants continue to apply. + +The available scopes are the same as [`process.permission.has()`][]: + +* `fs` - All File System (drops both read and write) +* `fs.read` - File System read operations +* `fs.write` - File System write operations +* `child` - Child process spawning operations +* `worker` - Worker thread spawning operation +* `net` - Network operations +* `inspector` - Inspector operations +* `wasi` - WASI operations +* `addon` - Native addon operations + +```js +const fs = require('node:fs'); + +// Read configuration during startup +const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); + +// Drop read access to the config directory after initialization +process.permission.drop('fs.read', '/etc/myapp'); + +// This will now throw ERR_ACCESS_DENIED +fs.readFileSync('/etc/myapp/config.json'); +``` + ## `process.pid` -* Type: {integer} **Default:** `8192` +* Type: {integer} **Default:** `65536` This is the size (in bytes) of pre-allocated internal `Buffer` instances used for pooling. This value may be modified. diff --git a/lib/buffer.js b/lib/buffer.js index 5c983b5a240108..4377b53d865f65 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -169,7 +169,7 @@ const constants = ObjectDefineProperties({}, { }, }); -Buffer.poolSize = 8 * 1024; +Buffer.poolSize = 64 * 1024; let poolSize, poolOffset, allocPool, allocBuffer; function createPool() { From 569369f92773911fa4e97d89869f0f996f448149 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 29 May 2026 19:57:34 +0200 Subject: [PATCH 81/89] vfs: dispatch fs/promises to mounted VFS instances Add mount/unmount lifecycle on `VirtualFileSystem`, a handler registry that fs.js and fs/promises.js consult via `vfsState.handlers`, and a router that maps absolute paths to the VFS that owns them. When a VFS is mounted, the public `fs.*` and `fs/promises` APIs (including streams, `fs.watch`, and `opendir`) dispatch to the provider for paths under the mount point, and fall through to the real filesystem otherwise. Includes per-method dispatch tests, error-path coverage, multi-mount routing tests, and router unit tests. Ref: https://github.com/nodejs/node/pull/63115 Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/63537 Refs: https://github.com/nodejs/node/pull/63115 Reviewed-By: James M Snell Reviewed-By: Paolo Insogna --- lib/fs.js | 571 ++++++++++++++- lib/internal/fs/dir.js | 21 + lib/internal/fs/promises.js | 167 ++++- lib/internal/fs/utils.js | 7 + lib/internal/vfs/errors.js | 12 + lib/internal/vfs/file_system.js | 140 +++- lib/internal/vfs/router.js | 46 ++ lib/internal/vfs/setup.js | 661 ++++++++++++++++++ test/parallel/test-vfs-destructuring.js | 77 ++ test/parallel/test-vfs-fs-accessSync.js | 26 + .../test-vfs-fs-callback-error-paths.js | 75 ++ test/parallel/test-vfs-fs-chmod-callback.js | 33 + test/parallel/test-vfs-fs-chmodSync.js | 30 + test/parallel/test-vfs-fs-copyFileSync.js | 27 + test/parallel/test-vfs-fs-createReadStream.js | 59 ++ .../parallel/test-vfs-fs-createWriteStream.js | 47 ++ test/parallel/test-vfs-fs-existsSync.js | 22 + test/parallel/test-vfs-fs-fchmod-callback.js | 34 + test/parallel/test-vfs-fs-linkSync.js | 27 + test/parallel/test-vfs-fs-mkdir-callback.js | 66 ++ test/parallel/test-vfs-fs-mkdirSync.js | 31 + test/parallel/test-vfs-fs-mkdtempSync.js | 34 + test/parallel/test-vfs-fs-open-callback.js | 77 ++ test/parallel/test-vfs-fs-openAsBlob.js | 25 + test/parallel/test-vfs-fs-openSync.js | 93 +++ test/parallel/test-vfs-fs-opendir-callback.js | 50 ++ test/parallel/test-vfs-fs-opendirSync.js | 29 + .../test-vfs-fs-promises-buffer-encoding.js | 64 ++ .../test-vfs-fs-promises-stat-no-throw.js | 35 + test/parallel/test-vfs-fs-promises.js | 84 +++ .../parallel/test-vfs-fs-readFile-callback.js | 75 ++ test/parallel/test-vfs-fs-readFileSync.js | 45 ++ test/parallel/test-vfs-fs-readdirSync.js | 47 ++ test/parallel/test-vfs-fs-realpathSync.js | 30 + test/parallel/test-vfs-fs-rename-callback.js | 55 ++ test/parallel/test-vfs-fs-renameSync.js | 29 + test/parallel/test-vfs-fs-rmSync.js | 36 + test/parallel/test-vfs-fs-stat-callback.js | 42 ++ test/parallel/test-vfs-fs-statSync.js | 61 ++ test/parallel/test-vfs-fs-symlink-callback.js | 25 + test/parallel/test-vfs-fs-symlinkSync.js | 30 + .../parallel/test-vfs-fs-truncate-callback.js | 45 ++ test/parallel/test-vfs-fs-truncateSync.js | 24 + test/parallel/test-vfs-fs-watch-dispatch.js | 24 + .../test-vfs-fs-writeFile-callback.js | 41 ++ test/parallel/test-vfs-fs-writeFileSync.js | 29 + test/parallel/test-vfs-mount-errors.js | 159 +++++ test/parallel/test-vfs-mount.js | 173 +++++ test/parallel/test-vfs-multi-mount.js | 65 ++ test/parallel/test-vfs-router.js | 63 ++ 50 files changed, 3733 insertions(+), 35 deletions(-) create mode 100644 lib/internal/vfs/router.js create mode 100644 lib/internal/vfs/setup.js create mode 100644 test/parallel/test-vfs-destructuring.js create mode 100644 test/parallel/test-vfs-fs-accessSync.js create mode 100644 test/parallel/test-vfs-fs-callback-error-paths.js create mode 100644 test/parallel/test-vfs-fs-chmod-callback.js create mode 100644 test/parallel/test-vfs-fs-chmodSync.js create mode 100644 test/parallel/test-vfs-fs-copyFileSync.js create mode 100644 test/parallel/test-vfs-fs-createReadStream.js create mode 100644 test/parallel/test-vfs-fs-createWriteStream.js create mode 100644 test/parallel/test-vfs-fs-existsSync.js create mode 100644 test/parallel/test-vfs-fs-fchmod-callback.js create mode 100644 test/parallel/test-vfs-fs-linkSync.js create mode 100644 test/parallel/test-vfs-fs-mkdir-callback.js create mode 100644 test/parallel/test-vfs-fs-mkdirSync.js create mode 100644 test/parallel/test-vfs-fs-mkdtempSync.js create mode 100644 test/parallel/test-vfs-fs-open-callback.js create mode 100644 test/parallel/test-vfs-fs-openAsBlob.js create mode 100644 test/parallel/test-vfs-fs-openSync.js create mode 100644 test/parallel/test-vfs-fs-opendir-callback.js create mode 100644 test/parallel/test-vfs-fs-opendirSync.js create mode 100644 test/parallel/test-vfs-fs-promises-buffer-encoding.js create mode 100644 test/parallel/test-vfs-fs-promises-stat-no-throw.js create mode 100644 test/parallel/test-vfs-fs-promises.js create mode 100644 test/parallel/test-vfs-fs-readFile-callback.js create mode 100644 test/parallel/test-vfs-fs-readFileSync.js create mode 100644 test/parallel/test-vfs-fs-readdirSync.js create mode 100644 test/parallel/test-vfs-fs-realpathSync.js create mode 100644 test/parallel/test-vfs-fs-rename-callback.js create mode 100644 test/parallel/test-vfs-fs-renameSync.js create mode 100644 test/parallel/test-vfs-fs-rmSync.js create mode 100644 test/parallel/test-vfs-fs-stat-callback.js create mode 100644 test/parallel/test-vfs-fs-statSync.js create mode 100644 test/parallel/test-vfs-fs-symlink-callback.js create mode 100644 test/parallel/test-vfs-fs-symlinkSync.js create mode 100644 test/parallel/test-vfs-fs-truncate-callback.js create mode 100644 test/parallel/test-vfs-fs-truncateSync.js create mode 100644 test/parallel/test-vfs-fs-watch-dispatch.js create mode 100644 test/parallel/test-vfs-fs-writeFile-callback.js create mode 100644 test/parallel/test-vfs-fs-writeFileSync.js create mode 100644 test/parallel/test-vfs-mount-errors.js create mode 100644 test/parallel/test-vfs-mount.js create mode 100644 test/parallel/test-vfs-multi-mount.js create mode 100644 test/parallel/test-vfs-router.js diff --git a/lib/fs.js b/lib/fs.js index d63fad8b2a258b..043e29211a1580 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -127,6 +127,7 @@ const { validateRmOptionsSync, validateRmdirOptions, validateStringAfterArrayBufferView, + vfsState, warnOnNonPortableTemplate, } = require('internal/fs/utils'); const { @@ -195,6 +196,30 @@ function makeStatsCallback(cb) { const isFd = isInt32; +/** + * Route VFS async result (Promise) to error-first callback. + * @param {Promise|undefined} promise The VFS handler result + * @param {Function} callback The error-first callback + * @returns {boolean} true if VFS handled, false otherwise + */ +function vfsResult(promise, callback) { + if (promise === undefined) return false; + PromisePrototypeThen(promise, (result) => callback(null, result), callback); + return true; +} + +/** + * Route VFS async void result to error-first callback. + * @param {Promise|undefined} promise The VFS handler result + * @param {Function} callback The error-first callback + * @returns {boolean} true if VFS handled, false otherwise + */ +function vfsVoid(promise, callback) { + if (promise === undefined) return false; + PromisePrototypeThen(promise, () => callback(null), callback); + return true; +} + function isFileType(stats, fileType) { // Use stats array directly to avoid creating an fs.Stats instance just for // our internal use. @@ -218,6 +243,9 @@ function access(path, mode, callback) { mode = F_OK; } + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.access(path, mode), callback)) return; + path = getValidatedPath(path); callback = makeCallback(callback); @@ -234,6 +262,11 @@ function access(path, mode, callback) { * @returns {void} */ function accessSync(path, mode) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.accessSync(path, mode); + if (result !== undefined) return; + } binding.access(getValidatedPath(path), mode); } @@ -246,6 +279,15 @@ function accessSync(path, mode) { function exists(path, callback) { validateFunction(callback, 'cb'); + const h = vfsState.handlers; + if (h !== null) { + const result = h.existsSync(path); + if (result !== undefined) { + process.nextTick(callback, result); + return; + } + } + function suppressedCallback(err) { callback(!err); } @@ -271,6 +313,11 @@ let showExistsDeprecation = true; * @returns {boolean} */ function existsSync(path) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.existsSync(path); + if (result !== undefined) return result; + } try { path = getValidatedPath(path); } catch (err) { @@ -357,6 +404,14 @@ function checkAborted(signal, callback) { function readFile(path, options, callback) { callback ||= options; validateFunction(callback, 'cb'); + + const h = vfsState.handlers; + if (h !== null) { + const opts = typeof options === 'function' ? undefined : options; + if (checkAborted(opts?.signal, callback)) return; + if (vfsResult(h.readFile(path, opts), callback)) return; + } + options = getOptions(options, { flag: 'r' }); ReadFileContext ??= require('internal/fs/read/context'); const context = new ReadFileContext(callback, options.encoding); @@ -427,6 +482,11 @@ function tryReadSync(fd, isUserFd, buffer, pos, len) { * @returns {string | Buffer} */ function readFileSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.readFileSync(path, options); + if (result !== undefined) return result; + } options = getOptions(options, { flag: 'r' }); if (options.encoding === 'utf8' || options.encoding === 'utf-8') { @@ -499,6 +559,9 @@ function close(fd, callback = defaultCloseCallback) { if (callback !== defaultCloseCallback) callback = makeCallback(callback); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.close(fd), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.close(fd, req); @@ -510,6 +573,11 @@ function close(fd, callback = defaultCloseCallback) { * @returns {void} */ function closeSync(fd) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.closeSync(fd); + if (result !== undefined) return; + } binding.close(fd); } @@ -525,7 +593,6 @@ function closeSync(fd) { * @returns {void} */ function open(path, flags, mode, callback) { - path = getValidatedPath(path); if (arguments.length < 3) { callback = flags; flags = 'r'; @@ -536,7 +603,13 @@ function open(path, flags, mode, callback) { } else { mode = parseFileMode(mode, 'mode', 0o666); } + const flagsNumber = stringToFlags(flags); + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.open(path, flagsNumber, mode), callback)) return; + + path = getValidatedPath(path); callback = makeCallback(callback); const req = new FSReqCallback(); @@ -553,10 +626,17 @@ function open(path, flags, mode, callback) { * @returns {number} */ function openSync(path, flags, mode) { + flags = stringToFlags(flags); + mode = parseFileMode(mode, 'mode', 0o666); + const h = vfsState.handlers; + if (h !== null) { + const result = h.openSync(path, flags, mode); + if (result !== undefined) return result; + } return binding.open( getValidatedPath(path), - stringToFlags(flags), - parseFileMode(mode, 'mode', 0o666), + flags, + mode, ); } @@ -571,6 +651,13 @@ function openAsBlob(path, options = kEmptyObject) { validateObject(options, 'options'); const type = options.type || ''; validateString(type, 'options.type'); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.openAsBlob(path, options); + if (result !== undefined) return PromiseResolve(result); + } + // The underlying implementation here returns the Blob synchronously for now. // To give ourselves flexibility to maybe return the Blob asynchronously, // this API returns a Promise. @@ -660,6 +747,16 @@ function read(fd, buffer, offsetOrOptions, length, position, callback) { validateOffsetLengthRead(offset, length, buffer.byteLength); + const h = vfsState.handlers; + if (h !== null) { + const promise = h.read(fd, buffer, offset, length, position); + if (promise !== undefined) { + PromisePrototypeThen(promise, + (bytesRead) => callback(null, bytesRead, buffer), callback); + return; + } + } + function wrapper(err, bytesRead) { // Retain a reference to buffer so that it can't be GC'ed too soon. callback(err, bytesRead || 0, buffer); @@ -729,6 +826,12 @@ function readSync(fd, buffer, offsetOrOptions, length, position) { validateOffsetLengthRead(offset, length, buffer.byteLength); + const h = vfsState.handlers; + if (h !== null) { + const result = h.readSync(fd, buffer, offset, length, position); + if (result !== undefined) return result; + } + return binding.read(fd, buffer, offset, length, position); } @@ -755,12 +858,22 @@ function readv(fd, buffers, position, callback) { callback ||= position; validateFunction(callback, 'cb'); - const req = new FSReqCallback(); - req.oncomplete = wrapper; - if (typeof position !== 'number') position = null; + const h = vfsState.handlers; + if (h !== null) { + const promise = h.readv(fd, buffers, position); + if (promise !== undefined) { + PromisePrototypeThen(promise, + (read) => callback(null, read, buffers), callback); + return; + } + } + + const req = new FSReqCallback(); + req.oncomplete = wrapper; + binding.readBuffers(fd, buffers, position, req); } @@ -782,6 +895,12 @@ function readvSync(fd, buffers, position) { if (typeof position !== 'number') position = null; + const h = vfsState.handlers; + if (h !== null) { + const result = h.readvSync(fd, buffers, position); + if (result !== undefined) return result; + } + return binding.readBuffers(fd, buffers, position); } @@ -830,6 +949,16 @@ function write(fd, buffer, offsetOrOptions, length, position, callback) { position = null; validateOffsetLengthWrite(offset, length, buffer.byteLength); + const h = vfsState.handlers; + if (h !== null) { + const promise = h.write(fd, buffer, offset, length, position); + if (promise !== undefined) { + PromisePrototypeThen(promise, + (bytesWritten) => callback(null, bytesWritten, buffer), callback); + return; + } + } + const req = new FSReqCallback(); req.oncomplete = wrapper; binding.writeBuffer(fd, buffer, offset, length, position, req); @@ -853,6 +982,17 @@ function write(fd, buffer, offsetOrOptions, length, position, callback) { callback = position; validateFunction(callback, 'cb'); + const h = vfsState.handlers; + if (h !== null) { + const bufAsBuffer = Buffer.from(str, length); + const promise = h.write(fd, bufAsBuffer, 0, bufAsBuffer.length, offset); + if (promise !== undefined) { + PromisePrototypeThen(promise, + (bytesWritten) => callback(null, bytesWritten, buffer), callback); + return; + } + } + const req = new FSReqCallback(); req.oncomplete = wrapper; binding.writeString(fd, str, offset, length, req); @@ -898,6 +1038,13 @@ function writeSync(fd, buffer, offsetOrOptions, length, position) { if (typeof length !== 'number') length = buffer.byteLength - offset; validateOffsetLengthWrite(offset, length, buffer.byteLength); + + const h = vfsState.handlers; + if (h !== null) { + const vfsResult = h.writeSync(fd, buffer, offset, length, position); + if (vfsResult !== undefined) return vfsResult; + } + result = binding.writeBuffer(fd, buffer, offset, length, position, undefined, ctx); } else { @@ -906,6 +1053,14 @@ function writeSync(fd, buffer, offsetOrOptions, length, position) { if (offset === undefined) offset = null; + + const h = vfsState.handlers; + if (h !== null) { + const bufAsBuffer = Buffer.from(buffer, length); + const vfsResult = h.writeSync(fd, bufAsBuffer, 0, bufAsBuffer.length, offset); + if (vfsResult !== undefined) return vfsResult; + } + result = binding.writeString(fd, buffer, offset, length, undefined, ctx); } @@ -941,12 +1096,22 @@ function writev(fd, buffers, position, callback) { return; } - const req = new FSReqCallback(); - req.oncomplete = wrapper; - if (typeof position !== 'number') position = null; + const h = vfsState.handlers; + if (h !== null) { + const promise = h.writev(fd, buffers, position); + if (promise !== undefined) { + PromisePrototypeThen(promise, + (written) => callback(null, written, buffers), callback); + return; + } + } + + const req = new FSReqCallback(); + req.oncomplete = wrapper; + binding.writeBuffers(fd, buffers, position, req); } @@ -974,6 +1139,12 @@ function writevSync(fd, buffers, position) { if (typeof position !== 'number') position = null; + const h = vfsState.handlers; + if (h !== null) { + const result = h.writevSync(fd, buffers, position); + if (result !== undefined) return result; + } + return binding.writeBuffers(fd, buffers, position); } @@ -986,6 +1157,9 @@ function writevSync(fd, buffers, position) { * @returns {void} */ function rename(oldPath, newPath, callback) { + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.rename(oldPath, newPath), callback)) return; + callback = makeCallback(callback); const req = new FSReqCallback(); req.oncomplete = callback; @@ -1005,6 +1179,11 @@ function rename(oldPath, newPath, callback) { * @returns {void} */ function renameSync(oldPath, newPath) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.renameSync(oldPath, newPath); + if (result !== undefined) return; + } binding.rename( getValidatedPath(oldPath, 'oldPath'), getValidatedPath(newPath, 'newPath'), @@ -1029,6 +1208,10 @@ function truncate(path, len, callback) { validateInteger(len, 'len'); len = MathMax(0, len); validateFunction(callback, 'cb'); + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.truncate(path, len), callback)) return; + fs.open(path, 'r+', (er, fd) => { if (er) return callback(er); const req = new FSReqCallback(); @@ -1051,6 +1234,13 @@ function truncateSync(path, len) { if (len === undefined) { len = 0; } + + const h = vfsState.handlers; + if (h !== null) { + const result = h.truncateSync(path, len); + if (result !== undefined) return; + } + // Allow error to be thrown, but still close fd. const fd = fs.openSync(path, 'r+'); try { @@ -1076,6 +1266,9 @@ function ftruncate(fd, len = 0, callback) { len = MathMax(0, len); callback = makeCallback(callback); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.ftruncate(fd, len), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.ftruncate(fd, len, req); @@ -1089,6 +1282,13 @@ function ftruncate(fd, len = 0, callback) { */ function ftruncateSync(fd, len = 0) { validateInteger(len, 'len'); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.ftruncateSync(fd, len < 0 ? 0 : len); + if (result !== undefined) return; + } + binding.ftruncate(fd, len < 0 ? 0 : len); } @@ -1118,6 +1318,9 @@ function rmdir(path, options, callback) { options = undefined; } + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.rmdir(path), callback)) return; + if (options?.recursive !== undefined) { // This API previously accepted a `recursive` option that was deprecated // and removed. However, in order to make the change more visible, we @@ -1146,6 +1349,11 @@ function rmdir(path, options, callback) { * @returns {void} */ function rmdirSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.rmdirSync(path); + if (result !== undefined) return; + } path = getValidatedPath(path); if (options?.recursive !== undefined) { @@ -1178,6 +1386,10 @@ function rm(path, options, callback) { callback = options; options = undefined; } + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.rm(path, options), callback)) return; + path = getValidatedPath(path); validateRmOptions(path, options, false, (err, options) => { @@ -1202,6 +1414,11 @@ function rm(path, options, callback) { * @returns {void} */ function rmSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.rmSync(path, options); + if (result !== undefined) return; + } const opts = validateRmOptionsSync(path, options, false); return binding.rmSync(getValidatedPath(path), opts.maxRetries, opts.recursive, opts.retryDelay); } @@ -1215,8 +1432,13 @@ function rmSync(path, options) { * @returns {void} */ function fdatasync(fd, callback) { + callback = makeCallback(callback); + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.fdatasync(fd), callback)) return; + const req = new FSReqCallback(); - req.oncomplete = makeCallback(callback); + req.oncomplete = callback; if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.')); @@ -1233,6 +1455,12 @@ function fdatasync(fd, callback) { * @returns {void} */ function fdatasyncSync(fd) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.fdatasyncSync(fd); + if (result !== undefined) return; + } + if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'); } @@ -1247,8 +1475,13 @@ function fdatasyncSync(fd) { * @returns {void} */ function fsync(fd, callback) { + callback = makeCallback(callback); + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.fsync(fd), callback)) return; + const req = new FSReqCallback(); - req.oncomplete = makeCallback(callback); + req.oncomplete = callback; if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.')); return; @@ -1263,6 +1496,12 @@ function fsync(fd, callback) { * @returns {void} */ function fsyncSync(fd) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.fsyncSync(fd); + if (result !== undefined) return; + } + if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'); } @@ -1280,11 +1519,23 @@ function fsyncSync(fd) { * @returns {void} */ function mkdir(path, options, callback) { - let mode = 0o777; - let recursive = false; if (typeof options === 'function') { callback = options; - } else if (typeof options === 'number' || typeof options === 'string') { + options = undefined; + } + + const h = vfsState.handlers; + if (h !== null) { + const promise = h.mkdir(path, options); + if (promise !== undefined) { + PromisePrototypeThen(promise, (r) => callback(null, r.result), callback); + return; + } + } + + let mode = 0o777; + let recursive = false; + if (typeof options === 'number' || typeof options === 'string') { mode = parseFileMode(options, 'mode'); } else if (options) { if (options.recursive !== undefined) { @@ -1317,6 +1568,11 @@ function mkdir(path, options, callback) { * @returns {string | void} */ function mkdirSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const vfsResult = h.mkdirSync(path, options); + if (vfsResult !== undefined) return vfsResult.result; + } let mode = 0o777; let recursive = false; if (typeof options === 'number' || typeof options === 'string') { @@ -1495,7 +1751,15 @@ function readdirSyncRecursive(basePath, options) { * @returns {void} */ function readdir(path, options, callback) { - callback = makeCallback(typeof options === 'function' ? options : callback); + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.readdir(path, options), callback)) return; + + callback = makeCallback(callback); options = getOptions(options); path = getValidatedPath(path); if (options.recursive != null) { @@ -1541,6 +1805,11 @@ function readdir(path, options, callback) { * @returns {string | Buffer[] | Dirent[]} */ function readdirSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.readdirSync(path, options); + if (result !== undefined) return result; + } options = getOptions(options); path = getValidatedPath(path); if (options.recursive != null) { @@ -1576,6 +1845,10 @@ function fstat(fd, options = { bigint: false }, callback) { callback = options; options = kEmptyObject; } + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.fstat(fd, options), callback)) return; + callback = makeStatsCallback(callback); const req = new FSReqCallback(options.bigint); @@ -1599,6 +1872,10 @@ function lstat(path, options = { bigint: false }, callback) { callback = options; options = kEmptyObject; } + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.lstat(path, options), callback)) return; + callback = makeStatsCallback(callback); path = getValidatedPath(path); if (permission.isEnabled() && !permission.has('fs.read', path)) { @@ -1632,6 +1909,9 @@ function stat(path, options = { bigint: false, throwIfNoEntry: true }, callback) options = getOptions(options, { bigint: false }); } + const h = vfsState.handlers; + if (h !== null && vfsResult(h.stat(path, options), callback)) return; + callback = makeStatsCallback(callback); path = getValidatedPath(path); @@ -1648,6 +1928,16 @@ function statfs(path, options = { bigint: false }, callback) { options = kEmptyObject; } validateFunction(callback, 'cb'); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.statfsSync(path, options); + if (result !== undefined) { + process.nextTick(callback, null, result); + return; + } + } + path = getValidatedPath(path); const req = new FSReqCallback(options.bigint); req.oncomplete = (err, stats) => { @@ -1668,6 +1958,11 @@ function statfs(path, options = { bigint: false }, callback) { * @returns {Stats | undefined} */ function fstatSync(fd, options = { bigint: false }) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.fstatSync(fd); + if (result !== undefined) return result; + } const stats = binding.fstat(fd, options.bigint, undefined, false); if (stats === undefined) { return; @@ -1686,6 +1981,11 @@ function fstatSync(fd, options = { bigint: false }) { * @returns {Stats | undefined} */ function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.lstatSync(path, options); + if (result !== undefined) return result; + } path = getValidatedPath(path); if (permission.isEnabled() && !permission.has('fs.read', path)) { const resource = BufferIsBuffer(path) ? BufferToString(path) : path; @@ -1715,6 +2015,11 @@ function lstatSync(path, options = { bigint: false, throwIfNoEntry: true }) { * @returns {Stats} */ function statSync(path, options = { bigint: false, throwIfNoEntry: true }) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.statSync(path, options); + if (result !== undefined) return result; + } const stats = binding.stat( getValidatedPath(path), options.bigint, @@ -1728,6 +2033,12 @@ function statSync(path, options = { bigint: false, throwIfNoEntry: true }) { } function statfsSync(path, options = { bigint: false }) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.statfsSync(path, options); + if (result !== undefined) return result; + } + const stats = binding.statfs(getValidatedPath(path), options.bigint); return getStatFsFromBinding(stats); } @@ -1744,7 +2055,15 @@ function statfsSync(path, options = { bigint: false }) { * @returns {void} */ function readlink(path, options, callback) { - callback = makeCallback(typeof options === 'function' ? options : callback); + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.readlink(path, options), callback)) return; + + callback = makeCallback(callback); options = getOptions(options); const req = new FSReqCallback(); req.oncomplete = callback; @@ -1759,6 +2078,11 @@ function readlink(path, options, callback) { * @returns {string | Buffer} */ function readlinkSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.readlinkSync(path, options); + if (result !== undefined) return result; + } options = getOptions(options); return binding.readlink(getValidatedPath(path), options.encoding); } @@ -1779,6 +2103,9 @@ function symlink(target, path, type, callback) { validateOneOf(type, 'type', ['dir', 'file', 'junction', null, undefined]); } + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.symlink(target, path, type), callback)) return; + // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass // the permission model security guarantees. Thus, this API is disabled unless fs.read // and fs.write permission has been given. @@ -1840,6 +2167,11 @@ function symlink(target, path, type, callback) { * @returns {void} */ function symlinkSync(target, path, type) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.symlinkSync(target, path, type); + if (result !== undefined) return; + } validateOneOf(type, 'type', ['dir', 'file', 'junction', null, undefined]); if (isWindows && type == null) { const absoluteTarget = pathModule.resolve(`${path}`, '..', `${target}`); @@ -1876,6 +2208,9 @@ function symlinkSync(target, path, type) { function link(existingPath, newPath, callback) { callback = makeCallback(callback); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.link(existingPath, newPath), callback)) return; + existingPath = getValidatedPath(existingPath, 'existingPath'); newPath = getValidatedPath(newPath, 'newPath'); @@ -1893,6 +2228,12 @@ function link(existingPath, newPath, callback) { * @returns {void} */ function linkSync(existingPath, newPath) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.linkSync(existingPath, newPath); + if (result !== undefined) return; + } + existingPath = getValidatedPath(existingPath, 'existingPath'); newPath = getValidatedPath(newPath, 'newPath'); @@ -1909,6 +2250,9 @@ function linkSync(existingPath, newPath) { * @returns {void} */ function unlink(path, callback) { + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.unlink(path), callback)) return; + callback = makeCallback(callback); const req = new FSReqCallback(); req.oncomplete = callback; @@ -1921,6 +2265,11 @@ function unlink(path, callback) { * @returns {void} */ function unlinkSync(path) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.unlinkSync(path); + if (result !== undefined) return; + } binding.unlink(getValidatedPath(path)); } @@ -1935,6 +2284,9 @@ function fchmod(fd, mode, callback) { mode = parseFileMode(mode, 'mode'); callback = makeCallback(callback); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.fchmod(fd), callback)) return; + if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.')); return; @@ -1952,6 +2304,12 @@ function fchmod(fd, mode, callback) { * @returns {void} */ function fchmodSync(fd, mode) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.fchmodSync(fd); + if (result !== undefined) return; + } + if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fchmod API is disabled when Permission Model is enabled.'); } @@ -1971,6 +2329,10 @@ function fchmodSync(fd, mode) { function lchmod(path, mode, callback) { validateFunction(callback, 'cb'); mode = parseFileMode(mode, 'mode'); + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.lchmod(path, mode), callback)) return; + fs.open(path, O_WRONLY | O_SYMLINK, (err, fd) => { if (err) { callback(err); @@ -2016,6 +2378,9 @@ function chmod(path, mode, callback) { mode = parseFileMode(mode, 'mode'); callback = makeCallback(callback); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.chmod(path, mode), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.chmod(path, mode, req); @@ -2031,6 +2396,12 @@ function chmodSync(path, mode) { path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); + const h = vfsState.handlers; + if (h !== null) { + const result = h.chmodSync(path, mode); + if (result !== undefined) return; + } + binding.chmod(path, mode); } @@ -2047,6 +2418,10 @@ function lchown(path, uid, gid, callback) { path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.lchown(path, uid, gid), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.lchown(path, uid, gid, req); @@ -2063,6 +2438,13 @@ function lchownSync(path, uid, gid) { path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.lchownSync(path, uid, gid); + if (result !== undefined) return; + } + binding.lchown(path, uid, gid); } @@ -2078,6 +2460,10 @@ function fchown(fd, uid, gid, callback) { validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); callback = makeCallback(callback); + + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.fchown(fd), callback)) return; + if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.')); return; @@ -2098,6 +2484,13 @@ function fchown(fd, uid, gid, callback) { function fchownSync(fd, uid, gid) { validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.fchownSync(fd); + if (result !== undefined) return; + } + if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('fchown API is disabled when Permission Model is enabled.'); } @@ -2120,6 +2513,9 @@ function chown(path, uid, gid, callback) { validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.chown(path, uid, gid), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.chown(path, uid, gid, req); @@ -2137,6 +2533,13 @@ function chownSync(path, uid, gid) { path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.chownSync(path, uid, gid); + if (result !== undefined) return; + } + binding.chown(path, uid, gid); } @@ -2153,6 +2556,9 @@ function utimes(path, atime, mtime, callback) { callback = makeCallback(callback); path = getValidatedPath(path); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.utimes(path, atime, mtime), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.utimes( @@ -2172,8 +2578,16 @@ function utimes(path, atime, mtime, callback) { * @returns {void} */ function utimesSync(path, atime, mtime) { + path = getValidatedPath(path); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.utimesSync(path, atime, mtime); + if (result !== undefined) return; + } + binding.utimes( - getValidatedPath(path), + path, toUnixTimestamp(atime), toUnixTimestamp(mtime), ); @@ -2193,6 +2607,9 @@ function futimes(fd, atime, mtime, callback) { mtime = toUnixTimestamp(mtime, 'mtime'); callback = makeCallback(callback); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.futimes(fd), callback)) return; + if (permission.isEnabled()) { callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.')); return; @@ -2213,6 +2630,12 @@ function futimes(fd, atime, mtime, callback) { * @returns {void} */ function futimesSync(fd, atime, mtime) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.futimesSync(fd); + if (result !== undefined) return; + } + if (permission.isEnabled()) { throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'); } @@ -2237,6 +2660,9 @@ function lutimes(path, atime, mtime, callback) { callback = makeCallback(callback); path = getValidatedPath(path); + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.lutimes(path, atime, mtime), callback)) return; + const req = new FSReqCallback(); req.oncomplete = callback; binding.lutimes( @@ -2256,8 +2682,16 @@ function lutimes(path, atime, mtime, callback) { * @returns {void} */ function lutimesSync(path, atime, mtime) { + path = getValidatedPath(path); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.lutimesSync(path, atime, mtime); + if (result !== undefined) return; + } + binding.lutimes( - getValidatedPath(path), + path, toUnixTimestamp(atime), toUnixTimestamp(mtime), ); @@ -2334,16 +2768,24 @@ function writeAll(fd, isUserFd, buffer, offset, length, signal, flush, callback) function writeFile(path, data, options, callback) { callback ||= options; validateFunction(callback, 'cb'); - options = getOptions(options, { + + options = getOptions(typeof options === 'function' ? null : options, { encoding: 'utf8', mode: 0o666, flag: 'w', flush: false, }); - const flag = options.flag || 'w'; const flush = options.flush ?? false; - validateBoolean(flush, 'options.flush'); + parseFileMode(options.mode, 'mode', 0o666); + + const h = vfsState.handlers; + if (h !== null) { + if (checkAborted(options.signal, callback)) return; + if (vfsVoid(h.writeFile(path, data, options), callback)) return; + } + + const flag = options.flag || 'w'; if (!isArrayBufferView(data)) { validateStringAfterArrayBufferView(data, 'data'); @@ -2390,10 +2832,15 @@ function writeFileSync(path, data, options) { flag: 'w', flush: false, }); - const flush = options.flush ?? false; - validateBoolean(flush, 'options.flush'); + parseFileMode(options.mode, 'mode', 0o666); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.writeFileSync(path, data, options); + if (result !== undefined) return; + } const flag = options.flag || 'w'; @@ -2452,7 +2899,17 @@ function writeFileSync(path, data, options) { function appendFile(path, data, options, callback) { callback ||= options; validateFunction(callback, 'cb'); - options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); + + options = getOptions(typeof options === 'function' ? null : options, { + encoding: 'utf8', mode: 0o666, flag: 'a', + }); + parseFileMode(options.mode, 'mode', 0o666); + + const h = vfsState.handlers; + if (h !== null) { + if (checkAborted(options.signal, callback)) return; + if (vfsVoid(h.appendFile(path, data, options), callback)) return; + } // Don't make changes directly on options object options = copyObject(options); @@ -2477,6 +2934,13 @@ function appendFile(path, data, options, callback) { */ function appendFileSync(path, data, options) { options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); + parseFileMode(options.mode, 'mode', 0o666); + + const h = vfsState.handlers; + if (h !== null) { + const result = h.appendFileSync(path, data, options); + if (result !== undefined) return; + } // Don't make changes directly on options object options = copyObject(options); @@ -2505,6 +2969,11 @@ function appendFileSync(path, data, options) { * @returns {watchers.FSWatcher} */ function watch(filename, options, listener) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.watch(filename, options, listener); + if (result !== undefined) return result; + } if (typeof options === 'function') { listener = options; } @@ -2574,6 +3043,11 @@ const statWatchers = new SafeMap(); * @returns {watchers.StatWatcher} */ function watchFile(filename, options, listener) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.watchFile(filename, options, listener); + if (result !== undefined) return result; + } filename = getValidatedPath(filename); filename = pathModule.resolve(filename); let stat; @@ -2616,6 +3090,10 @@ function watchFile(filename, options, listener) { * @returns {void} */ function unwatchFile(filename, listener) { + const h = vfsState.handlers; + if (h !== null) { + if (h.unwatchFile(filename, listener)) return; + } filename = getValidatedPath(filename); filename = pathModule.resolve(filename); const stat = statWatchers.get(filename); @@ -2693,6 +3171,11 @@ if (isWindows) { * @returns {string | Buffer} */ function realpathSync(p, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.realpathSync(p, options); + if (result !== undefined) return result; + } options = getOptions(options); p = toPathIfFileURL(p); if (typeof p !== 'string') { @@ -2829,6 +3312,11 @@ function realpathSync(p, options) { * @returns {string | Buffer} */ realpathSync.native = (path, options) => { + const h = vfsState.handlers; + if (h !== null) { + const result = h.realpathSync(path, options); + if (result !== undefined) return result; + } options = getOptions(options); return binding.realpath( getValidatedPath(path), @@ -2850,9 +3338,14 @@ realpathSync.native = (path, options) => { function realpath(p, options, callback) { if (typeof options === 'function') { callback = options; + options = undefined; } else { validateFunction(callback, 'cb'); } + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.realpath(p, options), callback)) return; + options = getOptions(options); p = toPathIfFileURL(p); @@ -2991,6 +3484,8 @@ function realpath(p, options, callback) { */ realpath.native = (path, options, callback) => { callback = makeCallback(callback || options); + const h = vfsState.handlers; + if (h !== null && vfsResult(h.realpath(path, options), callback)) return; options = getOptions(options); path = getValidatedPath(path); const req = new FSReqCallback(); @@ -3010,6 +3505,10 @@ realpath.native = (path, options, callback) => { */ function mkdtemp(prefix, options, callback) { callback = makeCallback(typeof options === 'function' ? options : callback); + + const h = vfsState.handlers; + if (h !== null && vfsResult(h.mkdtemp(prefix, typeof options === 'function' ? undefined : options), callback)) return; + options = getOptions(options); prefix = getValidatedPath(prefix, 'prefix'); @@ -3027,6 +3526,12 @@ function mkdtemp(prefix, options, callback) { * @returns {string} */ function mkdtempSync(prefix, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.mkdtempSync(prefix, options); + if (result !== undefined) return result; + } + options = getOptions(options); prefix = getValidatedPath(prefix, 'prefix'); @@ -3079,6 +3584,9 @@ function copyFile(src, dest, mode, callback) { mode = 0; } + const h = vfsState.handlers; + if (h !== null && vfsVoid(h.copyFile(src, dest, mode), callback)) return; + src = getValidatedPath(src, 'src'); dest = getValidatedPath(dest, 'dest'); callback = makeCallback(callback); @@ -3097,6 +3605,11 @@ function copyFile(src, dest, mode, callback) { * @returns {void} */ function copyFileSync(src, dest, mode) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.copyFileSync(src, dest, mode); + if (result !== undefined) return; + } binding.copyFile( getValidatedPath(src, 'src'), getValidatedPath(dest, 'dest'), @@ -3170,6 +3683,11 @@ function lazyLoadStreams() { * @returns {ReadStream} */ function createReadStream(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.createReadStream(path, options); + if (result !== undefined) return result; + } lazyLoadStreams(); return new ReadStream(path, options); } @@ -3193,6 +3711,11 @@ function createReadStream(path, options) { * @returns {WriteStream} */ function createWriteStream(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.createWriteStream(path, options); + if (result !== undefined) return result; + } lazyLoadStreams(); return new WriteStream(path, options); } diff --git a/lib/internal/fs/dir.js b/lib/internal/fs/dir.js index 03f585bab2afaf..32050f31ae6d5d 100644 --- a/lib/internal/fs/dir.js +++ b/lib/internal/fs/dir.js @@ -31,6 +31,7 @@ const { getDirent, getOptions, getValidatedPath, + vfsState, } = require('internal/fs/utils'); const { validateFunction, @@ -330,6 +331,20 @@ function opendir(path, options, callback) { callback = typeof options === 'function' ? options : callback; validateFunction(callback, 'callback'); + const h = vfsState.handlers; + if (h !== null) { + try { + const result = h.opendirSync(path, options); + if (result !== undefined) { + process.nextTick(callback, null, result); + return; + } + } catch (err) { + process.nextTick(callback, err); + return; + } + } + path = getValidatedPath(path); options = getOptions(options, { encoding: 'utf8', @@ -354,6 +369,12 @@ function opendir(path, options, callback) { } function opendirSync(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.opendirSync(path, options); + if (result !== undefined) return result; + } + path = getValidatedPath(path); options = getOptions(options, { encoding: 'utf8' }); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index e8694843987c2b..45072c1581703d 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -79,6 +79,7 @@ const { validateRmOptions, validateRmdirOptions, validateStringAfterArrayBufferView, + vfsState, warnOnNonPortableTemplate, } = require('internal/fs/utils'); const { opendir } = require('internal/fs/dir'); @@ -1251,6 +1252,11 @@ async function readFileHandle(filehandle, options) { // All of the functions are defined as async in order to ensure that errors // thrown cause promise rejections rather than being thrown synchronously. async function access(path, mode = F_OK) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.access(path, mode); + if (promise !== undefined) { await promise; return; } + } return await PromisePrototypeThen( binding.access(getValidatedPath(path), mode, kUsePromises), undefined, @@ -1266,6 +1272,11 @@ async function cp(src, dest, options) { } async function copyFile(src, dest, mode) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.copyFile(src, dest, mode); + if (promise !== undefined) { await promise; return; } + } return await PromisePrototypeThen( binding.copyFile( getValidatedPath(src, 'src'), @@ -1281,6 +1292,11 @@ async function copyFile(src, dest, mode) { // Note that unlike fs.open() which uses numeric file descriptors, // fsPromises.open() uses the fs.FileHandle class. async function open(path, flags, mode) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.promisesOpen(path, flags, mode); + if (result !== undefined) return result; + } path = getValidatedPath(path); const flagsNumber = stringToFlags(flags); mode = parseFileMode(mode, 'mode', 0o666); @@ -1427,6 +1443,11 @@ async function writev(handle, buffers, position) { } async function rename(oldPath, newPath) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.rename(oldPath, newPath); + if (promise !== undefined) { await promise; return; } + } oldPath = getValidatedPath(oldPath, 'oldPath'); newPath = getValidatedPath(newPath, 'newPath'); return await PromisePrototypeThen( @@ -1437,6 +1458,11 @@ async function rename(oldPath, newPath) { } async function truncate(path, len = 0) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.truncate(path, len); + if (promise !== undefined) { await promise; return; } + } const fd = await open(path, 'r+'); return handleFdClose(ftruncate(fd, len), fd.close); } @@ -1452,12 +1478,22 @@ async function ftruncate(handle, len = 0) { } async function rm(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.rm(path, options); + if (promise !== undefined) { await promise; return; } + } path = getValidatedPath(path); options = await validateRmOptionsPromise(path, options, false); return lazyRimRaf()(path, options); } async function rmdir(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.rmdir(path); + if (promise !== undefined) { await promise; return; } + } path = getValidatedPath(path); if (options?.recursive !== undefined) { @@ -1494,6 +1530,11 @@ async function fsync(handle) { } async function mkdir(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.mkdir(path, options); + if (promise !== undefined) return (await promise).result; + } if (typeof options === 'number' || typeof options === 'string') { options = { mode: options }; } @@ -1592,6 +1633,11 @@ async function readdirRecursive(originalPath, options) { } async function readdir(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.readdir(path, options); + if (promise !== undefined) return await promise; + } options = getOptions(options); // Make shallow copy to prevent mutating options from affecting results @@ -1617,6 +1663,11 @@ async function readdir(path, options) { } async function readlink(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.readlink(path, options); + if (promise !== undefined) return await promise; + } options = getOptions(options); path = getValidatedPath(path, 'oldPath'); return await PromisePrototypeThen( @@ -1627,6 +1678,11 @@ async function readlink(path, options) { } async function symlink(target, path, type) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.symlink(target, path, type); + if (promise !== undefined) { await promise; return; } + } validateOneOf(type, 'type', ['dir', 'file', 'junction', null, undefined]); if (isWindows && type == null) { try { @@ -1669,6 +1725,11 @@ async function fstat(handle, options = { bigint: false }) { } async function lstat(path, options = { bigint: false }) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.lstat(path, options); + if (promise !== undefined) return await promise; + } path = getValidatedPath(path); if (permission.isEnabled() && !permission.has('fs.read', path)) { const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path); @@ -1683,6 +1744,11 @@ async function lstat(path, options = { bigint: false }) { } async function stat(path, options = { bigint: false, throwIfNoEntry: true }) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.stat(path, options); + if (promise !== undefined) return await promise; + } const result = await PromisePrototypeThen( binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry), undefined, @@ -1696,6 +1762,12 @@ async function stat(path, options = { bigint: false, throwIfNoEntry: true }) { } async function statfs(path, options = { bigint: false }) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.statfs(path, options); + if (result !== undefined) return result; + } + const result = await PromisePrototypeThen( binding.statfs(getValidatedPath(path), options.bigint, kUsePromises), undefined, @@ -1705,6 +1777,11 @@ async function statfs(path, options = { bigint: false }) { } async function link(existingPath, newPath) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.link(existingPath, newPath); + if (promise !== undefined) { await promise; return; } + } existingPath = getValidatedPath(existingPath, 'existingPath'); newPath = getValidatedPath(newPath, 'newPath'); return await PromisePrototypeThen( @@ -1715,6 +1792,11 @@ async function link(existingPath, newPath) { } async function unlink(path) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.unlink(path); + if (promise !== undefined) { await promise; return; } + } return await PromisePrototypeThen( binding.unlink(getValidatedPath(path), kUsePromises), undefined, @@ -1737,6 +1819,13 @@ async function fchmod(handle, mode) { async function chmod(path, mode) { path = getValidatedPath(path); mode = parseFileMode(mode, 'mode'); + + const h = vfsState.handlers; + if (h !== null) { + const promise = h.chmod(path, mode); + if (promise !== undefined) { await promise; return; } + } + return await PromisePrototypeThen( binding.chmod(path, mode, kUsePromises), undefined, @@ -1745,6 +1834,12 @@ async function chmod(path, mode) { } async function lchmod(path, mode) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.lchmod(path, mode); + if (promise !== undefined) { await promise; return; } + } + if (O_SYMLINK === undefined) throw new ERR_METHOD_NOT_IMPLEMENTED('lchmod()'); @@ -1753,6 +1848,12 @@ async function lchmod(path, mode) { } async function lchown(path, uid, gid) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.lchown(path, uid, gid); + if (promise !== undefined) { await promise; return; } + } + path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -1777,6 +1878,12 @@ async function fchown(handle, uid, gid) { } async function chown(path, uid, gid) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.chown(path, uid, gid); + if (promise !== undefined) { await promise; return; } + } + path = getValidatedPath(path); validateInteger(uid, 'uid', -1, kMaxUserId); validateInteger(gid, 'gid', -1, kMaxUserId); @@ -1789,6 +1896,13 @@ async function chown(path, uid, gid) { async function utimes(path, atime, mtime) { path = getValidatedPath(path); + + const h = vfsState.handlers; + if (h !== null) { + const promise = h.utimes(path, atime, mtime); + if (promise !== undefined) { await promise; return; } + } + return await PromisePrototypeThen( binding.utimes( path, @@ -1812,6 +1926,12 @@ async function futimes(handle, atime, mtime) { } async function lutimes(path, atime, mtime) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.lutimes(path, atime, mtime); + if (promise !== undefined) { await promise; return; } + } + return await PromisePrototypeThen( binding.lutimes( getValidatedPath(path), @@ -1825,6 +1945,11 @@ async function lutimes(path, atime, mtime) { } async function realpath(path, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.realpath(path, options); + if (promise !== undefined) return await promise; + } options = getOptions(options); return await PromisePrototypeThen( binding.realpath(getValidatedPath(path), options.encoding, kUsePromises), @@ -1834,6 +1959,12 @@ async function realpath(path, options) { } async function mkdtemp(prefix, options) { + const h = vfsState.handlers; + if (h !== null) { + const promise = h.mkdtemp(prefix, options); + if (promise !== undefined) return await promise; + } + options = getOptions(options); prefix = getValidatedPath(prefix, 'prefix'); @@ -1886,10 +2017,18 @@ async function writeFile(path, data, options) { flag: 'w', flush: false, }); - const flag = options.flag || 'w'; const flush = options.flush ?? false; - validateBoolean(flush, 'options.flush'); + parseFileMode(options.mode, 'mode', 0o666); + + const h = vfsState.handlers; + if (h !== null) { + checkAborted(options.signal); + const promise = h.writeFile(path, data, options); + if (promise !== undefined) { await promise; return; } + } + + const flag = options.flag || 'w'; if (!isArrayBufferView(data) && !isCustomIterable(data)) { validateStringAfterArrayBufferView(data, 'data'); @@ -1918,12 +2057,26 @@ function isCustomIterable(obj) { async function appendFile(path, data, options) { options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); + parseFileMode(options.mode, 'mode', 0o666); + + const h = vfsState.handlers; + if (h !== null) { + checkAborted(options.signal); + const promise = h.appendFile(path, data, options); + if (promise !== undefined) { await promise; return; } + } options = copyObject(options); options.flag ||= 'a'; return writeFile(path, data, options); } async function readFile(path, options) { + const h = vfsState.handlers; + if (h !== null) { + checkAborted(options?.signal); + const result = h.readFile(path, options); + if (result !== undefined) return result; + } options = getOptions(options, { flag: 'r' }); const flag = options.flag || 'r'; @@ -1937,6 +2090,14 @@ async function readFile(path, options) { } async function* _watch(filename, options = kEmptyObject) { + const h = vfsState.handlers; + if (h !== null) { + const result = h.promisesWatch(filename, options); + if (result !== undefined) { + yield* result; + return; + } + } validateObject(options, 'options'); if (options.recursive != null) { @@ -1995,7 +2156,7 @@ module.exports = { writeFile, appendFile, readFile, - watch: !isMacOS && !isWindows ? _watch : watch, + watch: _watch, constants, }, diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index 811c52aeffb8b9..f15b63dc20a367 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -1048,6 +1048,11 @@ const validatePosition = hideStackFrames((position, name, length) => { } }); +// Shared VFS handler state for fs wrapping. +// When handlers is null, no VFS is active (zero overhead). +const vfsState = { __proto__: null, handlers: null }; +function setVfsHandlers(handlers) { vfsState.handlers = handlers; } + module.exports = { constants: { kIoMaxLength, @@ -1057,6 +1062,8 @@ module.exports = { kWriteFileMaxChunkSize, }, assertEncoding, + setVfsHandlers, + vfsState, BigIntStats, // for testing copyObject, Dirent, diff --git a/lib/internal/vfs/errors.js b/lib/internal/vfs/errors.js index 79e4a647d133b1..6af91c4bf7aca4 100644 --- a/lib/internal/vfs/errors.js +++ b/lib/internal/vfs/errors.js @@ -19,6 +19,7 @@ const { UV_EINVAL, UV_ELOOP, UV_EACCES, + UV_EXDEV, } = internalBinding('uv'); /** @@ -179,6 +180,16 @@ function createEACCES(syscall, path) { return err; } +function createEXDEV(syscall, path) { + const err = new UVException({ + errno: UV_EXDEV, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEXDEV); + return err; +} + module.exports = { createENOENT, createENOTDIR, @@ -190,4 +201,5 @@ module.exports = { createEINVAL, createELOOP, createEACCES, + createEXDEV, }; diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index c48478ee85aa6c..ae38639582581b 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -4,29 +4,58 @@ const { MathRandom, ObjectFreeze, Symbol, + SymbolDispose, } = primordials; +const { + codes: { + ERR_INVALID_STATE, + }, +} = require('internal/errors'); const { validateBoolean } = require('internal/validators'); const { MemoryProvider } = require('internal/vfs/providers/memory'); -const { posix: pathPosix } = require('path'); +const path = require('path'); +const { posix: pathPosix, isAbsolute, resolve: resolvePath } = path; const { join: joinPath } = pathPosix; +const { + isUnderMountPoint, + getRelativePath, +} = require('internal/vfs/router'); const { openVirtualFd, getVirtualFd, closeVirtualFd, } = require('internal/vfs/fd'); const { + createENOENT, createEBADF, createEISDIR, } = require('internal/vfs/errors'); const { VirtualReadStream, VirtualWriteStream } = require('internal/vfs/streams'); const { VirtualDir } = require('internal/vfs/dir'); const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); +let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => { + debug = fn; +}); // Private symbols const kProvider = Symbol('kProvider'); +const kMountPoint = Symbol('kMountPoint'); +const kMounted = Symbol('kMounted'); const kPromises = Symbol('kPromises'); +// Lazy-loaded VFS setup +let registerVFS; +let deregisterVFS; + +function loadVfsSetup() { + if (!registerVFS) { + const setup = require('internal/vfs/setup'); + registerVFS = setup.registerVFS; + deregisterVFS = setup.deregisterVFS; + } +} + /** * Virtual File System implementation using Provider architecture. * Wraps a Provider and exposes an fs-like API operating on @@ -62,6 +91,8 @@ class VirtualFileSystem { } this[kProvider] = provider ?? new MemoryProvider(); + this[kMountPoint] = null; + this[kMounted] = false; this[kPromises] = null; // Lazy-initialized } @@ -73,6 +104,22 @@ class VirtualFileSystem { return this[kProvider]; } + /** + * Gets the mount point path, or null if not mounted. + * @returns {string|null} + */ + get mountPoint() { + return this[kMountPoint]; + } + + /** + * Returns true if VFS is mounted. + * @returns {boolean} + */ + get mounted() { + return this[kMounted]; + } + /** * Returns true if the provider is read-only. * @returns {boolean} @@ -81,17 +128,91 @@ class VirtualFileSystem { return this[kProvider].readonly; } + // ==================== Mount ==================== + + /** + * Mounts the VFS at a specific path prefix. + * @param {string} prefix The mount point path + * @returns {VirtualFileSystem} The VFS instance for chaining + */ + mount(prefix) { + if (this[kMounted]) { + throw new ERR_INVALID_STATE('VFS is already mounted'); + } + this[kMountPoint] = resolvePath(prefix); + this[kMounted] = true; + debug('mount %s', this[kMountPoint]); + loadVfsSetup(); + registerVFS(this); + return this; + } + + /** + * Unmounts the VFS. + */ + unmount() { + debug('unmount %s', this[kMountPoint]); + loadVfsSetup(); + deregisterVFS(this); + this[kMountPoint] = null; + this[kMounted] = false; + } + + /** + * Disposes of the VFS by unmounting it. + * Supports the Explicit Resource Management proposal (using declaration). + */ + [SymbolDispose]() { + if (this[kMounted]) { + this.unmount(); + } + } + + /** + * Checks if a path should be handled by this VFS. + * @param {string} inputPath The path to check (must be absolute & normalized) + * @returns {boolean} + */ + shouldHandle(inputPath) { + if (!this[kMounted] || !this[kMountPoint]) { + return false; + } + const normalized = isAbsolute(inputPath) ? inputPath : resolvePath(inputPath); + return isUnderMountPoint(normalized, this[kMountPoint]); + } + // ==================== Path Resolution ==================== /** - * Normalizes a path to a provider-relative POSIX path. - * @param {string} inputPath The path to normalize + * Converts an absolute mounted path to a provider-relative POSIX path. + * If not mounted, treats the path as already provider-relative. + * @param {string} inputPath The path to convert * @returns {string} */ #toProviderPath(inputPath) { + if (this[kMounted] && this[kMountPoint]) { + const resolved = isAbsolute(inputPath) ? inputPath : resolvePath(inputPath); + if (!isUnderMountPoint(resolved, this[kMountPoint])) { + throw createENOENT('open', inputPath); + } + return getRelativePath(resolved, this[kMountPoint]); + } return pathPosix.normalize(inputPath); } + /** + * Converts a provider-relative path back to a mounted path. + * If not mounted, returns the path as-is. + * @param {string} providerPath The provider-relative path + * @returns {string} The mounted path + */ + #toMountedPath(providerPath) { + if (this[kMounted] && this[kMountPoint]) { + return path.join(this[kMountPoint], providerPath); + } + return providerPath; + } + // ==================== FS Operations (Sync) ==================== /** @@ -258,7 +379,8 @@ class VirtualFileSystem { */ realpathSync(filePath, options) { const providerPath = this.#toProviderPath(filePath); - return this[kProvider].realpathSync(providerPath, options); + const realProviderPath = this[kProvider].realpathSync(providerPath, options); + return this.#toMountedPath(realProviderPath); } /** @@ -409,7 +531,7 @@ class VirtualFileSystem { } const dirPath = providerPrefix + suffix; this[kProvider].mkdirSync(dirPath); - return dirPath; + return this.#toMountedPath(dirPath); } /** @@ -612,7 +734,8 @@ class VirtualFileSystem { } this[kProvider].realpath(this.#toProviderPath(filePath), options) - .then((realPath) => callback(null, realPath), (err) => callback(err)); + .then((realPath) => callback(null, this.#toMountedPath(realPath)), + (err) => callback(err)); } /** @@ -959,6 +1082,7 @@ class VirtualFileSystem { // Use arrow function to capture `this` for private method access const toProviderPath = (p) => this.#toProviderPath(p); + const toMountedPath = (p) => this.#toMountedPath(p); return ObjectFreeze({ async readFile(filePath, options) { @@ -1020,7 +1144,7 @@ class VirtualFileSystem { async realpath(filePath, options) { const providerPath = toProviderPath(filePath); - return provider.realpath(providerPath, options); + return toMountedPath(await provider.realpath(providerPath, options)); }, async readlink(linkPath, options) { @@ -1095,7 +1219,7 @@ class VirtualFileSystem { } const dirPath = providerPrefix + suffix; await provider.mkdir(dirPath); - return dirPath; + return toMountedPath(dirPath); }, async chmod(filePath, mode) { diff --git a/lib/internal/vfs/router.js b/lib/internal/vfs/router.js new file mode 100644 index 00000000000000..b610b271695a3d --- /dev/null +++ b/lib/internal/vfs/router.js @@ -0,0 +1,46 @@ +'use strict'; + +const { + ArrayPrototypeJoin, + StringPrototypeSplit, + StringPrototypeStartsWith, +} = primordials; + +const { isAbsolute, relative, sep } = require('path'); + +// `path.sep` is required here because on Windows `path.resolve('/virtual')` +// produces 'C:\virtual' and all resolved paths use backslashes - a hardcoded +// '/' check would never match. The trailing-separator guard handles root +// mount points like 'C:\' so we don't end up with 'C:\\'. +function isUnderMountPoint(normalizedPath, mountPoint) { + if (normalizedPath === mountPoint) { + return true; + } + if (mountPoint === '/') { + return StringPrototypeStartsWith(normalizedPath, '/'); + } + const prefix = mountPoint[mountPoint.length - 1] === sep ? + mountPoint : mountPoint + sep; + return StringPrototypeStartsWith(normalizedPath, prefix); +} + +// Returns a POSIX-style relative path the provider can consume. Uses +// `path.relative()` so Windows backslash paths are handled correctly, then +// re-joins with forward slashes for the provider's internal POSIX format. +function getRelativePath(normalizedPath, mountPoint) { + if (normalizedPath === mountPoint) { + return '/'; + } + if (mountPoint === '/') { + return normalizedPath; + } + const rel = relative(mountPoint, normalizedPath); + const segments = StringPrototypeSplit(rel, sep); + return '/' + ArrayPrototypeJoin(segments, '/'); +} + +module.exports = { + isUnderMountPoint, + getRelativePath, + isAbsolutePath: isAbsolute, +}; diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js new file mode 100644 index 00000000000000..fe06f578b2f6ca --- /dev/null +++ b/lib/internal/vfs/setup.js @@ -0,0 +1,661 @@ +'use strict'; + +const { + ArrayPrototypeIndexOf, + ArrayPrototypePush, + ArrayPrototypeSplice, + PromiseResolve, + StringPrototypeStartsWith, +} = primordials; + +const { Buffer } = require('buffer'); +const { resolve, sep } = require('path'); +const { fileURLToPath, URL } = require('internal/url'); +const { kEmptyObject } = require('internal/util'); +const { validateObject } = require('internal/validators'); +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_STATE, + }, +} = require('internal/errors'); +const { createENOENT, createEXDEV } = require('internal/vfs/errors'); +const { getVirtualFd, closeVirtualFd } = require('internal/vfs/fd'); +const { assertEncoding, vfsState, setVfsHandlers } = require('internal/fs/utils'); +const permission = require('internal/process/permission'); +const { getOptionValue } = require('internal/options'); +let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => { + debug = fn; +}); + +function toPathStr(pathOrUrl) { + if (typeof pathOrUrl === 'string') return pathOrUrl; + if (pathOrUrl instanceof URL) return fileURLToPath(pathOrUrl); + if (Buffer.isBuffer(pathOrUrl)) return pathOrUrl.toString(); + return null; +} + +function noopFdSync(fd) { + if (getVirtualFd(fd)) return true; + return undefined; +} + +const noopFdPromise = PromiseResolve(true); +function noopFd(fd) { + if (getVirtualFd(fd)) return noopFdPromise; + return undefined; +} + +// Registry of active VFS instances. +const activeVFSList = []; + +let hooksInstalled = false; +let vfsHandlerObj; + +function registerVFS(vfs) { + if (permission.isEnabled() && !getOptionValue('--allow-fs-vfs')) { + throw new ERR_INVALID_STATE( + 'VFS cannot be used when the permission model is enabled. ' + + 'Use --allow-fs-vfs to allow it.', + ); + } + if (ArrayPrototypeIndexOf(activeVFSList, vfs) !== -1) return; + + const newMount = vfs.mountPoint; + if (newMount != null) { + for (let i = 0; i < activeVFSList.length; i++) { + const existingMount = activeVFSList[i].mountPoint; + if (existingMount == null) continue; + // Use path.sep so the trailing-separator guard works on Windows where + // mountPoint values are resolved to drive-letter / backslash paths. + const newPrefix = newMount === sep ? sep : newMount + sep; + const existingPrefix = existingMount === sep ? sep : existingMount + sep; + if (newMount === existingMount || + StringPrototypeStartsWith(newMount, existingPrefix) || + StringPrototypeStartsWith(existingMount, newPrefix)) { + throw new ERR_INVALID_STATE( + `VFS mount '${newMount}' overlaps with existing mount '${existingMount}'`, + ); + } + } + } + ArrayPrototypePush(activeVFSList, vfs); + debug('register mount=%s active=%d', newMount, activeVFSList.length); + if (!hooksInstalled) { + vfsHandlerObj = createVfsHandlers(); + setVfsHandlers(vfsHandlerObj); + hooksInstalled = true; + } else if (vfsState.handlers === null) { + setVfsHandlers(vfsHandlerObj); + } +} + +function deregisterVFS(vfs) { + const index = ArrayPrototypeIndexOf(activeVFSList, vfs); + if (index === -1) return; + ArrayPrototypeSplice(activeVFSList, index, 1); + debug('deregister active=%d', activeVFSList.length); + if (activeVFSList.length === 0) { + setVfsHandlers(null); + } +} + +function findVFSForExists(filename) { + const normalized = resolve(filename); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + return { vfs, exists: vfs.existsSync(normalized) }; + } + } + return null; +} + +function findVFSForPath(filename) { + const normalized = resolve(filename); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + return { vfs, normalized }; + } + } + return null; +} + +// Sync read: check exists first, fall through to ENOENT for mounted VFS. +function findVFSWith(filename, syscall, fn) { + const normalized = resolve(filename); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + if (vfs.existsSync(normalized)) { + return fn(vfs, normalized); + } + throw createENOENT(syscall, filename); + } + } + return undefined; +} + +function vfsRead(path, syscall, fn) { + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + return findVFSWith(pathStr, syscall, fn); +} + +function vfsOp(path, fn) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) return fn(r.vfs, r.normalized); + } + return undefined; +} + +function vfsOpVoid(path, fn) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) { fn(r.vfs, r.normalized); return true; } + } + return undefined; +} + +function checkSameVFS(srcPath, destPath, syscall, srcVfs) { + const destNormalized = resolve(destPath); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(destNormalized)) { + if (vfs !== srcVfs) { + throw createEXDEV(syscall, srcPath); + } + return; + } + } + throw createEXDEV(syscall, srcPath); +} + +function createVfsHandlers() { + return { + __proto__: null, + + // ==================== Sync path-based read ops ==================== + + existsSync(path) { + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + const r = findVFSForExists(pathStr); + return r !== null ? r.exists : undefined; + }, + readFileSync(path, options) { + if (typeof path === 'number') { + const vfd = getVirtualFd(path); + if (vfd) { + const enc = typeof options === 'string' ? options : options?.encoding; + if (enc && enc !== 'buffer') assertEncoding(enc); + return vfd.entry.readFileSync(options); + } + return undefined; + } + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + const enc = typeof options === 'string' ? options : options?.encoding; + if (enc && enc !== 'buffer') assertEncoding(enc); + return findVFSWith(pathStr, 'open', (vfs, n) => vfs.readFileSync(n, options)); + }, + readdirSync(path, options) { + const result = vfsRead(path, 'scandir', (vfs, n) => vfs.readdirSync(n, options)); + if (result !== undefined && options?.encoding === 'buffer' && !options?.withFileTypes) { + for (let i = 0; i < result.length; i++) { + if (typeof result[i] === 'string') result[i] = Buffer.from(result[i]); + } + } + return result; + }, + lstatSync(path, options) { + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + const normalized = resolve(pathStr); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + try { + return vfs.lstatSync(normalized, options); + } catch (e) { + if (e?.code === 'ENOENT' && options?.throwIfNoEntry === false) return undefined; + throw e; + } + } + } + return undefined; + }, + statSync(path, options) { + try { + return vfsRead(path, 'stat', (vfs, n) => vfs.statSync(n, options)); + } catch (err) { + if (err?.code === 'ENOENT' && options?.throwIfNoEntry === false) return undefined; + throw err; + } + }, + realpathSync(path, options) { + const result = vfsRead(path, 'realpath', (vfs, n) => vfs.realpathSync(n)); + if (result !== undefined && options?.encoding === 'buffer') { + return Buffer.from(result); + } + return result; + }, + accessSync(path, mode) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) { + if (mode != null && typeof mode !== 'number') { + throw new ERR_INVALID_ARG_TYPE('mode', 'integer', mode); + } + r.vfs.accessSync(r.normalized, mode); + return true; + } + } + return undefined; + }, + readlinkSync(path, options) { + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + const normalized = resolve(pathStr); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + const result = vfs.readlinkSync(normalized, options); + if (options?.encoding === 'buffer') return Buffer.from(result); + return result; + } + } + return undefined; + }, + statfsSync(path, options) { + const pathStr = toPathStr(path); + if (pathStr !== null && findVFSForPath(pathStr) !== null) { + if (options?.bigint) { + return { + type: 0n, bsize: 4096n, blocks: 0n, + bfree: 0n, bavail: 0n, files: 0n, ffree: 0n, + }; + } + return { type: 0, bsize: 4096, blocks: 0, bfree: 0, bavail: 0, files: 0, ffree: 0 }; + } + return undefined; + }, + + // ==================== Sync path-based write ops ==================== + + writeFileSync: (path, data, options) => + vfsOpVoid(path, (vfs, n) => vfs.writeFileSync(n, data, options)), + appendFileSync: (path, data, options) => + vfsOpVoid(path, (vfs, n) => vfs.appendFileSync(n, data, options)), + mkdirSync: (path, options) => + vfsOp(path, (vfs, n) => ({ result: vfs.mkdirSync(n, options) })), + rmdirSync: (path) => vfsOpVoid(path, (vfs, n) => vfs.rmdirSync(n)), + rmSync: (path, options) => vfsOpVoid(path, (vfs, n) => vfs.rmSync(n, options)), + unlinkSync: (path) => vfsOpVoid(path, (vfs, n) => vfs.unlinkSync(n)), + renameSync(oldPath, newPath) { + return vfsOpVoid(oldPath, (vfs, n) => { + checkSameVFS(n, toPathStr(newPath), 'rename', vfs); + vfs.renameSync(n, resolve(toPathStr(newPath))); + }); + }, + copyFileSync(src, dest, mode) { + return vfsOpVoid(src, (vfs, n) => { + checkSameVFS(n, toPathStr(dest), 'copyfile', vfs); + vfs.copyFileSync(n, resolve(toPathStr(dest)), mode); + }); + }, + symlinkSync: (target, path, type) => + vfsOpVoid(path, (vfs, n) => vfs.symlinkSync(target, n, type)), + chmodSync: (path, mode) => vfsOpVoid(path, (vfs, n) => vfs.chmodSync(n, mode)), + chownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.chownSync(n, uid, gid)), + lchownSync: (path, uid, gid) => vfsOpVoid(path, (vfs, n) => vfs.chownSync(n, uid, gid)), + utimesSync: (path, atime, mtime) => + vfsOpVoid(path, (vfs, n) => vfs.utimesSync(n, atime, mtime)), + lutimesSync: (path, atime, mtime) => + vfsOpVoid(path, (vfs, n) => vfs.lutimesSync(n, atime, mtime)), + truncateSync: (path, len) => vfsOpVoid(path, (vfs, n) => vfs.truncateSync(n, len)), + linkSync(existingPath, newPath) { + return vfsOpVoid(existingPath, (vfs, n) => { + checkSameVFS(n, toPathStr(newPath), 'link', vfs); + vfs.linkSync(n, resolve(toPathStr(newPath))); + }); + }, + mkdtempSync(prefix, options) { + const result = vfsOp(prefix, (vfs, n) => vfs.mkdtempSync(n)); + if (result !== undefined && options?.encoding === 'buffer') { + return Buffer.from(result); + } + return result; + }, + opendirSync: (path, options) => vfsOp(path, (vfs, n) => vfs.opendirSync(n, options)), + openAsBlob(path, options) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const normalized = resolve(pathStr); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized) && vfs.existsSync(normalized)) { + return vfs.openAsBlob(normalized, options); + } + } + } + return undefined; + }, + + // ==================== Sync FD-based ops ==================== + + openSync: (path, flags, mode) => vfsOp(path, (vfs, n) => vfs.openSync(n, flags, mode)), + closeSync(fd) { + const vfd = getVirtualFd(fd); + if (vfd) { vfd.entry.closeSync(); closeVirtualFd(fd); return true; } + return undefined; + }, + readSync(fd, buffer, offset, length, position) { + const vfd = getVirtualFd(fd); + if (vfd) return vfd.entry.readSync(buffer, offset, length, position); + return undefined; + }, + writeSync(fd, buffer, offset, length, position) { + const vfd = getVirtualFd(fd); + if (vfd) return vfd.entry.writeSync(buffer, offset, length, position); + return undefined; + }, + fstatSync(fd, options) { + const vfd = getVirtualFd(fd); + if (vfd) return vfd.entry.statSync(options); + return undefined; + }, + ftruncateSync(fd, len) { + const vfd = getVirtualFd(fd); + if (vfd) { vfd.entry.truncateSync(len); return true; } + return undefined; + }, + fchmodSync: noopFdSync, + fchownSync: noopFdSync, + futimesSync: noopFdSync, + fdatasyncSync: noopFdSync, + fsyncSync: noopFdSync, + readvSync(fd, buffers, position) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + let totalRead = 0; + for (let i = 0; i < buffers.length; i++) { + const buf = buffers[i]; + const pos = position != null ? position + totalRead : position; + const bytesRead = vfd.entry.readSync(buf, 0, buf.byteLength, pos); + totalRead += bytesRead; + if (bytesRead < buf.byteLength) break; + } + return totalRead; + }, + writevSync(fd, buffers, position) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + let totalWritten = 0; + for (let i = 0; i < buffers.length; i++) { + const buf = buffers[i]; + const pos = position != null ? position + totalWritten : position; + const bytesWritten = vfd.entry.writeSync(buf, 0, buf.byteLength, pos); + totalWritten += bytesWritten; + if (bytesWritten < buf.byteLength) break; + } + return totalWritten; + }, + + // ==================== Async FD-based ops ==================== + + close(fd) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.close().then(() => { closeVirtualFd(fd); return true; }); + }, + read(fd, buffer, offset, length, position) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.read(buffer, offset, length, position) + .then(({ bytesRead }) => bytesRead); + }, + write(fd, buffer, offset, length, position) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.write(buffer, offset, length, position) + .then(({ bytesWritten }) => bytesWritten); + }, + fstat(fd, options) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.stat(options); + }, + ftruncate(fd, len) { + const vfd = getVirtualFd(fd); + if (!vfd) return undefined; + return vfd.entry.truncate(len).then(() => true); + }, + fchmod: noopFd, + fchown: noopFd, + futimes: noopFd, + fdatasync: noopFd, + fsync: noopFd, + + // ==================== Stream ops ==================== + + createReadStream(path, options) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) return r.vfs.createReadStream(r.normalized, options); + } + return undefined; + }, + createWriteStream(path, options) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) return r.vfs.createWriteStream(r.normalized, options); + } + return undefined; + }, + + // ==================== Watch ops ==================== + + watch(filename, options, listener) { + if (typeof options === 'function') { + listener = options; + options = kEmptyObject; + } else if (options != null) { + validateObject(options, 'options'); + } else { + options = kEmptyObject; + } + const pathStr = toPathStr(filename); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) return r.vfs.watch(pathStr, options, listener); + } + return undefined; + }, + + // ==================== Async path-based ops ==================== + + readdir(path, options) { + const promise = vfsOp(path, (vfs, n) => vfs.promises.readdir(n, options)); + if (promise !== undefined && options?.encoding === 'buffer' && !options?.withFileTypes) { + return promise.then((result) => { + for (let i = 0; i < result.length; i++) { + if (typeof result[i] === 'string') result[i] = Buffer.from(result[i]); + } + return result; + }); + } + return promise; + }, + lstat(path, options) { + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + const normalized = resolve(pathStr); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + return vfs.promises.lstat(normalized, options); + } + } + return undefined; + }, + stat(path, options) { + const promise = vfsOp(path, (vfs, n) => vfs.promises.stat(n, options)); + if (promise !== undefined && options?.throwIfNoEntry === false) { + return promise.catch((err) => { + if (err?.code === 'ENOENT') return undefined; + throw err; + }); + } + return promise; + }, + readFile(path, options) { + if (typeof path === 'number') { + const vfd = getVirtualFd(path); + if (vfd) { + const enc = typeof options === 'string' ? options : options?.encoding; + if (enc && enc !== 'buffer') assertEncoding(enc); + return vfd.entry.readFile(options); + } + return undefined; + } + const enc = typeof options === 'string' ? options : options?.encoding; + if (enc && enc !== 'buffer') assertEncoding(enc); + return vfsOp(path, (vfs, n) => vfs.promises.readFile(n, options)); + }, + realpath(path, options) { + const promise = vfsOp(path, (vfs, n) => vfs.promises.realpath(n, options)); + if (promise !== undefined && options?.encoding === 'buffer') { + return promise.then((result) => Buffer.from(result)); + } + return promise; + }, + access(path, mode) { + return vfsOp(path, (vfs, n) => { + if (mode != null && typeof mode !== 'number') { + throw new ERR_INVALID_ARG_TYPE('mode', 'integer', mode); + } + return vfs.promises.access(n, mode).then(() => true); + }); + }, + readlink(path, options) { + const pathStr = toPathStr(path); + if (pathStr === null) return undefined; + const normalized = resolve(pathStr); + for (let i = 0; i < activeVFSList.length; i++) { + const vfs = activeVFSList[i]; + if (vfs.shouldHandle(normalized)) { + const promise = vfs.promises.readlink(normalized, options); + if (options?.encoding === 'buffer') { + return promise.then((result) => Buffer.from(result)); + } + return promise; + } + } + return undefined; + }, + chown: (path, uid, gid) => + vfsOp(path, (vfs, n) => vfs.promises.chown(n, uid, gid).then(() => true)), + lchown: (path, uid, gid) => + vfsOp(path, (vfs, n) => vfs.promises.lchown(n, uid, gid).then(() => true)), + lutimes: (path, atime, mtime) => + vfsOp(path, (vfs, n) => vfs.promises.lutimes(n, atime, mtime).then(() => true)), + statfs(path, options) { + const pathStr = toPathStr(path); + if (pathStr !== null && findVFSForPath(pathStr) !== null) { + if (options?.bigint) { + return { + __proto__: null, + type: 0n, bsize: 4096n, blocks: 0n, + bfree: 0n, bavail: 0n, files: 0n, ffree: 0n, + }; + } + return { + __proto__: null, + type: 0, bsize: 4096, blocks: 0, + bfree: 0, bavail: 0, files: 0, ffree: 0, + }; + } + return undefined; + }, + writeFile(path, data, options) { + return vfsOp(path, (vfs, n) => vfs.promises.writeFile(n, data, options).then(() => true)); + }, + appendFile(path, data, options) { + return vfsOp(path, (vfs, n) => vfs.promises.appendFile(n, data, options).then(() => true)); + }, + mkdir(path, options) { + return vfsOp(path, (vfs, n) => + vfs.promises.mkdir(n, options).then((result) => ({ __proto__: null, result }))); + }, + rmdir: (path) => vfsOp(path, (vfs, n) => vfs.promises.rmdir(n).then(() => true)), + rm: (path, options) => vfsOp(path, (vfs, n) => vfs.promises.rm(n, options).then(() => true)), + unlink: (path) => vfsOp(path, (vfs, n) => vfs.promises.unlink(n).then(() => true)), + rename(oldPath, newPath) { + return vfsOp(oldPath, (vfs, n) => { + checkSameVFS(n, toPathStr(newPath), 'rename', vfs); + return vfs.promises.rename(n, resolve(toPathStr(newPath))).then(() => true); + }); + }, + copyFile(src, dest, mode) { + return vfsOp(src, (vfs, n) => { + checkSameVFS(n, toPathStr(dest), 'copyfile', vfs); + return vfs.promises.copyFile(n, resolve(toPathStr(dest)), mode).then(() => true); + }); + }, + symlink(target, path, type) { + return vfsOp(path, (vfs, n) => vfs.promises.symlink(target, n, type).then(() => true)); + }, + truncate: (path, len) => + vfsOp(path, (vfs, n) => vfs.promises.truncate(n, len).then(() => true)), + link(existingPath, newPath) { + return vfsOp(existingPath, (vfs, n) => { + checkSameVFS(n, toPathStr(newPath), 'link', vfs); + return vfs.promises.link(n, resolve(toPathStr(newPath))).then(() => true); + }); + }, + mkdtemp(prefix, options) { + const promise = vfsOp(prefix, (vfs, n) => vfs.promises.mkdtemp(n)); + if (promise !== undefined && options?.encoding === 'buffer') { + return promise.then((result) => Buffer.from(result)); + } + return promise; + }, + chmod: (path, mode) => + vfsOp(path, (vfs, n) => vfs.promises.chmod(n, mode).then(() => true)), + utimes: (path, atime, mtime) => + vfsOp(path, (vfs, n) => vfs.promises.utimes(n, atime, mtime).then(() => true)), + open(path, flags, mode) { + // openSync is synchronous, so an error thrown by the provider would + // escape via fs.open's caller (instead of going through the callback). + // Catch it here and surface as a rejected promise. + return vfsOp(path, async (vfs, n) => vfs.openSync(n, flags, mode)); + }, + promisesOpen(path, flags, mode) { + const pathStr = toPathStr(path); + if (pathStr !== null) { + const r = findVFSForPath(pathStr); + if (r !== null) { + const fd = r.vfs.openSync(r.normalized, flags, mode); + const vfd = getVirtualFd(fd); + return PromiseResolve(vfd.entry); + } + } + return undefined; + }, + lchmod: (path, mode) => + vfsOp(path, (vfs, n) => vfs.promises.lchmod(n, mode).then(() => true)), + }; +} + +module.exports = { + registerVFS, + deregisterVFS, +}; diff --git a/test/parallel/test-vfs-destructuring.js b/test/parallel/test-vfs-destructuring.js new file mode 100644 index 00000000000000..11422cf4eda89d --- /dev/null +++ b/test/parallel/test-vfs-destructuring.js @@ -0,0 +1,77 @@ +// Flags: --experimental-vfs +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const vfs = require('node:vfs'); + +// Destructure fs methods BEFORE mounting any VFS. Because the guards are +// inside each fs method body (not done via monkey-patching), these captured +// references must still route through VFS once a mount is created. +const { + readFileSync, + existsSync, + statSync, + lstatSync, + readdirSync, + realpathSync, +} = require('fs'); + +// path.resolve here so the mount point and the assertion targets are in the +// platform's native form (e.g. 'D:\vfs_destr' on Windows). VirtualFileSystem +// stores the mount point via path.resolve internally, so we mirror that. +const MOUNT = path.resolve('/vfs_destr'); +const FILE = path.join(MOUNT, 'file.txt'); + +const myVfs = vfs.create(); +myVfs.mkdirSync('/sub', { recursive: true }); +myVfs.writeFileSync('/file.txt', 'hello from vfs'); +myVfs.writeFileSync('/sub/nested.txt', 'nested content'); +myVfs.mount(MOUNT); + +{ + const content = readFileSync(FILE, 'utf8'); + assert.strictEqual(content, 'hello from vfs'); +} + +{ + assert.strictEqual(existsSync(FILE), true); + assert.strictEqual(existsSync(path.join(MOUNT, 'nonexistent')), false); +} + +{ + const stats = statSync(FILE); + assert.strictEqual(stats.isFile(), true); + assert.strictEqual(stats.isDirectory(), false); +} + +{ + const stats = lstatSync(FILE); + assert.strictEqual(stats.isFile(), true); +} + +{ + const entries = readdirSync(MOUNT); + assert.ok(entries.includes('file.txt')); + assert.ok(entries.includes('sub')); +} + +{ + const real = realpathSync(FILE); + assert.strictEqual(real, FILE); +} + +const { readdir, lstat } = require('fs/promises'); + +async function testPromises() { + const entries = await readdir(MOUNT); + assert.ok(entries.includes('file.txt')); + + const stats = await lstat(FILE); + assert.strictEqual(stats.isFile(), true); +} + +testPromises().then(common.mustCall(() => { + myVfs.unmount(); +})); diff --git a/test/parallel/test-vfs-fs-accessSync.js b/test/parallel/test-vfs-fs-accessSync.js new file mode 100644 index 00000000000000..a05bfd13282306 --- /dev/null +++ b/test/parallel/test-vfs-fs-accessSync.js @@ -0,0 +1,26 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.accessSync dispatches to VFS; missing paths throw ENOENT. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-accessSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +// Existing path succeeds +fs.accessSync(path.join(mountPoint, 'src/hello.txt')); +fs.accessSync(path.join(mountPoint, 'src/hello.txt'), fs.constants.F_OK); + +// Missing path throws ENOENT +assert.throws(() => fs.accessSync(path.join(mountPoint, 'missing')), + { code: 'ENOENT' }); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-callback-error-paths.js b/test/parallel/test-vfs-fs-callback-error-paths.js new file mode 100644 index 00000000000000..8a9bf95980e59a --- /dev/null +++ b/test/parallel/test-vfs-fs-callback-error-paths.js @@ -0,0 +1,75 @@ +// Flags: --experimental-vfs +'use strict'; + +// VFS dispatch on the async callback fs methods must surface provider errors +// through the callback, not as a synchronous throw or unhandled rejection. + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-cb-err-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// fs.access on missing file inside a mount +{ + const { mountPoint } = mounted(); + fs.access(path.join(mountPoint, 'missing'), + common.expectsError({ code: 'ENOENT' })); +} + +// fs.lstat on missing file inside a mount. +// lstat passes (err) only on the error path, so expectsError works here. +{ + const { mountPoint } = mounted(); + fs.lstat(path.join(mountPoint, 'missing'), + common.expectsError({ code: 'ENOENT' })); +} + +// fs.open on a path whose parent directory does not exist +{ + const { mountPoint } = mounted(); + fs.open(path.join(mountPoint, 'missing-parent/x.txt'), 'wx', + common.expectsError({ code: 'ENOENT' })); +} + +// fs.read on a VFS fd that has been closed -> EBADF through callback. +// fs.read invokes the callback with (err, bytesRead, buffer), so the +// single-argument expectsError contract does not match - use mustCall here. +{ + const { mountPoint } = mounted(); + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r'); + fs.closeSync(fd); + fs.read(fd, Buffer.alloc(5), 0, 5, 0, common.mustCall((err) => { + common.expectsError({ code: 'EBADF' })(err); + })); +} + +// fs.write on a VFS fd that has been closed -> EBADF through callback. +// fs.write invokes the callback with (err, bytesWritten, buffer); same +// rationale as fs.read above. +{ + const { mountPoint } = mounted(); + const fd = fs.openSync(path.join(mountPoint, 'src/w.txt'), 'w'); + fs.closeSync(fd); + fs.write(fd, Buffer.from('x'), 0, 1, 0, common.mustCall((err) => { + common.expectsError({ code: 'EBADF' })(err); + })); +} + +// fs.fstat on a VFS fd that has been closed -> EBADF through callback +{ + const { mountPoint } = mounted(); + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r'); + fs.closeSync(fd); + fs.fstat(fd, common.expectsError({ code: 'EBADF' })); +} diff --git a/test/parallel/test-vfs-fs-chmod-callback.js b/test/parallel/test-vfs-fs-chmod-callback.js new file mode 100644 index 00000000000000..72523a3bd7e831 --- /dev/null +++ b/test/parallel/test-vfs-fs-chmod-callback.js @@ -0,0 +1,33 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.chmod, fs.chown, fs.lchown, fs.utimes, and fs.lutimes callbacks dispatch +// through VFS. + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-chmod-cb-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +const target = path.join(mountPoint, 'src/hello.txt'); +const uid = process.getuid?.() ?? 0; +const gid = process.getgid?.() ?? 0; +const now = new Date(); + +fs.chmod(target, 0o644, common.mustSucceed(() => { + fs.chown(target, uid, gid, common.mustSucceed(() => { + fs.lchown(target, uid, gid, common.mustSucceed(() => { + fs.utimes(target, now, now, common.mustSucceed(() => { + fs.lutimes(target, now, now, common.mustSucceed(() => { + myVfs.unmount(); + })); + })); + })); + })); +})); diff --git a/test/parallel/test-vfs-fs-chmodSync.js b/test/parallel/test-vfs-fs-chmodSync.js new file mode 100644 index 00000000000000..f4403d3708726c --- /dev/null +++ b/test/parallel/test-vfs-fs-chmodSync.js @@ -0,0 +1,30 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.chmodSync, fs.chownSync, fs.lchownSync, fs.utimesSync, and +// fs.lutimesSync dispatch to VFS. The MemoryProvider accepts these calls as +// metadata mutations without throwing. + +require('../common'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-chmodSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +const target = path.join(mountPoint, 'src/hello.txt'); +const uid = process.getuid?.() ?? 0; +const gid = process.getgid?.() ?? 0; +const now = new Date(); + +fs.chmodSync(target, 0o644); +fs.chownSync(target, uid, gid); +fs.lchownSync(target, uid, gid); +fs.utimesSync(target, now, now); +fs.lutimesSync(target, now, now); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-copyFileSync.js b/test/parallel/test-vfs-fs-copyFileSync.js new file mode 100644 index 00000000000000..9f97421329c541 --- /dev/null +++ b/test/parallel/test-vfs-fs-copyFileSync.js @@ -0,0 +1,27 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.copyFileSync dispatches to VFS when both paths are within the same mount. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-copyFileSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +fs.copyFileSync( + path.join(mountPoint, 'src/hello.txt'), + path.join(mountPoint, 'src/copy.txt'), +); +assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/copy.txt'), 'utf8'), + 'hello world', +); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-createReadStream.js b/test/parallel/test-vfs-fs-createReadStream.js new file mode 100644 index 00000000000000..08303a361e1fb8 --- /dev/null +++ b/test/parallel/test-vfs-fs-createReadStream.js @@ -0,0 +1,59 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.createReadStream dispatches through VFS, including the emitted 'open' +// event with the bitmask-encoded virtual fd. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-createReadStream-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// Whole-file read +{ + const { myVfs, mountPoint } = mounted(); + const chunks = []; + const stream = fs.createReadStream(path.join(mountPoint, 'src/hello.txt')); + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', common.mustCall(() => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world'); + myVfs.unmount(); + })); +} + +// Slice with start + end (inclusive) +{ + const { myVfs, mountPoint } = mounted(); + const chunks = []; + const stream = fs.createReadStream(path.join(mountPoint, 'src/hello.txt'), + { start: 0, end: 4 }); + assert.strictEqual(stream.path, path.join(mountPoint, 'src/hello.txt')); + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', common.mustCall(() => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello'); + myVfs.unmount(); + })); +} + +// 'open' event fires with a VFS fd +{ + const { myVfs, mountPoint } = mounted(); + const stream = fs.createReadStream(path.join(mountPoint, 'src/hello.txt')); + stream.on('open', common.mustCall((fd) => { + assert.notStrictEqual(fd & 0x40000000, 0); + })); + stream.on('end', common.mustCall(() => myVfs.unmount())); + stream.resume(); +} diff --git a/test/parallel/test-vfs-fs-createWriteStream.js b/test/parallel/test-vfs-fs-createWriteStream.js new file mode 100644 index 00000000000000..42660acb7f63b7 --- /dev/null +++ b/test/parallel/test-vfs-fs-createWriteStream.js @@ -0,0 +1,47 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.createWriteStream dispatches through VFS, exposes a `path` property and +// emits an 'open' event with the bitmask-encoded virtual fd. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve( + '/tmp/vfs-createWriteStream-' + process.pid, +); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// Basic write +{ + const { myVfs, mountPoint } = mounted(); + const target = path.join(mountPoint, 'src/sw.txt'); + const stream = fs.createWriteStream(target); + stream.write('stream '); + stream.end('data', common.mustCall(() => { + assert.strictEqual(fs.readFileSync(target, 'utf8'), 'stream data'); + myVfs.unmount(); + })); +} + +// Path getter + 'open' event with a VFS fd +{ + const { myVfs, mountPoint } = mounted(); + const target = path.join(mountPoint, 'src/ws-open.txt'); + const stream = fs.createWriteStream(target); + assert.strictEqual(stream.path, target); + stream.on('open', common.mustCall((fd) => { + assert.notStrictEqual(fd & 0x40000000, 0); + })); + stream.end('done', common.mustCall(() => myVfs.unmount())); +} diff --git a/test/parallel/test-vfs-fs-existsSync.js b/test/parallel/test-vfs-fs-existsSync.js new file mode 100644 index 00000000000000..190304a154ca42 --- /dev/null +++ b/test/parallel/test-vfs-fs-existsSync.js @@ -0,0 +1,22 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.existsSync dispatches to VFS for paths under a mount. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-existsSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), true); +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src')), true); +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'missing')), false); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-fchmod-callback.js b/test/parallel/test-vfs-fs-fchmod-callback.js new file mode 100644 index 00000000000000..fa55c934f36f4b --- /dev/null +++ b/test/parallel/test-vfs-fs-fchmod-callback.js @@ -0,0 +1,34 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.fchmod, fs.fchown, fs.futimes, fs.fdatasync, and fs.fsync callbacks +// short-circuit through VFS as no-ops on virtual fds. + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-fchmod-cb-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r+'); +const uid = process.getuid?.() ?? 0; +const gid = process.getgid?.() ?? 0; +const now = new Date(); + +fs.fchmod(fd, 0o644, common.mustSucceed(() => { + fs.fchown(fd, uid, gid, common.mustSucceed(() => { + fs.futimes(fd, now, now, common.mustSucceed(() => { + fs.fdatasync(fd, common.mustSucceed(() => { + fs.fsync(fd, common.mustSucceed(() => { + fs.closeSync(fd); + myVfs.unmount(); + })); + })); + })); + })); +})); diff --git a/test/parallel/test-vfs-fs-linkSync.js b/test/parallel/test-vfs-fs-linkSync.js new file mode 100644 index 00000000000000..f8090b24720d92 --- /dev/null +++ b/test/parallel/test-vfs-fs-linkSync.js @@ -0,0 +1,27 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.linkSync dispatches to VFS for hard links within the same mount. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-linkSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +fs.linkSync( + path.join(mountPoint, 'src/hello.txt'), + path.join(mountPoint, 'src/hello-link.txt'), +); +assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello-link.txt'), 'utf8'), + 'hello world', +); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-mkdir-callback.js b/test/parallel/test-vfs-fs-mkdir-callback.js new file mode 100644 index 00000000000000..e354cc94d01b98 --- /dev/null +++ b/test/parallel/test-vfs-fs-mkdir-callback.js @@ -0,0 +1,66 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.mkdir, fs.rmdir, fs.rm, and fs.unlink callbacks dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-mkdir-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// mkdir (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.mkdir(path.join(mountPoint, 'src/cb-d'), common.mustSucceed(() => { + assert.strictEqual( + fs.statSync(path.join(mountPoint, 'src/cb-d')).isDirectory(), true, + ); + myVfs.unmount(); + })); +} + +// rmdir (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.mkdirSync(path.join(mountPoint, 'src/empty')); + fs.rmdir(path.join(mountPoint, 'src/empty'), common.mustSucceed(() => { + assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/empty')), + false); + myVfs.unmount(); + })); +} + +// rm (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.rm(path.join(mountPoint, 'src/hello.txt'), + common.mustSucceed(() => { + assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), + false); + myVfs.unmount(); + })); +} + +// unlink (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.unlink(path.join(mountPoint, 'src/hello.txt'), + common.mustSucceed(() => { + assert.strictEqual( + fs.existsSync(path.join(mountPoint, 'src/hello.txt')), false, + ); + myVfs.unmount(); + })); +} diff --git a/test/parallel/test-vfs-fs-mkdirSync.js b/test/parallel/test-vfs-fs-mkdirSync.js new file mode 100644 index 00000000000000..de3959a06e3b79 --- /dev/null +++ b/test/parallel/test-vfs-fs-mkdirSync.js @@ -0,0 +1,31 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.mkdirSync dispatches to VFS, including the `recursive: true` form. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-mkdirSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.mount(mountPoint); + +// Plain mkdir +fs.mkdirSync(path.join(mountPoint, 'src/d1')); +assert.strictEqual( + fs.statSync(path.join(mountPoint, 'src/d1')).isDirectory(), true, +); + +// Recursive mkdir creates intermediate directories and returns the first one +const created = fs.mkdirSync(path.join(mountPoint, 'src/a/b/c'), + { recursive: true }); +assert.ok(created !== undefined); +assert.strictEqual( + fs.statSync(path.join(mountPoint, 'src/a/b/c')).isDirectory(), true, +); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-mkdtempSync.js b/test/parallel/test-vfs-fs-mkdtempSync.js new file mode 100644 index 00000000000000..ba837b716cb6ca --- /dev/null +++ b/test/parallel/test-vfs-fs-mkdtempSync.js @@ -0,0 +1,34 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.mkdtempSync dispatches to VFS and returns a mount-rooted path, including +// the buffer-encoding variant. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-mkdtempSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.mount(mountPoint); + +const prefix = path.join(mountPoint, 'src/tmp-'); + +// String result +{ + const dir = fs.mkdtempSync(prefix); + assert.ok(dir.startsWith(prefix)); + assert.strictEqual(dir.length, prefix.length + 6); + assert.strictEqual(fs.statSync(dir).isDirectory(), true); +} + +// Buffer result +{ + const dir = fs.mkdtempSync(prefix, { encoding: 'buffer' }); + assert.ok(Buffer.isBuffer(dir)); +} + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-open-callback.js b/test/parallel/test-vfs-fs-open-callback.js new file mode 100644 index 00000000000000..b1cacb11d921d2 --- /dev/null +++ b/test/parallel/test-vfs-fs-open-callback.js @@ -0,0 +1,77 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.open / fs.fstat / fs.read / fs.write / fs.close / fs.ftruncate callbacks +// dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-open-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// Open + fstat + read + close +{ + const { myVfs, mountPoint } = mounted(); + fs.open(path.join(mountPoint, 'src/hello.txt'), 'r', + common.mustSucceed((fd) => { + assert.notStrictEqual(fd & 0x40000000, 0); + fs.fstat(fd, common.mustSucceed((stats) => { + assert.strictEqual(stats.isFile(), true); + const buf = Buffer.alloc(11); + fs.read(fd, buf, 0, 11, 0, + common.mustSucceed((n, buffer) => { + assert.strictEqual(n, 11); + assert.strictEqual(buffer.toString(), 'hello world'); + fs.close(fd, + common.mustSucceed(() => myVfs.unmount())); + })); + })); + })); +} + +// Open + write + close +{ + const { myVfs, mountPoint } = mounted(); + fs.open(path.join(mountPoint, 'src/aw.txt'), 'w', + common.mustSucceed((fd) => { + const data = Buffer.from('async-fd'); + fs.write(fd, data, 0, data.length, 0, + common.mustSucceed((n) => { + assert.strictEqual(n, data.length); + fs.close(fd, common.mustSucceed(() => { + assert.strictEqual( + fs.readFileSync( + path.join(mountPoint, 'src/aw.txt'), 'utf8'), + 'async-fd', + ); + myVfs.unmount(); + })); + })); + })); +} + +// ftruncate (cb) +{ + const { myVfs, mountPoint } = mounted(); + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r+'); + fs.ftruncate(fd, 5, common.mustSucceed(() => { + fs.closeSync(fd); + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), 'utf8'), + 'hello', + ); + myVfs.unmount(); + })); +} diff --git a/test/parallel/test-vfs-fs-openAsBlob.js b/test/parallel/test-vfs-fs-openAsBlob.js new file mode 100644 index 00000000000000..1c8d175c8f99c4 --- /dev/null +++ b/test/parallel/test-vfs-fs-openAsBlob.js @@ -0,0 +1,25 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.openAsBlob dispatches to VFS and returns a Blob over the virtual file. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-openAsBlob-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +fs.openAsBlob(path.join(mountPoint, 'src/hello.txt')) + .then(async (blob) => { + assert.ok(blob instanceof Blob); + assert.strictEqual(blob.size, 11); + assert.strictEqual(await blob.text(), 'hello world'); + myVfs.unmount(); + }) + .then(common.mustCall()); diff --git a/test/parallel/test-vfs-fs-openSync.js b/test/parallel/test-vfs-fs-openSync.js new file mode 100644 index 00000000000000..f2c73f0d634469 --- /dev/null +++ b/test/parallel/test-vfs-fs-openSync.js @@ -0,0 +1,93 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.openSync / fs.readSync / fs.writeSync / fs.fstatSync / fs.closeSync / +// fs.ftruncateSync / fs.readvSync / fs.writevSync dispatch to VFS and operate +// on the bitmask-encoded virtual fd. The noop FD handlers (fchmodSync, etc.) +// short-circuit to true for virtual fds. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-openSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +// openSync + fstatSync + readSync + closeSync +{ + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r'); + assert.notStrictEqual(fd & 0x40000000, 0); // VFS bitmask is set + const stats = fs.fstatSync(fd); + assert.strictEqual(stats.isFile(), true); + const buf = Buffer.alloc(11); + assert.strictEqual(fs.readSync(fd, buf, 0, 11, 0), 11); + assert.strictEqual(buf.toString(), 'hello world'); + fs.closeSync(fd); +} + +// openSync + writeSync (buffer) + closeSync +{ + const fd = fs.openSync(path.join(mountPoint, 'src/wfd.txt'), 'w'); + assert.strictEqual(fs.writeSync(fd, Buffer.from('via-fd')), 6); + fs.closeSync(fd); + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/wfd.txt'), 'utf8'), + 'via-fd', + ); +} + +// writeSync with string + encoding +{ + const fd = fs.openSync(path.join(mountPoint, 'src/str.txt'), 'w'); + const n = fs.writeSync(fd, 'string-data', 0, 'utf8'); + assert.ok(n > 0); + fs.closeSync(fd); + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/str.txt'), 'utf8'), + 'string-data', + ); +} + +// ftruncateSync +{ + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r+'); + fs.ftruncateSync(fd, 5); + fs.closeSync(fd); + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), 'utf8'), + 'hello', + ); +} + +// fchmodSync, fchownSync, futimesSync, fdatasyncSync, fsyncSync are noops +{ + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r+'); + fs.fchmodSync(fd, 0o644); + fs.fchownSync(fd, process.getuid?.() ?? 0, process.getgid?.() ?? 0); + const now = new Date(); + fs.futimesSync(fd, now, now); + fs.fdatasyncSync(fd); + fs.fsyncSync(fd); + fs.closeSync(fd); +} + +// readvSync + writevSync +{ + const wf = fs.openSync(path.join(mountPoint, 'src/v.txt'), 'w'); + fs.writevSync(wf, [Buffer.from('abc'), Buffer.from('def')]); + fs.closeSync(wf); + + const rf = fs.openSync(path.join(mountPoint, 'src/v.txt'), 'r'); + const b1 = Buffer.alloc(3); + const b2 = Buffer.alloc(3); + assert.strictEqual(fs.readvSync(rf, [b1, b2], 0), 6); + assert.strictEqual(b1.toString() + b2.toString(), 'abcdef'); + fs.closeSync(rf); +} + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-opendir-callback.js b/test/parallel/test-vfs-fs-opendir-callback.js new file mode 100644 index 00000000000000..36b8ef81d74ed7 --- /dev/null +++ b/test/parallel/test-vfs-fs-opendir-callback.js @@ -0,0 +1,50 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.opendir callback dispatches through VFS, both via readSync() iteration +// and via async iteration. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-opendir-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello'); + myVfs.writeFileSync('/src/data.json', '{}'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// readSync() iteration +{ + const { myVfs, mountPoint } = mounted(); + fs.opendir(path.join(mountPoint, 'src'), + common.mustSucceed((dir) => { + const names = []; + let entry; + while ((entry = dir.readSync()) !== null) names.push(entry.name); + dir.closeSync(); + assert.ok(names.includes('hello.txt')); + myVfs.unmount(); + })); +} + +// for-await-of iteration +{ + const { myVfs, mountPoint } = mounted(); + fs.opendir(path.join(mountPoint, 'src'), + common.mustSucceed(async (dir) => { + const names = []; + for await (const entry of dir) names.push(entry.name); + assert.ok(names.includes('hello.txt')); + assert.ok(names.includes('data.json')); + myVfs.unmount(); + })); +} diff --git a/test/parallel/test-vfs-fs-opendirSync.js b/test/parallel/test-vfs-fs-opendirSync.js new file mode 100644 index 00000000000000..18ba4c49dd21eb --- /dev/null +++ b/test/parallel/test-vfs-fs-opendirSync.js @@ -0,0 +1,29 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.opendirSync dispatches to VFS and returns a Dir-like iterable. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-opendirSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src/subdir', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.writeFileSync('/src/data.json', '{}'); +myVfs.mount(mountPoint); + +const dir = fs.opendirSync(path.join(mountPoint, 'src')); +const names = []; +let entry; +while ((entry = dir.readSync()) !== null) names.push(entry.name); +dir.closeSync(); + +assert.ok(names.includes('hello.txt')); +assert.ok(names.includes('data.json')); +assert.ok(names.includes('subdir')); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-promises-buffer-encoding.js b/test/parallel/test-vfs-fs-promises-buffer-encoding.js new file mode 100644 index 00000000000000..cfb38787ab465c --- /dev/null +++ b/test/parallel/test-vfs-fs-promises-buffer-encoding.js @@ -0,0 +1,64 @@ +// Flags: --experimental-vfs +'use strict'; + +// The promise-based fs methods that accept `encoding: 'buffer'` must convert +// the (string) provider result into a Buffer before resolving. + +const common = require('../common'); +const assert = require('assert'); +const fsp = require('fs/promises'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-buf-enc-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +(async () => { + // readdir + { + const { myVfs, mountPoint } = mounted(); + const entries = await fsp.readdir(path.join(mountPoint, 'src'), + { encoding: 'buffer' }); + assert.ok(entries.every(Buffer.isBuffer)); + assert.ok(entries.some((b) => b.toString() === 'hello.txt')); + myVfs.unmount(); + } + + // realpath + { + const { myVfs, mountPoint } = mounted(); + const p = path.join(mountPoint, 'src/hello.txt'); + const rp = await fsp.realpath(p, { encoding: 'buffer' }); + assert.ok(Buffer.isBuffer(rp)); + assert.strictEqual(rp.toString(), p); + myVfs.unmount(); + } + + // readlink + { + const { myVfs, mountPoint } = mounted(); + await fsp.symlink('hello.txt', path.join(mountPoint, 'src/ln.txt')); + const target = await fsp.readlink(path.join(mountPoint, 'src/ln.txt'), + { encoding: 'buffer' }); + assert.ok(Buffer.isBuffer(target)); + assert.strictEqual(target.toString(), 'hello.txt'); + myVfs.unmount(); + } + + // mkdtemp + { + const { myVfs, mountPoint } = mounted(); + const dir = await fsp.mkdtemp(path.join(mountPoint, 'src/td-'), + { encoding: 'buffer' }); + assert.ok(Buffer.isBuffer(dir)); + myVfs.unmount(); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-fs-promises-stat-no-throw.js b/test/parallel/test-vfs-fs-promises-stat-no-throw.js new file mode 100644 index 00000000000000..6bd0c546a02d8c --- /dev/null +++ b/test/parallel/test-vfs-fs-promises-stat-no-throw.js @@ -0,0 +1,35 @@ +// Flags: --experimental-vfs +'use strict'; + +// fsp.stat() with throwIfNoEntry: false on a missing path within a mount +// must resolve with undefined instead of rejecting with ENOENT. + +const common = require('../common'); +const assert = require('assert'); +const fsp = require('fs/promises'); +const path = require('path'); +const vfs = require('node:vfs'); + +(async () => { + const mountPoint = path.resolve('/tmp/vfs-stat-no-throw-' + process.pid); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello'); + myVfs.mount(mountPoint); + + // Missing file -> undefined + const missing = await fsp.stat(path.join(mountPoint, 'src/nope'), + { throwIfNoEntry: false }); + assert.strictEqual(missing, undefined); + + // Existing file -> normal Stats + const stats = await fsp.stat(path.join(mountPoint, 'src/hello.txt'), + { throwIfNoEntry: false }); + assert.strictEqual(stats.isFile(), true); + + // Default behaviour (no option) still rejects on ENOENT + await assert.rejects(fsp.stat(path.join(mountPoint, 'src/nope')), + { code: 'ENOENT' }); + + myVfs.unmount(); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-fs-promises.js b/test/parallel/test-vfs-fs-promises.js new file mode 100644 index 00000000000000..d4b151162f2802 --- /dev/null +++ b/test/parallel/test-vfs-fs-promises.js @@ -0,0 +1,84 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs/promises dispatches through VFS for each supported path-based and +// FileHandle-based operation. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fsp = require('fs/promises'); +const path = require('path'); +const vfs = require('node:vfs'); + +(async () => { + const mountPoint = path.resolve('/tmp/vfs-promises-' + process.pid); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + const p = (s) => path.join(mountPoint, s); + + // Path-based reads + assert.strictEqual((await fsp.stat(p('src/hello.txt'))).isFile(), true); + assert.strictEqual((await fsp.lstat(p('src/hello.txt'))).isFile(), true); + assert.ok((await fsp.readdir(p('src'))).includes('hello.txt')); + assert.strictEqual(await fsp.readFile(p('src/hello.txt'), 'utf8'), + 'hello world'); + assert.strictEqual(await fsp.realpath(p('src/hello.txt')), + p('src/hello.txt')); + await fsp.access(p('src/hello.txt')); + + // statfs + const sfs = await fsp.statfs(p('src/hello.txt')); + assert.strictEqual(typeof sfs.bsize, 'number'); + + // Path-based writes + await fsp.writeFile(p('src/pw.txt'), 'pdata'); + assert.strictEqual(fs.readFileSync(p('src/pw.txt'), 'utf8'), 'pdata'); + await fsp.appendFile(p('src/pw.txt'), ' more'); + assert.strictEqual(fs.readFileSync(p('src/pw.txt'), 'utf8'), 'pdata more'); + + await fsp.mkdir(p('src/pd')); + await fsp.rmdir(p('src/pd')); + await fsp.rm(p('src/pw.txt')); + assert.strictEqual(fs.existsSync(p('src/pw.txt')), false); + + await fsp.copyFile(p('src/hello.txt'), p('src/pcopy.txt')); + assert.strictEqual(fs.readFileSync(p('src/pcopy.txt'), 'utf8'), + 'hello world'); + + await fsp.rename(p('src/pcopy.txt'), p('src/prenamed.txt')); + assert.strictEqual(fs.existsSync(p('src/pcopy.txt')), false); + await fsp.unlink(p('src/prenamed.txt')); + + await fsp.symlink('hello.txt', p('src/plnk.txt')); + assert.strictEqual(await fsp.readlink(p('src/plnk.txt')), 'hello.txt'); + + await fsp.truncate(p('src/hello.txt'), 5); + assert.strictEqual(fs.readFileSync(p('src/hello.txt'), 'utf8'), 'hello'); + + await fsp.link(p('src/hello.txt'), p('src/plink.txt')); + assert.strictEqual(fs.readFileSync(p('src/plink.txt'), 'utf8'), 'hello'); + + const tmp = await fsp.mkdtemp(p('src/ptmp-')); + assert.ok(tmp.startsWith(p('src/ptmp-'))); + assert.strictEqual(fs.statSync(tmp).isDirectory(), true); + + // Attribute mutations + const uid = process.getuid?.() ?? 0; + const gid = process.getgid?.() ?? 0; + const now = new Date(); + await fsp.chmod(p('src/hello.txt'), 0o644); + await fsp.chown(p('src/hello.txt'), uid, gid); + await fsp.lchown(p('src/hello.txt'), uid, gid); + await fsp.utimes(p('src/hello.txt'), now, now); + await fsp.lutimes(p('src/hello.txt'), now, now); + + // FileHandle via fsp.open + const handle = await fsp.open(p('src/hello.txt'), 'r'); + assert.strictEqual(await handle.readFile('utf8'), 'hello'); + await handle.close(); + + myVfs.unmount(); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-fs-readFile-callback.js b/test/parallel/test-vfs-fs-readFile-callback.js new file mode 100644 index 00000000000000..5902f399b30a9a --- /dev/null +++ b/test/parallel/test-vfs-fs-readFile-callback.js @@ -0,0 +1,75 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.readFile, fs.readdir, fs.realpath, fs.access, and fs.exists callbacks +// dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-readFile-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// readFile (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.readFile(path.join(mountPoint, 'src/hello.txt'), 'utf8', + common.mustSucceed((data) => { + assert.strictEqual(data, 'hello world'); + myVfs.unmount(); + })); +} + +// readdir (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.readdir(path.join(mountPoint, 'src'), + common.mustSucceed((entries) => { + assert.ok(entries.includes('hello.txt')); + myVfs.unmount(); + })); +} + +// realpath (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.realpath(path.join(mountPoint, 'src/hello.txt'), + common.mustSucceed((rp) => { + assert.strictEqual(rp, path.join(mountPoint, 'src/hello.txt')); + myVfs.unmount(); + })); +} + +// access (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.access(path.join(mountPoint, 'src/hello.txt'), + common.mustSucceed(() => { + myVfs.unmount(); + })); +} + +// exists (cb) - signature is (exists) not (err, exists), use mustCall +{ + const { myVfs, mountPoint } = mounted(); + fs.exists(path.join(mountPoint, 'src/hello.txt'), + common.mustCall((ok) => { + assert.strictEqual(ok, true); + fs.exists(path.join(mountPoint, 'missing'), + common.mustCall((ok2) => { + assert.strictEqual(ok2, false); + myVfs.unmount(); + })); + })); +} diff --git a/test/parallel/test-vfs-fs-readFileSync.js b/test/parallel/test-vfs-fs-readFileSync.js new file mode 100644 index 00000000000000..96e39892e66a9f --- /dev/null +++ b/test/parallel/test-vfs-fs-readFileSync.js @@ -0,0 +1,45 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.readFileSync dispatches to VFS for both string paths and VFS-owned fds. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-readFileSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +// Default (buffer) result +{ + const buf = fs.readFileSync(path.join(mountPoint, 'src/hello.txt')); + assert.ok(Buffer.isBuffer(buf)); + assert.strictEqual(buf.toString(), 'hello world'); +} + +// utf8 encoding -> string result +assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), 'utf8'), + 'hello world', +); + +// Encoding via options object +assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), + { encoding: 'utf8' }), + 'hello world', +); + +// readFileSync via a VFS fd +{ + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r'); + assert.strictEqual(fs.readFileSync(fd, 'utf8'), 'hello world'); + fs.closeSync(fd); +} + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-readdirSync.js b/test/parallel/test-vfs-fs-readdirSync.js new file mode 100644 index 00000000000000..9a795548133914 --- /dev/null +++ b/test/parallel/test-vfs-fs-readdirSync.js @@ -0,0 +1,47 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.readdirSync dispatches to VFS, including the buffer-encoding and +// withFileTypes options. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-readdirSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src/subdir', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.writeFileSync('/src/data.json', '{}'); +myVfs.mount(mountPoint); + +// Default (utf8 string array) +{ + const entries = fs.readdirSync(path.join(mountPoint, 'src')); + assert.ok(entries.includes('hello.txt')); + assert.ok(entries.includes('data.json')); + assert.ok(entries.includes('subdir')); +} + +// withFileTypes: true -> Dirent array +{ + const dirents = fs.readdirSync(path.join(mountPoint, 'src'), + { withFileTypes: true }); + const hello = dirents.find((d) => d.name === 'hello.txt'); + assert.ok(hello); + assert.strictEqual(hello.isFile(), true); + const subdir = dirents.find((d) => d.name === 'subdir'); + assert.strictEqual(subdir.isDirectory(), true); +} + +// encoding: 'buffer' -> Buffer entries +{ + const entries = fs.readdirSync(path.join(mountPoint, 'src'), + { encoding: 'buffer' }); + assert.ok(entries.every(Buffer.isBuffer)); + assert.ok(entries.some((b) => b.toString() === 'hello.txt')); +} + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-realpathSync.js b/test/parallel/test-vfs-fs-realpathSync.js new file mode 100644 index 00000000000000..77d0288776161d --- /dev/null +++ b/test/parallel/test-vfs-fs-realpathSync.js @@ -0,0 +1,30 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.realpathSync dispatches to VFS and returns a mount-rooted absolute path. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-realpathSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +const p = path.join(mountPoint, 'src/hello.txt'); + +// Default string return +assert.strictEqual(fs.realpathSync(p), p); + +// Buffer encoding +{ + const rp = fs.realpathSync(p, { encoding: 'buffer' }); + assert.ok(Buffer.isBuffer(rp)); + assert.strictEqual(rp.toString(), p); +} + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-rename-callback.js b/test/parallel/test-vfs-fs-rename-callback.js new file mode 100644 index 00000000000000..ccf6cccdd56e68 --- /dev/null +++ b/test/parallel/test-vfs-fs-rename-callback.js @@ -0,0 +1,55 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.rename and fs.copyFile callbacks dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-rename-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// rename (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.rename( + path.join(mountPoint, 'src/hello.txt'), + path.join(mountPoint, 'src/renamed-cb.txt'), + common.mustSucceed(() => { + assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), + false); + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/renamed-cb.txt'), 'utf8'), + 'hello world', + ); + myVfs.unmount(); + }), + ); +} + +// copyFile (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.copyFile( + path.join(mountPoint, 'src/hello.txt'), + path.join(mountPoint, 'src/copy-cb.txt'), + common.mustSucceed(() => { + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/copy-cb.txt'), 'utf8'), + 'hello world', + ); + myVfs.unmount(); + }), + ); +} diff --git a/test/parallel/test-vfs-fs-renameSync.js b/test/parallel/test-vfs-fs-renameSync.js new file mode 100644 index 00000000000000..88c3f17eef5c83 --- /dev/null +++ b/test/parallel/test-vfs-fs-renameSync.js @@ -0,0 +1,29 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.renameSync dispatches to VFS when both paths are within the same mount. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-renameSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +fs.renameSync( + path.join(mountPoint, 'src/hello.txt'), + path.join(mountPoint, 'src/renamed.txt'), +); +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), + false); +assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/renamed.txt'), 'utf8'), + 'hello world', +); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-rmSync.js b/test/parallel/test-vfs-fs-rmSync.js new file mode 100644 index 00000000000000..a6b77a66bc41d7 --- /dev/null +++ b/test/parallel/test-vfs-fs-rmSync.js @@ -0,0 +1,36 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.rmSync, fs.rmdirSync, and fs.unlinkSync dispatch to VFS. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-rmSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src/subdir', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.writeFileSync('/src/subdir/inside.txt', 'inside'); +myVfs.mkdirSync('/empty'); +myVfs.mount(mountPoint); + +// rmdirSync on an empty directory +fs.rmdirSync(path.join(mountPoint, 'empty')); +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'empty')), false); + +// unlinkSync on a file +fs.unlinkSync(path.join(mountPoint, 'src/hello.txt')); +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), + false); + +// rmSync with force on a missing path is a no-op +fs.rmSync(path.join(mountPoint, 'missing'), { force: true }); + +// rmSync recursive on a non-empty directory tree +fs.rmSync(path.join(mountPoint, 'src'), { recursive: true }); +assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src')), false); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-stat-callback.js b/test/parallel/test-vfs-fs-stat-callback.js new file mode 100644 index 00000000000000..873d27e14445ef --- /dev/null +++ b/test/parallel/test-vfs-fs-stat-callback.js @@ -0,0 +1,42 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.stat and fs.lstat callbacks dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-stat-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// stat (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.stat(path.join(mountPoint, 'src/hello.txt'), + common.mustSucceed((s) => { + assert.strictEqual(s.isFile(), true); + assert.strictEqual(s.size, 11); + myVfs.unmount(); + })); +} + +// lstat (cb) +{ + const { myVfs, mountPoint } = mounted(); + fs.lstat(path.join(mountPoint, 'src/hello.txt'), + common.mustSucceed((s) => { + assert.strictEqual(s.isFile(), true); + myVfs.unmount(); + })); +} diff --git a/test/parallel/test-vfs-fs-statSync.js b/test/parallel/test-vfs-fs-statSync.js new file mode 100644 index 00000000000000..e952dfab9ad56c --- /dev/null +++ b/test/parallel/test-vfs-fs-statSync.js @@ -0,0 +1,61 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.statSync / fs.lstatSync / fs.statfsSync dispatch through the VFS layer, +// including the `throwIfNoEntry: false` option. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-statSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +// statSync on a regular file +{ + const s = fs.statSync(path.join(mountPoint, 'src/hello.txt')); + assert.strictEqual(s.isFile(), true); + assert.strictEqual(s.size, 11); +} + +// statSync with throwIfNoEntry:false on missing path +assert.strictEqual( + fs.statSync(path.join(mountPoint, 'missing'), { throwIfNoEntry: false }), + undefined, +); + +// statSync on missing path throws ENOENT by default +assert.throws(() => fs.statSync(path.join(mountPoint, 'missing')), + { code: 'ENOENT' }); + +// lstatSync on a regular file +{ + const s = fs.lstatSync(path.join(mountPoint, 'src/hello.txt')); + assert.strictEqual(s.isFile(), true); +} + +// lstatSync with throwIfNoEntry:false on missing path +assert.strictEqual( + fs.lstatSync(path.join(mountPoint, 'missing'), { throwIfNoEntry: false }), + undefined, +); + +// statfsSync returns number-typed values by default +{ + const s = fs.statfsSync(path.join(mountPoint, 'src/hello.txt')); + assert.strictEqual(typeof s.bsize, 'number'); +} + +// statfsSync with bigint:true returns BigInt fields +{ + const s = fs.statfsSync(path.join(mountPoint, 'src/hello.txt'), + { bigint: true }); + assert.strictEqual(typeof s.bsize, 'bigint'); +} + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-symlink-callback.js b/test/parallel/test-vfs-fs-symlink-callback.js new file mode 100644 index 00000000000000..004745e0fb973c --- /dev/null +++ b/test/parallel/test-vfs-fs-symlink-callback.js @@ -0,0 +1,25 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.symlink and fs.readlink callbacks dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-symlink-cb-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +fs.symlink('hello.txt', path.join(mountPoint, 'src/lnk.txt'), + common.mustSucceed(() => { + fs.readlink(path.join(mountPoint, 'src/lnk.txt'), + common.mustSucceed((target) => { + assert.strictEqual(target, 'hello.txt'); + myVfs.unmount(); + })); + })); diff --git a/test/parallel/test-vfs-fs-symlinkSync.js b/test/parallel/test-vfs-fs-symlinkSync.js new file mode 100644 index 00000000000000..6d043c59a98ac7 --- /dev/null +++ b/test/parallel/test-vfs-fs-symlinkSync.js @@ -0,0 +1,30 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.symlinkSync and fs.readlinkSync dispatch through VFS, including the +// buffer-encoding variant of readlinkSync. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-symlinkSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +fs.symlinkSync('hello.txt', path.join(mountPoint, 'src/link.txt')); +assert.strictEqual( + fs.readlinkSync(path.join(mountPoint, 'src/link.txt')), + 'hello.txt', +); + +const buf = fs.readlinkSync(path.join(mountPoint, 'src/link.txt'), + { encoding: 'buffer' }); +assert.ok(Buffer.isBuffer(buf)); +assert.strictEqual(buf.toString(), 'hello.txt'); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-truncate-callback.js b/test/parallel/test-vfs-fs-truncate-callback.js new file mode 100644 index 00000000000000..e33ed7e4d61402 --- /dev/null +++ b/test/parallel/test-vfs-fs-truncate-callback.js @@ -0,0 +1,45 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.truncate, fs.link, and fs.mkdtemp callbacks dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-truncate-cb-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +fs.truncate(path.join(mountPoint, 'src/hello.txt'), 5, + common.mustSucceed(() => { + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), + 'utf8'), + 'hello', + ); + + fs.link(path.join(mountPoint, 'src/hello.txt'), + path.join(mountPoint, 'src/lk.txt'), + common.mustSucceed(() => { + assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/lk.txt'), + 'utf8'), + 'hello', + ); + + fs.mkdtemp(path.join(mountPoint, 'src/td-'), + common.mustSucceed((dir) => { + assert.ok(dir.startsWith( + path.join(mountPoint, 'src/td-'))); + assert.strictEqual( + fs.statSync(dir).isDirectory(), true, + ); + myVfs.unmount(); + })); + })); + })); diff --git a/test/parallel/test-vfs-fs-truncateSync.js b/test/parallel/test-vfs-fs-truncateSync.js new file mode 100644 index 00000000000000..1c15f5647a4716 --- /dev/null +++ b/test/parallel/test-vfs-fs-truncateSync.js @@ -0,0 +1,24 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.truncateSync dispatches to VFS and shrinks the file content. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-truncateSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello world'); +myVfs.mount(mountPoint); + +fs.truncateSync(path.join(mountPoint, 'src/hello.txt'), 5); +assert.strictEqual( + fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), 'utf8'), + 'hello', +); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-watch-dispatch.js b/test/parallel/test-vfs-fs-watch-dispatch.js new file mode 100644 index 00000000000000..6d528d6fef7c62 --- /dev/null +++ b/test/parallel/test-vfs-fs-watch-dispatch.js @@ -0,0 +1,24 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.watch on a path under a mount returns the provider's watcher object +// rather than calling the real-fs watcher. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-watch-dispatch-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.writeFileSync('/src/hello.txt', 'hello'); +myVfs.mount(mountPoint); + +const watcher = fs.watch(path.join(mountPoint, 'src/hello.txt')); +assert.ok(watcher); +assert.strictEqual(typeof watcher.close, 'function'); +watcher.close(); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-fs-writeFile-callback.js b/test/parallel/test-vfs-fs-writeFile-callback.js new file mode 100644 index 00000000000000..a02dc686b0e0a6 --- /dev/null +++ b/test/parallel/test-vfs-fs-writeFile-callback.js @@ -0,0 +1,41 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.writeFile and fs.appendFile callbacks dispatch through VFS. + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseMountPoint = path.resolve('/tmp/vfs-writeFile-cb-' + process.pid); +let counter = 0; +function mounted() { + const mountPoint = baseMountPoint + '-' + (counter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// writeFile (cb) +{ + const { myVfs, mountPoint } = mounted(); + const target = path.join(mountPoint, 'src/cb-w.txt'); + fs.writeFile(target, 'cbw', common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(target, 'utf8'), 'cbw'); + myVfs.unmount(); + })); +} + +// appendFile (cb) +{ + const { myVfs, mountPoint } = mounted(); + const target = path.join(mountPoint, 'src/cb-a.txt'); + fs.writeFileSync(target, 'base'); + fs.appendFile(target, ' more', common.mustSucceed(() => { + assert.strictEqual(fs.readFileSync(target, 'utf8'), 'base more'); + myVfs.unmount(); + })); +} diff --git a/test/parallel/test-vfs-fs-writeFileSync.js b/test/parallel/test-vfs-fs-writeFileSync.js new file mode 100644 index 00000000000000..7469127bb1fde3 --- /dev/null +++ b/test/parallel/test-vfs-fs-writeFileSync.js @@ -0,0 +1,29 @@ +// Flags: --experimental-vfs +'use strict'; + +// fs.writeFileSync and fs.appendFileSync dispatch to VFS. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const mountPoint = path.resolve('/tmp/vfs-writeFileSync-' + process.pid); +const myVfs = vfs.create(); +myVfs.mkdirSync('/src', { recursive: true }); +myVfs.mount(mountPoint); + +const target = path.join(mountPoint, 'src/new.txt'); + +fs.writeFileSync(target, 'fresh'); +assert.strictEqual(fs.readFileSync(target, 'utf8'), 'fresh'); + +fs.appendFileSync(target, ' more'); +assert.strictEqual(fs.readFileSync(target, 'utf8'), 'fresh more'); + +// Buffer input +fs.writeFileSync(target, Buffer.from('binary')); +assert.strictEqual(fs.readFileSync(target, 'utf8'), 'binary'); + +myVfs.unmount(); diff --git a/test/parallel/test-vfs-mount-errors.js b/test/parallel/test-vfs-mount-errors.js new file mode 100644 index 00000000000000..905f1c6d0683ab --- /dev/null +++ b/test/parallel/test-vfs-mount-errors.js @@ -0,0 +1,159 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// Error paths in the VFS mount layer: +// - EXDEV when renaming/linking across different VFS instances or VFS<->real +// - lastunmount handler cleanup (vfsState.handlers becomes null again) +// - rename of root mount point is rejected as overlapping + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); +const { vfsState } = require('internal/fs/utils'); + +const baseMountPoint = path.resolve('/tmp/vfs-mount-errors-' + process.pid); +let mountCounter = 0; +const nextMount = () => baseMountPoint + '-' + (mountCounter++); + +// EXDEV: rename across two different VFS instances +{ + const mountA = nextMount(); + const mountB = nextMount(); + const a = vfs.create(); + const b = vfs.create(); + a.writeFileSync('/file.txt', 'a'); + b.mkdirSync('/x', { recursive: true }); + a.mount(mountA); + b.mount(mountB); + + assert.throws( + () => fs.renameSync(path.join(mountA, 'file.txt'), + path.join(mountB, 'x/file.txt')), + { code: 'EXDEV' }, + ); + a.unmount(); + b.unmount(); +} + +// EXDEV: copyFileSync across two different VFS instances +{ + const mountA = nextMount(); + const mountB = nextMount(); + const a = vfs.create(); + const b = vfs.create(); + a.writeFileSync('/file.txt', 'a'); + b.mkdirSync('/x', { recursive: true }); + a.mount(mountA); + b.mount(mountB); + + assert.throws( + () => fs.copyFileSync(path.join(mountA, 'file.txt'), + path.join(mountB, 'x/copy.txt')), + { code: 'EXDEV' }, + ); + a.unmount(); + b.unmount(); +} + +// EXDEV: linkSync across two different VFS instances +{ + const mountA = nextMount(); + const mountB = nextMount(); + const a = vfs.create(); + const b = vfs.create(); + a.writeFileSync('/file.txt', 'a'); + b.mkdirSync('/x', { recursive: true }); + a.mount(mountA); + b.mount(mountB); + + assert.throws( + () => fs.linkSync(path.join(mountA, 'file.txt'), + path.join(mountB, 'x/lk.txt')), + { code: 'EXDEV' }, + ); + a.unmount(); + b.unmount(); +} + +// EXDEV: rename from VFS to a real-fs path +{ + const mountA = nextMount(); + const a = vfs.create(); + a.writeFileSync('/file.txt', 'a'); + a.mount(mountA); + + const tmpReal = '/tmp/vfs-mount-real-' + process.pid + '.txt'; + assert.throws( + () => fs.renameSync(path.join(mountA, 'file.txt'), tmpReal), + { code: 'EXDEV' }, + ); + a.unmount(); +} + +// Handler cleanup: after last unmount, vfsState.handlers returns to null +{ + assert.strictEqual(vfsState.handlers, null); + const x = vfs.create(); + x.mount(nextMount()); + assert.notStrictEqual(vfsState.handlers, null); + x.unmount(); + assert.strictEqual(vfsState.handlers, null); + + // And it re-installs on a subsequent mount + const y = vfs.create(); + y.mount(nextMount()); + assert.notStrictEqual(vfsState.handlers, null); + y.unmount(); + assert.strictEqual(vfsState.handlers, null); +} + +// Two parallel non-overlapping mounts both register, last-out clears handlers +{ + const a = vfs.create(); + const b = vfs.create(); + a.mount(nextMount()); + b.mount(nextMount()); + assert.notStrictEqual(vfsState.handlers, null); + a.unmount(); + assert.notStrictEqual(vfsState.handlers, null); + b.unmount(); + assert.strictEqual(vfsState.handlers, null); +} + +// Overlap detection: nested-under and parent-of both rejected +{ + const parent = nextMount(); + const child = path.join(parent, 'child'); + const a = vfs.create(); + const b = vfs.create(); + a.mount(parent); + assert.throws(() => b.mount(child), { code: 'ERR_INVALID_STATE' }); + a.unmount(); + + // Reverse direction: child first, then parent rejected + const c = vfs.create(); + const d = vfs.create(); + c.mount(child); + assert.throws(() => d.mount(parent), { code: 'ERR_INVALID_STATE' }); + c.unmount(); +} + +// Equal mount points: second one rejected +{ + const m = nextMount(); + const a = vfs.create(); + const b = vfs.create(); + a.mount(m); + assert.throws(() => b.mount(m), { code: 'ERR_INVALID_STATE' }); + a.unmount(); +} + +// Double-mount of same instance rejected +{ + const a = vfs.create(); + a.mount(nextMount()); + assert.throws(() => a.mount(nextMount()), { code: 'ERR_INVALID_STATE' }); + a.unmount(); +} diff --git a/test/parallel/test-vfs-mount.js b/test/parallel/test-vfs-mount.js new file mode 100644 index 00000000000000..e59e39eef9713e --- /dev/null +++ b/test/parallel/test-vfs-mount.js @@ -0,0 +1,173 @@ +// Flags: --experimental-vfs +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +// Basic mount/unmount API and dispatch through node:vfs from the public fs. + +const baseMountPoint = path.resolve('/tmp/vfs-mount-' + process.pid); +let mountCounter = 0; + +function createMountedVfs() { + const mountPoint = baseMountPoint + '-' + (mountCounter++); + const myVfs = vfs.create(); + myVfs.mkdirSync('/src', { recursive: true }); + myVfs.writeFileSync('/src/hello.txt', 'hello world'); + myVfs.mount(mountPoint); + return { myVfs, mountPoint }; +} + +// Test: mounted/mountPoint getters +{ + const myVfs = vfs.create(); + assert.strictEqual(myVfs.mounted, false); + assert.strictEqual(myVfs.mountPoint, null); + + const mountPoint = baseMountPoint + '-' + (mountCounter++); + myVfs.mount(mountPoint); + assert.strictEqual(myVfs.mounted, true); + assert.strictEqual(myVfs.mountPoint, mountPoint); + + myVfs.unmount(); + assert.strictEqual(myVfs.mounted, false); + assert.strictEqual(myVfs.mountPoint, null); +} + +// Test: double-mount throws +{ + const myVfs = vfs.create(); + const mountPoint = baseMountPoint + '-' + (mountCounter++); + myVfs.mount(mountPoint); + assert.throws(() => myVfs.mount(mountPoint), { code: 'ERR_INVALID_STATE' }); + myVfs.unmount(); +} + +// Test: overlapping mounts throw +{ + const a = vfs.create(); + const b = vfs.create(); + const mountPoint = baseMountPoint + '-' + (mountCounter++); + a.mount(mountPoint); + assert.throws(() => b.mount(mountPoint), { code: 'ERR_INVALID_STATE' }); + assert.throws(() => b.mount(path.join(mountPoint, 'inner')), + { code: 'ERR_INVALID_STATE' }); + a.unmount(); +} + +// Test: fs.readFileSync intercepted +{ + const { myVfs, mountPoint } = createMountedVfs(); + const content = fs.readFileSync(path.join(mountPoint, 'src/hello.txt'), 'utf8'); + assert.strictEqual(content, 'hello world'); + myVfs.unmount(); +} + +// Test: fs.existsSync intercepted +{ + const { myVfs, mountPoint } = createMountedVfs(); + assert.strictEqual(fs.existsSync(path.join(mountPoint, 'src/hello.txt')), true); + assert.strictEqual(fs.existsSync(path.join(mountPoint, 'nonexistent')), false); + myVfs.unmount(); +} + +// Test: fs.statSync intercepted +{ + const { myVfs, mountPoint } = createMountedVfs(); + const stats = fs.statSync(path.join(mountPoint, 'src/hello.txt')); + assert.strictEqual(stats.isFile(), true); + assert.strictEqual(stats.size, 11); + myVfs.unmount(); +} + +// Test: fs.readdirSync intercepted +{ + const { myVfs, mountPoint } = createMountedVfs(); + const entries = fs.readdirSync(path.join(mountPoint, 'src')); + assert.ok(entries.includes('hello.txt')); + myVfs.unmount(); +} + +// Test: fs.writeFileSync intercepted +{ + const { myVfs, mountPoint } = createMountedVfs(); + const newPath = path.join(mountPoint, 'src/new.txt'); + fs.writeFileSync(newPath, 'fresh'); + assert.strictEqual(fs.readFileSync(newPath, 'utf8'), 'fresh'); + myVfs.unmount(); +} + +// Test: fs callback API +{ + const { myVfs, mountPoint } = createMountedVfs(); + fs.readFile(path.join(mountPoint, 'src/hello.txt'), 'utf8', + common.mustSucceed((data) => { + assert.strictEqual(data, 'hello world'); + myVfs.unmount(); + })); +} + +// Test: fs.promises API +{ + const { myVfs, mountPoint } = createMountedVfs(); + fs.promises.readFile(path.join(mountPoint, 'src/hello.txt'), 'utf8') + .then(common.mustCall((data) => { + assert.strictEqual(data, 'hello world'); + myVfs.unmount(); + })); +} + +// Test: streams +{ + const { myVfs, mountPoint } = createMountedVfs(); + const chunks = []; + const stream = fs.createReadStream(path.join(mountPoint, 'src/hello.txt')); + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', common.mustCall(() => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'hello world'); + myVfs.unmount(); + })); +} + +// Test: openSync/readSync/closeSync via public fs +{ + const { myVfs, mountPoint } = createMountedVfs(); + const fd = fs.openSync(path.join(mountPoint, 'src/hello.txt'), 'r'); + assert.notStrictEqual(fd & 0x40000000, 0); + const buf = Buffer.alloc(11); + const n = fs.readSync(fd, buf, 0, 11, 0); + assert.strictEqual(n, 11); + assert.strictEqual(buf.toString(), 'hello world'); + fs.closeSync(fd); + myVfs.unmount(); +} + +// Test: ENOENT thrown for missing path under mount +{ + const { myVfs, mountPoint } = createMountedVfs(); + assert.throws(() => fs.readFileSync(path.join(mountPoint, 'src/missing.txt')), + { code: 'ENOENT' }); + myVfs.unmount(); +} + +// Test: paths outside the mount point go to the real fs (no interference) +{ + const { myVfs, mountPoint } = createMountedVfs(); + // /etc/hostname (or any real path) should pass through; assert it doesn't + // hit our VFS by checking that mountPoint is not a prefix of the path. + assert.ok(!path.resolve('/etc').startsWith(mountPoint)); + myVfs.unmount(); +} + +// Test: Symbol.dispose unmounts +{ + const myVfs = vfs.create(); + const mountPoint = baseMountPoint + '-' + (mountCounter++); + myVfs.mount(mountPoint); + assert.strictEqual(myVfs.mounted, true); + myVfs[Symbol.dispose](); + assert.strictEqual(myVfs.mounted, false); +} diff --git a/test/parallel/test-vfs-multi-mount.js b/test/parallel/test-vfs-multi-mount.js new file mode 100644 index 00000000000000..879f7cfcbf2b83 --- /dev/null +++ b/test/parallel/test-vfs-multi-mount.js @@ -0,0 +1,65 @@ +// Flags: --experimental-vfs +'use strict'; + +// Two concurrent non-overlapping mounts must each route to its own VFS without +// interference. Also exercises that the handler registry iterates and routes +// correctly when more than one VFS is active. + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vfs = require('node:vfs'); + +const baseA = path.resolve('/tmp/vfs-multi-a-' + process.pid); +const baseB = path.resolve('/tmp/vfs-multi-b-' + process.pid); + +const a = vfs.create(); +a.writeFileSync('/file.txt', 'from-a'); +a.mkdirSync('/dir', { recursive: true }); +a.writeFileSync('/dir/inside.txt', 'a-inside'); + +const b = vfs.create(); +b.writeFileSync('/file.txt', 'from-b'); +b.mkdirSync('/dir', { recursive: true }); +b.writeFileSync('/dir/inside.txt', 'b-inside'); + +a.mount(baseA); +b.mount(baseB); + +// Each mount sees its own content +assert.strictEqual(fs.readFileSync(path.join(baseA, 'file.txt'), 'utf8'), + 'from-a'); +assert.strictEqual(fs.readFileSync(path.join(baseB, 'file.txt'), 'utf8'), + 'from-b'); + +// Per-mount directory listings are isolated +assert.deepStrictEqual( + fs.readdirSync(baseA).sort(), + ['dir', 'file.txt'], +); +assert.deepStrictEqual( + fs.readdirSync(baseB).sort(), + ['dir', 'file.txt'], +); + +// Writing to one mount doesn't bleed into the other +fs.writeFileSync(path.join(baseA, 'only-a.txt'), 'A'); +assert.strictEqual(fs.existsSync(path.join(baseB, 'only-a.txt')), false); +assert.strictEqual(fs.readFileSync(path.join(baseA, 'only-a.txt'), 'utf8'), + 'A'); + +// realpathSync returns the mount-rooted path (proves #toMountedPath on each) +assert.strictEqual(fs.realpathSync(path.join(baseA, 'file.txt')), + path.join(baseA, 'file.txt')); +assert.strictEqual(fs.realpathSync(path.join(baseB, 'file.txt')), + path.join(baseB, 'file.txt')); + +// Unmount one, the other still works +a.unmount(); +assert.strictEqual(a.mounted, false); +assert.strictEqual(b.mounted, true); +assert.strictEqual(fs.readFileSync(path.join(baseB, 'file.txt'), 'utf8'), + 'from-b'); + +b.unmount(); diff --git a/test/parallel/test-vfs-router.js b/test/parallel/test-vfs-router.js new file mode 100644 index 00000000000000..864a684f5c2df4 --- /dev/null +++ b/test/parallel/test-vfs-router.js @@ -0,0 +1,63 @@ +// Flags: --experimental-vfs --expose-internals +'use strict'; + +// Unit-level coverage for the mount-router helpers in +// lib/internal/vfs/router.js. The router operates on resolved (platform- +// native) absolute paths, so the test inputs are constructed via +// path.resolve / path.join to exercise both POSIX and Windows runs. + +require('../common'); +const assert = require('assert'); +const path = require('path'); +const { isUnderMountPoint, getRelativePath, isAbsolutePath } = + require('internal/vfs/router'); + +const mount = path.resolve('/app'); +const nested = path.join(mount, 'src', 'index.js'); + +// isUnderMountPoint: equal paths are always considered "under" +assert.strictEqual(isUnderMountPoint(mount, mount), true); + +// isUnderMountPoint: nested paths +assert.strictEqual(isUnderMountPoint(nested, mount), true); + +// isUnderMountPoint: rejects sibling paths that share the prefix string +assert.strictEqual( + isUnderMountPoint(path.resolve('/app2/index.js'), mount), false, +); +assert.strictEqual(isUnderMountPoint(path.resolve('/applebrick'), mount), + false); + +// isUnderMountPoint: rejects an unrelated absolute path +assert.strictEqual(isUnderMountPoint(path.resolve('/other'), mount), false); + +// isUnderMountPoint: root mount matches any absolute path on POSIX. +// On Windows the root mount '/' resolves to a drive-letter root, so the +// special-case in router.js only applies when mountPoint === '/'. Skip the +// root-mount checks where they would not be representative on Windows. +if (process.platform !== 'win32') { + assert.strictEqual(isUnderMountPoint('/anywhere', '/'), true); + assert.strictEqual(isUnderMountPoint('/', '/'), true); + assert.strictEqual(isUnderMountPoint('/a/b/c', '/'), true); +} + +// getRelativePath: equal => '/' +assert.strictEqual(getRelativePath(mount, mount), '/'); + +// getRelativePath: nested - always returned in POSIX form regardless of +// the platform-native input separators. +assert.strictEqual(getRelativePath(nested, mount), '/src/index.js'); + +// getRelativePath: root mount returns the original (already absolute) path +if (process.platform !== 'win32') { + assert.strictEqual(getRelativePath('/foo/bar', '/'), '/foo/bar'); +} + +// getRelativePath: deeper nesting +const mountA = path.resolve('/m/a'); +const deep = path.join(mountA, 'b', 'c', 'd'); +assert.strictEqual(getRelativePath(deep, mountA), '/b/c/d'); + +// isAbsolutePath is re-exported from node:path +assert.strictEqual(isAbsolutePath(path.resolve('/foo')), true); +assert.strictEqual(isAbsolutePath('foo'), false); From 31965d6df291bb5ceae4ad3ab06635997ad8dba8 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma <155464075+DivyanshuX9@users.noreply.github.com> Date: Sat, 30 May 2026 00:18:53 +0530 Subject: [PATCH 82/89] errors: handle V8 warnings in DisallowJavascriptExecutionScope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defer non-critical warnings to the next event loop iteration when can_call_into_js() returns false. This prevents crashes when V8 emits warnings during REPL preview evaluation or other contexts where JavaScript execution is temporarily forbidden. When a warning is emitted inside DisallowJavascriptExecutionScope, ProcessEmitWarningGeneric cannot be called immediately. Instead, use env->SetImmediate() to queue the warning emission for after the scope exits. This preserves full warning formatting, deprecation codes, and --redirect-warnings routing. Signed-off-by: Divyanshu Sharma PR-URL: https://github.com/nodejs/node/pull/63491 Fixes: https://github.com/nodejs/node/issues/63473 Reviewed-By: René Reviewed-By: James M Snell Reviewed-By: Anna Henningsen --- src/node_errors.cc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node_errors.cc b/src/node_errors.cc index 74326496132773..63db97f6a56db0 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -1064,7 +1064,12 @@ void PerIsolateMessageListener(Local message, Local error) { filename, message->GetLineNumber(env->context()).FromMaybe(-1), msg); - USE(ProcessEmitWarningGeneric(env, warning, "V8")); + // Defer the warning to the next event loop iteration. This prevents + // crashes when V8 emits warnings during code evaluation with + // throwOnSideEffect. + env->SetImmediate([warning](Environment* env) { + ProcessEmitWarningGeneric(env, warning, "V8"); + }); break; } case Isolate::MessageErrorLevel::kMessageError: From 6f29e1a292e4c8be770ab7583e107524aad8b3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 29 May 2026 16:43:34 -0300 Subject: [PATCH 83/89] util: create hex style cache and fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Guilherme Araújo PR-URL: https://github.com/nodejs/node/pull/62999 Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Matteo Collina Reviewed-By: Ethan Arrowood --- benchmark/util/style-text.js | 18 +++++++- lib/util.js | 83 ++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/benchmark/util/style-text.js b/benchmark/util/style-text.js index f04a26646e052d..c50a225fd39331 100644 --- a/benchmark/util/style-text.js +++ b/benchmark/util/style-text.js @@ -5,9 +5,22 @@ const common = require('../common.js'); const { styleText } = require('node:util'); const assert = require('node:assert'); +// 1000 distinct hex colors to exercise the cache under high-miss conditions. +// Spread evenly across hue space so colors are valid and maximally varied. +const kHexColorCount = 1000; +const toHex = (n) => n.toString(16).padStart(2, '0'); +const hexColors = Array.from({ length: kHexColorCount }, (_, i) => { + const r = (i * 37) & 0xff; + const g = (i * 73) & 0xff; + const b = (i * 137) & 0xff; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +}); + const bench = common.createBenchmark(main, { messageType: ['string', 'number', 'boolean', 'invalid'], - format: ['red', 'italic', 'invalid', '#ff0000'], + // '#rotating' cycles through kHexColorCount distinct colors to simulate + // the high-miss-rate / large-cache scenario (e.g. user-randomised colors). + format: ['red', 'italic', 'invalid', '#ff0000', '#rotating'], validateStream: [1, 0], n: [1e3], }); @@ -31,9 +44,10 @@ function main({ messageType, format, validateStream, n }) { bench.start(); for (let i = 0; i < n; i++) { + const fmt = format === '#rotating' ? hexColors[i % kHexColorCount] : format; let colored = ''; try { - colored = styleText(format, str, { validateStream }); + colored = styleText(fmt, str, { validateStream }); assert.ok(colored); // Attempt to avoid dead-code elimination } catch { // eslint-disable no-empty diff --git a/lib/util.js b/lib/util.js index 9601593eaf404a..adebd890adcd71 100644 --- a/lib/util.js +++ b/lib/util.js @@ -38,6 +38,7 @@ const { ObjectValues, ReflectApply, RegExpPrototypeExec, + SafeMap, StringPrototypeSlice, StringPrototypeToWellFormed, } = primordials; @@ -114,8 +115,20 @@ const kEscapeEnd = 'm'; const kDimCode = 2; const kBoldCode = 1; +// Close sequence for 24-bit foreground colors (reset to default foreground) +const kHexCloseSeq = kEscape + '39' + kEscapeEnd; + let styleCache; +const kHexStyleCacheMax = 256; + +let hexStyleCache; + +function getHexStyleCache() { + hexStyleCache ??= new SafeMap(); + return hexStyleCache; +} + function getStyleCache() { if (styleCache === undefined) { styleCache = { __proto__: null }; @@ -137,6 +150,28 @@ function getStyleCache() { return styleCache; } +/** + * Returns the cached ANSI escape sequences for a hex color. + * Computes and caches on first use to avoid repeated Buffer allocations. + * @param {string} hex A valid hex color string (#RGB or #RRGGBB) + * @returns {{openSeq: string, closeSeq: string}} + */ +function getHexStyle(hex) { + const cache = getHexStyleCache(); + const cached = cache.get(hex); + if (cached !== undefined) return cached; + const { 0: r, 1: g, 2: b } = hexToRgb(hex); + const style = { + __proto__: null, + openSeq: kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd, + closeSeq: kHexCloseSeq, + }; + if (cache.size >= kHexStyleCacheMax) + cache.delete(cache.keys().next().value); + cache.set(hex, style); + return style; +} + function replaceCloseCode(str, closeSeq, openSeq, keepClose) { const closeLen = closeSeq.length; let index = str.indexOf(closeSeq); @@ -163,15 +198,6 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) { // Matches #RGB or #RRGGBB const hexColorRegExp = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; -/** - * Validates whether a string is a valid hex color code. - * @param {string} hex The hex string to validate (e.g., '#fff' or '#ffffff') - * @returns {boolean} True if valid hex color, false otherwise - */ -function isValidHexColor(hex) { - return typeof hex === 'string' && RegExpPrototypeExec(hexColorRegExp, hex) !== null; -} - /** * Parses a hex color string into RGB components. * Supports both 3-digit (#RGB) and 6-digit (#RRGGBB) formats. @@ -225,6 +251,17 @@ function styleText(format, text, options) { const processed = replaceCloseCode(text, style.closeSeq, style.openSeq, style.keepClose); return style.openSeq + processed + style.closeSeq; } + + if (format[0] === '#') { + let hexStyle = getHexStyleCache().get(format); + if (hexStyle === undefined && RegExpPrototypeExec(hexColorRegExp, format) !== null) { + hexStyle = getHexStyle(format); + } + if (hexStyle !== undefined) { + const processed = replaceCloseCode(text, hexStyle.closeSeq, hexStyle.openSeq, false); + return hexStyle.openSeq + processed + hexStyle.closeSeq; + } + } } validateString(text, 'text'); @@ -255,24 +292,26 @@ function styleText(format, text, options) { for (const key of formatArray) { if (key === 'none') continue; - if (isValidHexColor(key)) { - if (skipColorize) continue; - const { 0: r, 1: g, 2: b } = hexToRgb(key); - const openSeq = kEscape + rgbToAnsi24Bit(r, g, b) + kEscapeEnd; - const closeSeq = kEscape + '39' + kEscapeEnd; - openCodes += openSeq; - closeCodes = closeSeq + closeCodes; - processedText = replaceCloseCode(processedText, closeSeq, openSeq, false); + if (typeof key === 'string' && key[0] === '#') { + let hexStyle = getHexStyleCache().get(key); + if (hexStyle === undefined) { + if (RegExpPrototypeExec(hexColorRegExp, key) === null) { + throw new ERR_INVALID_ARG_VALUE('format', key, + 'must be a valid hex color (#RGB or #RRGGBB)'); + } + if (skipColorize) continue; + hexStyle = getHexStyle(key); + } else if (skipColorize) { + continue; + } + openCodes += hexStyle.openSeq; + closeCodes = hexStyle.closeSeq + closeCodes; + processedText = replaceCloseCode(processedText, hexStyle.closeSeq, hexStyle.openSeq, false); continue; } const style = cache[key]; if (style === undefined) { - // Check if it looks like an invalid hex color (starts with #) - if (typeof key === 'string' && key[0] === '#') { - throw new ERR_INVALID_ARG_VALUE('format', key, - 'must be a valid hex color (#RGB or #RRGGBB)'); - } validateOneOf(key, 'format', ObjectGetOwnPropertyNames(inspect.colors)); } openCodes += style.openSeq; From 30a7e289b607fd91cfbc6e7bef961a0f1f85f1cc Mon Sep 17 00:00:00 2001 From: npm CLI robot Date: Fri, 29 May 2026 14:22:30 -0700 Subject: [PATCH 84/89] deps: upgrade npm to 11.16.0 PR-URL: https://github.com/nodejs/node/pull/63602 Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- .../content/commands/npm-approve-scripts.md | 125 ++++ deps/npm/docs/content/commands/npm-ci.md | 50 ++ .../docs/content/commands/npm-deny-scripts.md | 109 +++ deps/npm/docs/content/commands/npm-exec.md | 50 ++ .../content/commands/npm-install-ci-test.md | 50 ++ .../docs/content/commands/npm-install-test.md | 50 ++ deps/npm/docs/content/commands/npm-install.md | 50 ++ deps/npm/docs/content/commands/npm-ls.md | 2 +- deps/npm/docs/content/commands/npm-publish.md | 4 +- deps/npm/docs/content/commands/npm-rebuild.md | 50 ++ deps/npm/docs/content/commands/npm-stage.md | 4 +- deps/npm/docs/content/commands/npm-update.md | 50 ++ deps/npm/docs/content/commands/npm-version.md | 2 + deps/npm/docs/content/commands/npm.md | 2 +- deps/npm/docs/content/using-npm/config.md | 77 ++- deps/npm/docs/content/using-npm/scripts.md | 7 + deps/npm/docs/lib/index.js | 2 + deps/npm/docs/output/commands/npm-access.html | 4 +- .../npm/docs/output/commands/npm-adduser.html | 4 +- .../output/commands/npm-approve-scripts.html | 304 +++++++++ deps/npm/docs/output/commands/npm-audit.html | 4 +- deps/npm/docs/output/commands/npm-bugs.html | 4 +- deps/npm/docs/output/commands/npm-cache.html | 4 +- deps/npm/docs/output/commands/npm-ci.html | 44 +- .../docs/output/commands/npm-completion.html | 4 +- deps/npm/docs/output/commands/npm-config.html | 4 +- deps/npm/docs/output/commands/npm-dedupe.html | 4 +- .../output/commands/npm-deny-scripts.html | 290 ++++++++ .../docs/output/commands/npm-deprecate.html | 4 +- deps/npm/docs/output/commands/npm-diff.html | 4 +- .../docs/output/commands/npm-dist-tag.html | 4 +- deps/npm/docs/output/commands/npm-docs.html | 4 +- deps/npm/docs/output/commands/npm-doctor.html | 4 +- deps/npm/docs/output/commands/npm-edit.html | 4 +- deps/npm/docs/output/commands/npm-exec.html | 44 +- .../npm/docs/output/commands/npm-explain.html | 4 +- .../npm/docs/output/commands/npm-explore.html | 4 +- .../docs/output/commands/npm-find-dupes.html | 4 +- deps/npm/docs/output/commands/npm-fund.html | 4 +- deps/npm/docs/output/commands/npm-get.html | 4 +- .../docs/output/commands/npm-help-search.html | 4 +- deps/npm/docs/output/commands/npm-help.html | 4 +- deps/npm/docs/output/commands/npm-init.html | 4 +- .../output/commands/npm-install-ci-test.html | 44 +- .../output/commands/npm-install-test.html | 44 +- .../npm/docs/output/commands/npm-install.html | 44 +- deps/npm/docs/output/commands/npm-link.html | 4 +- deps/npm/docs/output/commands/npm-ll.html | 4 +- deps/npm/docs/output/commands/npm-login.html | 4 +- deps/npm/docs/output/commands/npm-logout.html | 4 +- deps/npm/docs/output/commands/npm-ls.html | 6 +- deps/npm/docs/output/commands/npm-org.html | 4 +- .../docs/output/commands/npm-outdated.html | 4 +- deps/npm/docs/output/commands/npm-owner.html | 4 +- deps/npm/docs/output/commands/npm-pack.html | 4 +- deps/npm/docs/output/commands/npm-ping.html | 4 +- deps/npm/docs/output/commands/npm-pkg.html | 4 +- deps/npm/docs/output/commands/npm-prefix.html | 4 +- .../npm/docs/output/commands/npm-profile.html | 4 +- deps/npm/docs/output/commands/npm-prune.html | 4 +- .../npm/docs/output/commands/npm-publish.html | 7 +- deps/npm/docs/output/commands/npm-query.html | 4 +- .../npm/docs/output/commands/npm-rebuild.html | 44 +- deps/npm/docs/output/commands/npm-repo.html | 4 +- .../npm/docs/output/commands/npm-restart.html | 4 +- deps/npm/docs/output/commands/npm-root.html | 4 +- deps/npm/docs/output/commands/npm-run.html | 4 +- deps/npm/docs/output/commands/npm-sbom.html | 4 +- deps/npm/docs/output/commands/npm-search.html | 4 +- deps/npm/docs/output/commands/npm-set.html | 4 +- .../docs/output/commands/npm-shrinkwrap.html | 4 +- deps/npm/docs/output/commands/npm-stage.html | 53 +- deps/npm/docs/output/commands/npm-star.html | 4 +- deps/npm/docs/output/commands/npm-stars.html | 4 +- deps/npm/docs/output/commands/npm-start.html | 4 +- deps/npm/docs/output/commands/npm-stop.html | 4 +- deps/npm/docs/output/commands/npm-team.html | 4 +- deps/npm/docs/output/commands/npm-test.html | 4 +- deps/npm/docs/output/commands/npm-token.html | 4 +- deps/npm/docs/output/commands/npm-trust.html | 4 +- .../docs/output/commands/npm-undeprecate.html | 4 +- .../docs/output/commands/npm-uninstall.html | 4 +- .../docs/output/commands/npm-unpublish.html | 4 +- deps/npm/docs/output/commands/npm-unstar.html | 4 +- deps/npm/docs/output/commands/npm-update.html | 44 +- .../npm/docs/output/commands/npm-version.html | 5 +- deps/npm/docs/output/commands/npm-view.html | 4 +- deps/npm/docs/output/commands/npm-whoami.html | 4 +- deps/npm/docs/output/commands/npm.html | 6 +- deps/npm/docs/output/commands/npx.html | 4 +- .../docs/output/configuring-npm/folders.html | 4 +- .../docs/output/configuring-npm/install.html | 4 +- .../output/configuring-npm/npm-global.html | 4 +- .../docs/output/configuring-npm/npm-json.html | 4 +- .../configuring-npm/npm-shrinkwrap-json.html | 4 +- .../docs/output/configuring-npm/npmrc.html | 4 +- .../output/configuring-npm/package-json.html | 4 +- .../configuring-npm/package-lock-json.html | 4 +- deps/npm/docs/output/using-npm/config.html | 64 +- .../using-npm/dependency-selectors.html | 4 +- .../npm/docs/output/using-npm/developers.html | 4 +- deps/npm/docs/output/using-npm/logging.html | 4 +- deps/npm/docs/output/using-npm/orgs.html | 4 +- .../docs/output/using-npm/package-spec.html | 4 +- deps/npm/docs/output/using-npm/registry.html | 4 +- deps/npm/docs/output/using-npm/removal.html | 4 +- deps/npm/docs/output/using-npm/scope.html | 4 +- deps/npm/docs/output/using-npm/scripts.html | 12 +- .../npm/docs/output/using-npm/workspaces.html | 4 +- deps/npm/lib/commands/approve-scripts.js | 10 + deps/npm/lib/commands/ci.js | 8 + deps/npm/lib/commands/config.js | 6 +- deps/npm/lib/commands/deny-scripts.js | 10 + deps/npm/lib/commands/edit.js | 10 +- deps/npm/lib/commands/exec.js | 12 + deps/npm/lib/commands/install.js | 8 + deps/npm/lib/commands/publish.js | 2 +- deps/npm/lib/commands/rebuild.js | 27 +- deps/npm/lib/commands/update.js | 11 +- deps/npm/lib/utils/allow-scripts-cmd.js | 245 +++++++ deps/npm/lib/utils/allow-scripts-writer.js | 323 +++++++++ deps/npm/lib/utils/check-allow-scripts.js | 54 ++ deps/npm/lib/utils/cmd-list.js | 2 + deps/npm/lib/utils/reify-finish.js | 6 +- deps/npm/lib/utils/reify-output.js | 52 +- deps/npm/lib/utils/resolve-allow-scripts.js | 181 +++++ .../utils/strict-allow-scripts-preflight.js | 61 ++ .../lib/utils/warn-workspace-allow-scripts.js | 40 ++ deps/npm/man/man1/npm-access.1 | 2 +- deps/npm/man/man1/npm-adduser.1 | 2 +- deps/npm/man/man1/npm-approve-scripts.1 | 113 ++++ deps/npm/man/man1/npm-audit.1 | 2 +- deps/npm/man/man1/npm-bugs.1 | 2 +- deps/npm/man/man1/npm-cache.1 | 2 +- deps/npm/man/man1/npm-ci.1 | 38 +- deps/npm/man/man1/npm-completion.1 | 2 +- deps/npm/man/man1/npm-config.1 | 2 +- deps/npm/man/man1/npm-dedupe.1 | 2 +- deps/npm/man/man1/npm-deny-scripts.1 | 99 +++ deps/npm/man/man1/npm-deprecate.1 | 2 +- deps/npm/man/man1/npm-diff.1 | 2 +- deps/npm/man/man1/npm-dist-tag.1 | 2 +- deps/npm/man/man1/npm-docs.1 | 2 +- deps/npm/man/man1/npm-doctor.1 | 2 +- deps/npm/man/man1/npm-edit.1 | 2 +- deps/npm/man/man1/npm-exec.1 | 38 +- deps/npm/man/man1/npm-explain.1 | 2 +- deps/npm/man/man1/npm-explore.1 | 2 +- deps/npm/man/man1/npm-find-dupes.1 | 2 +- deps/npm/man/man1/npm-fund.1 | 2 +- deps/npm/man/man1/npm-get.1 | 2 +- deps/npm/man/man1/npm-help-search.1 | 2 +- deps/npm/man/man1/npm-help.1 | 2 +- deps/npm/man/man1/npm-init.1 | 2 +- deps/npm/man/man1/npm-install-ci-test.1 | 38 +- deps/npm/man/man1/npm-install-test.1 | 38 +- deps/npm/man/man1/npm-install.1 | 38 +- deps/npm/man/man1/npm-link.1 | 2 +- deps/npm/man/man1/npm-ll.1 | 2 +- deps/npm/man/man1/npm-login.1 | 2 +- deps/npm/man/man1/npm-logout.1 | 2 +- deps/npm/man/man1/npm-ls.1 | 4 +- deps/npm/man/man1/npm-org.1 | 2 +- deps/npm/man/man1/npm-outdated.1 | 2 +- deps/npm/man/man1/npm-owner.1 | 2 +- deps/npm/man/man1/npm-pack.1 | 2 +- deps/npm/man/man1/npm-ping.1 | 2 +- deps/npm/man/man1/npm-pkg.1 | 2 +- deps/npm/man/man1/npm-prefix.1 | 2 +- deps/npm/man/man1/npm-profile.1 | 2 +- deps/npm/man/man1/npm-prune.1 | 2 +- deps/npm/man/man1/npm-publish.1 | 6 +- deps/npm/man/man1/npm-query.1 | 2 +- deps/npm/man/man1/npm-rebuild.1 | 38 +- deps/npm/man/man1/npm-repo.1 | 2 +- deps/npm/man/man1/npm-restart.1 | 2 +- deps/npm/man/man1/npm-root.1 | 2 +- deps/npm/man/man1/npm-run.1 | 2 +- deps/npm/man/man1/npm-sbom.1 | 2 +- deps/npm/man/man1/npm-search.1 | 2 +- deps/npm/man/man1/npm-set.1 | 2 +- deps/npm/man/man1/npm-shrinkwrap.1 | 2 +- deps/npm/man/man1/npm-stage.1 | 4 +- deps/npm/man/man1/npm-star.1 | 2 +- deps/npm/man/man1/npm-stars.1 | 2 +- deps/npm/man/man1/npm-start.1 | 2 +- deps/npm/man/man1/npm-stop.1 | 2 +- deps/npm/man/man1/npm-team.1 | 2 +- deps/npm/man/man1/npm-test.1 | 2 +- deps/npm/man/man1/npm-token.1 | 2 +- deps/npm/man/man1/npm-trust.1 | 2 +- deps/npm/man/man1/npm-undeprecate.1 | 2 +- deps/npm/man/man1/npm-uninstall.1 | 2 +- deps/npm/man/man1/npm-unpublish.1 | 2 +- deps/npm/man/man1/npm-unstar.1 | 2 +- deps/npm/man/man1/npm-update.1 | 38 +- deps/npm/man/man1/npm-version.1 | 4 +- deps/npm/man/man1/npm-view.1 | 2 +- deps/npm/man/man1/npm-whoami.1 | 2 +- deps/npm/man/man1/npm.1 | 4 +- deps/npm/man/man1/npx.1 | 2 +- deps/npm/man/man5/folders.5 | 2 +- deps/npm/man/man5/install.5 | 2 +- deps/npm/man/man5/npm-global.5 | 2 +- deps/npm/man/man5/npm-json.5 | 2 +- deps/npm/man/man5/npm-shrinkwrap-json.5 | 2 +- deps/npm/man/man5/npmrc.5 | 2 +- deps/npm/man/man5/package-json.5 | 2 +- deps/npm/man/man5/package-lock-json.5 | 2 +- deps/npm/man/man7/config.7 | 62 +- deps/npm/man/man7/dependency-selectors.7 | 2 +- deps/npm/man/man7/developers.7 | 2 +- deps/npm/man/man7/logging.7 | 2 +- deps/npm/man/man7/orgs.7 | 2 +- deps/npm/man/man7/package-spec.7 | 2 +- deps/npm/man/man7/registry.7 | 2 +- deps/npm/man/man7/removal.7 | 2 +- deps/npm/man/man7/scope.7 | 2 +- deps/npm/man/man7/scripts.7 | 12 +- deps/npm/man/man7/workspaces.7 | 2 +- .../node_modules/@npmcli/agent/lib/agents.js | 52 ++ .../node_modules/@npmcli/agent/lib/options.js | 4 + .../node_modules/@npmcli/agent/package.json | 16 +- .../@npmcli/arborist/lib/arborist/index.js | 2 + .../arborist/lib/arborist/isolated-reifier.js | 5 +- .../@npmcli/arborist/lib/arborist/rebuild.js | 13 + .../@npmcli/arborist/lib/arborist/reify.js | 12 +- .../@npmcli/arborist/lib/install-scripts.js | 88 +++ .../@npmcli/arborist/lib/script-allowed.js | 340 ++++++++++ .../@npmcli/arborist/package.json | 2 +- .../config/lib/definitions/definitions.js | 84 ++- .../config/lib/parse-allow-scripts-list.js | 23 + .../node_modules/@npmcli/config/package.json | 2 +- .../node_modules/@sigstore/core/dist/dsse.js | 15 +- .../node_modules/@sigstore/core/package.json | 2 +- .../@sigstore/verify/dist/key/index.js | 8 + .../@sigstore/verify/dist/policy.js | 26 + .../@sigstore/verify/dist/timestamp/index.js | 4 + .../@sigstore/verify/dist/verifier.js | 30 +- .../@sigstore/verify/package.json | 4 +- deps/npm/node_modules/libnpmdiff/package.json | 4 +- deps/npm/node_modules/libnpmexec/lib/index.js | 6 +- deps/npm/node_modules/libnpmexec/package.json | 4 +- deps/npm/node_modules/libnpmfund/package.json | 4 +- deps/npm/node_modules/libnpmpack/package.json | 4 +- deps/npm/node_modules/libnpmversion/README.md | 3 + .../node_modules/libnpmversion/package.json | 2 +- deps/npm/node_modules/lru-cache/package.json | 2 +- .../make-fetch-happen/package.json | 2 +- deps/npm/node_modules/semver/classes/range.js | 7 + deps/npm/node_modules/semver/package.json | 2 +- deps/npm/node_modules/semver/ranges/subset.js | 4 +- deps/npm/node_modules/sigstore/dist/config.js | 6 + deps/npm/node_modules/sigstore/package.json | 14 +- .../undici/lib/dispatcher/agent.js | 1 - .../undici/lib/dispatcher/client-h1.js | 90 ++- deps/npm/node_modules/undici/package.json | 2 +- deps/npm/package.json | 20 +- .../test/lib/commands/completion.js.test.cjs | 1 + .../test/lib/commands/config.js.test.cjs | 12 + .../test/lib/commands/publish.js.test.cjs | 24 + .../tap-snapshots/test/lib/docs.js.test.cjs | 293 +++++++- .../tap-snapshots/test/lib/npm.js.test.cjs | 130 ++-- deps/npm/test/lib/commands/approve-scripts.js | 562 +++++++++++++++ deps/npm/test/lib/commands/config.js | 6 + deps/npm/test/lib/commands/deny-scripts.js | 163 +++++ deps/npm/test/lib/commands/edit.js | 6 + deps/npm/test/lib/commands/exec.js | 65 ++ deps/npm/test/lib/commands/publish.js | 21 + deps/npm/test/lib/commands/rebuild.js | 60 ++ deps/npm/test/lib/commands/update.js | 30 + .../test/lib/utils/allow-scripts-writer.js | 637 ++++++++++++++++++ .../npm/test/lib/utils/check-allow-scripts.js | 263 ++++++++ deps/npm/test/lib/utils/reify-output.js | 111 +++ .../test/lib/utils/resolve-allow-scripts.js | 347 ++++++++++ .../utils/strict-allow-scripts-preflight.js | 191 ++++++ .../lib/utils/warn-workspace-allow-scripts.js | 108 +++ 277 files changed, 7294 insertions(+), 482 deletions(-) create mode 100644 deps/npm/docs/content/commands/npm-approve-scripts.md create mode 100644 deps/npm/docs/content/commands/npm-deny-scripts.md create mode 100644 deps/npm/docs/output/commands/npm-approve-scripts.html create mode 100644 deps/npm/docs/output/commands/npm-deny-scripts.html create mode 100644 deps/npm/lib/commands/approve-scripts.js create mode 100644 deps/npm/lib/commands/deny-scripts.js create mode 100644 deps/npm/lib/utils/allow-scripts-cmd.js create mode 100644 deps/npm/lib/utils/allow-scripts-writer.js create mode 100644 deps/npm/lib/utils/check-allow-scripts.js create mode 100644 deps/npm/lib/utils/resolve-allow-scripts.js create mode 100644 deps/npm/lib/utils/strict-allow-scripts-preflight.js create mode 100644 deps/npm/lib/utils/warn-workspace-allow-scripts.js create mode 100644 deps/npm/man/man1/npm-approve-scripts.1 create mode 100644 deps/npm/man/man1/npm-deny-scripts.1 create mode 100644 deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js create mode 100644 deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js create mode 100644 deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js create mode 100644 deps/npm/test/lib/commands/approve-scripts.js create mode 100644 deps/npm/test/lib/commands/deny-scripts.js create mode 100644 deps/npm/test/lib/utils/allow-scripts-writer.js create mode 100644 deps/npm/test/lib/utils/check-allow-scripts.js create mode 100644 deps/npm/test/lib/utils/resolve-allow-scripts.js create mode 100644 deps/npm/test/lib/utils/strict-allow-scripts-preflight.js create mode 100644 deps/npm/test/lib/utils/warn-workspace-allow-scripts.js diff --git a/deps/npm/docs/content/commands/npm-approve-scripts.md b/deps/npm/docs/content/commands/npm-approve-scripts.md new file mode 100644 index 00000000000000..e3445447c79052 --- /dev/null +++ b/deps/npm/docs/content/commands/npm-approve-scripts.md @@ -0,0 +1,125 @@ +--- +title: npm-approve-scripts +section: 1 +description: Approve install scripts for specific dependencies +--- + +### Synopsis + +```bash +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +``` + +Note: This command is unaware of workspaces. + +### Description + +Manages the `allowScripts` field in your project's `package.json`, which +records which of your dependencies are permitted to run install scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +sources). This command is the recommended way to maintain that field. + +In the current release, this field is advisory: install scripts still run +by default, but installs print a list of packages whose scripts have not +been reviewed. A future release will block unreviewed install scripts. + +There are three modes: + +```bash +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +``` + +`` matches every installed version of that package. By default the +command writes pinned entries (`pkg@1.2.3`), which keep their approval +narrowed to the specific version you reviewed. Pass `--no-allow-scripts-pin` to write +name-only entries that allow any future version. + +`--all` approves every package with unreviewed install scripts in one go. + +`--allow-scripts-pending` is read-only: it lists every package whose install scripts +are not yet covered by `allowScripts`, without modifying `package.json`. + +`approve-scripts` honours the asymmetric pin rule: if you re-approve a +package whose installed version has changed, the existing pin is rewritten +to track the new installed version. Multi-version statements +(`pkg@1 || 2`) are left alone, since they likely capture intent that +the command cannot infer. Existing `false` entries always win; +`approve-scripts` will not silently re-allow a package you previously +denied. + +### Examples + +```bash +# Approve all currently-installed install scripts after reviewing them +npm approve-scripts --all + +# Approve specific packages, pinned to their installed version +npm approve-scripts canvas sharp + +# Approve name-only (any version of this package is allowed) +npm approve-scripts --no-allow-scripts-pin canvas + +# Preview which packages still need review +npm approve-scripts --allow-scripts-pending +``` + +### Configuration + +#### `all` + +* Default: false +* Type: Boolean + +When running `npm outdated` and `npm ls`, setting `--all` will show all +outdated or installed packages, rather than only those directly depended +upon by the current project. + + + +#### `allow-scripts-pending` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +`allowScripts` policy, without modifying `package.json`. Only meaningful for +`npm approve-scripts`. + + + +#### `allow-scripts-pin` + +* Default: true +* Type: Boolean + +Write pinned (`pkg@version`) entries when approving install scripts. Set to +`false` to write name-only entries that allow any version. Has no effect on +`npm deny-scripts`, which always writes name-only entries regardless of this +setting. + + + +#### `json` + +* Default: false +* Type: Boolean + +Whether or not to output JSON data, rather than the normal output. + +* In `npm pkg set` it enables parsing set values with JSON.parse() before + saving them to your `package.json`. + +Not supported by all npm commands. + + + +### See Also + +* [npm deny-scripts](/commands/npm-deny-scripts) +* [npm install](/commands/npm-install) +* [npm rebuild](/commands/npm-rebuild) +* [package.json](/configuring-npm/package-json) diff --git a/deps/npm/docs/content/commands/npm-ci.md b/deps/npm/docs/content/commands/npm-ci.md index 6f8dd5bd3f6655..bc460070459604 100644 --- a/deps/npm/docs/content/commands/npm-ci.md +++ b/deps/npm/docs/content/commands/npm-ci.md @@ -262,6 +262,56 @@ like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-deny-scripts.md b/deps/npm/docs/content/commands/npm-deny-scripts.md new file mode 100644 index 00000000000000..51915b09fe1dfc --- /dev/null +++ b/deps/npm/docs/content/commands/npm-deny-scripts.md @@ -0,0 +1,109 @@ +--- +title: npm-deny-scripts +section: 1 +description: Deny install scripts for specific dependencies +--- + +### Synopsis + +```bash +npm deny-scripts [ ...] +npm deny-scripts --all +``` + +Note: This command is unaware of workspaces. + +### Description + +The companion command to [`npm approve-scripts`](/commands/npm-approve-scripts). +Writes `false` entries into the `allowScripts` field of your project's +`package.json`, recording that a dependency must not run install scripts +even if a future version would otherwise be eligible. + +In the current release, install scripts still run by default, so `deny-scripts` +only affects how installs of denied packages are reported. A future release +will block unreviewed install scripts and respect deny entries at install +time. + +```bash +npm deny-scripts [ ...] +npm deny-scripts --all +``` + +`` matches every installed version of that package. Denies are always +written name-only (`"pkg": false`), regardless of `--allow-scripts-pin`. Pinning a deny +to a specific version would silently re-allow scripts for any other version +of the same package, which defeats the purpose; the command picks the +safer default for you. + +`--all` denies every package with unreviewed install scripts. + +If a `true` (pinned or name-only) entry exists for a package and you then +deny it, the existing allow entries are removed so the name-only deny is +unambiguous. + +### Examples + +```bash +# Deny a specific package outright +npm deny-scripts telemetry-pkg + +# Deny everything that has install scripts and isn't already approved +npm deny-scripts --all +``` + +### Configuration + +#### `all` + +* Default: false +* Type: Boolean + +When running `npm outdated` and `npm ls`, setting `--all` will show all +outdated or installed packages, rather than only those directly depended +upon by the current project. + + + +#### `allow-scripts-pending` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +`allowScripts` policy, without modifying `package.json`. Only meaningful for +`npm approve-scripts`. + + + +#### `allow-scripts-pin` + +* Default: true +* Type: Boolean + +Write pinned (`pkg@version`) entries when approving install scripts. Set to +`false` to write name-only entries that allow any version. Has no effect on +`npm deny-scripts`, which always writes name-only entries regardless of this +setting. + + + +#### `json` + +* Default: false +* Type: Boolean + +Whether or not to output JSON data, rather than the normal output. + +* In `npm pkg set` it enables parsing set values with JSON.parse() before + saving them to your `package.json`. + +Not supported by all npm commands. + + + +### See Also + +* [npm approve-scripts](/commands/npm-approve-scripts) +* [npm install](/commands/npm-install) +* [package.json](/configuring-npm/package-json) diff --git a/deps/npm/docs/content/commands/npm-exec.md b/deps/npm/docs/content/commands/npm-exec.md index 72c63163be4d2f..13a0939209a5ea 100644 --- a/deps/npm/docs/content/commands/npm-exec.md +++ b/deps/npm/docs/content/commands/npm-exec.md @@ -158,6 +158,56 @@ the specified workspaces, and not on the root project. This value is not exported to the environment for child processes. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + ### Examples Run the version of `tap` in the local dependencies, with the provided arguments: diff --git a/deps/npm/docs/content/commands/npm-install-ci-test.md b/deps/npm/docs/content/commands/npm-install-ci-test.md index 22dc87ce8bb6ca..4528f63dfe28e8 100644 --- a/deps/npm/docs/content/commands/npm-install-ci-test.md +++ b/deps/npm/docs/content/commands/npm-install-ci-test.md @@ -215,6 +215,56 @@ like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-install-test.md b/deps/npm/docs/content/commands/npm-install-test.md index 999019bde3668d..5a2f33a84ca96d 100644 --- a/deps/npm/docs/content/commands/npm-install-test.md +++ b/deps/npm/docs/content/commands/npm-install-test.md @@ -292,6 +292,56 @@ like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-install.md b/deps/npm/docs/content/commands/npm-install.md index 925439ceb21dd3..7bc00701e7bf2d 100644 --- a/deps/npm/docs/content/commands/npm-install.md +++ b/deps/npm/docs/content/commands/npm-install.md @@ -634,6 +634,56 @@ like `npm view` +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-ls.md b/deps/npm/docs/content/commands/npm-ls.md index 91e06ed30836d4..c0a341e46fd10b 100644 --- a/deps/npm/docs/content/commands/npm-ls.md +++ b/deps/npm/docs/content/commands/npm-ls.md @@ -23,7 +23,7 @@ Note that nested packages will *also* show the paths to the specified packages. For example, running `npm ls promzard` in npm's source tree will show: ```bash -npm@11.15.0 /path/to/npm +npm@11.16.0 /path/to/npm └─┬ init-package-json@0.0.4 └── promzard@0.1.5 ``` diff --git a/deps/npm/docs/content/commands/npm-publish.md b/deps/npm/docs/content/commands/npm-publish.md index c69e187429eabb..04c020b3563f7d 100644 --- a/deps/npm/docs/content/commands/npm-publish.md +++ b/deps/npm/docs/content/commands/npm-publish.md @@ -117,7 +117,7 @@ the package submitted to the registry. * Default: 'public' for new packages, existing packages it will not change the current level -* Type: null, "restricted", or "public" +* Type: null, "restricted", "public", or "private" If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. @@ -129,6 +129,8 @@ packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. +The value `private` is an alias for `restricted`. + #### `dry-run` diff --git a/deps/npm/docs/content/commands/npm-rebuild.md b/deps/npm/docs/content/commands/npm-rebuild.md index 9fb43567ac2eb4..18b1d37c779956 100644 --- a/deps/npm/docs/content/commands/npm-rebuild.md +++ b/deps/npm/docs/content/commands/npm-rebuild.md @@ -100,6 +100,56 @@ run any pre- or post-scripts. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `workspace` * Default: diff --git a/deps/npm/docs/content/commands/npm-stage.md b/deps/npm/docs/content/commands/npm-stage.md index 9a761bbd8a5146..cda1b493f9ac4f 100644 --- a/deps/npm/docs/content/commands/npm-stage.md +++ b/deps/npm/docs/content/commands/npm-stage.md @@ -152,9 +152,7 @@ npm stage publish | Flag | Default | Type | Description | | --- | --- | --- | --- | | `--tag` | "latest" | String | If you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. It is the tag added to the package@version specified in the `npm dist-tag add` command, if no explicit tag is given. When used by the `npm diff` command, this is the tag used to fetch the tarball that will be compared with the local files by default. If used in the `npm publish` command, this is the tag that will be added to the package submitted to the registry. | -| `--access` | - 'public' for new packages, existing packages it will not change the current level - | null, "restricted", or "public" | If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. Unscoped packages cannot be set to `restricted`. Note: This defaults to not changing the current access level for existing packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. | +| `--access` | 'public' for new packages, existing packages it will not change the current level | null, "restricted", "public", or "private" | If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. Unscoped packages cannot be set to `restricted`. Note: This defaults to not changing the current access level for existing packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. The value `private` is an alias for `restricted`. | | `--dry-run` | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, `install`, `update`, `dedupe`, `uninstall`, as well as `pack` and `publish`. Note: This is NOT honored by other network related commands, eg `dist-tags`, `owner`, etc. | | `--otp` | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with `npm access`. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | | `--workspace`, `-w` | | String (can be set multiple times) | Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the `workspace` config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the `npm init` command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. | diff --git a/deps/npm/docs/content/commands/npm-update.md b/deps/npm/docs/content/commands/npm-update.md index 4dc7e9a1edccbe..86b54eed070924 100644 --- a/deps/npm/docs/content/commands/npm-update.md +++ b/deps/npm/docs/content/commands/npm-update.md @@ -303,6 +303,56 @@ run any pre- or post-scripts. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `audit` * Default: true diff --git a/deps/npm/docs/content/commands/npm-version.md b/deps/npm/docs/content/commands/npm-version.md index cd504b37b7f5eb..9016c8071f7593 100644 --- a/deps/npm/docs/content/commands/npm-version.md +++ b/deps/npm/docs/content/commands/npm-version.md @@ -229,6 +229,8 @@ The exact order of execution is as follows: 6. Run the `postversion` script. Use it to clean up the file system or automatically push the commit and/or tag. +For the `preversion`, `version` and `postversion` scripts, npm also sets the [environment variables](/using-npm/scripts#environment) `npm_old_version` and `npm_new_version`. + Take the following example: ```json diff --git a/deps/npm/docs/content/commands/npm.md b/deps/npm/docs/content/commands/npm.md index 89e5dd6ef50d78..ba1890149fcdd0 100644 --- a/deps/npm/docs/content/commands/npm.md +++ b/deps/npm/docs/content/commands/npm.md @@ -14,7 +14,7 @@ Note: This command is unaware of workspaces. ### Version -11.15.0 +11.16.0 ### Description diff --git a/deps/npm/docs/content/using-npm/config.md b/deps/npm/docs/content/using-npm/config.md index 0e99c58f3c002b..d1167e0d14880b 100644 --- a/deps/npm/docs/content/using-npm/config.md +++ b/deps/npm/docs/content/using-npm/config.md @@ -140,7 +140,7 @@ safer to use a registry-provided authentication bearer token stored in the * Default: 'public' for new packages, existing packages it will not change the current level -* Type: null, "restricted", or "public" +* Type: null, "restricted", "public", or "private" If you do not want your scoped package to be publicly viewable (and installable) set `--access=restricted`. @@ -152,6 +152,8 @@ packages. Specifying a value of `restricted` or `public` during publish will change the access for an existing package the same way that `npm access set status` would. +The value `private` is an alias for `restricted`. + #### `all` @@ -248,6 +250,51 @@ to the same value as the current version. +#### `allow-scripts` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: `npm exec`, `npx`, +and `npm install -g`, where no project `package.json` is involved. For +team-wide policy in a project, use the `allowScripts` field in +`package.json` (which also supports explicit denials), or configure it in +`.npmrc`. Passing `--allow-scripts` on the command line during a +project-scoped `npm install`, `ci`, `update`, or `rebuild` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. `--ignore-scripts` and +`--dangerously-allow-all-scripts` both override this setting. + + + +#### `allow-scripts-pending` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +`allowScripts` policy, without modifying `package.json`. Only meaningful for +`npm approve-scripts`. + + + +#### `allow-scripts-pin` + +* Default: true +* Type: Boolean + +Write pinned (`pkg@version`) entries when approving install scripts. Set to +`false` to write name-only entries that allow any version. Has no effect on +`npm deny-scripts`, which always writes name-only entries regardless of this +setting. + + + #### `audit` * Default: true @@ -443,6 +490,18 @@ are same as `cpu` field of package.json, which comes from `process.arch`. +#### `dangerously-allow-all-scripts` + +* Default: false +* Type: Boolean + +If `true`, bypass the `allowScripts` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +`--ignore-scripts` still takes precedence over this setting. + + + #### `depth` * Default: `Infinity` if `--all` is set; otherwise, `0` @@ -1769,6 +1828,22 @@ this to work properly. +#### `strict-allow-scripts` + +* Default: false +* Type: Boolean + +If `true`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by `allowScripts` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with `false` in `allowScripts` are always +silently skipped; this setting only affects unreviewed entries. +`--ignore-scripts` and `--dangerously-allow-all-scripts` both override this +setting. + + + #### `strict-peer-deps` * Default: false diff --git a/deps/npm/docs/content/using-npm/scripts.md b/deps/npm/docs/content/using-npm/scripts.md index 91de8f22d47f0a..dcae0c66da0e3f 100644 --- a/deps/npm/docs/content/using-npm/scripts.md +++ b/deps/npm/docs/content/using-npm/scripts.md @@ -290,6 +290,13 @@ For example, if you had `{"name":"foo", "version":"1.2.5"}` in your package.json See [`package.json`](/configuring-npm/package-json) for more on package configs. +#### versioning variables + +For versioning scripts (`preversion`, `version`, `postversion`), npm sets these environment variables: + +* `npm_old_version` - The version before being bumped +* `npm_new_version` – The version after being bumped + #### current lifecycle event Lastly, the `npm_lifecycle_event` environment variable is set to whichever stage of the cycle is being executed. diff --git a/deps/npm/docs/lib/index.js b/deps/npm/docs/lib/index.js index d7a5e83ccf5062..9779d546572930 100644 --- a/deps/npm/docs/lib/index.js +++ b/deps/npm/docs/lib/index.js @@ -151,10 +151,12 @@ const generateFlagsTable = (definitionPool) => { if (!defaultVal) { defaultVal = String(def.default) } + defaultVal = defaultVal.replace(/\n/g, ' ').trim() let typeVal = def.typeDescription || String(def.type) if (def.required) { typeVal = `${typeVal} (required)` } + typeVal = typeVal.replace(/\n/g, ' ').trim() const desc = (def.description || '').replace(/\n/g, ' ').trim() return `| ${flagsStr} | ${defaultVal} | ${typeVal} | ${desc} |` }) diff --git a/deps/npm/docs/output/commands/npm-access.html b/deps/npm/docs/output/commands/npm-access.html index 224177685f2e4a..983b44e86a639d 100644 --- a/deps/npm/docs/output/commands/npm-access.html +++ b/deps/npm/docs/output/commands/npm-access.html @@ -186,9 +186,9 @@
-

+

npm-access - @11.15.0 + @11.16.0

Set access level on published packages
diff --git a/deps/npm/docs/output/commands/npm-adduser.html b/deps/npm/docs/output/commands/npm-adduser.html index c73b60b16ed7ea..8945c6ef6cb33b 100644 --- a/deps/npm/docs/output/commands/npm-adduser.html +++ b/deps/npm/docs/output/commands/npm-adduser.html @@ -186,9 +186,9 @@
-

+

npm-adduser - @11.15.0 + @11.16.0

Add a registry user account
diff --git a/deps/npm/docs/output/commands/npm-approve-scripts.html b/deps/npm/docs/output/commands/npm-approve-scripts.html new file mode 100644 index 00000000000000..1849ae8c5011c4 --- /dev/null +++ b/deps/npm/docs/output/commands/npm-approve-scripts.html @@ -0,0 +1,304 @@ + + +npm-approve-scripts + + + + + +
+
+

+ npm-approve-scripts + @11.16.0 +

+Approve install scripts for specific dependencies +
+ +
+

Table of contents

+ +
+ +

Synopsis

+
npm approve-scripts <pkg> [<pkg> ...]
+npm approve-scripts --all
+npm approve-scripts --allow-scripts-pending
+
+

Note: This command is unaware of workspaces.

+

Description

+

Manages the allowScripts field in your project's package.json, which +records which of your dependencies are permitted to run install scripts +(preinstall, install, postinstall, and prepare for non-registry +sources). This command is the recommended way to maintain that field.

+

In the current release, this field is advisory: install scripts still run +by default, but installs print a list of packages whose scripts have not +been reviewed. A future release will block unreviewed install scripts.

+

There are three modes:

+
npm approve-scripts <pkg> [<pkg> ...]
+npm approve-scripts --all
+npm approve-scripts --allow-scripts-pending
+
+

<pkg> matches every installed version of that package. By default the +command writes pinned entries (pkg@1.2.3), which keep their approval +narrowed to the specific version you reviewed. Pass --no-allow-scripts-pin to write +name-only entries that allow any future version.

+

--all approves every package with unreviewed install scripts in one go.

+

--allow-scripts-pending is read-only: it lists every package whose install scripts +are not yet covered by allowScripts, without modifying package.json.

+

approve-scripts honours the asymmetric pin rule: if you re-approve a +package whose installed version has changed, the existing pin is rewritten +to track the new installed version. Multi-version statements +(pkg@1 || 2) are left alone, since they likely capture intent that +the command cannot infer. Existing false entries always win; +approve-scripts will not silently re-allow a package you previously +denied.

+

Examples

+
# Approve all currently-installed install scripts after reviewing them
+npm approve-scripts --all
+
+# Approve specific packages, pinned to their installed version
+npm approve-scripts canvas sharp
+
+# Approve name-only (any version of this package is allowed)
+npm approve-scripts --no-allow-scripts-pin canvas
+
+# Preview which packages still need review
+npm approve-scripts --allow-scripts-pending
+
+

Configuration

+

all

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

When running npm outdated and npm ls, setting --all will show all +outdated or installed packages, rather than only those directly depended +upon by the current project.

+

allow-scripts-pending

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

List packages with install scripts that are not yet covered by the +allowScripts policy, without modifying package.json. Only meaningful for +npm approve-scripts.

+

allow-scripts-pin

+
    +
  • Default: true
  • +
  • Type: Boolean
  • +
+

Write pinned (pkg@version) entries when approving install scripts. Set to +false to write name-only entries that allow any version. Has no effect on +npm deny-scripts, which always writes name-only entries regardless of this +setting.

+

json

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

Whether or not to output JSON data, rather than the normal output.

+
    +
  • In npm pkg set it enables parsing set values with JSON.parse() before +saving them to your package.json.
  • +
+

Not supported by all npm commands.

+

See Also

+
+ + +
+ + + + \ No newline at end of file diff --git a/deps/npm/docs/output/commands/npm-audit.html b/deps/npm/docs/output/commands/npm-audit.html index eff894d79e5adf..f018a7ae7f1c57 100644 --- a/deps/npm/docs/output/commands/npm-audit.html +++ b/deps/npm/docs/output/commands/npm-audit.html @@ -186,9 +186,9 @@
-

+

npm-audit - @11.15.0 + @11.16.0

Run a security audit
diff --git a/deps/npm/docs/output/commands/npm-bugs.html b/deps/npm/docs/output/commands/npm-bugs.html index 143b82ff563534..45ca5bec8ef537 100644 --- a/deps/npm/docs/output/commands/npm-bugs.html +++ b/deps/npm/docs/output/commands/npm-bugs.html @@ -186,9 +186,9 @@
-

+

npm-bugs - @11.15.0 + @11.16.0

Report bugs for a package in a web browser
diff --git a/deps/npm/docs/output/commands/npm-cache.html b/deps/npm/docs/output/commands/npm-cache.html index 185ac37463e9dd..0e561b39dabdaa 100644 --- a/deps/npm/docs/output/commands/npm-cache.html +++ b/deps/npm/docs/output/commands/npm-cache.html @@ -186,9 +186,9 @@
-

+

npm-cache - @11.15.0 + @11.16.0

Manipulates packages cache
diff --git a/deps/npm/docs/output/commands/npm-ci.html b/deps/npm/docs/output/commands/npm-ci.html index 772d4ae14de699..745a22ea53c966 100644 --- a/deps/npm/docs/output/commands/npm-ci.html +++ b/deps/npm/docs/output/commands/npm-ci.html @@ -186,16 +186,16 @@
-

+

npm-ci - @11.15.0 + @11.16.0

Clean install a project

Table of contents

- +

Synopsis

@@ -390,6 +390,44 @@

allow-remote

installed. root only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like npm view

+

allow-scripts

+
    +
  • Default: ""
  • +
  • Type: String (can be set multiple times)
  • +
+

Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

+

This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

+

Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

+

strict-allow-scripts

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

+

Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

+

dangerously-allow-all-scripts

+
    +
  • Default: false
  • +
  • Type: Boolean
  • +
+

If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

audit

  • Default: true
  • diff --git a/deps/npm/docs/output/commands/npm-completion.html b/deps/npm/docs/output/commands/npm-completion.html index 161ac0c6de585a..866085f42351f5 100644 --- a/deps/npm/docs/output/commands/npm-completion.html +++ b/deps/npm/docs/output/commands/npm-completion.html @@ -186,9 +186,9 @@
    -

    +

    npm-completion - @11.15.0 + @11.16.0

    Tab Completion for npm
    diff --git a/deps/npm/docs/output/commands/npm-config.html b/deps/npm/docs/output/commands/npm-config.html index 0e8ebe721f7ec2..3f9be6dedfb64a 100644 --- a/deps/npm/docs/output/commands/npm-config.html +++ b/deps/npm/docs/output/commands/npm-config.html @@ -186,9 +186,9 @@
    -

    +

    npm-config - @11.15.0 + @11.16.0

    Manage the npm configuration files
    diff --git a/deps/npm/docs/output/commands/npm-dedupe.html b/deps/npm/docs/output/commands/npm-dedupe.html index f0d32be8df62b7..e15165a5d00e44 100644 --- a/deps/npm/docs/output/commands/npm-dedupe.html +++ b/deps/npm/docs/output/commands/npm-dedupe.html @@ -186,9 +186,9 @@
    -

    +

    npm-dedupe - @11.15.0 + @11.16.0

    Reduce duplication in the package tree
    diff --git a/deps/npm/docs/output/commands/npm-deny-scripts.html b/deps/npm/docs/output/commands/npm-deny-scripts.html new file mode 100644 index 00000000000000..e9b18afb88b2a2 --- /dev/null +++ b/deps/npm/docs/output/commands/npm-deny-scripts.html @@ -0,0 +1,290 @@ + + +npm-deny-scripts + + + + + +
    +
    +

    + npm-deny-scripts + @11.16.0 +

    +Deny install scripts for specific dependencies +
    + +
    +

    Table of contents

    + +
    + +

    Synopsis

    +
    npm deny-scripts <pkg> [<pkg> ...]
    +npm deny-scripts --all
    +
    +

    Note: This command is unaware of workspaces.

    +

    Description

    +

    The companion command to npm approve-scripts. +Writes false entries into the allowScripts field of your project's +package.json, recording that a dependency must not run install scripts +even if a future version would otherwise be eligible.

    +

    In the current release, install scripts still run by default, so deny-scripts +only affects how installs of denied packages are reported. A future release +will block unreviewed install scripts and respect deny entries at install +time.

    +
    npm deny-scripts <pkg> [<pkg> ...]
    +npm deny-scripts --all
    +
    +

    <pkg> matches every installed version of that package. Denies are always +written name-only ("pkg": false), regardless of --allow-scripts-pin. Pinning a deny +to a specific version would silently re-allow scripts for any other version +of the same package, which defeats the purpose; the command picks the +safer default for you.

    +

    --all denies every package with unreviewed install scripts.

    +

    If a true (pinned or name-only) entry exists for a package and you then +deny it, the existing allow entries are removed so the name-only deny is +unambiguous.

    +

    Examples

    +
    # Deny a specific package outright
    +npm deny-scripts telemetry-pkg
    +
    +# Deny everything that has install scripts and isn't already approved
    +npm deny-scripts --all
    +
    +

    Configuration

    +

    all

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    When running npm outdated and npm ls, setting --all will show all +outdated or installed packages, rather than only those directly depended +upon by the current project.

    +

    allow-scripts-pending

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    List packages with install scripts that are not yet covered by the +allowScripts policy, without modifying package.json. Only meaningful for +npm approve-scripts.

    +

    allow-scripts-pin

    +
      +
    • Default: true
    • +
    • Type: Boolean
    • +
    +

    Write pinned (pkg@version) entries when approving install scripts. Set to +false to write name-only entries that allow any version. Has no effect on +npm deny-scripts, which always writes name-only entries regardless of this +setting.

    +

    json

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    Whether or not to output JSON data, rather than the normal output.

    +
      +
    • In npm pkg set it enables parsing set values with JSON.parse() before +saving them to your package.json.
    • +
    +

    Not supported by all npm commands.

    +

    See Also

    +
    + + +
    + + + + \ No newline at end of file diff --git a/deps/npm/docs/output/commands/npm-deprecate.html b/deps/npm/docs/output/commands/npm-deprecate.html index 8286ff6dfde58b..9bda62f1a891ca 100644 --- a/deps/npm/docs/output/commands/npm-deprecate.html +++ b/deps/npm/docs/output/commands/npm-deprecate.html @@ -186,9 +186,9 @@
    -

    +

    npm-deprecate - @11.15.0 + @11.16.0

    Deprecate a version of a package
    diff --git a/deps/npm/docs/output/commands/npm-diff.html b/deps/npm/docs/output/commands/npm-diff.html index b006a5fe1565d9..7b72340cb35e86 100644 --- a/deps/npm/docs/output/commands/npm-diff.html +++ b/deps/npm/docs/output/commands/npm-diff.html @@ -186,9 +186,9 @@
    -

    +

    npm-diff - @11.15.0 + @11.16.0

    The registry diff command
    diff --git a/deps/npm/docs/output/commands/npm-dist-tag.html b/deps/npm/docs/output/commands/npm-dist-tag.html index ea3f353bce7e13..3b95fe2e3ebc7e 100644 --- a/deps/npm/docs/output/commands/npm-dist-tag.html +++ b/deps/npm/docs/output/commands/npm-dist-tag.html @@ -186,9 +186,9 @@
    -

    +

    npm-dist-tag - @11.15.0 + @11.16.0

    Modify package distribution tags
    diff --git a/deps/npm/docs/output/commands/npm-docs.html b/deps/npm/docs/output/commands/npm-docs.html index fa3f70a34e43d9..a12dd65697c1df 100644 --- a/deps/npm/docs/output/commands/npm-docs.html +++ b/deps/npm/docs/output/commands/npm-docs.html @@ -186,9 +186,9 @@
    -

    +

    npm-docs - @11.15.0 + @11.16.0

    Open documentation for a package in a web browser
    diff --git a/deps/npm/docs/output/commands/npm-doctor.html b/deps/npm/docs/output/commands/npm-doctor.html index 35289c9f4baa65..d9094606fa2a14 100644 --- a/deps/npm/docs/output/commands/npm-doctor.html +++ b/deps/npm/docs/output/commands/npm-doctor.html @@ -186,9 +186,9 @@
    -

    +

    npm-doctor - @11.15.0 + @11.16.0

    Check the health of your npm environment
    diff --git a/deps/npm/docs/output/commands/npm-edit.html b/deps/npm/docs/output/commands/npm-edit.html index 47a653e76bc67e..a5f7d958498c21 100644 --- a/deps/npm/docs/output/commands/npm-edit.html +++ b/deps/npm/docs/output/commands/npm-edit.html @@ -186,9 +186,9 @@
    -

    +

    npm-edit - @11.15.0 + @11.16.0

    Edit an installed package
    diff --git a/deps/npm/docs/output/commands/npm-exec.html b/deps/npm/docs/output/commands/npm-exec.html index b27f5aeef44ff5..333fd7312a6140 100644 --- a/deps/npm/docs/output/commands/npm-exec.html +++ b/deps/npm/docs/output/commands/npm-exec.html @@ -186,16 +186,16 @@
    -

    +

    npm-exec - @11.15.0 + @11.16.0

    Run a command from a local or remote npm package

    Table of contents

    - +

    Synopsis

    @@ -307,6 +307,44 @@

    include-workspace-root

    all workspaces via the workspaces flag, will cause npm to operate only on the specified workspaces, and not on the root project.

    This value is not exported to the environment for child processes.

    +

    allow-scripts

    +
      +
    • Default: ""
    • +
    • Type: String (can be set multiple times)
    • +
    +

    Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

    +

    This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

    +

    Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

    +

    strict-allow-scripts

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

    +

    Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

    +

    dangerously-allow-all-scripts

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

    Examples

    Run the version of tap in the local dependencies, with the provided arguments:

    $ npm exec -- tap --bail test/foo.js
    diff --git a/deps/npm/docs/output/commands/npm-explain.html b/deps/npm/docs/output/commands/npm-explain.html
    index 4e054a719f6943..feef6d1cf95315 100644
    --- a/deps/npm/docs/output/commands/npm-explain.html
    +++ b/deps/npm/docs/output/commands/npm-explain.html
    @@ -186,9 +186,9 @@
     
     
    -

    +

    npm-explain - @11.15.0 + @11.16.0

    Explain installed packages
    diff --git a/deps/npm/docs/output/commands/npm-explore.html b/deps/npm/docs/output/commands/npm-explore.html index 0302e915dc24df..0985c9bb2e8a19 100644 --- a/deps/npm/docs/output/commands/npm-explore.html +++ b/deps/npm/docs/output/commands/npm-explore.html @@ -186,9 +186,9 @@
    -

    +

    npm-explore - @11.15.0 + @11.16.0

    Browse an installed package
    diff --git a/deps/npm/docs/output/commands/npm-find-dupes.html b/deps/npm/docs/output/commands/npm-find-dupes.html index c0e9ecaecd3884..c013dd0db414bf 100644 --- a/deps/npm/docs/output/commands/npm-find-dupes.html +++ b/deps/npm/docs/output/commands/npm-find-dupes.html @@ -186,9 +186,9 @@
    -

    +

    npm-find-dupes - @11.15.0 + @11.16.0

    Find duplication in the package tree
    diff --git a/deps/npm/docs/output/commands/npm-fund.html b/deps/npm/docs/output/commands/npm-fund.html index 43aa08e534662c..927ce09fa8cde9 100644 --- a/deps/npm/docs/output/commands/npm-fund.html +++ b/deps/npm/docs/output/commands/npm-fund.html @@ -186,9 +186,9 @@
    -

    +

    npm-fund - @11.15.0 + @11.16.0

    Retrieve funding information
    diff --git a/deps/npm/docs/output/commands/npm-get.html b/deps/npm/docs/output/commands/npm-get.html index ba24cd9fe4e5b2..675a4fcecb9855 100644 --- a/deps/npm/docs/output/commands/npm-get.html +++ b/deps/npm/docs/output/commands/npm-get.html @@ -186,9 +186,9 @@
    -

    +

    npm-get - @11.15.0 + @11.16.0

    Get a value from the npm configuration
    diff --git a/deps/npm/docs/output/commands/npm-help-search.html b/deps/npm/docs/output/commands/npm-help-search.html index 839f3d9e3df53c..ba77fc89ae508a 100644 --- a/deps/npm/docs/output/commands/npm-help-search.html +++ b/deps/npm/docs/output/commands/npm-help-search.html @@ -186,9 +186,9 @@
    -

    +

    npm-help-search - @11.15.0 + @11.16.0

    Search npm help documentation
    diff --git a/deps/npm/docs/output/commands/npm-help.html b/deps/npm/docs/output/commands/npm-help.html index 76b8f03fdabd23..5c83c72e328208 100644 --- a/deps/npm/docs/output/commands/npm-help.html +++ b/deps/npm/docs/output/commands/npm-help.html @@ -186,9 +186,9 @@
    -

    +

    npm-help - @11.15.0 + @11.16.0

    Get help on npm
    diff --git a/deps/npm/docs/output/commands/npm-init.html b/deps/npm/docs/output/commands/npm-init.html index 570d84d38c09e8..321a462a5f0162 100644 --- a/deps/npm/docs/output/commands/npm-init.html +++ b/deps/npm/docs/output/commands/npm-init.html @@ -186,9 +186,9 @@
    -

    +

    npm-init - @11.15.0 + @11.16.0

    Create a package.json file
    diff --git a/deps/npm/docs/output/commands/npm-install-ci-test.html b/deps/npm/docs/output/commands/npm-install-ci-test.html index 10cf3475a8cb97..2658ecedd0efb2 100644 --- a/deps/npm/docs/output/commands/npm-install-ci-test.html +++ b/deps/npm/docs/output/commands/npm-install-ci-test.html @@ -186,16 +186,16 @@
    -

    +

    npm-install-ci-test - @11.15.0 + @11.16.0

    Install a project with a clean slate and run tests

    Table of contents

    - +

    Synopsis

    @@ -354,6 +354,44 @@

    allow-remote

    installed. root only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like npm view

    +

    allow-scripts

    +
      +
    • Default: ""
    • +
    • Type: String (can be set multiple times)
    • +
    +

    Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

    +

    This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

    +

    Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

    +

    strict-allow-scripts

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

    +

    Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

    +

    dangerously-allow-all-scripts

    +
      +
    • Default: false
    • +
    • Type: Boolean
    • +
    +

    If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

    audit

    • Default: true
    • diff --git a/deps/npm/docs/output/commands/npm-install-test.html b/deps/npm/docs/output/commands/npm-install-test.html index 3aa17aef585d3a..cda0bf383f0fe5 100644 --- a/deps/npm/docs/output/commands/npm-install-test.html +++ b/deps/npm/docs/output/commands/npm-install-test.html @@ -186,16 +186,16 @@
      -

      +

      npm-install-test - @11.15.0 + @11.16.0

      Install package(s) and run tests

      Table of contents

      - +

      Synopsis

      @@ -410,6 +410,44 @@

      allow-remote

      installed. root only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like npm view

      +

      allow-scripts

      +
        +
      • Default: ""
      • +
      • Type: String (can be set multiple times)
      • +
      +

      Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

      +

      This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

      +

      Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

      +

      strict-allow-scripts

      +
        +
      • Default: false
      • +
      • Type: Boolean
      • +
      +

      If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

      +

      Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

      +

      dangerously-allow-all-scripts

      +
        +
      • Default: false
      • +
      • Type: Boolean
      • +
      +

      If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

      audit

      • Default: true
      • diff --git a/deps/npm/docs/output/commands/npm-install.html b/deps/npm/docs/output/commands/npm-install.html index 072f465f26a4b5..c9ae37e393238c 100644 --- a/deps/npm/docs/output/commands/npm-install.html +++ b/deps/npm/docs/output/commands/npm-install.html @@ -186,16 +186,16 @@
        -

        +

        npm-install - @11.15.0 + @11.16.0

        Install a package

        Table of contents

        - +

        Synopsis

        @@ -685,6 +685,44 @@

        allow-remote

        installed. root only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like npm view

        +

        allow-scripts

        +
          +
        • Default: ""
        • +
        • Type: String (can be set multiple times)
        • +
        +

        Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

        +

        This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

        +

        Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

        +

        strict-allow-scripts

        +
          +
        • Default: false
        • +
        • Type: Boolean
        • +
        +

        If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

        +

        Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

        +

        dangerously-allow-all-scripts

        +
          +
        • Default: false
        • +
        • Type: Boolean
        • +
        +

        If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

        audit

        • Default: true
        • diff --git a/deps/npm/docs/output/commands/npm-link.html b/deps/npm/docs/output/commands/npm-link.html index 06097944e8ca5f..dcc559329dfc51 100644 --- a/deps/npm/docs/output/commands/npm-link.html +++ b/deps/npm/docs/output/commands/npm-link.html @@ -186,9 +186,9 @@
          -

          +

          npm-link - @11.15.0 + @11.16.0

          Symlink a package folder
          diff --git a/deps/npm/docs/output/commands/npm-ll.html b/deps/npm/docs/output/commands/npm-ll.html index c61d58d80d9ef8..52f363b8b17d68 100644 --- a/deps/npm/docs/output/commands/npm-ll.html +++ b/deps/npm/docs/output/commands/npm-ll.html @@ -186,9 +186,9 @@
          -

          +

          npm-ll - @11.15.0 + @11.16.0

          List installed packages
          diff --git a/deps/npm/docs/output/commands/npm-login.html b/deps/npm/docs/output/commands/npm-login.html index 58c694e1d4e19a..eac37fc5a66665 100644 --- a/deps/npm/docs/output/commands/npm-login.html +++ b/deps/npm/docs/output/commands/npm-login.html @@ -186,9 +186,9 @@
          -

          +

          npm-login - @11.15.0 + @11.16.0

          Login to a registry user account
          diff --git a/deps/npm/docs/output/commands/npm-logout.html b/deps/npm/docs/output/commands/npm-logout.html index 2229efdbe6a7f4..0930332b862b4d 100644 --- a/deps/npm/docs/output/commands/npm-logout.html +++ b/deps/npm/docs/output/commands/npm-logout.html @@ -186,9 +186,9 @@
          -

          +

          npm-logout - @11.15.0 + @11.16.0

          Log out of the registry
          diff --git a/deps/npm/docs/output/commands/npm-ls.html b/deps/npm/docs/output/commands/npm-ls.html index 00829f01ae061d..cbf7f4e8208c93 100644 --- a/deps/npm/docs/output/commands/npm-ls.html +++ b/deps/npm/docs/output/commands/npm-ls.html @@ -186,9 +186,9 @@
          -

          +

          npm-ls - @11.15.0 + @11.16.0

          List installed packages
          @@ -209,7 +209,7 @@

          Description

          Positional arguments are name@version-range identifiers, which will limit the results to only the paths to the packages named. Note that nested packages will also show the paths to the specified packages. For example, running npm ls promzard in npm's source tree will show:

          -
          npm@11.15.0 /path/to/npm
          +
          npm@11.16.0 /path/to/npm
           └─┬ init-package-json@0.0.4
             └── promzard@0.1.5
           
          diff --git a/deps/npm/docs/output/commands/npm-org.html b/deps/npm/docs/output/commands/npm-org.html index 257e5fa0c12910..99e8c472dc5f74 100644 --- a/deps/npm/docs/output/commands/npm-org.html +++ b/deps/npm/docs/output/commands/npm-org.html @@ -186,9 +186,9 @@
          -

          +

          npm-org - @11.15.0 + @11.16.0

          Manage orgs
          diff --git a/deps/npm/docs/output/commands/npm-outdated.html b/deps/npm/docs/output/commands/npm-outdated.html index 0c95c2bfa59ead..cb154b4a234c7e 100644 --- a/deps/npm/docs/output/commands/npm-outdated.html +++ b/deps/npm/docs/output/commands/npm-outdated.html @@ -186,9 +186,9 @@
          -

          +

          npm-outdated - @11.15.0 + @11.16.0

          Check for outdated packages
          diff --git a/deps/npm/docs/output/commands/npm-owner.html b/deps/npm/docs/output/commands/npm-owner.html index 5f7dda96d69e2a..fa568741602212 100644 --- a/deps/npm/docs/output/commands/npm-owner.html +++ b/deps/npm/docs/output/commands/npm-owner.html @@ -186,9 +186,9 @@
          -

          +

          npm-owner - @11.15.0 + @11.16.0

          Manage package owners
          diff --git a/deps/npm/docs/output/commands/npm-pack.html b/deps/npm/docs/output/commands/npm-pack.html index 7097d255509ae9..a99ae2dba99c61 100644 --- a/deps/npm/docs/output/commands/npm-pack.html +++ b/deps/npm/docs/output/commands/npm-pack.html @@ -186,9 +186,9 @@
          -

          +

          npm-pack - @11.15.0 + @11.16.0

          Create a tarball from a package
          diff --git a/deps/npm/docs/output/commands/npm-ping.html b/deps/npm/docs/output/commands/npm-ping.html index 6f0dd51b517e4e..bd867fbd3ef12b 100644 --- a/deps/npm/docs/output/commands/npm-ping.html +++ b/deps/npm/docs/output/commands/npm-ping.html @@ -186,9 +186,9 @@
          -

          +

          npm-ping - @11.15.0 + @11.16.0

          Ping npm registry
          diff --git a/deps/npm/docs/output/commands/npm-pkg.html b/deps/npm/docs/output/commands/npm-pkg.html index d1953066d18b0f..145b47fde4e069 100644 --- a/deps/npm/docs/output/commands/npm-pkg.html +++ b/deps/npm/docs/output/commands/npm-pkg.html @@ -186,9 +186,9 @@
          -

          +

          npm-pkg - @11.15.0 + @11.16.0

          Manages your package.json
          diff --git a/deps/npm/docs/output/commands/npm-prefix.html b/deps/npm/docs/output/commands/npm-prefix.html index d248c0e19d5d91..2bccd93bb26aae 100644 --- a/deps/npm/docs/output/commands/npm-prefix.html +++ b/deps/npm/docs/output/commands/npm-prefix.html @@ -186,9 +186,9 @@
          -

          +

          npm-prefix - @11.15.0 + @11.16.0

          Display prefix
          diff --git a/deps/npm/docs/output/commands/npm-profile.html b/deps/npm/docs/output/commands/npm-profile.html index 7f27c45182e770..9e3d975aa3c2ca 100644 --- a/deps/npm/docs/output/commands/npm-profile.html +++ b/deps/npm/docs/output/commands/npm-profile.html @@ -186,9 +186,9 @@
          -

          +

          npm-profile - @11.15.0 + @11.16.0

          Change settings on your registry profile
          diff --git a/deps/npm/docs/output/commands/npm-prune.html b/deps/npm/docs/output/commands/npm-prune.html index a2782ed0eae29c..f6a356f53cc238 100644 --- a/deps/npm/docs/output/commands/npm-prune.html +++ b/deps/npm/docs/output/commands/npm-prune.html @@ -186,9 +186,9 @@
          -

          +

          npm-prune - @11.15.0 + @11.16.0

          Remove extraneous packages
          diff --git a/deps/npm/docs/output/commands/npm-publish.html b/deps/npm/docs/output/commands/npm-publish.html index 2f194d3c4086a3..b9c7824c8456d7 100644 --- a/deps/npm/docs/output/commands/npm-publish.html +++ b/deps/npm/docs/output/commands/npm-publish.html @@ -186,9 +186,9 @@
          -

          +

          npm-publish - @11.15.0 + @11.16.0

          Publish a package
          @@ -279,7 +279,7 @@

          access

          • Default: 'public' for new packages, existing packages it will not change the current level
          • -
          • Type: null, "restricted", or "public"
          • +
          • Type: null, "restricted", "public", or "private"

          If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted.

          @@ -287,6 +287,7 @@

          access

          Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would.

          +

          The value private is an alias for restricted.

          dry-run

          • Default: false
          • diff --git a/deps/npm/docs/output/commands/npm-query.html b/deps/npm/docs/output/commands/npm-query.html index 30bddb72964b0d..efa6bd81f130f8 100644 --- a/deps/npm/docs/output/commands/npm-query.html +++ b/deps/npm/docs/output/commands/npm-query.html @@ -186,9 +186,9 @@
            -

            +

            npm-query - @11.15.0 + @11.16.0

            Dependency selector query
            diff --git a/deps/npm/docs/output/commands/npm-rebuild.html b/deps/npm/docs/output/commands/npm-rebuild.html index 30301c4cda1c2e..0aff44579c5075 100644 --- a/deps/npm/docs/output/commands/npm-rebuild.html +++ b/deps/npm/docs/output/commands/npm-rebuild.html @@ -186,16 +186,16 @@
            -

            +

            npm-rebuild - @11.15.0 + @11.16.0

            Rebuild a package

            Table of contents

            - +

            Synopsis

            @@ -269,6 +269,44 @@

            ignore-scripts

            npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

            +

            allow-scripts

            +
              +
            • Default: ""
            • +
            • Type: String (can be set multiple times)
            • +
            +

            Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

            +

            This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

            +

            Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

            +

            strict-allow-scripts

            +
              +
            • Default: false
            • +
            • Type: Boolean
            • +
            +

            If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

            +

            Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

            +

            dangerously-allow-all-scripts

            +
              +
            • Default: false
            • +
            • Type: Boolean
            • +
            +

            If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

            workspace

            • Default:
            • diff --git a/deps/npm/docs/output/commands/npm-repo.html b/deps/npm/docs/output/commands/npm-repo.html index e6bd1024c2252f..0efe01e32391d1 100644 --- a/deps/npm/docs/output/commands/npm-repo.html +++ b/deps/npm/docs/output/commands/npm-repo.html @@ -186,9 +186,9 @@
              -

              +

              npm-repo - @11.15.0 + @11.16.0

              Open package repository page in the browser
              diff --git a/deps/npm/docs/output/commands/npm-restart.html b/deps/npm/docs/output/commands/npm-restart.html index 3f08556ca35fd4..d58d9d362a03a9 100644 --- a/deps/npm/docs/output/commands/npm-restart.html +++ b/deps/npm/docs/output/commands/npm-restart.html @@ -186,9 +186,9 @@
              -

              +

              npm-restart - @11.15.0 + @11.16.0

              Restart a package
              diff --git a/deps/npm/docs/output/commands/npm-root.html b/deps/npm/docs/output/commands/npm-root.html index 638fe0c5f29a31..8f1a7319765932 100644 --- a/deps/npm/docs/output/commands/npm-root.html +++ b/deps/npm/docs/output/commands/npm-root.html @@ -186,9 +186,9 @@
              -

              +

              npm-root - @11.15.0 + @11.16.0

              Display npm root
              diff --git a/deps/npm/docs/output/commands/npm-run.html b/deps/npm/docs/output/commands/npm-run.html index 38864357391f68..c234db61b936de 100644 --- a/deps/npm/docs/output/commands/npm-run.html +++ b/deps/npm/docs/output/commands/npm-run.html @@ -186,9 +186,9 @@
              -

              +

              npm-run - @11.15.0 + @11.16.0

              Run arbitrary package scripts
              diff --git a/deps/npm/docs/output/commands/npm-sbom.html b/deps/npm/docs/output/commands/npm-sbom.html index a08eee8e909bf4..df30ae75770012 100644 --- a/deps/npm/docs/output/commands/npm-sbom.html +++ b/deps/npm/docs/output/commands/npm-sbom.html @@ -186,9 +186,9 @@
              -

              +

              npm-sbom - @11.15.0 + @11.16.0

              Generate a Software Bill of Materials (SBOM)
              diff --git a/deps/npm/docs/output/commands/npm-search.html b/deps/npm/docs/output/commands/npm-search.html index 8a009f559e779c..63efdaad281e8b 100644 --- a/deps/npm/docs/output/commands/npm-search.html +++ b/deps/npm/docs/output/commands/npm-search.html @@ -186,9 +186,9 @@
              -

              +

              npm-search - @11.15.0 + @11.16.0

              Search for packages
              diff --git a/deps/npm/docs/output/commands/npm-set.html b/deps/npm/docs/output/commands/npm-set.html index c6811ae41180e3..988c341f8fee77 100644 --- a/deps/npm/docs/output/commands/npm-set.html +++ b/deps/npm/docs/output/commands/npm-set.html @@ -186,9 +186,9 @@
              -

              +

              npm-set - @11.15.0 + @11.16.0

              Set a value in the npm configuration
              diff --git a/deps/npm/docs/output/commands/npm-shrinkwrap.html b/deps/npm/docs/output/commands/npm-shrinkwrap.html index 3dff3e5d1db87c..46c96bdef91111 100644 --- a/deps/npm/docs/output/commands/npm-shrinkwrap.html +++ b/deps/npm/docs/output/commands/npm-shrinkwrap.html @@ -186,9 +186,9 @@
              -

              +

              npm-shrinkwrap - @11.15.0 + @11.16.0

              Lock down dependency versions for publication
              diff --git a/deps/npm/docs/output/commands/npm-stage.html b/deps/npm/docs/output/commands/npm-stage.html index 6abe7e8bb5daa1..e98b5e5aca18a7 100644 --- a/deps/npm/docs/output/commands/npm-stage.html +++ b/deps/npm/docs/output/commands/npm-stage.html @@ -186,9 +186,9 @@
              -

              +

              npm-stage - @11.15.0 + @11.16.0

              Stage packages for publishing
              @@ -395,21 +395,48 @@

              Flags

              --access +'public' for new packages, existing packages it will not change the current level +null, "restricted", "public", or "private" +If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted. Unscoped packages cannot be set to restricted. Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would. The value private is an alias for restricted. + + +--dry-run +false +Boolean +Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, install, update, dedupe, uninstall, as well as pack and publish. Note: This is NOT honored by other network related commands, eg dist-tags, owner, etc. + + +--otp +null +null or String +This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with npm access. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. + + +--workspace, -w - - +String (can be set multiple times) +Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the workspace config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the npm init command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. + + +--workspaces +null +null or Boolean +Set to true to run the command in the context of all configured workspaces. Explicitly setting this to false will cause commands like install to ignore workspaces altogether. When not set explicitly: - Commands that operate on the node_modules tree (install, update, etc.) will link workspaces into the node_modules folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, unless one or more workspaces are specified in the workspace config. + + +--include-workspace-root +false +Boolean +Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the workspace config, or all workspaces via the workspaces flag, will cause npm to operate only on the specified workspaces, and not on the root project. + + +--provenance +false +Boolean +When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. -
              'public' for new packages, existing packages it will not change the current level
              -
              -

              | null, "restricted", or "public" | If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted. Unscoped packages cannot be set to restricted. Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would. | -| --dry-run | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, install, update, dedupe, uninstall, as well as pack and publish. Note: This is NOT honored by other network related commands, eg dist-tags, owner, etc. | -| --otp | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with npm access. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | -| --workspace, -w | | String (can be set multiple times) | Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the workspace config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the npm init command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. | -| --workspaces | null | null or Boolean | Set to true to run the command in the context of all configured workspaces. Explicitly setting this to false will cause commands like install to ignore workspaces altogether. When not set explicitly: - Commands that operate on the node_modules tree (install, update, etc.) will link workspaces into the node_modules folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, unless one or more workspaces are specified in the workspace config. | -| --include-workspace-root | false | Boolean | Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the workspace config, or all workspaces via the workspaces flag, will cause npm to operate only on the specified workspaces, and not on the root project. | -| --provenance | false | Boolean | When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. |

              npm stage list

              List all staged package versions

              Synopsis

              diff --git a/deps/npm/docs/output/commands/npm-star.html b/deps/npm/docs/output/commands/npm-star.html index 40433712a41fef..5ecd1df01d01d2 100644 --- a/deps/npm/docs/output/commands/npm-star.html +++ b/deps/npm/docs/output/commands/npm-star.html @@ -186,9 +186,9 @@
              -

              +

              npm-star - @11.15.0 + @11.16.0

              Mark your favorite packages
              diff --git a/deps/npm/docs/output/commands/npm-stars.html b/deps/npm/docs/output/commands/npm-stars.html index 4fa00ccd89e134..d4e8bb1ae9764d 100644 --- a/deps/npm/docs/output/commands/npm-stars.html +++ b/deps/npm/docs/output/commands/npm-stars.html @@ -186,9 +186,9 @@
              -

              +

              npm-stars - @11.15.0 + @11.16.0

              View packages marked as favorites
              diff --git a/deps/npm/docs/output/commands/npm-start.html b/deps/npm/docs/output/commands/npm-start.html index 79e15eaeabe32e..bcc5463e6ddfe3 100644 --- a/deps/npm/docs/output/commands/npm-start.html +++ b/deps/npm/docs/output/commands/npm-start.html @@ -186,9 +186,9 @@
              -

              +

              npm-start - @11.15.0 + @11.16.0

              Start a package
              diff --git a/deps/npm/docs/output/commands/npm-stop.html b/deps/npm/docs/output/commands/npm-stop.html index 132a6540c9031e..abbb05aa873c1d 100644 --- a/deps/npm/docs/output/commands/npm-stop.html +++ b/deps/npm/docs/output/commands/npm-stop.html @@ -186,9 +186,9 @@
              -

              +

              npm-stop - @11.15.0 + @11.16.0

              Stop a package
              diff --git a/deps/npm/docs/output/commands/npm-team.html b/deps/npm/docs/output/commands/npm-team.html index 42031ba120ae7b..a1d0941e2e541b 100644 --- a/deps/npm/docs/output/commands/npm-team.html +++ b/deps/npm/docs/output/commands/npm-team.html @@ -186,9 +186,9 @@
              -

              +

              npm-team - @11.15.0 + @11.16.0

              Manage organization teams and team memberships
              diff --git a/deps/npm/docs/output/commands/npm-test.html b/deps/npm/docs/output/commands/npm-test.html index 2da5db2ff7f08b..d5fb0a1ded18a7 100644 --- a/deps/npm/docs/output/commands/npm-test.html +++ b/deps/npm/docs/output/commands/npm-test.html @@ -186,9 +186,9 @@
              -

              +

              npm-test - @11.15.0 + @11.16.0

              Test a package
              diff --git a/deps/npm/docs/output/commands/npm-token.html b/deps/npm/docs/output/commands/npm-token.html index bff16ccc4e1c42..10163668153233 100644 --- a/deps/npm/docs/output/commands/npm-token.html +++ b/deps/npm/docs/output/commands/npm-token.html @@ -186,9 +186,9 @@
              -

              +

              npm-token - @11.15.0 + @11.16.0

              Manage your authentication tokens
              diff --git a/deps/npm/docs/output/commands/npm-trust.html b/deps/npm/docs/output/commands/npm-trust.html index e4041c733f4b3b..e269490efdff39 100644 --- a/deps/npm/docs/output/commands/npm-trust.html +++ b/deps/npm/docs/output/commands/npm-trust.html @@ -186,9 +186,9 @@
              -

              +

              npm-trust - @11.15.0 + @11.16.0

              Manage trusted publishing relationships between packages and CI/CD providers
              diff --git a/deps/npm/docs/output/commands/npm-undeprecate.html b/deps/npm/docs/output/commands/npm-undeprecate.html index bafbabefdfecef..45fcb65deed0ac 100644 --- a/deps/npm/docs/output/commands/npm-undeprecate.html +++ b/deps/npm/docs/output/commands/npm-undeprecate.html @@ -186,9 +186,9 @@
              -

              +

              npm-undeprecate - @11.15.0 + @11.16.0

              Undeprecate a version of a package
              diff --git a/deps/npm/docs/output/commands/npm-uninstall.html b/deps/npm/docs/output/commands/npm-uninstall.html index a42ee20cf0509b..bdeb05ee1b30f8 100644 --- a/deps/npm/docs/output/commands/npm-uninstall.html +++ b/deps/npm/docs/output/commands/npm-uninstall.html @@ -186,9 +186,9 @@
              -

              +

              npm-uninstall - @11.15.0 + @11.16.0

              Remove a package
              diff --git a/deps/npm/docs/output/commands/npm-unpublish.html b/deps/npm/docs/output/commands/npm-unpublish.html index 3e3f4e3e9d541b..eec4aee8c603e7 100644 --- a/deps/npm/docs/output/commands/npm-unpublish.html +++ b/deps/npm/docs/output/commands/npm-unpublish.html @@ -186,9 +186,9 @@
              -

              +

              npm-unpublish - @11.15.0 + @11.16.0

              Remove a package from the registry
              diff --git a/deps/npm/docs/output/commands/npm-unstar.html b/deps/npm/docs/output/commands/npm-unstar.html index 0e98f60d7bc737..7d3d40d29928a3 100644 --- a/deps/npm/docs/output/commands/npm-unstar.html +++ b/deps/npm/docs/output/commands/npm-unstar.html @@ -186,9 +186,9 @@
              -

              +

              npm-unstar - @11.15.0 + @11.16.0

              Remove an item from your favorite packages
              diff --git a/deps/npm/docs/output/commands/npm-update.html b/deps/npm/docs/output/commands/npm-update.html index a57b62afbf6943..1558e84ce06729 100644 --- a/deps/npm/docs/output/commands/npm-update.html +++ b/deps/npm/docs/output/commands/npm-update.html @@ -186,16 +186,16 @@
              -

              +

              npm-update - @11.15.0 + @11.16.0

              Update packages

              Table of contents

              - +

              Synopsis

              @@ -407,6 +407,44 @@

              ignore-scripts

              npm start, npm stop, npm restart, npm test, and npm run will still run their intended script if ignore-scripts is set, but they will not run any pre- or post-scripts.

              +

              allow-scripts

              +
                +
              • Default: ""
              • +
              • Type: String (can be set multiple times)
              • +
              +

              Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

              +

              This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

              +

              Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

              +

              strict-allow-scripts

              +
                +
              • Default: false
              • +
              • Type: Boolean
              • +
              +

              If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

              +

              Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

              +

              dangerously-allow-all-scripts

              +
                +
              • Default: false
              • +
              • Type: Boolean
              • +
              +

              If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

              audit

              • Default: true
              • diff --git a/deps/npm/docs/output/commands/npm-version.html b/deps/npm/docs/output/commands/npm-version.html index 03468cb82e91b8..4deac0758f8d5d 100644 --- a/deps/npm/docs/output/commands/npm-version.html +++ b/deps/npm/docs/output/commands/npm-version.html @@ -186,9 +186,9 @@
                -

                +

                npm-version - @11.15.0 + @11.16.0

                Bump a package version
                @@ -367,6 +367,7 @@

                Description

              • Run the postversion script. Use it to clean up the file system or automatically push the commit and/or tag.
              • +

                For the preversion, version and postversion scripts, npm also sets the environment variables npm_old_version and npm_new_version.

                Take the following example:

                {
                   "scripts": {
                diff --git a/deps/npm/docs/output/commands/npm-view.html b/deps/npm/docs/output/commands/npm-view.html
                index 2943db55d7643f..71fac734e58daa 100644
                --- a/deps/npm/docs/output/commands/npm-view.html
                +++ b/deps/npm/docs/output/commands/npm-view.html
                @@ -186,9 +186,9 @@
                 
                 
                -

                +

                npm-view - @11.15.0 + @11.16.0

                View registry info
                diff --git a/deps/npm/docs/output/commands/npm-whoami.html b/deps/npm/docs/output/commands/npm-whoami.html index f18819e8cc4586..f92bd67d1f5d55 100644 --- a/deps/npm/docs/output/commands/npm-whoami.html +++ b/deps/npm/docs/output/commands/npm-whoami.html @@ -186,9 +186,9 @@
                -

                +

                npm-whoami - @11.15.0 + @11.16.0

                Display npm username
                diff --git a/deps/npm/docs/output/commands/npm.html b/deps/npm/docs/output/commands/npm.html index 9f0ceff2c5fea1..52befd0760479b 100644 --- a/deps/npm/docs/output/commands/npm.html +++ b/deps/npm/docs/output/commands/npm.html @@ -186,9 +186,9 @@
                -

                +

                npm - @11.15.0 + @11.16.0

                javascript package manager
                @@ -203,7 +203,7 @@

                Table of contents

                Note: This command is unaware of workspaces.

                Version

                -

                11.15.0

                +

                11.16.0

                Description

                npm is the package manager for the Node JavaScript platform. It puts modules in place so that node can find them, and manages dependency conflicts intelligently.

                diff --git a/deps/npm/docs/output/commands/npx.html b/deps/npm/docs/output/commands/npx.html index de7bab625b3800..5786f4332f3b6f 100644 --- a/deps/npm/docs/output/commands/npx.html +++ b/deps/npm/docs/output/commands/npx.html @@ -186,9 +186,9 @@
                -

                +

                npx - @11.15.0 + @11.16.0

                Run a command from a local or remote npm package
                diff --git a/deps/npm/docs/output/configuring-npm/folders.html b/deps/npm/docs/output/configuring-npm/folders.html index ead948bfa7a22e..c88270a3799930 100644 --- a/deps/npm/docs/output/configuring-npm/folders.html +++ b/deps/npm/docs/output/configuring-npm/folders.html @@ -186,9 +186,9 @@
                -

                +

                Folders - @11.15.0 + @11.16.0

                Folder structures used by npm
                diff --git a/deps/npm/docs/output/configuring-npm/install.html b/deps/npm/docs/output/configuring-npm/install.html index af9419e0f8496c..cb308af4d962a9 100644 --- a/deps/npm/docs/output/configuring-npm/install.html +++ b/deps/npm/docs/output/configuring-npm/install.html @@ -186,9 +186,9 @@
                -

                +

                Install - @11.15.0 + @11.16.0

                Download and install node and npm
                diff --git a/deps/npm/docs/output/configuring-npm/npm-global.html b/deps/npm/docs/output/configuring-npm/npm-global.html index ead948bfa7a22e..c88270a3799930 100644 --- a/deps/npm/docs/output/configuring-npm/npm-global.html +++ b/deps/npm/docs/output/configuring-npm/npm-global.html @@ -186,9 +186,9 @@
                -

                +

                Folders - @11.15.0 + @11.16.0

                Folder structures used by npm
                diff --git a/deps/npm/docs/output/configuring-npm/npm-json.html b/deps/npm/docs/output/configuring-npm/npm-json.html index 05565040705356..057c119042240f 100644 --- a/deps/npm/docs/output/configuring-npm/npm-json.html +++ b/deps/npm/docs/output/configuring-npm/npm-json.html @@ -186,9 +186,9 @@
                -

                +

                package.json - @11.15.0 + @11.16.0

                Specifics of npm's package.json handling
                diff --git a/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html b/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html index 740e79a65e15f0..2c585ff171c2c7 100644 --- a/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html +++ b/deps/npm/docs/output/configuring-npm/npm-shrinkwrap-json.html @@ -186,9 +186,9 @@
                -

                +

                npm-shrinkwrap.json - @11.15.0 + @11.16.0

                A publishable lockfile
                diff --git a/deps/npm/docs/output/configuring-npm/npmrc.html b/deps/npm/docs/output/configuring-npm/npmrc.html index 76eca4327e8c69..c90a8b19c6be09 100644 --- a/deps/npm/docs/output/configuring-npm/npmrc.html +++ b/deps/npm/docs/output/configuring-npm/npmrc.html @@ -186,9 +186,9 @@
                -

                +

                .npmrc - @11.15.0 + @11.16.0

                The npm config files
                diff --git a/deps/npm/docs/output/configuring-npm/package-json.html b/deps/npm/docs/output/configuring-npm/package-json.html index 05565040705356..057c119042240f 100644 --- a/deps/npm/docs/output/configuring-npm/package-json.html +++ b/deps/npm/docs/output/configuring-npm/package-json.html @@ -186,9 +186,9 @@
                -

                +

                package.json - @11.15.0 + @11.16.0

                Specifics of npm's package.json handling
                diff --git a/deps/npm/docs/output/configuring-npm/package-lock-json.html b/deps/npm/docs/output/configuring-npm/package-lock-json.html index 63c60f97b4c5ab..64a2dbb13601d2 100644 --- a/deps/npm/docs/output/configuring-npm/package-lock-json.html +++ b/deps/npm/docs/output/configuring-npm/package-lock-json.html @@ -186,9 +186,9 @@
                -

                +

                package-lock.json - @11.15.0 + @11.16.0

                A manifestation of the manifest
                diff --git a/deps/npm/docs/output/using-npm/config.html b/deps/npm/docs/output/using-npm/config.html index 364b56910260da..687d077639eda6 100644 --- a/deps/npm/docs/output/using-npm/config.html +++ b/deps/npm/docs/output/using-npm/config.html @@ -186,16 +186,16 @@
                -

                +

                Config - @11.15.0 + @11.16.0

                About npm configuration

                Table of contents

                -
                +

                Description

                @@ -307,7 +307,7 @@

                access

                • Default: 'public' for new packages, existing packages it will not change the current level
                • -
                • Type: null, "restricted", or "public"
                • +
                • Type: null, "restricted", "public", or "private"

                If you do not want your scoped package to be publicly viewable (and installable) set --access=restricted.

                @@ -315,6 +315,7 @@

                access

                Note: This defaults to not changing the current access level for existing packages. Specifying a value of restricted or public during publish will change the access for an existing package the same way that npm access set status would.

                +

                The value private is an alias for restricted.

                all

                • Default: false
                • @@ -387,6 +388,40 @@

                  allow-same-version

                Prevents throwing an error when npm version is used to set the new version to the same value as the current version.

                +

                allow-scripts

                +
                  +
                • Default: ""
                • +
                • Type: String (can be set multiple times)
                • +
                +

                Comma-separated list of packages whose install-time lifecycle scripts +(preinstall, install, postinstall, and prepare for non-registry +dependencies) are allowed to run.

                +

                This setting is intended for one-off and global contexts: npm exec, npx, +and npm install -g, where no project package.json is involved. For +team-wide policy in a project, use the allowScripts field in +package.json (which also supports explicit denials), or configure it in +.npmrc. Passing --allow-scripts on the command line during a +project-scoped npm install, ci, update, or rebuild is an error.

                +

                Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. --ignore-scripts and +--dangerously-allow-all-scripts both override this setting.

                +

                allow-scripts-pending

                +
                  +
                • Default: false
                • +
                • Type: Boolean
                • +
                +

                List packages with install scripts that are not yet covered by the +allowScripts policy, without modifying package.json. Only meaningful for +npm approve-scripts.

                +

                allow-scripts-pin

                +
                  +
                • Default: true
                • +
                • Type: Boolean
                • +
                +

                Write pinned (pkg@version) entries when approving install scripts. Set to +false to write name-only entries that allow any version. Has no effect on +npm deny-scripts, which always writes name-only entries regardless of this +setting.

                audit

                • Default: true
                • @@ -523,6 +558,15 @@

                  cpu

                Override CPU architecture of native modules to install. Acceptable values are same as cpu field of package.json, which comes from process.arch.

                +

                dangerously-allow-all-scripts

                +
                  +
                • Default: false
                • +
                • Type: Boolean
                • +
                +

                If true, bypass the allowScripts policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +--ignore-scripts still takes precedence over this setting.

                depth

                • Default: Infinity if --all is set; otherwise, 0
                • @@ -1469,6 +1513,18 @@

                  sign-git-tag

                  -s to add a signature.

                  Note that git requires you to have set up GPG keys in your git configs for this to work properly.

                  +

                  strict-allow-scripts

                  +
                    +
                  • Default: false
                  • +
                  • Type: Boolean
                  • +
                  +

                  If true, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by allowScripts will fail +the install instead of running with a notice.

                  +

                  Dependencies explicitly denied with false in allowScripts are always +silently skipped; this setting only affects unreviewed entries. +--ignore-scripts and --dangerously-allow-all-scripts both override this +setting.

                  strict-peer-deps

                  • Default: false
                  • diff --git a/deps/npm/docs/output/using-npm/dependency-selectors.html b/deps/npm/docs/output/using-npm/dependency-selectors.html index f624240e2ce4b1..da260de09888ec 100644 --- a/deps/npm/docs/output/using-npm/dependency-selectors.html +++ b/deps/npm/docs/output/using-npm/dependency-selectors.html @@ -186,9 +186,9 @@
                    -

                    +

                    Dependency Selectors - @11.15.0 + @11.16.0

                    Dependency Selector Syntax & Querying
                    diff --git a/deps/npm/docs/output/using-npm/developers.html b/deps/npm/docs/output/using-npm/developers.html index d9e4e3aff91678..9f8825dccd2dda 100644 --- a/deps/npm/docs/output/using-npm/developers.html +++ b/deps/npm/docs/output/using-npm/developers.html @@ -186,9 +186,9 @@
                    -

                    +

                    Developers - @11.15.0 + @11.16.0

                    Developer guide
                    diff --git a/deps/npm/docs/output/using-npm/logging.html b/deps/npm/docs/output/using-npm/logging.html index 075cc73a67a09f..675c116ed70c64 100644 --- a/deps/npm/docs/output/using-npm/logging.html +++ b/deps/npm/docs/output/using-npm/logging.html @@ -186,9 +186,9 @@
                    -

                    +

                    Logging - @11.15.0 + @11.16.0

                    Why, What & How we Log
                    diff --git a/deps/npm/docs/output/using-npm/orgs.html b/deps/npm/docs/output/using-npm/orgs.html index 53ae0de316be80..4da8761b61ffca 100644 --- a/deps/npm/docs/output/using-npm/orgs.html +++ b/deps/npm/docs/output/using-npm/orgs.html @@ -186,9 +186,9 @@
                    -

                    +

                    Organizations - @11.15.0 + @11.16.0

                    Working with teams & organizations
                    diff --git a/deps/npm/docs/output/using-npm/package-spec.html b/deps/npm/docs/output/using-npm/package-spec.html index 7124466377618f..b682f1889687a2 100644 --- a/deps/npm/docs/output/using-npm/package-spec.html +++ b/deps/npm/docs/output/using-npm/package-spec.html @@ -186,9 +186,9 @@
                    -

                    +

                    Package spec - @11.15.0 + @11.16.0

                    Package name specifier
                    diff --git a/deps/npm/docs/output/using-npm/registry.html b/deps/npm/docs/output/using-npm/registry.html index 71cea945e1251b..e6efaed669f708 100644 --- a/deps/npm/docs/output/using-npm/registry.html +++ b/deps/npm/docs/output/using-npm/registry.html @@ -186,9 +186,9 @@
                    -

                    +

                    Registry - @11.15.0 + @11.16.0

                    The JavaScript Package Registry
                    diff --git a/deps/npm/docs/output/using-npm/removal.html b/deps/npm/docs/output/using-npm/removal.html index a4ec8e6adb23ab..0d58d278fa6f8f 100644 --- a/deps/npm/docs/output/using-npm/removal.html +++ b/deps/npm/docs/output/using-npm/removal.html @@ -186,9 +186,9 @@
                    -

                    +

                    Removal - @11.15.0 + @11.16.0

                    Cleaning the slate
                    diff --git a/deps/npm/docs/output/using-npm/scope.html b/deps/npm/docs/output/using-npm/scope.html index f0dee65b1afa0b..4004c513323c3b 100644 --- a/deps/npm/docs/output/using-npm/scope.html +++ b/deps/npm/docs/output/using-npm/scope.html @@ -186,9 +186,9 @@
                    -

                    +

                    Scope - @11.15.0 + @11.16.0

                    Scoped packages
                    diff --git a/deps/npm/docs/output/using-npm/scripts.html b/deps/npm/docs/output/using-npm/scripts.html index 8c2de4a8c1fdc5..15ca8072c3450f 100644 --- a/deps/npm/docs/output/using-npm/scripts.html +++ b/deps/npm/docs/output/using-npm/scripts.html @@ -186,16 +186,16 @@
                    -

                    +

                    Scripts - @11.15.0 + @11.16.0

                    How npm handles the "scripts" field

                    Table of contents

                    - +

                    Description

                    @@ -459,6 +459,12 @@

                    package.json vars

                    For example, if you had {"name":"foo", "version":"1.2.5"} in your package.json file, then your package scripts would have the npm_package_name environment variable set to "foo", and the npm_package_version set to "1.2.5". You can access these variables in your code with process.env.npm_package_name and process.env.npm_package_version.

                    Note: In npm 7 and later, most package.json fields are no longer provided as environment variables. Scripts that need access to other package.json fields should read the package.json file directly. The npm_package_json environment variable provides the path to the file for this purpose.

                    See package.json for more on package configs.

                    +

                    versioning variables

                    +

                    For versioning scripts (preversion, version, postversion), npm sets these environment variables:

                    +
                      +
                    • npm_old_version - The version before being bumped
                    • +
                    • npm_new_version – The version after being bumped
                    • +

                    current lifecycle event

                    Lastly, the npm_lifecycle_event environment variable is set to whichever stage of the cycle is being executed. So, you could have a single script used for different parts of the process which switches based on what's currently happening.

                    diff --git a/deps/npm/docs/output/using-npm/workspaces.html b/deps/npm/docs/output/using-npm/workspaces.html index 3b0b1090bf8620..a544b68ce46c23 100644 --- a/deps/npm/docs/output/using-npm/workspaces.html +++ b/deps/npm/docs/output/using-npm/workspaces.html @@ -186,9 +186,9 @@
                    -

                    +

                    Workspaces - @11.15.0 + @11.16.0

                    Working with workspaces
                    diff --git a/deps/npm/lib/commands/approve-scripts.js b/deps/npm/lib/commands/approve-scripts.js new file mode 100644 index 00000000000000..929c692112f16c --- /dev/null +++ b/deps/npm/lib/commands/approve-scripts.js @@ -0,0 +1,10 @@ +const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js') + +class ApproveScripts extends AllowScriptsCmd { + static description = 'Approve install scripts for specific dependencies' + static name = 'approve-scripts' + static usage = [' [ ...]', '--all', '--allow-scripts-pending'] + static verb = 'approve' +} + +module.exports = ApproveScripts diff --git a/deps/npm/lib/commands/ci.js b/deps/npm/lib/commands/ci.js index 354d68ad7adffd..e82438543295a1 100644 --- a/deps/npm/lib/commands/ci.js +++ b/deps/npm/lib/commands/ci.js @@ -1,4 +1,6 @@ const reifyFinish = require('../utils/reify-finish.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') const runScript = require('@npmcli/run-script') const fs = require('node:fs/promises') const path = require('node:path') @@ -25,6 +27,9 @@ class CI extends ArboristWorkspaceCmd { 'allow-file', 'allow-git', 'allow-remote', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', 'audit', 'bin-links', 'fund', @@ -43,12 +48,14 @@ class CI extends ArboristWorkspaceCmd { const ignoreScripts = this.npm.config.get('ignore-scripts') const where = this.npm.prefix const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const opts = { ...this.npm.flatOptions, packageLock: true, // npm ci should never skip lock files path: where, save: false, // npm ci should never modify the lockfile or package.json workspaces: this.workspaceNames, + allowScripts: allowScriptsPolicy, } // generate an inventory from the virtual tree in the lockfile @@ -69,6 +76,7 @@ class CI extends ArboristWorkspaceCmd { // We need a new one because the virtual tree fromt the lockfile can have extraneous dependencies in it that won't install on this platform const arb = new Arborist(opts) await arb.buildIdealTree() + await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: opts }) // Verifies that the packages from the ideal tree will match the same versions that are present in the virtual tree (lock file). const errors = validateLockfile(virtualInventory, arb.idealTree.inventory) diff --git a/deps/npm/lib/commands/config.js b/deps/npm/lib/commands/config.js index 015850c48304a6..0a8b84aba2666d 100644 --- a/deps/npm/lib/commands/config.js +++ b/deps/npm/lib/commands/config.js @@ -5,7 +5,7 @@ const { EOL } = require('node:os') const localeCompare = require('@isaacs/string-locale-compare')('en') const pkgJson = require('@npmcli/package-json') const { defaults, definitions, nerfDarts, proxyEnv } = require('@npmcli/config/lib/definitions') -const { log, output } = require('proc-log') +const { log, output, input } = require('proc-log') const BaseCommand = require('../base-cmd.js') const { redact } = require('@npmcli/redact') @@ -266,7 +266,7 @@ ${defData} `.split('\n').join(EOL) await mkdir(dirname(file), { recursive: true }) await writeFile(file, tmpData, 'utf8') - await new Promise((res, rej) => { + await input.start(() => new Promise((res, rej) => { const [bin, ...args] = e.split(/\s+/) const editor = spawn(bin, [...args, file], { stdio: 'inherit' }) editor.on('exit', (code) => { @@ -275,7 +275,7 @@ ${defData} } return res() }) - }) + })) } async fix () { diff --git a/deps/npm/lib/commands/deny-scripts.js b/deps/npm/lib/commands/deny-scripts.js new file mode 100644 index 00000000000000..53b0cdd3cc50a6 --- /dev/null +++ b/deps/npm/lib/commands/deny-scripts.js @@ -0,0 +1,10 @@ +const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js') + +class DenyScripts extends AllowScriptsCmd { + static description = 'Deny install scripts for specific dependencies' + static name = 'deny-scripts' + static usage = [' [ ...]', '--all'] + static verb = 'deny' +} + +module.exports = DenyScripts diff --git a/deps/npm/lib/commands/edit.js b/deps/npm/lib/commands/edit.js index 1140c59efa3e40..0b1a200264d982 100644 --- a/deps/npm/lib/commands/edit.js +++ b/deps/npm/lib/commands/edit.js @@ -1,6 +1,7 @@ const { resolve } = require('node:path') const { lstat } = require('node:fs/promises') const cp = require('node:child_process') +const { input } = require('proc-log') const completion = require('../utils/installed-shallow.js') const BaseCommand = require('../base-cmd.js') @@ -46,16 +47,17 @@ class Edit extends BaseCommand { const dir = resolve(this.npm.dir, path) await lstat(dir) - await new Promise((res, rej) => { + await input.start(() => new Promise((res, rej) => { const [bin, ...spawnArgs] = this.npm.config.get('editor').split(/\s+/) const editor = cp.spawn(bin, [...spawnArgs, dir], { stdio: 'inherit' }) - editor.on('exit', async (code) => { + editor.on('exit', (code) => { if (code) { return rej(new Error(`editor process exited with code: ${code}`)) } - await this.npm.exec('rebuild', [dir]).then(res).catch(rej) + res() }) - }) + })) + await this.npm.exec('rebuild', [dir]) } } diff --git a/deps/npm/lib/commands/exec.js b/deps/npm/lib/commands/exec.js index 5b1d117889a1ee..23c47a0cc1ad77 100644 --- a/deps/npm/lib/commands/exec.js +++ b/deps/npm/lib/commands/exec.js @@ -1,5 +1,6 @@ const { resolve } = require('node:path') const libexec = require('libnpmexec') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') const BaseCommand = require('../base-cmd.js') class Exec extends BaseCommand { @@ -10,6 +11,9 @@ class Exec extends BaseCommand { 'workspace', 'workspaces', 'include-workspace-root', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', ] static name = 'exec' @@ -74,8 +78,16 @@ class Exec extends BaseCommand { throw this.usageError() } + // Resolve the install-script policy from the user/global .npmrc layer + // only. The RFC requires exec/npx to ignore any project + // package.json#allowScripts; CLI flags still apply. + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm, { + skipProjectConfig: true, + }) + return libexec({ ...flatOptions, + allowScripts: allowScriptsPolicy, // we explicitly set packageLockOnly to false because if it's true when we try to install a missing package, we won't actually install it packageLockOnly: false, // what the user asked to run args[0] is run by default diff --git a/deps/npm/lib/commands/install.js b/deps/npm/lib/commands/install.js index 287b585f132313..0bc3591d4af731 100644 --- a/deps/npm/lib/commands/install.js +++ b/deps/npm/lib/commands/install.js @@ -5,6 +5,8 @@ const runScript = require('@npmcli/run-script') const pacote = require('pacote') const checks = require('npm-install-checks') const reifyFinish = require('../utils/reify-finish.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Install extends ArboristWorkspaceCmd { @@ -31,6 +33,9 @@ class Install extends ArboristWorkspaceCmd { 'allow-file', 'allow-git', 'allow-remote', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', 'audit', 'before', 'min-release-age', @@ -138,14 +143,17 @@ class Install extends ArboristWorkspaceCmd { } const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const opts = { ...this.npm.flatOptions, auditLevel: null, path: where, add: args, workspaces: this.workspaceNames, + allowScripts: allowScriptsPolicy, } const arb = new Arborist(opts) + await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: opts }) await arb.reify(opts) if (!args.length && !isGlobalInstall && !ignoreScripts) { diff --git a/deps/npm/lib/commands/publish.js b/deps/npm/lib/commands/publish.js index 854633e1d29e08..450c51858ba017 100644 --- a/deps/npm/lib/commands/publish.js +++ b/deps/npm/lib/commands/publish.js @@ -287,7 +287,7 @@ class Publish extends BaseCommand { } else { manifest = await pacote.manifest(spec, { ...opts, - fullmetadata: true, + fullMetadata: true, fullReadJson: true, }) } diff --git a/deps/npm/lib/commands/rebuild.js b/deps/npm/lib/commands/rebuild.js index a23df39f1560be..333a879026cbc1 100644 --- a/deps/npm/lib/commands/rebuild.js +++ b/deps/npm/lib/commands/rebuild.js @@ -1,8 +1,11 @@ const { resolve } = require('node:path') -const { output } = require('proc-log') +const { log, output } = require('proc-log') const npa = require('npm-package-arg') const semver = require('semver') const ArboristWorkspaceCmd = require('../arborist-cmd.js') +const checkAllowScripts = require('../utils/check-allow-scripts.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') class Rebuild extends ArboristWorkspaceCmd { static description = 'Rebuild a package' @@ -12,6 +15,9 @@ class Rebuild extends ArboristWorkspaceCmd { 'bin-links', 'foreground-scripts', 'ignore-scripts', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', ...super.params, ] @@ -26,9 +32,11 @@ class Rebuild extends ArboristWorkspaceCmd { const globalTop = resolve(this.npm.globalDir, '..') const where = this.npm.global ? globalTop : this.npm.prefix const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const arb = new Arborist({ ...this.npm.flatOptions, path: where, + allowScripts: allowScriptsPolicy, // TODO when extending ReifyCmd // workspaces: this.workspaceNames, }) @@ -50,11 +58,28 @@ class Rebuild extends ArboristWorkspaceCmd { }) const nodes = tree.inventory.filter(node => this.isNode(specs, node)) + await strictAllowScriptsPreflight({ arb, npm: this.npm }) await arb.rebuild({ nodes }) } else { + await arb.loadActual() + await strictAllowScriptsPreflight({ arb, npm: this.npm }) await arb.rebuild() } + // Phase 1 advisory: list any packages whose install scripts ran (or + // would have run) and are not yet covered by allowScripts. Rebuild + // doesn't go through reifyFinish, so the walker is invoked here. + const unreviewed = await checkAllowScripts({ arb, npm: this.npm }) + if (unreviewed.length > 0) { + const count = unreviewed.length + const noun = count === 1 ? 'package has' : 'packages have' + log.warn( + 'rebuild', + `${count} ${noun} install scripts not yet covered by allowScripts. ` + + 'Run `npm approve-scripts --allow-scripts-pending` to review.' + ) + } + output.standard('rebuilt dependencies successfully') } diff --git a/deps/npm/lib/commands/update.js b/deps/npm/lib/commands/update.js index a7fa14d8fcf24f..22f77390b25a31 100644 --- a/deps/npm/lib/commands/update.js +++ b/deps/npm/lib/commands/update.js @@ -1,6 +1,8 @@ const path = require('node:path') const { log } = require('proc-log') const reifyFinish = require('../utils/reify-finish.js') +const resolveAllowScripts = require('../utils/resolve-allow-scripts.js') +const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Update extends ArboristWorkspaceCmd { @@ -19,6 +21,9 @@ class Update extends ArboristWorkspaceCmd { 'package-lock', 'foreground-scripts', 'ignore-scripts', + 'allow-scripts', + 'strict-allow-scripts', + 'dangerously-allow-all-scripts', 'audit', 'before', 'min-release-age', @@ -51,15 +56,19 @@ class Update extends ArboristWorkspaceCmd { } const Arborist = require('@npmcli/arborist') + const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm) const opts = { ...this.npm.flatOptions, path: where, save, workspaces: this.workspaceNames, + allowScripts: allowScriptsPolicy, } const arb = new Arborist(opts) - await arb.reify({ ...opts, update }) + const reifyOpts = { ...opts, update } + await strictAllowScriptsPreflight({ arb, npm: this.npm, idealTreeOpts: reifyOpts }) + await arb.reify(reifyOpts) await reifyFinish(this.npm, arb) } } diff --git a/deps/npm/lib/utils/allow-scripts-cmd.js b/deps/npm/lib/utils/allow-scripts-cmd.js new file mode 100644 index 00000000000000..c1ff242abeaa82 --- /dev/null +++ b/deps/npm/lib/utils/allow-scripts-cmd.js @@ -0,0 +1,245 @@ +const { log, output } = require('proc-log') +const pkgJson = require('@npmcli/package-json') +const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js') +const checkAllowScripts = require('./check-allow-scripts.js') +const resolveAllowScripts = require('./resolve-allow-scripts.js') +const { + applyApprovalForPackage, + applyDenyForPackage, + nameKeyFor, +} = require('./allow-scripts-writer.js') +const BaseCommand = require('../base-cmd.js') + +// Shared implementation for `npm approve-scripts` and `npm deny-scripts`. +// Subclasses set `verb` to `'approve'` or `'deny'`. +// +// Extends `BaseCommand` rather than `ArboristCmd` on purpose. Per RFC, +// `allowScripts` is read from the workspace root's `package.json` only; +// individual workspaces don't have their own `allowScripts` field, and +// running approve/deny inside a sub-workspace is identical to running +// it at the root. There's no per-workspace targeting to do, so the +// `--workspace` / `--workspaces` / `--include-workspace-root` params +// from `ArboristCmd` would be misleading no-ops. +class AllowScriptsCmd extends BaseCommand { + static params = ['all', 'allow-scripts-pending', 'allow-scripts-pin', 'json'] + static ignoreImplicitWorkspace = false + + // Subclasses set `static verb = 'approve' | 'deny'`. + get verb () { + /* istanbul ignore next: every concrete subclass declares static verb */ + return this.constructor.verb + } + + async exec (args) { + if (this.npm.global) { + throw Object.assign( + new Error(`\`npm ${this.constructor.name}\` does not work for global installs`), + { code: 'EGLOBAL' } + ) + } + + const pending = !!this.npm.config.get('allow-scripts-pending') + const all = !!this.npm.config.get('all') + + if (pending && (args.length > 0 || all)) { + throw this.usageError( + '`--allow-scripts-pending` cannot be combined with positional arguments or `--all`.' + ) + } + if (!pending && !all && args.length === 0) { + throw this.usageError() + } + if (this.verb === 'deny' && pending) { + throw this.usageError('`npm deny-scripts --allow-scripts-pending` is not supported.') + } + + const Arborist = require('@npmcli/arborist') + const { policy } = await resolveAllowScripts(this.npm) + const arb = new Arborist({ + ...this.npm.flatOptions, + path: this.npm.prefix, + allowScripts: policy, + }) + await arb.loadActual() + + const unreviewed = await checkAllowScripts({ arb, npm: this.npm }) + + if (pending) { + return this.runPending(unreviewed) + } + + if (all) { + return this.runAll(unreviewed) + } + + return this.runPositional(args, arb) + } + + runPending (unreviewed) { + if (unreviewed.length === 0) { + output.standard('No packages with unreviewed install scripts.') + return + } + const count = unreviewed.length + const has = count === 1 ? 'has' : 'have' + const pkg = count === 1 ? 'package' : 'packages' + output.standard( + `${count} ${pkg} ${has} install scripts not yet covered by allowScripts:` + ) + for (const { node, scripts } of unreviewed) { + const { name, version } = trustedDisplay(node) + /* istanbul ignore next: every test node has a name */ + const display = name || '' + const ver = version ? `@${version}` : '' + const events = Object.entries(scripts) + .map(([event, cmd]) => `${event}: ${cmd}`) + .join('; ') + output.standard(` ${display}${ver} (${events})`) + } + output.standard('') + output.standard( + 'Run `npm approve-scripts ` to allow, or `npm deny-scripts ` to deny.' + ) + } + + async runAll (unreviewed) { + if (unreviewed.length === 0) { + output.standard('No packages with unreviewed install scripts.') + return + } + // Bundled dependencies cannot be allowlisted in Phase 1 (RFC defers + // this to a follow-up because matching by name@version from the + // bundled tarball would reintroduce manifest confusion). Exclude + // them from `--all` so we don't silently write a policy entry under + // attacker-controlled identity. + const candidates = unreviewed.filter(({ node }) => !node.inBundle) + const skipped = unreviewed.length - candidates.length + if (skipped > 0) { + /* istanbul ignore next: plural variant covered separately */ + const noun = skipped === 1 ? 'dependency' : 'dependencies' + log.warn( + this.logTitle, + `Skipping ${skipped} bundled ${noun}; bundled deps with install ` + + 'scripts cannot be allowlisted in this release.' + ) + } + if (candidates.length === 0) { + output.standard('No packages eligible for approval.') + return + } + const groups = this.groupByPackage(candidates.map(({ node }) => node)) + await this.writePolicyChanges(groups) + } + + async runPositional (args, arb) { + const matched = this.findNodesForArgs(args, arb) + const groups = this.groupByPackage(matched) + if (Object.keys(groups).length === 0) { + throw Object.assign( + new Error(`No installed packages match: ${args.join(', ')}`), + { code: 'ENOMATCH' } + ) + } + await this.writePolicyChanges(groups) + } + + findNodesForArgs (args, arb) { + // Match positional args against each node's trusted name. Registry + // deps use the URL-derived name; non-registry deps fall back to the + // dependency edge name. Bundled deps are excluded for the same reason + // as --all. + const wanted = new Set(args) + const matched = [] + for (const node of arb.actualTree.inventory.values()) { + if (node.isProjectRoot || node.isWorkspace || node.inBundle) { + continue + } + const { name } = trustedDisplay(node) + if (name && wanted.has(name)) { + matched.push(node) + } + } + return matched + } + + get logTitle () { + return this.constructor.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + } + + groupByPackage (nodes) { + const groups = {} + for (const node of nodes) { + const key = nameKeyFor(node) + /* istanbul ignore if: callers prefilter via inBundle and trustedDisplay so untrusted nodes don't reach here */ + if (!key) { + log.warn( + this.logTitle, + `skipping ${node.name || ''}: no trusted identity for policy key` + ) + continue + } + if (!groups[key]) { + groups[key] = [] + } + groups[key].push(node) + } + return groups + } + + async writePolicyChanges (groups) { + const pin = this.npm.config.get('allow-scripts-pin') !== false + + const pkg = await pkgJson.load(this.npm.prefix) + const content = pkg.content + const existing = content.allowScripts && typeof content.allowScripts === 'object' + ? content.allowScripts + : {} + + let updated = existing + const summary = [] + + for (const [name, nodes] of Object.entries(groups)) { + const result = this.verb === 'approve' + ? applyApprovalForPackage(updated, nodes, { pin }) + : applyDenyForPackage(updated, nodes) + + if (result.warning) { + log.warn(this.logTitle, result.warning) + } + updated = result.allowScripts + summary.push({ name, changes: result.changes }) + } + + /* istanbul ignore else: writePolicyChanges only called when changes are expected */ + if (updated !== existing) { + pkg.update({ allowScripts: updated }) + await pkg.save() + } + + this.printSummary(summary) + } + + printSummary (summary) { + if (this.npm.flatOptions.json) { + output.buffer({ allowScripts: summary }) + return + } + const verb = this.verb === 'approve' ? 'Approved' : 'Denied' + let touched = 0 + for (const { name, changes } of summary) { + if (changes.length === 0) { + continue + } + touched++ + output.standard(`${verb} ${name}:`) + for (const { key, change } of changes) { + output.standard(` ${change} ${key}`) + } + } + if (touched === 0) { + output.standard(`Nothing to ${this.verb}; allowScripts unchanged.`) + } + } +} + +module.exports = AllowScriptsCmd diff --git a/deps/npm/lib/utils/allow-scripts-writer.js b/deps/npm/lib/utils/allow-scripts-writer.js new file mode 100644 index 00000000000000..5f43bbebeedefa --- /dev/null +++ b/deps/npm/lib/utils/allow-scripts-writer.js @@ -0,0 +1,323 @@ +const npa = require('npm-package-arg') +const { log } = require('proc-log') +const { getTrustedRegistryIdentity } = require('@npmcli/arborist/lib/script-allowed.js') + +// Pure helpers that implement the RFC's pin-mismatch table for +// `npm approve-scripts` and `npm deny-scripts`. +// +// Approving writes either `"": true` or `"": true` to the +// project's `allowScripts` field, depending on `--allow-scripts-pin` and the currently +// installed versions. +// +// Denying always writes `"": false`, regardless of `--allow-scripts-pin`, per the +// RFC's asymmetric-pin rule. + +// Convert an arborist Node into the spec string used for a versioned policy +// entry. Returns `null` if the node cannot be represented as a versioned key +// derived from trusted sources (lockfile URL for registry, hosted shortcut +// for git, the resolved file path for local installs). Never falls back to +// `node.packageName` / `node.version`, which are tarball-controlled. +const versionedKeyFor = (node) => { + if (!node) { + return null + } + /* istanbul ignore next: callers guarantee a string resolved */ + const resolved = typeof node.resolved === 'string' ? node.resolved : '' + if (resolved.startsWith('git')) { + try { + const parsed = npa(resolved) + if (parsed.hosted) { + const committish = parsed.gitCommittish || parsed.hosted.committish + const base = parsed.hosted.shortcut({ noCommittish: true }) + return committish ? `${base}#${committish}` : base + } + } catch { + /* istanbul ignore next: npa already parsed this string in keyTargetsNode */ + return null + } + return null + } + if (/^https?:\/\//.test(resolved)) { + const trusted = getTrustedRegistryIdentity(node) + if (trusted && trusted.version) { + return `${trusted.name}@${trusted.version}` + } + // Registry node with a resolved URL that versionFromTgz couldn't + // parse (private-registry mirror, alternate CDN URL shape). Leave a + // breadcrumb so users notice when policy keys are silently pruned. + log.silly( + 'allow-scripts', + `unable to derive trusted versioned key for ${node.path || node.name || ''} ` + + `(resolved: ${resolved}); key will be pruned on next save` + ) + return null + } + /* istanbul ignore next: 'file:' and '/' branches are each covered separately */ + if (resolved.startsWith('file:') || resolved.startsWith('/')) { + return resolved + } + // No trusted source. Refuse to compose a key from attacker-controlled + // `node.packageName` / `node.version`. + /* istanbul ignore next: callers filter out non-registry/non-file nodes before reaching this fallback */ + return null +} + +// Convert an arborist Node into the spec string used for a name-only policy +// entry. Same trust rules as versionedKeyFor — returns `null` rather than +// falling back to tarball-controlled fields. +const nameKeyFor = (node) => { + if (!node) { + return null + } + /* istanbul ignore next: callers guarantee a string resolved */ + const resolved = typeof node.resolved === 'string' ? node.resolved : '' + if (resolved.startsWith('git')) { + try { + const parsed = npa(resolved) + if (parsed.hosted) { + return parsed.hosted.shortcut({ noCommittish: true }) + } + } catch { + /* istanbul ignore next: npa already parsed this string in keyTargetsNode */ + return null + } + return null + } + if (resolved.startsWith('file:') || resolved.startsWith('/')) { + return resolved + } + // Registry deps: only the URL-derived (or edges-derived, in the + // omit-lockfile case) trusted name is acceptable. + const trusted = getTrustedRegistryIdentity(node) + return trusted ? trusted.name : null +} + +const isSingleVersionPin = (key) => { + try { + const parsed = npa(key) + return parsed.type === 'version' + } catch { + return false + } +} + +// Build the warning string emitted when an existing deny entry blocks +// an approval. Per RFC, a name-only deny ("pkg": false) is widest and +// the only remediation is to remove the entry. A versioned deny +// ("pkg@1.2.3": false or a disjunction) blocks only specific versions; +// the user can either widen it via `npm deny-scripts ` or remove +// it to approve the currently-installed version only. +const denyWarning = (key, subject, name) => { + if (isNameOnlyKey(key)) { + return `${key} is denied; remove the entry from allowScripts to approve ${subject}.` + } + /* istanbul ignore next: name fallback is defensive; callers pass nameKeyFor(sample) */ + const widenTarget = name || 'this package' + return `${key} is a versioned deny; run \`npm deny-scripts ${widenTarget}\` ` + + `to widen the deny to all versions of ${widenTarget}, or remove the entry ` + + `to approve ${subject}.` +} + +const isNameOnlyKey = (key) => { + try { + const parsed = npa(key) + if (parsed.type === 'tag') { + return true + } + if (parsed.type === 'range') { + return parsed.fetchSpec === '*' + || parsed.rawSpec === '' + || parsed.rawSpec === '*' + } + return false + } catch { + /* istanbul ignore next: keys reaching this helper have already parsed via keyTargetsNode */ + return false + } +} + +// Does this policy key target this node by identity (ignoring the +// allow/deny value)? +// +// Registry keys (`tag`, `range`, `version`) require a trusted identity on +// the node. If the node has no `getTrustedRegistryIdentity` result, the +// key does not match — never fall back to `node.name`, which is the +// install-directory name and is forgeable through aliases / manifest +// confusion. +const keyTargetsNode = (key, node) => { + let parsed + try { + parsed = npa(key) + } catch { + return false + } + switch (parsed.type) { + case 'tag': + case 'range': + case 'version': { + const trusted = getTrustedRegistryIdentity(node) + if (!trusted) { + return false + } + return trusted.name === parsed.name + } + case 'git': { + let resolvedParsed + try { + resolvedParsed = node.resolved ? npa(node.resolved) : null + } catch { + /* istanbul ignore next */ + return false + } + const keyHost = parsed.hosted?.ssh({ noCommittish: true }) + const nodeHost = resolvedParsed?.hosted?.ssh({ noCommittish: true }) + return !!(keyHost && nodeHost && keyHost === nodeHost) + } + case 'file': + case 'directory': + case 'remote': + return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec + default: + return false + } +} + +// Apply approvals for all currently-installed versions of a single package. +// +// `nodes` must all share an identity (same package name for registry deps, +// or same hosted shortcut for git deps, etc.). The caller is responsible +// for grouping nodes correctly. +// +// Returns `{ allowScripts, changes, warning }` where: +// - `allowScripts` is the new object (the input is never mutated) +// - `changes` is a list of `{ key, change }` entries describing edits +// - `warning` is an optional message to surface to the user +const applyApprovalForPackage = (existing, nodes, { pin = true } = {}) => { + const allowScripts = { ...existing } + const changes = [] + + if (!Array.isArray(nodes) || nodes.length === 0) { + return { allowScripts, changes } + } + + const sample = nodes[0] + const name = nameKeyFor(sample) + + // Deny-wins: any existing false that targets any installed version aborts. + for (const node of nodes) { + for (const [key, value] of Object.entries(allowScripts)) { + if (value === false && keyTargetsNode(key, node)) { + /* istanbul ignore next: name fallback covers the empty-name edge case */ + const subject = name || 'this package' + return { + allowScripts, + changes, + warning: denyWarning(key, subject, name), + } + } + } + } + + if (!pin) { + // Name-only mode: collapse any single-version pins for this package + // into a single name-only entry. + for (const key of Object.keys(allowScripts)) { + if ( + keyTargetsNode(key, sample) && + key !== name && + isSingleVersionPin(key) && + allowScripts[key] === true + ) { + delete allowScripts[key] + } + } + + /* istanbul ignore else: name === null is the no-identity path tested separately */ + if (name && allowScripts[name] !== true) { + allowScripts[name] = true + changes.push({ key: name, change: 'added' }) + } + return { allowScripts, changes } + } + + // Pin mode. For each currently installed version, write a single-version + // pin if one is not already in place. Stale single-version pins for this + // package are removed. Per the RFC's pin-mismatch table, an existing + // name-only entry (`pkg: true`) is replaced by `pkg@x.y.z: true` once + // every installed version has a pin. + const installedKeys = new Set(nodes.map(versionedKeyFor).filter(Boolean)) + + for (const key of Object.keys(allowScripts)) { + if ( + keyTargetsNode(key, sample) && + isSingleVersionPin(key) && + allowScripts[key] === true && + !installedKeys.has(key) + ) { + delete allowScripts[key] + changes.push({ key, change: 'removed-stale' }) + } + } + + for (const key of installedKeys) { + if (allowScripts[key] !== true) { + allowScripts[key] = true + changes.push({ key, change: 'added' }) + } + } + + // Upgrade: drop the name-only entry once every installed version has a + // pin. The operation is convergent: running the command twice produces + // the same shape regardless of the starting state. + if ( + installedKeys.size > 0 && + name && + !installedKeys.has(name) && + allowScripts[name] === true + ) { + delete allowScripts[name] + changes.push({ key: name, change: 'replaced-by-pin' }) + } + + return { allowScripts, changes } +} + +// Apply a deny for a single package. Always name-only; ignores `--allow-scripts-pin`. +const applyDenyForPackage = (existing, nodes) => { + const allowScripts = { ...existing } + const changes = [] + + if (!Array.isArray(nodes) || nodes.length === 0) { + return { allowScripts, changes } + } + + const sample = nodes[0] + const name = nameKeyFor(sample) + if (!name) { + return { allowScripts, changes } + } + + // Drop any pinned allow entries for this package: the name-only deny + // overrides them anyway, and leaving them in place is confusing. + for (const key of Object.keys(allowScripts)) { + if (keyTargetsNode(key, sample) && key !== name) { + delete allowScripts[key] + changes.push({ key, change: 'removed-pinned-allow' }) + } + } + + if (allowScripts[name] !== false) { + allowScripts[name] = false + changes.push({ key: name, change: 'added' }) + } + return { allowScripts, changes } +} + +module.exports = { + applyApprovalForPackage, + applyDenyForPackage, + versionedKeyFor, + nameKeyFor, + keyTargetsNode, + isSingleVersionPin, +} diff --git a/deps/npm/lib/utils/check-allow-scripts.js b/deps/npm/lib/utils/check-allow-scripts.js new file mode 100644 index 00000000000000..5ef2bfb74cf153 --- /dev/null +++ b/deps/npm/lib/utils/check-allow-scripts.js @@ -0,0 +1,54 @@ +const isScriptAllowed = require('@npmcli/arborist/lib/script-allowed.js') +const getInstallScripts = require('@npmcli/arborist/lib/install-scripts.js') + +// Walks arb.actualTree.inventory and returns the list of dep nodes that +// have install-relevant lifecycle scripts and are not yet covered (or +// explicitly denied) by the allowScripts policy. +// +// Returns an array of `{ node, scripts }` entries. `scripts` is an object +// describing the relevant lifecycle scripts that would run. + +const checkAllowScripts = async ({ arb, npm, tree }) => { + const ignoreScripts = !!arb.options?.ignoreScripts + const dangerouslyAllowAll = !!npm?.flatOptions?.dangerouslyAllowAllScripts + + if (ignoreScripts || dangerouslyAllowAll) { + return [] + } + + // Defaults to actualTree (post-reify) but accepts an explicit tree so + // callers can pre-flight against the idealTree before scripts run. + const targetTree = tree || arb.actualTree + if (!targetTree?.inventory) { + return [] + } + + const policy = arb.options?.allowScripts || null + + const unreviewed = [] + for (const node of targetTree.inventory.values()) { + if (node.isProjectRoot || node.isWorkspace) { + continue + } + if (node.isLink) { + // Linked workspace dependencies are managed by the workspace owner. + continue + } + + const verdict = isScriptAllowed(node, policy) + if (verdict === true || verdict === false) { + continue + } + + const scripts = await getInstallScripts(node) + if (Object.keys(scripts).length === 0) { + continue + } + + unreviewed.push({ node, scripts }) + } + + return unreviewed +} + +module.exports = checkAllowScripts diff --git a/deps/npm/lib/utils/cmd-list.js b/deps/npm/lib/utils/cmd-list.js index 0166b1cc862ba2..1909df0d045469 100644 --- a/deps/npm/lib/utils/cmd-list.js +++ b/deps/npm/lib/utils/cmd-list.js @@ -5,6 +5,7 @@ const abbrev = require('abbrev') const commands = [ 'access', 'adduser', + 'approve-scripts', 'audit', 'bugs', 'cache', @@ -12,6 +13,7 @@ const commands = [ 'completion', 'config', 'dedupe', + 'deny-scripts', 'deprecate', 'diff', 'dist-tag', diff --git a/deps/npm/lib/utils/reify-finish.js b/deps/npm/lib/utils/reify-finish.js index 5e1330f4937bbd..1041c53fdb9357 100644 --- a/deps/npm/lib/utils/reify-finish.js +++ b/deps/npm/lib/utils/reify-finish.js @@ -1,4 +1,6 @@ const reifyOutput = require('./reify-output.js') +const checkAllowScripts = require('./check-allow-scripts.js') +const warnWorkspaceAllowScripts = require('./warn-workspace-allow-scripts.js') const ini = require('ini') const { writeFile } = require('node:fs/promises') const { resolve } = require('node:path') @@ -15,7 +17,9 @@ const reifyFinish = async (npm, arb) => { } } } - reifyOutput(npm, arb) + warnWorkspaceAllowScripts(arb.actualTree) + const unreviewedScripts = await checkAllowScripts({ arb, npm }) + reifyOutput(npm, arb, { unreviewedScripts }) } module.exports = reifyFinish diff --git a/deps/npm/lib/utils/reify-output.js b/deps/npm/lib/utils/reify-output.js index 99427faaf66488..b1e1ffbcddd175 100644 --- a/deps/npm/lib/utils/reify-output.js +++ b/deps/npm/lib/utils/reify-output.js @@ -14,11 +14,12 @@ const { depth } = require('treeverse') const ms = require('ms') const npmAuditReport = require('npm-audit-report') const { readTree: getFundingInfo } = require('libnpmfund') +const { trustedDisplay } = require('@npmcli/arborist/lib/script-allowed.js') const auditError = require('./audit-error.js') -// TODO: output JSON if flatOptions.json is true -const reifyOutput = (npm, arb) => { +const reifyOutput = (npm, arb, extras = {}) => { const { diff, actualTree } = arb + const unreviewedScripts = extras.unreviewedScripts || [] // note: fails and crashes if we're running audit fix and there was an error which is a good thing, because there's no point printing all this other stuff in that case! const auditReport = auditError(npm, arb.auditReport) ? null : arb.auditReport @@ -113,11 +114,23 @@ const reifyOutput = (npm, arb) => { summary.audit = npm.command === 'audit' ? auditReport : auditReport.toJSON().metadata } + if (unreviewedScripts.length) { + summary.unreviewedScripts = unreviewedScripts.map(({ node, scripts }) => { + const { name, version } = trustedDisplay(node) + return { + name, + version, + path: node.path, + scripts, + } + }) + } output.buffer(summary) } else { packagesChangedMessage(npm, summary) packagesFundingMessage(npm, summary) printAuditReport(npm, auditReport) + unreviewedScriptsMessage(npm, unreviewedScripts) } } @@ -217,4 +230,39 @@ const packagesFundingMessage = (npm, { funding }) => { output.standard(' run `npm fund` for details') } +const unreviewedScriptsMessage = (npm, unreviewedScripts) => { + if (!unreviewedScripts.length) { + return + } + + // Goes through log.warn so it respects --loglevel / --silent and lands + // on stderr like every other "FYI, here's something to know" message. + // stdout is reserved for things the user explicitly asked to see + // (npm ls, npm view). + const count = unreviewedScripts.length + const pkg = count === 1 ? 'package has' : 'packages have' + const header = `${count} ${pkg} install scripts not yet covered by allowScripts:` + + const lines = unreviewedScripts.map(({ node, scripts }) => { + const { name, version } = trustedDisplay(node) + /* istanbul ignore next: every test node has a name */ + const display = name || '' + const ver = version ? `@${version}` : '' + const events = Object.entries(scripts) + .map(([event, cmd]) => `${event}: ${cmd}`) + .join('; ') + return ` ${display}${ver} (${events})` + }) + + log.warn( + 'allow-scripts', + [ + header, + ...lines, + '', + 'Run `npm approve-scripts --allow-scripts-pending` to review, or `npm approve-scripts ` to allow.', + ].join('\n') + ) +} + module.exports = reifyOutput diff --git a/deps/npm/lib/utils/resolve-allow-scripts.js b/deps/npm/lib/utils/resolve-allow-scripts.js new file mode 100644 index 00000000000000..b658e1a68ad0cf --- /dev/null +++ b/deps/npm/lib/utils/resolve-allow-scripts.js @@ -0,0 +1,181 @@ +const { log } = require('proc-log') +const npa = require('npm-package-arg') +const pkgJson = require('@npmcli/package-json') +const { isExactVersionDisjunction } = require('@npmcli/arborist/lib/script-allowed.js') +const parseAllowScriptsList = require('@npmcli/config/lib/parse-allow-scripts-list.js') + +const buildPolicyFromNames = (names) => { + /* istanbul ignore if: callers only pass non-empty arrays */ + if (!names.length) { + return null + } + const policy = {} + for (const name of names) { + policy[name] = true + } + return policy +} + +// Read the `allow-scripts` value from one or more named config sources and +// build a policy object. Returns `null` if none of the sources has a value. +const policyFromSources = (npm, sources) => { + for (const where of sources) { + const value = npm.config.get?.('allow-scripts', where) + if (value === undefined) { + continue + } + const names = parseAllowScriptsList(value) + /* istanbul ignore else: parseAllowScriptsList returns non-empty when value is set */ + if (names.length) { + return buildPolicyFromNames(names) + } + } + return null +} + +const validatePolicy = (policy, sourceLabel) => { + // Drop and warn about keys with forbidden semver ranges (^, ~, >=, <, *). + // The RFC only permits exact versions joined by `||`. Bare names like + // `canvas` and explicit name-only wildcards (`canvas@*`) are allowed. + if (!policy) { + return policy + } + const cleaned = {} + for (const [key, value] of Object.entries(policy)) { + let parsed + try { + parsed = npa(key) + } catch { + log.warn('allow-scripts', `${sourceLabel}: ignoring unparseable entry "${key}"`) + continue + } + if (parsed.type === 'tag') { + // `pkg@latest`, `pkg@next`, etc. look like a pin but behave name- + // only — the matcher has no way to verify what the tag points at + // when scripts run. Reject for the same reason as semver ranges. + log.warn( + 'allow-scripts', + `${sourceLabel}: ignoring "${key}" — dist-tag specs (@latest, @next, ...) are not allowed; ` + + 'use exact versions joined by "||", or the bare package name, instead' + ) + continue + } + if (parsed.type === 'range') { + const isNameOnly = parsed.fetchSpec === '*' + || parsed.rawSpec === '' + || parsed.rawSpec === '*' + if (!isNameOnly && !isExactVersionDisjunction(parsed.fetchSpec)) { + log.warn( + 'allow-scripts', + `${sourceLabel}: ignoring "${key}" — semver ranges (^, ~, >=, <) are not allowed; ` + + 'use exact versions joined by "||" instead' + ) + continue + } + } + cleaned[key] = value + } + return Object.keys(cleaned).length > 0 ? cleaned : null +} + +// Resolve the effective allowScripts policy from the layered sources. +// Returns `{ policy, source }` where: +// - `policy` is an object map of `package-spec` -> boolean, or `null` if +// no layer has any configuration +// - `source` is one of `'cli'`, `'package.json'`, `'.npmrc'`, or `null` +// +// Precedence order (highest to lowest), per RFC npm/rfcs#868: +// 1. CLI flags (--allow-scripts) and env vars +// 2. Root `package.json#allowScripts` +// 3. `.npmrc` cascade (project, user, global) +// +// The project `package.json` layer is skipped when: +// - `npm.global` is true (no project context exists for global installs) +// - `skipProjectConfig` is true (e.g. npm exec / npx, which per the RFC +// consult only user/global .npmrc) +// +// In both skipped cases, the CLI and .npmrc layers are still consulted; +// only the project package.json layer is skipped. +// +// The first source with any configuration wins for the entire install; +// lower layers are ignored. A `log.warn` is emitted whenever a setting is +// being suppressed by a higher-priority source. +// +// Reads `package.json` from `npm.prefix` (not `npm.localPrefix`) so an +// install run from a workspace sub-directory still picks up the project +// root's policy. +const resolveAllowScripts = async (npm, { skipProjectConfig = false } = {}) => { + // Independently probe each RFC layer. + const cliPolicy = policyFromSources(npm, ['cli', 'env']) + const npmrcPolicy = policyFromSources(npm, ['project', 'user', 'global', 'builtin']) + + // The --allow-scripts CLI flag is intended for one-off and global + // contexts (npm exec, npx, npm install -g). In a project-scoped install, + // team policy belongs in package.json or .npmrc, so reject the flag + // outright to avoid the "works on my machine" footgun. + if (cliPolicy && !npm.global && !skipProjectConfig) { + throw Object.assign( + new Error( + '--allow-scripts is not allowed in project-scoped installs. ' + + 'Add the entries to the "allowScripts" field in package.json, ' + + 'or to .npmrc, instead.' + ), + { code: 'EALLOWSCRIPTS' } + ) + } + + // Project package.json is consulted only when the caller is operating + // inside a real project (not -g, not npx). + let pkgPolicy = null + if (!npm.global && !skipProjectConfig) { + try { + const { content } = await pkgJson.normalize(npm.prefix) + if (content?.allowScripts && typeof content.allowScripts === 'object') { + const entries = Object.entries(content.allowScripts) + if (entries.length > 0) { + pkgPolicy = Object.fromEntries(entries) + } + } + } catch (err) { + log.silly('allow-scripts', 'no package.json at prefix', err.message) + } + } + + // Validate each candidate layer: drop forbidden ranges, warn the user. + const cli = validatePolicy(cliPolicy, 'CLI flag') + const pkg = validatePolicy(pkgPolicy, 'package.json') + const rc = validatePolicy(npmrcPolicy, '.npmrc') + + // Apply RFC precedence. + if (cli) { + // Note: `pkg` is never set alongside `cli` here. Project package.json is + // only read when `!npm.global && !skipProjectConfig`, but in that same + // case a CLI policy throws above. With `npm.global` or skipProjectConfig + // set, package.json is never consulted. + if (rc) { + log.warn( + 'allow-scripts', + '.npmrc allow-scripts setting is being ignored because --allow-scripts was passed on the command line' + ) + } + return { policy: cli, source: 'cli' } + } + + if (pkg) { + if (rc) { + log.warn( + 'allow-scripts', + '.npmrc allow-scripts setting is being ignored because package.json declares its own allowScripts field' + ) + } + return { policy: pkg, source: 'package.json' } + } + + if (rc) { + return { policy: rc, source: '.npmrc' } + } + + return { policy: null, source: null } +} + +module.exports = resolveAllowScripts diff --git a/deps/npm/lib/utils/strict-allow-scripts-preflight.js b/deps/npm/lib/utils/strict-allow-scripts-preflight.js new file mode 100644 index 00000000000000..a3f83ea4b662bc --- /dev/null +++ b/deps/npm/lib/utils/strict-allow-scripts-preflight.js @@ -0,0 +1,61 @@ +const checkAllowScripts = require('./check-allow-scripts.js') + +// Pre-flight check for `--strict-allow-scripts`. Call after arborist has +// been constructed but before `arb.reify()` runs, so that install scripts +// never execute when strict mode would block them. +// +// Behaviour: +// - No-op unless `npm.flatOptions.strictAllowScripts` is set. +// - Bypassed by `--dangerously-allow-all-scripts` and `--ignore-scripts` +// (the per-flag short-circuits already live in checkAllowScripts). +// - Builds the ideal tree (idempotent — subsequent reify reuses it), +// walks it for nodes whose install scripts have not been covered by +// the `allowScripts` policy, and throws if any are found. +const strictAllowScriptsPreflight = async ({ arb, npm, idealTreeOpts }) => { + if (!npm?.flatOptions?.strictAllowScripts) { + return + } + + // Prefer the idealTree when reify is about to run; fall back to + // actualTree for npm rebuild (which never builds an ideal tree). + let tree + if (idealTreeOpts !== undefined) { + // `npm ci` builds the ideal tree before calling the preflight, so + // skip the rebuild when one already exists. `npm install` calls the + // preflight before reify and needs us to build. + if (!arb.idealTree) { + await arb.buildIdealTree(idealTreeOpts) + } + tree = arb.idealTree + } else { + tree = arb.actualTree + } + + const unreviewed = await checkAllowScripts({ arb, npm, tree }) + if (unreviewed.length === 0) { + return + } + + const lines = unreviewed.map(({ node, scripts }) => { + const events = Object.entries(scripts) + .map(([event, body]) => `${event}: ${body}`) + .join('; ') + const name = node.package?.name || node.name + const version = node.package?.version || '' + const label = version ? `${name}@${version}` : name + return ` ${label} (${events})` + }).join('\n') + + throw Object.assign( + new Error( + `--strict-allow-scripts: ${unreviewed.length} package(s) have install ` + + `scripts not covered by allowScripts:\n${lines}\n` + + 'Approve them with `npm approve-scripts`, deny them with ' + + '`npm deny-scripts`, or bypass this check with ' + + '`--dangerously-allow-all-scripts`.' + ), + { code: 'ESTRICTALLOWSCRIPTS' } + ) +} + +module.exports = strictAllowScriptsPreflight diff --git a/deps/npm/lib/utils/warn-workspace-allow-scripts.js b/deps/npm/lib/utils/warn-workspace-allow-scripts.js new file mode 100644 index 00000000000000..e46e6cf4d2a10b --- /dev/null +++ b/deps/npm/lib/utils/warn-workspace-allow-scripts.js @@ -0,0 +1,40 @@ +const { log } = require('proc-log') + +// The allowScripts policy MUST live at the project root (RFC npm/rfcs#868). +// A non-root workspace declaring its own allowScripts is almost always a +// mistake: that policy would be silently ignored at install time. +// +// `findWorkspaceAllowScripts` returns the list of offending workspace nodes. +// `warnWorkspaceAllowScripts` is the side-effecting variant that emits one +// install-time `log.warn` per offender. + +const findWorkspaceAllowScripts = (tree) => { + const offenders = [] + if (!tree?.inventory) { + return offenders + } + for (const node of tree.inventory.values()) { + if (!node.isWorkspace || node.isProjectRoot) { + continue + } + if (node.package?.allowScripts !== undefined) { + offenders.push(node) + } + } + return offenders +} + +const warnWorkspaceAllowScripts = (tree) => { + for (const node of findWorkspaceAllowScripts(tree)) { + const name = node.packageName || node.name + log.warn( + 'allow-scripts', + `allowScripts in workspace ${name} (${node.path}) is ignored. ` + + 'Move the field to the project root package.json.' + ) + } +} + +module.exports = warnWorkspaceAllowScripts +module.exports.warnWorkspaceAllowScripts = warnWorkspaceAllowScripts +module.exports.findWorkspaceAllowScripts = findWorkspaceAllowScripts diff --git a/deps/npm/man/man1/npm-access.1 b/deps/npm/man/man1/npm-access.1 index 6490de4ada7a33..1137b4d5fbee07 100644 --- a/deps/npm/man/man1/npm-access.1 +++ b/deps/npm/man/man1/npm-access.1 @@ -1,4 +1,4 @@ -.TH "NPM-ACCESS" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-ACCESS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-access\fR - Set access level on published packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-adduser.1 b/deps/npm/man/man1/npm-adduser.1 index 849f6a40416269..b52bf3ed7f980e 100644 --- a/deps/npm/man/man1/npm-adduser.1 +++ b/deps/npm/man/man1/npm-adduser.1 @@ -1,4 +1,4 @@ -.TH "NPM-ADDUSER" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-ADDUSER" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-adduser\fR - Add a registry user account .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-approve-scripts.1 b/deps/npm/man/man1/npm-approve-scripts.1 new file mode 100644 index 00000000000000..b6ac279a025584 --- /dev/null +++ b/deps/npm/man/man1/npm-approve-scripts.1 @@ -0,0 +1,113 @@ +.TH "NPM-APPROVE-SCRIPTS" "1" "May 2026" "NPM@11.16.0" "" +.SH "NAME" +\fBnpm-approve-scripts\fR - Approve install scripts for specific dependencies +.SS "Synopsis" +.P +.RS 2 +.nf +npm approve-scripts \[lB] ...\[rB] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +.fi +.RE +.P +Note: This command is unaware of workspaces. +.SS "Description" +.P +Manages the \fBallowScripts\fR field in your project's \fBpackage.json\fR, which records which of your dependencies are permitted to run install scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry sources). This command is the recommended way to maintain that field. +.P +In the current release, this field is advisory: install scripts still run by default, but installs print a list of packages whose scripts have not been reviewed. A future release will block unreviewed install scripts. +.P +There are three modes: +.P +.RS 2 +.nf +npm approve-scripts \[lB] ...\[rB] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +.fi +.RE +.P +\fB\fR matches every installed version of that package. By default the command writes pinned entries (\fBpkg@1.2.3\fR), which keep their approval narrowed to the specific version you reviewed. Pass \fB--no-allow-scripts-pin\fR to write name-only entries that allow any future version. +.P +\fB--all\fR approves every package with unreviewed install scripts in one go. +.P +\fB--allow-scripts-pending\fR is read-only: it lists every package whose install scripts are not yet covered by \fBallowScripts\fR, without modifying \fBpackage.json\fR. +.P +\fBapprove-scripts\fR honours the asymmetric pin rule: if you re-approve a package whose installed version has changed, the existing pin is rewritten to track the new installed version. Multi-version statements (\fBpkg@1 || 2\fR) are left alone, since they likely capture intent that the command cannot infer. Existing \fBfalse\fR entries always win; \fBapprove-scripts\fR will not silently re-allow a package you previously denied. +.SS "Examples" +.P +.RS 2 +.nf +# Approve all currently-installed install scripts after reviewing them +npm approve-scripts --all + +# Approve specific packages, pinned to their installed version +npm approve-scripts canvas sharp + +# Approve name-only (any version of this package is allowed) +npm approve-scripts --no-allow-scripts-pin canvas + +# Preview which packages still need review +npm approve-scripts --allow-scripts-pending +.fi +.RE +.SS "Configuration" +.SS "\fBall\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +When running \fBnpm outdated\fR and \fBnpm ls\fR, setting \fB--all\fR will show all outdated or installed packages, rather than only those directly depended upon by the current project. +.SS "\fBallow-scripts-pending\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +List packages with install scripts that are not yet covered by the \fBallowScripts\fR policy, without modifying \fBpackage.json\fR. Only meaningful for \fBnpm approve-scripts\fR. +.SS "\fBallow-scripts-pin\fR" +.RS 0 +.IP \(bu 4 +Default: true +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Write pinned (\fBpkg@version\fR) entries when approving install scripts. Set to \fBfalse\fR to write name-only entries that allow any version. Has no effect on \fBnpm deny-scripts\fR, which always writes name-only entries regardless of this setting. +.SS "\fBjson\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Whether or not to output JSON data, rather than the normal output. +.RS 0 +.IP \(bu 4 +In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. +.RE 0 + +.P +Not supported by all npm commands. +.SS "See Also" +.RS 0 +.IP \(bu 4 +npm help deny-scripts +.IP \(bu 4 +npm help install +.IP \(bu 4 +npm help rebuild +.IP \(bu 4 +\fBpackage.json\fR \fI\(la/configuring-npm/package-json\(ra\fR +.RE 0 diff --git a/deps/npm/man/man1/npm-audit.1 b/deps/npm/man/man1/npm-audit.1 index c0704aafc31b73..f066529a8b8530 100644 --- a/deps/npm/man/man1/npm-audit.1 +++ b/deps/npm/man/man1/npm-audit.1 @@ -1,4 +1,4 @@ -.TH "NPM-AUDIT" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-AUDIT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-audit\fR - Run a security audit .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-bugs.1 b/deps/npm/man/man1/npm-bugs.1 index 77f78edd37ee28..6c9d745c20f3f2 100644 --- a/deps/npm/man/man1/npm-bugs.1 +++ b/deps/npm/man/man1/npm-bugs.1 @@ -1,4 +1,4 @@ -.TH "NPM-BUGS" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-BUGS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-bugs\fR - Report bugs for a package in a web browser .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-cache.1 b/deps/npm/man/man1/npm-cache.1 index 8ed27305563e99..17e38319e16223 100644 --- a/deps/npm/man/man1/npm-cache.1 +++ b/deps/npm/man/man1/npm-cache.1 @@ -1,4 +1,4 @@ -.TH "NPM-CACHE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-CACHE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-cache\fR - Manipulates packages cache .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ci.1 b/deps/npm/man/man1/npm-ci.1 index fb0a6b25221d2c..467929979eda6c 100644 --- a/deps/npm/man/man1/npm-ci.1 +++ b/deps/npm/man/man1/npm-ci.1 @@ -1,4 +1,4 @@ -.TH "NPM-CI" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-CI" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ci\fR - Clean install a project .SS "Synopsis" @@ -216,6 +216,42 @@ Type: "all", "none", or "root" Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P \fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-completion.1 b/deps/npm/man/man1/npm-completion.1 index 9dbfd321c9a643..c6a82af87d93fa 100644 --- a/deps/npm/man/man1/npm-completion.1 +++ b/deps/npm/man/man1/npm-completion.1 @@ -1,4 +1,4 @@ -.TH "NPM-COMPLETION" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-COMPLETION" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-completion\fR - Tab Completion for npm .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-config.1 b/deps/npm/man/man1/npm-config.1 index f73f7d0815a9be..28ae9ed07de461 100644 --- a/deps/npm/man/man1/npm-config.1 +++ b/deps/npm/man/man1/npm-config.1 @@ -1,4 +1,4 @@ -.TH "NPM-CONFIG" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-CONFIG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-config\fR - Manage the npm configuration files .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-dedupe.1 b/deps/npm/man/man1/npm-dedupe.1 index f0c0134b20e5f2..c62112ff7f9a8c 100644 --- a/deps/npm/man/man1/npm-dedupe.1 +++ b/deps/npm/man/man1/npm-dedupe.1 @@ -1,4 +1,4 @@ -.TH "NPM-DEDUPE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-DEDUPE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-dedupe\fR - Reduce duplication in the package tree .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-deny-scripts.1 b/deps/npm/man/man1/npm-deny-scripts.1 new file mode 100644 index 00000000000000..a466da7a30dc8f --- /dev/null +++ b/deps/npm/man/man1/npm-deny-scripts.1 @@ -0,0 +1,99 @@ +.TH "NPM-DENY-SCRIPTS" "1" "May 2026" "NPM@11.16.0" "" +.SH "NAME" +\fBnpm-deny-scripts\fR - Deny install scripts for specific dependencies +.SS "Synopsis" +.P +.RS 2 +.nf +npm deny-scripts \[lB] ...\[rB] +npm deny-scripts --all +.fi +.RE +.P +Note: This command is unaware of workspaces. +.SS "Description" +.P +The companion command to npm help approve-scripts. Writes \fBfalse\fR entries into the \fBallowScripts\fR field of your project's \fBpackage.json\fR, recording that a dependency must not run install scripts even if a future version would otherwise be eligible. +.P +In the current release, install scripts still run by default, so \fBdeny-scripts\fR only affects how installs of denied packages are reported. A future release will block unreviewed install scripts and respect deny entries at install time. +.P +.RS 2 +.nf +npm deny-scripts \[lB] ...\[rB] +npm deny-scripts --all +.fi +.RE +.P +\fB\fR matches every installed version of that package. Denies are always written name-only (\fB"pkg": false\fR), regardless of \fB--allow-scripts-pin\fR. Pinning a deny to a specific version would silently re-allow scripts for any other version of the same package, which defeats the purpose; the command picks the safer default for you. +.P +\fB--all\fR denies every package with unreviewed install scripts. +.P +If a \fBtrue\fR (pinned or name-only) entry exists for a package and you then deny it, the existing allow entries are removed so the name-only deny is unambiguous. +.SS "Examples" +.P +.RS 2 +.nf +# Deny a specific package outright +npm deny-scripts telemetry-pkg + +# Deny everything that has install scripts and isn't already approved +npm deny-scripts --all +.fi +.RE +.SS "Configuration" +.SS "\fBall\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +When running \fBnpm outdated\fR and \fBnpm ls\fR, setting \fB--all\fR will show all outdated or installed packages, rather than only those directly depended upon by the current project. +.SS "\fBallow-scripts-pending\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +List packages with install scripts that are not yet covered by the \fBallowScripts\fR policy, without modifying \fBpackage.json\fR. Only meaningful for \fBnpm approve-scripts\fR. +.SS "\fBallow-scripts-pin\fR" +.RS 0 +.IP \(bu 4 +Default: true +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Write pinned (\fBpkg@version\fR) entries when approving install scripts. Set to \fBfalse\fR to write name-only entries that allow any version. Has no effect on \fBnpm deny-scripts\fR, which always writes name-only entries regardless of this setting. +.SS "\fBjson\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Whether or not to output JSON data, rather than the normal output. +.RS 0 +.IP \(bu 4 +In \fBnpm pkg set\fR it enables parsing set values with JSON.parse() before saving them to your \fBpackage.json\fR. +.RE 0 + +.P +Not supported by all npm commands. +.SS "See Also" +.RS 0 +.IP \(bu 4 +npm help approve-scripts +.IP \(bu 4 +npm help install +.IP \(bu 4 +\fBpackage.json\fR \fI\(la/configuring-npm/package-json\(ra\fR +.RE 0 diff --git a/deps/npm/man/man1/npm-deprecate.1 b/deps/npm/man/man1/npm-deprecate.1 index 5d4fdd380cef63..0e67565fba394c 100644 --- a/deps/npm/man/man1/npm-deprecate.1 +++ b/deps/npm/man/man1/npm-deprecate.1 @@ -1,4 +1,4 @@ -.TH "NPM-DEPRECATE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-DEPRECATE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-deprecate\fR - Deprecate a version of a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-diff.1 b/deps/npm/man/man1/npm-diff.1 index 4f0e943ec7b4a3..1051276045fc84 100644 --- a/deps/npm/man/man1/npm-diff.1 +++ b/deps/npm/man/man1/npm-diff.1 @@ -1,4 +1,4 @@ -.TH "NPM-DIFF" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-DIFF" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-diff\fR - The registry diff command .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-dist-tag.1 b/deps/npm/man/man1/npm-dist-tag.1 index 0faa3fd6b2b388..a2a7e0ea36a018 100644 --- a/deps/npm/man/man1/npm-dist-tag.1 +++ b/deps/npm/man/man1/npm-dist-tag.1 @@ -1,4 +1,4 @@ -.TH "NPM-DIST-TAG" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-DIST-TAG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-dist-tag\fR - Modify package distribution tags .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-docs.1 b/deps/npm/man/man1/npm-docs.1 index ad5cfb87e86115..943c26eb53fbe3 100644 --- a/deps/npm/man/man1/npm-docs.1 +++ b/deps/npm/man/man1/npm-docs.1 @@ -1,4 +1,4 @@ -.TH "NPM-DOCS" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-DOCS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-docs\fR - Open documentation for a package in a web browser .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-doctor.1 b/deps/npm/man/man1/npm-doctor.1 index eafa7a811ffc65..0d40b9b45b14d2 100644 --- a/deps/npm/man/man1/npm-doctor.1 +++ b/deps/npm/man/man1/npm-doctor.1 @@ -1,4 +1,4 @@ -.TH "NPM-DOCTOR" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-DOCTOR" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-doctor\fR - Check the health of your npm environment .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-edit.1 b/deps/npm/man/man1/npm-edit.1 index d7f2c198a7046f..d57bdc0ce6f910 100644 --- a/deps/npm/man/man1/npm-edit.1 +++ b/deps/npm/man/man1/npm-edit.1 @@ -1,4 +1,4 @@ -.TH "NPM-EDIT" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-EDIT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-edit\fR - Edit an installed package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-exec.1 b/deps/npm/man/man1/npm-exec.1 index 3c9b5dff8b96bf..8987175e5d8410 100644 --- a/deps/npm/man/man1/npm-exec.1 +++ b/deps/npm/man/man1/npm-exec.1 @@ -1,4 +1,4 @@ -.TH "NPM-EXEC" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-EXEC" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-exec\fR - Run a command from a local or remote npm package .SS "Synopsis" @@ -167,6 +167,42 @@ Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the \fBworkspace\fR config, or all workspaces via the \fBworkspaces\fR flag, will cause npm to operate only on the specified workspaces, and not on the root project. .P This value is not exported to the environment for child processes. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "Examples" .P Run the version of \fBtap\fR in the local dependencies, with the provided arguments: diff --git a/deps/npm/man/man1/npm-explain.1 b/deps/npm/man/man1/npm-explain.1 index 551df063471993..ec315300c2f1ba 100644 --- a/deps/npm/man/man1/npm-explain.1 +++ b/deps/npm/man/man1/npm-explain.1 @@ -1,4 +1,4 @@ -.TH "NPM-EXPLAIN" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-EXPLAIN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-explain\fR - Explain installed packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-explore.1 b/deps/npm/man/man1/npm-explore.1 index abbc9975b705ef..fbc61b1de01014 100644 --- a/deps/npm/man/man1/npm-explore.1 +++ b/deps/npm/man/man1/npm-explore.1 @@ -1,4 +1,4 @@ -.TH "NPM-EXPLORE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-EXPLORE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-explore\fR - Browse an installed package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-find-dupes.1 b/deps/npm/man/man1/npm-find-dupes.1 index bae0c98a90073d..57daa9d322595e 100644 --- a/deps/npm/man/man1/npm-find-dupes.1 +++ b/deps/npm/man/man1/npm-find-dupes.1 @@ -1,4 +1,4 @@ -.TH "NPM-FIND-DUPES" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-FIND-DUPES" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-find-dupes\fR - Find duplication in the package tree .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-fund.1 b/deps/npm/man/man1/npm-fund.1 index 188d91950db7fa..05a0177501955f 100644 --- a/deps/npm/man/man1/npm-fund.1 +++ b/deps/npm/man/man1/npm-fund.1 @@ -1,4 +1,4 @@ -.TH "NPM-FUND" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-FUND" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-fund\fR - Retrieve funding information .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-get.1 b/deps/npm/man/man1/npm-get.1 index 10a03171c209b9..bbcfcae3a21158 100644 --- a/deps/npm/man/man1/npm-get.1 +++ b/deps/npm/man/man1/npm-get.1 @@ -1,4 +1,4 @@ -.TH "NPM-GET" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-GET" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-get\fR - Get a value from the npm configuration .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-help-search.1 b/deps/npm/man/man1/npm-help-search.1 index d42e5d0d84bc9b..b50eb9a9ac9c5b 100644 --- a/deps/npm/man/man1/npm-help-search.1 +++ b/deps/npm/man/man1/npm-help-search.1 @@ -1,4 +1,4 @@ -.TH "NPM-HELP-SEARCH" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-HELP-SEARCH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-help-search\fR - Search npm help documentation .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-help.1 b/deps/npm/man/man1/npm-help.1 index e4e34292fea758..eb4353fbd7c0ea 100644 --- a/deps/npm/man/man1/npm-help.1 +++ b/deps/npm/man/man1/npm-help.1 @@ -1,4 +1,4 @@ -.TH "NPM-HELP" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-HELP" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-help\fR - Get help on npm .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-init.1 b/deps/npm/man/man1/npm-init.1 index 66444a2bd30a9a..a55bf5cf7e42d3 100644 --- a/deps/npm/man/man1/npm-init.1 +++ b/deps/npm/man/man1/npm-init.1 @@ -1,4 +1,4 @@ -.TH "NPM-INIT" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-INIT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-init\fR - Create a package.json file .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-install-ci-test.1 b/deps/npm/man/man1/npm-install-ci-test.1 index d9991dd61f8fc9..4d0d125aac3a57 100644 --- a/deps/npm/man/man1/npm-install-ci-test.1 +++ b/deps/npm/man/man1/npm-install-ci-test.1 @@ -1,4 +1,4 @@ -.TH "NPM-INSTALL-CI-TEST" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-INSTALL-CI-TEST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-install-ci-test\fR - Install a project with a clean slate and run tests .SS "Synopsis" @@ -164,6 +164,42 @@ Type: "all", "none", or "root" Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P \fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-install-test.1 b/deps/npm/man/man1/npm-install-test.1 index 33311b005154fb..dd238cbf6c613b 100644 --- a/deps/npm/man/man1/npm-install-test.1 +++ b/deps/npm/man/man1/npm-install-test.1 @@ -1,4 +1,4 @@ -.TH "NPM-INSTALL-TEST" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-INSTALL-TEST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-install-test\fR - Install package(s) and run tests .SS "Synopsis" @@ -241,6 +241,42 @@ Type: "all", "none", or "root" Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P \fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-install.1 b/deps/npm/man/man1/npm-install.1 index 10163784e45dee..b51006e58e26d1 100644 --- a/deps/npm/man/man1/npm-install.1 +++ b/deps/npm/man/man1/npm-install.1 @@ -1,4 +1,4 @@ -.TH "NPM-INSTALL" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-INSTALL" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-install\fR - Install a package .SS "Synopsis" @@ -631,6 +631,42 @@ Type: "all", "none", or "root" Limits the ability for npm to fetch dependencies from urls. That is, dependencies that point to a tarball url instead of a version or semver range. Please note that this could leave your tree incomplete and some packages may not function as intended or designed. Changing this setting will not remove dependencies that are already installed. .P \fBall\fR allows any url to be installed. \fBnone\fR prevents any url from being installed. \fBroot\fR only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \fBnpm view\fR +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-link.1 b/deps/npm/man/man1/npm-link.1 index 0eb208243b715b..5fc0057bfe9b6b 100644 --- a/deps/npm/man/man1/npm-link.1 +++ b/deps/npm/man/man1/npm-link.1 @@ -1,4 +1,4 @@ -.TH "NPM-LINK" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-LINK" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-link\fR - Symlink a package folder .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ll.1 b/deps/npm/man/man1/npm-ll.1 index 774052db1d799c..860cb296e86a60 100644 --- a/deps/npm/man/man1/npm-ll.1 +++ b/deps/npm/man/man1/npm-ll.1 @@ -1,4 +1,4 @@ -.TH "NPM-LL" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-LL" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ll\fR - List installed packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-login.1 b/deps/npm/man/man1/npm-login.1 index 58ecb80834d259..037be1840a4f1a 100644 --- a/deps/npm/man/man1/npm-login.1 +++ b/deps/npm/man/man1/npm-login.1 @@ -1,4 +1,4 @@ -.TH "NPM-LOGIN" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-LOGIN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-login\fR - Login to a registry user account .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-logout.1 b/deps/npm/man/man1/npm-logout.1 index 6248c2010970f8..d3fdd55251f4ab 100644 --- a/deps/npm/man/man1/npm-logout.1 +++ b/deps/npm/man/man1/npm-logout.1 @@ -1,4 +1,4 @@ -.TH "NPM-LOGOUT" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-LOGOUT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-logout\fR - Log out of the registry .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ls.1 b/deps/npm/man/man1/npm-ls.1 index 7f4e7ee59e3244..ac695c5c633c55 100644 --- a/deps/npm/man/man1/npm-ls.1 +++ b/deps/npm/man/man1/npm-ls.1 @@ -1,4 +1,4 @@ -.TH "NPM-LS" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-LS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ls\fR - List installed packages .SS "Synopsis" @@ -20,7 +20,7 @@ Positional arguments are \fBname@version-range\fR identifiers, which will limit .P .RS 2 .nf -npm@11.15.0 /path/to/npm +npm@11.16.0 /path/to/npm └─┬ init-package-json@0.0.4 └── promzard@0.1.5 .fi diff --git a/deps/npm/man/man1/npm-org.1 b/deps/npm/man/man1/npm-org.1 index 7bcc7393e4de30..25f1ca4680cd27 100644 --- a/deps/npm/man/man1/npm-org.1 +++ b/deps/npm/man/man1/npm-org.1 @@ -1,4 +1,4 @@ -.TH "NPM-ORG" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-ORG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-org\fR - Manage orgs .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-outdated.1 b/deps/npm/man/man1/npm-outdated.1 index 8d82a119fcc6de..462141f446fa46 100644 --- a/deps/npm/man/man1/npm-outdated.1 +++ b/deps/npm/man/man1/npm-outdated.1 @@ -1,4 +1,4 @@ -.TH "NPM-OUTDATED" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-OUTDATED" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-outdated\fR - Check for outdated packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-owner.1 b/deps/npm/man/man1/npm-owner.1 index 883d500a403958..7a90c8e0c28856 100644 --- a/deps/npm/man/man1/npm-owner.1 +++ b/deps/npm/man/man1/npm-owner.1 @@ -1,4 +1,4 @@ -.TH "NPM-OWNER" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-OWNER" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-owner\fR - Manage package owners .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-pack.1 b/deps/npm/man/man1/npm-pack.1 index cb23bdb9d82541..945b3f42e3c910 100644 --- a/deps/npm/man/man1/npm-pack.1 +++ b/deps/npm/man/man1/npm-pack.1 @@ -1,4 +1,4 @@ -.TH "NPM-PACK" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PACK" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-pack\fR - Create a tarball from a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-ping.1 b/deps/npm/man/man1/npm-ping.1 index 6ac9326abf40b6..0c9f579acb2cd5 100644 --- a/deps/npm/man/man1/npm-ping.1 +++ b/deps/npm/man/man1/npm-ping.1 @@ -1,4 +1,4 @@ -.TH "NPM-PING" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PING" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-ping\fR - Ping npm registry .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-pkg.1 b/deps/npm/man/man1/npm-pkg.1 index 841cf44e420d69..525cc2fa92f67d 100644 --- a/deps/npm/man/man1/npm-pkg.1 +++ b/deps/npm/man/man1/npm-pkg.1 @@ -1,4 +1,4 @@ -.TH "NPM-PKG" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PKG" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-pkg\fR - Manages your package.json .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-prefix.1 b/deps/npm/man/man1/npm-prefix.1 index fbfadd6959fef2..0ad1dced9b9fca 100644 --- a/deps/npm/man/man1/npm-prefix.1 +++ b/deps/npm/man/man1/npm-prefix.1 @@ -1,4 +1,4 @@ -.TH "NPM-PREFIX" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PREFIX" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-prefix\fR - Display prefix .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-profile.1 b/deps/npm/man/man1/npm-profile.1 index e8cb8f6c6a6a14..ca1cdd366d80e1 100644 --- a/deps/npm/man/man1/npm-profile.1 +++ b/deps/npm/man/man1/npm-profile.1 @@ -1,4 +1,4 @@ -.TH "NPM-PROFILE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PROFILE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-profile\fR - Change settings on your registry profile .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-prune.1 b/deps/npm/man/man1/npm-prune.1 index 1afac2948d2e5d..b7dc1e212c4c89 100644 --- a/deps/npm/man/man1/npm-prune.1 +++ b/deps/npm/man/man1/npm-prune.1 @@ -1,4 +1,4 @@ -.TH "NPM-PRUNE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PRUNE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-prune\fR - Remove extraneous packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-publish.1 b/deps/npm/man/man1/npm-publish.1 index 88f976cf9fe908..3bbb7139839af8 100644 --- a/deps/npm/man/man1/npm-publish.1 +++ b/deps/npm/man/man1/npm-publish.1 @@ -1,4 +1,4 @@ -.TH "NPM-PUBLISH" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-PUBLISH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-publish\fR - Publish a package .SS "Synopsis" @@ -120,7 +120,7 @@ If used in the \fBnpm publish\fR command, this is the tag that will be added to .IP \(bu 4 Default: 'public' for new packages, existing packages it will not change the current level .IP \(bu 4 -Type: null, "restricted", or "public" +Type: null, "restricted", "public", or "private" .RE 0 .P @@ -130,6 +130,8 @@ Unscoped packages cannot be set to \fBrestricted\fR. .P Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. +.P +The value \fBprivate\fR is an alias for \fBrestricted\fR. .SS "\fBdry-run\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-query.1 b/deps/npm/man/man1/npm-query.1 index eef55eeddd7af1..cc73ad1e92470f 100644 --- a/deps/npm/man/man1/npm-query.1 +++ b/deps/npm/man/man1/npm-query.1 @@ -1,4 +1,4 @@ -.TH "NPM-QUERY" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-QUERY" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-query\fR - Dependency selector query .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-rebuild.1 b/deps/npm/man/man1/npm-rebuild.1 index 457e8013fb0e5c..af0562603369e6 100644 --- a/deps/npm/man/man1/npm-rebuild.1 +++ b/deps/npm/man/man1/npm-rebuild.1 @@ -1,4 +1,4 @@ -.TH "NPM-REBUILD" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-REBUILD" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-rebuild\fR - Rebuild a package .SS "Synopsis" @@ -101,6 +101,42 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBworkspace\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-repo.1 b/deps/npm/man/man1/npm-repo.1 index 8ad564f7ddcaeb..00b1754b495937 100644 --- a/deps/npm/man/man1/npm-repo.1 +++ b/deps/npm/man/man1/npm-repo.1 @@ -1,4 +1,4 @@ -.TH "NPM-REPO" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-REPO" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-repo\fR - Open package repository page in the browser .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-restart.1 b/deps/npm/man/man1/npm-restart.1 index e65ef425c25cca..5fb602be957ba2 100644 --- a/deps/npm/man/man1/npm-restart.1 +++ b/deps/npm/man/man1/npm-restart.1 @@ -1,4 +1,4 @@ -.TH "NPM-RESTART" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-RESTART" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-restart\fR - Restart a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-root.1 b/deps/npm/man/man1/npm-root.1 index cc07c34f7efeeb..79ab7c6debcb3e 100644 --- a/deps/npm/man/man1/npm-root.1 +++ b/deps/npm/man/man1/npm-root.1 @@ -1,4 +1,4 @@ -.TH "NPM-ROOT" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-ROOT" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-root\fR - Display npm root .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-run.1 b/deps/npm/man/man1/npm-run.1 index ef4291544ae20a..f20c43ab6a7113 100644 --- a/deps/npm/man/man1/npm-run.1 +++ b/deps/npm/man/man1/npm-run.1 @@ -1,4 +1,4 @@ -.TH "NPM-RUN" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-RUN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-run\fR - Run arbitrary package scripts .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-sbom.1 b/deps/npm/man/man1/npm-sbom.1 index 150c4b165f86bf..e04ac0e689169a 100644 --- a/deps/npm/man/man1/npm-sbom.1 +++ b/deps/npm/man/man1/npm-sbom.1 @@ -1,4 +1,4 @@ -.TH "NPM-SBOM" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-SBOM" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-sbom\fR - Generate a Software Bill of Materials (SBOM) .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-search.1 b/deps/npm/man/man1/npm-search.1 index f8d393b45317a6..51c9a99e58aefe 100644 --- a/deps/npm/man/man1/npm-search.1 +++ b/deps/npm/man/man1/npm-search.1 @@ -1,4 +1,4 @@ -.TH "NPM-SEARCH" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-SEARCH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-search\fR - Search for packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-set.1 b/deps/npm/man/man1/npm-set.1 index 13895c62176d38..280d610a6e32c4 100644 --- a/deps/npm/man/man1/npm-set.1 +++ b/deps/npm/man/man1/npm-set.1 @@ -1,4 +1,4 @@ -.TH "NPM-SET" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-SET" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-set\fR - Set a value in the npm configuration .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-shrinkwrap.1 b/deps/npm/man/man1/npm-shrinkwrap.1 index d7eadbd5438feb..0f7214bda3fd09 100644 --- a/deps/npm/man/man1/npm-shrinkwrap.1 +++ b/deps/npm/man/man1/npm-shrinkwrap.1 @@ -1,4 +1,4 @@ -.TH "NPM-SHRINKWRAP" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-SHRINKWRAP" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-shrinkwrap\fR - Lock down dependency versions for publication .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-stage.1 b/deps/npm/man/man1/npm-stage.1 index bbef577334aa9f..0fe39e8ff8cffe 100644 --- a/deps/npm/man/man1/npm-stage.1 +++ b/deps/npm/man/man1/npm-stage.1 @@ -1,4 +1,4 @@ -.TH "NPM-STAGE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-STAGE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-stage\fR - Stage packages for publishing .SS "Synopsis" @@ -101,7 +101,7 @@ npm stage publish .RE .SS "Flags" .P -| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--tag\fR | "latest" | String | If you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. It is the tag added to the package@version specified in the \fBnpm dist-tag add\fR command, if no explicit tag is given. When used by the \fBnpm diff\fR command, this is the tag used to fetch the tarball that will be compared with the local files by default. If used in the \fBnpm publish\fR command, this is the tag that will be added to the package submitted to the registry. | | \fB--access\fR | 'public' for new packages, existing packages it will not change the current level | null, "restricted", or "public" | If you do not want your scoped package to be publicly viewable (and installable) set \fB--access=restricted\fR. Unscoped packages cannot be set to \fBrestricted\fR. Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--otp\fR | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with \fBnpm access\fR. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | | \fB--workspace\fR, \fB-w\fR | | String (can be set multiple times) | Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the \fBworkspace\fR config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the \fBnpm init\fR command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. | | \fB--workspaces\fR | null | null or Boolean | Set to true to run the command in the context of \fBall\fR configured workspaces. Explicitly setting this to false will cause commands like \fBinstall\fR to ignore workspaces altogether. When not set explicitly: - Commands that operate on the \fBnode_modules\fR tree (install, update, etc.) will link workspaces into the \fBnode_modules\fR folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, \fIunless\fR one or more workspaces are specified in the \fBworkspace\fR config. | | \fB--include-workspace-root\fR | false | Boolean | Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the \fBworkspace\fR config, or all workspaces via the \fBworkspaces\fR flag, will cause npm to operate only on the specified workspaces, and not on the root project. | | \fB--provenance\fR | false | Boolean | When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. | +| Flag | Default | Type | Description | | --- | --- | --- | --- | | \fB--tag\fR | "latest" | String | If you ask npm to install a package and don't tell it a specific version, then it will install the specified tag. It is the tag added to the package@version specified in the \fBnpm dist-tag add\fR command, if no explicit tag is given. When used by the \fBnpm diff\fR command, this is the tag used to fetch the tarball that will be compared with the local files by default. If used in the \fBnpm publish\fR command, this is the tag that will be added to the package submitted to the registry. | | \fB--access\fR | 'public' for new packages, existing packages it will not change the current level | null, "restricted", "public", or "private" | If you do not want your scoped package to be publicly viewable (and installable) set \fB--access=restricted\fR. Unscoped packages cannot be set to \fBrestricted\fR. Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. The value \fBprivate\fR is an alias for \fBrestricted\fR. | | \fB--dry-run\fR | false | Boolean | Indicates that you don't want npm to make any changes and that it should only report what it would have done. This can be passed into any of the commands that modify your local installation, eg, \fBinstall\fR, \fBupdate\fR, \fBdedupe\fR, \fBuninstall\fR, as well as \fBpack\fR and \fBpublish\fR. Note: This is NOT honored by other network related commands, eg \fBdist-tags\fR, \fBowner\fR, etc. | | \fB--otp\fR | null | null or String | This is a one-time password from a two-factor authenticator. It's needed when publishing or changing package permissions with \fBnpm access\fR. If not set, and a registry response fails with a challenge for a one-time password, npm will prompt on the command line for one. | | \fB--workspace\fR, \fB-w\fR | | String (can be set multiple times) | Enable running a command in the context of the configured workspaces of the current project while filtering by running only the workspaces defined by this configuration option. Valid values for the \fBworkspace\fR config are either: * Workspace names * Path to a workspace directory * Path to a parent workspace directory (will result in selecting all workspaces within that folder) When set for the \fBnpm init\fR command, this may be set to the folder of a workspace which does not yet exist, to create the folder and set it up as a brand new workspace within the project. | | \fB--workspaces\fR | null | null or Boolean | Set to true to run the command in the context of \fBall\fR configured workspaces. Explicitly setting this to false will cause commands like \fBinstall\fR to ignore workspaces altogether. When not set explicitly: - Commands that operate on the \fBnode_modules\fR tree (install, update, etc.) will link workspaces into the \fBnode_modules\fR folder. - Commands that do other things (test, exec, publish, etc.) will operate on the root project, \fIunless\fR one or more workspaces are specified in the \fBworkspace\fR config. | | \fB--include-workspace-root\fR | false | Boolean | Include the workspace root when workspaces are enabled for a command. When false, specifying individual workspaces via the \fBworkspace\fR config, or all workspaces via the \fBworkspaces\fR flag, will cause npm to operate only on the specified workspaces, and not on the root project. | | \fB--provenance\fR | false | Boolean | When publishing from a supported cloud CI/CD system, the package will be publicly linked to where it was built and published from. | .SS "\fBnpm stage list\fR" .P List all staged package versions diff --git a/deps/npm/man/man1/npm-star.1 b/deps/npm/man/man1/npm-star.1 index ff2f6219714d1b..385d6e8a23249e 100644 --- a/deps/npm/man/man1/npm-star.1 +++ b/deps/npm/man/man1/npm-star.1 @@ -1,4 +1,4 @@ -.TH "NPM-STAR" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-STAR" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-star\fR - Mark your favorite packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-stars.1 b/deps/npm/man/man1/npm-stars.1 index 34f968d1331f75..f028349ba81f8c 100644 --- a/deps/npm/man/man1/npm-stars.1 +++ b/deps/npm/man/man1/npm-stars.1 @@ -1,4 +1,4 @@ -.TH "NPM-STARS" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-STARS" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-stars\fR - View packages marked as favorites .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-start.1 b/deps/npm/man/man1/npm-start.1 index 5af8739d5978e5..de0605d2fa8fa2 100644 --- a/deps/npm/man/man1/npm-start.1 +++ b/deps/npm/man/man1/npm-start.1 @@ -1,4 +1,4 @@ -.TH "NPM-START" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-START" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-start\fR - Start a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-stop.1 b/deps/npm/man/man1/npm-stop.1 index e869b3a796f120..57cadfb2fa80bd 100644 --- a/deps/npm/man/man1/npm-stop.1 +++ b/deps/npm/man/man1/npm-stop.1 @@ -1,4 +1,4 @@ -.TH "NPM-STOP" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-STOP" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-stop\fR - Stop a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-team.1 b/deps/npm/man/man1/npm-team.1 index 0bc981401b0da4..06d7b94acb7f24 100644 --- a/deps/npm/man/man1/npm-team.1 +++ b/deps/npm/man/man1/npm-team.1 @@ -1,4 +1,4 @@ -.TH "NPM-TEAM" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-TEAM" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-team\fR - Manage organization teams and team memberships .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-test.1 b/deps/npm/man/man1/npm-test.1 index a506a985023502..6c1108360e5789 100644 --- a/deps/npm/man/man1/npm-test.1 +++ b/deps/npm/man/man1/npm-test.1 @@ -1,4 +1,4 @@ -.TH "NPM-TEST" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-TEST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-test\fR - Test a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-token.1 b/deps/npm/man/man1/npm-token.1 index 646db6a114a570..41a69ed9d9c355 100644 --- a/deps/npm/man/man1/npm-token.1 +++ b/deps/npm/man/man1/npm-token.1 @@ -1,4 +1,4 @@ -.TH "NPM-TOKEN" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-TOKEN" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-token\fR - Manage your authentication tokens .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-trust.1 b/deps/npm/man/man1/npm-trust.1 index 61e2e39efa94fc..7b3c4d7990a8c9 100644 --- a/deps/npm/man/man1/npm-trust.1 +++ b/deps/npm/man/man1/npm-trust.1 @@ -1,4 +1,4 @@ -.TH "NPM-TRUST" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-TRUST" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-trust\fR - Manage trusted publishing relationships between packages and CI/CD providers .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-undeprecate.1 b/deps/npm/man/man1/npm-undeprecate.1 index 46d9f977f7c98a..8bb2936691c5c4 100644 --- a/deps/npm/man/man1/npm-undeprecate.1 +++ b/deps/npm/man/man1/npm-undeprecate.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNDEPRECATE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-UNDEPRECATE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-undeprecate\fR - Undeprecate a version of a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-uninstall.1 b/deps/npm/man/man1/npm-uninstall.1 index d58ca50d2ba9ce..10845d76cb050f 100644 --- a/deps/npm/man/man1/npm-uninstall.1 +++ b/deps/npm/man/man1/npm-uninstall.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNINSTALL" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-UNINSTALL" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-uninstall\fR - Remove a package .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-unpublish.1 b/deps/npm/man/man1/npm-unpublish.1 index bc1c5ed7b2c5ba..1386bb85ce279d 100644 --- a/deps/npm/man/man1/npm-unpublish.1 +++ b/deps/npm/man/man1/npm-unpublish.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNPUBLISH" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-UNPUBLISH" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-unpublish\fR - Remove a package from the registry .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-unstar.1 b/deps/npm/man/man1/npm-unstar.1 index bb96432baee821..4fabb116f4242f 100644 --- a/deps/npm/man/man1/npm-unstar.1 +++ b/deps/npm/man/man1/npm-unstar.1 @@ -1,4 +1,4 @@ -.TH "NPM-UNSTAR" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-UNSTAR" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-unstar\fR - Remove an item from your favorite packages .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-update.1 b/deps/npm/man/man1/npm-update.1 index 75aa70256aff10..e157c16d8a30ee 100644 --- a/deps/npm/man/man1/npm-update.1 +++ b/deps/npm/man/man1/npm-update.1 @@ -1,4 +1,4 @@ -.TH "NPM-UPDATE" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-UPDATE" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-update\fR - Update packages .SS "Synopsis" @@ -277,6 +277,42 @@ Type: Boolean If true, npm does not run scripts specified in package.json files. .P Note that commands explicitly intended to run a particular script, such as \fBnpm start\fR, \fBnpm stop\fR, \fBnpm restart\fR, \fBnpm test\fR, and \fBnpm run\fR will still run their intended script if \fBignore-scripts\fR is set, but they will \fInot\fR run any pre- or post-scripts. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man1/npm-version.1 b/deps/npm/man/man1/npm-version.1 index 7321a301400e4a..78612356235a44 100644 --- a/deps/npm/man/man1/npm-version.1 +++ b/deps/npm/man/man1/npm-version.1 @@ -1,4 +1,4 @@ -.TH "NPM-VERSION" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-VERSION" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-version\fR - Bump a package version .SS "Synopsis" @@ -226,6 +226,8 @@ Commit and tag. Run the \fBpostversion\fR script. Use it to clean up the file system or automatically push the commit and/or tag. .RE 0 +.P +For the \fBpreversion\fR, \fBversion\fR and \fBpostversion\fR scripts, npm also sets the \fBenvironment variables\fR \fI\(la/using-npm/scripts#environment\(ra\fR \fBnpm_old_version\fR and \fBnpm_new_version\fR. .P Take the following example: .P diff --git a/deps/npm/man/man1/npm-view.1 b/deps/npm/man/man1/npm-view.1 index 0ef6c63b3dc6d8..5ac8d354fbb51d 100644 --- a/deps/npm/man/man1/npm-view.1 +++ b/deps/npm/man/man1/npm-view.1 @@ -1,4 +1,4 @@ -.TH "NPM-VIEW" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-VIEW" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-view\fR - View registry info .SS "Synopsis" diff --git a/deps/npm/man/man1/npm-whoami.1 b/deps/npm/man/man1/npm-whoami.1 index e7f660e4628eaa..a0c1956514c26e 100644 --- a/deps/npm/man/man1/npm-whoami.1 +++ b/deps/npm/man/man1/npm-whoami.1 @@ -1,4 +1,4 @@ -.TH "NPM-WHOAMI" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM-WHOAMI" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-whoami\fR - Display npm username .SS "Synopsis" diff --git a/deps/npm/man/man1/npm.1 b/deps/npm/man/man1/npm.1 index e2f3b3e81bb9f5..68369b132c9168 100644 --- a/deps/npm/man/man1/npm.1 +++ b/deps/npm/man/man1/npm.1 @@ -1,4 +1,4 @@ -.TH "NPM" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPM" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm\fR - javascript package manager .SS "Synopsis" @@ -12,7 +12,7 @@ npm Note: This command is unaware of workspaces. .SS "Version" .P -11.15.0 +11.16.0 .SS "Description" .P npm is the package manager for the Node JavaScript platform. It puts modules in place so that node can find them, and manages dependency conflicts intelligently. diff --git a/deps/npm/man/man1/npx.1 b/deps/npm/man/man1/npx.1 index cfe09b033681af..23671ac8cfb611 100644 --- a/deps/npm/man/man1/npx.1 +++ b/deps/npm/man/man1/npx.1 @@ -1,4 +1,4 @@ -.TH "NPX" "1" "May 2026" "NPM@11.15.0" "" +.TH "NPX" "1" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpx\fR - Run a command from a local or remote npm package .SS "Synopsis" diff --git a/deps/npm/man/man5/folders.5 b/deps/npm/man/man5/folders.5 index b7eb083b084bb5..b0a8c9b4825ccb 100644 --- a/deps/npm/man/man5/folders.5 +++ b/deps/npm/man/man5/folders.5 @@ -1,4 +1,4 @@ -.TH "FOLDERS" "5" "May 2026" "NPM@11.15.0" "" +.TH "FOLDERS" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBFolders\fR - Folder structures used by npm .SS "Description" diff --git a/deps/npm/man/man5/install.5 b/deps/npm/man/man5/install.5 index c5e7b921cd50dd..e70fd2ed7b602e 100644 --- a/deps/npm/man/man5/install.5 +++ b/deps/npm/man/man5/install.5 @@ -1,4 +1,4 @@ -.TH "INSTALL" "5" "May 2026" "NPM@11.15.0" "" +.TH "INSTALL" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBInstall\fR - Download and install node and npm .SS "Description" diff --git a/deps/npm/man/man5/npm-global.5 b/deps/npm/man/man5/npm-global.5 index b7eb083b084bb5..b0a8c9b4825ccb 100644 --- a/deps/npm/man/man5/npm-global.5 +++ b/deps/npm/man/man5/npm-global.5 @@ -1,4 +1,4 @@ -.TH "FOLDERS" "5" "May 2026" "NPM@11.15.0" "" +.TH "FOLDERS" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBFolders\fR - Folder structures used by npm .SS "Description" diff --git a/deps/npm/man/man5/npm-json.5 b/deps/npm/man/man5/npm-json.5 index 0d4d8d0480c8cd..3d0c548f7042fa 100644 --- a/deps/npm/man/man5/npm-json.5 +++ b/deps/npm/man/man5/npm-json.5 @@ -1,4 +1,4 @@ -.TH "PACKAGE.JSON" "5" "May 2026" "NPM@11.15.0" "" +.TH "PACKAGE.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBpackage.json\fR - Specifics of npm's package.json handling .SS "Description" diff --git a/deps/npm/man/man5/npm-shrinkwrap-json.5 b/deps/npm/man/man5/npm-shrinkwrap-json.5 index 68d66807e0f5c6..104ac0703d7710 100644 --- a/deps/npm/man/man5/npm-shrinkwrap-json.5 +++ b/deps/npm/man/man5/npm-shrinkwrap-json.5 @@ -1,4 +1,4 @@ -.TH "NPM-SHRINKWRAP.JSON" "5" "May 2026" "NPM@11.15.0" "" +.TH "NPM-SHRINKWRAP.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBnpm-shrinkwrap.json\fR - A publishable lockfile .SS "Description" diff --git a/deps/npm/man/man5/npmrc.5 b/deps/npm/man/man5/npmrc.5 index 3cf3729867b79d..7c5273d6344a25 100644 --- a/deps/npm/man/man5/npmrc.5 +++ b/deps/npm/man/man5/npmrc.5 @@ -1,4 +1,4 @@ -.TH ".NPMRC" "5" "May 2026" "NPM@11.15.0" "" +.TH ".NPMRC" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fB.npmrc\fR - The npm config files .SS "Description" diff --git a/deps/npm/man/man5/package-json.5 b/deps/npm/man/man5/package-json.5 index 0d4d8d0480c8cd..3d0c548f7042fa 100644 --- a/deps/npm/man/man5/package-json.5 +++ b/deps/npm/man/man5/package-json.5 @@ -1,4 +1,4 @@ -.TH "PACKAGE.JSON" "5" "May 2026" "NPM@11.15.0" "" +.TH "PACKAGE.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBpackage.json\fR - Specifics of npm's package.json handling .SS "Description" diff --git a/deps/npm/man/man5/package-lock-json.5 b/deps/npm/man/man5/package-lock-json.5 index 7e0bd7fcd68729..40742966252fa4 100644 --- a/deps/npm/man/man5/package-lock-json.5 +++ b/deps/npm/man/man5/package-lock-json.5 @@ -1,4 +1,4 @@ -.TH "PACKAGE-LOCK.JSON" "5" "May 2026" "NPM@11.15.0" "" +.TH "PACKAGE-LOCK.JSON" "5" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBpackage-lock.json\fR - A manifestation of the manifest .SS "Description" diff --git a/deps/npm/man/man7/config.7 b/deps/npm/man/man7/config.7 index 3dcb6d67ecea85..d4b0b124d3f142 100644 --- a/deps/npm/man/man7/config.7 +++ b/deps/npm/man/man7/config.7 @@ -1,4 +1,4 @@ -.TH "CONFIG" "7" "May 2026" "NPM@11.15.0" "" +.TH "CONFIG" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBConfig\fR - About npm configuration .SS "Description" @@ -174,7 +174,7 @@ Warning: This should generally not be set via a command-line option. It is safer .IP \(bu 4 Default: 'public' for new packages, existing packages it will not change the current level .IP \(bu 4 -Type: null, "restricted", or "public" +Type: null, "restricted", "public", or "private" .RE 0 .P @@ -184,6 +184,8 @@ Unscoped packages cannot be set to \fBrestricted\fR. .P Note: This defaults to not changing the current access level for existing packages. Specifying a value of \fBrestricted\fR or \fBpublic\fR during publish will change the access for an existing package the same way that \fBnpm access set status\fR would. +.P +The value \fBprivate\fR is an alias for \fBrestricted\fR. .SS "\fBall\fR" .RS 0 .IP \(bu 4 @@ -252,6 +254,40 @@ Type: Boolean .P Prevents throwing an error when \fBnpm version\fR is used to set the new version to the same value as the current version. +.SS "\fBallow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: "" +.IP \(bu 4 +Type: String (can be set multiple times) +.RE 0 + +.P +Comma-separated list of packages whose install-time lifecycle scripts (\fBpreinstall\fR, \fBinstall\fR, \fBpostinstall\fR, and \fBprepare\fR for non-registry dependencies) are allowed to run. +.P +This setting is intended for one-off and global contexts: \fBnpm exec\fR, \fBnpx\fR, and \fBnpm install -g\fR, where no project \fBpackage.json\fR is involved. For team-wide policy in a project, use the \fBallowScripts\fR field in \fBpackage.json\fR (which also supports explicit denials), or configure it in \fB.npmrc\fR. Passing \fB--allow-scripts\fR on the command line during a project-scoped \fBnpm install\fR, \fBci\fR, \fBupdate\fR, or \fBrebuild\fR is an error. +.P +Each name is matched against a dependency's resolved identity, not against the package's self-reported name. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. +.SS "\fBallow-scripts-pending\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +List packages with install scripts that are not yet covered by the \fBallowScripts\fR policy, without modifying \fBpackage.json\fR. Only meaningful for \fBnpm approve-scripts\fR. +.SS "\fBallow-scripts-pin\fR" +.RS 0 +.IP \(bu 4 +Default: true +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +Write pinned (\fBpkg@version\fR) entries when approving install scripts. Set to \fBfalse\fR to write name-only entries that allow any version. Has no effect on \fBnpm deny-scripts\fR, which always writes name-only entries regardless of this setting. .SS "\fBaudit\fR" .RS 0 .IP \(bu 4 @@ -437,6 +473,16 @@ Type: null or String .P Override CPU architecture of native modules to install. Acceptable values are same as \fBcpu\fR field of package.json, which comes from \fBprocess.arch\fR. +.SS "\fBdangerously-allow-all-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, bypass the \fBallowScripts\fR policy entirely and run every dependency install script regardless of whether it was approved or denied. Intended as a migration escape hatch only; its use is strongly discouraged. \fB--ignore-scripts\fR still takes precedence over this setting. .SS "\fBdepth\fR" .RS 0 .IP \(bu 4 @@ -1759,6 +1805,18 @@ Type: Boolean If set to true, then the \fBnpm version\fR command will tag the version using \fB-s\fR to add a signature. .P Note that git requires you to have set up GPG keys in your git configs for this to work properly. +.SS "\fBstrict-allow-scripts\fR" +.RS 0 +.IP \(bu 4 +Default: false +.IP \(bu 4 +Type: Boolean +.RE 0 + +.P +If \fBtrue\fR, turn the install-script policy from a warning into a hard error: any dependency with install scripts not covered by \fBallowScripts\fR will fail the install instead of running with a notice. +.P +Dependencies explicitly denied with \fBfalse\fR in \fBallowScripts\fR are always silently skipped; this setting only affects unreviewed entries. \fB--ignore-scripts\fR and \fB--dangerously-allow-all-scripts\fR both override this setting. .SS "\fBstrict-peer-deps\fR" .RS 0 .IP \(bu 4 diff --git a/deps/npm/man/man7/dependency-selectors.7 b/deps/npm/man/man7/dependency-selectors.7 index 95849879c7256f..4b67c2a3be5892 100644 --- a/deps/npm/man/man7/dependency-selectors.7 +++ b/deps/npm/man/man7/dependency-selectors.7 @@ -1,4 +1,4 @@ -.TH "SELECTORS" "7" "May 2026" "NPM@11.15.0" "" +.TH "SELECTORS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBSelectors\fR - Dependency Selector Syntax & Querying .SS "Description" diff --git a/deps/npm/man/man7/developers.7 b/deps/npm/man/man7/developers.7 index 2f7d9113a180bc..5b41ed26374205 100644 --- a/deps/npm/man/man7/developers.7 +++ b/deps/npm/man/man7/developers.7 @@ -1,4 +1,4 @@ -.TH "DEVELOPERS" "7" "May 2026" "NPM@11.15.0" "" +.TH "DEVELOPERS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBDevelopers\fR - Developer guide .SS "Description" diff --git a/deps/npm/man/man7/logging.7 b/deps/npm/man/man7/logging.7 index f5c65d13c25680..474becd6ff6431 100644 --- a/deps/npm/man/man7/logging.7 +++ b/deps/npm/man/man7/logging.7 @@ -1,4 +1,4 @@ -.TH "LOGGING" "7" "May 2026" "NPM@11.15.0" "" +.TH "LOGGING" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBLogging\fR - Why, What & How we Log .SS "Description" diff --git a/deps/npm/man/man7/orgs.7 b/deps/npm/man/man7/orgs.7 index d9c76b7bc683cd..e0666bdf0f3389 100644 --- a/deps/npm/man/man7/orgs.7 +++ b/deps/npm/man/man7/orgs.7 @@ -1,4 +1,4 @@ -.TH "ORGANIZATIONS" "7" "May 2026" "NPM@11.15.0" "" +.TH "ORGANIZATIONS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBOrganizations\fR - Working with teams & organizations .SS "Description" diff --git a/deps/npm/man/man7/package-spec.7 b/deps/npm/man/man7/package-spec.7 index 9e4eb36e857b0f..76d07709632b0c 100644 --- a/deps/npm/man/man7/package-spec.7 +++ b/deps/npm/man/man7/package-spec.7 @@ -1,4 +1,4 @@ -.TH "SPEC" "7" "May 2026" "NPM@11.15.0" "" +.TH "SPEC" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBspec\fR - Package name specifier .SS "Description" diff --git a/deps/npm/man/man7/registry.7 b/deps/npm/man/man7/registry.7 index 41ce62cdc6b792..cbfca0c6f42d3c 100644 --- a/deps/npm/man/man7/registry.7 +++ b/deps/npm/man/man7/registry.7 @@ -1,4 +1,4 @@ -.TH "REGISTRY" "7" "May 2026" "NPM@11.15.0" "" +.TH "REGISTRY" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBRegistry\fR - The JavaScript Package Registry .SS "Description" diff --git a/deps/npm/man/man7/removal.7 b/deps/npm/man/man7/removal.7 index 144c6ca788afc6..fa44b56539a008 100644 --- a/deps/npm/man/man7/removal.7 +++ b/deps/npm/man/man7/removal.7 @@ -1,4 +1,4 @@ -.TH "REMOVAL" "7" "May 2026" "NPM@11.15.0" "" +.TH "REMOVAL" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBRemoval\fR - Cleaning the slate .SS "Synopsis" diff --git a/deps/npm/man/man7/scope.7 b/deps/npm/man/man7/scope.7 index 7491b29be87116..7857ede645fa92 100644 --- a/deps/npm/man/man7/scope.7 +++ b/deps/npm/man/man7/scope.7 @@ -1,4 +1,4 @@ -.TH "SCOPE" "7" "May 2026" "NPM@11.15.0" "" +.TH "SCOPE" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBScope\fR - Scoped packages .SS "Description" diff --git a/deps/npm/man/man7/scripts.7 b/deps/npm/man/man7/scripts.7 index 4c24ce5e54c772..5cfab1d64cd2e9 100644 --- a/deps/npm/man/man7/scripts.7 +++ b/deps/npm/man/man7/scripts.7 @@ -1,4 +1,4 @@ -.TH "SCRIPTS" "7" "May 2026" "NPM@11.15.0" "" +.TH "SCRIPTS" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBScripts\fR - How npm handles the "scripts" field .SS "Description" @@ -382,6 +382,16 @@ For example, if you had \fB{"name":"foo", "version":"1.2.5"}\fR in your package. \fBNote:\fR In npm 7 and later, most package.json fields are no longer provided as environment variables. Scripts that need access to other package.json fields should read the package.json file directly. The \fBnpm_package_json\fR environment variable provides the path to the file for this purpose. .P See \fB\[rs]fBpackage.json\[rs]fR\fR \fI\(la/configuring-npm/package-json\(ra\fR for more on package configs. +.SS "versioning variables" +.P +For versioning scripts (\fBpreversion\fR, \fBversion\fR, \fBpostversion\fR), npm sets these environment variables: +.RS 0 +.IP \(bu 4 +\fBnpm_old_version\fR - The version before being bumped +.IP \(bu 4 +\fBnpm_new_version\fR \[en] The version after being bumped +.RE 0 + .SS "current lifecycle event" .P Lastly, the \fBnpm_lifecycle_event\fR environment variable is set to whichever stage of the cycle is being executed. So, you could have a single script used for different parts of the process which switches based on what's currently happening. diff --git a/deps/npm/man/man7/workspaces.7 b/deps/npm/man/man7/workspaces.7 index 9cd6628201a000..be7a22047d9971 100644 --- a/deps/npm/man/man7/workspaces.7 +++ b/deps/npm/man/man7/workspaces.7 @@ -1,4 +1,4 @@ -.TH "WORKSPACES" "7" "May 2026" "NPM@11.15.0" "" +.TH "WORKSPACES" "7" "May 2026" "NPM@11.16.0" "" .SH "NAME" \fBWorkspaces\fR - Working with workspaces .SS "Description" diff --git a/deps/npm/node_modules/@npmcli/agent/lib/agents.js b/deps/npm/node_modules/@npmcli/agent/lib/agents.js index c541b93001517e..e9624dfeb90090 100644 --- a/deps/npm/node_modules/@npmcli/agent/lib/agents.js +++ b/deps/npm/node_modules/@npmcli/agent/lib/agents.js @@ -203,4 +203,56 @@ module.exports = class Agent extends AgentBase { return super.addRequest(request, options) } + + // When connect() rejects, agent-base removes only its placeholder socket, so Node never drains this.requests[name] and requests queued past maxSockets hang forever. + // On a failure we dispatch the next queued request ourselves. + // See npm/cli#9386 and TooTallNate/proxy-agents#427. + createSocket (req, options, cb) { + super.createSocket(req, options, (err, socket) => { + if (err) { + this.#drainPendingRequests(req, options) + } + cb(err, socket) + }) + } + + // Dispatch the next request queued behind maxSockets, reusing the slot the failed connection freed. + #drainPendingRequests (failedReq, options) { + const name = this.getName(options) + const queue = this.requests[name] + if (!queue || queue.length === 0) { + return + } + + // Node's removeSocket() picks a queued request without shifting it off, so drop the failed one to avoid dispatching it twice. + const failedIndex = queue.indexOf(failedReq) + if (failedIndex !== -1) { + queue.splice(failedIndex, 1) + } + if (queue.length === 0) { + delete this.requests[name] + return + } + + // Safety belt: only dispatch if a socket slot is genuinely free. + const socketCount = this.sockets[name] ? this.sockets[name].length : 0 + if (socketCount >= this.maxSockets || this.totalSocketCount >= this.maxTotalSockets) { + return + } + + const nextReq = queue.shift() + if (queue.length === 0) { + delete this.requests[name] + } + + // All queued requests share this origin, so the failed request's options suit the next one. + // createSocket() recurses here if this connection also fails, draining the whole queue. + this.createSocket(nextReq, options, (err, socket) => { + if (err) { + nextReq.onSocket(null, err) + } else { + nextReq.onSocket(socket) + } + }) + } } diff --git a/deps/npm/node_modules/@npmcli/agent/lib/options.js b/deps/npm/node_modules/@npmcli/agent/lib/options.js index 0bf53f725f0846..a6ae490a89c3b3 100644 --- a/deps/npm/node_modules/@npmcli/agent/lib/options.js +++ b/deps/npm/node_modules/@npmcli/agent/lib/options.js @@ -37,6 +37,10 @@ const normalizeOptions = (opts) => { // remove timeout since we already used it to set our own idle timeout delete normalized.timeout + // since opts is often passed when initiating requests, it may contain + // headers, which should not be saved in an agent + delete normalized.headers + return normalized } diff --git a/deps/npm/node_modules/@npmcli/agent/package.json b/deps/npm/node_modules/@npmcli/agent/package.json index 67670a0c1c484e..8c0d358b02a717 100644 --- a/deps/npm/node_modules/@npmcli/agent/package.json +++ b/deps/npm/node_modules/@npmcli/agent/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/agent", - "version": "4.0.0", + "version": "4.0.2", "description": "the http/https agent used by the npm cli", "main": "lib/index.js", "scripts": { @@ -29,8 +29,10 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "4.25.0", - "publish": "true" + "version": "4.30.0", + "publish": "true", + "updateNpm": false, + "latestCiVersion": 24 }, "dependencies": { "agent-base": "^7.1.0", @@ -40,11 +42,11 @@ "socks-proxy-agent": "^8.0.3" }, "devDependencies": { - "@npmcli/eslint-config": "^5.0.0", - "@npmcli/template-oss": "4.25.0", - "minipass-fetch": "^4.0.1", + "@npmcli/eslint-config": "^6.0.0", + "@npmcli/template-oss": "4.30.0", + "ip-address": "^10.1.0", + "minipass-fetch": "^5.0.0", "nock": "^14.0.3", - "socksv5": "^0.0.6", "tap": "^16.3.0" }, "repository": { diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js index eda38947462609..11581cb4fd9400 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/index.js @@ -100,8 +100,10 @@ class Arborist extends Base { nodeVersion: process.version, ...options, Arborist: this.constructor, + allowScripts: options.allowScripts ?? null, binLinks: 'binLinks' in options ? !!options.binLinks : true, cache: options.cache || `${homedir()}/.npm/_cacache`, + dangerouslyAllowAllScripts: !!options.dangerouslyAllowAllScripts, dryRun: !!options.dryRun, formatPackageLock: 'formatPackageLock' in options ? !!options.formatPackageLock : true, force: !!options.force, diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js index a285da0a45c9a2..14f432ca977459 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/isolated-reifier.js @@ -335,7 +335,8 @@ module.exports = cls => class IsolatedReifier extends cls { root.inventory.set(workspace.location, workspace) root.workspaces.set(wsName, workspace.path) - // Create workspace Link. For root declared deps, link at root node_modules/. For undeclared deps, link at the workspace's own node_modules/ (self-link). + // Declared workspaces are symlinked at root node_modules/. + // Undeclared workspaces get a tree-only Link kept for diff/filter participation but not materialized on disk. const isDeclared = this.#rootDeclaredDeps.has(wsName) const wsLink = new IsolatedLink({ location: isDeclared ? join('node_modules', wsName) : join(c.localLocation, 'node_modules', wsName), @@ -348,7 +349,7 @@ module.exports = cls => class IsolatedReifier extends cls { target: workspace, }) if (!isDeclared) { - workspace.children.set(wsName, wsLink) + wsLink.isUndeclaredWorkspaceLink = true } root.children.set(wsName, wsLink) root.inventory.set(wsLink.location, wsLink) diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js index d4cce1ac02776c..e70a2186c29713 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/rebuild.js @@ -12,6 +12,7 @@ const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp' const { promiseRetry } = require('@gar/promise-retry') const { log, time } = require('proc-log') const { resolve } = require('node:path') +const { isScriptAllowed } = require('../script-allowed.js') const boolEnv = b => b ? '1' : '' const sortNodes = (a, b) => (a.depth - b.depth) || localeCompare(a.path, b.path) @@ -225,6 +226,18 @@ module.exports = cls => class Builder extends cls { return } + // Phase 1 allowScripts gate: a `false` verdict from the policy matcher + // means the user explicitly denied install scripts for this node, so skip + // it. `true` and `null` (unreviewed) both fall through to the existing + // detection logic — unreviewed nodes still run their scripts in Phase 1 + // and are surfaced via the post-reify advisory warning. The global + // --ignore-scripts kill switch in #build() still takes precedence, and + // --dangerously-allow-all-scripts bypasses this gate entirely. + if (!this.options.dangerouslyAllowAllScripts && + isScriptAllowed(node, this.options.allowScripts) === false) { + return + } + if (this.#oldMeta === null) { const { root: { meta } } = node this.#oldMeta = meta && meta.loadedFromDisk && diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js index c9f08de776e462..38fb4e37589255 100644 --- a/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js +++ b/deps/npm/node_modules/@npmcli/arborist/lib/arborist/reify.js @@ -239,7 +239,7 @@ module.exports = cls => class Reifier extends cls { this.actualTree = this.idealTree this.idealTree = null - if (!this.options.global) { + if (!this.options.global && !this.options.dryRun) { await this.actualTree.meta.save() const ignoreScripts = !!this.options.ignoreScripts // if we aren't doing a dry run or ignoring scripts and we actually made changes to the dep @@ -760,6 +760,12 @@ module.exports = cls => class Reifier extends cls { } // node.isLink + + // Tree-only Link: present in the tree for diff/filter participation, never materialized on disk. + if (node.isUndeclaredWorkspaceLink) { + return + } + await rm(node.path, { recursive: true, force: true }) // symlink @@ -1381,6 +1387,10 @@ module.exports = cls => class Reifier extends cls { if (!child.isLink) { continue } + // Tree-only Links never exist on disk; skipping them lets the sweep remove any stale self-link left by an older npm version. + if (child.isUndeclaredWorkspaceLink) { + continue + } const nmIdx = loc.lastIndexOf(NM_PREFIX) if (nmIdx === -1 || loc.includes(STORE_MARKER)) { continue diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js b/deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js new file mode 100644 index 00000000000000..47a7f982c04ef1 --- /dev/null +++ b/deps/npm/node_modules/@npmcli/arborist/lib/install-scripts.js @@ -0,0 +1,88 @@ +const { isNodeGypPackage } = require('@npmcli/node-gyp') + +// Returns the install-relevant lifecycle scripts that would run for a +// given arborist Node, or `{}` if there are none. +// +// Includes: +// - explicit preinstall/install/postinstall +// - prepare, but only for non-registry sources (git, file, link, remote) +// - synthetic `node-gyp rebuild`, when `binding.gyp` is present on disk +// and the package does not opt out via `gypfile: false` or define its +// own install / preinstall script + +// Lifecycle-script enumeration boundary. +// +// IMPORTANT: this helper decides whether `prepare` should be included +// in the enumerated install scripts (true for non-registry sources only). +// It is NOT a policy-matching predicate. The policy matcher in +// script-allowed.js uses `isRegistryNode`, which is strictly tied to +// versionFromTgz(node.resolved). The two helpers exist separately on +// purpose: +// +// - `hasNonRegistryShape` (here): "should we consider running prepare +// on this node?" — a yes/no for what to enumerate. +// - `isRegistryNode` (script-allowed.js): "do we trust this node's +// identity enough to apply a policy entry?" — a security check. +// +// The looser fallback here (treating unknown-resolved nodes as registry, +// thus skipping `prepare`) is the safer default for enumeration: we'd +// rather omit a script we should have run than synthesise one for a +// non-registry source we couldn't confirm. The policy matcher's stricter +// behaviour is correct for its boundary; the two helpers must not be +// merged. +const hasNonRegistryShape = (node) => { + if (typeof node.isRegistryDependency === 'boolean') { + return !node.isRegistryDependency + } + if (!node.resolved) { + return false + } + return !/^https?:\/\/[^/]+\/.+\/-\/[^/]+-\d/.test(node.resolved) +} + +const getInstallScripts = async (node) => { + /* istanbul ignore next: arborist Nodes always carry a `package` object; + defensive fallbacks for non-arborist callers. */ + const pkg = node.package || {} + /* istanbul ignore next */ + const scripts = pkg.scripts || {} + const collected = {} + + if (scripts.preinstall) { + collected.preinstall = scripts.preinstall + } + if (scripts.install) { + collected.install = scripts.install + } + if (scripts.postinstall) { + collected.postinstall = scripts.postinstall + } + if (scripts.prepare && hasNonRegistryShape(node)) { + collected.prepare = scripts.prepare + } + + const hasExplicitGypGate = !!(collected.preinstall || collected.install) + if ( + !hasExplicitGypGate && + pkg.gypfile !== false && + await isNodeGypPackage(node.path).catch(() => false) + ) { + collected.install = 'node-gyp rebuild' + } + + // Lockfile-only nodes (e.g. `npm ci` before reify) carry + // `hasInstallScript: true` but no enumerated scripts: the lockfile + // records the presence flag but never the script bodies. Without this + // fallback the strict-allow-scripts preflight would miss them entirely + // and let postinstall run. We can't recover the real script body + // without fetching the manifest, so emit a sentinel describing that + // install scripts are present. + if (Object.keys(collected).length === 0 && node.hasInstallScript === true) { + collected.install = '(install scripts present)' + } + + return collected +} + +module.exports = getInstallScripts +module.exports.getInstallScripts = getInstallScripts diff --git a/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js b/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js new file mode 100644 index 00000000000000..91734fa38c1034 --- /dev/null +++ b/deps/npm/node_modules/@npmcli/arborist/lib/script-allowed.js @@ -0,0 +1,340 @@ +const npa = require('npm-package-arg') +const semver = require('semver') +const versionFromTgz = require('./version-from-tgz.js') + +// Identity matcher for the allowScripts policy. +// +// Returns: +// - true: at least one allow entry matches and no deny entry matches +// - false: at least one deny entry matches (deny wins on conflict) +// - null: no entry matches (unreviewed) +// +// `policy` is a flat object of `spec-key -> boolean`, where spec-key is +// anything `npm-package-arg` can parse. `node` is an arborist Node. +// +// Identity rules (see RFC npm/rfcs#868): +// - registry deps match by the name+version parsed from the lockfile's +// resolved URL, NOT by `node.packageName` / `node.version`. Those two +// getters return `node.package.name` / `node.package.version`, which +// come from the tarball's own package.json and are therefore +// attacker-controlled. A package can publish a tarball claiming any +// name; the only trusted name is the one baked into the registry URL. +// - tarball / file / link / remote: exact match on node.resolved +// - git: match on hosted.ssh() plus a short-SHA prefix of the +// resolved committish + +const isScriptAllowed = (node, policy) => { + // Bundled dependencies cannot be allowlisted in Phase 1. The RFC defers + // allowlisting them to a follow-up RFC because matching by name@version + // from the bundled tarball would reintroduce manifest confusion (a + // bundled tarball can claim any name and version). Returning null here + // marks bundled deps as unreviewed regardless of any policy entries, so + // their install scripts surface in the Phase 1 advisory warning and + // (eventually) get blocked at the install-time gate. + if (node.inBundle) { + return null + } + + if (!policy || typeof policy !== 'object') { + return null + } + + let anyAllow = false + let anyDeny = false + + for (const [key, value] of Object.entries(policy)) { + if (!matches(node, key)) { + continue + } + if (value === false) { + anyDeny = true + continue + } + /* istanbul ignore else: policy values are strictly true/false; + defensive guard against unexpected coercions. */ + if (value === true) { + anyAllow = true + } + } + + if (anyDeny) { + return false + } + if (anyAllow) { + return true + } + return null +} + +const matches = (node, key) => { + let parsed + try { + parsed = npa(key) + } catch { + return false + } + + switch (parsed.type) { + case 'tag': + case 'range': + case 'version': + return matchRegistry(node, parsed) + case 'git': + return matchGit(node, parsed) + case 'file': + case 'directory': + return matchFileOrDir(node, parsed) + case 'remote': + return matchRemote(node, parsed) + case 'alias': + // Disallowed: aliases as policy keys do not match anything. + // The user has to address the real package name. + return false + /* istanbul ignore next: switch above covers every npa type we expect; + defensive fallback for future npa types. */ + default: + return false + } +} + +const matchRegistry = (node, parsed) => { + // If this node is not a registry dep, refuse the match. A registry-style + // key (`pkg`, `pkg@1`, `pkg@1 || 2`) must not match a tarball or git node + // even if their names happen to coincide. + if (!isRegistryNode(node)) { + return false + } + + // Derive the trusted name+version from the lockfile's resolved URL. + // Never use `node.packageName` / `node.version` here: those read from + // the tarball's own package.json and can be forged by a malicious + // publisher to bypass an allowScripts entry. + const trusted = getTrustedRegistryIdentity(node) + if (!trusted || trusted.name !== parsed.name) { + return false + } + + // `tag` covers `pkg@latest`. Rejected up front by validatePolicy in + // resolve-allow-scripts.js because tags look like a pin but can't be + // verified at install time. Defense-in-depth: if one slips through + // (e.g. arborist invoked directly without the resolver), don't match. + if (parsed.type === 'tag') { + /* istanbul ignore next: validatePolicy filters this; defensive */ + return false + } + + // `range` includes `pkg@^1`, `pkg@1 || 2`, `pkg@*`, `pkg@>=0`, and bare + // names like `pkg` (npa parses these as range with fetchSpec='*'). The + // RFC permits bare names (name-only allow) and exact versions joined by + // `||`; ranges like ^/~/>=/< are rejected because they would silently + // allow versions the user has never reviewed. + if (parsed.type === 'range') { + // Bare name or `pkg@*`: treat as name-only allow. + if (parsed.fetchSpec === '*' || parsed.rawSpec === '' || parsed.rawSpec === '*') { + return true + } + if (!trusted.version || !isExactVersionDisjunction(parsed.fetchSpec)) { + return false + } + return semver.satisfies(trusted.version, parsed.fetchSpec, { loose: true }) + } + + // `version` is an exact pin like `pkg@1.2.3`. + /* istanbul ignore else: parsed.type at this point is always 'version'; + the istanbul-ignored fallback below handles the impossible case. */ + if (parsed.type === 'version') { + return trusted.version === parsed.fetchSpec + } + + /* istanbul ignore next: parsed.type is constrained to tag/range/version + by the caller; this final fallback is defensive. */ + return false +} + +// Derive a registry node's trusted name+version. +// +// Preferred source: the lockfile's resolved URL parsed via +// versionFromTgz. arborist records the URL when it first adds the dep, +// before any tarball is unpacked, so the URL cannot be forged by the +// package's own package.json. +// +// Fallback for lockfiles produced with omit-lockfile-registry-resolved +// (where the URL is absent): take the dep name from an incoming +// dependency edge. The edge's spec was written by the consumer (or by an +// upstream package.json), not by the installed tarball. For aliases like +// `"trusted": "npm:naughty@1.0.0"`, the underlying registered package +// name is parsed out of the alias `subSpec`. The install location +// (`node_modules/trusted`) is deliberately not consulted because for +// aliases it carries only the alias name, which would let a malicious +// publisher bypass an allowScripts entry written for the real package. +// +// Version is left null in the fallback case because the only remaining +// source for it (`node.version`) reads from the tarball. +// +// Returns `{ name, version }` or `null` if no trusted identity exists. +const getTrustedRegistryIdentity = (node) => { + if (node.resolved && typeof node.resolved === 'string') { + const parsed = versionFromTgz('', node.resolved) + /* istanbul ignore else: versionFromTgz returns either a complete + { name, version } or null; partial objects are not produced. */ + if (parsed && parsed.name && parsed.version) { + return parsed + } + } + const name = nameFromEdges(node) + if (name) { + return { name, version: null } + } + return null +} + +const nameFromEdges = (node) => { + if (!node.edgesIn || typeof node.edgesIn[Symbol.iterator] !== 'function') { + return null + } + for (const edge of node.edgesIn) { + let parsed + try { + parsed = npa.resolve(edge.name, edge.spec) + } catch { + continue + } + // Aliases: trust the underlying registered package, not the alias. + if (parsed.type === 'alias' && parsed.subSpec && parsed.subSpec.registry) { + return parsed.subSpec.name + } + // Non-aliased registry edge: the edge name is the package name as + // written by the consumer / upstream, which is trusted (it is not + // read from the installed tarball). + if (parsed.registry) { + return parsed.name + } + } + return null +} + +// True if `rangeSpec` is one or more exact versions joined by `||`. Anything +// containing comparator operators (^, ~, >=, <, *) returns false. +const isExactVersionDisjunction = (rangeSpec) => { + /* istanbul ignore next: caller always passes parsed.fetchSpec, which + npa guarantees to be a non-empty string for range specs. */ + if (typeof rangeSpec !== 'string' || rangeSpec.trim() === '') { + return false + } + const parts = rangeSpec.split('||').map(p => p.trim()) + /* istanbul ignore next: String.prototype.split always returns at least + one element; defensive guard only. */ + if (parts.length === 0) { + return false + } + return parts.every(p => p !== '' && semver.valid(p) !== null) +} + +const matchGit = (node, parsed) => { + if (!node.resolved || !node.resolved.startsWith('git')) { + return false + } + + let nodeParsed + try { + nodeParsed = npa(node.resolved) + } catch { + /* istanbul ignore next: npa parsing a git URL we already validated + starts with `git` should not throw; defensive guard only. */ + return false + } + + // Compare the host/repo. Both sides should resolve to the same canonical + // ssh URL. + const noCommittish = { noCommittish: true } + const keyHost = parsed.hosted?.ssh(noCommittish) + const nodeHost = nodeParsed.hosted?.ssh(noCommittish) + if (keyHost && nodeHost) { + if (keyHost !== nodeHost) { + return false + } + } else if (parsed.fetchSpec && nodeParsed.fetchSpec) { + // Non-hosted git URLs: fall back to fetch spec. + if (parsed.fetchSpec !== nodeParsed.fetchSpec) { + return false + } + } else { + return false + } + + // If the policy key has no committish, name-only match. + const keyCommittish = parsed.gitCommittish || parsed.hosted?.committish + if (!keyCommittish) { + return true + } + + // Match the resolved full SHA against the key's committish. Users + // typically write short SHAs in the policy; the lockfile stores 40-char + // SHAs. Direction matters: the lockfile's full SHA must START WITH the + // key's short SHA, never the reverse. A longer key matching a shorter + // resolved committish would let a malformed lockfile or a divergent + // resolver allow scripts the user never approved. + const nodeCommittish = nodeParsed.gitCommittish || nodeParsed.hosted?.committish || '' + if (!nodeCommittish) { + return false + } + return nodeCommittish.startsWith(keyCommittish) +} + +const matchFileOrDir = (node, parsed) => { + if (!node.resolved) { + return false + } + return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec +} + +const matchRemote = (node, parsed) => { + if (!node.resolved) { + return false + } + return node.resolved === parsed.fetchSpec || node.resolved === parsed.saveSpec +} + +const isRegistryNode = (node) => { + // Prefer arborist's edge-based check when available (real Node objects). + // It inspects the incoming edges' specs and only returns true if every + // edge resolves to a registry spec, which is much harder to spoof than + // the URL. + if (typeof node.isRegistryDependency === 'boolean') { + return node.isRegistryDependency + } + // Fall back to URL parsing for nodes without the arborist getter + // (e.g. test fixtures, lockfiles with omit-lockfile-registry-resolved). + // Treat the node as a registry dep when: + // - resolved is missing entirely (omitLockfileRegistryResolved), + // - resolved is an https/http URL pointing at a registry tarball, or + // - resolved is undefined and the node has a version (defensive). + if (!node.resolved) { + return !!node.version + } + // Registry tarballs live at `//-/-.tgz`. + // Require a path segment before `/-/` so an attacker can't lift a + // registry-style allow entry to a hostile URL like + // `https://evil.com/-/trusted-1.0.0.tgz`. + return /^https?:\/\/[^/]+\/.+\/-\/[^/]+-\d/.test(node.resolved) +} + +// Trusted display identity for human-facing output (`npm install` +// advisory, `npm approve-scripts --allow-scripts-pending`). Same idea as +// getTrustedRegistryIdentity, but for DISPLAY only — version falls back +// to node.version when the URL doesn't carry one. Must never be used +// for policy matching. +const trustedDisplay = (node) => { + const trusted = getTrustedRegistryIdentity(node) + /* istanbul ignore next: defensive fallbacks for nodes without name/version */ + return { + name: (trusted && trusted.name) || node.name || null, + version: (trusted && trusted.version) || node.version || null, + } +} + +module.exports = isScriptAllowed +module.exports.isScriptAllowed = isScriptAllowed +module.exports.isExactVersionDisjunction = isExactVersionDisjunction +module.exports.getTrustedRegistryIdentity = getTrustedRegistryIdentity +module.exports.trustedDisplay = trustedDisplay diff --git a/deps/npm/node_modules/@npmcli/arborist/package.json b/deps/npm/node_modules/@npmcli/arborist/package.json index c8c464e8d3a7e4..712151e63a1c65 100644 --- a/deps/npm/node_modules/@npmcli/arborist/package.json +++ b/deps/npm/node_modules/@npmcli/arborist/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/arborist", - "version": "9.6.0", + "version": "9.7.0", "description": "Manage node_modules trees", "dependencies": { "@gar/promise-retry": "^1.0.0", diff --git a/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js b/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js index d54e1845d60777..2cb03709d73b5e 100644 --- a/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js +++ b/deps/npm/node_modules/@npmcli/config/lib/definitions/definitions.js @@ -1,4 +1,5 @@ const Definition = require('./definition.js') +const parseAllowScriptsList = require('../parse-allow-scripts-list.js') const ciInfo = require('ci-info') const querystring = require('node:querystring') @@ -153,7 +154,7 @@ const definitions = { defaultDescription: ` 'public' for new packages, existing packages it will not change the current level `, - type: [null, 'restricted', 'public'], + type: [null, 'restricted', 'public', 'private'], description: ` If you do not want your scoped package to be publicly viewable (and installable) set \`--access=restricted\`. @@ -164,8 +165,13 @@ const definitions = { packages. Specifying a value of \`restricted\` or \`public\` during publish will change the access for an existing package the same way that \`npm access set status\` would. + + The value \`private\` is an alias for \`restricted\`. `, - flatten, + flatten (key, obj, flatOptions) { + const value = obj[key] + flatOptions.access = value === 'private' ? 'restricted' : value + }, }), all: new Definition('all', { default: false, @@ -247,6 +253,31 @@ const definitions = { `, flatten, }), + 'allow-scripts': new Definition('allow-scripts', { + default: '', + type: [String, Array], + hint: '', + description: ` + Comma-separated list of packages whose install-time lifecycle scripts + (\`preinstall\`, \`install\`, \`postinstall\`, and \`prepare\` for + non-registry dependencies) are allowed to run. + + This setting is intended for one-off and global contexts: \`npm exec\`, + \`npx\`, and \`npm install -g\`, where no project \`package.json\` is + involved. For team-wide policy in a project, use the \`allowScripts\` + field in \`package.json\` (which also supports explicit denials), or + configure it in \`.npmrc\`. Passing \`--allow-scripts\` on the command + line during a project-scoped \`npm install\`, \`ci\`, \`update\`, or + \`rebuild\` is an error. + + Each name is matched against a dependency's resolved identity, not + against the package's self-reported name. \`--ignore-scripts\` and + \`--dangerously-allow-all-scripts\` both override this setting. + `, + flatten (key, obj, flatOptions) { + flatOptions.allowScripts = parseAllowScriptsList(obj[key]) + }, + }), also: new Definition('also', { default: null, type: [null, 'dev', 'development'], @@ -535,6 +566,18 @@ const definitions = { `, flatten, }), + 'dangerously-allow-all-scripts': new Definition('dangerously-allow-all-scripts', { + default: false, + type: Boolean, + description: ` + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + dependency install script regardless of whether it was approved or + denied. Intended as a migration escape hatch only; its use is strongly + discouraged. \`--ignore-scripts\` still takes precedence over this + setting. + `, + flatten, + }), depth: new Definition('depth', { default: null, defaultDescription: ` @@ -1667,6 +1710,27 @@ const definitions = { `, flatten, }), + 'allow-scripts-pending': new Definition('allow-scripts-pending', { + default: false, + type: Boolean, + description: ` + List packages with install scripts that are not yet covered by the + \`allowScripts\` policy, without modifying \`package.json\`. Only + meaningful for \`npm approve-scripts\`. + `, + flatten, + }), + 'allow-scripts-pin': new Definition('allow-scripts-pin', { + default: true, + type: Boolean, + description: ` + Write pinned (\`pkg@version\`) entries when approving install scripts. + Set to \`false\` to write name-only entries that allow any version. + Has no effect on \`npm deny-scripts\`, which always writes name-only + entries regardless of this setting. + `, + flatten, + }), 'prefer-dedupe': new Definition('prefer-dedupe', { default: false, type: Boolean, @@ -2238,6 +2302,22 @@ const definitions = { `, flatten, }), + 'strict-allow-scripts': new Definition('strict-allow-scripts', { + default: false, + type: Boolean, + description: ` + If \`true\`, turn the install-script policy from a warning into a hard + error: any dependency with install scripts not covered by + \`allowScripts\` will fail the install instead of running with a + notice. + + Dependencies explicitly denied with \`false\` in \`allowScripts\` are + always silently skipped; this setting only affects unreviewed entries. + \`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both + override this setting. + `, + flatten, + }), 'strict-ssl': new Definition('strict-ssl', { default: true, type: Boolean, diff --git a/deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js b/deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js new file mode 100644 index 00000000000000..0f13d4a75b6349 --- /dev/null +++ b/deps/npm/node_modules/@npmcli/config/lib/parse-allow-scripts-list.js @@ -0,0 +1,23 @@ +// Parse an `allow-scripts` raw config value (string or array of strings) +// into a flat array of trimmed package-spec entries. Shared between the +// CLI/env layer (via the `allow-scripts` definition's `flatten`) and the +// package.json / .npmrc layer (in lib/utils/resolve-allow-scripts.js) so +// both paths agree on quoting, whitespace, and duplicate handling. +const parseAllowScriptsList = (raw) => { + const parts = [] + const entries = Array.isArray(raw) ? raw : (typeof raw === 'string' ? [raw] : []) + for (const entry of entries) { + if (typeof entry !== 'string') { + continue + } + for (const part of entry.split(',')) { + const trimmed = part.trim() + if (trimmed) { + parts.push(trimmed) + } + } + } + return parts +} + +module.exports = parseAllowScriptsList diff --git a/deps/npm/node_modules/@npmcli/config/package.json b/deps/npm/node_modules/@npmcli/config/package.json index 09627833e07971..295855d76df3e0 100644 --- a/deps/npm/node_modules/@npmcli/config/package.json +++ b/deps/npm/node_modules/@npmcli/config/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/config", - "version": "10.9.1", + "version": "10.10.0", "files": [ "bin/", "lib/" diff --git a/deps/npm/node_modules/@sigstore/core/dist/dsse.js b/deps/npm/node_modules/@sigstore/core/dist/dsse.js index ca7b63630e2ba9..9dcc2649198c19 100644 --- a/deps/npm/node_modules/@sigstore/core/dist/dsse.js +++ b/deps/npm/node_modules/@sigstore/core/dist/dsse.js @@ -19,12 +19,11 @@ limitations under the License. const PAE_PREFIX = 'DSSEv1'; // DSSE Pre-Authentication Encoding function preAuthEncoding(payloadType, payload) { - const prefix = [ - PAE_PREFIX, - payloadType.length, - payloadType, - payload.length, - '', - ].join(' '); - return Buffer.concat([Buffer.from(prefix, 'ascii'), payload]); + const typeBytes = Buffer.from(payloadType, 'utf-8'); + return Buffer.concat([ + Buffer.from(`${PAE_PREFIX} ${typeBytes.length} `, 'ascii'), + typeBytes, + Buffer.from(` ${payload.length} `, 'ascii'), + payload, + ]); } diff --git a/deps/npm/node_modules/@sigstore/core/package.json b/deps/npm/node_modules/@sigstore/core/package.json index 0564a373c6fa31..82cab44654a1c9 100644 --- a/deps/npm/node_modules/@sigstore/core/package.json +++ b/deps/npm/node_modules/@sigstore/core/package.json @@ -1,6 +1,6 @@ { "name": "@sigstore/core", - "version": "3.2.0", + "version": "3.2.1", "description": "Base library for Sigstore", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/deps/npm/node_modules/@sigstore/verify/dist/key/index.js b/deps/npm/node_modules/@sigstore/verify/dist/key/index.js index c966ccb1e925ef..880ad04bd235d7 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/key/index.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/key/index.js @@ -56,9 +56,17 @@ function getSigner(cert) { else { issuer = cert.extension(OID_FULCIO_ISSUER_V1)?.value.toString('ascii'); } + const oids = cert.extensions.map((ext) => { + const oid = ext.subs[0].toOID(); + return { + oid: { id: oid.split('.').map(Number) }, + value: ext.subs[ext.subs.length - 1].value, + }; + }); const identity = { extensions: { issuer }, subjectAlternativeName: cert.subjectAltName, + oids, }; return { key: core_1.crypto.createPublicKey(cert.publicKey), diff --git a/deps/npm/node_modules/@sigstore/verify/dist/policy.js b/deps/npm/node_modules/@sigstore/verify/dist/policy.js index f5960cf047b84b..b08d083a296fb8 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/policy.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/policy.js @@ -2,7 +2,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.verifySubjectAlternativeName = verifySubjectAlternativeName; exports.verifyExtensions = verifyExtensions; +exports.verifyOIDs = verifyOIDs; const error_1 = require("./error"); +// Verifies that the signer's SAN matches the policy identity. The +// policyIdentity is treated as a JavaScript regular expression pattern and +// tested against the full signerIdentity string. For exact matching, use +// anchored patterns (e.g. '^user@example\\.com$'). function verifySubjectAlternativeName(policyIdentity, signerIdentity) { if (signerIdentity === undefined || !signerIdentity.match(policyIdentity)) { throw new error_1.PolicyError({ @@ -22,3 +27,24 @@ function verifyExtensions(policyExtensions, signerExtensions = {}) { } } } +function verifyOIDs(policyOIDs, signerOIDs = []) { + for (const policyOID of policyOIDs) { + const match = signerOIDs.find((signerOID) => oidEquals(policyOID.oid?.id, signerOID.oid?.id) && + policyOID.value.equals(signerOID.value)); + if (!match) { + /* istanbul ignore next */ + const oid = policyOID.oid?.id.join('.') ?? ''; + throw new error_1.PolicyError({ + code: 'UNTRUSTED_SIGNER_ERROR', + message: `invalid certificate extension - missing OID ${oid}`, + }); + } + } +} +function oidEquals(a, b) { + /* istanbul ignore if */ + if (a === undefined || b === undefined) { + return false; + } + return a.length === b.length && a.every((v, i) => v === b[i]); +} diff --git a/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js b/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js index 03a51083e10827..603e559831a9d8 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/timestamp/index.js @@ -12,6 +12,10 @@ function getTSATimestamp(timestamp, data, timestampAuthorities) { }; } function getTLogTimestamp(entry) { + // Only entries with an inclusion promise provide a verifiable timestamp + if (!entry.inclusionPromise) { + return undefined; + } return { type: 'transparency-log', logID: entry.logId.keyId, diff --git a/deps/npm/node_modules/@sigstore/verify/dist/verifier.js b/deps/npm/node_modules/@sigstore/verify/dist/verifier.js index 5751087ff178d2..eeba4128fabe34 100644 --- a/deps/npm/node_modules/@sigstore/verify/dist/verifier.js +++ b/deps/npm/node_modules/@sigstore/verify/dist/verifier.js @@ -46,17 +46,22 @@ class Verifier { } // Checks that all of the timestamps in the entity are valid and returns them verifyTimestamps(entity) { - let timestampCount = 0; - const timestamps = entity.timestamps.map((timestamp) => { + const timestamps = []; + for (const timestamp of entity.timestamps) { switch (timestamp.$case) { case 'timestamp-authority': - timestampCount++; - return (0, timestamp_1.getTSATimestamp)(timestamp.timestamp, entity.signature.signature, this.trustMaterial.timestampAuthorities); - case 'transparency-log': - timestampCount++; - return (0, timestamp_1.getTLogTimestamp)(timestamp.tlogEntry); + timestamps.push((0, timestamp_1.getTSATimestamp)(timestamp.timestamp, entity.signature.signature, this.trustMaterial.timestampAuthorities)); + break; + case 'transparency-log': { + const result = (0, timestamp_1.getTLogTimestamp)(timestamp.tlogEntry); + /* istanbul ignore else */ + if (result) { + timestamps.push(result); + } + break; + } } - }); + } // Check for duplicate timestamps if (containsDupes(timestamps)) { throw new error_1.VerificationError({ @@ -64,10 +69,10 @@ class Verifier { message: 'duplicate timestamp', }); } - if (timestampCount < this.options.timestampThreshold) { + if (timestamps.length < this.options.timestampThreshold) { throw new error_1.VerificationError({ code: 'TIMESTAMP_ERROR', - message: `expected ${this.options.timestampThreshold} timestamps, got ${timestampCount}`, + message: `expected ${this.options.timestampThreshold} timestamps, got ${timestamps.length}`, }); } return timestamps.map((t) => t.timestamp); @@ -133,6 +138,11 @@ class Verifier { if (policy.extensions) { (0, policy_1.verifyExtensions)(policy.extensions, identity.extensions); } + // Check that the OIDs of the signer match the policy + /* istanbul ignore if */ + if (policy.oids) { + (0, policy_1.verifyOIDs)(policy.oids, identity.oids); + } } } exports.Verifier = Verifier; diff --git a/deps/npm/node_modules/@sigstore/verify/package.json b/deps/npm/node_modules/@sigstore/verify/package.json index 79826a80bddebf..9c4e5dc7a727a7 100644 --- a/deps/npm/node_modules/@sigstore/verify/package.json +++ b/deps/npm/node_modules/@sigstore/verify/package.json @@ -1,6 +1,6 @@ { "name": "@sigstore/verify", - "version": "3.1.0", + "version": "3.1.1", "description": "Verification of Sigstore signatures", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -28,7 +28,7 @@ "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0" + "@sigstore/core": "^3.2.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/deps/npm/node_modules/libnpmdiff/package.json b/deps/npm/node_modules/libnpmdiff/package.json index 974b7346e01068..08783e3ecb13e8 100644 --- a/deps/npm/node_modules/libnpmdiff/package.json +++ b/deps/npm/node_modules/libnpmdiff/package.json @@ -1,6 +1,6 @@ { "name": "libnpmdiff", - "version": "8.1.8", + "version": "8.1.9", "description": "The registry diff", "repository": { "type": "git", @@ -47,7 +47,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.6.0", + "@npmcli/arborist": "^9.7.0", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", diff --git a/deps/npm/node_modules/libnpmexec/lib/index.js b/deps/npm/node_modules/libnpmexec/lib/index.js index 3681653d8217d6..3add22cd2edca5 100644 --- a/deps/npm/node_modules/libnpmexec/lib/index.js +++ b/deps/npm/node_modules/libnpmexec/lib/index.js @@ -87,8 +87,10 @@ const missingFromTree = async ({ spec, tree, flatOptions, isNpxTree, shallow }) } // see if the package.json at `path` has an entry that matches `cmd` +// the path is a known-local directory, not a user-supplied dep, so +// allow-directory must not gate this introspection const hasPkgBin = (path, cmd, flatOptions) => - pacote.manifest(path, flatOptions) + pacote.manifest(path, { ...flatOptions, allowDirectory: 'all' }) .then(manifest => manifest?.bin?.[cmd]).catch(() => null) const exec = async (opts) => { @@ -147,6 +149,8 @@ const exec = async (opts) => { // we have to install the local package into the npx cache so that its // bin links get set up flatOptions.installLinks = false + // self-execution of a local bin, not a directory dep install + flatOptions.allowDirectory = 'all' // args[0] will exist when the package is installed packages.push(p) yes = true diff --git a/deps/npm/node_modules/libnpmexec/package.json b/deps/npm/node_modules/libnpmexec/package.json index 52a1e1539d2697..b672050048bd3f 100644 --- a/deps/npm/node_modules/libnpmexec/package.json +++ b/deps/npm/node_modules/libnpmexec/package.json @@ -1,6 +1,6 @@ { "name": "libnpmexec", - "version": "10.2.8", + "version": "10.2.9", "files": [ "bin/", "lib/" @@ -61,7 +61,7 @@ }, "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.6.0", + "@npmcli/arborist": "^9.7.0", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", diff --git a/deps/npm/node_modules/libnpmfund/package.json b/deps/npm/node_modules/libnpmfund/package.json index 9c35a66a27e31f..ab5b5a86d98339 100644 --- a/deps/npm/node_modules/libnpmfund/package.json +++ b/deps/npm/node_modules/libnpmfund/package.json @@ -1,6 +1,6 @@ { "name": "libnpmfund", - "version": "7.0.22", + "version": "7.0.23", "main": "lib/index.js", "files": [ "bin/", @@ -46,7 +46,7 @@ "tap": "^16.3.8" }, "dependencies": { - "@npmcli/arborist": "^9.6.0" + "@npmcli/arborist": "^9.7.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/deps/npm/node_modules/libnpmpack/package.json b/deps/npm/node_modules/libnpmpack/package.json index 11029cf91ee08a..58ff8edc24d844 100644 --- a/deps/npm/node_modules/libnpmpack/package.json +++ b/deps/npm/node_modules/libnpmpack/package.json @@ -1,6 +1,6 @@ { "name": "libnpmpack", - "version": "9.1.8", + "version": "9.1.9", "description": "Programmatic API for the bits behind npm pack", "author": "GitHub Inc.", "main": "lib/index.js", @@ -37,7 +37,7 @@ "bugs": "https://github.com/npm/libnpmpack/issues", "homepage": "https://npmjs.com/package/libnpmpack", "dependencies": { - "@npmcli/arborist": "^9.6.0", + "@npmcli/arborist": "^9.7.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" diff --git a/deps/npm/node_modules/libnpmversion/README.md b/deps/npm/node_modules/libnpmversion/README.md index b81a231d05ce04..d60a144bcc1bf1 100644 --- a/deps/npm/node_modules/libnpmversion/README.md +++ b/deps/npm/node_modules/libnpmversion/README.md @@ -86,6 +86,9 @@ The exact order of execution is as follows: 6. Run the `postversion` script. Use it to clean up the file system or automatically push the commit and/or tag. +For the `preversion`, `version` and `postversion` scripts, npm also sets the +environment variables `npm_old_version` and `npm_new_version`. + Take the following example: ```json diff --git a/deps/npm/node_modules/libnpmversion/package.json b/deps/npm/node_modules/libnpmversion/package.json index cac11cc36bd385..f8be6d8fdb3af7 100644 --- a/deps/npm/node_modules/libnpmversion/package.json +++ b/deps/npm/node_modules/libnpmversion/package.json @@ -1,6 +1,6 @@ { "name": "libnpmversion", - "version": "8.0.3", + "version": "8.0.4", "main": "lib/index.js", "files": [ "bin/", diff --git a/deps/npm/node_modules/lru-cache/package.json b/deps/npm/node_modules/lru-cache/package.json index 760fee478270d7..6ada2c211f2d6c 100644 --- a/deps/npm/node_modules/lru-cache/package.json +++ b/deps/npm/node_modules/lru-cache/package.json @@ -1,7 +1,7 @@ { "name": "lru-cache", "description": "A cache object that deletes the least-recently-used items.", - "version": "11.5.0", + "version": "11.5.1", "author": "Isaac Z. Schlueter ", "keywords": [ "mru", diff --git a/deps/npm/node_modules/make-fetch-happen/package.json b/deps/npm/node_modules/make-fetch-happen/package.json index 1d06ac4889c3e3..92c48b45871586 100644 --- a/deps/npm/node_modules/make-fetch-happen/package.json +++ b/deps/npm/node_modules/make-fetch-happen/package.json @@ -1,6 +1,6 @@ { "name": "make-fetch-happen", - "version": "15.0.5", + "version": "15.0.6", "description": "Opinionated, caching, retrying fetch client", "main": "lib/index.js", "files": [ diff --git a/deps/npm/node_modules/semver/classes/range.js b/deps/npm/node_modules/semver/classes/range.js index 94629ce6f5df60..c2e605e5173601 100644 --- a/deps/npm/node_modules/semver/classes/range.js +++ b/deps/npm/node_modules/semver/classes/range.js @@ -98,6 +98,9 @@ class Range { } parseRange (range) { + // strip build metadata so it can't bleed into the version + range = range.replace(BUILDSTRIPRE, '') + // memoize range parsing for performance. // this is a very hot path, and fully deterministic. const memoOpts = @@ -223,6 +226,7 @@ const debug = require('../internal/debug') const SemVer = require('./semver') const { safeRe: re, + src, t, comparatorTrimReplace, tildeTrimReplace, @@ -230,6 +234,9 @@ const { } = require('../internal/re') const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants') +// unbounded global build-metadata stripper used by parseRange +const BUILDSTRIPRE = new RegExp(src[t.BUILD], 'g') + const isNullSet = c => c.value === '<0.0.0-0' const isAny = c => c.value === '' diff --git a/deps/npm/node_modules/semver/package.json b/deps/npm/node_modules/semver/package.json index f8447c4951594d..6edb9ab49d9774 100644 --- a/deps/npm/node_modules/semver/package.json +++ b/deps/npm/node_modules/semver/package.json @@ -1,6 +1,6 @@ { "name": "semver", - "version": "7.8.0", + "version": "7.8.1", "description": "The semantic version parser used by npm.", "main": "index.js", "scripts": { diff --git a/deps/npm/node_modules/semver/ranges/subset.js b/deps/npm/node_modules/semver/ranges/subset.js index 99f43218075c86..a949832329003b 100644 --- a/deps/npm/node_modules/semver/ranges/subset.js +++ b/deps/npm/node_modules/semver/ranges/subset.js @@ -174,7 +174,7 @@ const simpleSubset = (sub, dom, options) => { if (higher === c && higher !== gt) { return false } - } else if (gt.operator === '>=' && !satisfies(gt.semver, String(c), options)) { + } else if (gt.operator === '>=' && !c.test(gt.semver)) { return false } } @@ -192,7 +192,7 @@ const simpleSubset = (sub, dom, options) => { if (lower === c && lower !== lt) { return false } - } else if (lt.operator === '<=' && !satisfies(lt.semver, String(c), options)) { + } else if (lt.operator === '<=' && !c.test(lt.semver)) { return false } } diff --git a/deps/npm/node_modules/sigstore/dist/config.js b/deps/npm/node_modules/sigstore/dist/config.js index e8b2392f97f236..373149fe22fb75 100644 --- a/deps/npm/node_modules/sigstore/dist/config.js +++ b/deps/npm/node_modules/sigstore/dist/config.js @@ -65,6 +65,12 @@ function createVerificationPolicy(options) { if (options.certificateIssuer) { policy.extensions = { issuer: options.certificateIssuer }; } + if (options.certificateOIDs) { + policy.oids = Object.entries(options.certificateOIDs).map(([oid, value]) => ({ + oid: { id: oid.split('.').map(Number) }, + value: Buffer.from(value), + })); + } return policy; } // Instantiate the FulcioSigner based on the supplied options. diff --git a/deps/npm/node_modules/sigstore/package.json b/deps/npm/node_modules/sigstore/package.json index 5965f0889ca7db..e0acea6d96287e 100644 --- a/deps/npm/node_modules/sigstore/package.json +++ b/deps/npm/node_modules/sigstore/package.json @@ -1,6 +1,6 @@ { "name": "sigstore", - "version": "4.1.0", + "version": "4.1.1", "description": "code-signing for npm packages", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -29,17 +29,17 @@ "devDependencies": { "@sigstore/rekor-types": "^4.0.0", "@sigstore/jest": "^0.0.0", - "@sigstore/mock": "^0.11.0", - "@tufjs/repo-mock": "^4.0.0", + "@sigstore/mock": "^0.12.1", + "@tufjs/repo-mock": "^4.0.1", "@types/make-fetch-happen": "^10.0.4" }, "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" diff --git a/deps/npm/node_modules/undici/lib/dispatcher/agent.js b/deps/npm/node_modules/undici/lib/dispatcher/agent.js index db2f817d0fe978..90b46fe3aeb4b4 100644 --- a/deps/npm/node_modules/undici/lib/dispatcher/agent.js +++ b/deps/npm/node_modules/undici/lib/dispatcher/agent.js @@ -24,7 +24,6 @@ function defaultFactory (origin, opts) { class Agent extends DispatcherBase { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } diff --git a/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js b/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js index 2b8fa05da29427..ef3d38ea4f2ed3 100644 --- a/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js +++ b/deps/npm/node_modules/undici/lib/dispatcher/client-h1.js @@ -279,29 +279,71 @@ class Parser { const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr - if (ret === constants.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)) - } else if (ret === constants.ERROR.PAUSED) { - this.paused = true - socket.unshift(data.slice(offset)) - } else if (ret !== constants.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr) - let message = '' - /* istanbul ignore else: difficult to make a test case for */ - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) - message = - 'Response does not match the HTTP/1.1 protocol (' + - Buffer.from(llhttp.memory.buffer, ptr, len).toString() + - ')' + if (ret !== constants.ERROR.OK) { + const body = data.subarray(offset) + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(body) + } else { + throw this.createError(ret, body) } - throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) } } catch (err) { util.destroy(socket, err) } } + finish () { + assert(currentParser === null) + assert(this.ptr != null) + assert(!this.paused) + + const { llhttp } = this + + let ret + + try { + currentParser = this + ret = llhttp.llhttp_finish(this.ptr) + } finally { + currentParser = null + } + + if (ret === constants.ERROR.OK) { + return null + } + + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true + return null + } + + return this.createError(ret, EMPTY_BUF) + } + + createError (ret, data) { + const { llhttp, contentLength, bytesRead } = this + + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError() + } + + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + + return new HTTPParserError(message, constants.ERROR[ret], data) + } + destroy () { assert(this.ptr != null) assert(currentParser == null) @@ -673,8 +715,11 @@ async function connectH1 (client, socket) { // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // to the user. if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + this[kError] = parserErr + this[kClient][kOnError](parserErr) + } return } @@ -693,8 +738,10 @@ async function connectH1 (client, socket) { const parser = this[kParser] if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + const parserErr = parser.finish() + if (parserErr) { + util.destroy(this, parserErr) + } return } @@ -706,8 +753,7 @@ async function connectH1 (client, socket) { if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() + this[kError] = parser.finish() || this[kError] } this[kParser].destroy() diff --git a/deps/npm/node_modules/undici/package.json b/deps/npm/node_modules/undici/package.json index 46cb9a8292618f..d1eef502c4169f 100644 --- a/deps/npm/node_modules/undici/package.json +++ b/deps/npm/node_modules/undici/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.25.0", + "version": "6.26.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/npm/package.json b/deps/npm/package.json index 24c176130a2121..e600a3c0095246 100644 --- a/deps/npm/package.json +++ b/deps/npm/package.json @@ -1,5 +1,5 @@ { - "version": "11.15.0", + "version": "11.16.0", "name": "npm", "description": "a package manager for JavaScript", "workspaces": [ @@ -52,8 +52,8 @@ }, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.6.0", - "@npmcli/config": "^10.9.1", + "@npmcli/arborist": "^9.7.0", + "@npmcli/config": "^10.10.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -77,16 +77,16 @@ "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.8", - "libnpmexec": "^10.2.8", - "libnpmfund": "^7.0.22", + "libnpmdiff": "^8.1.9", + "libnpmexec": "^10.2.9", + "libnpmfund": "^7.0.23", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.8", + "libnpmpack": "^9.1.9", "libnpmpublish": "^11.2.0", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", - "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.5", + "libnpmversion": "^8.0.4", + "make-fetch-happen": "^15.0.6", "minimatch": "^10.2.5", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", @@ -106,7 +106,7 @@ "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", - "semver": "^7.8.0", + "semver": "^7.8.1", "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", diff --git a/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs index a5f0f6748f8d74..12f1c803cd7695 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -62,6 +62,7 @@ Array [ String( access adduser + approve-scripts audit author add diff --git a/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs index 4a224a5cffbff8..829b64b3f800b6 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -20,6 +20,9 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "allow-file": "all", "allow-git": "all", "allow-remote": "all", + "allow-scripts": [ + "" + ], "also": null, "audit": true, "audit-level": null, @@ -37,6 +40,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "cidr": null, "commit-hooks": true, "cpu": null, + "dangerously-allow-all-scripts": false, "depth": null, "description": true, "dev": false, @@ -127,6 +131,8 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "pack-destination": ".", "packages": [], "parseable": false, + "allow-scripts-pending": false, + "allow-scripts-pin": true, "prefer-dedupe": false, "prefer-offline": false, "prefer-online": false, @@ -167,6 +173,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "sign-git-commit": false, "sign-git-tag": false, "strict-peer-deps": false, + "strict-allow-scripts": false, "strict-ssl": true, "tag": "latest", "tag-version-prefix": "v", @@ -200,6 +207,9 @@ allow-file = "all" allow-git = "all" allow-remote = "all" allow-same-version = false +allow-scripts = [""] +allow-scripts-pending = false +allow-scripts-pin = true also = null audit = true audit-level = null @@ -219,6 +229,7 @@ cidr = null ; color = {COLOR} commit-hooks = true cpu = null +dangerously-allow-all-scripts = false depth = null description = true dev = false @@ -348,6 +359,7 @@ shell = "{SHELL}" shrinkwrap = true sign-git-commit = false sign-git-tag = false +strict-allow-scripts = false strict-peer-deps = false strict-ssl = true tag = "latest" diff --git a/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs b/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs index 2eda536c6bb33e..143d08dda8ff4b 100644 --- a/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -156,6 +156,7 @@ Object { "man": Array [ "man/man1/npm-access.1", "man/man1/npm-adduser.1", + "man/man1/npm-approve-scripts.1", "man/man1/npm-audit.1", "man/man1/npm-bugs.1", "man/man1/npm-cache.1", @@ -163,6 +164,7 @@ Object { "man/man1/npm-completion.1", "man/man1/npm-config.1", "man/man1/npm-dedupe.1", + "man/man1/npm-deny-scripts.1", "man/man1/npm-deprecate.1", "man/man1/npm-diff.1", "man/man1/npm-dist-tag.1", @@ -269,6 +271,28 @@ exports[`test/lib/commands/publish.js TAP prioritize CLI flags over publishConfi + @npmcli/test-package@1.0.0 ` +exports[`test/lib/commands/publish.js TAP private access > must match snapshot 1`] = ` +Array [ + "package: @npm/test-package@1.0.0", + "Tarball Contents", + "55B package.json", + "Tarball Details", + "name: @npm/test-package", + "version: 1.0.0", + "filename: npm-test-package-1.0.0.tgz", + "package size: {size}", + "unpacked size: 55 B", + "shasum: {sha}", + "integrity: {integrity} + "total files: 1", + "Publishing to https://registry.npmjs.org/ with tag latest and restricted access", +] +` + +exports[`test/lib/commands/publish.js TAP private access > new package version 1`] = ` ++ @npm/test-package@1.0.0 +` + exports[`test/lib/commands/publish.js TAP public access > must match snapshot 1`] = ` Array [ "package: @npm/test-package@1.0.0", diff --git a/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs b/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs index dfc170636ee500..72671906850ee0 100644 --- a/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/docs.js.test.cjs @@ -99,6 +99,7 @@ exports[`test/lib/docs.js TAP command list > commands 1`] = ` Array [ "access", "adduser", + "approve-scripts", "audit", "bugs", "cache", @@ -106,6 +107,7 @@ Array [ "completion", "config", "dedupe", + "deny-scripts", "deprecate", "diff", "dist-tag", @@ -193,7 +195,7 @@ safer to use a registry-provided authentication bearer token stored in the * Default: 'public' for new packages, existing packages it will not change the current level -* Type: null, "restricted", or "public" +* Type: null, "restricted", "public", or "private" If you do not want your scoped package to be publicly viewable (and installable) set \`--access=restricted\`. @@ -205,6 +207,8 @@ packages. Specifying a value of \`restricted\` or \`public\` during publish will change the access for an existing package the same way that \`npm access set status\` would. +The value \`private\` is an alias for \`restricted\`. + #### \`all\` @@ -301,6 +305,51 @@ to the same value as the current version. +#### \`allow-scripts\` + +* Default: "" +* Type: String (can be set multiple times) + +Comma-separated list of packages whose install-time lifecycle scripts +(\`preinstall\`, \`install\`, \`postinstall\`, and \`prepare\` for non-registry +dependencies) are allowed to run. + +This setting is intended for one-off and global contexts: \`npm exec\`, \`npx\`, +and \`npm install -g\`, where no project \`package.json\` is involved. For +team-wide policy in a project, use the \`allowScripts\` field in +\`package.json\` (which also supports explicit denials), or configure it in +\`.npmrc\`. Passing \`--allow-scripts\` on the command line during a +project-scoped \`npm install\`, \`ci\`, \`update\`, or \`rebuild\` is an error. + +Each name is matched against a dependency's resolved identity, not against +the package's self-reported name. \`--ignore-scripts\` and +\`--dangerously-allow-all-scripts\` both override this setting. + + + +#### \`allow-scripts-pending\` + +* Default: false +* Type: Boolean + +List packages with install scripts that are not yet covered by the +\`allowScripts\` policy, without modifying \`package.json\`. Only meaningful for +\`npm approve-scripts\`. + + + +#### \`allow-scripts-pin\` + +* Default: true +* Type: Boolean + +Write pinned (\`pkg@version\`) entries when approving install scripts. Set to +\`false\` to write name-only entries that allow any version. Has no effect on +\`npm deny-scripts\`, which always writes name-only entries regardless of this +setting. + + + #### \`audit\` * Default: true @@ -496,6 +545,18 @@ are same as \`cpu\` field of package.json, which comes from \`process.arch\`. +#### \`dangerously-allow-all-scripts\` + +* Default: false +* Type: Boolean + +If \`true\`, bypass the \`allowScripts\` policy entirely and run every +dependency install script regardless of whether it was approved or denied. +Intended as a migration escape hatch only; its use is strongly discouraged. +\`--ignore-scripts\` still takes precedence over this setting. + + + #### \`depth\` * Default: \`Infinity\` if \`--all\` is set; otherwise, \`0\` @@ -1822,6 +1883,22 @@ this to work properly. +#### \`strict-allow-scripts\` + +* Default: false +* Type: Boolean + +If \`true\`, turn the install-script policy from a warning into a hard error: +any dependency with install scripts not covered by \`allowScripts\` will fail +the install instead of running with a notice. + +Dependencies explicitly denied with \`false\` in \`allowScripts\` are always +silently skipped; this setting only affects unreviewed entries. +\`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both override this +setting. + + + #### \`strict-peer-deps\` * Default: false @@ -2327,6 +2404,7 @@ Array [ "allow-file", "allow-git", "allow-remote", + "allow-scripts", "also", "audit", "audit-level", @@ -2346,6 +2424,7 @@ Array [ "color", "commit-hooks", "cpu", + "dangerously-allow-all-scripts", "depth", "description", "dev", @@ -2435,6 +2514,8 @@ Array [ "pack-destination", "packages", "parseable", + "allow-scripts-pending", + "allow-scripts-pin", "prefer-dedupe", "prefer-offline", "prefer-online", @@ -2476,6 +2557,7 @@ Array [ "sign-git-commit", "sign-git-tag", "strict-peer-deps", + "strict-allow-scripts", "strict-ssl", "tag", "tag-version-prefix", @@ -2507,6 +2589,7 @@ Array [ "allow-file", "allow-git", "allow-remote", + "allow-scripts", "also", "audit", "audit-level", @@ -2526,6 +2609,7 @@ Array [ "color", "commit-hooks", "cpu", + "dangerously-allow-all-scripts", "depth", "description", "dev", @@ -2595,6 +2679,8 @@ Array [ "pack-destination", "packages", "parseable", + "allow-scripts-pending", + "allow-scripts-pin", "prefer-dedupe", "prefer-offline", "prefer-online", @@ -2635,6 +2721,7 @@ Array [ "sign-git-commit", "sign-git-tag", "strict-peer-deps", + "strict-allow-scripts", "strict-ssl", "tag", "tag-version-prefix", @@ -2692,6 +2779,9 @@ Object { "allowGit": "all", "allowRemote": "all", "allowSameVersion": false, + "allowScripts": Array [], + "allowScriptsPending": false, + "allowScriptsPin": true, "audit": true, "auditLevel": null, "authType": "web", @@ -2707,6 +2797,7 @@ Object { "color": false, "commitHooks": true, "cpu": null, + "dangerouslyAllowAllScripts": false, "defaultTag": "latest", "depth": null, "diff": Array [], @@ -2812,6 +2903,7 @@ Object { "signGitCommit": false, "signGitTag": false, "silent": false, + "strictAllowScripts": false, "strictPeerDeps": false, "strictSSL": true, "tagVersionPrefix": "v", @@ -2949,6 +3041,46 @@ Note: This command is unaware of workspaces. #### \`auth-type\` ` +exports[`test/lib/docs.js TAP usage approve-scripts > must match snapshot 1`] = ` +Approve install scripts for specific dependencies + +Usage: +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending + +Options: +[-a|--all] [--allow-scripts-pending] [--no-allow-scripts-pin] [--json] + + -a|--all + When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show + + --allow-scripts-pending + List packages with install scripts that are not yet covered by the + + --allow-scripts-pin + Write pinned (\`pkg@version\`) entries when approving install scripts. + + --json + Whether or not to output JSON data, rather than the normal output. + + +Run "npm help approve-scripts" for more info + +\`\`\`bash +npm approve-scripts [ ...] +npm approve-scripts --all +npm approve-scripts --allow-scripts-pending +\`\`\` + +Note: This command is unaware of workspaces. + +#### \`all\` +#### \`allow-scripts-pending\` +#### \`allow-scripts-pin\` +#### \`json\` +` + exports[`test/lib/docs.js TAP usage audit > must match snapshot 1`] = ` Run a security audit @@ -3125,7 +3257,9 @@ Options: [--include [--include ...]] [--strict-peer-deps] [--foreground-scripts] [--ignore-scripts] [--allow-directory ] [--allow-file ] -[--allow-git ] [--allow-remote ] [--no-audit] +[--allow-git ] [--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -3166,6 +3300,15 @@ Options: --allow-remote Limits the ability for npm to fetch dependencies from urls. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -3213,6 +3356,9 @@ aliases: clean-install, ic, install-clean, isntall-clean #### \`allow-file\` #### \`allow-git\` #### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`bin-links\` #### \`fund\` @@ -3409,6 +3555,44 @@ alias: ddp #### \`install-links\` ` +exports[`test/lib/docs.js TAP usage deny-scripts > must match snapshot 1`] = ` +Deny install scripts for specific dependencies + +Usage: +npm deny-scripts [ ...] +npm deny-scripts --all + +Options: +[-a|--all] [--allow-scripts-pending] [--no-allow-scripts-pin] [--json] + + -a|--all + When running \`npm outdated\` and \`npm ls\`, setting \`--all\` will show + + --allow-scripts-pending + List packages with install scripts that are not yet covered by the + + --allow-scripts-pin + Write pinned (\`pkg@version\`) entries when approving install scripts. + + --json + Whether or not to output JSON data, rather than the normal output. + + +Run "npm help deny-scripts" for more info + +\`\`\`bash +npm deny-scripts [ ...] +npm deny-scripts --all +\`\`\` + +Note: This command is unaware of workspaces. + +#### \`all\` +#### \`allow-scripts-pending\` +#### \`allow-scripts-pin\` +#### \`json\` +` + exports[`test/lib/docs.js TAP usage deprecate > must match snapshot 1`] = ` Deprecate a version of a package @@ -3660,6 +3844,8 @@ Options: [--package [--package ...]] [-c|--call ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] --package The package or packages to install for [\`npm exec\`](/commands/npm-exec) @@ -3676,6 +3862,15 @@ Options: --include-workspace-root Include the workspace root when workspaces are enabled for a command. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + alias: x @@ -3695,6 +3890,9 @@ alias: x #### \`workspace\` #### \`workspaces\` #### \`include-workspace-root\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` ` exports[`test/lib/docs.js TAP usage explain > must match snapshot 1`] = ` @@ -4050,9 +4248,11 @@ Options: [--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only] [--foreground-scripts] [--ignore-scripts] [--allow-directory ] [--allow-file ] [--allow-git ] -[--allow-remote ] [--no-audit] [--before ] -[--min-release-age ] [--no-bin-links] [--no-fund] [--dry-run] [--cpu ] -[--os ] [--libc ] +[--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--before ] [--min-release-age ] [--no-bin-links] [--no-fund] +[--dry-run] [--cpu ] [--os ] [--libc ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -4110,6 +4310,15 @@ Options: --allow-remote Limits the ability for npm to fetch dependencies from urls. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -4178,6 +4387,9 @@ aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall #### \`allow-file\` #### \`allow-git\` #### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`before\` #### \`min-release-age\` @@ -4205,7 +4417,9 @@ Options: [--include [--include ...]] [--strict-peer-deps] [--foreground-scripts] [--ignore-scripts] [--allow-directory ] [--allow-file ] -[--allow-git ] [--allow-remote ] [--no-audit] +[--allow-git ] [--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] [--no-bin-links] [--no-fund] [--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -4246,6 +4460,15 @@ Options: --allow-remote Limits the ability for npm to fetch dependencies from urls. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -4293,6 +4516,9 @@ aliases: cit, clean-install-test, sit #### \`allow-file\` #### \`allow-git\` #### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`bin-links\` #### \`fund\` @@ -4318,9 +4544,11 @@ Options: [--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only] [--foreground-scripts] [--ignore-scripts] [--allow-directory ] [--allow-file ] [--allow-git ] -[--allow-remote ] [--no-audit] [--before ] -[--min-release-age ] [--no-bin-links] [--no-fund] [--dry-run] [--cpu ] -[--os ] [--libc ] +[--allow-remote ] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--before ] [--min-release-age ] [--no-bin-links] [--no-fund] +[--dry-run] [--cpu ] [--os ] [--libc ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -4378,6 +4606,15 @@ Options: --allow-remote Limits the ability for npm to fetch dependencies from urls. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -4446,6 +4683,9 @@ alias: it #### \`allow-file\` #### \`allow-git\` #### \`allow-remote\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`before\` #### \`min-release-age\` @@ -5234,7 +5474,7 @@ Usage: npm publish Options: -[--tag ] [--access ] [--dry-run] [--otp ] +[--tag ] [--access ] [--dry-run] [--otp ] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--provenance|--provenance-file ] @@ -5334,6 +5574,8 @@ npm rebuild [] ...] Options: [-g|--global] [--no-bin-links] [--foreground-scripts] [--ignore-scripts] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -5349,6 +5591,15 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + -w|--workspace Enable running a command in the context of the configured workspaces of the @@ -5376,6 +5627,9 @@ alias: rb #### \`bin-links\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`workspace\` #### \`workspaces\` #### \`include-workspace-root\` @@ -6219,8 +6473,11 @@ Options: [--omit [--omit ...]] [--include [--include ...]] [--strict-peer-deps] [--no-package-lock] [--foreground-scripts] -[--ignore-scripts] [--no-audit] [--before ] [--min-release-age ] -[--no-bin-links] [--no-fund] [--dry-run] +[--ignore-scripts] +[--allow-scripts [--allow-scripts ...]] +[--strict-allow-scripts] [--dangerously-allow-all-scripts] [--no-audit] +[--before ] [--min-release-age ] [--no-bin-links] [--no-fund] +[--dry-run] [-w|--workspace [-w|--workspace ...]] [--workspaces] [--include-workspace-root] [--install-links] @@ -6257,6 +6514,15 @@ Options: --ignore-scripts If true, npm does not run scripts specified in package.json files. + --allow-scripts + Comma-separated list of packages whose install-time lifecycle scripts + + --strict-allow-scripts + If \`true\`, turn the install-script policy from a warning into a hard + + --dangerously-allow-all-scripts + If \`true\`, bypass the \`allowScripts\` policy entirely and run every + --audit When "true" submit audit reports alongside the current npm command to the @@ -6309,6 +6575,9 @@ aliases: u, up, upgrade, udpate #### \`package-lock\` #### \`foreground-scripts\` #### \`ignore-scripts\` +#### \`allow-scripts\` +#### \`strict-allow-scripts\` +#### \`dangerously-allow-all-scripts\` #### \`audit\` #### \`before\` #### \`min-release-age\` diff --git a/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs b/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs index 337095989e6638..dc1c95cd3c5763 100644 --- a/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs +++ b/deps/npm/tap-snapshots/test/lib/npm.js.test.cjs @@ -31,16 +31,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - stage, star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -69,9 +69,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -122,9 +124,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -174,16 +178,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - stage, star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -212,9 +216,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -265,9 +271,11 @@ npm help npm more involved overview All commands: access, adduser, - audit, bugs, cache, ci, + approve-scripts, audit, + bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -317,10 +325,12 @@ npm help npm more involved overview All commands: - access, adduser, audit, + access, adduser, + approve-scripts, audit, bugs, cache, ci, completion, config, - dedupe, deprecate, diff, + dedupe, deny-scripts, + deprecate, diff, dist-tag, docs, doctor, edit, exec, explain, explore, find-dupes, @@ -369,16 +379,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - stage, star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -406,16 +416,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - stage, star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -443,16 +453,16 @@ npm help npm more involved overview All commands: - access, adduser, audit, bugs, cache, ci, completion, - config, dedupe, deprecate, diff, dist-tag, docs, doctor, - edit, exec, explain, explore, find-dupes, fund, get, help, - help-search, init, install, install-ci-test, install-test, - link, ll, login, logout, ls, org, outdated, owner, pack, - ping, pkg, prefix, profile, prune, publish, query, rebuild, - repo, restart, root, run, sbom, search, set, shrinkwrap, - stage, star, stars, start, stop, team, test, token, trust, - undeprecate, uninstall, unpublish, unstar, update, version, - view, whoami + access, adduser, approve-scripts, audit, bugs, cache, ci, + completion, config, dedupe, deny-scripts, deprecate, diff, + dist-tag, docs, doctor, edit, exec, explain, explore, + find-dupes, fund, get, help, help-search, init, install, + install-ci-test, install-test, link, ll, login, logout, ls, + org, outdated, owner, pack, ping, pkg, prefix, profile, + prune, publish, query, rebuild, repo, restart, root, run, + sbom, search, set, shrinkwrap, stage, star, stars, start, + stop, team, test, token, trust, undeprecate, uninstall, + unpublish, unstar, update, version, view, whoami Specify configs in the ini-formatted file: {USERCONFIG} diff --git a/deps/npm/test/lib/commands/approve-scripts.js b/deps/npm/test/lib/commands/approve-scripts.js new file mode 100644 index 00000000000000..dde7a358b12e2b --- /dev/null +++ b/deps/npm/test/lib/commands/approve-scripts.js @@ -0,0 +1,562 @@ +const t = require('tap') +const fs = require('node:fs') +const { resolve } = require('node:path') +const _mockNpm = require('../../fixtures/mock-npm') + +const mockNpm = async (t, opts = {}) => { + return _mockNpm(t, opts) +} + +const setupProject = ({ allowScripts, withScripts = ['canvas'] } = {}) => { + const pkg = { + name: 'host', + version: '1.0.0', + dependencies: Object.fromEntries(withScripts.map((n) => [n, '*'])), + } + if (allowScripts !== undefined) { + pkg.allowScripts = allowScripts + } + + const lockPackages = { '': pkg } + const nodeModules = {} + for (const name of withScripts) { + const tarUrl = `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz` + nodeModules[name] = { + 'package.json': JSON.stringify({ + name, + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + } + lockPackages[`node_modules/${name}`] = { + version: '1.0.0', + resolved: tarUrl, + hasInstallScript: true, + } + } + + return { + 'package.json': JSON.stringify(pkg, null, 2), + 'package-lock.json': JSON.stringify({ + name: pkg.name, + version: pkg.version, + lockfileVersion: 3, + requires: true, + packages: lockPackages, + }), + node_modules: nodeModules, + } +} + +t.test('approve-scripts --pending lists unreviewed packages', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + const out = joinedOutput() + t.match(out, /2 packages have install scripts not yet covered/) + t.match(out, /canvas@1\.0\.0/) + t.match(out, /sharp@1\.0\.0/) +}) + +t.test('approve-scripts --pending with no unreviewed says so', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ + allowScripts: { canvas: true }, + withScripts: ['canvas'], + }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('approve-scripts writes pinned entry by default', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) +}) + +t.test('approve-scripts --no-pin writes name-only entry', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pin': false }, + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { canvas: true }) +}) + +t.test('approve-scripts --all approves every unreviewed package', async t => { + const { npm, prefix } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas', 'sharp'] }), + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { + 'canvas@1.0.0': true, + 'sharp@1.0.0': true, + }) +}) + +t.test('approve-scripts errors on unknown package', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects( + npm.exec('approve-scripts', ['not-installed']), + { code: 'ENOMATCH' } + ) +}) + +t.test('approve-scripts respects existing deny entry', async t => { + const { npm, prefix, logs } = await mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { canvas: false }, + }), + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + // Deny wins; unchanged. + t.strictSame(pkg.allowScripts, { canvas: false }) + t.match(logs.warn.byTitle('approve-scripts'), [/canvas is denied/]) +}) + +t.test('approve-scripts requires positional args, --all, or --pending', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + }) + await t.rejects(npm.exec('approve-scripts', []), { code: 'EUSAGE' }) +}) + +t.test('approve-scripts --pending cannot be combined with positional', async t => { + const { npm } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await t.rejects(npm.exec('approve-scripts', ['canvas']), { code: 'EUSAGE' }) +}) + +t.test('approve-scripts fails on global', async t => { + const { npm } = await mockNpm(t, { + config: { global: true }, + }) + await t.rejects(npm.exec('approve-scripts', ['canvas']), { code: 'EGLOBAL' }) +}) + +t.test('approve-scripts --json outputs structured summary', async t => { + const { npm, joinedOutput } = await mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { json: true }, + }) + await npm.exec('approve-scripts', ['canvas']) + const parsed = JSON.parse(joinedOutput()) + t.match(parsed, { + allowScripts: [{ name: 'canvas', changes: [{ key: 'canvas@1.0.0', change: 'added' }] }], + }) +}) + +t.test('approve-scripts --all with no unreviewed packages prints message', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'host', version: '1.0.0' } }, + }), + node_modules: {}, + }, + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('approve-scripts on a package already at the right pin is no-op', async t => { + const { npm, prefix, joinedOutput } = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + allowScripts: { 'canvas@1.0.0': true }, + }), + }) + await npm.exec('approve-scripts', ['canvas']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'canvas@1.0.0': true }) + t.match(joinedOutput(), /Nothing to approve/) +}) + +t.test('approve-scripts --pending with single package uses singular wording', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /1 package has install scripts/) +}) + +t.test('approve-scripts --pending lists package with no version', async t => { + // Use a fixture where the lockfile records a synthetic node without a version + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['canvas'] }), + config: { 'allow-scripts-pending': true }, + }) + await npm.exec('approve-scripts', []) + // Just exercising; no assertion needed for additional coverage. + t.pass() +}) + +t.test('approve-scripts groups multiple installed versions of the same package', async t => { + // Two versions of lodash exist in the tree; both have install scripts. + // groupByPackage should put them in the same group (hits the + // `if (!groups[key])` falsy branch on the second node). + const { npm, prefix } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'top-of-tree': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'top-of-tree': '*' } }, + 'node_modules/lodash': { + version: '4.17.21', + resolved: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz', + hasInstallScript: true, + }, + 'node_modules/top-of-tree': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/top-of-tree/-/top-of-tree-1.0.0.tgz', + dependencies: { lodash: '3.10.1' }, + }, + 'node_modules/top-of-tree/node_modules/lodash': { + version: '3.10.1', + resolved: 'https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz', + hasInstallScript: true, + }, + }, + }), + node_modules: { + lodash: { + 'package.json': JSON.stringify({ + name: 'lodash', + version: '4.17.21', + scripts: { install: 'echo install' }, + }), + }, + 'top-of-tree': { + 'package.json': JSON.stringify({ name: 'top-of-tree', version: '1.0.0' }), + node_modules: { + lodash: { + 'package.json': JSON.stringify({ + name: 'lodash', + version: '3.10.1', + scripts: { install: 'echo install' }, + }), + }, + }, + }, + }, + }, + }) + await npm.exec('approve-scripts', ['lodash']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + // Both versions get pinned. + t.strictSame(pkg.allowScripts, { + 'lodash@3.10.1': true, + 'lodash@4.17.21': true, + }) +}) + +t.test('approve-scripts --pending handles node with no version', async t => { + // Exercise the ternary's falsy branch in runPending: `node.version ? '@'... : ''` + // when the node has no version field. + const mockSync = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'host', version: '1.0.0' } }, + }), + node_modules: {}, + }, + config: { 'allow-scripts-pending': true }, + mocks: { + // Make the walker return a synthetic node with no version + '{LIB}/utils/check-allow-scripts.js': async () => [{ + node: { packageName: 'no-version-pkg', name: 'no-version-pkg', version: undefined }, + scripts: { install: 'do-stuff' }, + }], + }, + }) + await mockSync.npm.exec('approve-scripts', []) + // Output should mention the package without an @version suffix. + t.match(mockSync.joinedOutput(), / no-version-pkg \(install: do-stuff\)/) +}) + +t.test('forbidden semver range in package.json#allowScripts is dropped with a warning', async t => { + // End-to-end: project declares a caret range in allowScripts. The + // resolver must drop the entry, emit a warning, and the matching node + // must remain unreviewed (listed by --pending). + const mock = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['canvas'], + // ^0.33.0 is a forbidden range per RFC. + allowScripts: { 'canvas@^0.33.0': true }, + }), + config: { 'allow-scripts-pending': true }, + }) + await mock.npm.exec('approve-scripts', []) + + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.ok( + warnings.some(m => /semver ranges/.test(m) && /canvas@\^0\.33\.0/.test(m)), + 'resolver emits warning about forbidden range' + ) + // canvas was installed with version 1.0.0 (setupProject default) and + // the forbidden allowlist entry was dropped, so canvas appears in the + // pending list. + t.match(mock.joinedOutput(), /canvas@1\.0\.0/) +}) + +t.test('approve-scripts --pending lists packages that only have binding.gyp', async t => { + // End-to-end: a package with no preinstall/install/postinstall but a + // binding.gyp on disk gets a synthetic `node-gyp rebuild` install + // script. The runtime isNodeGypPackage check must see it and surface + // the package in --pending output. + const mock = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'native-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'native-pkg': '*' } }, + 'node_modules/native-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/native-pkg/-/native-pkg-1.0.0.tgz', + // No hasInstallScript — the synthetic node-gyp injection is + // what we want this test to exercise. + }, + }, + }), + node_modules: { + 'native-pkg': { + 'package.json': JSON.stringify({ name: 'native-pkg', version: '1.0.0' }), + // The file that triggers isNodeGypPackage to return true. + 'binding.gyp': '{}', + }, + }, + }, + config: { 'allow-scripts-pending': true }, + }) + await mock.npm.exec('approve-scripts', []) + + const out = mock.joinedOutput() + t.match(out, /native-pkg@1\.0\.0/, 'binding.gyp-only package appears in --pending') + t.match(out, /install: node-gyp rebuild/, 'synthetic node-gyp install is named') +}) + +t.test('approve-scripts --all skips bundled deps with a notice', async t => { + // Bundled deps cannot be allowlisted in Phase 1 (RFC defers their + // allowlisting to a follow-up). --all must not silently write a key + // derived from the bundled tarball's self-claimed identity. + const { npm, logs, prefix } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'parent-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'parent-pkg': '*' } }, + 'node_modules/parent-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/parent-pkg/-/parent-pkg-1.0.0.tgz', + hasInstallScript: true, + }, + 'node_modules/parent-pkg/node_modules/inner': { + version: '1.0.0', + inBundle: true, + hasInstallScript: true, + }, + }, + }), + node_modules: { + 'parent-pkg': { + 'package.json': JSON.stringify({ + name: 'parent-pkg', + version: '1.0.0', + scripts: { install: 'echo install' }, + bundleDependencies: ['inner'], + }), + node_modules: { + inner: { + 'package.json': JSON.stringify({ + name: 'inner', + version: '1.0.0', + scripts: { install: 'echo bundled-install' }, + }), + }, + }, + }, + }, + }, + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + // parent-pkg is approvable. inner is bundled and must be excluded. + t.equal(pkg.allowScripts['parent-pkg@1.0.0'], true, + 'non-bundled parent gets approved') + t.notOk(Object.keys(pkg.allowScripts).some(k => k.startsWith('inner')), + 'bundled inner is not approved') + t.match(logs.warn.byTitle('approve-scripts'), [/Skipping 1 bundled dependency/]) +}) + +t.test('approve-scripts positional is ignored', async t => { + // Same protection on the positional path: a user typing a bundled + // package name must not get a policy entry written. + const { npm } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'parent-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'parent-pkg': '*' } }, + 'node_modules/parent-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/parent-pkg/-/parent-pkg-1.0.0.tgz', + hasInstallScript: true, + }, + 'node_modules/parent-pkg/node_modules/inner': { + version: '1.0.0', + inBundle: true, + hasInstallScript: true, + }, + }, + }), + node_modules: { + 'parent-pkg': { + 'package.json': JSON.stringify({ + name: 'parent-pkg', + version: '1.0.0', + scripts: { install: 'echo install' }, + bundleDependencies: ['inner'], + }), + node_modules: { + inner: { + 'package.json': JSON.stringify({ + name: 'inner', + version: '1.0.0', + scripts: { install: 'echo bundled' }, + }), + }, + }, + }, + }, + }, + }) + await t.rejects( + npm.exec('approve-scripts', ['inner']), + { code: 'ENOMATCH' }, + 'typing the bundled package name does not match any approvable node' + ) +}) + +t.test('approve-scripts --all with only bundled deps prints "no eligible" notice', async t => { + const { npm, logs, joinedOutput, prefix } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { 'parent-pkg': '*' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { 'parent-pkg': '*' } }, + 'node_modules/parent-pkg': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/parent-pkg/-/parent-pkg-1.0.0.tgz', + // parent-pkg has NO install scripts; only the bundled child does. + }, + 'node_modules/parent-pkg/node_modules/only-bundled': { + version: '1.0.0', + inBundle: true, + hasInstallScript: true, + }, + }, + }), + node_modules: { + 'parent-pkg': { + 'package.json': JSON.stringify({ + name: 'parent-pkg', + version: '1.0.0', + bundleDependencies: ['only-bundled'], + }), + node_modules: { + 'only-bundled': { + 'package.json': JSON.stringify({ + name: 'only-bundled', + version: '1.0.0', + scripts: { install: 'echo evil' }, + }), + }, + }, + }, + }, + }, + config: { all: true }, + }) + await npm.exec('approve-scripts', []) + t.match(joinedOutput(), /No packages eligible for approval/) + t.match(logs.warn.byTitle('approve-scripts'), [/Skipping 1 bundled dependency/]) + // Ensure no policy entry was written. + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.notOk(pkg.allowScripts, 'no allowScripts written') +}) diff --git a/deps/npm/test/lib/commands/config.js b/deps/npm/test/lib/commands/config.js index 9a65e883cfebc1..8237ffff22a42a 100644 --- a/deps/npm/test/lib/commands/config.js +++ b/deps/npm/test/lib/commands/config.js @@ -582,6 +582,11 @@ t.test('config edit', async t => { }, }) + const inputEvents = [] + const inputListener = (level) => inputEvents.push(level) + process.on('input', inputListener) + t.teardown(() => process.off('input', inputListener)) + await npm.exec('config', ['edit']) t.ok(editor.called, 'editor was spawned') @@ -590,6 +595,7 @@ t.test('config edit', async t => { [join(home, '.npmrc')], 'editor opened the user config file' ) + t.same(inputEvents.slice(0, 2), ['start', 'end'], 'progress paused and resumed around editor') const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) t.ok(contents.includes('foo=bar'), 'kept foo') diff --git a/deps/npm/test/lib/commands/deny-scripts.js b/deps/npm/test/lib/commands/deny-scripts.js new file mode 100644 index 00000000000000..fd9031c665d6a8 --- /dev/null +++ b/deps/npm/test/lib/commands/deny-scripts.js @@ -0,0 +1,163 @@ +const t = require('tap') +const fs = require('node:fs') +const { resolve } = require('node:path') +const _mockNpm = require('../../fixtures/mock-npm') + +const setupProject = ({ allowScripts, withScripts = ['core-js'] } = {}) => { + const pkg = { + name: 'host', + version: '1.0.0', + dependencies: Object.fromEntries(withScripts.map((n) => [n, '*'])), + } + if (allowScripts !== undefined) { + pkg.allowScripts = allowScripts + } + const lockPackages = { '': pkg } + const nodeModules = {} + for (const name of withScripts) { + nodeModules[name] = { + 'package.json': JSON.stringify({ + name, + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + } + lockPackages[`node_modules/${name}`] = { + version: '1.0.0', + resolved: `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz`, + hasInstallScript: true, + } + } + return { + 'package.json': JSON.stringify(pkg, null, 2), + 'package-lock.json': JSON.stringify({ + name: pkg.name, + version: pkg.version, + lockfileVersion: 3, + requires: true, + packages: lockPackages, + }), + node_modules: nodeModules, + } +} + +t.test('deny-scripts writes name-only false entry', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + }) + await npm.exec('deny-scripts', ['core-js']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) +}) + +t.test('deny-scripts ignores --pin and always writes name-only', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + config: { 'allow-scripts-pin': true }, + }) + await npm.exec('deny-scripts', ['core-js']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) +}) + +t.test('deny-scripts replaces existing pinned allow', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['core-js'], + allowScripts: { 'core-js@1.0.0': true }, + }), + }) + await npm.exec('deny-scripts', ['core-js']) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) +}) + +t.test('deny-scripts --pending is rejected', async t => { + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + config: { 'allow-scripts-pending': true }, + }) + await t.rejects(npm.exec('deny-scripts', []), { code: 'EUSAGE' }) +}) + +t.test('deny-scripts --all denies every unreviewed package', async t => { + const { npm, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js', 'telemetry'] }), + config: { all: true }, + }) + await npm.exec('deny-scripts', []) + + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false, telemetry: false }) +}) + +t.test('deny-scripts errors on unknown package', async t => { + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + }) + await t.rejects( + npm.exec('deny-scripts', ['not-installed']), + { code: 'ENOMATCH' } + ) +}) + +t.test('deny-scripts requires positional args or --all', async t => { + const { npm } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + }) + await t.rejects(npm.exec('deny-scripts', []), { code: 'EUSAGE' }) +}) + +t.test('deny-scripts --all with no unreviewed packages prints message', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { '': { name: 'host', version: '1.0.0' } }, + }), + node_modules: {}, + }, + config: { all: true }, + }) + await npm.exec('deny-scripts', []) + t.match(joinedOutput(), /No packages with unreviewed install scripts/) +}) + +t.test('deny-scripts fails on global', async t => { + const { npm } = await _mockNpm(t, { + config: { global: true }, + }) + await t.rejects(npm.exec('deny-scripts', ['canvas']), { code: 'EGLOBAL' }) +}) + +t.test('deny-scripts on a package already denied is no-op', async t => { + const { npm, joinedOutput, prefix } = await _mockNpm(t, { + prefixDir: setupProject({ + withScripts: ['core-js'], + allowScripts: { 'core-js': false }, + }), + }) + await npm.exec('deny-scripts', ['core-js']) + const pkg = JSON.parse(fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')) + t.strictSame(pkg.allowScripts, { 'core-js': false }) + t.match(joinedOutput(), /Nothing to deny/) +}) + +t.test('deny-scripts --json outputs structured summary', async t => { + const { npm, joinedOutput } = await _mockNpm(t, { + prefixDir: setupProject({ withScripts: ['core-js'] }), + config: { json: true }, + }) + await npm.exec('deny-scripts', ['core-js']) + const parsed = JSON.parse(joinedOutput()) + t.match(parsed, { + allowScripts: [{ name: 'core-js', changes: [{ key: 'core-js', change: 'added' }] }], + }) +}) diff --git a/deps/npm/test/lib/commands/edit.js b/deps/npm/test/lib/commands/edit.js index b55bb2df218ba2..915241c82f6da8 100644 --- a/deps/npm/test/lib/commands/edit.js +++ b/deps/npm/test/lib/commands/edit.js @@ -58,8 +58,14 @@ t.test('npm edit', async t => { : ['-c', 'testinstall'] spawk.spawn(scriptShell, scriptArgs, { cwd: semverPath }) + const inputEvents = [] + const inputListener = (level) => inputEvents.push(level) + process.on('input', inputListener) + t.teardown(() => process.off('input', inputListener)) + await npm.exec('edit', ['semver']) t.match(joinedOutput(), 'rebuilt dependencies successfully') + t.same(inputEvents.slice(0, 2), ['start', 'end'], 'progress paused and resumed around editor') }) t.test('rebuild failure', async t => { diff --git a/deps/npm/test/lib/commands/exec.js b/deps/npm/test/lib/commands/exec.js index 2a6d3f6b8e0aff..92ea993e3edfb2 100644 --- a/deps/npm/test/lib/commands/exec.js +++ b/deps/npm/test/lib/commands/exec.js @@ -303,3 +303,68 @@ t.test('can run packages with keywords', async t => { t.fail(err, 'should not throw') } }) + +t.test('exec threads allowScripts policy from .npmrc through to libexec', async t => { + let capturedOpts + const fakeLibexec = async (opts) => { + capturedOpts = opts + } + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + '.npmrc': 'allow-scripts = canvas', + }, + mocks: { + libnpmexec: fakeLibexec, + }, + }) + await npm.exec('exec', ['some-pkg']) + t.strictSame(capturedOpts.allowScripts, { canvas: true }, + 'allowScripts populated from .npmrc layer') +}) + +t.test('exec ignores project package.json#allowScripts (RFC: .npmrc-only)', async t => { + // Per RFC line 299, exec/npx consults only user/global .npmrc. Project + // package.json policy must NOT influence npx behaviour, even when the + // user is running npx inside a project that has its own allowScripts. + let capturedOpts + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + allowScripts: { sharp: true }, + }), + }, + mocks: { + libnpmexec: async (opts) => { + capturedOpts = opts + }, + }, + }) + await npm.exec('exec', ['some-pkg']) + // package.json policy is skipped; no other layer has policy; result is null. + t.equal(capturedOpts.allowScripts, null) +}) + +t.test('exec reads .npmrc policy even when project package.json has a different policy', async t => { + // .npmrc-tier policy wins because package.json is skipped entirely. + let capturedOpts + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + mocks: { + libnpmexec: async (opts) => { + capturedOpts = opts + }, + }, + }) + await npm.exec('exec', ['some-pkg']) + t.strictSame(capturedOpts.allowScripts, { canvas: true }) +}) diff --git a/deps/npm/test/lib/commands/publish.js b/deps/npm/test/lib/commands/publish.js index ad528c2c8dd3ef..acf8c4c96a93d6 100644 --- a/deps/npm/test/lib/commands/publish.js +++ b/deps/npm/test/lib/commands/publish.js @@ -742,6 +742,27 @@ t.test('restricted access', async t => { t.matchSnapshot(logs.notice) }) +t.test('private access', async t => { + const packageJson = { + name: '@npm/test-package', + version: '1.0.0', + } + const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { + config: { + ...auth, + access: 'private', + }, + prefixDir: { + 'package.json': JSON.stringify(packageJson, null, 2), + }, + authorization: token, + }) + registry.publish('@npm/test-package', { packageJson, access: 'restricted' }) + await npm.exec('publish', []) + t.matchSnapshot(joinedOutput(), 'new package version') + t.matchSnapshot(logs.notice) +}) + t.test('public access', async t => { const { npm, joinedOutput, logs, registry } = await loadNpmWithRegistry(t, { config: { diff --git a/deps/npm/test/lib/commands/rebuild.js b/deps/npm/test/lib/commands/rebuild.js index 0062362b61329b..de91fd3471b4e1 100644 --- a/deps/npm/test/lib/commands/rebuild.js +++ b/deps/npm/test/lib/commands/rebuild.js @@ -221,3 +221,63 @@ t.test('completion', async t => { const res = await rebuild.completion({ conf: { argv: { remain: ['npm', 'rebuild'] } } }) t.type(res, Array) }) + +t.test('emits Phase 1 advisory warning for unreviewed install scripts', async t => { + const { npm, logs } = await setupMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }), + node_modules: { + canvas: { + 'package.json': JSON.stringify({ + name: 'canvas', + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + }, + }, + }, + }) + await npm.exec('rebuild', []) + t.match( + logs.warn.byTitle('rebuild'), + [/install scripts not yet covered by allowScripts/] + ) +}) + +t.test('no advisory warning when allowScripts covers the package', async t => { + const { npm, logs } = await setupMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + dependencies: { canvas: '1.0.0' }, + allowScripts: { canvas: true }, + }), + 'package-lock.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { name: 'host', version: '1.0.0', dependencies: { canvas: '1.0.0' } }, + 'node_modules/canvas': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/canvas/-/canvas-1.0.0.tgz', + hasInstallScript: true, + }, + }, + }), + node_modules: { + canvas: { + 'package.json': JSON.stringify({ + name: 'canvas', + version: '1.0.0', + scripts: { install: 'echo install' }, + }), + }, + }, + }, + }) + await npm.exec('rebuild', []) + t.strictSame(logs.warn.byTitle('rebuild'), []) +}) diff --git a/deps/npm/test/lib/commands/update.js b/deps/npm/test/lib/commands/update.js index a8c68bd65bb361..68067b8af8168f 100644 --- a/deps/npm/test/lib/commands/update.js +++ b/deps/npm/test/lib/commands/update.js @@ -95,3 +95,33 @@ t.test('completion', async t => { const res = await update.completion({ conf: { argv: { remain: ['npm', 'update'] } } }) t.type(res, Array) }) + +t.test('update threads allowScripts policy through to arborist', async t => { + // The reify step uses the resolved policy. The advisory warning is + // emitted from reifyFinish (already covered by install.js tests), + // so here we verify the call site populates opts.allowScripts. + let capturedOpts + const FakeArborist = function (opts) { + capturedOpts = opts + this.options = opts + this.actualTree = { inventory: new Map() } + } + FakeArborist.prototype.reify = async function () {} + + const mock = await _mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'host', + version: '1.0.0', + allowScripts: { canvas: true }, + }), + }, + mocks: { + '@npmcli/arborist': FakeArborist, + '{LIB}/utils/reify-finish.js': async () => {}, + }, + }) + await mock.npm.exec('update', []) + t.strictSame(capturedOpts.allowScripts, { canvas: true }, + 'opts.allowScripts populated from package.json') +}) diff --git a/deps/npm/test/lib/utils/allow-scripts-writer.js b/deps/npm/test/lib/utils/allow-scripts-writer.js new file mode 100644 index 00000000000000..56314f8eb5a521 --- /dev/null +++ b/deps/npm/test/lib/utils/allow-scripts-writer.js @@ -0,0 +1,637 @@ +const t = require('tap') +const path = require('node:path') +const { + applyApprovalForPackage, + applyDenyForPackage, + nameKeyFor, + versionedKeyFor, + isSingleVersionPin, +} = require('../../../lib/utils/allow-scripts-writer.js') + +const node = (overrides = {}) => { + const name = overrides.name ?? overrides.packageName ?? 'pkg' + const packageName = overrides.packageName ?? name + const version = overrides.version ?? '1.0.0' + const urlPkg = packageName + return { + name, + packageName, + version, + resolved: overrides.resolved + ?? `https://registry.npmjs.org/${urlPkg}/-/${urlPkg}-${version}.tgz`, + location: overrides.location ?? `node_modules/${name}`, + isRegistryDependency: overrides.isRegistryDependency ?? true, + } +} + +t.test('nameKeyFor / versionedKeyFor — registry', async t => { + const n = node({ name: 'canvas', version: '2.11.0' }) + t.equal(nameKeyFor(n), 'canvas') + t.equal(versionedKeyFor(n), 'canvas@2.11.0') +}) + +t.test('nameKeyFor / versionedKeyFor — git', async t => { + const n = node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#deadbeefcafebabe1234567890abcdef12345678', + }) + t.equal(nameKeyFor(n), 'github:foo/bar') + t.equal(versionedKeyFor(n), 'github:foo/bar#deadbeefcafebabe1234567890abcdef12345678') +}) + +t.test('nameKeyFor / versionedKeyFor — file', async t => { + const n = node({ name: 'local', resolved: 'file:../local' }) + t.equal(nameKeyFor(n), 'file:../local') + t.equal(versionedKeyFor(n), 'file:../local') +}) + +t.test('isSingleVersionPin', async t => { + t.ok(isSingleVersionPin('pkg@1.2.3')) + t.notOk(isSingleVersionPin('pkg')) + t.notOk(isSingleVersionPin('pkg@^1')) + t.notOk(isSingleVersionPin('pkg@1.2.3 || 2.0.0')) + t.notOk(isSingleVersionPin('@@@bad')) +}) + +t.test('applyApprovalForPackage — empty allowScripts, --pin', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + {}, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(changes, [{ key: 'canvas@2.11.0', change: 'added' }]) +}) + +t.test('applyApprovalForPackage — empty allowScripts, --no-pin', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + {}, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) + t.strictSame(changes, [{ key: 'canvas', change: 'added' }]) +}) + +t.test('applyApprovalForPackage — stale pin rewritten to new installed version', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { 'canvas@2.10.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.match(changes, [ + { key: 'canvas@2.10.0', change: 'removed-stale' }, + { key: 'canvas@2.11.0', change: 'added' }, + ]) +}) + +t.test('applyApprovalForPackage — multi-version disjunction is preserved', async t => { + const { allowScripts } = applyApprovalForPackage( + { 'canvas@2.10.0 || 2.11.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { + 'canvas@2.10.0 || 2.11.0': true, + 'canvas@2.11.0': true, + }) +}) + +t.test('applyApprovalForPackage — already-allowed exact version is a no-op', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { 'canvas@2.11.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — existing deny wins, returns warning', async t => { + const { allowScripts, changes, warning } = applyApprovalForPackage( + { canvas: false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { canvas: false }) + t.strictSame(changes, []) + t.match(warning, /canvas is denied/) +}) + +t.test('applyApprovalForPackage — versioned deny wins too', async t => { + const { changes, warning } = applyApprovalForPackage( + { 'canvas@2.11.0': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(changes, []) + t.match(warning, /denied|versioned deny/) +}) + +t.test('applyApprovalForPackage — name-only existing, --no-pin no-op', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { canvas: true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — --no-pin downgrades pinned entry to name-only', async t => { + const { allowScripts } = applyApprovalForPackage( + { 'canvas@2.10.0': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) +}) + +t.test('applyApprovalForPackage — multiple installed versions write multiple pins', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [ + node({ name: 'lodash', version: '4.17.21' }), + node({ name: 'lodash', version: '3.10.1' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) +}) + +t.test('applyApprovalForPackage — keeps existing pin matching one installed, adds pin for other', async t => { + const { allowScripts } = applyApprovalForPackage( + { 'lodash@4.17.21': true }, + [ + node({ name: 'lodash', version: '4.17.21' }), + node({ name: 'lodash', version: '3.10.1' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) +}) + +t.test('applyDenyForPackage — empty allowScripts adds name-only false', async t => { + const { allowScripts, changes } = applyDenyForPackage( + {}, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(allowScripts, { 'core-js': false }) + t.strictSame(changes, [{ key: 'core-js', change: 'added' }]) +}) + +t.test('applyDenyForPackage — pinned allow is replaced by name-only deny', async t => { + const { allowScripts } = applyDenyForPackage( + { 'core-js@3.0.0': true }, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(allowScripts, { 'core-js': false }) +}) + +t.test('applyDenyForPackage — already-denied is a no-op', async t => { + const { changes } = applyDenyForPackage( + { 'core-js': false }, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(changes, []) +}) + +t.test('applyDenyForPackage — name-only true is replaced by name-only false', async t => { + const { allowScripts } = applyDenyForPackage( + { 'core-js': true }, + [node({ name: 'core-js', version: '3.0.0' })] + ) + t.strictSame(allowScripts, { 'core-js': false }) +}) + +t.test('applyApprovalForPackage — preserves unrelated entries', async t => { + const { allowScripts } = applyApprovalForPackage( + { other: true, 'unrelated@1.0.0': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.strictSame(allowScripts, { + other: true, + 'unrelated@1.0.0': false, + 'canvas@2.11.0': true, + }) +}) + +t.test('applyApprovalForPackage — git node writes hosted shortcut with commit', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#deadbeefcafebabe1234567890abcdef12345678', + })], + { pin: true } + ) + t.strictSame(allowScripts, { + 'github:foo/bar#deadbeefcafebabe1234567890abcdef12345678': true, + }) +}) + +t.test('applyApprovalForPackage — git node --no-pin writes hosted shortcut without commit', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#deadbeef', + })], + { pin: false } + ) + t.strictSame(allowScripts, { 'github:foo/bar': true }) +}) + +t.test('applyApprovalForPackage — file dep uses resolved as both keys', async t => { + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ name: 'local', resolved: 'file:../local' })], + { pin: true } + ) + t.strictSame(allowScripts, { 'file:../local': true }) +}) + +t.test('applyApprovalForPackage — empty nodes returns unchanged', async t => { + const { allowScripts, changes } = applyApprovalForPackage({ x: true }, [], { pin: true }) + t.strictSame(allowScripts, { x: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — name-only entry is replaced by pin (RFC table)', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { canvas: true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + // Per RFC table: pkg: true + --pin must upgrade to pkg@x.y.z: true. + // Both entries left behind would be wrong. + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) + t.match(changes, [ + { key: 'canvas@2.11.0', change: 'added' }, + { key: 'canvas', change: 'replaced-by-pin' }, + ]) +}) + +t.test('applyApprovalForPackage — name-only + multi-version installs replaces with all pins', async t => { + const { allowScripts } = applyApprovalForPackage( + { lodash: true }, + [ + node({ name: 'lodash', version: '4.17.21' }), + node({ name: 'lodash', version: '3.10.1' }), + ], + { pin: true } + ) + t.strictSame(allowScripts, { 'lodash@3.10.1': true, 'lodash@4.17.21': true }) +}) + +t.test('applyApprovalForPackage — name-only is preserved when --no-pin', async t => { + const { allowScripts, changes } = applyApprovalForPackage( + { canvas: true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { canvas: true }) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — name-only NOT dropped when no pinning could happen', async t => { + // Node has no version, so installedKeys is empty. The name-only entry + // must NOT be dropped or we silently lose the policy. + const noVersion = { name: 'pkg', packageName: 'pkg', version: undefined, resolved: 'https://registry.npmjs.org/pkg/-/pkg-1.tgz' } + const { allowScripts } = applyApprovalForPackage( + { pkg: true }, + [noVersion], + { pin: true } + ) + t.strictSame(allowScripts, { pkg: true }) +}) + +t.test('applyApprovalForPackage — convergent: running twice gives the same result', async t => { + // Start with stale state including a name-only entry. + const start = { canvas: true, 'canvas@2.10.0': true } + const nodes = [node({ name: 'canvas', version: '2.11.0' })] + + const run1 = applyApprovalForPackage(start, nodes, { pin: true }) + const run2 = applyApprovalForPackage(run1.allowScripts, nodes, { pin: true }) + + t.strictSame(run1.allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(run2.allowScripts, { 'canvas@2.11.0': true }) + t.strictSame(run2.changes, [], 'second run is a no-op') +}) + +t.test('applyApprovalForPackage — deny still wins even when name-only is upgraded', async t => { + const { allowScripts, warning } = applyApprovalForPackage( + { canvas: true, 'canvas@2.11.0': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + // Existing deny on the version blocks the approval. + t.strictSame(allowScripts, { canvas: true, 'canvas@2.11.0': false }) + t.match(warning, /denied|versioned deny/) +}) + +t.test('keyTargetsNode — unparseable key returns false (via applyApproval)', async t => { + // An unparseable key in the existing object should be ignored. + const { allowScripts } = applyApprovalForPackage( + { '@@@invalid': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.equal(allowScripts['canvas@2.11.0'], true) + t.equal(allowScripts['@@@invalid'], true) +}) + +t.test('applyDenyForPackage — empty nodes array returns unchanged', async t => { + const { allowScripts, changes } = applyDenyForPackage({ existing: true }, []) + t.strictSame(allowScripts, { existing: true }) + t.strictSame(changes, []) +}) + +t.test('applyDenyForPackage — node with no nameable identity is a no-op', async t => { + // A node whose resolved field is unparseable as a git URL and has no + // version/name produces a null name; the writer must short-circuit. + const weird = { name: '', packageName: '', version: undefined, resolved: undefined } + const { allowScripts, changes } = applyDenyForPackage({}, [weird]) + t.strictSame(allowScripts, {}) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — file dep with deny entry blocks approval', async t => { + const { warning } = applyApprovalForPackage( + { 'file:../local': false }, + [node({ name: 'local', resolved: 'file:../local' })], + { pin: true } + ) + t.match(warning, /denied|versioned deny/) +}) + +t.test('applyApprovalForPackage — remote tarball deny blocks approval', async t => { + const remote = { name: 'pkg', packageName: 'pkg', version: '1.0.0', resolved: 'https://example.com/pkg.tgz' } + const { warning } = applyApprovalForPackage( + { 'https://example.com/pkg.tgz': false }, + [remote], + { pin: true } + ) + t.match(warning, /denied|versioned deny/) +}) + +t.test('applyApprovalForPackage — no-pin with no name produces no-op', async t => { + const weird = { name: '', packageName: '', resolved: 'git+ssh://no.parse' } + const { allowScripts, changes } = applyApprovalForPackage({}, [weird], { pin: false }) + t.strictSame(allowScripts, {}) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — pin with no versioned key is a no-op', async t => { + const noVersion = { name: 'pkg', packageName: 'pkg', version: undefined, resolved: 'https://registry.npmjs.org/pkg/-/pkg-1.tgz' } + const { allowScripts, changes } = applyApprovalForPackage({}, [noVersion], { pin: true }) + t.strictSame(allowScripts, {}) + t.strictSame(changes, []) +}) + +t.test('applyApprovalForPackage — pin with no versioned key and existing name-only is no-op', async t => { + const noVersion = { name: 'pkg', packageName: 'pkg', version: undefined, resolved: 'https://registry.npmjs.org/pkg/-/pkg-1.tgz' } + const { changes } = applyApprovalForPackage({ pkg: true }, [noVersion], { pin: true }) + t.strictSame(changes, []) +}) + +t.test('keyTargetsNode handles file with directory-typed key', async t => { + // A "directory" spec for a relative path. + const dirNode = { name: 'local', packageName: 'local', resolved: 'file:./local-dir' } + const { allowScripts } = applyApprovalForPackage( + {}, + [dirNode], + { pin: true } + ) + t.equal(allowScripts['file:./local-dir'], true) +}) + +t.test('nameKeyFor / versionedKeyFor — null node', async t => { + t.equal(nameKeyFor(null), null) + t.equal(versionedKeyFor(null), null) +}) + +t.test('nameKeyFor / versionedKeyFor — non-hosted git url returns null', async t => { + const n = { name: 'pkg', packageName: 'pkg', resolved: 'git+https://example.invalid/foo/bar.git#abc' } + t.equal(nameKeyFor(n), null) + t.equal(versionedKeyFor(n), null) +}) + +t.test('versionedKeyFor — absolute path resolved field', async t => { + const n = { name: 'pkg', packageName: 'pkg', resolved: '/abs/path/local' } + t.equal(versionedKeyFor(n), '/abs/path/local') + t.equal(nameKeyFor(n), '/abs/path/local') +}) + +t.test('applyApprovalForPackage — node.resolved parse error in keyTargetsNode is safe', async t => { + // An existing git-style key for a package whose own resolved field + // doesn't parse: the key just doesn't target anything. + const gitNode = node({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git#abc', + }) + // Add an explicit unparseable existing entry. + const { allowScripts } = applyApprovalForPackage( + { 'github:other/other': true }, + [gitNode], + { pin: true } + ) + // Existing entry unchanged; new git entry added. + t.equal(allowScripts['github:other/other'], true) + t.equal(allowScripts['github:foo/bar#abc'], true) +}) + +t.test('keyTargetsNode — alias key does not target anything (via writer)', async t => { + // Alias-typed key falls through the switch default. + const { allowScripts } = applyApprovalForPackage( + { 'foo@npm:bar@1.0.0': true }, + [node({ name: 'foo', packageName: 'foo', version: '1.0.0' })], + { pin: true } + ) + // Alias entry untouched, new pin added separately. + t.equal(allowScripts['foo@npm:bar@1.0.0'], true) + t.equal(allowScripts['foo@1.0.0'], true) +}) +t.test('keyTargetsNode handles tag-type key', async t => { + // 'canvas@latest' parses as type='tag'. The writer should treat it like + // a name-only match (any installed version of canvas). + const { allowScripts } = applyApprovalForPackage( + { 'canvas@latest': true }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + // The tag key targets the canvas node (same package name), so the + // 'canvas@2.11.0' pin gets added; tag key is preserved. + t.equal(allowScripts['canvas@latest'], true) + t.equal(allowScripts['canvas@2.11.0'], true) +}) + +t.test('keyTargetsNode handles file-type tarball key matching saveSpec', async t => { + // 'file:pkg.tgz' parses as type='file' with saveSpec='file:pkg.tgz'. + const tarballNode = { + name: 'pkg', + packageName: 'pkg', + version: '1.0.0', + resolved: 'file:pkg.tgz', + } + const { allowScripts } = applyApprovalForPackage( + { 'file:pkg.tgz': false }, + [tarballNode], + { pin: true } + ) + // saveSpec match: deny wins, no pin added. + t.equal(allowScripts['file:pkg.tgz'], false) +}) + +t.test('keyTargetsNode handles file-type tarball key matching fetchSpec', async t => { + // When node.resolved is an absolute path matching parsed.fetchSpec. + // Use path.resolve so the absolute path is platform-correct (npa + // parses POSIX-style `/abs/...` as a directory on Windows). + const absTgz = path.resolve('pkg.tgz') + const tarballNode = { + name: 'pkg', + packageName: 'pkg', + version: '1.0.0', + resolved: absTgz, + } + const { allowScripts, warning } = applyApprovalForPackage( + { './pkg.tgz': false }, + [tarballNode], + { pin: true } + ) + t.equal(allowScripts['./pkg.tgz'], false) + t.match(warning, /denied|versioned deny/) +}) + +t.test('versionedKeyFor — git node without committish', async t => { + // versionedKeyFor's ternary takes the "no committish" branch. + t.equal( + versionedKeyFor({ + name: 'bar', + resolved: 'git+ssh://git@github.com/foo/bar.git', + }), + 'github:foo/bar' + ) +}) + +t.test('versionedKeyFor / nameKeyFor — absolute path resolved field', async t => { + // Hits the `resolved.startsWith('/')` branch in both helpers. + const n = { name: 'pkg', packageName: 'pkg', resolved: '/abs/local-dir' } + t.equal(versionedKeyFor(n), '/abs/local-dir') + t.equal(nameKeyFor(n), '/abs/local-dir') +}) + +t.test('keyTargetsNode — git key against a node with no resolved field', async t => { + // Defensive: if existing has a git-shaped key and the installed node + // has no resolved field, keyTargetsNode bails out and no policy entry + // can be derived from untrusted sources. + const noResolved = { name: 'bar', packageName: 'bar', resolved: undefined } + const { allowScripts } = applyApprovalForPackage( + { 'github:foo/bar': true }, + [noResolved], + { pin: false } + ) + // Existing entry untouched. No new key written: nameKeyFor returns + // null for a node with no trusted identity source. + t.equal(allowScripts['github:foo/bar'], true) + t.notOk('bar' in allowScripts, 'no entry written under attacker-controlled node.name') +}) + +t.test('applyApprovalForPackage — default args (no options object)', async t => { + // Hits the `{ pin = true } = {}` default-arg branch. + const { allowScripts } = applyApprovalForPackage( + {}, + [node({ name: 'canvas', version: '2.11.0' })] + ) + t.strictSame(allowScripts, { 'canvas@2.11.0': true }) +}) + +t.test('applyApprovalForPackage — deny-wins warning when node has no name', async t => { + // Hits the `name || 'this package'` fallback in the warning message. + const noName = { name: '', packageName: '', resolved: 'git+ssh://no.parse' } + const { warning } = applyApprovalForPackage( + { 'github:foo/bar': false }, + [noName], + { pin: true } + ) + // No keys target this node (its resolved doesn't parse to a hosted URL), + // so deny-wins doesn't trigger. Result is no warning. + t.notOk(warning) +}) + +t.test('denyWarning branches on key shape per RFC §approve-scripts', async t => { + // Name-only deny: only remedy is to remove the entry. + const nameOnly = applyApprovalForPackage( + { canvas: false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.match(nameOnly.warning, /remove the entry from allowScripts/) + t.notMatch(nameOnly.warning, /widen the deny/) + + // Pinned deny on a different version: suggest both widen and remove. + const pinned = applyApprovalForPackage( + { 'canvas@2.10.0': false }, + [node({ name: 'canvas', version: '2.10.0' })], + { pin: true } + ) + t.match(pinned.warning, /versioned deny/) + t.match(pinned.warning, /npm deny-scripts canvas/) + t.match(pinned.warning, /widen the deny to all versions/) + t.match(pinned.warning, /remove the entry/) + + // Multi-version deny disjunction: same as pinned (versioned). + const multi = applyApprovalForPackage( + { 'canvas@2.10.0 || 2.11.0': false }, + [node({ name: 'canvas', version: '2.10.0' })], + { pin: true } + ) + t.match(multi.warning, /versioned deny/) + t.match(multi.warning, /npm deny-scripts canvas/) +}) + +t.test('denyWarning: tag-type key (pkg@latest: false) is name-only', async t => { + // `canvas@latest` parses as type='tag'. Treat the same as a bare name. + const { warning } = applyApprovalForPackage( + { 'canvas@latest': false }, + [node({ name: 'canvas', version: '2.11.0' })], + { pin: true } + ) + t.match(warning, /remove the entry/) + t.notMatch(warning, /versioned deny/) +}) + +t.test('applyApprovalForPackage — multi-version entry + --pin=false adds name-only alongside', async t => { + // RFC table: existing `pkg@a.b.c || d.e.f: true` + installed `pkg@x.y.z` + // + --pin=false adds `pkg: true`. The multi-version disjunction stays + // (it captures intent the command can't infer), and the name-only + // entry is added. + const { allowScripts } = applyApprovalForPackage( + { 'canvas@1.0.0 || 2.0.0': true }, + [node({ name: 'canvas', version: '3.0.0' })], + { pin: false } + ) + t.strictSame(allowScripts, { + 'canvas@1.0.0 || 2.0.0': true, + canvas: true, + }) +}) + +t.test('versionedKeyFor — registry resolved that versionFromTgz cannot parse returns null', async t => { + // Private-registry mirror / alternate CDN URL shape that doesn't match + // the standard `/-/name-version.tgz` pattern. Exercises the log.silly + // breadcrumb path in versionedKeyFor, including each fallback branch + // of the `node.path || node.name || ''` label expression. + const resolved = 'https://private-mirror.example.com/blobs/abc123' + t.equal(versionedKeyFor({ + path: '/fake/mystery', name: 'mystery', resolved, isRegistryDependency: true, + }), null, 'falls back when node has a path') + t.equal(versionedKeyFor({ + name: 'mystery', resolved, isRegistryDependency: true, + }), null, 'falls back when node has only a name') + t.equal(versionedKeyFor({ + resolved, isRegistryDependency: true, + }), null, 'falls back when node has neither path nor name') +}) diff --git a/deps/npm/test/lib/utils/check-allow-scripts.js b/deps/npm/test/lib/utils/check-allow-scripts.js new file mode 100644 index 00000000000000..8dea9674375df4 --- /dev/null +++ b/deps/npm/test/lib/utils/check-allow-scripts.js @@ -0,0 +1,263 @@ +const t = require('tap') + +const mockCheck = (t, mocks = {}) => + t.mock('../../../lib/utils/check-allow-scripts.js', mocks) + +// Build a minimal "arborist tree" fixture for the walker. +const arb = ({ nodes, allowScripts = null, ignoreScripts = false } = {}) => ({ + options: { allowScripts, ignoreScripts }, + actualTree: { + inventory: new Map(nodes.map((n, i) => [`node_modules/${n.name || `n${i}`}`, n])), + }, +}) + +const node = ({ + name = 'pkg', + packageName, + version = '1.0.0', + resolved, + scripts = {}, + gypfile, + path: nodePath = `/fake/${name}`, + isProjectRoot = false, + isWorkspace = false, + isLink = false, + isRegistryDependency, +} = {}) => { + const pkgName = packageName ?? name + const resolvedUrl = resolved + ?? `https://registry.npmjs.org/${pkgName}/-/${pkgName}-${version}.tgz` + // Default isRegistryDependency to match the shape of resolved: registry + // tarballs are registry, anything else (git, file, remote) is not. + const isReg = isRegistryDependency ?? /^https?:\/\/[^/]+\/.+\/-\/[^/]+-\d/.test(resolvedUrl) + return { + name, + packageName: pkgName, + version, + resolved: resolvedUrl, + location: `node_modules/${name}`, + isRegistryDependency: isReg, + path: nodePath, + isProjectRoot, + isWorkspace, + isLink, + package: { scripts, ...(gypfile !== undefined ? { gypfile } : {}) }, + } +} + +t.test('returns [] when ignoreScripts is set', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ scripts: { install: 'do-stuff' } })], + ignoreScripts: true, + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('returns [] when dangerouslyAllowAllScripts is set', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ nodes: [node({ scripts: { install: 'do-stuff' } })] }), + npm: { flatOptions: { dangerouslyAllowAllScripts: true } }, + }) + t.strictSame(result, []) +}) + +t.test('skips project root, workspace, and linked nodes', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'root', scripts: { install: 'x' }, isProjectRoot: true }), + node({ name: 'ws', scripts: { install: 'x' }, isWorkspace: true }), + node({ name: 'linked', scripts: { install: 'x' }, isLink: true }), + ], + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('skips nodes with no install-relevant scripts', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ scripts: { test: 'jest' } })], + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('includes nodes with preinstall/install/postinstall', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'a', scripts: { preinstall: 'pre' } }), + node({ name: 'b', scripts: { install: 'inst' } }), + node({ name: 'c', scripts: { postinstall: 'post' } }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 3) + t.strictSame(result[0].scripts, { preinstall: 'pre' }) + t.strictSame(result[1].scripts, { install: 'inst' }) + t.strictSame(result[2].scripts, { postinstall: 'post' }) +}) + +t.test('prepare counts for non-registry sources only', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + // registry: prepare ignored + node({ + name: 'registry-pkg', + resolved: 'https://registry.npmjs.org/registry-pkg/-/registry-pkg-1.0.0.tgz', + scripts: { prepare: 'do' }, + }), + // git: prepare counts + node({ + name: 'git-pkg', + resolved: 'git+ssh://git@github.com/foo/bar.git#abcdef0123456789', + scripts: { prepare: 'do' }, + }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1) + t.equal(result[0].node.name, 'git-pkg') +}) + +t.test('detects synthetic node-gyp via binding.gyp runtime check', async t => { + const checkAllowScripts = mockCheck(t, { + '@npmcli/arborist/lib/install-scripts.js': async (n) => { + if (n.path === '/has-bindings') { + return { install: 'node-gyp rebuild' } + } + return {} + }, + }) + + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'native', path: '/has-bindings' }), + node({ name: 'pure-js', path: '/no-bindings' }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1) + t.equal(result[0].node.name, 'native') + t.strictSame(result[0].scripts, { install: 'node-gyp rebuild' }) +}) + +t.test('skips node-gyp detection when gypfile is explicitly false', async t => { + // Mock returns no scripts to simulate the gypfile:false short-circuit + // inside getInstallScripts. + const checkAllowScripts = mockCheck(t, { + '@npmcli/arborist/lib/install-scripts.js': async () => ({}), + }) + + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ name: 'opt-out', gypfile: false })], + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('skips approved nodes', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ name: 'allowed', scripts: { install: 'x' } })], + allowScripts: { allowed: true }, + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('skips denied nodes (false counts as reviewed)', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [node({ name: 'denied', scripts: { install: 'x' } })], + allowScripts: { denied: false }, + }), + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('includes unreviewed nodes when policy is set but does not cover them', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'allowed', scripts: { install: 'x' } }), + node({ name: 'unreviewed', scripts: { install: 'y' } }), + ], + allowScripts: { allowed: true }, + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1) + t.equal(result[0].node.name, 'unreviewed') +}) + +t.test('reports every install-script node when no policy is set', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: arb({ + nodes: [ + node({ name: 'a', scripts: { install: 'x' } }), + node({ name: 'b', scripts: { postinstall: 'y' } }), + ], + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 2) +}) + +t.test('survives missing actualTree', async t => { + const checkAllowScripts = mockCheck(t) + const result = await checkAllowScripts({ + arb: { options: {} }, + npm: { flatOptions: {} }, + }) + t.strictSame(result, []) +}) + +t.test('bundled dep with install scripts is reported as unreviewed regardless of policy', async t => { + const checkAllowScripts = mockCheck(t) + const bundled = node({ + name: 'bundled-pkg', + version: '1.0.0', + resolved: undefined, + scripts: { install: 'do-stuff' }, + }) + bundled.inBundle = true + + const result = await checkAllowScripts({ + arb: arb({ + nodes: [bundled], + // Policy explicitly allows the bundled name — the matcher should + // still return null and the walker should still flag the bundled + // dep as unreviewed. + allowScripts: { 'bundled-pkg': true }, + }), + npm: { flatOptions: {} }, + }) + t.equal(result.length, 1, 'bundled dep flagged despite explicit allow entry') + t.equal(result[0].node, bundled) +}) diff --git a/deps/npm/test/lib/utils/reify-output.js b/deps/npm/test/lib/utils/reify-output.js index 134951e40aabd1..b1bc92b1c77aed 100644 --- a/deps/npm/test/lib/utils/reify-output.js +++ b/deps/npm/test/lib/utils/reify-output.js @@ -448,3 +448,114 @@ t.test('prints dedupe difference on long', async t => { t.matchSnapshot(out, 'diff table') }) + +t.test('prints unreviewed install scripts summary', async t => { + const mockReifyWithExtras = async (t, reify, extras, { command, ...config } = {}) => { + const mock = await mockNpm(t, { command, config }) + Object.defineProperty(mock.npm, 'command', { + get () { + return command + }, + enumerable: true, + }) + reifyOutput(mock.npm, reify, extras) + mock.npm.finish() + return mock + } + + const baseReify = { + actualTree: { name: 'host', inventory: { has: () => false } }, + diff: { children: [] }, + } + + const unreviewedScripts = [ + { + node: { packageName: 'canvas', name: 'canvas', version: '2.11.0', path: '/x/canvas' }, + scripts: { install: 'node-gyp rebuild' }, + }, + { + node: { packageName: 'sharp', name: 'sharp', version: '0.33.2', path: '/x/sharp' }, + scripts: { preinstall: 'pre', postinstall: 'post' }, + }, + ] + + const mock = await mockReifyWithExtras(t, baseReify, { unreviewedScripts }) + const warn = mock.logs.warn.byTitle('allow-scripts').join('\n') + t.match(warn, /2 packages have install scripts not yet covered/) + t.match(warn, /canvas@2\.11\.0 \(install: node-gyp rebuild\)/) + t.match(warn, /sharp@0\.33\.2 \(preinstall: pre; postinstall: post\)/) + t.match(warn, /npm approve-scripts --allow-scripts-pending/) +}) + +t.test('single unreviewed script uses singular wording', async t => { + const mockReifyWithExtras = async (t, reify, extras) => { + const mock = await mockNpm(t, {}) + reifyOutput(mock.npm, reify, extras) + mock.npm.finish() + return mock + } + + const mock = await mockReifyWithExtras( + t, + { actualTree: { inventory: { has: () => false } }, diff: { children: [] } }, + { + unreviewedScripts: [{ + node: { packageName: 'one', name: 'one', version: '1.0.0', path: '/x' }, + scripts: { install: 'do' }, + }], + } + ) + t.match(mock.logs.warn.byTitle('allow-scripts').join('\n'), /1 package has install scripts/) +}) + +t.test('json output includes unreviewedScripts', async t => { + const mock = await mockNpm(t, { config: { json: true } }) + reifyOutput(mock.npm, { + actualTree: { inventory: { size: 0 } }, + diff: null, + }, { + unreviewedScripts: [{ + node: { packageName: 'pkg', name: 'pkg', version: '1.0.0', path: '/x' }, + scripts: { install: 'cmd' }, + }], + }) + mock.npm.finish() + const parsed = JSON.parse(mock.joinedOutput()) + t.match(parsed.unreviewedScripts, [{ + name: 'pkg', + version: '1.0.0', + path: '/x', + scripts: { install: 'cmd' }, + }]) +}) + +t.test('unreviewed script with node.name only (no packageName) still renders', async t => { + const mock = await mockNpm(t, {}) + reifyOutput(mock.npm, { + actualTree: { inventory: { has: () => false } }, + diff: { children: [] }, + }, { + unreviewedScripts: [{ + node: { name: 'fallback', path: '/x' }, // no packageName, no version + scripts: { install: 'cmd' }, + }], + }) + mock.npm.finish() + t.match(mock.logs.warn.byTitle('allow-scripts').join('\n'), / fallback \(install: cmd\)/) +}) + +t.test('json output includes node.name when packageName is missing', async t => { + const mock = await mockNpm(t, { config: { json: true } }) + reifyOutput(mock.npm, { + actualTree: { inventory: { size: 0 } }, + diff: null, + }, { + unreviewedScripts: [{ + node: { name: 'fallback', path: '/x' }, + scripts: { install: 'cmd' }, + }], + }) + mock.npm.finish() + const parsed = JSON.parse(mock.joinedOutput()) + t.equal(parsed.unreviewedScripts[0].name, 'fallback') +}) diff --git a/deps/npm/test/lib/utils/resolve-allow-scripts.js b/deps/npm/test/lib/utils/resolve-allow-scripts.js new file mode 100644 index 00000000000000..0d6cdb8c040ac9 --- /dev/null +++ b/deps/npm/test/lib/utils/resolve-allow-scripts.js @@ -0,0 +1,347 @@ +const t = require('tap') +const mockNpm = require('../../fixtures/mock-npm') +const tmock = require('../../fixtures/tmock') + +const loadResolver = (t) => tmock(t, '{LIB}/utils/resolve-allow-scripts.js') + +// Helper that simulates config layering. `cliConfig` sets the value at +// the 'cli' source; `npmrcConfig` sets it at the 'user' source. mockNpm +// puts all `config` keys into the 'cli' source by default, so for npmrc +// tests we use an .npmrc file instead. + +t.test('returns null when no policy is set anywhere', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { 'package.json': JSON.stringify({ name: 'p' }) }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.strictSame(result, { policy: null, source: null }) +}) + +t.test('global install: skips package.json but still consults CLI', async t => { + const { npm } = await mockNpm(t, { + config: { global: true, 'allow-scripts': 'canvas' }, + prefixDir: { 'package.json': JSON.stringify({ name: 'p', allowScripts: { sharp: true } }) }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('global install: skips package.json but still consults .npmrc', async t => { + const { npm } = await mockNpm(t, { + config: { global: true }, + homeDir: { '.npmrc': 'allow-scripts = canvas' }, + prefixDir: { + 'package.json': JSON.stringify({ name: 'p', allowScripts: { sharp: true } }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('global install with no CLI or .npmrc returns null', async t => { + const { npm } = await mockNpm(t, { + config: { global: true }, + prefixDir: { 'package.json': JSON.stringify({ name: 'p', allowScripts: { x: true } }) }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.strictSame(result, { policy: null, source: null }) +}) + +t.test('reads from package.json when only package.json is set', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { canvas: true, 'core-js': false, 'sharp@0.33.2': true }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { canvas: true, 'core-js': false, 'sharp@0.33.2': true }) +}) + +t.test('--allow-scripts CLI flag is rejected in project-scoped installs', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + }, + // mock-npm puts all config keys at the 'cli' source. + config: { 'allow-scripts': 'canvas' }, + }) + const resolveAllowScripts = loadResolver(t) + await t.rejects( + resolveAllowScripts(mock.npm), + { code: 'EALLOWSCRIPTS', message: /--allow-scripts is not allowed/ } + ) +}) + +t.test('--allow-scripts CLI flag is accepted in global installs (RFC layer 1 wins)', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + }, + config: { 'allow-scripts': 'canvas', global: true }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('package.json wins over .npmrc setting (RFC layer 2 > layer 3)', async t => { + // Put the allow-scripts setting in an .npmrc file so it loads at the + // 'user' source, not 'cli'. + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { sharp: true }) + t.match( + mock.logs.warn.byTitle('allow-scripts'), + [/\.npmrc allow-scripts setting is being ignored because package.json/] + ) +}) + +t.test('.npmrc setting is used when nothing higher is set', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p' }), + '.npmrc': 'allow-scripts = canvas, sharp', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true, sharp: true }) +}) + +t.test('--allow-scripts CLI flag is accepted via skipProjectConfig (npm exec)', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p' }), + '.npmrc': 'allow-scripts = canvas', + }, + config: { 'allow-scripts': 'sharp' }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm, { skipProjectConfig: true }) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { sharp: true }) + t.match( + mock.logs.warn.byTitle('allow-scripts'), + [/\.npmrc allow-scripts setting is being ignored because --allow-scripts/] + ) +}) + +t.test('empty allowScripts object in package.json falls through to .npmrc', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p', allowScripts: {} }), + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('missing package.json with .npmrc setting uses .npmrc', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('reads from npm.prefix, not cwd, so workspace sub-installs find root policy', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'root', + workspaces: ['packages/*'], + allowScripts: { sharp: true }, + }), + packages: { + sub: { 'package.json': JSON.stringify({ name: 'sub' }) }, + }, + }, + chdir: ({ prefix }) => require('node:path').join(prefix, 'packages', 'sub'), + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { sharp: true }) +}) + +t.test('drops package.json entries with forbidden semver ranges and warns', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { + 'sharp@^0.33.0': true, // forbidden: caret range + 'canvas@~2.11.0': true, // forbidden: tilde range + 'core-js@>=3.0.0': true, // forbidden: gte range + 'good@1.2.3': true, // OK: exact pin + 'also-good': true, // OK: bare name + 'disjunction@1.0.0 || 2.0.0': true, // OK: exact disjunction + }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { + 'good@1.2.3': true, + 'also-good': true, + 'disjunction@1.0.0 || 2.0.0': true, + }) + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.equal(warnings.filter(m => /semver ranges/.test(m)).length, 3) +}) + +t.test('drops package.json entries with dist-tag specs and warns', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { + 'sharp@latest': true, // forbidden: dist-tag + 'canvas@next': true, // forbidden: dist-tag + 'good@1.2.3': true, // OK: exact pin + }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { 'good@1.2.3': true }) + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.equal(warnings.filter(m => /dist-tag specs/.test(m)).length, 2) +}) + +t.test('drops .npmrc forbidden ranges (and warns) but keeps valid entries', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: 'p' }), + '.npmrc': 'allow-scripts = canvas, sharp@^0.33.0, lodash@4.17.21', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true, 'lodash@4.17.21': true }) + const warnings = mock.logs.warn.byTitle('allow-scripts') + t.ok(warnings.some(m => /sharp@\^0\.33\.0/.test(m) && /semver ranges/.test(m))) +}) + +t.test('drops package.json entries that fail npa parse', async t => { + const mock = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { + '@@@invalid@@@': true, + good: true, + }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(mock.npm) + t.equal(result.source, 'package.json') + t.strictSame(result.policy, { good: true }) + t.ok(mock.logs.warn.byTitle('allow-scripts').some(m => /unparseable/.test(m))) +}) + +t.test('returns null when all package.json entries are dropped as invalid', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { 'sharp@^0.33.0': true }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm) + t.strictSame(result, { policy: null, source: null }) +}) + +t.test('skipProjectConfig: ignores package.json even when present', async t => { + // Per RFC line 299, exec/npx consults only user/global .npmrc. + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm, { skipProjectConfig: true }) + // package.json is skipped, falls through to .npmrc. + t.equal(result.source, '.npmrc') + t.strictSame(result.policy, { canvas: true }) +}) + +t.test('skipProjectConfig: CLI still wins over .npmrc', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + '.npmrc': 'allow-scripts = canvas', + }, + config: { 'allow-scripts': 'lodash' }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm, { skipProjectConfig: true }) + t.equal(result.source, 'cli') + t.strictSame(result.policy, { lodash: true }) +}) + +t.test('skipProjectConfig: returns null when only package.json is set', async t => { + const { npm } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'p', + allowScripts: { sharp: true }, + }), + }, + }) + const resolveAllowScripts = loadResolver(t) + const result = await resolveAllowScripts(npm, { skipProjectConfig: true }) + t.strictSame(result, { policy: null, source: null }) +}) diff --git a/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js b/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js new file mode 100644 index 00000000000000..e246c68998c451 --- /dev/null +++ b/deps/npm/test/lib/utils/strict-allow-scripts-preflight.js @@ -0,0 +1,191 @@ +const t = require('tap') + +const preflight = require('../../../lib/utils/strict-allow-scripts-preflight.js') + +// Build a node fixture that checkAllowScripts will pick up as "unreviewed": +// registry-resolved, hasInstallScript true, not project root / workspace / +// link, and no allowScripts entry covering it. +const node = ({ + name = 'pkg', + version = '1.0.0', + scripts = { install: 'node-gyp rebuild' }, +} = {}) => ({ + name, + resolved: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`, + hasInstallScript: !!Object.keys(scripts).length, + path: `/fake/${name}`, + isProjectRoot: false, + isWorkspace: false, + isLink: false, + package: { name, version, scripts }, +}) + +const tree = (nodes) => ({ + inventory: new Map(nodes.map((n, i) => [`node_modules/${n.name}-${i}`, n])), +}) + +const makeArb = ({ ideal, actual, allowScripts = null } = {}) => { + const arb = { + options: { allowScripts, ignoreScripts: false }, + idealTree: ideal ?? null, + actualTree: actual ?? null, + } + arb.buildIdealTree = async () => arb.idealTree + return arb +} + +t.test('no-op when strictAllowScripts is not set', async t => { + const arb = makeArb({ ideal: tree([node()]) }) + await preflight({ arb, npm: { flatOptions: {} }, idealTreeOpts: {} }) + t.pass('returned without throwing') +}) + +t.test('no-op when dangerouslyAllowAllScripts overrides', async t => { + const arb = makeArb({ ideal: tree([node()]) }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true, dangerouslyAllowAllScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('returned without throwing') +}) + +t.test('no-op when ignoreScripts overrides', async t => { + const arb = makeArb({ ideal: tree([node()]) }) + arb.options.ignoreScripts = true + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('returned without throwing') +}) + +t.test('throws when unreviewed install scripts exist (idealTree path)', async t => { + const arb = makeArb({ ideal: tree([node({ name: 'canvas' }), node({ name: 'sharp' })]) }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }), + { + code: 'ESTRICTALLOWSCRIPTS', + message: /2 package\(s\) have install scripts not covered/, + } + ) +}) + +t.test('passes when all install-script nodes are explicitly approved', async t => { + const arb = makeArb({ + ideal: tree([node({ name: 'canvas' })]), + allowScripts: { canvas: true }, + }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('no error thrown') +}) + +t.test('passes when all install-script nodes are explicitly denied', async t => { + const arb = makeArb({ + ideal: tree([node({ name: 'canvas' })]), + allowScripts: { canvas: false }, + }) + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.pass('no error thrown') +}) + +t.test('skips buildIdealTree when arb.idealTree already exists (npm ci path)', async t => { + // `npm ci` builds the ideal tree before calling the preflight. The + // helper must not rebuild it. + const ideal = tree([node({ name: 'pre-built' })]) + const arb = makeArb({ ideal, allowScripts: { 'pre-built': true } }) + let buildCalls = 0 + arb.buildIdealTree = async () => { + buildCalls++ + return arb.idealTree + } + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.equal(buildCalls, 0, 'buildIdealTree was not called a second time') +}) + +t.test('builds the ideal tree when arb.idealTree is empty (npm install path)', async t => { + // `npm install` does not pre-build the ideal tree. The helper must + // build it so checkAllowScripts has something to walk. + const arb = makeArb({ allowScripts: { 'fresh-pkg': true } }) + let buildCalls = 0 + arb.buildIdealTree = async () => { + buildCalls++ + arb.idealTree = tree([node({ name: 'fresh-pkg' })]) + } + await preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }) + t.equal(buildCalls, 1, 'buildIdealTree was called once') +}) + +t.test('uses actualTree when idealTreeOpts is not provided (rebuild path)', async t => { + const arb = makeArb({ actual: tree([node({ name: 'rebuild-pkg' })]) }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + }), + { + code: 'ESTRICTALLOWSCRIPTS', + message: /rebuild-pkg@1\.0\.0/, + } + ) +}) + +t.test('error message includes script bodies', async t => { + const arb = makeArb({ + ideal: tree([node({ name: 'canvas', version: '2.11.0', scripts: { install: 'node-gyp rebuild' } })]), + }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }), + { message: /canvas@2\.11\.0 \(install: node-gyp rebuild\)/ } + ) +}) + +t.test('error label falls back to node.name when package.version is missing', async t => { + // Exercises the `version ? '${name}@${version}' : name` branch in the + // error formatter when a node has no package.version (and the name + // falls back to node.name via `node.package?.name || node.name`). + const bare = { + name: 'no-version-pkg', + resolved: 'https://registry.npmjs.org/no-version-pkg/-/no-version-pkg-1.0.0.tgz', + hasInstallScript: true, + path: '/fake/no-version-pkg', + isProjectRoot: false, + isWorkspace: false, + isLink: false, + package: { scripts: { install: 'node-gyp rebuild' } }, + } + const arb = makeArb({ ideal: tree([bare]) }) + await t.rejects( + preflight({ + arb, + npm: { flatOptions: { strictAllowScripts: true } }, + idealTreeOpts: {}, + }), + { message: /no-version-pkg \(install: node-gyp rebuild\)/ } + ) +}) diff --git a/deps/npm/test/lib/utils/warn-workspace-allow-scripts.js b/deps/npm/test/lib/utils/warn-workspace-allow-scripts.js new file mode 100644 index 00000000000000..c9a5727157c21e --- /dev/null +++ b/deps/npm/test/lib/utils/warn-workspace-allow-scripts.js @@ -0,0 +1,108 @@ +const t = require('tap') +const { + findWorkspaceAllowScripts, + warnWorkspaceAllowScripts, +} = require('../../../lib/utils/warn-workspace-allow-scripts.js') + +const node = ({ + name = 'pkg', + packageName, + isWorkspace = false, + isProjectRoot = false, + allowScripts, + path = `/fake/${name}`, +} = {}) => ({ + name, + packageName: packageName ?? name, + path, + isWorkspace, + isProjectRoot, + package: allowScripts !== undefined ? { allowScripts } : {}, +}) + +const tree = (nodes) => ({ + inventory: new Map(nodes.map((n, i) => [`node_modules/${n.name || `n${i}`}`, n])), +}) + +t.test('returns [] for empty tree', async t => { + t.strictSame(findWorkspaceAllowScripts(tree([])), []) +}) + +t.test('returns [] for missing tree', async t => { + t.strictSame(findWorkspaceAllowScripts(null), []) + t.strictSame(findWorkspaceAllowScripts(undefined), []) +}) + +t.test('ignores project root with allowScripts', async t => { + const t1 = tree([ + node({ name: 'root', isProjectRoot: true, isWorkspace: true, allowScripts: { x: true } }), + ]) + t.strictSame(findWorkspaceAllowScripts(t1), []) +}) + +t.test('ignores non-workspace dep with allowScripts', async t => { + const t1 = tree([ + node({ name: 'dep', allowScripts: { x: true } }), + ]) + t.strictSame(findWorkspaceAllowScripts(t1), []) +}) + +t.test('finds non-root workspace with allowScripts', async t => { + const ws = node({ name: 'ws', isWorkspace: true, allowScripts: { x: true } }) + const t1 = tree([ + node({ name: 'root', isProjectRoot: true, isWorkspace: true }), + ws, + ]) + t.equal(findWorkspaceAllowScripts(t1).length, 1) + t.equal(findWorkspaceAllowScripts(t1)[0], ws) +}) + +t.test('finds workspace with empty allowScripts object too', async t => { + const ws = node({ name: 'ws', isWorkspace: true, allowScripts: {} }) + t.equal(findWorkspaceAllowScripts(tree([ws])).length, 1) +}) + +t.test('warnWorkspaceAllowScripts emits one log.warn per offender', async t => { + const warnings = [] + const listener = (level, ...args) => { + if (level === 'warn') { + warnings.push(args) + } + } + process.on('log', listener) + t.teardown(() => process.off('log', listener)) + + const t1 = tree([ + node({ name: 'root', isProjectRoot: true, isWorkspace: true }), + node({ name: 'a', isWorkspace: true, allowScripts: { x: true } }), + node({ name: 'b', isWorkspace: true, allowScripts: { y: false } }), + node({ name: 'c', isWorkspace: true }), // no allowScripts; no warning + ]) + warnWorkspaceAllowScripts(t1) + + t.equal(warnings.length, 2) + t.match(warnings[0][1], /allowScripts in workspace a/) + t.match(warnings[1][1], /allowScripts in workspace b/) +}) + +t.test('warnWorkspaceAllowScripts uses node.name when packageName missing', async t => { + const warnings = [] + const listener = (level, ...args) => { + if (level === 'warn') { + warnings.push(args) + } + } + process.on('log', listener) + t.teardown(() => process.off('log', listener)) + + // packageName undefined, name set + const ws = { + name: 'fallback-name', + path: '/x', + isWorkspace: true, + isProjectRoot: false, + package: { allowScripts: { x: true } }, + } + warnWorkspaceAllowScripts({ inventory: new Map([['node_modules/ws', ws]]) }) + t.match(warnings[0][1], /workspace fallback-name/) +}) From 51c89fa3a8398831f6c2efea5364c006757d9fd4 Mon Sep 17 00:00:00 2001 From: RajeshKumar11 Date: Sat, 30 May 2026 13:40:58 +0530 Subject: [PATCH 85/89] http: add httpValidation option to configure header value validation Add a new httpValidation option to http.createServer() and http.request() / http.ClientRequest that controls how strictly HTTP header values are validated: - 'strict' - reject any non-ASCII or control characters (default) - 'relaxed' - allow the non-ASCII characters permitted by the Fetch specification (kLenientHeaderValueRelaxed) - 'insecure' - disable all validation (like insecureHTTPParser) The option is threaded through _storeHeader -> processHeader -> storeHeader -> validateHeaderValue, and also through writeInformation -> processInformationHeader -> validateHeaderValue. Cannot be used together with insecureHTTPParser. Fixes: https://github.com/nodejs/node/issues/61582 Signed-off-by: RajeshKumar11 PR-URL: https://github.com/nodejs/node/pull/61597 Refs: https://github.com/nodejs/node/issues/61582 Refs: https://fetch.spec.whatwg.org/#header-value Reviewed-By: Matteo Collina Reviewed-By: Tim Perry --- doc/api/http.md | 26 ++ lib/_http_client.js | 27 +- lib/_http_common.js | 42 +- lib/_http_outgoing.js | 72 ++- lib/_http_server.js | 36 +- src/node_http_parser.cc | 16 + .../test-http-header-value-relaxed.js | 429 ++++++++++++++++++ .../parallel/test-http-invalidheaderfield2.js | 73 ++- 8 files changed, 668 insertions(+), 53 deletions(-) create mode 100644 test/parallel/test-http-header-value-relaxed.js diff --git a/doc/api/http.md b/doc/api/http.md index 41ecdf6d00b3be..ec17fe33e71e50 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -3680,6 +3680,9 @@ Found'`.