diff --git a/.github/label-pr-config.yml b/.github/label-pr-config.yml index 4ff8bee4c56d64..8b36066473f415 100644 --- a/.github/label-pr-config.yml +++ b/.github/label-pr-config.yml @@ -104,6 +104,7 @@ subSystemLabels: /^lib\/internal\/url\.js$/: whatwg-url /^lib\/internal\/modules\/esm/: esm /^lib\/internal\/modules/: module + /^lib\/internal\/source_map/: source maps /^lib\/internal\/webstreams/: web streams /^lib\/internal\/test_runner/: test_runner /^lib\/internal\/v8\//: v8 module @@ -128,6 +129,8 @@ exlusiveLabels: /^test\/report\//: test, report /^test\/fixtures\/es-module/: test, esm /^test\/es-module\//: test, esm + /^test\/fixtures\/source-map/: test, source maps + /^test\/fixtures\/test-426/: test, source maps /^test\/fixtures\/wpt\/streams\//: test, web streams /^test\/fixtures\/typescript/: test, strip-types /^test\/module-hooks\//: test, module, loaders @@ -214,6 +217,7 @@ allJsSubSystems: - url - util - v8 + - vfs - vm - wasi - worker diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5ff9daaa630d2a..01d7f37e38149e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,6 +9,7 @@ permissions: jobs: analyze: + if: github.repository == 'nodejs/node' name: Analyze runs-on: ubuntu-slim permissions: 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/* diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 80e7f8294d693f..0b82dd2aac04c7 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -13,6 +13,7 @@ permissions: jobs: build-lto: + if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 07f05ecbbca57f..39be55d20d5fd8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -20,6 +20,7 @@ permissions: read-all jobs: analysis: + if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' name: Scorecard analysis # cannot use ubuntu-slim here because ossf/scorecard-action is dockerized # cannot use ubuntu-24.04-arm here because the docker image is x86 only 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 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, diff --git a/Makefile b/Makefile index 1e1264619d24bb..961682e607272c 100644 --- a/Makefile +++ b/Makefile @@ -856,7 +856,7 @@ VERSION=v$(RAWVER) .PHONY: doc-only .NOTPARALLEL: doc-only -doc-only: $(apidoc_dirs) $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json out/doc/apilinks.json ## Builds the docs with the local or the global Node.js binary. +doc-only: $(apidoc_dirs) $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json out/doc/llms.txt out/doc/apilinks.json ## Builds the docs with the local or the global Node.js binary. .PHONY: doc doc: $(NODE_EXE) doc-only ## Build Node.js, and then build the documentation with the new binary. @@ -901,6 +901,22 @@ $(apidocs_html) $(apidocs_json) out/doc/api/all.html out/doc/api/all.json &: $(a fi endif +out/doc/llms.txt: $(apidoc_sources) tools/doc/node_modules | out/doc + @if [ "$(shell $(node_use_openssl_and_icu))" != "true" ]; then \ + echo "Skipping $@ (no crypto and/or no ICU)"; \ + else \ + $(call available-node, \ + $(DOC_KIT) generate \ + -t llms-txt \ + -i doc/api/*.md \ + --ignore $(skip_apidoc_files) \ + -o $(@D) \ + -c ./CHANGELOG.md \ + -v $(VERSION) \ + --type-map doc/type-map.json \ + ) \ + fi + out/doc/apilinks.json: $(wildcard lib/*.js) tools/doc/node_modules | out/doc @if [ "$(shell $(node_use_openssl_and_icu))" != "true" ]; then \ echo "Skipping $@ (no crypto and/or no ICU)"; \ diff --git a/SECURITY.md b/SECURITY/Symbols similarity index 99% rename from SECURITY.md rename to SECURITY/Symbols index 0e88d7b50702fa..180dd89e956920 100644 --- a/SECURITY.md +++ b/SECURITY/Symbols @@ -1,5 +1,5 @@ # Security - +[access] inheritFrom = All-projects = Active ## Reporting a bug in Node.js Report security bugs in Node.js via [HackerOne](https://hackerone.com/nodejs). @@ -85,6 +85,7 @@ When reporting security vulnerabilities, reporters must adhere to the following 4. **Report Quality** * Provide clear, detailed steps to reproduce the vulnerability. + * Include reproducible code written in JavaScript. * Include only the minimum proof of concept required to demonstrate the issue. * Remove any malicious payloads or components that could cause harm. @@ -376,7 +377,7 @@ the community they pose. #### Permission Model Boundaries (`--permission`) The Node.js [Permission Model](https://nodejs.org/api/permissions.html) -(`--experimental-permission`) is an opt-in mechanism that limits which +(`--permission`) is an opt-in mechanism that limits which resources a Node.js process may access. It is designed to reduce the blast radius of mistakes in trusted application code, **not** to act as a security boundary against intentional misuse or a compromised process. diff --git a/benchmark/fs/readfile-utf8-fastpath.js b/benchmark/fs/readfile-utf8-fastpath.js new file mode 100644 index 00000000000000..9bf00717c5f0b2 --- /dev/null +++ b/benchmark/fs/readfile-utf8-fastpath.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common.js'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../../test/common/tmpdir'); + +const bench = common.createBenchmark(main, { + size: [64, 1024, 16384, 262144, 4194304], + content: ['ascii', 'latin1', 'utf8_mixed'], + source: ['path', 'fd'], + n: [3e3], +}); + +function buildContent(kind, size) { + if (kind === 'ascii') { + return Buffer.alloc(size, 0x61); // 'a' + } + if (kind === 'latin1') { + // 'é' in UTF-8 is 0xC3 0xA9 (2 bytes per char) + const pair = Buffer.from([0xC3, 0xA9]); + const buf = Buffer.alloc(size); + for (let i = 0; i + 2 <= size; i += 2) pair.copy(buf, i); + return buf; + } + if (kind === 'utf8_mixed') { + // mixed ASCII + 3-byte CJK (U+4E2D 中 = E4 B8 AD) + const cjk = Buffer.from([0xE4, 0xB8, 0xAD]); + const buf = Buffer.alloc(size); + let i = 0; + while (i + 4 <= size) { + buf[i++] = 0x61; + cjk.copy(buf, i); + i += 3; + } + return buf; + } + throw new Error('unknown content: ' + kind); +} + +function main({ n, size, content, source }) { + tmpdir.refresh(); + const file = path.join(tmpdir.path, `bench-${content}-${size}.bin`); + fs.writeFileSync(file, buildContent(content, size)); + + let arg; + let shouldClose = false; + if (source === 'fd') { + arg = fs.openSync(file, 'r'); + shouldClose = true; + } else { + arg = file; + } + + bench.start(); + for (let i = 0; i < n; i++) { + fs.readFileSync(arg, 'utf8'); + } + bench.end(n); + + if (shouldClose) fs.closeSync(arg); +} diff --git a/benchmark/streams/compose.js b/benchmark/streams/compose.js index b98596ffbd1411..283ad8b7e30b32 100644 --- a/benchmark/streams/compose.js +++ b/benchmark/streams/compose.js @@ -9,16 +9,35 @@ const { } = require('node:stream'); const bench = common.createBenchmark(main, { - n: [1e3], + type: ['creation', 'throughput'], + n: [1, 1e3], + streams: [100], + chunks: [1e4], +}, { + combinationFilter({ type, n }) { + return type === 'creation' ? n === 1e3 : n === 1; + }, + test: { + n: [1, 1e3], + type: ['creation', 'throughput'], + }, }); -function main({ n }) { +function main({ type, n, streams, chunks }) { + switch (type) { + case 'creation': + return benchCreation(n, streams); + case 'throughput': + return benchThroughput(n, streams, chunks); + } +} + +function benchCreation(n, numberOfPassThroughs) { const cachedPassThroughs = []; const cachedReadables = []; const cachedWritables = []; for (let i = 0; i < n; i++) { - const numberOfPassThroughs = 100; const passThroughs = []; for (let i = 0; i < numberOfPassThroughs; i++) { @@ -40,3 +59,41 @@ function main({ n }) { } bench.end(n); } + +function benchThroughput(n, numberOfPassThroughs, chunks) { + const chunk = Buffer.alloc(1024); + + let i = 0; + bench.start(); + + function run() { + if (i++ === n) { + bench.end(n * chunks); + return; + } + + const passThroughs = []; + for (let i = 0; i < numberOfPassThroughs; i++) { + passThroughs.push(new PassThrough()); + } + + let remaining = chunks; + const composed = compose(...passThroughs); + composed.on('data', () => {}); + composed.on('end', run); + + write(); + + function write() { + while (remaining-- > 0) { + if (!composed.write(chunk)) { + composed.once('drain', write); + return; + } + } + composed.end(); + } + } + + run(); +} 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/benchmark/util/text-decoder.js b/benchmark/util/text-decoder.js index 1aa60f2dd0bcd6..ecfba045c52fab 100644 --- a/benchmark/util/text-decoder.js +++ b/benchmark/util/text-decoder.js @@ -6,26 +6,42 @@ const bench = common.createBenchmark(main, { encoding: ['utf-8', 'windows-1252', 'iso-8859-3'], ignoreBOM: [0, 1], fatal: [0, 1], + type: ['SharedArrayBuffer', 'ArrayBuffer', 'Buffer'], + content: ['ascii', 'one-byte-string', 'two-byte-string'], len: [256, 1024 * 16, 1024 * 128], n: [1e3], - type: ['SharedArrayBuffer', 'ArrayBuffer', 'Buffer'], }); -function main({ encoding, len, n, ignoreBOM, type, fatal }) { +function buildContent(content, len) { + let base; + switch (content) { + case 'ascii': base = 'a'; break; + case 'one-byte-string': base = '\xff'; break; + case 'two-byte-string': base = 'ğ'; break; + } + const unitBytes = Buffer.byteLength(base, 'utf8'); + const copies = Math.max(1, Math.floor(len / unitBytes)); + return Buffer.from(base.repeat(copies)); +} + +function main({ encoding, len, n, ignoreBOM, type, fatal, content }) { const decoder = new TextDecoder(encoding, { ignoreBOM, fatal }); + const seed = buildContent(content, len); let buf; switch (type) { case 'SharedArrayBuffer': { - buf = new SharedArrayBuffer(len); + buf = new SharedArrayBuffer(seed.length); + new Uint8Array(buf).set(seed); break; } case 'ArrayBuffer': { - buf = new ArrayBuffer(len); + buf = new ArrayBuffer(seed.length); + new Uint8Array(buf).set(seed); break; } case 'Buffer': { - buf = Buffer.allocUnsafe(len); + buf = seed; break; } } diff --git a/configure.py b/configure.py index 57177b969bb523..714639bf03bf07 100755 --- a/configure.py +++ b/configure.py @@ -1965,18 +1965,29 @@ def configure_node(o): msvc_dir = target_arch # 'x64' or 'arm64' vc_tools_dir = os.environ.get('VCToolsInstallDir', '') - if vc_tools_dir: - clang_profile_lib = os.path.join(vc_tools_dir, 'lib', msvc_dir, lib_name) - if os.path.isfile(clang_profile_lib): - o['variables']['clang_profile_lib'] = clang_profile_lib - else: - raise Exception( - f'PGO profile runtime library not found at {clang_profile_lib}. ' - 'Ensure the ClangCL toolset is installed.') - else: + if not vc_tools_dir: raise Exception( 'VCToolsInstallDir not set. Run from a Visual Studio command prompt.') + # Primary location: VS2026 and VS2022 x64 + candidates = [os.path.join(vc_tools_dir, 'lib', msvc_dir, lib_name)] + + # Secondary location: VS2022 arm64 fallback + clang_major = options.clang_cl.split('.', 1)[0] + candidates.append(os.path.normpath(os.path.join( + vc_tools_dir, '..', '..', 'Llvm', msvc_dir, + 'lib', 'clang', clang_major, 'lib', 'windows', lib_name))) + + clang_profile_lib = next( + (p for p in candidates if os.path.isfile(p)), None) + if clang_profile_lib: + o['variables']['clang_profile_lib'] = clang_profile_lib + else: + raise Exception( + f'PGO profile runtime library {lib_name} not found. Searched:\n ' + + '\n '.join(candidates) + + '\nEnsure the ClangCL toolset is installed.') + if flavor != 'win' and options.enable_thin_lto: raise Exception( 'Use --enable-lto instead.') 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/) +}) diff --git a/deps/sqlite/sqlite3.c b/deps/sqlite/sqlite3.c index dfd557adeda581..0c83f247e89464 100644 --- a/deps/sqlite/sqlite3.c +++ b/deps/sqlite/sqlite3.c @@ -238388,7 +238388,7 @@ static int sessionApplyOneOp( for(i=0; rc==SQLITE_OK && iabPK[i] || (bPatchset==0 && pOld) ){ + if( pOld && (p->abPK[i] || bPatchset==0) ){ rc = sessionBindValue(pUp, i*2+2, pOld); } if( rc==SQLITE_OK && pNew ){ diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 9e6eabb77caa1e..9e0433b00067c7 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -881,7 +881,7 @@ _may contain sensitive data_. Use [`buf.fill(0)`][`buf.fill()`] to initialize such `Buffer` instances with zeroes. When using [`Buffer.allocUnsafe()`][] to allocate new `Buffer` instances, -allocations less than `Buffer.poolSize >>> 1` (4KiB when default poolSize is used) are sliced +allocations less than `Buffer.poolSize >>> 1` (32KiB when default poolSize is used) are sliced from a single pre-allocated `Buffer`. This allows applications to avoid the garbage collection overhead of creating many individually allocated `Buffer` instances. This approach improves both performance and memory usage by @@ -1513,9 +1513,13 @@ console.log(Buffer.isEncoding('')); -* 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/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` 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: {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` @@ -718,7 +853,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` @@ -726,7 +867,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` @@ -734,7 +881,24 @@ 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. + +### `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. + +### `endpointStats.packetsBlocked` + +* Type: {bigint} The total number of incoming packets dropped by the + block list filter. Read only. ## Class: `QuicSession` @@ -1515,6 +1679,11 @@ added: v23.8.0 * Type: {bigint} +### `sessionStats.streamsIdleTimedOut` + +* Type: {bigint} The total number of peer-initiated streams destroyed by the + stream idle timeout. Read only. + ## Class: `QuicError` +* 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. -Specifies the maximum number of stateless resets that are allowed per remote peer address. +#### `endpointOptions.immediateCloseBurst` + +* Type: {number} +* **Default:** `200` + +The maximum burst of immediate connection close packets allowed before rate +limiting takes effect. + +#### `endpointOptions.sessionCreationRate` + +* Type: {number} +* **Default:** `50` + +The maximum number of new sessions that a single remote address can create per +second. This is a per-host rate limit tracked in the address validation LRU +cache. It prevents a validated remote address from churning through sessions +(rapidly opening and abandoning connections) faster than the server can handle. +For benchmarking where traffic comes from a single source, set this to a high +value. + +#### `endpointOptions.sessionCreationBurst` + +* Type: {number} +* **Default:** `100` + +The maximum burst of new session creations allowed from a single remote address +before rate limiting takes effect. #### `endpointOptions.retryTokenExpiration` @@ -2746,9 +3007,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` @@ -2788,6 +3055,23 @@ reported as lost via the `ondatagramstatus` callback. This option is immutable after session creation. +#### `sessionOptions.streamIdleTimeout` + +* Type: {bigint|number} +* **Default:** `30000` (30 seconds) + +The maximum time in milliseconds that a peer-initiated stream can be idle +(no data received) before it is automatically destroyed. This protects +against slowloris-style attacks where a remote peer opens streams but never +sends data, holding server resources indefinitely. Only peer-initiated +streams are checked — locally-initiated streams are the application's +responsibility. Set to `0` to disable. + +The idle check runs as part of the normal send processing loop, so it adds +no additional timers or event loop overhead. The +`session.stats.streamsIdleTimedOut` counter tracks how many streams have been +destroyed by this mechanism. + #### `sessionOptions.maxDatagramSendAttempts` * Type: {number} @@ -2857,6 +3141,32 @@ value, PING frames will be sent automatically to keep the connection alive before the idle timeout fires. The value should be less than the effective idle timeout (`maxIdleTimeout` transport parameter) to be useful. +#### `sessionOptions.verifyPeer` (client only) + +* Type: {string} One of `'strict'`, `'auto'`, or `'manual'`. +* **Default:** `'auto'` + +Controls how the client handles server certificate validation: + +* `'strict'` — OpenSSL aborts the TLS handshake immediately if the server's + certificate fails validation. The `session.opened` promise rejects with a + TLS error. The application cannot inspect the certificate or the error + details. This is the most secure mode. + +* `'auto'` — The TLS handshake completes regardless of validation result. + If validation fails, the `session.opened` promise is rejected with an error + containing the validation reason, and the session is destroyed. The + `onhandshake` callback (if set) fires before rejection, allowing diagnostic + logging. This is the default and matches the behavior of `tls.connect()` + with `rejectUnauthorized: true`. + +* `'manual'` — The TLS handshake completes regardless of validation result. + The `session.opened` promise resolves with the handshake info, which includes + `validationErrorReason` and `validationErrorCode` if validation failed. The + application is responsible for checking these values and deciding whether to + continue. Use this mode for custom validation logic, certificate pinning, or + intentionally accepting self-signed certificates. + #### `sessionOptions.servername` (client only) + + + +> 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/api/webcrypto.md b/doc/api/webcrypto.md index f357ebb6ae0282..164b8569fb5299 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -850,7 +850,7 @@ The algorithms currently supported include: * `'ML-KEM-768'`[^modern-algos] * `'ML-KEM-1024'`[^modern-algos] -### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` +### `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, keyUsages)` -* `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/doc/api/worker_threads.md b/doc/api/worker_threads.md index 8f4b152bc26a68..720c5640d2065a 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -1369,17 +1369,14 @@ port2.postMessage(new Foo()); // Prints: { c: 3 } ``` -This limitation extends to many built-in objects, such as the global `URL` -object: +Some built-in objects cannot be cloned at all. For example, posting a +`URL` object throws a `DataCloneError`: ```js const { port1, port2 } = new MessageChannel(); -port1.onmessage = ({ data }) => console.log(data); - port2.postMessage(new URL('https://example.org')); - -// Prints: { } +// Throws DataCloneError: Cannot clone object of unsupported type. ``` ### `port.hasRef()` diff --git a/doc/contributing/releases.md b/doc/contributing/releases.md index 5299b0026298e3..e2eba8880b1db9 100644 --- a/doc/contributing/releases.md +++ b/doc/contributing/releases.md @@ -272,11 +272,12 @@ $ git reset --hard upstream/vN.x The list of patches to include should be listed in the "Next Security Release" issue in `nodejs-private`. Ask the security release steward if you're unsure. -The `git node land` tool does not work with the `nodejs-private` -organization. To land a PR in Node.js private, use `git cherry-pick` to apply -each commit from the PR. You will also need to manually apply the PR -metadata (`PR-URL`, `Reviewed-by`, etc.) by amending the commit messages. If +To use the `git node land` tool to land Pull Requests in the `nodejs-private` +organization, you need to specify the full URL to the Pull Request and make sure +you provide a GitHub token with read permission to the private repository. If known, additionally include `CVE-ID: CVE-XXXX-XXXXX` in the commit metadata. +Make sure to sign and push to resulting commit to the private repository and not +the public one. **Note**: Do not run CI on the PRs in `nodejs-private` until CI is locked down. You can integrate the PRs into the proposal without running full CI. 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/_http_client.js b/lib/_http_client.js index b7f0aa759b0643..73d7b84c17a8fd 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -46,7 +46,7 @@ const { freeParser, parsers, HTTPParser, - isLenient, + calculateLenientFlags, prepareError, kSkipPendingData, } = require('_http_common'); @@ -74,6 +74,7 @@ const { codes: { ERR_HTTP_HEADERS_SENT, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_HTTP_TOKEN, ERR_INVALID_PROTOCOL, ERR_UNESCAPED_CHARACTERS, @@ -82,6 +83,7 @@ const { const { validateInteger, validateBoolean, + validateOneOf, validateString, } = require('internal/validators'); const { getTimerDuration } = require('internal/timers'); @@ -119,9 +121,6 @@ const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; const kError = Symbol('kError'); const kPath = Symbol('kPath'); -const kLenientAll = HTTPParser.kLenientAll | 0; -const kLenientNone = HTTPParser.kLenientNone | 0; - const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request'; function validateHost(host, name) { @@ -299,6 +298,21 @@ function ClientRequest(input, options, cb) { this.insecureHTTPParser = insecureHTTPParser; + const httpValidation = options.httpValidation; + if (httpValidation !== undefined) { + validateOneOf(httpValidation, 'options.httpValidation', + ['strict', 'relaxed', 'insecure']); + if (insecureHTTPParser !== undefined) { + throw new ERR_INVALID_ARG_VALUE( + 'options.httpValidation', + httpValidation, + 'cannot be used together with options.insecureHTTPParser', + ); + } + } + + this.httpValidation = httpValidation; + if (options.joinDuplicateHeaders !== undefined) { validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders'); } @@ -907,12 +921,11 @@ function emitFreeNT(req) { function tickOnSocket(req, socket) { const parser = parsers.alloc(); req.socket = socket; - const lenient = req.insecureHTTPParser === undefined ? - isLenient() : req.insecureHTTPParser; + const lenientFlags = calculateLenientFlags(req.httpValidation, req.insecureHTTPParser); parser.initialize(HTTPParser.RESPONSE, new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req), req.maxHeaderSize || 0, - lenient ? kLenientAll : kLenientNone); + lenientFlags); parser.socket = socket; parser.outgoing = req; req.parser = parser; diff --git a/lib/_http_common.js b/lib/_http_common.js index 3c389ba054decc..d5e7bdedee39fb 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -256,17 +256,31 @@ function checkIsHttpToken(val) { return true; } -const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +// Strict header value regex per RFC 7230 (original/default behavior): +// field-value = *( field-content / obs-fold ) +// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] +// field-vchar = VCHAR / obs-text +// This rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f). +const strictHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + +// Lenient header value regex per Fetch spec (https://fetch.spec.whatwg.org/#header-value): +// - Must contain no 0x00 (NUL) or HTTP newline bytes (0x0a LF, 0x0d CR) +// - Must be byte sequences (0x00-0xff), not arbitrary unicode +// This allows most control characters except NUL, CR, and LF. +// eslint-disable-next-line no-control-regex +const lenientHeaderCharRegex = /[\x00\x0a\x0d]|[^\x00-\xff]/; + /** - * True if val contains an invalid field-vchar - * field-value = *( field-content / obs-fold ) - * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] - * field-vchar = VCHAR / obs-text + * True if val contains an invalid header value character. + * By default uses strict validation per RFC 7230. + * When lenient=true, uses relaxed validation per Fetch spec. * @param {string} val + * @param {boolean} [lenient] - Use lenient validation (Fetch spec rules) * @returns {boolean} */ -function checkInvalidHeaderChar(val) { - return headerCharRegex.test(val); +function checkInvalidHeaderChar(val, lenient = false) { + const regex = lenient ? lenientHeaderCharRegex : strictHeaderCharRegex; + return regex.test(val); } function cleanParser(parser) { @@ -300,6 +314,19 @@ function isLenient() { return insecureHTTPParser; } +function calculateLenientFlags(httpValidation, insecureHTTPParserOption) { + if (httpValidation === 'strict') { + return HTTPParser.kLenientNone | 0; + } else if (httpValidation === 'relaxed') { + return HTTPParser.kLenientHeaderValueRelaxed | 0; + } else if (httpValidation === 'insecure') { + return HTTPParser.kLenientAll | 0; + } + const lenient = insecureHTTPParserOption === undefined ? + isLenient() : insecureHTTPParserOption; + return lenient ? HTTPParser.kLenientAll | 0 : HTTPParser.kLenientNone | 0; +} + module.exports = { _checkInvalidHeaderChar: checkInvalidHeaderChar, _checkIsHttpToken: checkIsHttpToken, @@ -312,6 +339,7 @@ module.exports = { kIncomingMessage, HTTPParser, isLenient, + calculateLenientFlags, prepareError, kSkipPendingData, }; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 5a83849086294f..4498ee72fe48d8 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -44,6 +44,7 @@ const { _checkIsHttpToken: checkIsHttpToken, _checkInvalidHeaderChar: checkInvalidHeaderChar, chunkExpression: RE_TE_CHUNKED, + isLenient, } = require('_http_common'); const { defaultTriggerAsyncIdScope, @@ -158,6 +159,33 @@ function OutgoingMessage(options) { ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype); ObjectSetPrototypeOf(OutgoingMessage, Stream); +// Check if lenient header validation should be used. +// For ClientRequest: checks this.httpValidation or this.insecureHTTPParser +// For ServerResponse: checks the server's httpValidation or insecureHTTPParser +// Falls back to global --insecure-http-parser flag. +OutgoingMessage.prototype._isLenientHeaderValidation = function() { + // New httpValidation option takes priority (ClientRequest case) + if (this.httpValidation !== undefined) { + return this.httpValidation !== 'strict'; + } + // ServerResponse: check server's httpValidation option + const serverHttpValidation = this.req?.socket?.server?.httpValidation; + if (serverHttpValidation !== undefined) { + return serverHttpValidation !== 'strict'; + } + // Legacy insecureHTTPParser - ClientRequest has it directly + if (typeof this.insecureHTTPParser === 'boolean') { + return this.insecureHTTPParser; + } + // ServerResponse can access via req.socket.server + const serverOption = this.req?.socket?.server?.insecureHTTPParser; + if (typeof serverOption === 'boolean') { + return serverOption; + } + // Fall back to global option + return isLenient(); +}; + ObjectDefineProperty(OutgoingMessage.prototype, 'errored', { __proto__: null, get() { @@ -411,18 +439,19 @@ function _storeHeader(firstLine, headers) { trailer: false, header: firstLine, }; + const lenient = this._isLenientHeaderValidation(); if (headers) { if (headers === this[kOutHeaders]) { for (const key in headers) { const entry = headers[key]; - processHeader(this, state, entry[0], entry[1], false); + processHeader(this, state, entry[0], entry[1], false, lenient); } } else if (ArrayIsArray(headers)) { if (headers.length && ArrayIsArray(headers[0])) { for (let i = 0; i < headers.length; i++) { const entry = headers[i]; - processHeader(this, state, entry[0], entry[1], true); + processHeader(this, state, entry[0], entry[1], true, lenient); } } else { if (headers.length % 2 !== 0) { @@ -430,13 +459,13 @@ function _storeHeader(firstLine, headers) { } for (let n = 0; n < headers.length; n += 2) { - processHeader(this, state, headers[n + 0], headers[n + 1], true); + processHeader(this, state, headers[n + 0], headers[n + 1], true, lenient); } } } else { for (const key in headers) { if (ObjectHasOwn(headers, key)) { - processHeader(this, state, key, headers[key], true); + processHeader(this, state, key, headers[key], true, lenient); } } } @@ -535,7 +564,7 @@ function _storeHeader(firstLine, headers) { if (state.expect) this._send(''); } -function processHeader(self, state, key, value, validate) { +function processHeader(self, state, key, value, validate, lenient) { if (validate) validateHeaderName(key); @@ -562,17 +591,17 @@ function processHeader(self, state, key, value, validate) { // Retain for(;;) loop for performance reasons // Refs: https://github.com/nodejs/node/pull/30958 for (let i = 0; i < value.length; i++) - storeHeader(self, state, key, value[i], validate); + storeHeader(self, state, key, value[i], validate, lenient); return; } value = value.join('; '); } - storeHeader(self, state, key, value, validate); + storeHeader(self, state, key, value, validate, lenient); } -function storeHeader(self, state, key, value, validate) { +function storeHeader(self, state, key, value, validate, lenient) { if (validate) - validateHeaderValue(key, value); + validateHeaderValue(key, value, lenient); state.header += key + ': ' + value + '\r\n'; matchHeader(self, state, key, value); } @@ -618,11 +647,11 @@ const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFra } })); -const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value) => { +const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => { if (value === undefined) { throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name); } - if (checkInvalidHeaderChar(value)) { + if (checkInvalidHeaderChar(value, lenient)) { debug('Header "%s" contains invalid characters', name); throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name); } @@ -647,7 +676,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) { throw new ERR_HTTP_HEADERS_SENT('set'); } validateHeaderName(name); - validateHeaderValue(name, value); + if (value === undefined) { + throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); + } + if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) { + debug('Header "%s" contains invalid characters', name); + throw new ERR_INVALID_CHAR('header content', name); + } let headers = this[kOutHeaders]; if (headers === null) @@ -705,7 +740,13 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) { throw new ERR_HTTP_HEADERS_SENT('append'); } validateHeaderName(name); - validateHeaderValue(name, value); + if (value === undefined) { + throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); + } + if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) { + debug('Header "%s" contains invalid characters', name); + throw new ERR_INVALID_CHAR('header content', name); + } const field = name.toLowerCase(); const headers = this[kOutHeaders]; @@ -1005,12 +1046,13 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { // Check if the field must be sent several times const isArrayValue = ArrayIsArray(value); + const lenient = this._isLenientHeaderValidation(); if ( isArrayValue && value.length > 1 && (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase())) ) { for (let j = 0, l = value.length; j < l; j++) { - if (checkInvalidHeaderChar(value[j])) { + if (checkInvalidHeaderChar(value[j], lenient)) { debug('Trailer "%s"[%d] contains invalid characters', field, j); throw new ERR_INVALID_CHAR('trailer content', field); } @@ -1021,7 +1063,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { value = value.join('; '); } - if (checkInvalidHeaderChar(value)) { + if (checkInvalidHeaderChar(value, lenient)) { debug('Trailer "%s" contains invalid characters', field); throw new ERR_INVALID_CHAR('trailer content', field); } diff --git a/lib/_http_server.js b/lib/_http_server.js index 0b4077bf07367a..0f5865126689d3 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -46,7 +46,7 @@ const { kIncomingMessage, kSocket, HTTPParser, - isLenient, + calculateLenientFlags, _checkInvalidHeaderChar: checkInvalidHeaderChar, prepareError, } = require('_http_common'); @@ -92,6 +92,7 @@ const { const { validateInteger, validateBoolean, + validateOneOf, validateLinkHeaderValue, validateObject, validateFunction, @@ -187,8 +188,7 @@ const STATUS_CODES = { const kOnExecute = HTTPParser.kOnExecute | 0; const kOnTimeout = HTTPParser.kOnTimeout | 0; -const kLenientAll = HTTPParser.kLenientAll | 0; -const kLenientNone = HTTPParser.kLenientNone | 0; + const kConnections = Symbol('http.server.connections'); const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval'); @@ -325,19 +325,20 @@ ServerResponse.prototype.writeInformation = function writeInformation( const statusMessage = STATUS_CODES[statusCode] || 'unknown'; let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`; + const lenient = this._isLenientHeaderValidation(); if (headers !== undefined && headers !== null) { if (ArrayIsArray(headers)) { if (headers.length && ArrayIsArray(headers[0])) { for (let i = 0; i < headers.length; i++) { const entry = headers[i]; - head += processInformationHeader(entry[0], entry[1]); + head += processInformationHeader(entry[0], entry[1], lenient); } } else { if (headers.length % 2 !== 0) { throw new ERR_INVALID_ARG_VALUE('headers', headers); } for (let i = 0; i < headers.length; i += 2) { - head += processInformationHeader(headers[i], headers[i + 1]); + head += processInformationHeader(headers[i], headers[i + 1], lenient); } } } else { @@ -345,7 +346,7 @@ ServerResponse.prototype.writeInformation = function writeInformation( const keys = ObjectKeys(headers); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - head += processInformationHeader(key, headers[key]); + head += processInformationHeader(key, headers[key], lenient); } } } @@ -355,9 +356,9 @@ ServerResponse.prototype.writeInformation = function writeInformation( return this._writeRaw(head, 'ascii', cb); }; -function processInformationHeader(name, value) { +function processInformationHeader(name, value, lenient) { validateHeaderName(name); - validateHeaderValue(name, value); + validateHeaderValue(name, value, lenient); return `${name}: ${value}\r\n`; } @@ -516,6 +517,20 @@ function storeHTTPOptions(options) { validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser'); this.insecureHTTPParser = insecureHTTPParser; + const httpValidation = options.httpValidation; + if (httpValidation !== undefined) { + validateOneOf(httpValidation, 'options.httpValidation', + ['strict', 'relaxed', 'insecure']); + if (insecureHTTPParser !== undefined) { + throw new ERR_INVALID_ARG_VALUE( + 'options.httpValidation', + httpValidation, + 'cannot be used together with options.insecureHTTPParser', + ); + } + } + this.httpValidation = httpValidation; + const requestTimeout = options.requestTimeout; if (requestTimeout !== undefined) { validateInteger(requestTimeout, 'requestTimeout', 0); @@ -761,8 +776,7 @@ function connectionListenerInternal(server, socket) { const parser = parsers.alloc(); - const lenient = server.insecureHTTPParser === undefined ? - isLenient() : server.insecureHTTPParser; + const lenientFlags = calculateLenientFlags(server.httpValidation, server.insecureHTTPParser); // TODO(addaleax): This doesn't play well with the // `async_hooks.currentResource()` proposal, see @@ -771,7 +785,7 @@ function connectionListenerInternal(server, socket) { HTTPParser.REQUEST, new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket), server.maxHeaderSize || 0, - lenient ? kLenientAll : kLenientNone, + lenientFlags, server[kConnections], ); parser.socket = socket; 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() { diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index d4c3fd688314c2..005cd0a410c2f6 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -23,7 +23,7 @@ const noRestrictedSyntax = [ message: "`btoa` supports only latin-1 charset, use Buffer.from(str).toString('base64') instead", }, { - selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuotaExceededError)$/])', + selector: 'NewExpression[callee.name=/Error$/]:not([callee.name=/^(AssertionError|NghttpError|AbortError|NodeAggregateError|QuicError|QuotaExceededError)$/])', message: "Use an error exported by 'internal/errors' instead.", }, { @@ -421,6 +421,7 @@ export default [ 'node-core/alphabetize-errors': 'error', 'node-core/alphabetize-primordials': 'error', 'node-core/avoid-prototype-pollution': 'error', + 'node-core/iterator-result-done-first': 'error', 'node-core/lowercase-name-for-primitive': 'error', 'node-core/non-ascii-character': 'error', 'node-core/no-array-destructuring': 'error', diff --git a/lib/events.js b/lib/events.js index c6fc170d590aea..7044423692e1bf 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1032,7 +1032,7 @@ async function once(emitter, name, options = kEmptyObject) { } function createIterResult(value, done) { - return { value, done }; + return { done, value }; } function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) { 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/blocklist.js b/lib/internal/blocklist.js index 552819405a1a60..776b510dfd3bc6 100644 --- a/lib/internal/blocklist.js +++ b/lib/internal/blocklist.js @@ -296,4 +296,5 @@ ObjectSetPrototypeOf(InternalBlockList.prototype, BlockList.prototype); module.exports = { BlockList, InternalBlockList, + kHandle, }; 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/crypto/aes.js b/lib/internal/crypto/aes.js index 73fdde03d73ba8..981502c51700be 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,27 +175,31 @@ function aesCipher(mode, key, data, algorithm) { } } -async 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', '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( @@ -203,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/argon2.js b/lib/internal/crypto/argon2.js index 6110c55c16dfb8..6d9f9e462d01fe 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,27 @@ function validateArgon2DeriveBitsLength(length) { } } -async function argon2DeriveBits(algorithm, baseKey, length) { +function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); - let result; - 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, - }, - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - - return TypedArrayPrototypeGetBuffer(result); + const type = { + '__proto__': null, + 'Argon2d': kTypeArgon2d, + 'Argon2i': kTypeArgon2i, + 'Argon2id': kTypeArgon2id, + }[algorithm.name]; + + return jobPromise(() => new Argon2Job( + kCryptoJobWebCrypto, + getCryptoKeyHandle(baseKey), + algorithm.nonce, + algorithm.parallelism, + length / 8, + algorithm.memory, + algorithm.passes, + algorithm.secretValue === undefined ? new Uint8Array() : algorithm.secretValue, + algorithm.associatedData === undefined ? new Uint8Array() : algorithm.associatedData, + type)); } module.exports = { diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 8d26a2888200ff..3e6152b1f55501 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,10 +73,10 @@ function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { } } -async 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 @@ -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) { @@ -182,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( @@ -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..689cab59f3fbf2 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,25 +47,29 @@ function c20pCipher(mode, key, data, algorithm) { algorithm.additionalData)); } -async 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`, '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( @@ -73,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/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 81006c34b34758..d17b06ef155f76 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -19,6 +19,7 @@ const { ECDHConvertKey: _ECDHConvertKey, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -57,6 +58,7 @@ const { const { getArrayBufferOrView, jobPromise, + jobPromiseThen, toBuf, kHandle, } = require('internal/crypto/util'); @@ -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 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); - 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..d102b3fe05a29c 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,10 +77,10 @@ function verifyAcceptableEcKeyUse(name, isPublic, usages) { } } -async 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'])) { @@ -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) { @@ -182,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( @@ -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..73b16da6923024 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 { @@ -27,10 +29,9 @@ const { } = require('internal/crypto/util'); const { - createSecretKey, getCryptoKeyHandle, - getKeyObjectHandle, isKeyObject, + prepareSecretKey, } = require('internal/crypto/keys'); const { @@ -76,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); @@ -97,7 +98,7 @@ function prepareKey(key) { key); } - return getKeyObjectHandle(createSecretKey(key)); + return key; } function hkdf(hash, key, salt, info, length, callback) { @@ -148,26 +149,20 @@ 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); - - try { - return await jobPromise(() => new HKDFJob( - kCryptoJobAsync, - normalizeHashName(hash.name), - getCryptoKeyHandle(baseKey), - salt, - info, - length / 8)); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + return PromiseResolve(new ArrayBuffer(0)); + + return jobPromise(() => new HKDFJob( + kCryptoJobWebCrypto, + normalizeHashName(hash.name), + getCryptoKeyHandle(baseKey), + salt, + info, + length / 8)); } module.exports = { diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 57576f729b7b41..724b2104d4b8c8 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,30 +40,34 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -async 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', '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, usages) { const { name, length = { @@ -73,20 +77,24 @@ async 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`, '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( @@ -94,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`, @@ -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..e2497a2b722b97 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,50 +59,41 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { } } -async 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`, '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) { @@ -145,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( @@ -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..2dea4d00af052f 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,10 +49,10 @@ const { validateJwk, } = require('internal/crypto/webcrypto_util'); -async 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`, @@ -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) { @@ -141,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( @@ -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..f42ce3bbd133d5 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 { @@ -91,11 +92,12 @@ 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 }; } -const pbkdf2Promise = promisify(pbkdf2); function validatePbkdf2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -107,26 +109,20 @@ 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); - - let result; - 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), - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + return PromiseResolve(new ArrayBuffer(0)); - return TypedArrayPrototypeGetBuffer(result); + return jobPromise(() => new PBKDF2Job( + kCryptoJobWebCrypto, + getCryptoKeyHandle(baseKey), + salt, + iterations, + length / 8, + normalizeHashName(hash.name))); } module.exports = { diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 6034ed64e69514..a09dd7b9f0fda9 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,10 +105,10 @@ async function rsaOaepCipher(mode, key, data, algorithm) { algorithm.label)); } -async function rsaKeyGenerate( +function rsaKeyGenerate( algorithm, extractable, - keyUsages, + usages, ) { const publicExponentConverted = bigIntArrayToUnsignedInt(algorithm.publicExponent); if (publicExponentConverted === undefined) { @@ -123,7 +123,7 @@ async function rsaKeyGenerate( hash, } = algorithm; - const usageSet = new SafeSet(keyUsages); + const usageSet = new SafeSet(usages); switch (name) { case 'RSA-OAEP': @@ -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) { @@ -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': { @@ -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/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/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 663375b9e155d2..74d86de3f1b9e1 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -9,12 +9,13 @@ const { DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, - FunctionPrototypeBind, Number, ObjectDefineProperty, ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, + PromisePrototypeThen, + PromiseReject, PromiseWithResolvers, SafeMap, SafeSet, @@ -78,6 +79,7 @@ const { emitExperimentalWarning, filterDuplicateStrings, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -91,6 +93,7 @@ const { isDataView, isArrayBufferView, isAnyArrayBuffer, + isPromise, } = require('internal/util/types'); const kHandle = Symbol('kHandle'); @@ -665,25 +668,80 @@ const validateByteSource = hideStackFrames((val, name) => { val); }); -function onDone(resolve, reject, err, result) { - if (err) { - return reject(lazyDOMException( +// 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) { + try { + return getJob().run(); + } catch (err) { + return PromiseReject(lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err })); } - resolve(result); } -function jobPromise(getJob) { - const { promise, resolve, reject } = PromiseWithResolvers(); +// 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 { - const job = getJob(); - job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); - job.run(); + if (typeof handler === 'function') { + resolveWebCryptoResult(resolve, handler(value)); + } else if (isRejected) { + reject(value); + } else { + resolveWebCryptoResult(resolve, value); + } } catch (err) { - onDone(resolve, reject, err); + reject(err); } - return promise; +} + +// 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 @@ -902,12 +960,16 @@ module.exports = { toBuf, kNamedCurveAliases, + kSupportedAlgorithms, normalizeAlgorithm, normalizeHashName, hasAnyNotIn, validateByteSource, validateKeyOps, jobPromise, + jobPromiseThen, + cleanupWebCryptoResult, + prepareWebCryptoResult, validateMaxBufferLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 1d351ab90bc7c4..05c337d3262229 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1,16 +1,23 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeSlice, FunctionPrototypeCall, JSONParse, JSONStringify, ObjectDefineProperties, + ObjectKeys, + ObjectSetPrototypeOf, + PromiseReject, + PromiseResolve, ReflectApply, ReflectConstruct, + SafeArrayIterator, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeStartsWith, + SymbolIterator, SymbolToStringTag, TypedArrayPrototypeGetBuffer, } = primordials; @@ -23,7 +30,10 @@ const { kWebCryptoCipherDecrypt, } = internalBinding('crypto'); -const { TextDecoder, TextEncoder } = require('internal/encoding'); +const { + decodeUTF8, + encodeUtf8String, +} = internalBinding('encoding_binding'); const { codes: { @@ -51,9 +61,12 @@ const { } = require('internal/crypto/hash'); const { + cleanupWebCryptoResult, getBlockSize, + jobPromiseThen, normalizeAlgorithm, normalizeHashName, + prepareWebCryptoResult, validateMaxBufferLength, } = require('internal/crypto/util'); @@ -61,6 +74,7 @@ const { emitExperimentalWarning, kEnumerableProperty, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -68,9 +82,38 @@ const { randomUUID: _randomUUID, } = require('internal/crypto/random'); +const { + isPromise, +} = require('internal/util/types'); + 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 { + 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); + } +} + +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'); @@ -85,9 +128,9 @@ async function digest(algorithm, data) { context: '2nd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'digest'); + const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); - return await FunctionPrototypeCall(asyncDigest, this, algorithm, data); + return FunctionPrototypeCall(asyncDigest, this, normalizedAlgorithm, data); } function randomUUID() { @@ -95,7 +138,14 @@ function randomUUID() { return _randomUUID(); } -async function generateKey( +function generateKey( + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(generateKeyImpl, this, arguments); +} + +function generateKeyImpl( algorithm, extractable, keyUsages) { @@ -112,24 +162,20 @@ async function generateKey( prefix, context: '2nd argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '3rd argument', }); - algorithm = normalizeAlgorithm(algorithm, 'generateKey'); - let result; - let resultType; - 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': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/rsa') - .rsaKeyGenerate(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/rsa') + .rsaKeyGenerate(normalizedAlgorithm, extractable, usages); case 'Ed25519': // Fall through case 'Ed448': @@ -137,22 +183,16 @@ async function generateKey( case 'X25519': // Fall through case 'X448': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/cfrg') - .cfrgGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/cfrg') + .cfrgGenerateKey(normalizedAlgorithm, extractable, usages); case 'ECDSA': // Fall through case 'ECDH': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ec') - .ecGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/ec') + .ecGenerateKey(normalizedAlgorithm, extractable, usages); case 'HMAC': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') - .hmacGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/mac') + .hmacGenerateKey(normalizedAlgorithm, extractable, usages); case 'AES-CTR': // Fall through case 'AES-CBC': @@ -162,62 +202,40 @@ async function generateKey( case 'AES-OCB': // Fall through case 'AES-KW': - resultType = 'CryptoKey'; - result = await require('internal/crypto/aes') - .aesGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/aes') + .aesGenerateKey(normalizedAlgorithm, extractable, usages); case 'ChaCha20-Poly1305': - resultType = 'CryptoKey'; - result = await require('internal/crypto/chacha20_poly1305') - .c20pGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/chacha20_poly1305') + .c20pGenerateKey(normalizedAlgorithm, extractable, usages); 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') - .mlDsaGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/ml_dsa') + .mlDsaGenerateKey(normalizedAlgorithm, extractable, usages); 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') - .mlKemGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/ml_kem') + .mlKemGenerateKey(normalizedAlgorithm, extractable, usages); case 'KMAC128': // Fall through case 'KMAC256': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') - .kmacGenerateKey(algorithm, extractable, keyUsages); - break; + return require('internal/crypto/mac') + .kmacGenerateKey(normalizedAlgorithm, extractable, usages); 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'); @@ -238,35 +256,35 @@ async function deriveBits(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 await require('internal/crypto/diffiehellman') - .ecdhDeriveBits(algorithm, baseKey, length); + return require('internal/crypto/diffiehellman') + .ecdhDeriveBits(normalizedAlgorithm, baseKey, length); case 'HKDF': - return await require('internal/crypto/hkdf') - .hkdfDeriveBits(algorithm, baseKey, length); + return require('internal/crypto/hkdf') + .hkdfDeriveBits(normalizedAlgorithm, baseKey, length); case 'PBKDF2': - return await require('internal/crypto/pbkdf2') - .pbkdf2DeriveBits(algorithm, baseKey, length); + return require('internal/crypto/pbkdf2') + .pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length); case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': - return await require('internal/crypto/argon2') - .argon2DeriveBits(algorithm, baseKey, length); + return require('internal/crypto/argon2') + .argon2DeriveBits(normalizedAlgorithm, baseKey, length); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } @@ -310,10 +328,19 @@ function getKeyLength({ name, length, hash }) { } } -async function deriveKey( +function deriveKey( algorithm, baseKey, - derivedKeyAlgorithm, + derivedKeyType, + extractable, + keyUsages) { + return callSubtleCryptoMethod(deriveKeyImpl, this, arguments); +} + +function deriveKeyImpl( + algorithm, + baseKey, + derivedKeyType, extractable, keyUsages) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -329,7 +356,7 @@ async function deriveKey( prefix, context: '2nd argument', }); - derivedKeyAlgorithm = webidl.converters.AlgorithmIdentifier(derivedKeyAlgorithm, { + derivedKeyType = webidl.converters.AlgorithmIdentifier(derivedKeyType, { prefix, context: '3rd argument', }); @@ -337,60 +364,67 @@ async function deriveKey( 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 = await require('internal/crypto/diffiehellman') - .ecdhDeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/diffiehellman') + .ecdhDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'HKDF': - bits = await require('internal/crypto/hkdf') - .hkdfDeriveBits(algorithm, baseKey, length); + secret = require('internal/crypto/hkdf') + .hkdfDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'PBKDF2': - bits = await 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 = await 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 FunctionPrototypeCall( + return jobPromiseThen(secret, (secret) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages, - ); + 'raw-secret', + secret, + normalizedDerivedKeyAlgorithmImport, + extractable, + usages, + )); } -async function exportKeySpki(key) { +function exportKeySpki(key) { switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through @@ -432,7 +466,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 +508,7 @@ async function exportKeyPkcs8(key) { } } -async function exportKeyRawPublic(key, format) { +function exportKeyRawPublic(key, format) { switch (getCryptoKeyAlgorithm(key).name) { case 'ECDSA': // Fall through @@ -519,7 +553,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 +574,7 @@ async function exportKeyRawSeed(key) { } } -async function exportKeyRawSecret(key, format) { +function exportKeyRawSecret(key, format) { switch (getCryptoKeyAlgorithm(key).name) { case 'AES-CTR': // Fall through @@ -568,32 +602,26 @@ async function exportKeyRawSecret(key, format) { } } -async function exportKeyJWK(key) { +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': @@ -619,7 +647,7 @@ async function exportKeyJWK(key) { case 'Ed25519': // Fall through case 'Ed448': - parameters.alg = algorithm.name; + alg = algorithm.name; break; case 'AES-CTR': // Fall through @@ -630,48 +658,43 @@ async 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); } -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 +711,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 +762,82 @@ 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); +} + +// 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(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(data, 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': @@ -749,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': @@ -759,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 @@ -777,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 @@ -785,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 @@ -797,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 @@ -812,7 +941,7 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { format, keyData, extractable, - keyUsages); + usages); break; case 'Argon2d': // Fall through @@ -825,7 +954,7 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { format, keyData, extractable, - keyUsages); + usages); } break; case 'ML-DSA-44': @@ -834,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 @@ -842,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; } @@ -862,7 +1001,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, @@ -890,23 +1038,31 @@ async function importKey( 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(). -async function wrapKey(format, key, wrappingKey, algorithm) { +function wrapKey(format, key, wrappingKey, wrapAlgorithm) { + return callSubtleCryptoMethod(wrapKeyImpl, this, arguments); +} + +function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -924,55 +1080,74 @@ async function wrapKey(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 = await FunctionPrototypeCall(exportKey, this, format, key); + const exportedKey = exportKeySync(format, key); + let bytes = exportedKey; 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(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 - 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 (normalizedAlgorithm.name === 'AES-KW' && json.length % 8 !== 0) { + bytes = encodeUtf8String( + json + StringPrototypeRepeat(' ', 8 - (json.length % 8))); } else { - keyData = ec.encode(raw); + bytes = encodeUtf8String(json); } } - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, - algorithm, + normalizedAlgorithm, wrappingKey, - keyData, + bytes, 'wrapKey'); } // subtle.unwrapKey() is essentially a subtle.decrypt() followed // by a subtle.importKey(). -async function unwrapKey( +function unwrapKey( format, wrappedKey, unwrappingKey, - unwrapAlgo, - unwrappedKeyAlgo, + unwrapAlgorithm, + unwrappedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(unwrapKeyImpl, this, arguments); +} + +function unwrapKeyImpl( + format, + wrappedKey, + unwrappingKey, + unwrapAlgorithm, + unwrappedKeyAlgorithm, extractable, keyUsages) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -992,12 +1167,12 @@ async function unwrapKey( 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', @@ -1007,98 +1182,103 @@ async function unwrapKey( 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'); - let keyData = await cipherOrWrap( + const bytes = cipherOrWrap( kWebCryptoCipherDecrypt, - unwrapAlgo, + normalizedAlgorithm, 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 jobPromiseThen(bytes, (bytes) => { + let keyData = bytes; + if (format === 'jwk') { + keyData = parseJwk(bytes); } - } - return FunctionPrototypeCall( - importKeySync, - this, - format, keyData, unwrappedKeyAlgo, extractable, keyUsages, - ); + return FunctionPrototypeCall( + importKeySync, + this, + format, + keyData, + normalizedKeyAlgorithm, + extractable, + usages, + ); + }); } -async function signVerify(algorithm, key, data, signature) { - const op = signature !== undefined ? 'verify' : 'sign'; // This is also usage - algorithm = normalizeAlgorithm(algorithm, op); +function signVerify(algorithm, key, data, signature) { + 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 await require('internal/crypto/rsa') - .rsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/rsa') + .rsaSignVerify(key, data, normalizedAlgorithm, signature); case 'ECDSA': - return await require('internal/crypto/ec') - .ecdsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/ec') + .ecdsaSignVerify(key, data, normalizedAlgorithm, signature); case 'Ed25519': // Fall through case 'Ed448': // Fall through - return await require('internal/crypto/cfrg') - .eddsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/cfrg') + .eddsaSignVerify(key, data, normalizedAlgorithm, signature); case 'HMAC': - return await require('internal/crypto/mac') - .hmacSignVerify(key, data, algorithm, signature); + return require('internal/crypto/mac') + .hmacSignVerify(key, data, normalizedAlgorithm, signature); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - return await require('internal/crypto/ml_dsa') - .mlDsaSignVerify(key, data, algorithm, signature); + return require('internal/crypto/ml_dsa') + .mlDsaSignVerify(key, data, normalizedAlgorithm, signature); case 'KMAC128': // Fall through case 'KMAC256': - return await require('internal/crypto/mac') - .kmacSignVerify(key, data, algorithm, signature); + return require('internal/crypto/mac') + .kmacSignVerify(key, data, normalizedAlgorithm, 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 +1297,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,19 +1327,19 @@ 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, 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 await require('internal/crypto/rsa') - .rsaCipher(mode, key, data, algorithm); + return require('internal/crypto/rsa') + .rsaCipher(mode, key, data, normalizedAlgorithm); case 'AES-CTR': // Fall through case 'AES-CBC': @@ -1163,21 +1347,25 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-GCM': // Fall through case 'AES-OCB': - return await require('internal/crypto/aes') - .aesCipher(mode, key, data, algorithm); + return require('internal/crypto/aes') + .aesCipher(mode, key, data, normalizedAlgorithm); case 'ChaCha20-Poly1305': - return await require('internal/crypto/chacha20_poly1305') - .c20pCipher(mode, key, data, algorithm); + return require('internal/crypto/chacha20_poly1305') + .c20pCipher(mode, key, data, normalizedAlgorithm); case 'AES-KW': - if (op === 'wrapKey' || op === 'unwrapKey') { - return await require('internal/crypto/aes') - .aesCipher(mode, key, data, algorithm); + if (operation === 'wrapKey' || operation === 'unwrapKey') { + return require('internal/crypto/aes') + .aesCipher(mode, key, data, normalizedAlgorithm); } } 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'); @@ -1196,25 +1384,29 @@ async function encrypt(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')) throw lazyDOMException( 'Unable to use this key to encrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, - algorithm, + normalizedAlgorithm, key, data, 'encrypt', ); } -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'); @@ -1233,18 +1425,18 @@ async function decrypt(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')) throw lazyDOMException( 'Unable to use this key to decrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherDecrypt, - algorithm, + normalizedAlgorithm, key, data, 'decrypt', @@ -1252,7 +1444,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'); @@ -1263,7 +1459,7 @@ async function getPublicKey(key, keyUsages) { prefix, context: '1st argument', }); - keyUsages = webidl.converters['sequence'](keyUsages, { + const usages = webidl.converters['sequence'](keyUsages, { prefix, context: '2nd argument', }); @@ -1273,30 +1469,37 @@ 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); + return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, usages); +} + +function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { + return callSubtleCryptoMethod(encapsulateBitsImpl, this, arguments); } -async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { +function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { emitExperimentalWarning('The encapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); 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', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { @@ -1315,43 +1518,65 @@ 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, + keyUsages) { + return callSubtleCryptoMethod(encapsulateKeyImpl, this, arguments); +} + +function encapsulateKeyImpl( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + 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', }); - 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) { @@ -1366,43 +1591,54 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe 'InvalidAccessError'); } - let encapsulateBits; + let encapsulatedBits; switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - encapsulateBits = await require('internal/crypto/ml_kem') + encapsulatedBits = 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 jobPromiseThen(encapsulatedBits, (encapsulatedBits) => { + const sharedKey = FunctionPrototypeCall( + importKeySync, + this, + 'raw-secret', + encapsulatedBits.sharedKey, + normalizedSharedKeyAlgorithm, + extractable, + usages, + ); + + return { + ciphertext: encapsulatedBits.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'); 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', @@ -1412,7 +1648,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,26 +1668,43 @@ 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( - decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages, -) { +function decapsulateKey( + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(decapsulateKeyImpl, this, arguments); +} + +function decapsulateKeyImpl( + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + 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', @@ -1459,21 +1713,26 @@ async function decapsulateKey( 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', }); - 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 +1752,22 @@ 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 jobPromiseThen(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 @@ -1558,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; @@ -1575,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; @@ -1621,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 { @@ -1649,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; 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/lib/internal/errors.js b/lib/internal/errors.js index f989bc8fe60a6e..ba632359fbc185 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1689,14 +1689,12 @@ E('ERR_PERFORMANCE_INVALID_TIMESTAMP', E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); E('ERR_PROXY_INVALID_CONFIG', '%s', Error); E('ERR_PROXY_TUNNEL', '%s', Error); -E('ERR_QUIC_APPLICATION_ERROR', 'A QUIC application error occurred. %d [%s]', Error); E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error); E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error); E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error); E('ERR_QUIC_STREAM_ABORTED', '%s', Error); E('ERR_QUIC_STREAM_RESET', 'The QUIC stream was reset by the peer with error code %d', Error); -E('ERR_QUIC_TRANSPORT_ERROR', 'A QUIC transport error occurred. %d [%s]', Error); E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error); E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) { let message = 'require() cannot be used on an ESM ' + 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/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 0aa01d9b39dc3e..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'); @@ -657,7 +658,7 @@ if (getOptionValue('--experimental-stream-iter')) { done = true; cleanup(); } - return { value: undefined, done: true }; + return { done: true, value: undefined }; } const toRead = remaining > 0 ? MathMin(readSize, remaining) : readSize; @@ -673,20 +674,20 @@ if (getOptionValue('--experimental-stream-iter')) { if (bytesRead === 0) { done = true; cleanup(); - return { value: undefined, done: true }; + return { done: true, value: undefined }; } if (pos >= 0) pos += bytesRead; if (remaining > 0) remaining -= bytesRead; const chunk = bytesRead < toRead ? buf.subarray(0, bytesRead) : buf; - return { value: [chunk], done: false }; + return { done: false, value: [chunk] }; }, return() { if (!done) { done = true; cleanup(); } - return { value: undefined, done: true }; + return { done: true, value: undefined }; }, }; }, @@ -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/http2/core.js b/lib/internal/http2/core.js index 273ddd15414b51..1c6edd65cae8f0 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -838,6 +838,10 @@ function requestOnConnect(headersList, options) { } } +function requestOnError(error) { + this.destroy(error); +} + // Validates that priority options are correct, specifically: // 1. options.weight must be a number // 2. options.parent must be a positive number @@ -1153,7 +1157,7 @@ function setupHandle(socket, type, options) { process.nextTick(emit, this, 'connect', this, socket); } -// Emits a close event followed by an error event if err is truthy. Used +// Emits an error event followed by a close event if err is truthy. Used // by Http2Session.prototype.destroy() function emitClose(self, error) { if (error) @@ -1224,6 +1228,9 @@ function closeSession(session, code, error) { session.setTimeout(0); session.removeAllListeners('timeout'); + const socket = session[kSocket]; + const handle = session[kHandle]; + // Destroy any pending and open streams if (state.pendingStreams.size > 0 || state.streams.size > 0) { const cancel = new ERR_HTTP2_STREAM_CANCEL(error); @@ -1231,10 +1238,6 @@ function closeSession(session, code, error) { state.streams.forEach((stream) => stream.destroy(error)); } - // Disassociate from the socket and server. - const socket = session[kSocket]; - const handle = session[kHandle]; - // Destroy the handle if it exists at this point. if (handle !== undefined) { handle.ondone = finishSessionClose.bind(null, session, error); @@ -1809,11 +1812,15 @@ class ClientHttp2Session extends Http2Session { request(headersParam, options) { debugSessionObj(this, 'initiating request'); - if (this.destroyed) - throw new ERR_HTTP2_INVALID_SESSION(); - - if (this.closed) - throw new ERR_HTTP2_GOAWAY_SESSION(); + // Keep argument validation synchronous, but defer session-state failures + // to the returned stream so request retries from stream callbacks do not + // throw before session lifecycle handlers run. + let requestError; + if (this.destroyed) { + requestError = new ERR_HTTP2_INVALID_SESSION(); + } else if (this.closed) { + requestError = new ERR_HTTP2_GOAWAY_SESSION(); + } this[kUpdateTimer](); @@ -1899,19 +1906,24 @@ class ClientHttp2Session extends Http2Session { } } - const onConnect = reqAsync.bind(requestOnConnect.bind(stream, headersList, options)); - if (this.connecting) { - if (this[kPendingRequestCalls] !== null) { - this[kPendingRequestCalls].push(onConnect); + if (requestError) { + process.nextTick(reqAsync.bind(requestOnError.bind(stream, requestError))); + } else { + const onConnect = reqAsync.bind( + requestOnConnect.bind(stream, headersList, options)); + if (this.connecting) { + if (this[kPendingRequestCalls] !== null) { + this[kPendingRequestCalls].push(onConnect); + } else { + this[kPendingRequestCalls] = [onConnect]; + this.once('connect', () => { + this[kPendingRequestCalls].forEach((f) => f()); + this[kPendingRequestCalls] = null; + }); + } } else { - this[kPendingRequestCalls] = [onConnect]; - this.once('connect', () => { - this[kPendingRequestCalls].forEach((f) => f()); - this[kPendingRequestCalls] = null; - }); + onConnect(); } - } else { - onConnect(); } if (onClientStreamCreatedChannel.hasSubscribers) { 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/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 34a9393ac18f01..5099bc44f8a207 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -14,7 +14,8 @@ const { hardenRegExp, } = primordials; - +const { LoadCache, ResolveCache } = require('internal/modules/esm/module_map'); +const { ModuleJob, ModuleJobSync } = require('internal/modules/esm/module_job'); // This is needed to avoid cycles in esm/resolve <-> cjs/loader const { kIsExecuting, @@ -85,24 +86,6 @@ const { isPromise } = require('internal/util/types'); * @typedef {import('url').URL} URL */ -/** - * Lazy loads the module_map module and returns a new instance of ResolveCache. - * @returns {import('./module_map.js').ResolveCache} - */ -function newResolveCache() { - const { ResolveCache } = require('internal/modules/esm/module_map'); - return new ResolveCache(); -} - -/** - * Generate a load cache (to store the final result of a load-chain for a particular module). - * @returns {import('./module_map.js').LoadCache} - */ -function newLoadCache() { - const { LoadCache } = require('internal/modules/esm/module_map'); - return new LoadCache(); -} - const { translators } = require('internal/modules/esm/translators'); const { defaultResolve } = require('internal/modules/esm/resolve'); const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/esm/load'); @@ -161,12 +144,12 @@ class ModuleLoader { /** * Registry of resolved specifiers */ - #resolveCache = newResolveCache(); + #resolveCache = new ResolveCache(); /** * Registry of loaded modules, akin to `require.cache` */ - loadCache = newLoadCache(); + loadCache = new LoadCache(); /** * @see {AsyncLoaderHooks.isForAsyncLoaderHookWorker} @@ -238,7 +221,6 @@ class ModuleLoader { * @returns {Promise} The module object. */ async executeModuleJob(url, wrap, isEntryPoint = false) { - const { ModuleJob } = require('internal/modules/esm/module_job'); const module = await onImport.tracePromise(async () => { const job = new ModuleJob(this, url, undefined, wrap, kEvaluationPhase, false, false, kImportInImportedESM); this.loadCache.set(url, undefined, job); @@ -289,8 +271,8 @@ class ModuleLoader { let job = this.loadCache.get(url, kImplicitTypeAttribute); // This module job is already created: // 1. If it was loaded by `require()` before, at this point the instantiation - // is already completed and we can check the whether it is in a cycle - // (in that case the module status is kEvaluaing), and whether the + // is already completed and we can check whether it is in a cycle + // (in that case the module status is kEvaluating), and whether the // required graph is synchronous. // 2. If it was loaded by `import` before, only allow it if it's already evaluated // to forbid cycles. @@ -298,7 +280,7 @@ class ModuleLoader { // synchronously so that any previously imported synchronous graph is already // evaluated at this point. // TODO(joyeecheung): add something similar to CJS loader's requireStack to help - // debugging the the problematic links in the graph for import. + // debugging the problematic links in the graph for import. debug('importSyncForRequire', parent?.filename, '->', filename, job); if (job !== undefined) { mod[kRequiredModuleSymbol] = job.module; @@ -357,7 +339,6 @@ class ModuleLoader { const wrap = compileSourceTextModule(url, source, kUser); const inspectBrk = (isMain && getOptionValue('--inspect-brk')); - const { ModuleJobSync } = require('internal/modules/esm/module_job'); job = new ModuleJobSync(this, url, kEmptyObject, wrap, kEvaluationPhase, isMain, inspectBrk, kImportInRequiredESM); this.loadCache.set(url, kImplicitTypeAttribute, job); @@ -587,7 +568,6 @@ class ModuleLoader { assert(moduleOrModulePromise instanceof ModuleWrap, `Expected ModuleWrap for loading ${url}`); } - const { ModuleJob, ModuleJobSync } = require('internal/modules/esm/module_job'); // TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too. const ModuleJobCtor = (requestType === kImportInRequiredESM ? ModuleJobSync : ModuleJob); const isMain = (parentURL === undefined); diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 8d2a97259704a1..86ef400ca7559e 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -145,6 +145,11 @@ function processTypeScriptCode(code, options) { return transformedCode; } +function stripTypeScriptTypesForCoverage(code) { + validateString(code, 'code'); + return processTypeScriptCode(code, { mode: 'strip-only' }); +} + /** * Performs type-stripping to TypeScript source code internally. @@ -205,4 +210,5 @@ function addSourceMap(code, sourceMap) { module.exports = { stripTypeScriptModuleTypes, stripTypeScriptTypes, + stripTypeScriptTypesForCoverage, }; diff --git a/lib/internal/process/permission.js b/lib/internal/process/permission.js index b5da69d08c455e..78e10e6e15fd0d 100644 --- a/lib/internal/process/permission.js +++ b/lib/internal/process/permission.js @@ -43,6 +43,18 @@ module.exports = ObjectFreeze({ return permission.has(scope, reference); }, + drop(scope, reference) { + validateString(scope, 'scope'); + if (reference != null) { + if (isBuffer(reference)) { + validateBuffer(reference, 'reference'); + } else { + validateString(reference, 'reference'); + } + } + + permission.drop(scope, reference); + }, availableFlags() { if (_ffi === undefined) { const { getOptionValue } = require('internal/options'); diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 394c18887f72ce..38ea6675928ad8 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')) { @@ -670,7 +680,7 @@ function initializePermission() { }; // Guarantee path module isn't monkey-patched to bypass permission model ObjectFreeze(require('path')); - const { has } = require('internal/process/permission'); + const { has, drop } = require('internal/process/permission'); const warnFlags = [ '--allow-addons', '--allow-child-process', @@ -722,6 +732,7 @@ function initializePermission() { configurable: false, value: { has, + drop, }, }); } else { diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index a137f04a417a73..d237adccd448cc 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -9,6 +9,7 @@ const { ArrayPrototypePush, BigInt, DataViewPrototypeGetByteLength, + ErrorCaptureStackTrace, FunctionPrototypeBind, Number, ObjectDefineProperties, @@ -36,6 +37,10 @@ if (!process.features.quic || !getOptionValue('--experimental-quic')) { } const { inspect } = require('internal/util/inspect'); +const { + BlockList, + kHandle: kBlockListHandle, +} = require('internal/blocklist'); let debug = require('internal/util/debuglog').debuglog('quic', (fn) => { debug = fn; @@ -104,13 +109,11 @@ const { ERR_INVALID_THIS, ERR_MISSING_ARGS, ERR_OUT_OF_RANGE, - ERR_QUIC_APPLICATION_ERROR, ERR_QUIC_CONNECTION_FAILED, ERR_QUIC_ENDPOINT_CLOSED, ERR_QUIC_OPEN_STREAM_FAILED, ERR_QUIC_STREAM_ABORTED, ERR_QUIC_STREAM_RESET, - ERR_QUIC_TRANSPORT_ERROR, ERR_QUIC_VERSION_NEGOTIATION_ERROR, }, } = require('internal/errors'); @@ -183,6 +186,7 @@ const { kGoaway, kHandshake, kHandshakeCompleted, + kVerifyPeer, kHeaders, kOwner, kRemoveSession, @@ -307,8 +311,18 @@ 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 {number} [sessionCreationRate] Per-host rate limit for session creation (per second) + * @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter + * @property {net.BlockList} [blockList] Block list for filtering incoming packets by source address + * @property {'deny'|'allow'} [blockListPolicy='deny'] How to interpret the block list * @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) @@ -374,6 +388,7 @@ const endpointRegistry = new SafeSet(); * @property {number} [version] The QUIC version * @property {number} [minVersion] The minimum acceptable QUIC version * @property {'use'|'ignore'|'default'} [preferredAddressPolicy] The preferred address policy + * @property {'strict'|'auto'|'manual'} [verifyPeer='auto'] Peer certificate verification policy (client only) * @property {ApplicationOptions} [application] The application options * @property {TransportParams} [transportParams] The transport parameters * @property {string} [servername] The server name identifier (client only) @@ -420,6 +435,7 @@ const endpointRegistry = new SafeSet(); * @property {number} [drainingPeriodMultiplier] Multiplier applied to the * draining period (3 * PTO) used by ngtcp2. Range `3..255`. * **Default:** `3`. + * @property {bigint|number} [streamIdleTimeout] Time in ms before idle peer-initiated streams are destroyed * @property {number} [maxDatagramSendAttempts] Maximum number of times a * datagram is retried before being abandoned. Range `1..255`. * **Default:** `5`. @@ -721,10 +737,12 @@ setCallbacks({ * @param {number} errorType * @param {number} code * @param {string} [reason] + * @param {string} [errorName] Decoded TLS alert name when `code` is a + * CRYPTO_ERROR; otherwise undefined. */ - onSessionClose(errorType, code, reason) { - debug('session close callback', errorType, code, reason); - this[kOwner][kFinishClose](errorType, code, reason); + onSessionClose(errorType, code, reason, errorName) { + debug('session close callback', errorType, code, reason, errorName); + this[kOwner][kFinishClose](errorType, code, reason, errorName); }, /** @@ -906,6 +924,21 @@ setCallbacks({ // from QuicError::ToV8Value. Convert to a proper Node.js Error. if (error !== undefined) { error = convertQuicError(error); + } else if (this[kOwner] && !this[kOwner].destroyed) { + // The stream is closing cleanly, but it may have been reset by the + // peer (ReceiveStreamReset) or locally (resetStream). The C++ side + // records the reset code in state.resetCode. If set, surface the + // reset as the close error so stream.closed rejects -- the reset + // was an abnormal termination even if the session closed cleanly. + const resetCode = getQuicStreamState(this[kOwner]).resetCode; + if (resetCode !== undefined && resetCode > 0n) { + error = makeQuicError( + 'ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', + resetCode, + `stream reset with code ${resetCode}`); + } } debug(`stream ${this[kOwner].id} closed callback with error: ${error}`); this[kOwner][kFinishClose](error); @@ -1026,21 +1059,50 @@ class QuicError extends Error { } } -// Converts a raw QuicError array [type, code, reason] from C++ into a -// proper Node.js Error object. +// Build the human-readable message for an ERR_QUIC_TRANSPORT_ERROR or +// ERR_QUIC_APPLICATION_ERROR. `errorName` is the symbolic name for +// the wire code when known: either the OpenSSL-decoded TLS alert +// (CRYPTO_ERROR; 0x100..0x1ff) or one of the named transport codes +// from RFC 9000 (e.g. PROTOCOL_VIOLATION). Otherwise undefined. +// `reason` is the peer-supplied UTF-8 reason string from the +// CONNECTION_CLOSE / RESET_STREAM frame, often empty. +function quicErrorMessage(prefix, errorCode, reason, errorName) { + let msg = `${prefix} `; + msg += errorName ? `${errorName} (${errorCode})` : `${errorCode}`; + if (reason) msg += `: ${reason}`; + return msg; +} + +function makeQuicError(code, prefix, type, errorCode, reason, errorName) { + const err = new QuicError( + quicErrorMessage(prefix, errorCode, reason, errorName), + { errorCode, code, type }); + ErrorCaptureStackTrace(err, makeQuicError); + if (reason) err.reason = reason; + if (errorName) err.errorName = errorName; + return err; +} + function convertQuicError(error) { const type = error[0]; const code = error[1]; const reason = error[2]; + const errorName = error[3]; switch (type) { case 'transport': - return new ERR_QUIC_TRANSPORT_ERROR(code, reason); + return makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName); case 'application': - return new ERR_QUIC_APPLICATION_ERROR(code, reason); + return makeQuicError('ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', code, reason, errorName); case 'version_negotiation': return new ERR_QUIC_VERSION_NEGOTIATION_ERROR(); default: - return new ERR_QUIC_TRANSPORT_ERROR(code, reason); + return makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName); } } @@ -2620,6 +2682,11 @@ class QuicSession { onkeylog: undefined, onqlog: undefined, pendingQlog: undefined, + // Default to 'manual' (no auto-rejection). Client sessions override + // this via kVerifyPeer in kConnect. Server sessions keep 'manual' + // because server-side cert validation is handled by rejectUnauthorized + // at the C++ level. + verifyPeer: 'manual', handshakeInfo: undefined, /** @type {QuicSessionPath|undefined} */ path: undefined, @@ -3542,7 +3609,7 @@ class QuicSession { * @param {number} code * @param {string} [reason] */ - [kFinishClose](errorType, code, reason) { + [kFinishClose](errorType, code, reason, errorName) { // If code is zero, then we closed without an error. Yay! We can destroy // safely without specifying an error. if (code === 0n) { @@ -3551,7 +3618,8 @@ class QuicSession { return; } - debug('finishing closing the session with an error', errorType, code, reason); + debug('finishing closing the session with an error', + errorType, code, reason, errorName); // If the local side initiated this close with an error code (via // close({ code })), this is an intentional shutdown; not an error. @@ -3578,10 +3646,14 @@ class QuicSession { // session would leak with `closed` hanging forever. switch (errorType) { case 0: /* Transport Error */ - this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason)); + this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName)); break; case 1: /* Application Error */ - this.destroy(new ERR_QUIC_APPLICATION_ERROR(code, reason)); + this.destroy(makeQuicError('ERR_QUIC_APPLICATION_ERROR', + 'QUIC application error', + 'application', code, reason, errorName)); break; case 2: /* Version Negotiation Error */ this.destroy(new ERR_QUIC_VERSION_NEGOTIATION_ERROR()); @@ -3590,7 +3662,9 @@ class QuicSession { this.destroy(); break; default: - this.destroy(new ERR_QUIC_TRANSPORT_ERROR(code, reason)); + this.destroy(makeQuicError('ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', code, reason, errorName)); break; } } @@ -3836,6 +3910,26 @@ class QuicSession { safeCallbackInvoke(inner.onhandshake, this, info); } + // In 'auto' mode, reject the connection if peer certificate validation + // failed. In 'manual' mode, resolve regardless and let the application + // decide. In 'strict' mode, the handshake already failed at the C++ + // level (SSL_VERIFY_PEER) so we won't reach here. + if (inner.verifyPeer === 'auto' && validationErrorReason !== undefined) { + const err = makeQuicError( + 'ERR_QUIC_TRANSPORT_ERROR', + 'QUIC transport error', + 'transport', + 0n, + `Peer certificate validation failed: ${validationErrorReason}` + + ` [${validationErrorCode}]`); + inner.pendingOpen.reject?.(err); + inner.pendingOpen.resolve = undefined; + inner.pendingOpen.reject = undefined; + inner.handshakeCompleted = true; + this.destroy(); + return; + } + inner.pendingOpen.resolve?.(info); inner.pendingOpen.resolve = undefined; inner.pendingOpen.reject = undefined; @@ -3847,6 +3941,14 @@ class QuicSession { return this.#inner.handshakeCompleted; } + get [kVerifyPeer]() { + return this.#inner.verifyPeer; + } + + set [kVerifyPeer](value) { + this.#inner.verifyPeer = value; + } + /** * @param {object} handle * @param {number} direction @@ -3997,10 +4099,20 @@ class QuicEndpoint { tokenExpiration, maxConnectionsPerHost = 100, maxConnectionsTotal = 10_000, - maxStatelessResetsPerHost, disableStatelessReset, addressLRUSize, - maxRetries, + retryRate, + retryBurst, + statelessResetRate, + statelessResetBurst, + versionNegotiationRate, + versionNegotiationBurst, + immediateCloseRate, + immediateCloseBurst, + sessionCreationRate, + sessionCreationBurst, + blockList, + blockListPolicy = 'deny', rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, @@ -4015,6 +4127,16 @@ class QuicEndpoint { tokenSecret, } = options; + if (blockList !== undefined) { + if (!BlockList.isBlockList(blockList)) { + throw new ERR_INVALID_ARG_TYPE('options.blockList', + 'net.BlockList', blockList); + } + } + + validateOneOf(blockListPolicy, 'options.blockListPolicy', + ['deny', 'allow']); + // All of the other options will be validated internally by the C++ code if (address !== undefined && !SocketAddress.isSocketAddress(address)) { if (typeof address === 'string') { @@ -4034,10 +4156,21 @@ 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, + sessionCreationRate, + sessionCreationBurst, + // Pass the C++ handle, not the JS BlockList wrapper. + blockList: blockList?.[kBlockListHandle], + blockListPolicy, rxDiagnosticLoss, txDiagnosticLoss, udpReceiveBufferSize, @@ -4282,6 +4415,10 @@ class QuicEndpoint { // Set callbacks before any async work to avoid missing events // that fire during or immediately after the handshake. applyCallbacks(session, options); + // Store the verifyPeer policy for use in the handshake handler. + if (options.verifyPeer !== undefined) { + session[kVerifyPeer] = options.verifyPeer; + } return session; } @@ -4919,7 +5056,7 @@ function processSessionOptions(options, config = kEmptyObject) { reuseEndpoint = true, version, minVersion, - preferredAddressPolicy = 'default', + preferredAddressPolicy = 'ignore', transportParams = kEmptyObject, qlog = false, sessionTicket, @@ -4935,6 +5072,8 @@ function processSessionOptions(options, config = kEmptyObject) { datagramDropPolicy = 'drop-oldest', drainingPeriodMultiplier = 3, maxDatagramSendAttempts = 5, + streamIdleTimeout, + verifyPeer = 'auto', // HTTP/3 application-specific options. Nested under `application` // to separate protocol-specific settings from transport-level ones. application = kEmptyObject, @@ -4981,6 +5120,9 @@ function processSessionOptions(options, config = kEmptyObject) { validateOneOf(datagramDropPolicy, 'options.datagramDropPolicy', ['drop-oldest', 'drop-newest']); + validateOneOf(verifyPeer, 'options.verifyPeer', + ['strict', 'auto', 'manual']); + validateInteger(drainingPeriodMultiplier, 'options.drainingPeriodMultiplier', 3, 255); @@ -5030,7 +5172,19 @@ function processSessionOptions(options, config = kEmptyObject) { preferredAddressIpv4: preferredAddressIpv4?.[kSocketAddressHandle], preferredAddressIpv6: preferredAddressIpv6?.[kSocketAddressHandle], }, - tls: processTlsOptions(options, forServer), + tls: { + ...processTlsOptions(options, forServer), + // Forward strict mode to C++ so SSL_VERIFY_PEER is set on the + // client SSL_CTX. For 'auto' and 'manual' modes, the handshake + // completes regardless and the result is handled in JS. + verifyPeerStrict: verifyPeer === 'strict', + // Enable hostname verification for 'strict' and 'auto' modes. + // SSL_set1_host tells OpenSSL to verify the server certificate's + // SAN/CN matches the servername. Without this, a valid cert for + // any domain would be accepted. + verifyHostname: verifyPeer !== 'manual', + }, + verifyPeer, qlog, maxPayloadSize, unacknowledgedPacketThreshold, @@ -5045,6 +5199,7 @@ function processSessionOptions(options, config = kEmptyObject) { datagramDropPolicy, drainingPeriodMultiplier, maxDatagramSendAttempts, + streamIdleTimeout, application, onerror, onstream, diff --git a/lib/internal/quic/stats.js b/lib/internal/quic/stats.js index b6500e6700713e..cd986827c12c47 100644 --- a/lib/internal/quic/stats.js +++ b/lib/internal/quic/stats.js @@ -60,9 +60,15 @@ 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_ENDPOINT_SESSION_CREATION_RATE_LIMITED, + IDX_STATS_ENDPOINT_PACKETS_BLOCKED, IDX_STATS_SESSION_CREATED_AT, IDX_STATS_SESSION_DESTROYED_AT, @@ -95,6 +101,7 @@ const { IDX_STATS_SESSION_DATAGRAMS_SENT, IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED, IDX_STATS_SESSION_DATAGRAMS_LOST, + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT, IDX_STATS_SESSION_COUNT, IDX_STATS_STREAM_CREATED_AT, @@ -123,9 +130,15 @@ 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_ENDPOINT_SESSION_CREATION_RATE_LIMITED !== undefined); +assert(IDX_STATS_ENDPOINT_PACKETS_BLOCKED !== undefined); assert(IDX_STATS_SESSION_CREATED_AT !== undefined); assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined); assert(IDX_STATS_SESSION_CLOSING_AT !== undefined); @@ -157,6 +170,7 @@ assert(IDX_STATS_SESSION_DATAGRAMS_RECEIVED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_SENT !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED !== undefined); assert(IDX_STATS_SESSION_DATAGRAMS_LOST !== undefined); +assert(IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT !== undefined); assert(IDX_STATS_STREAM_CREATED_AT !== undefined); assert(IDX_STATS_STREAM_OPENED_AT !== undefined); assert(IDX_STATS_STREAM_RECEIVED_AT !== undefined); @@ -280,24 +294,60 @@ 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]; + } + + /** @type {bigint} */ + get sessionCreationRateLimited() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED]; + } + + /** @type {bigint} */ + get packetsBlocked() { + assertIsQuicEndpointStats(this); + return this.#handle[IDX_STATS_ENDPOINT_PACKETS_BLOCKED]; + } + toString() { return JSONStringify(this.toJSON()); } @@ -315,9 +365,15 @@ class QuicEndpointStats { clientSessions, serverBusyCount, retryCount, + retryRateLimited, versionNegotiationCount, + versionNegotiationRateLimited, statelessResetCount, + statelessResetRateLimited, immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, } = this; return { __proto__: null, @@ -334,9 +390,15 @@ class QuicEndpointStats { clientSessions: `${clientSessions}`, serverBusyCount: `${serverBusyCount}`, retryCount: `${retryCount}`, + retryRateLimited: `${retryRateLimited}`, versionNegotiationCount: `${versionNegotiationCount}`, + versionNegotiationRateLimited: `${versionNegotiationRateLimited}`, statelessResetCount: `${statelessResetCount}`, + statelessResetRateLimited: `${statelessResetRateLimited}`, immediateCloseCount: `${immediateCloseCount}`, + immediateCloseRateLimited: `${immediateCloseRateLimited}`, + sessionCreationRateLimited: `${sessionCreationRateLimited}`, + packetsBlocked: `${packetsBlocked}`, }; } @@ -363,9 +425,15 @@ class QuicEndpointStats { clientSessions, serverBusyCount, retryCount, + retryRateLimited, versionNegotiationCount, + versionNegotiationRateLimited, statelessResetCount, + statelessResetRateLimited, immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, } = this; return `QuicEndpointStats ${inspect({ @@ -380,9 +448,15 @@ class QuicEndpointStats { clientSessions, serverBusyCount, retryCount, + retryRateLimited, versionNegotiationCount, + versionNegotiationRateLimited, statelessResetCount, + statelessResetRateLimited, immediateCloseCount, + immediateCloseRateLimited, + sessionCreationRateLimited, + packetsBlocked, }, opts)}`; } @@ -617,6 +691,13 @@ class QuicSessionStats { return this.#handle[this.#offset + IDX_STATS_SESSION_DATAGRAMS_LOST]; } + /** @type {bigint} */ + get streamsIdleTimedOut() { + assertIsQuicSessionStats(this); + return this.#handle[this.#offset + + IDX_STATS_SESSION_STREAMS_IDLE_TIMED_OUT]; + } + toString() { return JSONStringify(this.toJSON()); } @@ -654,6 +735,7 @@ class QuicSessionStats { datagramsSent, datagramsAcknowledged, datagramsLost, + streamsIdleTimedOut, } = this; return { __proto__: null, @@ -690,6 +772,7 @@ class QuicSessionStats { datagramsSent: `${datagramsSent}`, datagramsAcknowledged: `${datagramsAcknowledged}`, datagramsLost: `${datagramsLost}`, + streamsIdleTimedOut: `${streamsIdleTimedOut}`, }; } @@ -735,6 +818,7 @@ class QuicSessionStats { datagramsSent, datagramsAcknowledged, datagramsLost, + streamsIdleTimedOut, } = this; return `QuicSessionStats ${inspect({ @@ -769,6 +853,7 @@ class QuicSessionStats { datagramsSent, datagramsAcknowledged, datagramsLost, + streamsIdleTimedOut, }, opts)}`; } diff --git a/lib/internal/quic/symbols.js b/lib/internal/quic/symbols.js index 75f5b72ae22669..288d3ebae2a8e8 100644 --- a/lib/internal/quic/symbols.js +++ b/lib/internal/quic/symbols.js @@ -38,6 +38,7 @@ const kFinishClose = Symbol('kFinishClose'); const kGoaway = Symbol('kGoaway'); const kHandshake = Symbol('kHandshake'); const kHandshakeCompleted = Symbol('kHandshakeCompleted'); +const kVerifyPeer = Symbol('kVerifyPeer'); const kHeaders = Symbol('kHeaders'); const kKeylog = Symbol('kKeylog'); const kListen = Symbol('kListen'); @@ -70,6 +71,7 @@ module.exports = { kGoaway, kHandshake, kHandshakeCompleted, + kVerifyPeer, kHeaders, kInspect, kKeylog, diff --git a/lib/internal/streams/compose.js b/lib/internal/streams/compose.js index d664a430b8d75a..7baa7974ffdf05 100644 --- a/lib/internal/streams/compose.js +++ b/lib/internal/streams/compose.js @@ -82,7 +82,6 @@ module.exports = function compose(...streams) { let ondrain; let onfinish; - let onreadable; let onclose; let d; @@ -184,31 +183,19 @@ module.exports = function compose(...streams) { if (readable) { if (isNodeStream(tail)) { - tail.on('readable', function() { - if (onreadable) { - const cb = onreadable; - onreadable = null; - cb(); + d._read = function() { + tail.resume(); + }; + + tail.on('data', function(chunk) { + if (!d.push(chunk)) { + tail.pause(); } }); tail.on('end', function() { d.push(null); }); - - d._read = function() { - while (true) { - const buf = tail.read(); - if (buf === null) { - onreadable = d._read; - return; - } - - if (!d.push(buf)) { - return; - } - } - }; } else if (isWebStream(tail)) { const readable = isTransformStream(tail) ? tail.readable : tail; const reader = readable.getReader(); @@ -238,7 +225,6 @@ module.exports = function compose(...streams) { err = new AbortError(); } - onreadable = null; ondrain = null; onfinish = null; diff --git a/lib/internal/streams/fast-utf8-stream.js b/lib/internal/streams/fast-utf8-stream.js index cb86f245302620..68601cf5c388c7 100644 --- a/lib/internal/streams/fast-utf8-stream.js +++ b/lib/internal/streams/fast-utf8-stream.js @@ -6,15 +6,12 @@ const { ArrayPrototypePush, - AtomicsWait, - Int32Array, MathMax, - Number, SymbolDispose, } = primordials; const { - constructSharedArrayBuffer, + sleep, } = require('internal/util'); const { @@ -50,22 +47,6 @@ const { const BUSY_WRITE_TIMEOUT = 100; const kEmptyBuffer = Buffer.allocUnsafe(0); -const kNil = new Int32Array(constructSharedArrayBuffer(4)); - -function sleep(ms) { - // Also filters out NaN, non-number types, including empty strings, but allows bigints - const valid = ms > 0 && ms < Infinity; - if (valid === false) { - if (typeof ms !== 'number' && typeof ms !== 'bigint') { - throw new ERR_INVALID_ARG_TYPE('ms', ['number', 'bigint'], ms); - } - throw new ERR_INVALID_ARG_VALUE.RangeError('ms', ms, - 'must be a number greater than 0 and less than Infinity'); - } - - AtomicsWait(kNil, 0, 0, Number(ms)); -} - // 16 KB. Don't write more than docker buffer size. // https://github.com/moby/moby/blob/513ec73831269947d38a644c278ce3cac36783b2/daemon/logger/copier.go#L13 const kMaxWrite = 16 * 1024; diff --git a/lib/internal/streams/iter/duplex.js b/lib/internal/streams/iter/duplex.js index 591837f70eb4cb..bd06f37303cfc6 100644 --- a/lib/internal/streams/iter/duplex.js +++ b/lib/internal/streams/iter/duplex.js @@ -74,9 +74,7 @@ function duplex(options = { __proto__: null }) { if (aClosed) return; aClosed = true; // End the writer (signals end-of-stream to B's readable) - if (aWriter.endSync() < 0) { - await aWriter.end(); - } + aWriter.endSync(); // Stop iteration of this channel's readable if (aReadableIterator?.return) { await aReadableIterator.return(); @@ -104,9 +102,7 @@ function duplex(options = { __proto__: null }) { async close() { if (bClosed) return; bClosed = true; - if (bWriter.endSync() < 0) { - await bWriter.end(); - } + bWriter.endSync(); if (bReadableIterator?.return) { await bReadableIterator.return(); bReadableIterator = null; 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/lib/internal/streams/iter/push.js b/lib/internal/streams/iter/push.js index 1c367ff02bae71..f00ad396e1f2ca 100644 --- a/lib/internal/streams/iter/push.js +++ b/lib/internal/streams/iter/push.js @@ -274,7 +274,10 @@ class PushQueue { if (this.#writerState === 'errored') { return -2; // Signal to reject with stored error } - if (this.#writerState === 'closing' || this.#writerState === 'closed') { + if (this.#writerState === 'closing') { + return -3; // Signal to PushWriter: wait for drain to complete + } + if (this.#writerState === 'closed') { return this.#bytesWritten; // Idempotent } @@ -397,17 +400,17 @@ class PushQueue { if (this.#writerState === 'closing' && this.#slots.length === 0) { this.endDrained(); } - return { __proto__: null, value: result, done: false }; + return { __proto__: null, done: false, value: result }; } // Buffer empty and writer closing = drain complete if (this.#writerState === 'closing') { this.endDrained(); - return { __proto__: null, value: undefined, done: true }; + return { __proto__: null, done: true, value: undefined }; } if (this.#writerState === 'closed') { - return { __proto__: null, value: undefined, done: true }; + return { __proto__: null, done: true, value: undefined }; } if (this.#writerState === 'errored' && this.#error) { @@ -423,6 +426,7 @@ class PushQueue { if (this.#consumerState !== 'active') return; this.#consumerState = 'returned'; this.#cleanup(); + this.#resolvePendingReads(); this.#rejectPendingWrites( new ERR_INVALID_STATE.TypeError('Stream closed by consumer')); // If closing, reject the pending end promise @@ -440,7 +444,12 @@ class PushQueue { this.#consumerState = 'thrown'; this.#error = error; this.#cleanup(); + this.#rejectPendingReads(error); this.#rejectPendingWrites(error); + if (this.#writerState === 'closing' && this.#pendingEnd) { + this.#pendingEnd.reject(error); + this.#pendingEnd = null; + } // Reject pending drains - the consumer errored this.#rejectPendingDrains(error); } @@ -471,17 +480,20 @@ class PushQueue { const pending = this.#pendingReads.shift(); const result = this.#drain(); this.#resolvePendingWrites(); - pending.resolve({ __proto__: null, value: result, done: false }); + pending.resolve({ __proto__: null, done: false, value: result }); } else if (this.#writerState === 'closing' && this.#slots.length === 0) { this.endDrained(); const pending = this.#pendingReads.shift(); - pending.resolve({ __proto__: null, value: undefined, done: true }); + pending.resolve({ __proto__: null, done: true, value: undefined }); } else if (this.#writerState === 'closed') { const pending = this.#pendingReads.shift(); - pending.resolve({ __proto__: null, value: undefined, done: true }); + pending.resolve({ __proto__: null, done: true, value: undefined }); } else if (this.#writerState === 'errored' && this.#error) { const pending = this.#pendingReads.shift(); pending.reject(this.#error); + } else if (this.#consumerState === 'returned') { + const pending = this.#pendingReads.shift(); + pending.resolve({ __proto__: null, done: true, value: undefined }); } else { break; } @@ -636,6 +648,10 @@ class PushWriter { if (result === -3) { // Closing: buffer has data, create deferred promise that resolves // when consumer drains past the end sentinel + const pendingEndPromise = this.#queue.pendingEndPromise; + if (pendingEndPromise !== null) { + return pendingEndPromise; + } const { promise, resolve, reject } = PromiseWithResolvers(); this.#queue.setPendingEnd({ __proto__: null, promise, resolve, reject }); return promise; @@ -687,11 +703,11 @@ function createReadable(queue) { }, async return() { queue.consumerReturn(); - return { __proto__: null, value: undefined, done: true }; + return { __proto__: null, done: true, value: undefined }; }, async throw(error) { queue.consumerThrow(error); - return { __proto__: null, value: undefined, done: true }; + return { __proto__: null, done: true, value: undefined }; }, }; }, diff --git a/lib/internal/streams/iter/share.js b/lib/internal/streams/iter/share.js index 3cd24409222712..f755550712efa7 100644 --- a/lib/internal/streams/iter/share.js +++ b/lib/internal/streams/iter/share.js @@ -113,6 +113,7 @@ class ShareImpl { resolve: null, reject: null, detached: false, + pendingNext: PromiseResolve(), }; this.#consumers.add(state); @@ -129,62 +130,72 @@ class ShareImpl { return { __proto__: null, [SymbolAsyncIterator]() { - return { - __proto__: null, - async next() { - if (self.#sourceError) { - state.detached = true; - self.#consumers.delete(state); - throw self.#sourceError; + const getNext = async () => { + if (self.#sourceError) { + state.detached = true; + self.#consumers.delete(state); + throw self.#sourceError; + } + + // Loop until we get data, source is exhausted, or + // consumer is detached. Multiple consumers may be woken + // after a single pull - those that find no data at their + // cursor must re-pull rather than terminating prematurely. + for (;;) { + if (state.detached) { + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; } - // Loop until we get data, source is exhausted, or - // consumer is detached. Multiple consumers may be woken - // after a single pull - those that find no data at their - // cursor must re-pull rather than terminating prematurely. - for (;;) { - if (state.detached) { - if (self.#sourceError) throw self.#sourceError; - return { __proto__: null, done: true, value: undefined }; - } + if (self.#cancelled) { + state.detached = true; + self.#deleteConsumer(state); + return { __proto__: null, done: true, value: undefined }; + } - if (self.#cancelled) { - state.detached = true; - self.#deleteConsumer(state); - return { __proto__: null, done: true, value: undefined }; + // Check if data is available in buffer + const bufferIndex = state.cursor - self.#bufferStart; + if (bufferIndex < self.#buffer.length) { + const chunk = self.#buffer.get(bufferIndex); + const cursor = state.cursor; + state.cursor++; + if (cursor === self.#cachedMinCursor && + --self.#cachedMinCursorConsumers === 0) { + self.#tryTrimBuffer(); } + return { __proto__: null, done: false, value: chunk }; + } - // Check if data is available in buffer - const bufferIndex = state.cursor - self.#bufferStart; - if (bufferIndex < self.#buffer.length) { - const chunk = self.#buffer.get(bufferIndex); - const cursor = state.cursor; - state.cursor++; - if (cursor === self.#cachedMinCursor && - --self.#cachedMinCursorConsumers === 0) { - self.#tryTrimBuffer(); - } - return { __proto__: null, done: false, value: chunk }; - } + if (self.#sourceExhausted) { + state.detached = true; + self.#deleteConsumer(state); + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } - if (self.#sourceExhausted) { - state.detached = true; - self.#deleteConsumer(state); - if (self.#sourceError) throw self.#sourceError; - return { __proto__: null, done: true, value: undefined }; - } + // Need to pull from source - check buffer limit + const canPull = await self.#waitForBufferSpace(); + if (!canPull) { + state.detached = true; + self.#deleteConsumer(state); + if (self.#sourceError) throw self.#sourceError; + return { __proto__: null, done: true, value: undefined }; + } - // Need to pull from source - check buffer limit - const canPull = await self.#waitForBufferSpace(); - if (!canPull) { - state.detached = true; - self.#deleteConsumer(state); - if (self.#sourceError) throw self.#sourceError; - return { __proto__: null, done: true, value: undefined }; - } + await self.#pullFromSource(); + } + }; - await self.#pullFromSource(); - } + return { + __proto__: null, + next() { + const next = PromisePrototypeThen( + state.pendingNext, + getNext, + getNext); + state.pendingNext = + PromisePrototypeThen(next, undefined, () => {}); + return next; }, async return() { diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..37ff2473b68011 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -16,6 +16,7 @@ const { StringPrototypeIncludes, StringPrototypeLocaleCompare, StringPrototypeStartsWith, + StringPrototypeTrim, } = primordials; const { copyFileSync, @@ -44,6 +45,20 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +const kTypeOnlyImportRegex = /^\s*import\s+type\b/u; +const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u; + +let stripTypeScriptTypesForCoverage; + +function getStripTypeScriptTypesForCoverage() { + if (!process.config.variables.node_use_amaro) { + return; + } + + stripTypeScriptTypesForCoverage ??= + require('internal/modules/typescript').stripTypeScriptTypesForCoverage; + return stripTypeScriptTypesForCoverage; +} class CoverageLine { constructor(line, startOffset, src, length = src?.length) { @@ -69,6 +84,7 @@ class TestCoverage { } #sourceLines = new SafeMap(); + #typeScriptLines = new SafeSet(); getLines(fileUrl, source) { // Split the file source into lines. Make sure the lines maintain their @@ -133,6 +149,57 @@ class TestCoverage { return lines; } + markTypeScriptOnlyLines(fileUrl, source) { + if (this.#typeScriptLines.has(fileUrl)) { + return; + } + this.#typeScriptLines.add(fileUrl); + + if (RegExpPrototypeExec(kTypeScriptSourceRegex, fileUrl) === null) { + return; + } + + const lines = this.getLines(fileUrl, source); + if (!lines) { + return; + } + + let strippedLines; + const stripSource = getStripTypeScriptTypesForCoverage(); + + if (stripSource) { + source ??= readFileSync(fileURLToPath(fileUrl), 'utf8'); + + try { + strippedLines = RegExpPrototypeSymbolSplit( + kLineSplitRegex, + stripSource(source), + ); + } catch { + strippedLines = undefined; + } + } + + for (let i = 0; i < lines.length; ++i) { + const originalLine = lines[i].src; + + if (StringPrototypeTrim(originalLine).length === 0) { + continue; + } + + if (strippedLines?.[i] !== undefined) { + if (StringPrototypeTrim(strippedLines[i]).length === 0) { + lines[i].ignore = true; + } + continue; + } + + if (RegExpPrototypeExec(kTypeOnlyImportRegex, originalLine) !== null) { + lines[i].ignore = true; + } + } + } + summary() { internalBinding('profiler').takeCoverage(); const coverage = this.getCoverageFromDirectory(); @@ -368,10 +435,12 @@ class TestCoverage { offset += length + 1; return coverageLine; }); - if (data.sourcesContent != null) { - for (let j = 0; j < data.sources.length; ++j) { - this.getLines(data.sources[j], data.sourcesContent[j]); + for (let j = 0; j < data.sources.length; ++j) { + const source = data.sourcesContent?.[j]; + if (source != null) { + this.getLines(data.sources[j], source); } + this.markTypeScriptOnlyLines(data.sources[j], source); } const sourceMap = new SourceMap(data, { __proto__: null, lineLengths }); @@ -533,6 +602,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/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/runner.js b/lib/internal/test_runner/runner.js index d6cb6438d2b52a..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); @@ -871,6 +880,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/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 7210ff38f2b518..d19d6349f104bf 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]; @@ -1811,15 +1812,22 @@ class Suite extends Test { publishError(err); } this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)); - } finally { - if (testChannel.end.hasSubscribers) { - publishEnd(); - } } + this.#publishEnd = publishEnd; this.buildPhaseFinished = true; } + #publishEnd = null; + + #publishSuiteEnd() { + const publishEnd = this.#publishEnd; + this.#publishEnd = null; + if (publishEnd !== null && testChannel.end.hasSubscribers) { + publishEnd(); + } + } + #ctx; getCtx() { this.#ctx ??= new TestContext(this); @@ -1871,6 +1879,7 @@ class Suite extends Test { } } finally { stopPromise?.[SymbolDispose](); + this.#publishSuiteEnd(); } this.postRun(); diff --git a/lib/internal/url.js b/lib/internal/url.js index dfcd071073a8d3..446c22f5b66067 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -242,8 +242,8 @@ class URLSearchParamsIterator { const len = values.length; if (index >= len) { return { - value: undefined, done: true, + value: undefined, }; } @@ -261,8 +261,8 @@ class URLSearchParamsIterator { } return { - value: result, done: false, + value: result, }; } diff --git a/lib/internal/util.js b/lib/internal/util.js index 34af9ca6f61a6f..2f72e636ab90dc 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -17,7 +17,6 @@ const { ObjectGetOwnPropertyDescriptors, ObjectGetPrototypeOf, ObjectKeys, - ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, ObjectValues, Promise, @@ -354,26 +353,6 @@ function cachedResult(fn) { }; } -// Useful for Wrapping an ES6 Class with a constructor Function that -// does not require the new keyword. For instance: -// class A { constructor(x) {this.x = x;}} -// const B = createClassWrapper(A); -// B() instanceof A // true -// B() instanceof B // true -function createClassWrapper(type) { - function fn(...args) { - return ReflectConstruct(type, args, new.target || type); - } - // Mask the wrapper function name and length values - ObjectDefineProperties(fn, { - name: { __proto__: null, value: type.name }, - length: { __proto__: null, value: type.length }, - }); - ObjectSetPrototypeOf(fn, type); - fn.prototype = type.prototype; - return fn; -} - let signalsToNamesMapping; function getSignalsToNamesMapping() { if (signalsToNamesMapping !== undefined) @@ -649,16 +628,6 @@ function exposeNamespace(target, name, namespaceObject) { }); } -function exposeGetterAndSetter(target, name, getter, setter = undefined) { - ObjectDefineProperty(target, name, { - __proto__: null, - enumerable: false, - configurable: true, - get: getter, - set: setter, - }); -} - function defineReplaceableLazyAttribute(target, id, keys, writable = true, check) { let mod; for (let i = 0; i < keys.length; i++) { @@ -726,24 +695,13 @@ const lazyDOMException = (message, name) => { }; -const kEnumerableProperty = { __proto__: null }; -kEnumerableProperty.enumerable = true; -ObjectFreeze(kEnumerableProperty); +const kEnumerableProperty = ObjectFreeze({ + __proto__: null, + enumerable: true, +}); const kEmptyObject = ObjectFreeze({ __proto__: null }); -function filterOwnProperties(source, keys) { - const filtered = { __proto__: null }; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (ObjectPrototypeHasOwnProperty(source, key)) { - filtered[key] = source[key]; - } - } - - return filtered; -} - /** * Mimics `obj[key] = value` but ignoring potential prototype inheritance. * @param {any} obj @@ -976,7 +934,6 @@ module.exports = { constructSharedArrayBuffer, convertProcessSignalToExitCode, convertToValidSignal, - createClassWrapper, decorateErrorStack, defineOperation, defineLazyProperties, @@ -989,13 +946,10 @@ module.exports = { exposeInterface, exposeLazyInterfaces, exposeNamespace, - exposeGetterAndSetter, filterDuplicateStrings, - filterOwnProperties, getConstructorOf, getCIDR, getCWDURL, - getInternalGlobal, getStructuredStack, getSystemErrorMap, getSystemErrorName, 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..6af91c4bf7aca4 --- /dev/null +++ b/lib/internal/vfs/errors.js @@ -0,0 +1,205 @@ +'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, + UV_EXDEV, +} = 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; +} + +function createEXDEV(syscall, path) { + const err = new UVException({ + errno: UV_EXDEV, + syscall, + path, + }); + ErrorCaptureStackTrace(err, createEXDEV); + return err; +} + +module.exports = { + createENOENT, + createENOTDIR, + createENOTEMPTY, + createEISDIR, + createEBADF, + createEEXIST, + createEROFS, + createEINVAL, + createELOOP, + createEACCES, + createEXDEV, +}; 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..ae38639582581b --- /dev/null +++ b/lib/internal/vfs/file_system.js @@ -0,0 +1,1271 @@ +'use strict'; + +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 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 + * 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[kMountPoint] = null; + this[kMounted] = false; + this[kPromises] = null; // Lazy-initialized + } + + /** + * Gets the underlying provider. + * @returns {VirtualProvider} + */ + get provider() { + 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} + */ + get readonly() { + 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 ==================== + + /** + * 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) ==================== + + /** + * 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); + const realProviderPath = this[kProvider].realpathSync(providerPath, options); + return this.#toMountedPath(realProviderPath); + } + + /** + * 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 this.#toMountedPath(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, this.#toMountedPath(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); + const toMountedPath = (p) => this.#toMountedPath(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 toMountedPath(await 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 toMountedPath(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/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/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..2b6b807ee85b44 --- /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({ done: false, value: event }); + } 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({ done: true, value: undefined }); + } + }); + + // 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({ done: true, value: undefined }); + } + + if (this.#pendingEvents.length > 0) { + const event = this.#pendingEvents.shift(); + return PromiseResolve({ done: false, value: event }); + } + + return new Promise((resolve, reject) => { + ArrayPrototypePush(this.#pendingResolvers, { resolve, reject }); + }); + } + + /** + * Closes the iterator and underlying watcher. + * @returns {Promise} + */ + return() { + this.#watcher.close(); + return PromiseResolve({ done: true, value: undefined }); + } + + /** + * Handles iterator throw. + * @param {Error} error The error to throw + * @returns {Promise} + */ + throw(error) { + this.#watcher.close(); + return PromiseResolve({ done: true, value: undefined }); + } +} + +module.exports = { + VFSWatcher, + VFSStatWatcher, + VFSWatchAsyncIterable, +}; diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 876e3a5bf6e2f0..a46289723a7f43 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -761,7 +761,7 @@ class ReadableStreamAsyncIteratorReadRequest { [kChunk](chunk) { this.state.current = undefined; - this.promise.resolve({ value: chunk, done: false }); + this.promise.resolve({ done: false, value: chunk }); } [kClose]() { @@ -785,11 +785,11 @@ class DefaultReadRequest { } [kChunk](value) { - this[kState].resolve?.({ value, done: false }); + this[kState].resolve?.({ done: false, value }); } [kClose]() { - this[kState].resolve?.({ value: undefined, done: true }); + this[kState].resolve?.({ done: true, value: undefined }); } [kError](error) { @@ -805,11 +805,11 @@ class ReadIntoRequest { } [kChunk](value) { - this[kState].resolve?.({ value, done: false }); + this[kState].resolve?.({ done: false, value }); } [kClose](value) { - this[kState].resolve?.({ value, done: true }); + this[kState].resolve?.({ done: true, value }); } [kError](error) { @@ -875,7 +875,7 @@ class ReadableStreamDefaultReader { readableStreamDefaultControllerCallPullIfNeeded(controller); } - return PromiseResolve({ value: chunk, done: false }); + return PromiseResolve({ done: false, value: chunk }); } // Slow path: create request and go through normal flow 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; 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/node.gyp b/node.gyp index 1a724ccb771342..28db2a955b74d6 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': [ @@ -1032,11 +1033,6 @@ '@rpath/lib<(node_core_target_name).<(shlib_suffix)' }, }], - [ 'node_use_node_code_cache=="true"', { - 'defines': [ - 'NODE_USE_NODE_CODE_CACHE=1', - ], - }], ['node_shared=="true" and OS in "aix os400"', { 'product_name': 'node_base', }], 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..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 (mode == kCryptoJobAsync) { - tracker->TrackFieldWithSize("pass", pass.size()); + if (key) tracker->TrackField("key", key); + if (IsCryptoJobAsync(mode)) { + 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]] { @@ -84,8 +98,7 @@ Maybe Argon2Traits::AdditionalConfig( return Nothing(); } - const bool isAsync = mode == kCryptoJobAsync; - config->pass = isAsync ? pass.ToCopy() : pass.ToByteSource(); + const bool isAsync = IsCryptoJobAsync(mode); 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_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_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_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..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]); @@ -77,13 +89,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 @@ -102,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(), @@ -128,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 (mode == kCryptoJobAsync) { + 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_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..09fbf0844f48f2 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 + ->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; + } + // 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..1702dfabb4af2a 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->DefineOwnProperty(env->context(), + OneByteString(isolate, "publicKey"), + public_key) + .FromMaybe(false) || + !ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "privateKey"), + private_key) + .FromMaybe(false)) { + 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..c1b7aee576519e 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; @@ -155,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) { @@ -1716,6 +1719,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..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 (mode == kCryptoJobAsync) { - tracker->TrackFieldWithSize("pass", pass.size()); + // If the job is sync, PBKDF2Config does not own the data. + if (key) tracker->TrackField("key", key); + if (IsCryptoJobAsync(mode)) { + 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,13 +89,7 @@ Maybe PBKDF2Traits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? 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 @@ -116,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; 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 e722f87b23fcbe..acec4b993613cd 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()); } @@ -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_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..42b248d84b43e5 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; @@ -702,17 +705,72 @@ 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) { 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 +838,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..742f23b0f5e789 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,80 @@ 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 (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; + } + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } + + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } + if (!exception.IsEmpty()) { + USE(resolver->Reject(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 +545,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/src/encoding_binding.cc b/src/encoding_binding.cc index c569375383e8d9..9c84d24c84576d 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -459,14 +459,15 @@ void BindingData::DecodeUTF8(const FunctionCallbackInfo& args) { return node::THROW_ERR_ENCODING_INVALID_ENCODED_DATA( env->isolate(), "The encoded data was not valid for encoding utf-8"); } - - // TODO(chalker): save on utf8 validity recheck in StringBytes::Encode() } if (length == 0) return args.GetReturnValue().SetEmptyString(); Local ret; - if (StringBytes::Encode(env->isolate(), data, length, UTF8).ToLocal(&ret)) { + v8::MaybeLocal encoded = + has_fatal ? StringBytes::EncodeValidUtf8(env->isolate(), data, length) + : StringBytes::Encode(env->isolate(), data, length, UTF8); + if (encoded.ToLocal(&ret)) { args.GetReturnValue().Set(ret); } } 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..cd9fde06dcbfca 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 {}; } @@ -202,6 +164,15 @@ Maybe ParseFunctionSignature(Environment* env, if (!ToFFIType(env, arg_str.ToStringView()).To(&arg_type)) { return {}; } + if (arg_type == &ffi_type_void) { + THROW_ERR_INVALID_ARG_VALUE( + env, + "Argument %u of function %s must not be 'void'; " + "use an empty array for no-argument functions", + i, + name); + return {}; + } args.push_back(arg_type); arg_type_names.emplace_back(arg_str.ToString()); 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/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_contextify.cc b/src/node_contextify.cc index 14534ab89650b4..f319420ae02f35 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -700,8 +700,7 @@ Intercepted ContextifyContext::PropertyDefinerCallback( if (desc.has_configurable()) { desc_for_sandbox->set_configurable(desc.configurable()); } - // Set the property on the sandbox. - USE(sandbox->DefineProperty(context, property, *desc_for_sandbox)); + return sandbox->DefineProperty(context, property, *desc_for_sandbox); }; if (desc.has_get() || desc.has_set()) { @@ -709,23 +708,23 @@ Intercepted ContextifyContext::PropertyDefinerCallback( desc.has_get() ? desc.get() : Undefined(isolate).As(), desc.has_set() ? desc.set() : Undefined(isolate).As()); - define_prop_on_sandbox(&desc_for_sandbox); - // TODO(https://github.com/nodejs/node/issues/52634): this should return - // kYes to behave according to the expected semantics. + if (define_prop_on_sandbox(&desc_for_sandbox).FromMaybe(false)) + return Intercepted::kYes; return Intercepted::kNo; } else { Local value = desc.has_value() ? desc.value() : Undefined(isolate).As(); + Maybe result; if (desc.has_writable()) { PropertyDescriptor desc_for_sandbox(value, desc.writable()); - define_prop_on_sandbox(&desc_for_sandbox); + result = define_prop_on_sandbox(&desc_for_sandbox); } else { PropertyDescriptor desc_for_sandbox(value); - define_prop_on_sandbox(&desc_for_sandbox); + result = define_prop_on_sandbox(&desc_for_sandbox); } - // TODO(https://github.com/nodejs/node/issues/52634): this should return - // kYes to behave according to the expected semantics. + + if (result.FromMaybe(false)) return Intercepted::kYes; return Intercepted::kNo; } } 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: 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/src/node_http_parser.cc b/src/node_http_parser.cc index 50d7f9e6916096..46e61b30bd1ae7 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -96,6 +96,7 @@ const uint32_t kLenientOptionalLFAfterCR = 1 << 6; const uint32_t kLenientOptionalCRLFAfterChunk = 1 << 7; const uint32_t kLenientOptionalCRBeforeLF = 1 << 8; const uint32_t kLenientSpacesAfterChunkSize = 1 << 9; +const uint32_t kLenientHeaderValueRelaxed = 1 << 10; const uint32_t kLenientAll = kLenientHeaders | kLenientChunkedLength | kLenientKeepAlive | kLenientTransferEncoding | kLenientVersion | kLenientDataAfterClose | @@ -1006,6 +1007,11 @@ class Parser : public AsyncWrap, public StreamListener { if (lenient_flags & kLenientSpacesAfterChunkSize) { llhttp_set_lenient_spaces_after_chunk_size(&parser_, 1); } +#if LLHTTP_VERSION_MAJOR * 1000 + LLHTTP_VERSION_MINOR >= 9004 + if (lenient_flags & kLenientHeaderValueRelaxed) { + llhttp_set_lenient_header_value_relaxed(&parser_, 1); + } +#endif header_nread_ = 0; url_.Reset(); @@ -1332,6 +1338,16 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, Integer::NewFromUnsigned(isolate, kLenientOptionalCRBeforeLF)); t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientSpacesAfterChunkSize"), Integer::NewFromUnsigned(isolate, kLenientSpacesAfterChunkSize)); + // kLenientHeaderValueRelaxed requires llhttp >= 9.4.0 for the + // llhttp_set_lenient_header_value_relaxed() API. Export 0 on older + // shared-library builds so JS can detect feature availability. +#if LLHTTP_VERSION_MAJOR * 1000 + LLHTTP_VERSION_MINOR >= 9004 + t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientHeaderValueRelaxed"), + Integer::NewFromUnsigned(isolate, kLenientHeaderValueRelaxed)); +#else + t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientHeaderValueRelaxed"), + Integer::NewFromUnsigned(isolate, 0)); +#endif t->Set(FIXED_ONE_BYTE_STRING(isolate, "kLenientAll"), Integer::NewFromUnsigned(isolate, kLenientAll)); 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/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/src/node_sockaddr-inl.h b/src/node_sockaddr-inl.h index bc055da535c2d4..b02999d047800f 100644 --- a/src/node_sockaddr-inl.h +++ b/src/node_sockaddr-inl.h @@ -186,10 +186,10 @@ typename T::Type* SocketAddressLRU::Peek( } template -void SocketAddressLRU::CheckExpired() { +void SocketAddressLRU::CheckExpired(uint64_t now) { auto it = list_.rbegin(); while (it != list_.rend()) { - if (T::CheckExpired(it->first, it->second)) { + if (T::CheckExpired(it->first, it->second, now)) { map_.erase(it->first); list_.pop_back(); it = list_.rbegin(); @@ -211,21 +211,20 @@ void SocketAddressLRU::MemoryInfo(MemoryTracker* tracker) const { // cache and adjust if necessary. Whether the item exists or not, // purge expired items. template -typename T::Type* SocketAddressLRU::Upsert( - const SocketAddress& address) { - - auto on_exit = OnScopeLeave([&]() { CheckExpired(); }); +typename T::Type* SocketAddressLRU::Upsert(const SocketAddress& address, + uint64_t now) { + auto on_exit = OnScopeLeave([&]() { CheckExpired(now); }); auto it = map_.find(address); if (it != std::end(map_)) { list_.splice(list_.begin(), list_, it->second); - T::Touch(it->first, &it->second->second); + T::Touch(it->first, &it->second->second, now); return &it->second->second; } list_.push_front(Pair(address, { })); map_[address] = list_.begin(); - T::Touch(list_.begin()->first, &list_.begin()->second); + T::Touch(list_.begin()->first, &list_.begin()->second, now); // Drop the last item in the list if we are // over the size limit... diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index a9f1a7376bc1fa..9348f0ac8e4dfc 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -434,8 +434,7 @@ void SocketAddressBlockList::AddSocketAddressMask( rules_.emplace_front(std::move(rule)); } -bool SocketAddressBlockList::Apply( - const std::shared_ptr& address) { +bool SocketAddressBlockList::Apply(const SocketAddress& address) { Mutex::ScopedLock lock(mutex_); for (const auto& rule : rules_) { if (rule->Apply(address)) return true; @@ -457,8 +456,8 @@ SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule( : network(network_), prefix(prefix_) {} bool SocketAddressBlockList::SocketAddressRule::Apply( - const std::shared_ptr& address) { - return this->address->is_match(*address.get()); + const SocketAddress& address) { + return this->address->is_match(address); } std::string SocketAddressBlockList::SocketAddressRule::ToString() { @@ -470,8 +469,8 @@ std::string SocketAddressBlockList::SocketAddressRule::ToString() { } bool SocketAddressBlockList::SocketAddressRangeRule::Apply( - const std::shared_ptr& address) { - return *address.get() >= *start.get() && *address.get() <= *end.get(); + const SocketAddress& address) { + return address >= *start.get() && address <= *end.get(); } std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { @@ -485,8 +484,8 @@ std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { } bool SocketAddressBlockList::SocketAddressMaskRule::Apply( - const std::shared_ptr& address) { - return address->is_in_network(*network.get(), prefix); + const SocketAddress& address) { + return address.is_in_network(*network.get(), prefix); } std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { @@ -656,7 +655,7 @@ void SocketAddressBlockListWrap::Check( SocketAddressBase* addr; ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); - args.GetReturnValue().Set(wrap->blocklist_->Apply(addr->address())); + args.GetReturnValue().Set(wrap->blocklist_->Apply(*addr->address())); } void SocketAddressBlockListWrap::GetRules( diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index d67a26e8615cdc..05bb127b012f83 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -213,8 +213,9 @@ class SocketAddressLRU : public MemoryRetainer { // If the item already exists, returns a reference to // the existing item, adjusting items position in the // LRU. If the item does not exist, emplaces the item - // and returns the new item. - Type* Upsert(const SocketAddress& address); + // and returns the new item. The caller provides a + // timestamp to avoid redundant uv_hrtime() calls. + Type* Upsert(const SocketAddress& address, uint64_t now); // Returns a reference to the item if it exists, or // nullptr. The position in the LRU is not modified. @@ -231,7 +232,7 @@ class SocketAddressLRU : public MemoryRetainer { using Pair = std::pair; using Iterator = typename std::list::iterator; - void CheckExpired(); + void CheckExpired(uint64_t now); std::list list_; SocketAddress::Map map_; @@ -257,14 +258,14 @@ class SocketAddressBlockList : public MemoryRetainer { void AddSocketAddressMask(const std::shared_ptr& address, int prefix); - bool Apply(const std::shared_ptr& address); + bool Apply(const SocketAddress& address); size_t size() const { return rules_.size(); } v8::MaybeLocal ListRules(Environment* env); struct Rule : public MemoryRetainer { - virtual bool Apply(const std::shared_ptr& address) = 0; + virtual bool Apply(const SocketAddress& address) = 0; inline v8::MaybeLocal ToV8String(Environment* env); virtual std::string ToString() = 0; }; @@ -274,7 +275,7 @@ class SocketAddressBlockList : public MemoryRetainer { explicit SocketAddressRule(const std::shared_ptr& address); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -289,7 +290,7 @@ class SocketAddressBlockList : public MemoryRetainer { SocketAddressRangeRule(const std::shared_ptr& start, const std::shared_ptr& end); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -304,7 +305,7 @@ class SocketAddressBlockList : public MemoryRetainer { SocketAddressMaskRule(const std::shared_ptr& address, int prefix); - bool Apply(const std::shared_ptr& address) override; + bool Apply(const SocketAddress& address) override; std::string ToString() override; void MemoryInfo(node::MemoryTracker* tracker) const override; @@ -352,6 +353,10 @@ class SocketAddressBlockListWrap : public BaseObject { std::shared_ptr blocklist = std::make_shared()); + inline const std::shared_ptr& blocklist() const { + return blocklist_; + } + void MemoryInfo(node::MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(SocketAddressBlockListWrap) SET_SELF_SIZE(SocketAddressBlockListWrap) 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) { diff --git a/src/node_webstorage.cc b/src/node_webstorage.cc index 10c3ccd68e49a1..21f846fbeb6225 100644 --- a/src/node_webstorage.cc +++ b/src/node_webstorage.cc @@ -43,6 +43,7 @@ using v8::PropertyAttribute; using v8::PropertyCallbackInfo; using v8::PropertyDescriptor; using v8::PropertyHandlerFlags; +using v8::Signature; using v8::String; using v8::Value; @@ -737,8 +738,9 @@ static void Initialize(Local target, Local(), PropertyHandlerFlags::kHasNoSideEffect)); - Local length_getter = - FunctionTemplate::New(isolate, StorageLengthGetter); + Local length_signature = Signature::New(isolate, ctor_tmpl); + Local length_getter = FunctionTemplate::New( + isolate, StorageLengthGetter, Local(), length_signature); ctor_tmpl->PrototypeTemplate()->SetAccessorProperty(env->length_string(), length_getter, Local(), diff --git a/src/permission/addon_permission.cc b/src/permission/addon_permission.cc index 20123a72e6e06e..66035556102ff3 100644 --- a/src/permission/addon_permission.cc +++ b/src/permission/addon_permission.cc @@ -14,6 +14,12 @@ void AddonPermission::Apply(Environment* env, deny_all_ = true; } +void AddonPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool AddonPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/addon_permission.h b/src/permission/addon_permission.h index 9702862d098703..b3eed910fe9f89 100644 --- a/src/permission/addon_permission.h +++ b/src/permission/addon_permission.h @@ -15,6 +15,9 @@ class AddonPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/child_process_permission.cc b/src/permission/child_process_permission.cc index 1dfbaecf1b11ed..7d31ff24f81398 100644 --- a/src/permission/child_process_permission.cc +++ b/src/permission/child_process_permission.cc @@ -15,6 +15,12 @@ void ChildProcessPermission::Apply(Environment* env, deny_all_ = true; } +void ChildProcessPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool ChildProcessPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/child_process_permission.h b/src/permission/child_process_permission.h index 7c9078c5b30714..33612b1c10a9af 100644 --- a/src/permission/child_process_permission.h +++ b/src/permission/child_process_permission.h @@ -15,6 +15,9 @@ class ChildProcessPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/ffi_permission.cc b/src/permission/ffi_permission.cc index b388c8fcc44a06..4b00d4c07b9c59 100644 --- a/src/permission/ffi_permission.cc +++ b/src/permission/ffi_permission.cc @@ -14,6 +14,12 @@ void FFIPermission::Apply(Environment* env, deny_all_ = true; } +void FFIPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool FFIPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/ffi_permission.h b/src/permission/ffi_permission.h index 70fb7a4b9a2e1d..3acd3c4642a048 100644 --- a/src/permission/ffi_permission.h +++ b/src/permission/ffi_permission.h @@ -15,6 +15,9 @@ class FFIPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/fs_permission.cc b/src/permission/fs_permission.cc index 3b817c5bcdce75..98146cf825ad38 100644 --- a/src/permission/fs_permission.cc +++ b/src/permission/fs_permission.cc @@ -154,15 +154,96 @@ void FSPermission::Apply(Environment* env, } } +void FSPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + if (param.empty()) { + // Drop all access for this scope + if (scope == PermissionScope::kFileSystemRead || + scope == PermissionScope::kFileSystem) { + deny_all_in_ = true; + allow_all_in_ = false; + granted_in_fs_.Clear(); + granted_paths_in_.clear(); + } + if (scope == PermissionScope::kFileSystemWrite || + scope == PermissionScope::kFileSystem) { + deny_all_out_ = true; + allow_all_out_ = false; + granted_out_fs_.Clear(); + granted_paths_out_.clear(); + } + return; + } + + // When allowed with *, you can only drop * (no specific paths) + std::string resolved = PathResolve(env, {param}); + if (scope == PermissionScope::kFileSystemRead || + scope == PermissionScope::kFileSystem) { + if (!allow_all_in_) { + RevokeAccess(PermissionScope::kFileSystemRead, resolved); + } + } + if (scope == PermissionScope::kFileSystemWrite || + scope == PermissionScope::kFileSystem) { + if (!allow_all_out_) { + RevokeAccess(PermissionScope::kFileSystemWrite, resolved); + } + } +} + +void FSPermission::RevokeAccess(PermissionScope perm, const std::string& res) { + const std::string path = WildcardIfDir(res); + if (perm == PermissionScope::kFileSystemRead) { + auto it = + std::find(granted_paths_in_.begin(), granted_paths_in_.end(), path); + if (it != granted_paths_in_.end()) { + granted_paths_in_.erase(it); + RebuildTree(PermissionScope::kFileSystemRead); + } + } else if (perm == PermissionScope::kFileSystemWrite) { + auto it = + std::find(granted_paths_out_.begin(), granted_paths_out_.end(), path); + if (it != granted_paths_out_.end()) { + granted_paths_out_.erase(it); + RebuildTree(PermissionScope::kFileSystemWrite); + } + } +} + +void FSPermission::RebuildTree(PermissionScope scope) { + if (scope == PermissionScope::kFileSystemRead) { + granted_in_fs_.Clear(); + if (granted_paths_in_.empty()) { + deny_all_in_ = true; + } else { + for (const auto& path : granted_paths_in_) { + granted_in_fs_.Insert(path); + } + } + } else if (scope == PermissionScope::kFileSystemWrite) { + granted_out_fs_.Clear(); + if (granted_paths_out_.empty()) { + deny_all_out_ = true; + } else { + for (const auto& path : granted_paths_out_) { + granted_out_fs_.Insert(path); + } + } + } +} + void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) { const std::string path = WildcardIfDir(res); if (perm == PermissionScope::kFileSystemRead && !granted_in_fs_.Lookup(path)) { granted_in_fs_.Insert(path); + granted_paths_in_.push_back(path); deny_all_in_ = false; } else if (perm == PermissionScope::kFileSystemWrite && !granted_out_fs_.Lookup(path)) { granted_out_fs_.Insert(path); + granted_paths_out_.push_back(path); deny_all_out_ = false; } } @@ -196,6 +277,16 @@ FSPermission::RadixTree::~RadixTree() { FreeRecursivelyNode(root_node_); } +void FSPermission::RadixTree::Clear() { + for (auto& c : root_node_->children) { + FreeRecursivelyNode(c.second); + } + root_node_->children.clear(); + delete root_node_->wildcard_child; + root_node_->wildcard_child = nullptr; + root_node_->is_leaf = false; +} + bool FSPermission::RadixTree::Lookup(const std::string_view& s, bool when_empty_return) const { FSPermission::RadixTree::Node* current_node = root_node_; diff --git a/src/permission/fs_permission.h b/src/permission/fs_permission.h index 22b29b017e2061..19d72ef654c9f0 100644 --- a/src/permission/fs_permission.h +++ b/src/permission/fs_permission.h @@ -18,6 +18,9 @@ class FSPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const override; @@ -139,6 +142,7 @@ class FSPermission final : public PermissionBase { RadixTree(); ~RadixTree(); void Insert(const std::string& s); + void Clear(); bool Lookup(const std::string_view& s) const { return Lookup(s, false); } bool Lookup(const std::string_view& s, bool when_empty_return) const; @@ -148,10 +152,15 @@ class FSPermission final : public PermissionBase { private: void GrantAccess(PermissionScope scope, const std::string& param); + void RevokeAccess(PermissionScope scope, const std::string& param); + void RebuildTree(PermissionScope scope); // fs granted on startup RadixTree granted_in_fs_; RadixTree granted_out_fs_; + std::vector granted_paths_in_; + std::vector granted_paths_out_; + bool deny_all_in_ = true; bool deny_all_out_ = true; diff --git a/src/permission/inspector_permission.cc b/src/permission/inspector_permission.cc index 95114e6634ab3f..ee775e778dcf52 100644 --- a/src/permission/inspector_permission.cc +++ b/src/permission/inspector_permission.cc @@ -14,6 +14,12 @@ void InspectorPermission::Apply(Environment* env, deny_all_ = true; } +void InspectorPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool InspectorPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/inspector_permission.h b/src/permission/inspector_permission.h index 9b214099095ee8..d851fb2fa25303 100644 --- a/src/permission/inspector_permission.h +++ b/src/permission/inspector_permission.h @@ -15,6 +15,9 @@ class InspectorPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/net_permission.cc b/src/permission/net_permission.cc index f1380f9b321a89..5f1cc139aa745e 100644 --- a/src/permission/net_permission.cc +++ b/src/permission/net_permission.cc @@ -13,6 +13,12 @@ void NetPermission::Apply(Environment* env, allow_net_ = true; } +void NetPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + allow_net_ = false; +} + bool NetPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/net_permission.h b/src/permission/net_permission.h index 3be4f5bc7c40eb..26b055b255a63d 100644 --- a/src/permission/net_permission.h +++ b/src/permission/net_permission.h @@ -15,6 +15,9 @@ class NetPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const override; diff --git a/src/permission/permission.cc b/src/permission/permission.cc index 0da8fdcd0b9c1e..c593502d94eb49 100644 --- a/src/permission/permission.cc +++ b/src/permission/permission.cc @@ -53,6 +53,29 @@ constexpr std::string_view GetDiagnosticsChannelName(PermissionScope scope) { } } +// permission.drop('fs.read', '/tmp/') +// permission.drop('child') +static void Drop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + + const std::string deny_scope = Utf8Value(env->isolate(), args[0]).ToString(); + PermissionScope scope = Permission::StringToPermission(deny_scope); + if (scope == PermissionScope::kPermissionsRoot) { + return; + } + + if (args.Length() > 1 && !args[1]->IsUndefined()) { + Utf8Value utf8_arg(env->isolate(), args[1]); + if (utf8_arg.length() > 0) { + env->permission()->Drop(env, scope, utf8_arg.ToStringView()); + return; + } + } + + env->permission()->Drop(env, scope); +} + // permission.has('fs.in', '/tmp/') // permission.has('fs.in') static void Has(const FunctionCallbackInfo& args) { @@ -283,17 +306,61 @@ void Permission::Apply(Environment* env, } } +void Permission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + auto permission = nodes_.find(scope); + if (permission != nodes_.end()) { + permission->second->Drop(env, scope, param); + } + + // Publish to diagnostics channel so observers can track drops + auto channel_name = GetDiagnosticsChannelName(scope); + if (!channel_name.empty() && !publishing_) { + auto ch = GetOrCreateChannel(env, scope); + if (ch && ch->HasSubscribers()) { + publishing_ = true; + v8::Isolate* isolate = env->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Local context = env->context(); + v8::Local msg = + v8::Object::New(isolate, v8::Null(isolate), nullptr, nullptr, 0); + const char* perm_str = PermissionToString(scope); + msg->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "permission"), + v8::String::NewFromUtf8(isolate, perm_str).ToLocalChecked()) + .Check(); + msg->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "resource"), + v8::String::NewFromUtf8(isolate, + param.data(), + v8::NewStringType::kNormal, + static_cast(param.size())) + .ToLocalChecked()) + .Check(); + msg->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "drop"), + v8::Boolean::New(isolate, true)) + .Check(); + ch->Publish(env, msg); + publishing_ = false; + } + } +} + void Initialize(Local target, Local unused, Local context, void* priv) { SetMethodNoSideEffect(context, target, "has", Has); + SetMethod(context, target, "drop", Drop); target->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromJust(); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(Has); + registry->Register(Drop); } } // namespace permission diff --git a/src/permission/permission.h b/src/permission/permission.h index b43c9648093b08..84e3ea67ed5e04 100644 --- a/src/permission/permission.h +++ b/src/permission/permission.h @@ -118,6 +118,10 @@ class Permission { void Apply(Environment* env, const std::vector& allow, PermissionScope scope); + // Runtime Call + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = ""); void EnablePermissions(); void EnableWarningOnly(); diff --git a/src/permission/permission_base.h b/src/permission/permission_base.h index 4c796c770dccf8..11f4d6c6bfa65f 100644 --- a/src/permission/permission_base.h +++ b/src/permission/permission_base.h @@ -59,6 +59,9 @@ class PermissionBase { virtual void Apply(Environment* env, const std::vector& allow, PermissionScope scope) = 0; + virtual void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") = 0; virtual bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const = 0; diff --git a/src/permission/wasi_permission.cc b/src/permission/wasi_permission.cc index 625b61cf1c1bbe..00ce927eb6254f 100644 --- a/src/permission/wasi_permission.cc +++ b/src/permission/wasi_permission.cc @@ -15,6 +15,12 @@ void WASIPermission::Apply(Environment* env, deny_all_ = true; } +void WASIPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool WASIPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/wasi_permission.h b/src/permission/wasi_permission.h index 2a12592d142eea..b5cdaca928dde8 100644 --- a/src/permission/wasi_permission.h +++ b/src/permission/wasi_permission.h @@ -15,6 +15,9 @@ class WASIPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/permission/worker_permission.cc b/src/permission/worker_permission.cc index 3a51cf12e4ee85..aa6867eb1e0f17 100644 --- a/src/permission/worker_permission.cc +++ b/src/permission/worker_permission.cc @@ -15,6 +15,12 @@ void WorkerPermission::Apply(Environment* env, deny_all_ = true; } +void WorkerPermission::Drop(Environment* env, + PermissionScope scope, + const std::string_view& param) { + deny_all_ = true; +} + bool WorkerPermission::is_granted(Environment* env, PermissionScope perm, const std::string_view& param) const { diff --git a/src/permission/worker_permission.h b/src/permission/worker_permission.h index 9ec40a3d1c66ca..fc7abe0d50f459 100644 --- a/src/permission/worker_permission.h +++ b/src/permission/worker_permission.h @@ -15,6 +15,9 @@ class WorkerPermission final : public PermissionBase { void Apply(Environment* env, const std::vector& allow, PermissionScope scope) override; + void Drop(Environment* env, + PermissionScope scope, + const std::string_view& param = "") override; bool is_granted(Environment* env, PermissionScope perm, const std::string_view& param = "") const override; diff --git a/src/quic/application.cc b/src/quic/application.cc index c37b22c2bd3209..ce5d5e12154d8a 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -115,6 +115,13 @@ Maybe Session::Application_Options::From( #undef SET + // Ensure the advertised max_field_section_size in SETTINGS is at least + // as large as max_header_length. Otherwise the peer would be told to + // restrict headers to a smaller size than what CanAddHeader accepts. + if (options.max_field_section_size < options.max_header_length) { + options.max_field_section_size = options.max_header_length; + } + return Just(options); } @@ -717,8 +724,11 @@ class DefaultApplication final : public Session::Application { void EarlyDataRejected() override { // Destroy all open streams — ngtcp2 has already discarded their - // internal state when it rejected the early data. - session().DestroyAllStreams(QuicError::ForApplication(0)); + // internal state when it rejected the early data. Use the + // application's internal error code since this is an error + // condition (code 0 would be treated as a clean close). + session().DestroyAllStreams( + QuicError::ForApplication(GetInternalErrorCode())); if (!session().is_destroyed()) { session().EmitEarlyDataRejected(); } diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index f029f0e8eb83da..31467a8477a792 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -435,6 +435,14 @@ QUIC_JS_CALLBACKS(V) #undef V +Local BindingData::error_name_string(const char* name) { + auto& slot = error_name_strings_[name]; + if (slot.IsEmpty()) { + slot.Set(env()->isolate(), OneByteString(env()->isolate(), name)); + } + return slot.Get(env()->isolate()); +} + JS_METHOD_IMPL(BindingData::SetCallbacks) { auto env = Environment::GetCurrent(args); auto isolate = env->isolate(); diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index c3c5e9fc7834df..2116a35c158ea6 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -70,6 +70,7 @@ class SessionManager; V(ack_delay_exponent, "ackDelayExponent") \ V(active_connection_id_limit, "activeConnectionIDLimit") \ V(address_lru_size, "addressLRUSize") \ + V(allow, "allow") \ V(application, "application") \ V(authoritative, "authoritative") \ V(bbr, "bbr") \ @@ -81,6 +82,7 @@ class SessionManager; V(crl, "crl") \ V(cubic, "cubic") \ V(datagram_drop_policy, "datagramDropPolicy") \ + V(deny, "deny") \ V(disable_stateless_reset, "disableStatelessReset") \ V(draining_period_multiplier, "drainingPeriodMultiplier") \ V(enable_connect_protocol, "enableConnectProtocol") \ @@ -111,14 +113,25 @@ class SessionManager; V(max_connections_total, "maxConnectionsTotal") \ V(max_datagram_frame_size, "maxDatagramFrameSize") \ V(max_datagram_send_attempts, "maxDatagramSendAttempts") \ + V(stream_idle_timeout, "streamIdleTimeout") \ V(max_field_section_size, "maxFieldSectionSize") \ V(max_header_length, "maxHeaderLength") \ V(max_header_pairs, "maxHeaderPairs") \ 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(session_creation_rate, "sessionCreationRate") \ + V(session_creation_burst, "sessionCreationBurst") \ + V(block_list, "blockList") \ + V(block_list_policy, "blockListPolicy") \ V(max_stream_window, "maxStreamWindow") \ V(max_window, "maxWindow") \ V(min_version, "minVersion") \ @@ -156,6 +169,8 @@ class SessionManager; V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \ V(validate_address, "validateAddress") \ V(verify_client, "verifyClient") \ + V(verify_hostname, "verifyHostname") \ + V(verify_peer_strict, "verifyPeerStrict") \ V(verify_private_key, "verifyPrivateKey") \ V(version, "version") @@ -290,6 +305,8 @@ class BindingData final std::unordered_map> listening_endpoints; + v8::Local error_name_string(const char* name); + size_t current_ngtcp2_memory_ = 0; // The following set up various storage and accessors for common strings, @@ -342,6 +359,9 @@ class BindingData final QUIC_JS_CALLBACKS(V) #undef V + // Lazy cache backing error_name_string() + std::unordered_map> error_name_strings_; + std::unique_ptr session_manager_; // Type-erased arena storage. The concrete AliasedStructArena types diff --git a/src/quic/data.cc b/src/quic/data.cc index aebae9099a2b5f..9599adec62f805 100644 --- a/src/quic/data.cc +++ b/src/quic/data.cc @@ -1,13 +1,15 @@ #if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC -#include "data.h" #include #include #include #include +#include #include #include +#include "bindingdata.h" +#include "data.h" #include "defs.h" #include "util.h" @@ -363,14 +365,70 @@ std::optional QuicError::get_crypto_error() const { return code() & ~NGTCP2_CRYPTO_ERROR; } +const char* QuicError::name() const { + // CRYPTO_ERROR carries a TLS alert in its low byte (RFC 9001 sec. 4.8). + // OpenSSL's SSL_alert_desc_string_long owns a stable string for every + // alert it knows about; we filter out the "unknown" placeholder so the + // JS side can present `errorName` as undefined for unrecognised alerts. + if (auto alert = get_crypto_error()) { + const char* n = SSL_alert_desc_string_long(*alert); + if (n != nullptr && std::string_view(n) != "unknown") return n; + return nullptr; + } + // Named transport-layer error codes from RFC 9000 sec. 20.1 (and the + // RFC 9368 version-negotiation extension). Application error codes are + // opaque to QUIC, so we only decode for transport. + if (type() != Type::TRANSPORT) return nullptr; + switch (code()) { + case NGTCP2_NO_ERROR: + return "NO_ERROR"; + case NGTCP2_INTERNAL_ERROR: + return "INTERNAL_ERROR"; + case NGTCP2_CONNECTION_REFUSED: + return "CONNECTION_REFUSED"; + case NGTCP2_FLOW_CONTROL_ERROR: + return "FLOW_CONTROL_ERROR"; + case NGTCP2_STREAM_LIMIT_ERROR: + return "STREAM_LIMIT_ERROR"; + case NGTCP2_STREAM_STATE_ERROR: + return "STREAM_STATE_ERROR"; + case NGTCP2_FINAL_SIZE_ERROR: + return "FINAL_SIZE_ERROR"; + case NGTCP2_FRAME_ENCODING_ERROR: + return "FRAME_ENCODING_ERROR"; + case NGTCP2_TRANSPORT_PARAMETER_ERROR: + return "TRANSPORT_PARAMETER_ERROR"; + case NGTCP2_CONNECTION_ID_LIMIT_ERROR: + return "CONNECTION_ID_LIMIT_ERROR"; + case NGTCP2_PROTOCOL_VIOLATION: + return "PROTOCOL_VIOLATION"; + case NGTCP2_INVALID_TOKEN: + return "INVALID_TOKEN"; + case NGTCP2_APPLICATION_ERROR: + return "APPLICATION_ERROR"; + case NGTCP2_CRYPTO_BUFFER_EXCEEDED: + return "CRYPTO_BUFFER_EXCEEDED"; + case NGTCP2_KEY_UPDATE_ERROR: + return "KEY_UPDATE_ERROR"; + case NGTCP2_AEAD_LIMIT_REACHED: + return "AEAD_LIMIT_REACHED"; + case NGTCP2_NO_VIABLE_PATH: + return "NO_VIABLE_PATH"; + case NGTCP2_VERSION_NEGOTIATION_ERROR: + return "VERSION_NEGOTIATION_ERROR"; + default: + return nullptr; + } +} + MaybeLocal QuicError::ToV8Value(Environment* env) const { if ((type() == Type::TRANSPORT && code() == NGTCP2_NO_ERROR) || - (type() == Type::APPLICATION && code() == NGHTTP3_H3_NO_ERROR) || + (type() == Type::APPLICATION && + (code() == 0 || code() == NGHTTP3_H3_NO_ERROR)) || type() == Type::IDLE_CLOSE) { - // Note that we only return undefined for *known* no-error application - // codes. It is possible that other application types use other specific - // no-error codes, but since we don't know which application is being used, - // we'll just return the error code value for those below. + // Application code 0 is the default no-error code for raw QUIC + // applications (DefaultApplication::GetNoErrorCode() returns 0). + // NGHTTP3_H3_NO_ERROR (0x100) is the HTTP/3 no-error code. // Idle close is always clean — the session timed out normally. return Undefined(env->isolate()); } @@ -384,6 +442,7 @@ MaybeLocal QuicError::ToV8Value(Environment* env) const { type_str, BigInt::NewFromUnsigned(env->isolate(), code()), Undefined(env->isolate()), + Undefined(env->isolate()), }; // Note that per the QUIC specification, the reason, if present, is @@ -397,6 +456,13 @@ MaybeLocal QuicError::ToV8Value(Environment* env) const { return {}; } + // Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1 + // names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown + // codes leave the slot as undefined. + if (const char* n = name()) { + argv[3] = BindingData::Get(env).error_name_string(n); + } + return Array::New(env->isolate(), argv, arraysize(argv)).As(); } diff --git a/src/quic/data.h b/src/quic/data.h index ec8d40cbc4c7a0..9c92a30c1ddf4c 100644 --- a/src/quic/data.h +++ b/src/quic/data.h @@ -265,6 +265,9 @@ class QuicError final : public MemoryRetainer { bool is_crypto_error() const; std::optional get_crypto_error() const; + // Returns a human-readable name for this error if known, or nullptr + const char* name() const; + // Note that since application errors are application-specific and we // don't know which application is being used here, it is possible that // the comparing two different QuicError instances from different applications diff --git a/src/quic/defs.h b/src/quic/defs.h index 6b18c19f4c3c6d..75ae915335be93 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -360,6 +360,33 @@ 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() : rate(0), burst(0), tokens(0), last_ts(0) {} + TokenBucket(double rate, double burst); + + // Reinitialize the bucket with new rate/burst parameters if it + // hasn't been initialized yet (last_ts == 0). Used for per-host + // buckets in the address LRU where the rate/burst aren't known + // at construction time. + void InitOnce(double r, double b, uint64_t now); + + // Try to consume one token. Refills based on elapsed time, then + // attempts to consume. Returns true if the request is allowed. + // The caller provides the current timestamp to avoid redundant + // uv_hrtime() calls in hot paths. + bool consume(uint64_t now); +}; + class DebugIndentScope final { public: inline DebugIndentScope() { ++indent_; } diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 1a72113dc7bea4..5a728a0a2a147e 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -73,9 +73,15 @@ 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) \ + V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited) \ + V(PACKETS_BLOCKED, packets_blocked) struct Endpoint::State { #define V(_, name, type) type name; @@ -85,6 +91,31 @@ struct Endpoint::State { STAT_STRUCT(Endpoint, ENDPOINT) +TokenBucket::TokenBucket(double rate, double burst) + : rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {} + +void TokenBucket::InitOnce(double r, double b, uint64_t now) { + if (last_ts == 0) { + rate = r; + burst = b; + tokens = b; + last_ts = now; + } +} + +// 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) { + 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 +128,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 +138,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 +171,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 +233,13 @@ 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) || !SET(session_creation_rate) || + !SET(session_creation_burst) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif @@ -227,6 +269,42 @@ Maybe Endpoint::Options::From(Environment* env, } } + // Parse block list option. Expects the C++ SocketAddressBlockListWrap handle + // (the JS side extracts [kHandle] before passing it through). + Local block_list_val; + if (!params->Get(env->context(), state.block_list_string()) + .ToLocal(&block_list_val)) { + return Nothing(); + } + if (!block_list_val->IsUndefined()) { + if (!SocketAddressBlockListWrap::HasInstance(env, block_list_val)) { + THROW_ERR_INVALID_ARG_TYPE( + env, "The blockList option must be a BlockList handle"); + return Nothing(); + } + auto* wrap = + FromJSObject(block_list_val.As()); + options.block_list = wrap->blocklist(); + } + + // Parse block list policy. + Local policy_val; + if (!params->Get(env->context(), state.block_list_policy_string()) + .ToLocal(&policy_val)) { + return Nothing(); + } + if (!policy_val->IsUndefined()) { + if (policy_val->StrictEquals(state.allow_string())) { + options.block_list_policy = Options::BlockListPolicy::ALLOW; + } else if (policy_val->StrictEquals(state.deny_string())) { + options.block_list_policy = Options::BlockListPolicy::DENY; + } else { + THROW_ERR_INVALID_ARG_VALUE( + env, "The blockListPolicy option must be 'deny' or 'allow'"); + return Nothing(); + } + } + return Just(options); #undef SET @@ -251,10 +329,26 @@ 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 + + "session creation rate: " + std::to_string(session_creation_rate) + + "/s"; + res += prefix + + "session creation burst: " + std::to_string(session_creation_burst); res += prefix + "validate address: " + boolToString(validate_address); res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); @@ -580,8 +674,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 +735,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(); @@ -963,63 +1062,41 @@ 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. +void Endpoint::SendRetry(const PathDescriptor& options, uint64_t now) { 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(now)) { + 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) { +void Endpoint::SendVersionNegotiation(const PathDescriptor& options, + uint64_t now) { 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(now)) { + 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)); } } bool Endpoint::SendStatelessReset(const PathDescriptor& options, - size_t source_len) { + size_t source_len, + uint64_t now) { if (options_.disable_stateless_reset) [[unlikely]] { return false; } @@ -1028,17 +1105,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(now)) { + Debug(this, "Stateless reset rate limit exceeded (global)"); + STAT_INCREMENT(Stats, stateless_reset_rate_limited); return false; } @@ -1047,7 +1116,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; @@ -1057,33 +1125,21 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, } void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, - QuicError reason) { + QuicError reason, + uint64_t now) { Debug(this, "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(now)) { + 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)); } @@ -1297,6 +1353,22 @@ void Endpoint::CloseGracefully() { void Endpoint::Receive(const uint8_t* data, size_t len, const SocketAddress& remote_address) { + const uint64_t now = uv_hrtime(); + + // Block list filtering — applied before any packet processing to + // minimize resource expenditure on blocked sources. + if (options_.block_list) { + bool matched = options_.block_list->Apply(remote_address); + bool drop = (options_.block_list_policy == Options::BlockListPolicy::DENY) + ? matched // deny list: drop if address matches + : !matched; // allow list: drop if address doesn't match + if (drop) { + Debug(this, "Packet from %s blocked by block list", remote_address); + STAT_INCREMENT(Stats, packets_blocked); + return; + } + } + const auto receive = [&](Session* session, const uint8_t* pkt_data, size_t pkt_len, @@ -1311,7 +1383,12 @@ void Endpoint::Receive(const uint8_t* data, // are generated. The deferred flush via BindingData's uv_check // callback calls SendPendingData once per dirty session after all // packets in the burst have been read. - if (session->ReadPacket(pkt_data, pkt_len, local_address, remote_address)) { + if (session->ReadPacket(pkt_data, + pkt_len, + local_address, + remote_address, + PacketInfo(), + now)) { STAT_INCREMENT_N(Stats, bytes_received, pkt_len); STAT_INCREMENT(Stats, packets_received); } @@ -1331,6 +1408,19 @@ void Endpoint::Receive(const uint8_t* data, // as a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; + // Per-host session creation rate limit. The bucket is initialized + // on first access with the configured rate/burst from options. + auto info = addr_validation_lru_.Upsert(config.remote_address, now); + info->session_creation_bucket.InitOnce( + options_.session_creation_rate, options_.session_creation_burst, now); + if (!info->session_creation_bucket.consume(now)) { + Debug(this, + "Session creation rate limit exceeded for %s", + config.remote_address); + STAT_INCREMENT(Stats, session_creation_rate_limited); + return; + } + Debug(this, "Creating new session for %s", config.dcid); std::optional no_ticket = std::nullopt; @@ -1422,7 +1512,8 @@ void Endpoint::Receive(const uint8_t* data, if (state_->busy) STAT_INCREMENT(Stats, server_busy_count); SendImmediateConnectionClose( PathDescriptor{version, dcid, scid, local_address, remote_address}, - QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), + now); // The packet was successfully processed, even if we did refuse the // connection. STAT_INCREMENT(Stats, packets_received); @@ -1496,7 +1587,8 @@ void Endpoint::Receive(const uint8_t* data, Debug(this, "Retry token from %s is invalid.", remote_address); SendImmediateConnectionClose( PathDescriptor{version, scid, dcid, local_address, remote_address}, - QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); + QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1512,7 +1604,7 @@ void Endpoint::Receive(const uint8_t* data, // Mark the address as validated since the retry round-trip proves // reachability. Debug(this, "Remote address %s is validated", remote_address); - addr_validation_lru_.Upsert(remote_address)->validated = true; + addr_validation_lru_.Upsert(remote_address, now)->validated = true; } // Step 2: Address validation — decide whether to send a Retry or @@ -1528,13 +1620,15 @@ void Endpoint::Receive(const uint8_t* data, "Initial packet has no token. Sending retry to %s to start " "validation", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1555,13 +1649,15 @@ void Endpoint::Receive(const uint8_t* data, Debug(this, "Regular token from %s is invalid.", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1573,20 +1669,22 @@ void Endpoint::Receive(const uint8_t* data, Debug(this, "Initial packet from %s has unknown token type", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } } Debug(this, "Remote address %s is validated", remote_address); - addr_validation_lru_.Upsert(remote_address)->validated = true; + addr_validation_lru_.Upsert(remote_address, now)->validated = true; } else if (hd.tokenlen > 0) { Debug(this, "Ignoring initial packet from %s with unexpected token", @@ -1598,13 +1696,15 @@ void Endpoint::Receive(const uint8_t* data, if (options_.validate_address) { Debug( this, "Sending retry to %s due to 0RTT packet", remote_address); - SendRetry(PathDescriptor{ - version, - dcid, - scid, - local_address, - remote_address, - }); + SendRetry( + PathDescriptor{ + version, + dcid, + scid, + local_address, + remote_address, + }, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1684,12 +1784,6 @@ void Endpoint::Receive(const uint8_t* data, } #endif // DEBUG - // TODO(@jasnell): Implement blocklist support - // if (block_list_->Apply(remote_address)) [[unlikely]] { - // Debug(this, "Ignoring blocked remote address: %s", remote_address); - // return; - // } - Debug(this, "Received %zu-byte packet from %s", len, remote_address); ngtcp2_version_cid pversion_cid; @@ -1713,8 +1807,12 @@ void Endpoint::Receive(const uint8_t* data, pversion_cid.version); CID dcid(pversion_cid.dcid, pversion_cid.dcidlen); CID scid(pversion_cid.scid, pversion_cid.scidlen); - SendVersionNegotiation(PathDescriptor{ - pversion_cid.version, dcid, scid, local_address(), remote_address}); + SendVersionNegotiation(PathDescriptor{pversion_cid.version, + dcid, + scid, + local_address(), + remote_address}, + now); STAT_INCREMENT(Stats, packets_received); return; } @@ -1793,7 +1891,8 @@ void Endpoint::Receive(const uint8_t* data, SendStatelessReset( PathDescriptor{ pversion_cid.version, dcid, scid, addr, remote_address}, - len); + len, + now); return; } @@ -1855,13 +1954,14 @@ void Endpoint::MemoryInfo(MemoryTracker* tracker) const { // Endpoint::SocketAddressInfoTraits bool Endpoint::SocketAddressInfoTraits::CheckExpired( - const SocketAddress& address, const Type& type) { - return (uv_hrtime() - type.timestamp) > kSocketAddressInfoTimeout; + const SocketAddress& address, const Type& type, uint64_t now) { + return (now - type.timestamp) > kSocketAddressInfoTimeout; } void Endpoint::SocketAddressInfoTraits::Touch(const SocketAddress& address, - Type* type) { - type->timestamp = uv_hrtime(); + Type* type, + uint64_t now) { + type->timestamp = now; } // ====================================================================================== diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index da2ea253dd8d88..f54e028ead4554 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -29,37 +29,28 @@ 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; + + // Per-host session creation rate limit. This is tracked per validated + // remote address in the address LRU, preventing a single source from + // churning through sessions faster than the server can handle. Unlike + // the global stateless response buckets, this only applies after address + // validation (spoofed sources can't reach this path). + static constexpr double DEFAULT_SESSION_CREATION_RATE = 50; + static constexpr double DEFAULT_SESSION_CREATION_BURST = 100; // Endpoint configuration options struct Options final : public MemoryRetainer { @@ -83,30 +74,28 @@ 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; + + // Per-host session creation rate limit. Tracked per validated remote + // address in the address LRU. Set to high values for benchmarking + // where traffic comes from a single source. + double session_creation_rate = DEFAULT_SESSION_CREATION_RATE; + double session_creation_burst = DEFAULT_SESSION_CREATION_BURST; // The validate_address parameter instructs the Endpoint to perform explicit // address validation using retry tokens. This is strongly recommended and @@ -170,6 +159,17 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { static constexpr uint64_t DEFAULT_IDLE_TIMEOUT = 0; uint64_t idle_timeout = DEFAULT_IDLE_TIMEOUT; + // Optional block list for filtering incoming packets by source address. + // When block_list_policy is DENY, packets from addresses matching the + // block list are dropped. When ALLOW, only packets from addresses + // matching the block list are accepted (all others dropped). + enum class BlockListPolicy : uint8_t { + DENY, // Drop packets from matching addresses (blocklist) + ALLOW, // Drop packets from non-matching addresses (allowlist) + }; + std::shared_ptr block_list; + BlockListPolicy block_list_policy = BlockListPolicy::DENY; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Endpoint::Config) SET_SELF_SIZE(Options) @@ -263,24 +263,27 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // ellicit retry packets (It can do so by intentionally sending initial // packets that ignore the retry token). To help mitigate that risk, we limit // the number of retries we send to a given remote endpoint. - void SendRetry(const PathDescriptor& options); + void SendRetry(const PathDescriptor& options, uint64_t now); // Sends a version negotiation packet. This is terminal for the connection and // is sent only when a QUIC packet is received for an unsupported QUIC // version. It is possible that a malicious packet triggered this so we need // to be careful not to commit too many resources. - void SendVersionNegotiation(const PathDescriptor& options); + void SendVersionNegotiation(const PathDescriptor& options, uint64_t now); // Possibly generates and sends a stateless reset packet. This is terminal for // the connection. It is possible that a malicious packet triggered this so we // need to be careful not to commit too many resources. - bool SendStatelessReset(const PathDescriptor& options, size_t source_len); + bool SendStatelessReset(const PathDescriptor& options, + size_t source_len, + uint64_t now); // Shutdown a connection prematurely, before a Session is created. This should // only be called at the start of a session before the crypto keys have been // established. void SendImmediateConnectionClose(const PathDescriptor& options, - QuicError error); + QuicError error, + uint64_t now); // Listen for connections (act as a server). void Listen(const Session::Options& options); @@ -475,20 +478,27 @@ 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; + TokenBucket session_creation_bucket; }; - static bool CheckExpired(const SocketAddress& address, const Type& type); - static void Touch(const SocketAddress& address, Type* type); + static bool CheckExpired(const SocketAddress& address, + const Type& type, + uint64_t now); + static void Touch(const SocketAddress& address, Type* type, uint64_t now); }; 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/src/quic/http3.cc b/src/quic/http3.cc index c9e249682f3a00..bf4bfb42b57611 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -177,10 +177,13 @@ class Http3ApplicationImpl final : public Session::Application { // When 0-RTT is rejected, destroy the nghttp3 connection and all // open streams — ngtcp2 has discarded their internal state. // Reset started_ so Start() is called again via on_receive_rx_key - // at 1RTT to recreate the nghttp3 connection. + // at 1RTT to recreate the nghttp3 connection. Use the + // application's internal error code since this is an error + // condition (code 0 would be treated as a clean close). conn_.reset(); started_ = false; - session().DestroyAllStreams(QuicError::ForApplication(0)); + session().DestroyAllStreams( + QuicError::ForApplication(GetInternalErrorCode())); if (!session().is_destroyed()) { session().EmitEarlyDataRejected(); } 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/src/quic/session.cc b/src/quic/session.cc index b42d19d0c0a307..bb94b6a7400766 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -174,7 +174,8 @@ uint64_t MaxDatagramPayload(uint64_t max_frame_size) { V(DATAGRAMS_RECEIVED, datagrams_received) \ V(DATAGRAMS_SENT, datagrams_sent) \ V(DATAGRAMS_ACKNOWLEDGED, datagrams_acknowledged) \ - V(DATAGRAMS_LOST, datagrams_lost) + V(DATAGRAMS_LOST, datagrams_lost) \ + V(STREAMS_IDLE_TIMED_OUT, streams_idle_timed_out) #define NO_SIDE_EFFECT true #define SIDE_EFFECT false @@ -617,7 +618,7 @@ Maybe Session::Options::From(Environment* env, !SET(keep_alive_timeout) || !SET(max_stream_window) || !SET(max_window) || !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || !SET(cc_algorithm) || !SET(draining_period_multiplier) || - !SET(max_datagram_send_attempts)) { + !SET(max_datagram_send_attempts) || !SET(stream_idle_timeout)) { return Nothing(); } @@ -2371,13 +2372,15 @@ bool Session::ReadPacket(const uint8_t* data, DCHECK(is_server()); Debug(this, "Receiving packet failed: Server must send a retry packet"); if (!is_destroyed()) { - endpoint().SendRetry(PathDescriptor{ - version(), - config().dcid, - config().scid, - impl_->local_address_, - impl_->remote_address_, - }); + endpoint().SendRetry( + PathDescriptor{ + version(), + config().dcid, + config().scid, + impl_->local_address_, + impl_->remote_address_, + }, + uv_hrtime()); Close(CloseMethod::SILENT); } return false; @@ -2809,24 +2812,36 @@ void Session::ShutdownStream(stream_id id, QuicError error) { DCHECK(!is_destroyed()); Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream(*this, - 0, - id, - error.type() == QuicError::Type::APPLICATION - ? error.code() - : application().GetNoErrorCode()); + // STOP_SENDING and RESET_STREAM frames carry application-level error + // codes (RFC 9000 §19.4, §19.5). Map the QuicError to an appropriate + // application code: APPLICATION errors pass through directly; transport + // no-error maps to the application's no-error code; any other error + // maps to the application's internal error code. + error_code code; + if (error.type() == QuicError::Type::APPLICATION) { + code = error.code(); + } else if (error.code() == NGTCP2_NO_ERROR) { + code = application().GetNoErrorCode(); + } else { + code = application().GetInternalErrorCode(); + } + ngtcp2_conn_shutdown_stream(*this, 0, id, code); } -void Session::ShutdownStreamWrite(stream_id id, QuicError code) { +void Session::ShutdownStreamWrite(stream_id id, QuicError error) { DCHECK(!is_destroyed()); - Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); + Debug( + this, "Shutting down stream %" PRIi64 " write with error %s", id, error); SendPendingDataScope send_scope(this); - ngtcp2_conn_shutdown_stream_write(*this, - 0, - id, - code.type() == QuicError::Type::APPLICATION - ? code.code() - : application().GetNoErrorCode()); + error_code code; + if (error.type() == QuicError::Type::APPLICATION) { + code = error.code(); + } else if (error.code() == NGTCP2_NO_ERROR) { + code = application().GetNoErrorCode(); + } else { + code = application().GetInternalErrorCode(); + } + ngtcp2_conn_shutdown_stream_write(*this, 0, id, code); } void Session::StreamDataBlocked(stream_id id) { @@ -3025,6 +3040,39 @@ void Session::UpdateDataStats() { std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); } +void Session::CheckStreamIdleTimeout(uint64_t now) { + if (is_destroyed()) return; + uint64_t timeout = options().stream_idle_timeout; + if (timeout == 0) return; + + uint64_t timeout_ns = timeout * NGTCP2_MILLISECONDS; + auto all_streams = streams(); + + for (const auto& [id, stream] : all_streams) { + if (!stream) continue; + + // Only check peer-initiated streams. Locally-initiated streams + // that haven't been written to are the application's concern. + if (ngtcp2_conn_is_local_stream(*this, id)) continue; + + uint64_t last_activity = stream->last_activity_timestamp(); + if (last_activity > 0 && (now - last_activity) > timeout_ns) { + Debug(this, "Stream %" PRId64 " idle timeout exceeded, destroying", id); + // Notify the peer before destroying. ShutdownStream sends both + // STOP_SENDING and RESET_STREAM as appropriate, using the + // application's no-error code for non-APPLICATION errors (since + // these frames carry application-level error codes per RFC 9000). + // Without this, the peer's stream sits orphaned until the + // session closes. + auto error = + QuicError::ForNgtcp2Error(NGTCP2_ERR_PROTO, "stream idle timeout"); + ShutdownStream(id, error); + stream->Destroy(error); + STAT_INCREMENT(Stats, streams_idle_timed_out); + } + } +} + void Session::SendConnectionClose() { // Method is a non-op if the session is already destroyed or the // endpoint cannot send. Note: we intentionally do NOT check @@ -3109,6 +3157,8 @@ void Session::OnTimeout() { if (is_destroyed()) return; if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { application().SendPendingData(); + if (is_destroyed()) return; + CheckStreamIdleTimeout(uv_hrtime()); return; } if (is_destroyed()) return; @@ -3155,6 +3205,15 @@ void Session::UpdateTimer() { auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; Debug(this, "Updating timeout to %zu milliseconds", timeout); + // If a stream idle timeout is configured, ensure the timer fires at + // least that often so CheckStreamIdleTimeout runs. Without this, an + // idle session with idle streams might not fire the timer until the + // connection idle timeout, which could be much longer. + uint64_t stream_idle = options().stream_idle_timeout; + if (stream_idle > 0 && timeout > stream_idle) { + timeout = stream_idle; + } + // If timeout is zero here, it means our timer is less than a millisecond // off from expiry. Let's bump the timer to 1. impl_->timer_.Update(timeout == 0 ? 1 : timeout); @@ -3392,12 +3451,21 @@ void Session::EmitClose(const QuicError& error) { Integer::New(env()->isolate(), static_cast(error.type())), BigInt::NewFromUnsigned(env()->isolate(), error.code()), Undefined(env()->isolate()), + Undefined(env()->isolate()), }; if (error.reason().length() > 0 && !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { return; } + // Attach a human-readable name for known wire codes (RFC 9000 sec. 20.1 + // names and OpenSSL TLS alert descriptions for CRYPTO_ERROR). Unknown + // codes leave the slot as undefined. See QuicError::name() for the + // matching path on stream-level errors. + if (const char* n = error.name()) { + argv[3] = BindingData::Get(env()).error_name_string(n); + } + MakeCallback( BindingData::Get(env()).session_close_callback(), arraysize(argv), argv); diff --git a/src/quic/session.h b/src/quic/session.h index 9fb67fb918d818..64c9b8a84d7cf0 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -72,7 +72,11 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint64_t max_header_length = DEFAULT_MAX_HEADER_LENGTH; // HTTP/3 specific options. - uint64_t max_field_section_size = 0; + // The maximum header section size advertised to the peer in SETTINGS. + // Defaults to match max_header_length so the SETTINGS frame accurately + // reflects the enforcement limit. A value of 0 would incorrectly tell + // the peer not to send any headers at all. + uint64_t max_field_section_size = DEFAULT_MAX_HEADER_LENGTH; uint64_t qpack_max_dtable_capacity = 4096; uint64_t qpack_encoder_max_dtable_capacity = 4096; uint64_t qpack_blocked_streams = 100; @@ -223,6 +227,14 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // 10.2 requires at least 3x PTO. Range: 3-255. Default: 3. uint8_t draining_period_multiplier = 3; + // The amount of time (in milliseconds) that a stream can be idle + // (no data received) before it is automatically destroyed. This + // protects against slowloris-style attacks where a peer opens streams + // but never sends data, holding server resources indefinitely. + // Only applies to peer-initiated streams. Set to 0 to disable. + static constexpr uint64_t DEFAULT_STREAM_IDLE_TIMEOUT = 30'000; + uint64_t stream_idle_timeout = DEFAULT_STREAM_IDLE_TIMEOUT; + // An optional NEW_TOKEN from a previous connection to the same // server. When set, the token is included in the Initial packet // to skip address validation. Client-side only. @@ -565,6 +577,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // Has to be called after certain operations that generate packets. void UpdatePacketTxTime(); void UpdateDataStats(); + void CheckStreamIdleTimeout(uint64_t now); void UpdatePath(const PathStorage& path); void ProcessPendingBidiStreams(); diff --git a/src/quic/streams.cc b/src/quic/streams.cc index a48b5243f26b03..7186aed89a78e9 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -1270,7 +1270,8 @@ void Stream::NotifyStreamOpened(stream_id id) { // Headers were enqueued while the application was not yet known // (headers_supported == 0), and the negotiated application does // not support headers. This is a fatal mismatch. - Destroy(QuicError::ForApplication(0)); + Destroy(QuicError::ForApplication( + session().application().GetInternalErrorCode())); return; } decltype(pending_headers_queue_) queue; @@ -1347,6 +1348,11 @@ Session& Stream::session() const { return *session_; } +uint64_t Stream::last_activity_timestamp() const { + uint64_t ts = stats()->received_at; + return ts != 0 ? ts : stats()->created_at; +} + bool Stream::is_local_unidirectional() const { return direction() == Direction::UNIDIRECTIONAL && ngtcp2_conn_is_local_stream(*session_, id()); @@ -1625,6 +1631,7 @@ void Stream::EndReadable(std::optional maybe_final_size) { void Stream::Destroy(QuicError error) { if (stats()->destroyed_at != 0) return; + // Record the destroyed at timestamp before notifying the JavaScript side // that the stream is being destroyed. STAT_RECORD_TIMESTAMP(Stats, destroyed_at); @@ -1884,6 +1891,7 @@ void Stream::EmitClose(const QuicError& error) { } void Stream::EmitHeaders() { + STAT_RECORD_TIMESTAMP(Stats, received_at); // state()->wants_headers will be set from the javascript side if the // stream object has a handler for the headers event. if (!env()->can_call_into_js() || !state()->wants_headers) { diff --git a/src/quic/streams.h b/src/quic/streams.h index 775ba6a33c9168..86cb36b2668985 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -258,6 +258,11 @@ class Stream final : public AsyncWrap, Session& session() const; + // Returns the most recent activity timestamp for this stream in + // nanoseconds (uv_hrtime). Uses received_at if data has been received, + // otherwise falls back to created_at. Returns 0 if neither is set. + uint64_t last_activity_timestamp() const; + // True if this stream was created in a pending state and is still waiting // to be created. bool is_pending() const; diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 2eba97ab8ad2a2..c0a1610540ed3a 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -261,6 +261,18 @@ bool OSSLContext::set_hostname(std::string_view hostname) const { const_cast(name.c_str())) == 1; } +bool OSSLContext::set_verify_hostname(std::string_view hostname) const { + // SSL_set1_host tells OpenSSL to verify the peer certificate's + // subject name (SAN/CN) matches this hostname. This is separate + // from SSL_set_tlsext_host_name which only sets the SNI extension. + static const char* kDefaultHostname = "localhost"; + if (hostname.empty()) { + return SSL_set1_host(*this, kDefaultHostname) == 1; + } else { + return SSL_set1_host(*this, hostname.data()) == 1; + } +} + bool OSSLContext::set_early_data_enabled() const { return SSL_set_quic_tls_early_data_enabled(*this, 1) == 1; } @@ -500,6 +512,14 @@ SSLCtxPointer TLSContext::Initialize(Environment* env) { SSL_CTX_set_session_cache_mode( ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL); SSL_CTX_sess_set_new_cb(ctx.get(), OnNewSession); + + // In strict mode, set SSL_VERIFY_PEER so OpenSSL aborts the + // handshake if the server's certificate fails validation. In + // non-strict modes, verification still occurs but the handshake + // completes regardless — the result is surfaced to JS. + if (options_.verify_peer_strict) { + SSL_CTX_set_verify(ctx.get(), SSL_VERIFY_PEER, nullptr); + } break; } } @@ -706,6 +726,7 @@ Maybe TLSContext::Options::From(Environment* env, env, &options, params, state.name##_string()) if (!SET(verify_client) || !SET(reject_unauthorized) || + !SET(verify_hostname) || !SET(verify_peer_strict) || !SET(enable_early_data) || !SET(enable_tls_trace) || !SET(alpn) || !SET(servername) || !SET(ciphers) || !SET(groups) || !SET(verify_private_key) || !SET(keylog) || !SET(port) || @@ -730,6 +751,8 @@ std::string TLSContext::Options::ToString() const { (verify_client ? std::string("yes") : std::string("no")); res += prefix + "reject unauthorized: " + (reject_unauthorized ? std::string("yes") : std::string("no")); + res += prefix + "verify peer strict: " + + (verify_peer_strict ? std::string("yes") : std::string("no")); res += prefix + "enable early data: " + (enable_early_data ? std::string("yes") : std::string("no")); res += prefix + "enable_tls_trace: " + @@ -844,6 +867,14 @@ void TLSSession::Initialize( return; } + if (options.verify_hostname) { + if (!ossl_context_.set_verify_hostname(options.servername)) { + validation_error_ = "Failed to set verify hostname"; + ossl_context_.reset(); + return; + } + } + if (maybeSessionTicket.has_value()) { const auto& sessionTicket = *maybeSessionTicket; uv_buf_t buf = sessionTicket.ticket(); diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index c1d9b95d5613b4..ba502cf1f868df 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -51,6 +51,7 @@ class OSSLContext final { bool set_alpn_protocols(std::string_view protocols) const; bool set_hostname(std::string_view hostname) const; + bool set_verify_hostname(std::string_view hostname) const; bool set_early_data_enabled() const; bool set_transport_params(const ngtcp2_vec& tp) const; @@ -207,6 +208,21 @@ class TLSContext final : public MemoryRetainer, // This option is only used by the server side. bool reject_unauthorized = true; + // When true, the client will set SSL_VERIFY_PEER so that OpenSSL + // aborts the handshake if the server's certificate fails validation. + // This is the "strict" verify_peer mode. When false (the default), + // the handshake completes regardless and VerifyPeerIdentity is + // called after to surface errors to JS. This option is only used + // by the client side. + bool verify_peer_strict = false; + + // When true, OpenSSL verifies that the server's certificate matches + // the servername (hostname verification via SSL_set1_host). Should + // be true for 'strict' and 'auto' verifyPeer modes, false for + // 'manual'. Without this, a valid certificate for any domain would + // be accepted. This option is only used by the client side. + bool verify_hostname = false; + // When true (the default), the server accepts 0-RTT early data // from clients with valid session tickets. When false, early data // is disabled and clients must complete a full handshake before diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index fb348b02e01b24..f9a429cef66279 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -170,10 +170,9 @@ ngtcp2_vec GenerateRetryToken(uint8_t* buffer, odcid, uv_hrtime()); DCHECK_GE(ret, 0); - DCHECK_LE(ret, RetryToken::kRetryTokenLen); DCHECK_EQ(buffer[0], RetryToken::kTokenMagic); // This shouldn't be possible but we handle it anyway just to be safe. - if (ret == 0) return {nullptr, 0}; + if (ret <= 0) return {nullptr, 0}; return {buffer, static_cast(ret)}; } @@ -189,10 +188,9 @@ ngtcp2_vec GenerateRegularToken(uint8_t* buffer, address.length(), uv_hrtime()); DCHECK_GE(ret, 0); - DCHECK_LE(ret, RegularToken::kRegularTokenLen); DCHECK_EQ(buffer[0], RegularToken::kTokenMagic); // This shouldn't be possible but we handle it anyway just to be safe. - if (ret == 0) return {nullptr, 0}; + if (ret <= 0) return {nullptr, 0}; return {buffer, static_cast(ret)}; } } // namespace diff --git a/src/string_bytes.cc b/src/string_bytes.cc index 865302bfd1b4de..1d4ee3a81803b2 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -671,6 +671,40 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, } } +MaybeLocal StringBytes::EncodeValidUtf8(Isolate* isolate, + const char* buf, + size_t buflen) { + CHECK_BUFLEN_IN_RANGE(buflen); + if (!buflen) return String::Empty(isolate); + buflen = keep_buflen_in_range(buflen); + + // ASCII fast path + if (!simdutf::validate_ascii_with_errors(buf, buflen).error) { + return ExternOneByteString::NewFromCopy(isolate, buf, buflen); + } + + if (buflen >= 32) { + size_t u16size = simdutf::utf16_length_from_utf8(buf, buflen); + if (u16size > static_cast(v8::String::kMaxLength)) { + isolate->ThrowException(ERR_STRING_TOO_LONG(isolate)); + return MaybeLocal(); + } + return EncodeTwoByteString( + isolate, u16size, [buf, buflen, u16size](uint16_t* dst) { + size_t written = simdutf::convert_valid_utf8_to_utf16( + buf, buflen, reinterpret_cast(dst)); + CHECK_EQ(written, u16size); + }); + } + + Local str; + if (!String::NewFromUtf8(isolate, buf, v8::NewStringType::kNormal, buflen) + .ToLocal(&str)) { + isolate->ThrowException(node::ERR_STRING_TOO_LONG(isolate)); + } + return str; +} + MaybeLocal StringBytes::Encode(Isolate* isolate, const uint16_t* buf, size_t buflen) { diff --git a/src/string_bytes.h b/src/string_bytes.h index 9949f508f83ffe..71aa9ff1f90a7c 100644 --- a/src/string_bytes.h +++ b/src/string_bytes.h @@ -83,6 +83,11 @@ class StringBytes { size_t buflen, enum encoding encoding); + // Like Encode(..., UTF8) but does not re-validate. Input must be valid UTF-8. + static v8::MaybeLocal EncodeValidUtf8(v8::Isolate* isolate, + const char* buf, + size_t buflen); + // Warning: This reverses endianness on BE platforms, even though the // signature using uint16_t implies that it should not. // However, the brokenness is already public API and can't therefore diff --git a/src/util-inl.h b/src/util-inl.h index d59e30a635b08b..e357d15a14496d 100644 --- a/src/util-inl.h +++ b/src/util-inl.h @@ -341,22 +341,6 @@ v8::Maybe FromV8Array(v8::Local context, return js_array->Iterate(context, PushItemToVector, &data); } -v8::MaybeLocal ToV8Value(v8::Local context, - std::string_view str, - v8::Isolate* isolate) { - if (isolate == nullptr) isolate = v8::Isolate::GetCurrent(); - if (str.size() >= static_cast(v8::String::kMaxLength)) [[unlikely]] { - // V8 only has a TODO comment about adding an exception when the maximum - // string size is exceeded. - ThrowErrStringTooLong(isolate); - return v8::MaybeLocal(); - } - - return v8::String::NewFromUtf8( - isolate, str.data(), v8::NewStringType::kNormal, str.size()) - .FromMaybe(v8::Local()); -} - v8::MaybeLocal ToV8Value(v8::Local context, std::u16string_view str, v8::Isolate* isolate) { diff --git a/src/util.cc b/src/util.cc index 1ea51cf7012963..317b8db0daac69 100644 --- a/src/util.cc +++ b/src/util.cc @@ -812,4 +812,15 @@ v8::Maybe GetValidFileMode(Environment* env, return v8::Just(mode); } +v8::MaybeLocal ToV8Value(v8::Local context, + std::string_view str, + v8::Isolate* isolate) { + if (isolate == nullptr) isolate = v8::Isolate::GetCurrent(); + if (str.size() >= static_cast(v8::String::kMaxLength)) [[unlikely]] { + ThrowErrStringTooLong(isolate); + return v8::MaybeLocal(); + } + return StringBytes::Encode(isolate, str.data(), str.size(), UTF8); +} + } // namespace node diff --git a/src/util.h b/src/util.h index 3dedeca4d227e9..48305bfdc13143 100644 --- a/src/util.h +++ b/src/util.h @@ -701,9 +701,9 @@ inline v8::Maybe FromV8Array(v8::Local context, v8::Local js_array, std::vector>* out); -inline v8::MaybeLocal ToV8Value(v8::Local context, - std::string_view str, - v8::Isolate* isolate = nullptr); +v8::MaybeLocal ToV8Value(v8::Local context, + std::string_view str, + v8::Isolate* isolate = nullptr); inline v8::MaybeLocal ToV8Value(v8::Local context, std::u16string_view str, v8::Isolate* isolate = nullptr); 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. diff --git a/test/cctest/test_quic_tokenbucket.cc b/test/cctest/test_quic_tokenbucket.cc new file mode 100644 index 00000000000000..5c5ef5b3cbc975 --- /dev/null +++ b/test/cctest/test_quic_tokenbucket.cc @@ -0,0 +1,125 @@ +#if HAVE_OPENSSL && HAVE_QUIC +#include "quic/guard.h" +#ifndef OPENSSL_NO_QUIC +#include +#include +#include +#include + +namespace node::quic { +namespace { + +// Helper: nanoseconds from seconds +static constexpr uint64_t secs(double s) { + return static_cast(s * 1e9); +} + +TEST(QuicTokenBucket, AllowsBurst) { + TokenBucket bucket(10, 5); // 10/sec rate, burst of 5 + uint64_t now = secs(1000); + + // Should allow up to burst count immediately + for (int i = 0; i < 5; i++) { + EXPECT_TRUE(bucket.consume(now)) << "consume " << i << " should succeed"; + } + + // Bucket should be empty now + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, RefillsOverTime) { + TokenBucket bucket(1000, 1); // 1000/sec rate, burst of 1 + uint64_t now = secs(1000); + + // Drain the bucket + EXPECT_TRUE(bucket.consume(now)); + EXPECT_FALSE(bucket.consume(now)); + + // Advance 2ms — at 1000/sec that's ~2 tokens, but burst is 1 + now += secs(0.002); + + // Should have refilled to 1 (capped by burst) + EXPECT_TRUE(bucket.consume(now)); + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, BurstCapacity) { + TokenBucket bucket(10000, 3); // high rate, burst of 3 + uint64_t now = secs(1000); + + // Drain + EXPECT_TRUE(bucket.consume(now)); + EXPECT_TRUE(bucket.consume(now)); + EXPECT_TRUE(bucket.consume(now)); + EXPECT_FALSE(bucket.consume(now)); + + // Advance enough to fully refill + now += secs(1); + + // Should be capped at burst (3), not more + int count = 0; + while (bucket.consume(now)) count++; + EXPECT_EQ(count, 3); +} + +TEST(QuicTokenBucket, ZeroRateAlwaysDenies) { + TokenBucket bucket(0, 0); + uint64_t now = secs(1000); + EXPECT_FALSE(bucket.consume(now)); + now += secs(10); + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, HighRateAllowsRapidConsume) { + TokenBucket bucket(1000000, 1000); // 1M/sec, burst 1000 + uint64_t now = secs(1000); + + // Should be able to consume the full burst + int count = 0; + for (int i = 0; i < 1000; i++) { + if (bucket.consume(now)) count++; + } + EXPECT_EQ(count, 1000); +} + +TEST(QuicTokenBucket, InitOnce) { + TokenBucket bucket; // default: rate=0, burst=0, last_ts=0 + uint64_t now = secs(1000); + + // Init with rate=100, burst=5 + bucket.InitOnce(100, 5, now); + EXPECT_TRUE(bucket.consume(now)); + + // InitOnce is idempotent — second call is a no-op + bucket.InitOnce(0, 0, now); // would make it deny-all if applied + EXPECT_TRUE(bucket.consume(now)); // still has tokens from first init +} + +TEST(QuicTokenBucket, DefaultConstructorDenies) { + TokenBucket bucket; // default: rate=0, burst=0, last_ts=0 + uint64_t now = secs(1000); + EXPECT_FALSE(bucket.consume(now)); +} + +TEST(QuicTokenBucket, GradualRefill) { + TokenBucket bucket(10, 10); // 10/sec rate, burst of 10 + uint64_t now = secs(1000); + + // Drain fully + for (int i = 0; i < 10; i++) { + EXPECT_TRUE(bucket.consume(now)); + } + EXPECT_FALSE(bucket.consume(now)); + + // Advance 500ms — should get 5 tokens + now += secs(0.5); + int count = 0; + while (bucket.consume(now)) count++; + EXPECT_EQ(count, 5); +} + +} // namespace +} // namespace node::quic + +#endif // OPENSSL_NO_QUIC +#endif // HAVE_OPENSSL && HAVE_QUIC diff --git a/test/cctest/test_sockaddr.cc b/test/cctest/test_sockaddr.cc index 9d589482f0188f..a4feefd6f4b3f6 100644 --- a/test/cctest/test_sockaddr.cc +++ b/test/cctest/test_sockaddr.cc @@ -146,11 +146,13 @@ TEST(SocketAddressLRU, SocketAddressLRU) { struct FooLRUTraits { using Type = Foo; - static bool CheckExpired(const SocketAddress& address, const Type& type) { + static bool CheckExpired(const SocketAddress& address, + const Type& type, + uint64_t now) { return type.expired; } - static void Touch(const SocketAddress& address, Type* type) { + static void Touch(const SocketAddress& address, Type* type, uint64_t now) { type->expired = false; } }; @@ -169,7 +171,8 @@ TEST(SocketAddressLRU, SocketAddressLRU) { SocketAddress addr3(reinterpret_cast(&storage[2])); SocketAddress addr4(reinterpret_cast(&storage[3])); - Foo* foo = lru.Upsert(addr1); + uint64_t now = uv_hrtime(); + Foo* foo = lru.Upsert(addr1, now); CHECK_NOT_NULL(foo); CHECK_EQ(foo->c, 0); CHECK_EQ(foo->expired, false); @@ -177,14 +180,14 @@ TEST(SocketAddressLRU, SocketAddressLRU) { foo->c = 1; foo->expired = true; - foo = lru.Upsert(addr1); + foo = lru.Upsert(addr1, now); CHECK_NOT_NULL(lru.Peek(addr1)); CHECK_EQ(lru.Peek(addr1), lru.Peek(addr4)); CHECK_EQ(lru.Peek(addr1)->c, 1); CHECK_EQ(lru.Peek(addr1)->expired, false); CHECK_EQ(lru.size(), 1); - foo = lru.Upsert(addr2); + foo = lru.Upsert(addr2, now); foo->c = 2; foo->expired = true; CHECK_NOT_NULL(lru.Peek(addr2)); @@ -193,7 +196,7 @@ TEST(SocketAddressLRU, SocketAddressLRU) { foo->expired = true; - foo = lru.Upsert(addr3); + foo = lru.Upsert(addr3, now); foo->c = 3; foo->expired = false; CHECK_NOT_NULL(lru.Peek(addr3)); @@ -283,11 +286,11 @@ TEST(SocketAddressBlockList, Simple) { bl.AddSocketAddress(addr1); bl.AddSocketAddress(addr2); - CHECK(bl.Apply(addr1)); - CHECK(bl.Apply(addr2)); + CHECK(bl.Apply(*addr1)); + CHECK(bl.Apply(*addr2)); bl.RemoveSocketAddress(addr1); - CHECK(!bl.Apply(addr1)); - CHECK(bl.Apply(addr2)); + CHECK(!bl.Apply(*addr1)); + CHECK(bl.Apply(*addr2)); } 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/common/quic.mjs b/test/common/quic.mjs index 7bc7a427b992ac..d05ee634f5e5ea 100644 --- a/test/common/quic.mjs +++ b/test/common/quic.mjs @@ -44,9 +44,13 @@ async function listen(callback, options = {}) { async function connect(address, options = {}) { const { alpn = 'quic-test', + // Test helper defaults to 'manual' because tests use self-signed + // certs without a CA. Tests that want to verify cert validation + // behavior should set verifyPeer explicitly. + verifyPeer = 'manual', ...rest } = options; - return quic.connect(address, { alpn, ...rest }); + return quic.connect(address, { alpn, verifyPeer, ...rest }); } export { diff --git a/test/eslint.config_partial.mjs b/test/eslint.config_partial.mjs index 6fbdf277044679..70c00ef5d4dcda 100644 --- a/test/eslint.config_partial.mjs +++ b/test/eslint.config_partial.mjs @@ -162,6 +162,7 @@ export default [ 'node-core/require-common-first': 'error', 'node-core/no-duplicate-requires': 'off', 'node-core/must-call-assert': 'error', + 'node-core/prefer-abort-signal-abort': 'error', }, }, { 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-void-parameter.js b/test/ffi/test-ffi-void-parameter.js new file mode 100644 index 00000000000000..d79631ed542f2b --- /dev/null +++ b/test/ffi/test-ffi-void-parameter.js @@ -0,0 +1,29 @@ +// Flags: --experimental-ffi +'use strict'; + +const common = require('../common'); +common.skipIfFFIMissing(); + +const assert = require('node:assert'); +const ffi = require('node:ffi'); +const { libraryPath } = require('./ffi-test-common'); + +// Regression test for https://github.com/nodejs/node/issues/63461 +// 'void' as a parameter type should throw ERR_INVALID_ARG_VALUE +// instead of triggering ERR_INTERNAL_ASSERTION. + +const lib = new ffi.DynamicLibrary(libraryPath); + +try { + assert.throws(() => { + lib.getFunction('add_i32', { return: 'i32', arguments: ['void'] }); + }, { code: 'ERR_INVALID_ARG_VALUE' }); + + assert.throws(() => { + lib.getFunctions({ + add_i32: { return: 'i32', arguments: ['void'] }, + }); + }, { code: 'ERR_INVALID_ARG_VALUE' }); +} finally { + lib.close(); +} 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); 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/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/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, }); 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/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs new file mode 100644 index 00000000000000..1cb9576a123d5a --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs @@ -0,0 +1,3 @@ +console.log('Hi'); +export {}; +//# sourceMappingURL=a.mjs.map diff --git a/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs.map b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs.map new file mode 100644 index 00000000000000..acbd8f58897d9a --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/dist/a.mjs.map @@ -0,0 +1 @@ +{"version":3,"file":"a.mjs","sourceRoot":"","sources":["../src/a.mts"],"names":[],"mappings":"AAEA,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC"} diff --git a/test/fixtures/test-runner/source-maps/type-only-import/src/a.mts b/test/fixtures/test-runner/source-maps/type-only-import/src/a.mts new file mode 100644 index 00000000000000..16e1f9f65763a7 --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/src/a.mts @@ -0,0 +1,3 @@ +import type {} from 'node:assert'; + +console.log('Hi'); diff --git a/test/fixtures/test-runner/source-maps/type-only-import/test.mjs b/test/fixtures/test-runner/source-maps/type-only-import/test.mjs new file mode 100644 index 00000000000000..dd791e471160c8 --- /dev/null +++ b/test/fixtures/test-runner/source-maps/type-only-import/test.mjs @@ -0,0 +1 @@ +import './dist/a.mjs'; diff --git a/test/fixtures/test426/decoding/scopes/README.md b/test/fixtures/test426/decoding/scopes/README.md new file mode 100644 index 00000000000000..7b3271adb3ada7 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/README.md @@ -0,0 +1,18 @@ +## Test cases + +- **empty-scopes-field**: Empty original scopes field +- **nil-scopes**: Multiple null original scopes +- **close-start-end-position-scopes**: Scopes with very close start and end + positions +- **single-root-original-scope**: A single global root original scope +- **nested-scopes**: Nested original scopes representing functions and blocks +- **sibling-scopes**: Multiple sibling top-level functions +- **scope-variables**: Scopes containing variable declarations +- **multiple-root-original-scopes-with-nil**: Multiple global root scopes with a + null scope in between +- **sibling-ranges**: Multiple sibling root ranges +- **nested-ranges**: A root range with a nested function range +- **range-values**: A root range with a non-function range with bindings +- **sub-range-values**: A root range with sub-range bindings +- **range-call-site**: A function inlined into the root scope +- **hidden-ranges**: A function range corresponding to an original block scope diff --git a/test/fixtures/test426/decoding/scopes/hidden-ranges.map b/test/fixtures/test426/decoding/scopes/hidden-ranges.map new file mode 100644 index 00000000000000..779a24269ff2a4 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/hidden-ranges.map @@ -0,0 +1,13 @@ +{ + "version": 3, + "file": "hidden-ranges.js", + "sources": [ + "original_0.js" + ], + "mappings": "", + "names": [ + "global", + "block" + ], + "scopes": "BCAAA,BCBAC,CEA,CFA,ECAA,EPBAC,FEA,FFA" +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/hidden-ranges.map.golden b/test/fixtures/test426/decoding/scopes/hidden-ranges.map.golden new file mode 100644 index 00000000000000..6564cca5ea4e9d --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/hidden-ranges.map.golden @@ -0,0 +1,75 @@ +{ + "file": "hidden-ranges.js", + "mappings": [], + "sources": [ + { + "url": "original_0.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [], + "children": [ + { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + }, + "name": null, + "kind": "block", + "isStackFrame": false, + "variables": [], + "children": [] + } + ] + } + } + ], + "ranges": [ + { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "definitionIndex": 0, + "stackFrameType": "none", + "callSite": null, + "bindings": [], + "children": [ + { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + }, + "definitionIndex": 1, + "stackFrameType": "hidden", + "callSite": null, + "bindings": [], + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/nested-ranges.map b/test/fixtures/test426/decoding/scopes/nested-ranges.map new file mode 100644 index 00000000000000..2ab612337ed9a5 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/nested-ranges.map @@ -0,0 +1,14 @@ +{ + "version": 3, + "file": "nested-ranges.js", + "sources": [ + "original_0.js" + ], + "mappings": "", + "names": [ + "global", + "foo", + "function" + ], + "scopes": "BCAAA,BHBACE,CEA,CFA,ECAA,EGCC,FC,FG" +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/nested-ranges.map.golden b/test/fixtures/test426/decoding/scopes/nested-ranges.map.golden new file mode 100644 index 00000000000000..4d4138f113d10e --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/nested-ranges.map.golden @@ -0,0 +1,75 @@ +{ + "file": "nested-ranges.js", + "mappings": [], + "sources": [ + { + "url": "original_0.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [], + "children": [ + { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + }, + "name": "foo", + "kind": "function", + "isStackFrame": true, + "variables": [], + "children": [] + } + ] + } + } + ], + "ranges": [ + { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 0, + "column": 10 + }, + "definitionIndex": 0, + "stackFrameType": "none", + "callSite": null, + "bindings": [], + "children": [ + { + "start": { + "line": 0, + "column": 2 + }, + "end": { + "line": 0, + "column": 4 + }, + "definitionIndex": 1, + "stackFrameType": "original", + "callSite": null, + "bindings": [], + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/range-call-site.map b/test/fixtures/test426/decoding/scopes/range-call-site.map new file mode 100644 index 00000000000000..61e5a4c5a5dcee --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/range-call-site.map @@ -0,0 +1,14 @@ +{ + "version": 3, + "file": "range-call-site.js", + "sources": [ + "original_0.js" + ], + "mappings": "", + "names": [ + "global", + "inlineMe", + "function" + ], + "scopes": "BCAAA,BHBACE,CEA,CFA,ECAA,EDBAC,IAGC,FK,FJA" +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/range-call-site.map.golden b/test/fixtures/test426/decoding/scopes/range-call-site.map.golden new file mode 100644 index 00000000000000..b3ee2302946c44 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/range-call-site.map.golden @@ -0,0 +1,79 @@ +{ + "file": "range-call-site.js", + "mappings": [], + "sources": [ + { + "url": "original_0.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [], + "children": [ + { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + }, + "name": "inlineMe", + "kind": "function", + "isStackFrame": true, + "variables": [], + "children": [] + } + ] + } + } + ], + "ranges": [ + { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "definitionIndex": 0, + "stackFrameType": "none", + "callSite": null, + "bindings": [], + "children": [ + { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 10 + }, + "definitionIndex": 1, + "stackFrameType": "none", + "callSite": { + "sourceIndex": 0, + "line": 6, + "column": 2 + }, + "bindings": [], + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/range-values.map b/test/fixtures/test426/decoding/scopes/range-values.map new file mode 100644 index 00000000000000..c397423a2431c8 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/range-values.map @@ -0,0 +1,20 @@ +{ + "version": 3, + "file": "range-values.js", + "sources": [ + "original_0.js" + ], + "mappings": "", + "names": [ + "global", + "x", + "y", + "z", + "block", + "a", + "x_val", + "z_val", + "a_val" + ], + "scopes": "BCAAA,DCCC,BCBAI,DE,CEA,CFA,ECAA,GHAI,ECCC,GJ,FC,FG" +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/range-values.map.golden b/test/fixtures/test426/decoding/scopes/range-values.map.golden new file mode 100644 index 00000000000000..a7cdec5139da88 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/range-values.map.golden @@ -0,0 +1,119 @@ +{ + "file": "range-values.js", + "mappings": [], + "sources": [ + { + "url": "original_0.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [ + "x", + "y", + "z" + ], + "children": [ + { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 0 + }, + "name": null, + "kind": "block", + "isStackFrame": false, + "variables": [ + "a" + ], + "children": [] + } + ] + } + } + ], + "ranges": [ + { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 0, + "column": 10 + }, + "definitionIndex": 0, + "stackFrameType": "none", + "callSite": null, + "bindings": [ + [ + { + "binding": "x_val", + "from": { + "line": 0, + "column": 0 + } + } + ], + [ + { + "binding": null, + "from": { + "line": 0, + "column": 0 + } + } + ], + [ + { + "binding": "z_val", + "from": { + "line": 0, + "column": 0 + } + } + ] + ], + "children": [ + { + "start": { + "line": 0, + "column": 2 + }, + "end": { + "line": 0, + "column": 4 + }, + "definitionIndex": 1, + "stackFrameType": "none", + "callSite": null, + "bindings": [ + [ + { + "binding": "a_val", + "from": { + "line": 0, + "column": 2 + } + } + ] + ], + "children": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/sibling-ranges.map b/test/fixtures/test426/decoding/scopes/sibling-ranges.map new file mode 100644 index 00000000000000..ea90f645612cc4 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/sibling-ranges.map @@ -0,0 +1,13 @@ +{ + "version": 3, + "file": "sibling-ranges.js", + "sources": [ + "original_0.js", + "original_1.js" + ], + "mappings": "", + "names": [ + "global" + ], + "scopes": "BCAAA,CKA,BCKAA,CKA,ECAA,FK,EDKAC,FK,EBKA,FK" +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/sibling-ranges.map.golden b/test/fixtures/test426/decoding/scopes/sibling-ranges.map.golden new file mode 100644 index 00000000000000..dc1975d40d00a5 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/sibling-ranges.map.golden @@ -0,0 +1,93 @@ +{ + "file": "sibling-ranges.js", + "mappings": [], + "sources": [ + { + "url": "original_0.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [], + "children": [] + } + }, + { + "url": "original_1.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 10, + "column": 0 + }, + "end": { + "line": 20, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [], + "children": [] + } + } + ], + "ranges": [ + { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 0, + "column": 10 + }, + "definitionIndex": 0, + "stackFrameType": "none", + "callSite": null, + "bindings": [], + "children": [] + }, + { + "start": { + "line": 10, + "column": 0 + }, + "end": { + "line": 10, + "column": 10 + }, + "definitionIndex": 1, + "stackFrameType": "none", + "callSite": null, + "bindings": [], + "children": [] + }, + { + "start": { + "line": 20, + "column": 0 + }, + "end": { + "line": 20, + "column": 10 + }, + "definitionIndex": null, + "stackFrameType": "none", + "callSite": null, + "bindings": [], + "children": [] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/sub-range-values.map b/test/fixtures/test426/decoding/scopes/sub-range-values.map new file mode 100644 index 00000000000000..40155a819383c4 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/sub-range-values.map @@ -0,0 +1,17 @@ +{ + "version": 3, + "file": "sub-range-values.js", + "sources": [ + "original_0.js" + ], + "mappings": "", + "names": [ + "global", + "x", + "y", + "x_sub_val1", + "y_val", + "x_sub_val2" + ], + "scopes": "BCAAA,DCC,CKA,ECAA,GEF,HAADAADG,FK" +} \ No newline at end of file diff --git a/test/fixtures/test426/decoding/scopes/sub-range-values.map.golden b/test/fixtures/test426/decoding/scopes/sub-range-values.map.golden new file mode 100644 index 00000000000000..c0662102819561 --- /dev/null +++ b/test/fixtures/test426/decoding/scopes/sub-range-values.map.golden @@ -0,0 +1,79 @@ +{ + "file": "sub-range-values.js", + "mappings": [], + "sources": [ + { + "url": "original_0.js", + "content": null, + "ignored": false, + "scope": { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 10, + "column": 0 + }, + "name": null, + "kind": "global", + "isStackFrame": false, + "variables": [ + "x", + "y" + ], + "children": [] + } + } + ], + "ranges": [ + { + "start": { + "line": 0, + "column": 0 + }, + "end": { + "line": 0, + "column": 10 + }, + "definitionIndex": 0, + "stackFrameType": "none", + "callSite": null, + "bindings": [ + [ + { + "binding": "x_sub_val1", + "from": { + "line": 0, + "column": 0 + } + }, + { + "binding": null, + "from": { + "line": 0, + "column": 3 + } + }, + { + "binding": "x_sub_val2", + "from": { + "line": 0, + "column": 6 + } + } + ], + [ + { + "binding": "y_val", + "from": { + "line": 0, + "column": 0 + } + } + ] + ], + "children": [] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/test426/range-mappings-proposal-tests.json b/test/fixtures/test426/range-mappings-proposal-tests.json index 42d47d57650338..7b108e7e5e15bd 100644 --- a/test/fixtures/test426/range-mappings-proposal-tests.json +++ b/test/fixtures/test426/range-mappings-proposal-tests.json @@ -36,6 +36,13 @@ "sourceMapFile": "invalid-base64-char-2.js.map", "sourceMapIsValid": false }, + { + "name": "rangeMappingsInvalidVLQZero", + "description": "VLQ of zero is invalid because offsets are 1-based", + "baseFile": "invalid-vlq-zero.js", + "sourceMapFile": "invalid-vlq-zero.js.map", + "sourceMapIsValid": false + }, { "name": "rangeMappingsOutOfRange", "description": "Test an invalid range mapping which is outside the mappings length", diff --git a/test/fixtures/test426/resources/proposals/range-mappings/invalid-vlq-zero.js b/test/fixtures/test426/resources/proposals/range-mappings/invalid-vlq-zero.js new file mode 100644 index 00000000000000..c19bce975ac9e5 --- /dev/null +++ b/test/fixtures/test426/resources/proposals/range-mappings/invalid-vlq-zero.js @@ -0,0 +1 @@ +//# sourceMappingURL=invalid-vlq-zero.js.map diff --git a/test/fixtures/test426/resources/proposals/range-mappings/invalid-vlq-zero.js.map b/test/fixtures/test426/resources/proposals/range-mappings/invalid-vlq-zero.js.map new file mode 100644 index 00000000000000..fe13e8f5225acd --- /dev/null +++ b/test/fixtures/test426/resources/proposals/range-mappings/invalid-vlq-zero.js.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "names": [], + "file": "invalid-vlq-zero.js", + "sources": ["empty-original.js"], + "sourcesContent": [""], + "mappings": "A", + "rangeMappings": "A" +} diff --git a/test/fixtures/test426/resources/proposals/range-mappings/multiple-mappings.js.map b/test/fixtures/test426/resources/proposals/range-mappings/multiple-mappings.js.map index 2fab6bd1977c25..c7ffc83e012283 100644 --- a/test/fixtures/test426/resources/proposals/range-mappings/multiple-mappings.js.map +++ b/test/fixtures/test426/resources/proposals/range-mappings/multiple-mappings.js.map @@ -4,5 +4,5 @@ "sources": ["multiple-mappings-original.js"], "sourcesContent": ["\"Hello World\"; function f() { } "], "mappings": ";CAAA,aAAa,EAAG,iBAAoB;A", - "rangeMappings": ";AC;" + "rangeMappings": ";BC;" } diff --git a/test/fixtures/test426/resources/proposals/range-mappings/newline-semantics.js.map b/test/fixtures/test426/resources/proposals/range-mappings/newline-semantics.js.map index 9f9e581b9edd17..b6f7b44282c3ff 100644 --- a/test/fixtures/test426/resources/proposals/range-mappings/newline-semantics.js.map +++ b/test/fixtures/test426/resources/proposals/range-mappings/newline-semantics.js.map @@ -5,5 +5,5 @@ "sources": ["newline-semantics-original.js"], "sourcesContent": ["1234\n5678"], "mappings": "CAAA;GACG", - "rangeMappings": "A;" + "rangeMappings": "B;" } diff --git a/test/fixtures/test426/resources/proposals/range-mappings/non-full-line-coverage.js.map b/test/fixtures/test426/resources/proposals/range-mappings/non-full-line-coverage.js.map index a381b123995747..952a3c30b1d084 100644 --- a/test/fixtures/test426/resources/proposals/range-mappings/non-full-line-coverage.js.map +++ b/test/fixtures/test426/resources/proposals/range-mappings/non-full-line-coverage.js.map @@ -5,5 +5,5 @@ "sources": ["simple-original.js"], "sourcesContent": ["\"Hello World\""], "mappings": ";CAAA;A", - "rangeMappings": ";A" + "rangeMappings": ";B" } diff --git a/test/fixtures/test426/resources/proposals/range-mappings/out-of-range-2.js.map b/test/fixtures/test426/resources/proposals/range-mappings/out-of-range-2.js.map index 152fd6653a8546..f573e035dca549 100644 --- a/test/fixtures/test426/resources/proposals/range-mappings/out-of-range-2.js.map +++ b/test/fixtures/test426/resources/proposals/range-mappings/out-of-range-2.js.map @@ -5,5 +5,5 @@ "sources": ["foo.js"], "sourcesContent": ["\"foo\""], "mappings": "AAA", - "rangeMappings": "B;A;A" + "rangeMappings": "C;B;B" } diff --git a/test/fixtures/test426/resources/proposals/range-mappings/out-of-range.js.map b/test/fixtures/test426/resources/proposals/range-mappings/out-of-range.js.map index 5ad9a234d91120..5da575a7a6355d 100644 --- a/test/fixtures/test426/resources/proposals/range-mappings/out-of-range.js.map +++ b/test/fixtures/test426/resources/proposals/range-mappings/out-of-range.js.map @@ -5,5 +5,5 @@ "sources": ["foo.js"], "sourcesContent": ["\"foo\""], "mappings": "AAA", - "rangeMappings": "B" + "rangeMappings": "C" } diff --git a/test/fixtures/test426/resources/proposals/range-mappings/simple.js.map b/test/fixtures/test426/resources/proposals/range-mappings/simple.js.map index 0cd782bcee0638..856d31a9c84e14 100644 --- a/test/fixtures/test426/resources/proposals/range-mappings/simple.js.map +++ b/test/fixtures/test426/resources/proposals/range-mappings/simple.js.map @@ -5,5 +5,5 @@ "sources": ["simple-original.js"], "sourcesContent": ["\"Hello World\""], "mappings": ";CAAA;A", - "rangeMappings": ";A;" + "rangeMappings": ";B;" } 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/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 10cedb4149edde..b8486fc5705fc3 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -28,7 +28,7 @@ Last update: - resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing - resources: https://github.com/web-platform-tests/wpt/tree/6a2f322376/resources - streams: https://github.com/web-platform-tests/wpt/tree/f8f26a372f/streams -- url: https://github.com/web-platform-tests/wpt/tree/258f285de0/url +- url: https://github.com/web-platform-tests/wpt/tree/e4a4672e9e/url - urlpattern: https://github.com/web-platform-tests/wpt/tree/f07c03cbed/urlpattern - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/288c467d35/wasm/jsapi diff --git a/test/fixtures/wpt/url/idlharness.any.js b/test/fixtures/wpt/url/idlharness.any.js index c0642729c0bec8..e27033e6e8edcc 100644 --- a/test/fixtures/wpt/url/idlharness.any.js +++ b/test/fixtures/wpt/url/idlharness.any.js @@ -1,6 +1,6 @@ // META: script=/resources/WebIDLParser.js // META: script=/resources/idlharness.js -// META: global=window,dedicatedworker,shadowrealm-in-window +// META: global=window,dedicatedworker idl_test( ['url'], diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 470592c842b925..8d1f153c4104b6 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -72,7 +72,7 @@ "path": "streams" }, "url": { - "commit": "258f285de043b79e44324228c0fd800b38d21879", + "commit": "e4a4672e9e607fc2b28e7173b83ce4e38ef53071", "path": "url" }, "urlpattern": { diff --git a/test/parallel/test-abortcontroller.js b/test/parallel/test-abortcontroller.js index 948bd208cd2b90..95eea21437ead9 100644 --- a/test/parallel/test-abortcontroller.js +++ b/test/parallel/test-abortcontroller.js @@ -161,6 +161,7 @@ test('AbortController inspection depth 1 or null works', () => { test('AbortSignal reason is set correctly', () => { // Test AbortSignal.reason + // eslint-disable-next-line node-core/prefer-abort-signal-abort const ac = new AbortController(); ac.abort('reason'); assert.strictEqual(ac.signal.reason, 'reason'); @@ -235,6 +236,7 @@ test('AbortSignal.reason should default', () => { assert.ok(signal.reason instanceof DOMException); assert.strictEqual(signal.reason.code, 20); + // eslint-disable-next-line node-core/prefer-abort-signal-abort const ac = new AbortController(); ac.abort(); assert.ok(ac.signal.reason instanceof DOMException); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 92bf3be1f612ff..69f1d9f44c9c6c 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -133,6 +133,8 @@ if (isMainThread) { 'NativeModule internal/modules/esm/load', 'NativeModule internal/modules/esm/resolve', 'NativeModule internal/modules/esm/translators', + 'NativeModule internal/modules/esm/module_job', + 'NativeModule internal/modules/esm/module_map', 'NativeModule url', ].forEach(expected.beforePreExec.add.bind(expected.beforePreExec)); } else { // Worker. 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-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()); +} 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, }); diff --git a/test/parallel/test-debugger-probe-timeout.js b/test/parallel/test-debugger-probe-timeout.js index da877741ca0cb1..de9309c3f60b94 100644 --- a/test/parallel/test-debugger-probe-timeout.js +++ b/test/parallel/test-debugger-probe-timeout.js @@ -9,10 +9,11 @@ const { spawnSyncAndExit } = require('../common/child_process'); const { assertProbeJson } = require('../common/debugger-probe'); const cwd = fixtures.path('debugger'); +const timeout = common.platformTimeout(1000); spawnSyncAndExit(process.execPath, [ 'inspect', '--json', - '--timeout=200', + `--timeout=${timeout}`, '--probe', 'probe-timeout.js:99', '--expr', '1', 'probe-timeout.js', @@ -31,7 +32,7 @@ spawnSyncAndExit(process.execPath, [ pending: [0], error: { code: 'probe_timeout', - message: 'Timed out after 200ms waiting for probes: probe-timeout.js:99', + message: `Timed out after ${timeout}ms waiting for probes: probe-timeout.js:99`, }, }], }); diff --git a/test/parallel/test-eslint-iterator-result-done-first.js b/test/parallel/test-eslint-iterator-result-done-first.js new file mode 100644 index 00000000000000..f65f6bd72ebe7a --- /dev/null +++ b/test/parallel/test-eslint-iterator-result-done-first.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const { RuleTester } = require('../../tools/eslint/node_modules/eslint'); +const rule = require('../../tools/eslint-rules/iterator-result-done-first'); + +const message = 'Iterator result objects should place `done` before `value`.'; + +new RuleTester().run('iterator-result-done-first', rule, { + valid: [ + 'function next() { return { done: true, value: undefined }; }', + 'function next() { return { __proto__: null, done: false, value: chunk }; }', + 'function next() { return { done, value }; }', + 'function next() { return { value }; }', + 'function next() { return { done }; }', + 'function next() { return { value: 1, other: 2 }; }', + 'function next() { return { [value]: 1, done: true }; }', + 'function next() { return { value: 1, [done]: true }; }', + 'function next() { return { "done": true, "value": undefined }; }', + 'function next() { return { ["done"]: true, ["value"]: undefined }; }', + ], + invalid: [ + { + code: 'function next() { return { value: undefined, done: true }; }', + errors: [{ message }], + output: 'function next() { return { done: true, value: undefined }; }', + }, + { + code: 'function next() { return { __proto__: null, value: chunk, done: false }; }', + errors: [{ message }], + output: 'function next() { return { __proto__: null, done: false, value: chunk }; }', + }, + { + code: 'function next() { return { value, done }; }', + errors: [{ message }], + output: 'function next() { return { done, value }; }', + }, + { + code: 'function next() { return { "value": undefined, "done": true }; }', + errors: [{ message }], + output: 'function next() { return { "done": true, "value": undefined }; }', + }, + { + code: 'function next() { return { ["value"]: undefined, ["done"]: true }; }', + errors: [{ message }], + output: 'function next() { return { ["done"]: true, ["value"]: undefined }; }', + }, + { + code: 'function next() { return { value: result, extra: true, done: false }; }', + errors: [{ message }], + output: 'function next() { return { done: false, extra: true, value: result }; }', + }, + ], +}); diff --git a/test/parallel/test-eslint-prefer-abort-signal-abort.js b/test/parallel/test-eslint-prefer-abort-signal-abort.js new file mode 100644 index 00000000000000..954d7df83dab02 --- /dev/null +++ b/test/parallel/test-eslint-prefer-abort-signal-abort.js @@ -0,0 +1,118 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/prefer-abort-signal-abort'); + +const message = 'Use AbortSignal.abort() instead of creating and aborting an AbortController.'; + +new RuleTester().run('prefer-abort-signal-abort', rule, { + valid: [ + 'const signal = AbortSignal.abort();', + ` + const controller = new AbortController(); + controller.abort(); + controller.abort(); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + controller.abort(); + console.log(controller); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + // This comment should not be removed. + controller.abort(); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + setImmediate(() => controller.abort()); + fn(controller.signal); + `, + ` + const controller = new AbortController(); + controller.abort('reason', 'extra'); + fn(controller.signal); + `, + ], + invalid: [ + { + code: ` + const controller = new AbortController(); + controller.abort(); + fn(controller.signal); + `, + errors: [{ message }], + output: ` + fn(AbortSignal.abort()); + `, + }, + { + code: ` + const abortController = new AbortController(); + abortController.abort(new Error('aborted')); + fn({ signal: abortController.signal }); + `, + errors: [{ message }], + output: ` + fn({ signal: AbortSignal.abort(new Error('aborted')) }); + `, + }, + { + code: ` + { + const ac = new AbortController(); + ac.abort(); + await wait({ signal: ac.signal }); + } + `, + errors: [{ message }], + output: ` + { + await wait({ signal: AbortSignal.abort() }); + } + `, + }, + { + code: ` + { + const controller = new AbortController(); + controller.abort(); + fn(controller.signal, controller.signal); + } + `, + errors: [{ message }], + output: ` + { + const controller = AbortSignal.abort(); + fn(controller, controller); + } + `, + }, + { + code: ` + { + const controller = new AbortController(); + controller.abort("reason"); + fn(controller.signal, controller.signal); + } + `, + errors: [{ message }], + output: ` + { + const controller = AbortSignal.abort("reason"); + fn(controller, controller); + } + `, + }, + ] +}); 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-fs-readfile-utf8-fast-path.js b/test/parallel/test-fs-readfile-utf8-fast-path.js new file mode 100644 index 00000000000000..18d0d884dfa455 --- /dev/null +++ b/test/parallel/test-fs-readfile-utf8-fast-path.js @@ -0,0 +1,103 @@ +'use strict'; + +require('../common'); +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +function writeFile(name, buf) { + const p = path.join(tmpdir.path, name); + fs.writeFileSync(p, buf); + return p; +} + +function expectMatches(filePath, rawBuf) { + assert.strictEqual( + fs.readFileSync(filePath, 'utf8'), + rawBuf.toString('utf8'), + ); +} + +describe('fs.readFileSync utf8 simdutf dispatch', () => { + it('empty file', () => { + const p = writeFile('empty.txt', Buffer.alloc(0)); + assert.strictEqual(fs.readFileSync(p, 'utf8'), ''); + }); + + it('ascii small', () => { + const buf = Buffer.from('hello'); + expectMatches(writeFile('tiny-ascii.txt', buf), buf); + }); + + it('ascii 20KB', () => { + const buf = Buffer.alloc(20 * 1024, 0x41); + expectMatches(writeFile('medium-ascii.txt', buf), buf); + }); + + it('ascii 1MB', () => { + const buf = Buffer.alloc(1024 * 1024, 0x61); + expectMatches(writeFile('large-ascii.txt', buf), buf); + }); + + it('fd input', () => { + const buf = Buffer.alloc(50 * 1024, 0x62); + const p = writeFile('fd-ascii.txt', buf); + const fd = fs.openSync(p, 'r'); + try { + assert.strictEqual(fs.readFileSync(fd, 'utf8'), buf.toString('utf8')); + } finally { + fs.closeSync(fd); + } + }); + + it('multibyte UTF-8', () => { + const buf = Buffer.from('中文测试 — café — 🚀'.repeat(500), 'utf8'); + expectMatches(writeFile('multibyte.txt', buf), buf); + }); + + it('latin1-fits utf8', () => { + const buf = Buffer.from('naïve café résumé — niño Köln '.repeat(500), 'utf8'); + expectMatches(writeFile('latin1-fits.txt', buf), buf); + }); + + it('invalid: lone continuation byte', () => { + const buf = Buffer.from([0x68, 0x69, 0x80, 0x21]); + expectMatches(writeFile('invalid-cont.txt', buf), buf); + }); + + it('invalid: overlong', () => { + const buf = Buffer.from([0x41, 0xC0, 0xAF, 0x42]); + expectMatches(writeFile('invalid-overlong.txt', buf), buf); + }); + + it('invalid: surrogate', () => { + const buf = Buffer.from([0x41, 0xED, 0xA0, 0x80, 0x42]); + expectMatches(writeFile('invalid-surrogate.txt', buf), buf); + }); + + it('latin1 boundary U+00FF', () => { + const buf = Buffer.from('ÿ'.repeat(2048), 'utf8'); + expectMatches(writeFile('latin1-boundary.txt', buf), buf); + }); + + it('above latin1 U+0100', () => { + const buf = Buffer.from('ĀāĂ'.repeat(1024), 'utf8'); + expectMatches(writeFile('above-latin1.txt', buf), buf); + }); + + it('single codepoint each UTF-8 length', () => { + for (const cp of [0x41, 0x00E9, 0x4E2D, 0x1F600]) { + const buf = Buffer.from(String.fromCodePoint(cp), 'utf8'); + expectMatches(writeFile(`single-cp-${cp.toString(16)}.txt`, buf), buf); + } + }); + + it('truncated multibyte at EOF', () => { + const buf = Buffer.from([0x41, 0xE4, 0xB8]); + expectMatches(writeFile('truncated-multibyte.txt', buf), buf); + }); +}); diff --git a/test/parallel/test-http-header-value-relaxed.js b/test/parallel/test-http-header-value-relaxed.js new file mode 100644 index 00000000000000..d002b808963771 --- /dev/null +++ b/test/parallel/test-http-header-value-relaxed.js @@ -0,0 +1,429 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const net = require('net'); +const { duplexPair } = require('stream'); +const { HTTPParser } = require('_http_common'); +// llhttp_set_lenient_header_value_relaxed() was added in llhttp 9.4.0. +// On shared-library builds using an older system llhttp the constant is +// exported as 0, so inbound-parsing tests must be skipped there. +const kRelaxedInboundSupported = HTTPParser.kLenientHeaderValueRelaxed > 0; + +// Integration tests for relaxed header value validation. +// When httpValidation is 'relaxed' or 'insecure', outgoing headers with control +// characters (0x01-0x1f except HTAB, and DEL 0x7f) are allowed per Fetch spec. +// NUL (0x00), CR (0x0d), and LF (0x0a) are always rejected. +// httpValidation: 'relaxed' - only enables relaxed header value parsing, not +// other insecure lenient behaviours (e.g. duplicate Transfer-Encoding). +// httpValidation: 'insecure' - enables all lenient parsing (same as insecureHTTPParser). +// httpValidation and insecureHTTPParser are mutually exclusive. + +// Helper: create a request that won't actually connect (for setHeader tests) +function dummyRequest(opts) { + const req = http.request({ host: '127.0.0.1', port: 1, ...opts }); + req.on('error', () => {}); // Suppress connection errors + return req; +} + +// ============================================================================ +// Test 1: Client setHeader with control chars in strict mode (default) - throws +// ============================================================================ +{ + const req = dummyRequest(); + assert.throws(() => { + req.setHeader('X-Test', 'value\x01here'); + }, { code: 'ERR_INVALID_CHAR' }); + req.destroy(); +} + +// ============================================================================ +// Test 2: Client setHeader with control chars in relaxed mode - allowed +// ============================================================================ +{ + const req = dummyRequest({ httpValidation: 'relaxed' }); + // Should not throw - control chars allowed in relaxed mode + req.setHeader('X-Test', 'value\x01here'); + req.setHeader('X-Bel', 'ding\x07'); + req.setHeader('X-Esc', 'esc\x1b'); + req.setHeader('X-Del', 'del\x7f'); + req.destroy(); +} + +// ============================================================================ +// Test 3: NUL, CR, LF always rejected even in relaxed mode (client) +// ============================================================================ +{ + const req = dummyRequest({ httpValidation: 'relaxed' }); + assert.throws(() => { + req.setHeader('X-Test', 'value\x00here'); + }, { code: 'ERR_INVALID_CHAR' }); + assert.throws(() => { + req.setHeader('X-Test', 'value\rhere'); + }, { code: 'ERR_INVALID_CHAR' }); + assert.throws(() => { + req.setHeader('X-Test', 'value\nhere'); + }, { code: 'ERR_INVALID_CHAR' }); + req.destroy(); +} + +// ============================================================================ +// Test 4: Server response setHeader with control chars in relaxed mode +// ============================================================================ +{ + const server = http.createServer({ + httpValidation: 'relaxed', + }, common.mustCall((req, res) => { + // Should not throw - control chars allowed in relaxed mode + res.setHeader('X-Custom', 'value\x01here'); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + // Use a raw TCP connection to read the response headers directly, + // since http.get would fail to parse the control char in the header. + const client = net.connect(port, common.mustCall(() => { + client.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'); + })); + let data = ''; + client.on('data', (chunk) => { data += chunk; }); + client.on('end', common.mustCall(() => { + // eslint-disable-next-line no-control-regex + assert.match(data, /X-Custom: value\x01here/); + server.close(); + })); + })); +} + +// ============================================================================ +// Test 5: Server response NUL/CR/LF always rejected in relaxed mode +// ============================================================================ +{ + const server = http.createServer({ + httpValidation: 'relaxed', + }, common.mustCall((req, res) => { + assert.throws(() => { + res.setHeader('X-Test', 'value\x00here'); + }, { code: 'ERR_INVALID_CHAR' }); + assert.throws(() => { + res.setHeader('X-Test', 'value\rhere'); + }, { code: 'ERR_INVALID_CHAR' }); + assert.throws(() => { + res.setHeader('X-Test', 'value\nhere'); + }, { code: 'ERR_INVALID_CHAR' }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + server.close(); + })); + })); + })); +} + +// ============================================================================ +// Test 6: Server response strict mode (default) rejects control chars +// ============================================================================ +{ + const server = http.createServer(common.mustCall((req, res) => { + assert.throws(() => { + res.setHeader('X-Test', 'value\x01here'); + }, { code: 'ERR_INVALID_CHAR' }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + server.close(); + })); + })); + })); +} + +// ============================================================================ +// Test 7: appendHeader also respects relaxed mode +// ============================================================================ +{ + const req = dummyRequest({ httpValidation: 'relaxed' }); + // Should not throw in relaxed mode + req.appendHeader('X-Test', 'value\x01here'); + req.destroy(); +} + +// ============================================================================ +// Test 8: appendHeader strict mode rejects control chars +// ============================================================================ +{ + const req = dummyRequest(); + assert.throws(() => { + req.appendHeader('X-Test', 'value\x01here'); + }, { code: 'ERR_INVALID_CHAR' }); + req.destroy(); +} + +// ============================================================================ +// Test 9: Explicit insecureHTTPParser: false overrides global flag +// ============================================================================ +{ + const req = dummyRequest({ insecureHTTPParser: false }); + assert.throws(() => { + req.setHeader('X-Test', 'value\x01here'); + }, { code: 'ERR_INVALID_CHAR' }); + req.destroy(); +} + +// ============================================================================ +// Test 10: Inbound response header with control char accepted in lenient mode +// (exercises the new llhttp_set_lenient_header_value_relaxed path) +// Only runs on builds with llhttp >= 9.4 (kLenientHeaderValueRelaxed > 0). +// ============================================================================ +if (kRelaxedInboundSupported) { + const [clientSide, serverSide] = duplexPair(); + + const req = http.request({ + createConnection: common.mustCall(() => clientSide), + httpValidation: 'relaxed', + }, common.mustCall((res) => { + assert.strictEqual(res.headers['x-ctrl'], 'value\x01here'); + res.resume(); + res.on('end', common.mustCall()); + })); + req.end(); + + serverSide.resume(); + serverSide.end( + 'HTTP/1.1 200 OK\r\n' + + 'X-Ctrl: value\x01here\r\n' + + 'Content-Length: 0\r\n' + + '\r\n', + ); +} + +// Test 10b: Same inbound header without insecureHTTPParser — parser must error +{ + const [clientSide, serverSide] = duplexPair(); + + const req = http.request({ + createConnection: common.mustCall(() => clientSide), + }, common.mustNotCall()); + req.end(); + req.on('error', common.mustCall()); + + serverSide.resume(); + serverSide.end( + 'HTTP/1.1 200 OK\r\n' + + 'X-Ctrl: value\x01here\r\n' + + 'Content-Length: 0\r\n' + + '\r\n', + ); +} + +// ============================================================================ +// Test 11: httpValidation: 'insecure' outbound - same as insecureHTTPParser +// ============================================================================ +{ + const req = dummyRequest({ httpValidation: 'insecure' }); + // Should not throw - control chars allowed in insecure mode + req.setHeader('X-Test', 'value\x01here'); + req.setHeader('X-Bel', 'ding\x07'); + req.destroy(); +} + +// ============================================================================ +// Test 12: Mutual exclusion - client throws when both options are set +// ============================================================================ +{ + assert.throws(() => { + dummyRequest({ httpValidation: 'relaxed', insecureHTTPParser: true }); + }, { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// ============================================================================ +// Test 13: Mutual exclusion - server throws when both options are set +// ============================================================================ +{ + assert.throws(() => { + http.createServer({ httpValidation: 'relaxed', insecureHTTPParser: true }); + }, { code: 'ERR_INVALID_ARG_VALUE' }); +} + +// ============================================================================ +// Test 14: httpValidation: 'relaxed' accepts inbound REQUEST headers with +// control chars (server side - exercises kLenientHeaderValueRelaxed) +// Only runs on builds with llhttp >= 9.4 (kLenientHeaderValueRelaxed > 0). +// ============================================================================ +if (kRelaxedInboundSupported) { + const server = http.createServer({ + httpValidation: 'relaxed', + }, common.mustCall((req, res) => { + assert.strictEqual(req.headers['x-ctrl'], 'value\x01here'); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + // Use a raw TCP connection to send a request with a control char header. + const client = net.connect(port, common.mustCall(() => { + client.write('GET / HTTP/1.1\r\nHost: localhost\r\nX-Ctrl: value\x01here\r\nConnection: close\r\n\r\n'); + })); + let data = ''; + client.on('data', (chunk) => { data += chunk; }); + client.on('end', common.mustCall(() => { + assert.match(data, /^HTTP\/1\.1 200/); + server.close(); + })); + })); +} + +// ============================================================================ +// Test 15: httpValidation: 'relaxed' inbound REQUEST - strict mode (default) +// rejects request with control char in header value +// ============================================================================ +{ + const server = http.createServer( + common.mustNotCall(), + ); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = net.connect(port, common.mustCall(() => { + client.write('GET / HTTP/1.1\r\nHost: localhost\r\nX-Ctrl: value\x01here\r\nConnection: close\r\n\r\n'); + })); + let data = ''; + client.on('data', (chunk) => { data += chunk; }); + client.on('end', common.mustCall(() => { + // Server should respond with 400 Bad Request or close the connection + assert.match(data, /^HTTP\/1\.1 400|^$/); + server.close(); + })); + })); +} + +// ============================================================================ +// Test 16: httpValidation: 'relaxed' does NOT enable all insecure lenient +// flags - duplicate Transfer-Encoding is still rejected in relaxed +// mode but accepted in insecure mode. +// (kLenientTransferEncoding is only in kLenientAll, not kLenientHeaderValueRelaxed) +// ============================================================================ +{ + // A request where Transfer-Encoding: chunked appears twice (joined internally + // as "chunked, chunked"), which llhttp rejects without kLenientTransferEncoding. + const doubleTE = + 'GET / HTTP/1.1\r\n' + + 'Host: localhost\r\n' + + 'Connection: close\r\n' + + 'Transfer-Encoding: chunked\r\n' + + 'Transfer-Encoding: chunked\r\n' + + '\r\n' + + '0\r\n\r\n'; + + // With httpValidation: 'relaxed', duplicate T-E should be rejected (400). + { + const server = http.createServer({ + httpValidation: 'relaxed', + }, common.mustNotCall()); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = net.connect(port, common.mustCall(() => { + client.write(doubleTE); + })); + client.resume(); + client.on('close', common.mustCall(() => { + server.close(); + })); + })); + } + + // With httpValidation: 'insecure', duplicate T-E is accepted (kLenientAll + // includes kLenientTransferEncoding). + { + const server = http.createServer({ + httpValidation: 'insecure', + }, common.mustCall((req, res) => { + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = net.connect(port, common.mustCall(() => { + client.write(doubleTE); + })); + let data = ''; + client.on('data', (chunk) => { data += chunk; }); + client.on('end', common.mustCall(() => { + assert.match(data, /^HTTP\/1\.1 200/); + server.close(); + })); + })); + } +} + +// ============================================================================ +// Test 17: writeHead respects httpValidation: 'relaxed' +// (exercises the storeHeader/validateHeaderValue path, not setHeader) +// ============================================================================ +{ + const server = http.createServer({ + httpValidation: 'relaxed', + }, common.mustCall((req, res) => { + // writeHead calls _storeHeader which calls storeHeader/validateHeaderValue. + // With httpValidation: 'relaxed', control chars should be allowed. + res.writeHead(200, { 'X-Custom': 'value\x01here' }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = net.connect(port, common.mustCall(() => { + client.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n'); + })); + let data = ''; + client.on('data', (chunk) => { data += chunk; }); + client.on('end', common.mustCall(() => { + // eslint-disable-next-line no-control-regex + assert.match(data, /X-Custom: value\x01here/); + server.close(); + })); + })); +} + +// ============================================================================ +// Test 18: writeHead strict mode (default) rejects control chars +// ============================================================================ +{ + const server = http.createServer(common.mustCall((req, res) => { + assert.throws(() => { + res.writeHead(200, { 'X-Custom': 'value\x01here' }); + }, { code: 'ERR_INVALID_CHAR' }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + server.close(); + })); + })); + })); +} + +// ============================================================================ +// Test 19: httpValidation: 'strict' (explicit) rejects control chars even +// when insecureHTTPParser would otherwise be lenient +// ============================================================================ +{ + const req = dummyRequest({ httpValidation: 'strict' }); + // 'strict' must always reject control chars, regardless of any global setting + assert.throws(() => { + req.setHeader('X-Test', 'value\x01here'); + }, { code: 'ERR_INVALID_CHAR' }); + req.destroy(); +} diff --git a/test/parallel/test-http-invalidheaderfield2.js b/test/parallel/test-http-invalidheaderfield2.js index 1b4e9e6edb01f3..c40de68011a03b 100644 --- a/test/parallel/test-http-invalidheaderfield2.js +++ b/test/parallel/test-http-invalidheaderfield2.js @@ -59,30 +59,77 @@ const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('_http_common'); }); -// Good header field values +// ============================================================================ +// Strict header value validation (default) - per RFC 7230 +// Rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f) +// ============================================================================ + +// Good header field values in strict mode [ 'foo bar', - 'foo\tbar', + 'foo\tbar', // HTAB is allowed '0123456789ABCdef', '!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`', + '\x80\x81\xff', // obs-text (0x80-0xff) is allowed ].forEach(function(str) { assert.strictEqual( _checkInvalidHeaderChar(str), false, - `_checkInvalidHeaderChar(${inspect(str)}) unexpectedly failed`); + `_checkInvalidHeaderChar(${inspect(str)}) unexpectedly failed in strict mode`); }); -// Bad header field values +// Bad header field values in strict mode +// Control characters (except HTAB) and DEL are rejected [ - 'foo\rbar', - 'foo\nbar', - 'foo\r\nbar', - '中文呢', // unicode - '\x7FMe!', - 'Testing 123\x00', - 'foo\vbar', - 'Ding!\x07', + 'foo\x00bar', // NUL + 'foo\x01bar', // SOH + 'foo\rbar', // CR + 'foo\nbar', // LF + 'foo\r\nbar', // CRLF + 'foo\x7Fbar', // DEL + '中文呢', // unicode > 0xff ].forEach(function(str) { assert.strictEqual( _checkInvalidHeaderChar(str), true, - `_checkInvalidHeaderChar(${inspect(str)}) unexpectedly succeeded`); + `_checkInvalidHeaderChar(${inspect(str)}) unexpectedly succeeded in strict mode`); +}); + + +// ============================================================================ +// Lenient header value validation (with insecureHTTPParser) - per Fetch spec +// Only NUL (0x00), CR (0x0d), LF (0x0a), and chars > 0xff are rejected +// ============================================================================ + +// Good header field values in lenient mode +// CTL characters (except NUL, LF, CR) are valid per Fetch spec +[ + 'foo bar', + 'foo\tbar', + '0123456789ABCdef', + '!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`', + '\x01\x02\x03\x04\x05\x06\x07\x08', // 0x01-0x08 + 'foo\x0bbar', // VT (0x0b) + 'foo\x0cbar', // FF (0x0c) + '\x0e\x0f\x10\x11\x12\x13\x14\x15', // 0x0e-0x15 + '\x16\x17\x18\x19\x1a\x1b\x1c\x1d', // 0x16-0x1d + '\x1e\x1f', // 0x1e-0x1f + '\x7FMe!', // DEL (0x7f) + '\x80\x81\xff', // obs-text (0x80-0xff) +].forEach(function(str) { + assert.strictEqual( + _checkInvalidHeaderChar(str, true), false, + `_checkInvalidHeaderChar(${inspect(str)}, true) unexpectedly failed in lenient mode`); +}); + +// Bad header field values in lenient mode +// Only NUL (0x00), LF (0x0a), CR (0x0d), and characters > 0xff are invalid +[ + 'foo\rbar', // CR (0x0d) + 'foo\nbar', // LF (0x0a) + 'foo\r\nbar', // CRLF + '中文呢', // unicode > 0xff + 'Testing 123\x00', // NUL (0x00) +].forEach(function(str) { + assert.strictEqual( + _checkInvalidHeaderChar(str, true), true, + `_checkInvalidHeaderChar(${inspect(str)}, true) unexpectedly succeeded in lenient mode`); }); diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js index ff98c23e864f74..7034f98abb6836 100644 --- a/test/parallel/test-http2-client-destroy.js +++ b/test/parallel/test-http2-client-destroy.js @@ -81,7 +81,19 @@ const { listenerCount } = require('events'); assert.throws(() => client.ping(), sessionError); assert.throws(() => client.settings({}), sessionError); assert.throws(() => client.goaway(), sessionError); - assert.throws(() => client.request(), sessionError); + + const pendingReq = client.request(); + pendingReq.on('response', common.mustNotCall()); + pendingReq.on('error', common.expectsError(sessionError)); + pendingReq.on('close', common.mustCall()); + + client.on('close', common.mustCall(() => { + const postCloseReq = client.request(); + postCloseReq.on('response', common.mustNotCall()); + postCloseReq.on('error', common.expectsError(sessionError)); + postCloseReq.on('close', common.mustCall()); + })); + client.close(); // Should be a non-op at this point // Wait for setImmediate call from destroy() to complete @@ -92,7 +104,6 @@ const { listenerCount } = require('events'); assert.throws(() => client.ping(), sessionError); assert.throws(() => client.settings({}), sessionError); assert.throws(() => client.goaway(), sessionError); - assert.throws(() => client.request(), sessionError); client.close(); // Should be a non-op at this point })); diff --git a/test/parallel/test-http2-client-session-close-before-stream-close.js b/test/parallel/test-http2-client-session-close-before-stream-close.js new file mode 100644 index 00000000000000..5ebcd8522dfb41 --- /dev/null +++ b/test/parallel/test-http2-client-session-close-before-stream-close.js @@ -0,0 +1,59 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +let serverSocket; + +server.on('connection', common.mustCall((socket) => { + serverSocket = socket; + socket.on('error', () => {}); +})); + +server.on('sessionError', () => {}); +server.on('stream', common.mustCall((stream, headers) => { + if (headers[':path'] === '/close') { + stream.respond({ ':status': 200 }); + stream.write('partial', common.mustCall(() => { + setImmediate(() => serverSocket.destroy()); + })); + return; + } + + stream.respond({ ':status': 200 }); + stream.end('ok'); +})); + +server.listen(0, common.mustCall(() => { + const session = http2.connect(`http://localhost:${server.address().port}`); + let cachedSession = session; + + session.on('error', () => {}); + session.on('close', common.mustCall(() => { + cachedSession = undefined; + server.close(); + })); + + const req = session.request({ ':path': '/close' }); + req.on('response', common.mustCall()); + req.on('error', () => {}); + req.on('close', common.mustCall(() => { + // This must not throw synchronously even though the session is no longer + // usable. Depending on teardown timing, the returned stream may report a + // closed session before the destroy state is fully observable here. + const req2 = session.request({ ':path': '/again' }); + + req2.on('error', common.mustCall((err) => { + assert.ok( + err.code === 'ERR_HTTP2_INVALID_SESSION' || + err.code === 'ERR_HTTP2_GOAWAY_SESSION'); + assert.strictEqual(cachedSession, undefined); + })); + })); + req.resume(); +})); diff --git a/test/parallel/test-internal-util-classwrapper.js b/test/parallel/test-internal-util-classwrapper.js deleted file mode 100644 index 52b3c2b0a9b50a..00000000000000 --- a/test/parallel/test-internal-util-classwrapper.js +++ /dev/null @@ -1,31 +0,0 @@ -// Flags: --expose-internals -'use strict'; - -require('../common'); -const assert = require('assert'); -const util = require('internal/util'); - -const createClassWrapper = util.createClassWrapper; - -class A { - constructor(a, b, c) { - this.a = a; - this.b = b; - this.c = c; - } -} - -const B = createClassWrapper(A); - -assert.strictEqual(typeof B, 'function'); -assert(B(1, 2, 3) instanceof B); -assert(B(1, 2, 3) instanceof A); -assert(new B(1, 2, 3) instanceof B); -assert(new B(1, 2, 3) instanceof A); -assert.strictEqual(B.name, A.name); -assert.strictEqual(B.length, A.length); - -const b = new B(1, 2, 3); -assert.strictEqual(b.a, 1); -assert.strictEqual(b.b, 2); -assert.strictEqual(b.c, 3); diff --git a/test/parallel/test-net-pipe-connect-errors.js b/test/parallel/test-net-pipe-connect-errors.js index fec4259b348dfb..33dc6a8a23b5e1 100644 --- a/test/parallel/test-net-pipe-connect-errors.js +++ b/test/parallel/test-net-pipe-connect-errors.js @@ -24,6 +24,7 @@ const common = require('../common'); const fixtures = require('../common/fixtures'); const fs = require('fs'); const net = require('net'); +const path = require('path'); const assert = require('assert'); // Test if ENOTSOCK is fired when trying to connect to a file which is not @@ -38,11 +39,11 @@ if (common.isWindows) { } else { const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); - // Keep the file name very short so that we don't exceed the 108 char limit - // on CI for a POSIX socket. Even though this isn't actually a socket file, - // the error will be different from the one we are expecting if we exceed the - // limit. - emptyTxt = `${tmpdir.path}0.txt`; + // Use a short relative path so that we don't exceed the 108 byte limit for + // Unix socket paths in long or multibyte CI workspaces. Even though this + // isn't actually a socket file, the error will be different from the one we + // are expecting if the path is too long. + emptyTxt = path.join(path.relative(process.cwd(), tmpdir.path), '0.txt'); function cleanup() { try { 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); - }); - } -}); diff --git a/test/parallel/test-permission-drop-child-process.js b/test/parallel/test-permission-drop-child-process.js new file mode 100644 index 00000000000000..6c6fe2065d11d7 --- /dev/null +++ b/test/parallel/test-permission-drop-child-process.js @@ -0,0 +1,38 @@ +// Flags: --permission --allow-child-process --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); +const childProcess = require('child_process'); + +{ + assert.ok(process.permission.has('child')); + const { status } = childProcess.spawnSync(process.execPath, ['--version']); + assert.strictEqual(status, 0); +} + +process.permission.drop('child'); +{ + assert.ok(!process.permission.has('child')); + assert.throws(() => { + childProcess.spawnSync(process.execPath, ['--version']); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'ChildProcess', + })); + assert.throws(() => { + childProcess.execSync(`"${process.execPath}" --version`); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'ChildProcess', + })); +} + +process.permission.drop('child'); +assert.ok(!process.permission.has('child')); diff --git a/test/parallel/test-permission-drop-diagnostics-channel.js b/test/parallel/test-permission-drop-diagnostics-channel.js new file mode 100644 index 00000000000000..e0006061e5a047 --- /dev/null +++ b/test/parallel/test-permission-drop-diagnostics-channel.js @@ -0,0 +1,34 @@ +// Flags: --permission --allow-fs-read=* --allow-child-process +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); +const dc = require('node:diagnostics_channel'); + +const fsMessages = []; +dc.subscribe('node:permission-model:fs', (msg) => { + fsMessages.push(msg); +}); + +const childMessages = []; +dc.subscribe('node:permission-model:child', (msg) => { + childMessages.push(msg); +}); + +process.permission.drop('fs.read', '/tmp/test-drop'); + +assert.ok(fsMessages.length > 0, 'Expected at least one fs drop message'); +assert.strictEqual(fsMessages[0].permission, 'FileSystemRead'); +assert.strictEqual(fsMessages[0].drop, true); + +process.permission.drop('child'); + +assert.ok(childMessages.length > 0, 'Expected at least one child drop message'); +assert.strictEqual(childMessages[0].permission, 'ChildProcess'); +assert.strictEqual(childMessages[0].drop, true); diff --git a/test/parallel/test-permission-drop-errors.js b/test/parallel/test-permission-drop-errors.js new file mode 100644 index 00000000000000..ae3a9b2fcc9ed3 --- /dev/null +++ b/test/parallel/test-permission-drop-errors.js @@ -0,0 +1,45 @@ +// Flags: --permission --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); + +{ + assert.throws(() => { + process.permission.drop(null); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "scope" argument must be of type string. Received null', + })); + + assert.throws(() => { + process.permission.drop(123); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + })); +} + +{ + assert.throws(() => { + process.permission.drop('fs.read', {}); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + })); + + assert.throws(() => { + process.permission.drop('fs.read', 123); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + })); +} + +{ + process.permission.drop('invalid-scope'); + process.permission.drop('invalid-scope', '/tmp'); +} diff --git a/test/parallel/test-permission-drop-ffi.js b/test/parallel/test-permission-drop-ffi.js new file mode 100644 index 00000000000000..f0d4093b6d8467 --- /dev/null +++ b/test/parallel/test-permission-drop-ffi.js @@ -0,0 +1,40 @@ +// Flags: --permission --allow-ffi --experimental-ffi --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const { fixtureSymbols, libraryPath } = require('../ffi/ffi-test-common'); + +common.skipIfFFIMissing(); + +const ffi = require('node:ffi'); +const assert = require('assert'); + +function openLibrary() { + const { lib } = ffi.dlopen(libraryPath, { + add_i32: fixtureSymbols.add_i32, + allocate_memory: fixtureSymbols.allocate_memory, + deallocate_memory: fixtureSymbols.deallocate_memory, + }); + lib.close(); +} + + +{ + assert.ok(process.permission.has('ffi')); +} + +{ + // shouldNotThrow + openLibrary(); +} + +{ + process.permission.drop('ffi'); + assert.ok(!process.permission.has('ffi')); + assert.throws(() => { + openLibrary(); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FFI', + })); +} diff --git a/test/parallel/test-permission-drop-fs-all.js b/test/parallel/test-permission-drop-fs-all.js new file mode 100644 index 00000000000000..632e1b270126c4 --- /dev/null +++ b/test/parallel/test-permission-drop-fs-all.js @@ -0,0 +1,38 @@ +// Flags: --permission --allow-fs-read=* --allow-fs-write=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); +const fs = require('fs'); + +{ + assert.ok(process.permission.has('fs.read')); + assert.ok(process.permission.has('fs.write')); +} + +process.permission.drop('fs.read'); +{ + assert.ok(!process.permission.has('fs.read')); + assert.ok(!process.permission.has('fs.read', __filename)); + assert.throws(() => { + fs.readFileSync(__filename); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemRead', + })); +} + +{ + assert.ok(process.permission.has('fs.write')); +} + +process.permission.drop('fs.write'); +{ + assert.ok(!process.permission.has('fs.write')); +} diff --git a/test/parallel/test-permission-drop-fs-granted-path.js b/test/parallel/test-permission-drop-fs-granted-path.js new file mode 100644 index 00000000000000..a011944cd0d970 --- /dev/null +++ b/test/parallel/test-permission-drop-fs-granted-path.js @@ -0,0 +1,158 @@ +'use strict'; + +require('../common'); + +const { spawnSync } = require('child_process'); +const assert = require('assert'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + process.exit(0); +} +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); +const path = require('path'); + +tmpdir.refresh(); + +const dir = path.join(tmpdir.path, 'granted-dir'); +fs.mkdirSync(dir); +fs.writeFileSync(path.join(dir, 'item1.txt'), 'aaa'); +fs.writeFileSync(path.join(dir, 'item2.txt'), 'bbb'); + +// Grant a directory, try to drop a file inside it - should be a no-op +{ + const child = spawnSync(process.execPath, [ + '--permission', + `--allow-fs-read=${dir}`, + '-e', + ` + const assert = require('assert'); + const fs = require('fs'); + const dir = ${JSON.stringify(dir)}; + + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + + process.permission.drop('fs.read', dir + '/item1.txt'); + + // Still readable — drop of a file inside a granted dir is a no-op + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + assert.ok(process.permission.has('fs.read', dir + '/item2.txt')); + `, + ]); + if (child.status !== 0) { + console.error('Case 1 stderr:', child.stderr?.toString()); + } + assert.strictEqual(child.status, 0); +} + +// Grant a directory, drop the same directory - should revoke all access +{ + const child = spawnSync(process.execPath, [ + '--permission', + `--allow-fs-read=${dir}`, + '-e', + ` + const assert = require('assert'); + const dir = ${JSON.stringify(dir)}; + + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + + process.permission.drop('fs.read', dir); + + assert.ok(!process.permission.has('fs.read', dir + '/item1.txt')); + assert.ok(!process.permission.has('fs.read', dir + '/item2.txt')); + `, + ]); + if (child.status !== 0) { + console.error('Case 2 stderr:', child.stderr?.toString()); + } + assert.strictEqual(child.status, 0); +} + +// Grant two directories, drop one - the other remains accessible +{ + const dir2 = path.join(tmpdir.path, 'other-dir'); + fs.mkdirSync(dir2); + fs.writeFileSync(path.join(dir2, 'other.txt'), 'ccc'); + + const child = spawnSync(process.execPath, [ + '--permission', + `--allow-fs-read=${dir}`, + `--allow-fs-read=${dir2}`, + '-e', + ` + const assert = require('assert'); + const fs = require('fs'); + const dir = ${JSON.stringify(dir)}; + const dir2 = ${JSON.stringify(dir2)}; + + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + assert.ok(process.permission.has('fs.read', dir2 + '/other.txt')); + + process.permission.drop('fs.read', dir); + + assert.ok(!process.permission.has('fs.read', dir + '/item1.txt')); + assert.ok(process.permission.has('fs.read', dir2 + '/other.txt')); + assert.strictEqual( + fs.readFileSync(dir2 + '/other.txt', 'utf8'), + 'ccc' + ); + `, + ]); + if (child.status !== 0) { + console.error('Case 3 stderr:', child.stderr?.toString()); + } + assert.strictEqual(child.status, 0); +} + +// Grant a directory and a file inside it separately, drop the file +// the directory grant still covers everything including that file +{ + const child = spawnSync(process.execPath, [ + '--permission', + `--allow-fs-read=${dir}`, + `--allow-fs-read=${path.join(dir, 'item1.txt')}`, + '-e', + ` + const assert = require('assert'); + const path = require('path'); + const dir = ${JSON.stringify(dir)}; + + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + + process.permission.drop('fs.read', path.join(dir, 'item1.txt')); + + // Still readable because the directory grant covers it + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + `, + ]); + if (child.status !== 0) { + console.error('Case 4 stderr:', child.stderr?.toString()); + } + assert.strictEqual(child.status, 0); +} + +// Drop entire scope without reference - revokes everything +{ + const child = spawnSync(process.execPath, [ + '--permission', + `--allow-fs-read=${dir}`, + '-e', + ` + const assert = require('assert'); + const dir = ${JSON.stringify(dir)}; + + assert.ok(process.permission.has('fs.read', dir + '/item1.txt')); + + process.permission.drop('fs.read'); + + assert.ok(!process.permission.has('fs.read', dir + '/item1.txt')); + assert.ok(!process.permission.has('fs.read')); + `, + ]); + if (child.status !== 0) { + console.error('Case 5 stderr:', child.stderr?.toString()); + } + assert.strictEqual(child.status, 0); +} diff --git a/test/parallel/test-permission-drop-fs-read.js b/test/parallel/test-permission-drop-fs-read.js new file mode 100644 index 00000000000000..1a73de8ec2da7a --- /dev/null +++ b/test/parallel/test-permission-drop-fs-read.js @@ -0,0 +1,40 @@ +// Flags: --permission --allow-fs-read=* --allow-fs-write=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); +const fs = require('fs'); + +{ + assert.ok(process.permission.has('fs.read')); + assert.ok(process.permission.has('fs.read', __filename)); +} + +// Specific path drop is a no-op when * was granted +process.permission.drop('fs.read', '/tmp/some-path'); +{ + assert.ok(process.permission.has('fs.read')); + assert.ok(process.permission.has('fs.read', __filename)); +} + +process.permission.drop('fs.read'); +{ + assert.ok(!process.permission.has('fs.read')); + assert.ok(!process.permission.has('fs.read', __filename)); + assert.throws(() => { + fs.readFileSync(__filename, 'utf8'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemRead', + })); +} + +{ + assert.ok(process.permission.has('fs.write')); +} diff --git a/test/parallel/test-permission-drop-fs-scope.js b/test/parallel/test-permission-drop-fs-scope.js new file mode 100644 index 00000000000000..f6948bf04ffa81 --- /dev/null +++ b/test/parallel/test-permission-drop-fs-scope.js @@ -0,0 +1,37 @@ +// Flags: --permission --allow-fs-read=* --allow-fs-write=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); +const fs = require('fs'); + +{ + assert.ok(process.permission.has('fs.read')); + assert.ok(process.permission.has('fs.write')); +} + +// Specific path drop is a no-op when * was granted +process.permission.drop('fs', '/tmp/some-path'); +{ + assert.ok(process.permission.has('fs.read')); + assert.ok(process.permission.has('fs.write')); +} + +process.permission.drop('fs'); +{ + assert.ok(!process.permission.has('fs.read')); + assert.ok(!process.permission.has('fs.write')); + + assert.throws(() => { + fs.readFileSync(__filename); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemRead', + })); +} diff --git a/test/parallel/test-permission-drop-fs-specific-path.js b/test/parallel/test-permission-drop-fs-specific-path.js new file mode 100644 index 00000000000000..0c46ee505ff3a5 --- /dev/null +++ b/test/parallel/test-permission-drop-fs-specific-path.js @@ -0,0 +1,60 @@ +'use strict'; + +require('../common'); + +const { spawnSync } = require('child_process'); +const assert = require('assert'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + process.exit(0); +} +const tmpdir = require('../common/tmpdir'); +const fs = require('fs'); +const path = require('path'); + +tmpdir.refresh(); + +const dirA = path.join(tmpdir.path, 'dir-a'); +const dirB = path.join(tmpdir.path, 'dir-b'); +fs.mkdirSync(dirA); +fs.mkdirSync(dirB); +fs.writeFileSync(path.join(dirA, 'a.txt'), 'aaa'); +fs.writeFileSync(path.join(dirB, 'b.txt'), 'bbb'); + +const child = spawnSync(process.execPath, [ + '--permission', + `--allow-fs-read=${dirA}`, + `--allow-fs-read=${dirB}`, + '-e', + ` + const assert = require('assert'); + const fs = require('fs'); + const path = require('path'); + + const dirA = ${JSON.stringify(dirA)}; + const dirB = ${JSON.stringify(dirB)}; + + assert.ok(process.permission.has('fs.read', path.join(dirA, 'a.txt'))); + assert.ok(process.permission.has('fs.read', path.join(dirB, 'b.txt'))); + + process.permission.drop('fs.read', dirA); + + assert.ok(!process.permission.has('fs.read', path.join(dirA, 'a.txt'))); + assert.throws(() => { + fs.readFileSync(path.join(dirA, 'a.txt')); + }, { code: 'ERR_ACCESS_DENIED' }); + + assert.ok(process.permission.has('fs.read', path.join(dirB, 'b.txt'))); + assert.strictEqual( + fs.readFileSync(path.join(dirB, 'b.txt'), 'utf8'), + 'bbb' + ); + `, +]); + +if (child.status !== 0) { + console.error('stdout:', child.stdout?.toString()); + console.error('stderr:', child.stderr?.toString()); +} +assert.strictEqual(child.status, 0); diff --git a/test/parallel/test-permission-drop-fs-write.js b/test/parallel/test-permission-drop-fs-write.js new file mode 100644 index 00000000000000..64cc791ebaff4c --- /dev/null +++ b/test/parallel/test-permission-drop-fs-write.js @@ -0,0 +1,31 @@ +// Flags: --permission --allow-fs-read=* --allow-fs-write=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); + +{ + assert.ok(process.permission.has('fs.write')); +} + +// Specific path drop is a no-op when * was granted +process.permission.drop('fs.write', '/tmp/some-path'); +{ + assert.ok(process.permission.has('fs.write')); +} + +process.permission.drop('fs.write'); +{ + assert.ok(!process.permission.has('fs.write')); +} + +{ + assert.ok(process.permission.has('fs.read')); + assert.ok(process.permission.has('fs.read', __filename)); +} diff --git a/test/parallel/test-permission-drop-net.js b/test/parallel/test-permission-drop-net.js new file mode 100644 index 00000000000000..81643dd61b450a --- /dev/null +++ b/test/parallel/test-permission-drop-net.js @@ -0,0 +1,20 @@ +// Flags: --permission --allow-net --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); + +{ + assert.ok(process.permission.has('net')); +} + +process.permission.drop('net'); +{ + assert.ok(!process.permission.has('net')); +} diff --git a/test/parallel/test-permission-drop-not-enabled.js b/test/parallel/test-permission-drop-not-enabled.js new file mode 100644 index 00000000000000..51992dff649858 --- /dev/null +++ b/test/parallel/test-permission-drop-not-enabled.js @@ -0,0 +1,14 @@ +'use strict'; + +require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + process.exit(0); +} + +const assert = require('assert'); + +{ + assert.strictEqual(process.permission, undefined); +} diff --git a/test/parallel/test-permission-drop-worker.js b/test/parallel/test-permission-drop-worker.js new file mode 100644 index 00000000000000..d800255e13e5c0 --- /dev/null +++ b/test/parallel/test-permission-drop-worker.js @@ -0,0 +1,26 @@ +// Flags: --permission --allow-worker --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const { isMainThread, Worker } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); + +{ + assert.ok(process.permission.has('worker')); +} + +process.permission.drop('worker'); +{ + assert.ok(!process.permission.has('worker')); + assert.throws(() => { + new Worker('console.log("hello")', { eval: true }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'WorkerThreads', + })); +} diff --git a/test/parallel/test-permission-net-quic.mjs b/test/parallel/test-permission-net-quic.mjs index 1855dcca0f40e6..0ad5639432f7b9 100644 --- a/test/parallel/test-permission-net-quic.mjs +++ b/test/parallel/test-permission-net-quic.mjs @@ -19,7 +19,7 @@ const cert = fixtures.readKey('agent1-cert.pem'); // Test: connect() should reject with ERR_ACCESS_DENIED { await assert.rejects( - connect('127.0.0.1:12345', { alpn: 'h3' }), + connect('127.0.0.1:12345', { alpn: 'h3', verifyPeer: 'manual' }), { code: 'ERR_ACCESS_DENIED', permission: 'Net', 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-quic-address-validation.mjs b/test/parallel/test-quic-address-validation.mjs index 7f6c57dfee8f52..6be0681ff49902 100644 --- a/test/parallel/test-quic-address-validation.mjs +++ b/test/parallel/test-quic-address-validation.mjs @@ -37,6 +37,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', servername: 'localhost', }); diff --git a/test/parallel/test-quic-alpn-h3.mjs b/test/parallel/test-quic-alpn-h3.mjs index 431175a743d502..e9adca59d1aeca 100644 --- a/test/parallel/test-quic-alpn-h3.mjs +++ b/test/parallel/test-quic-alpn-h3.mjs @@ -35,6 +35,7 @@ notStrictEqual(serverEndpoint.address, undefined); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); async function checkClient() { diff --git a/test/parallel/test-quic-alpn-mismatch.mjs b/test/parallel/test-quic-alpn-mismatch.mjs index 14c9dfe39bf2f3..64d8e1f0006380 100644 --- a/test/parallel/test-quic-alpn-mismatch.mjs +++ b/test/parallel/test-quic-alpn-mismatch.mjs @@ -6,9 +6,7 @@ // // The QUIC transport error code for a CRYPTO_ERROR carrying a TLS alert is // 0x100 | . For `no_application_protocol` (alert 120 / 0x78) this -// is 0x178 == 376. ERR_QUIC_TRANSPORT_ERROR formats the wire code into its -// message as a bigint, so we match `376n` to assert the specific alert was -// sent rather than some other handshake failure. +// is 0x178 == 376. import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; @@ -23,12 +21,14 @@ const { listen, connect } = await import('../common/quic.mjs'); const expected = { code: 'ERR_QUIC_TRANSPORT_ERROR', - message: /\b376n\b/, + errorCode: 376n, + message: /no application protocol/ }; const onerror = mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); - match(err.message, /\b376n\b/); + strictEqual(err.errorCode, 376n); + match(err.message, /no application protocol/); }, 2); const transportParams = { maxIdleTimeout: 1 }; diff --git a/test/parallel/test-quic-alpn.mjs b/test/parallel/test-quic-alpn.mjs index babb945d9d0c33..020fea3d308a86 100644 --- a/test/parallel/test-quic-alpn.mjs +++ b/test/parallel/test-quic-alpn.mjs @@ -41,6 +41,7 @@ notStrictEqual(serverEndpoint.address, undefined); const clientSession = await connect(serverEndpoint.address, { alpn: 'proto-b', servername: 'localhost', + verifyPeer: 'manual', }); await Promise.all([serverOpened.promise, checkSession(clientSession)]); diff --git a/test/parallel/test-quic-blocklist.mjs b/test/parallel/test-quic-blocklist.mjs new file mode 100644 index 00000000000000..5a3d4e98310f4b --- /dev/null +++ b/test/parallel/test-quic-blocklist.mjs @@ -0,0 +1,79 @@ +// Flags: --experimental-quic --no-warnings + +// Test: endpoint block list filtering. +// Deny mode: packets from blocked addresses are dropped. +// Allow mode: only packets from listed addresses are accepted. + +import { hasQuic, skip, mustCall, mustNotCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import net from 'node:net'; + +const { ok, rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Deny mode: block 127.0.0.1 --- +{ + const blockList = new net.BlockList(); + blockList.addAddress('127.0.0.1'); + + const serverEndpoint = await listen(mustNotCall(), { + endpoint: { + blockList, + blockListPolicy: 'deny', + }, + }); + + // Connecting from 127.0.0.1 should be silently dropped. + // The client will time out waiting for a response. + const clientSession = await connect(serverEndpoint.address, { + handshakeTimeout: 500, + verifyPeer: 'manual', + onerror: mustCall((err) => { + strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); + }), + }); + + // The session should fail — the server never sees the packet. + await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + + // Verify the stat counter. + ok(serverEndpoint.stats.packetsBlocked > 0n, + 'packetsBlocked should be non-zero'); + + await serverEndpoint.close(); +} + +// --- Allow mode: only allow 127.0.0.1 --- +{ + const allowList = new net.BlockList(); + allowList.addAddress('127.0.0.1'); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + await serverSession.opened; + serverSession.close(); + }), { + endpoint: { + blockList: allowList, + blockListPolicy: 'allow', + }, + }); + + // Connecting from 127.0.0.1 should succeed — it's in the allow list. + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + }); + await clientSession.opened; + await clientSession.close(); + + strictEqual(serverEndpoint.stats.packetsBlocked, 0n, + 'No packets should be blocked for allowed address'); + + await serverEndpoint.close(); +} 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-connection-limits.mjs b/test/parallel/test-quic-connection-limits.mjs index 2f41c388805dc4..7d2f4b07119f7b 100644 --- a/test/parallel/test-quic-connection-limits.mjs +++ b/test/parallel/test-quic-connection-limits.mjs @@ -49,6 +49,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { // First connection should succeed. const cs1 = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 2 }, }); await cs1.opened; @@ -56,6 +57,7 @@ await cs1.opened; // Second connection — server rejects with CONNECTION_REFUSED. const cs2 = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, onerror: mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); diff --git a/test/parallel/test-quic-datagram-drop-newest.mjs b/test/parallel/test-quic-datagram-drop-newest.mjs index 45f568af91687a..7225ea9cd08e0a 100644 --- a/test/parallel/test-quic-datagram-drop-newest.mjs +++ b/test/parallel/test-quic-datagram-drop-newest.mjs @@ -48,6 +48,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxDatagramFrameSize: 1200 }, datagramDropPolicy: 'drop-newest', ondatagramstatus: mustCall((_, status) => { diff --git a/test/parallel/test-quic-datagram-drop-oldest.mjs b/test/parallel/test-quic-datagram-drop-oldest.mjs index 1471323caf2e06..923e80bf73460a 100644 --- a/test/parallel/test-quic-datagram-drop-oldest.mjs +++ b/test/parallel/test-quic-datagram-drop-oldest.mjs @@ -48,6 +48,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxDatagramFrameSize: 1200 }, datagramDropPolicy: 'drop-oldest', ondatagramstatus: mustCall((_, status) => { diff --git a/test/parallel/test-quic-datagram-echo.mjs b/test/parallel/test-quic-datagram-echo.mjs index ad6a08b67443c8..a0870ba49ce77a 100644 --- a/test/parallel/test-quic-datagram-echo.mjs +++ b/test/parallel/test-quic-datagram-echo.mjs @@ -48,6 +48,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxDatagramFrameSize: 10 }, // Client receives datagram from server. ondatagram: mustCall(function(data) { diff --git a/test/parallel/test-quic-datagram-multiple.mjs b/test/parallel/test-quic-datagram-multiple.mjs index f8deef3ead2cd9..96e86bec8a99e7 100644 --- a/test/parallel/test-quic-datagram-multiple.mjs +++ b/test/parallel/test-quic-datagram-multiple.mjs @@ -56,6 +56,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxDatagramFrameSize: 1200 }, }); diff --git a/test/parallel/test-quic-datagram-status.mjs b/test/parallel/test-quic-datagram-status.mjs index b3391b7655ff23..98e6bf56cee986 100644 --- a/test/parallel/test-quic-datagram-status.mjs +++ b/test/parallel/test-quic-datagram-status.mjs @@ -45,6 +45,7 @@ let statusValue; const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxDatagramFrameSize: 1200 }, ondatagramstatus: mustCall((id, status) => { strictEqual(typeof id, 'bigint'); diff --git a/test/parallel/test-quic-datagram.mjs b/test/parallel/test-quic-datagram.mjs index f35966b19be3ec..acd2d88356239a 100644 --- a/test/parallel/test-quic-datagram.mjs +++ b/test/parallel/test-quic-datagram.mjs @@ -45,6 +45,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxDatagramFrameSize: 1200 }, }); 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-enable-early-data.mjs b/test/parallel/test-quic-enable-early-data.mjs index 90524aaf19d1da..0c868025240fbf 100644 --- a/test/parallel/test-quic-enable-early-data.mjs +++ b/test/parallel/test-quic-enable-early-data.mjs @@ -45,6 +45,7 @@ const serverEndpoint = await listen(mustCall((serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', servername: 'localhost', enableEarlyData: false, }); diff --git a/test/parallel/test-quic-endpoint-bind.mjs b/test/parallel/test-quic-endpoint-bind.mjs index 0f9c359075db75..dc91ccda01b73f 100644 --- a/test/parallel/test-quic-endpoint-bind.mjs +++ b/test/parallel/test-quic-endpoint-bind.mjs @@ -46,6 +46,7 @@ const cert = readKey('agent1-cert.pem'); // Verify a client can connect to the bound address. const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, }); await clientSession.opened; diff --git a/test/parallel/test-quic-endpoint-busy.mjs b/test/parallel/test-quic-endpoint-busy.mjs index b6c874943a0164..5df58ce5886542 100644 --- a/test/parallel/test-quic-endpoint-busy.mjs +++ b/test/parallel/test-quic-endpoint-busy.mjs @@ -36,6 +36,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { // First connection before busy — should succeed. const cs1 = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 2 }, }); await cs1.opened; @@ -48,6 +49,7 @@ strictEqual(endpoint.busy, true); // Second connection while busy — server rejects. const cs2 = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, onerror: mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); diff --git a/test/parallel/test-quic-endpoint-idle-timeout.mjs b/test/parallel/test-quic-endpoint-idle-timeout.mjs index aa75d16a82b7ed..90d19ca455411c 100644 --- a/test/parallel/test-quic-endpoint-idle-timeout.mjs +++ b/test/parallel/test-quic-endpoint-idle-timeout.mjs @@ -31,6 +31,7 @@ const { listen, connect } = await import('../common/quic.mjs'); const clientEndpoint = new QuicEndpoint(); const client = await connect(serverEndpoint.address, { endpoint: clientEndpoint, + verifyPeer: 'manual', }); await client.opened; @@ -57,6 +58,7 @@ const { listen, connect } = await import('../common/quic.mjs'); const clientEndpoint = new QuicEndpoint({ idleTimeout: 1 }); const client = await connect(serverEndpoint.address, { endpoint: clientEndpoint, + verifyPeer: 'manual', }); await client.opened; await client.close(); diff --git a/test/parallel/test-quic-endpoint-state-transitions.mjs b/test/parallel/test-quic-endpoint-state-transitions.mjs index 559356893379ff..5997f812ae8d72 100644 --- a/test/parallel/test-quic-endpoint-state-transitions.mjs +++ b/test/parallel/test-quic-endpoint-state-transitions.mjs @@ -40,7 +40,10 @@ const cert = readKey('agent1-cert.pem'); strictEqual(serverEndpoint.closing, false); strictEqual(serverEndpoint.destroyed, false); - const cs = await connect(serverEndpoint.address, { alpn: 'quic-test' }); + const cs = await connect(serverEndpoint.address, { + alpn: 'quic-test', + verifyPeer: 'manual', + }); await cs.opened; await cs.close(); @@ -75,6 +78,7 @@ const cert = readKey('agent1-cert.pem'); // Connect via 127.0.0.1 since 0.0.0.0 listens on all interfaces. const cs = await connect(`127.0.0.1:${addr.port}`, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, }); await cs.opened; diff --git a/test/parallel/test-quic-h3-callback-errors.mjs b/test/parallel/test-quic-h3-callback-errors.mjs index b6fa8e9422dfaf..226a3f6b96fbd5 100644 --- a/test/parallel/test-quic-h3-callback-errors.mjs +++ b/test/parallel/test-quic-h3-callback-errors.mjs @@ -56,6 +56,7 @@ async function makeServer(onheadersHandler, extraOpts = {}) { const c = await connect(ep.address, { servername: 'localhost', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, }); await c.opened; @@ -95,6 +96,7 @@ async function makeServer(onheadersHandler, extraOpts = {}) { const c = await connect(ep.address, { servername: 'localhost', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, }); await c.opened; @@ -139,6 +141,7 @@ async function makeServer(onheadersHandler, extraOpts = {}) { const c = await connect(ep.address, { servername: 'localhost', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, }); await c.opened; @@ -187,6 +190,7 @@ async function makeServer(onheadersHandler, extraOpts = {}) { const clientSession = await connect(serverEndpoint.address, { servername: 'example.com', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, onorigin: mustCall(function() { throw new Error('onorigin error'); @@ -251,6 +255,7 @@ async function makeServer(onheadersHandler, extraOpts = {}) { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-close-behavior.mjs b/test/parallel/test-quic-h3-close-behavior.mjs index 6b36909a9f6b1c..02b34945087267 100644 --- a/test/parallel/test-quic-h3-close-behavior.mjs +++ b/test/parallel/test-quic-h3-close-behavior.mjs @@ -53,6 +53,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-concurrent-requests.mjs b/test/parallel/test-quic-h3-concurrent-requests.mjs index 69ab450cb7e445..6f0aa50b7f02ae 100644 --- a/test/parallel/test-quic-h3-concurrent-requests.mjs +++ b/test/parallel/test-quic-h3-concurrent-requests.mjs @@ -57,6 +57,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-datagram.mjs b/test/parallel/test-quic-h3-datagram.mjs index 2c3ea0aad77138..ea00cec42bc8f4 100644 --- a/test/parallel/test-quic-h3-datagram.mjs +++ b/test/parallel/test-quic-h3-datagram.mjs @@ -66,6 +66,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', application: { enableDatagrams: true }, transportParams: { maxDatagramFrameSize: 100 }, // Client receives datagram from server. @@ -139,6 +140,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', application: { enableDatagrams: true }, transportParams: { maxDatagramFrameSize: 100 }, }); diff --git a/test/parallel/test-quic-h3-error-codes.mjs b/test/parallel/test-quic-h3-error-codes.mjs index f8d520d3fdbffe..f9aebadc85cfd2 100644 --- a/test/parallel/test-quic-h3-error-codes.mjs +++ b/test/parallel/test-quic-h3-error-codes.mjs @@ -46,6 +46,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; @@ -96,6 +97,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-goaway-non-h3.mjs b/test/parallel/test-quic-h3-goaway-non-h3.mjs index 86931ec46fb51a..e0dd89d20279a0 100644 --- a/test/parallel/test-quic-h3-goaway-non-h3.mjs +++ b/test/parallel/test-quic-h3-goaway-non-h3.mjs @@ -45,6 +45,7 @@ const serverEndpoint = await listen(mustCall(async (ss) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', alpn: 'quic-test', // Ongoaway must NOT fire for non-H3 sessions. ongoaway: mustNotCall(), diff --git a/test/parallel/test-quic-h3-goaway.mjs b/test/parallel/test-quic-h3-goaway.mjs index c8a597b6f2e115..7542849f35eeed 100644 --- a/test/parallel/test-quic-h3-goaway.mjs +++ b/test/parallel/test-quic-h3-goaway.mjs @@ -71,6 +71,7 @@ dc.subscribe('quic.session.goaway', mustCall((msg) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', // Ongoaway fires when the peer sends GOAWAY. ongoaway: mustCall(function(lastStreamId) { strictEqual(lastStreamId, -1n); diff --git a/test/parallel/test-quic-h3-handshake-failure.mjs b/test/parallel/test-quic-h3-handshake-failure.mjs index 128acab8fffe3d..640f7e54c40209 100644 --- a/test/parallel/test-quic-h3-handshake-failure.mjs +++ b/test/parallel/test-quic-h3-handshake-failure.mjs @@ -40,6 +40,7 @@ const serverEndpoint = await listen(async (serverSession) => { // exists but hasn't started (control streams not yet bound). const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', // h3 ALPN — must match the server so the H3 application is selected // on the server side before we tear it down. }); diff --git a/test/parallel/test-quic-h3-header-validation.mjs b/test/parallel/test-quic-h3-header-validation.mjs index 0388137148b0a1..43673e0cc00f1e 100644 --- a/test/parallel/test-quic-h3-header-validation.mjs +++ b/test/parallel/test-quic-h3-header-validation.mjs @@ -77,6 +77,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; @@ -138,6 +139,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-headers-support.mjs b/test/parallel/test-quic-h3-headers-support.mjs index d513fb15978c93..8807adde6ef276 100644 --- a/test/parallel/test-quic-h3-headers-support.mjs +++ b/test/parallel/test-quic-h3-headers-support.mjs @@ -61,6 +61,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', alpn: 'quic-test', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-informational-headers.mjs b/test/parallel/test-quic-h3-informational-headers.mjs index 0f05d08c9bf0ae..6fa950b7bccbd6 100644 --- a/test/parallel/test-quic-h3-informational-headers.mjs +++ b/test/parallel/test-quic-h3-informational-headers.mjs @@ -77,6 +77,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-origin.mjs b/test/parallel/test-quic-h3-origin.mjs index 0e801b8e54408b..4f1fdf50e58d6d 100644 --- a/test/parallel/test-quic-h3-origin.mjs +++ b/test/parallel/test-quic-h3-origin.mjs @@ -55,6 +55,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'example.com', + verifyPeer: 'manual', // Client receives ORIGIN frame via onorigin callback. onorigin: mustCall(function(origins) { ok(Array.isArray(origins)); @@ -132,6 +133,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'custom-port.example.com', + verifyPeer: 'manual', onorigin: mustCall(function(origins) { ok(Array.isArray(origins)); diff --git a/test/parallel/test-quic-h3-pending-stream.mjs b/test/parallel/test-quic-h3-pending-stream.mjs index 39f5a4d833d89e..fd269e64e0543a 100644 --- a/test/parallel/test-quic-h3-pending-stream.mjs +++ b/test/parallel/test-quic-h3-pending-stream.mjs @@ -51,6 +51,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); // Create the stream BEFORE awaiting opened. The stream is pending diff --git a/test/parallel/test-quic-h3-post-filehandle.mjs b/test/parallel/test-quic-h3-post-filehandle.mjs index 8264a55cecc30b..46e30d8376d6cd 100644 --- a/test/parallel/test-quic-h3-post-filehandle.mjs +++ b/test/parallel/test-quic-h3-post-filehandle.mjs @@ -61,6 +61,7 @@ writeFileSync(testFile, testContent); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); const info = await clientSession.opened; diff --git a/test/parallel/test-quic-h3-post-request.mjs b/test/parallel/test-quic-h3-post-request.mjs index 1bd9100810aa58..a12458ef10df30 100644 --- a/test/parallel/test-quic-h3-post-request.mjs +++ b/test/parallel/test-quic-h3-post-request.mjs @@ -68,6 +68,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); const info = await clientSession.opened; diff --git a/test/parallel/test-quic-h3-priority.mjs b/test/parallel/test-quic-h3-priority.mjs index 9aac69bd6d2f22..fc7ca231f0d63a 100644 --- a/test/parallel/test-quic-h3-priority.mjs +++ b/test/parallel/test-quic-h3-priority.mjs @@ -55,6 +55,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; @@ -194,6 +195,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-qpack-settings.mjs b/test/parallel/test-quic-h3-qpack-settings.mjs index 6d059b4cbee175..6ca5671ef5b91f 100644 --- a/test/parallel/test-quic-h3-qpack-settings.mjs +++ b/test/parallel/test-quic-h3-qpack-settings.mjs @@ -71,6 +71,7 @@ async function makeRequest(clientSession, path) { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', // Client also disables QPACK dynamic table. application: { qpackMaxDTableCapacity: 0, qpackBlockedStreams: 0 }, }); @@ -108,6 +109,7 @@ async function makeRequest(clientSession, path) { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', application: { qpackMaxDTableCapacity: 8192, qpackBlockedStreams: 200 }, }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-request-response.mjs b/test/parallel/test-quic-h3-request-response.mjs index 817f620c89f8ee..1610f8deec1d41 100644 --- a/test/parallel/test-quic-h3-request-response.mjs +++ b/test/parallel/test-quic-h3-request-response.mjs @@ -77,6 +77,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', // Default ALPN is h3. }); diff --git a/test/parallel/test-quic-h3-settings.mjs b/test/parallel/test-quic-h3-settings.mjs index 2b3b9a628f1c15..363aa985c23a82 100644 --- a/test/parallel/test-quic-h3-settings.mjs +++ b/test/parallel/test-quic-h3-settings.mjs @@ -60,6 +60,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; @@ -118,6 +119,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; @@ -164,6 +166,7 @@ const decoder = new TextDecoder(); const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', application: { enableConnectProtocol: true, enableDatagrams: true }, }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs b/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs index 135d0301b00386..67286d9a5ad6a9 100644 --- a/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs +++ b/test/parallel/test-quic-h3-stream-destroy-with-headers.mjs @@ -34,6 +34,7 @@ const serverEndpoint = await listen(mustCall(async (ss) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-stream-idle-timeout.mjs b/test/parallel/test-quic-h3-stream-idle-timeout.mjs new file mode 100644 index 00000000000000..5c851caca42465 --- /dev/null +++ b/test/parallel/test-quic-h3-stream-idle-timeout.mjs @@ -0,0 +1,185 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: HTTP/3 stream idle timeout. +// Peer-initiated streams that receive no data within the configured +// timeout are automatically destroyed. This test verifies the behavior +// with HTTP/3 bidirectional streams, where ShutdownStream maps +// transport errors to the application's internal error code +// (NGHTTP3_H3_INTERNAL_ERROR = 0x102) on the wire. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import * as fixtures from '../common/fixtures.mjs'; +import { text } from 'node:stream/iter'; + +const { rejects, strictEqual } = assert; +const { readKey } = fixtures; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const key = createPrivateKey(readKey('agent1-key.pem')); +const cert = readKey('agent1-cert.pem'); + +const encoder = new TextEncoder(); + +// --- H3 stream destroyed after idle timeout --- +{ + const streamDestroyed = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Don't read — let the stream sit idle after the initial headers. + // The stream idle timeout should destroy it, rejecting stream.closed. + await rejects(stream.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + streamDestroyed.resolve(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + streamIdleTimeout: 100, + onheaders() { + // Receive headers but do nothing — let the stream go idle. + }, + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + + await clientSession.opened; + + // Send a POST request with a body byte so the server creates the + // stream, then stop sending (don't end the write side). + const stream = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + onheaders() {}, + }); + const writer = stream.writer; + writer.writeSync(encoder.encode('x')); + + // Wait for the server to destroy the idle stream. + await streamDestroyed.promise; + + // The server sent STOP_SENDING / RESET_STREAM when it destroyed the + // idle stream. ShutdownStream maps the transport error to the H3 + // internal error code (0x102) on the wire. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- H3 stream with ongoing data is NOT destroyed --- +{ + const serverGotData = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await text(stream); + strictEqual(data, 'xy'); + serverGotData.resolve(); + await serverSession.close(); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + streamIdleTimeout: 500, + onheaders: mustCall(function(headers) { + strictEqual(headers[':method'], 'POST'); + // Send response headers so the stream is fully established. + this.sendHeaders({ ':status': '200' }, { terminal: true }); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + onheaders() {}, + }); + stream.sendHeaders({ + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + 'content-length': '2', + }, { terminal: false }); + const writer = stream.writer; + writer.writeSync(encoder.encode('x')); + + // Wait less than the 500ms idle timeout, then send more data. + await setTimeout(300); + + writer.writeSync(encoder.encode('y')); + writer.endSync(); + + await Promise.all([serverGotData.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- Disabled when set to 0 --- +{ + const streamSurvived = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await text(stream); + strictEqual(data, 'xy'); + streamSurvived.resolve(); + }); + + await setTimeout(700); + await serverSession.close(); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + streamIdleTimeout: 0, // Disabled + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }, { terminal: true }); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + const stream = await clientSession.createBidirectionalStream({ + onheaders() {}, + }); + stream.sendHeaders({ + ':method': 'POST', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, { terminal: false }); + const writer = stream.writer; + writer.writeSync(encoder.encode('x')); + + // Pause beyond any reasonable idle timeout to verify it's disabled. + await setTimeout(600); + + writer.writeSync(encoder.encode('y')); + writer.endSync(); + + await Promise.all([streamSurvived.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-h3-trailing-headers.mjs b/test/parallel/test-quic-h3-trailing-headers.mjs index c620488a920798..436cb243b3dd99 100644 --- a/test/parallel/test-quic-h3-trailing-headers.mjs +++ b/test/parallel/test-quic-h3-trailing-headers.mjs @@ -82,6 +82,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs b/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs index de542310f0a011..e724b6b04a485b 100644 --- a/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs +++ b/test/parallel/test-quic-h3-zero-rtt-bogus-ticket.mjs @@ -30,6 +30,7 @@ const serverEndpoint = await listen(mustNotCall(), { await rejects( connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', sessionTicket: randomBytes(256), }), { code: 'ERR_INVALID_ARG_VALUE' }, diff --git a/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs b/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs index bbc0cd48fe13f5..41f77a63a1f980 100644 --- a/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs +++ b/test/parallel/test-quic-h3-zero-rtt-rejected-settings.mjs @@ -53,6 +53,7 @@ async function getTicket(endpointOptions) { const cs = await connect(ep.address, { servername: 'localhost', + verifyPeer: 'manual', ...endpointOptions, onsessionticket(ticket) { ok(Buffer.isBuffer(ticket)); @@ -102,6 +103,7 @@ async function attemptRejected0RTT(endpointOptions, ticket, token) { const cs = await connect(ep.address, { servername: 'localhost', + verifyPeer: 'manual', ...endpointOptions, sessionTicket: ticket, token, diff --git a/test/parallel/test-quic-h3-zero-rtt.mjs b/test/parallel/test-quic-h3-zero-rtt.mjs index 18a841eb938516..4e51958d7c864a 100644 --- a/test/parallel/test-quic-h3-zero-rtt.mjs +++ b/test/parallel/test-quic-h3-zero-rtt.mjs @@ -58,6 +58,7 @@ const serverEndpoint = await listen(mustCall((ss) => { // --- First connection: establish H3 session, receive ticket --- const cs1 = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', onsessionticket: mustCall(function(ticket) { ok(Buffer.isBuffer(ticket)); ok(ticket.length > 0); @@ -99,6 +100,7 @@ ok(savedToken); // --- Second connection: 0-RTT with H3 --- const cs2 = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', sessionTicket: savedTicket, token: savedToken, }); diff --git a/test/parallel/test-quic-handshake-ipv6-only.mjs b/test/parallel/test-quic-handshake-ipv6-only.mjs index 03531b6ce42ca0..3b3c6a2aa08a42 100644 --- a/test/parallel/test-quic-handshake-ipv6-only.mjs +++ b/test/parallel/test-quic-handshake-ipv6-only.mjs @@ -60,6 +60,7 @@ ok(serverEndpoint.address !== undefined); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', endpoint: { address: { address: '::', diff --git a/test/parallel/test-quic-handshake.mjs b/test/parallel/test-quic-handshake.mjs index 1779ceaa23fbf8..1a8a90d1f32516 100644 --- a/test/parallel/test-quic-handshake.mjs +++ b/test/parallel/test-quic-handshake.mjs @@ -54,6 +54,7 @@ ok(serverEndpoint.address !== undefined); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', }); const info = await clientSession.opened; diff --git a/test/parallel/test-quic-internal-endpoint-options.mjs b/test/parallel/test-quic-internal-endpoint-options.mjs index 306d0c523f4611..1d922b1ce77136 100644 --- a/test/parallel/test-quic-internal-endpoint-options.mjs +++ b/test/parallel/test-quic-internal-endpoint-options.mjs @@ -11,6 +11,7 @@ if (!hasQuic) { // Import after the hasQuic check const { QuicEndpoint } = await import('node:quic'); +const { BlockList } = await import('node:net'); // Reject invalid options ['a', null, false, NaN].forEach((i) => { @@ -53,25 +54,71 @@ 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: 'sessionCreationRate', + valid: [0, 1, 10, 100.5, 1000, Infinity], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'sessionCreationBurst', + valid: [0, 1, 10, 100.5, 1000, Infinity], + invalid: [-1, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'blockList', + valid: [new BlockList()], + invalid: ['a', 0, null, false, true, {}, [], () => {}] + }, + { + key: 'blockListPolicy', + valid: ['deny', 'allow'], + invalid: ['invalid', 0, 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..2af27724eb4e18 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.mjs +++ b/test/parallel/test-quic-internal-endpoint-stats-state.mjs @@ -88,9 +88,15 @@ 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'); + strictEqual(typeof endpoint.stats.sessionCreationRateLimited, 'bigint'); + strictEqual(typeof endpoint.stats.packetsBlocked, 'bigint'); deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ 'connected', @@ -104,9 +110,15 @@ const { 'clientSessions', 'serverBusyCount', 'retryCount', + 'retryRateLimited', 'versionNegotiationCount', + 'versionNegotiationRateLimited', 'statelessResetCount', + 'statelessResetRateLimited', 'immediateCloseCount', + 'immediateCloseRateLimited', + 'sessionCreationRateLimited', + 'packetsBlocked', ]); strictEqual(typeof inspect(endpoint.stats), 'string'); } @@ -219,6 +231,7 @@ strictEqual(typeof sessionStats.datagramsReceived, 'bigint'); strictEqual(typeof sessionStats.datagramsSent, 'bigint'); strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint'); strictEqual(typeof sessionStats.datagramsLost, 'bigint'); +strictEqual(typeof sessionStats.streamsIdleTimedOut, 'bigint'); strictEqual(typeof sessionStats.toJSON(), 'object'); strictEqual(typeof inspect(sessionStats), 'string'); streamStats[kFinishClose](); diff --git a/test/parallel/test-quic-module-exports.mjs b/test/parallel/test-quic-module-exports.mjs index 73f69d6bcf8711..1b4b368c60ce61 100644 --- a/test/parallel/test-quic-module-exports.mjs +++ b/test/parallel/test-quic-module-exports.mjs @@ -37,6 +37,7 @@ ok(quic.QuicEndpoint); }); const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', onsessionticket(ticket) { savedTicket = ticket; gotTicket.resolve(); diff --git a/test/parallel/test-quic-new-token.mjs b/test/parallel/test-quic-new-token.mjs index cafff1146e0da0..5304fa66ccdfc5 100644 --- a/test/parallel/test-quic-new-token.mjs +++ b/test/parallel/test-quic-new-token.mjs @@ -40,6 +40,7 @@ const serverEndpoint = await listen(mustCall((serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', servername: 'localhost', // Set onnewtoken at connection time to avoid missing the event. onnewtoken: mustCall(function(token, address) { diff --git a/test/parallel/test-quic-reject-unauthorized.mjs b/test/parallel/test-quic-reject-unauthorized.mjs index e9900fbf31d990..33fa3661a857f2 100644 --- a/test/parallel/test-quic-reject-unauthorized.mjs +++ b/test/parallel/test-quic-reject-unauthorized.mjs @@ -25,30 +25,77 @@ await rejects(connect({ port: 1234 }, { code: 'ERR_INVALID_ARG_TYPE', }); -// With rejectUnauthorized: true (the default), connecting with self-signed -// certs and no CA should produce a validation error in the handshake info. +// verifyPeer must be one of 'strict', 'auto', 'manual' +await rejects(connect({ port: 1234 }, { + alpn: 'quic-test', + verifyPeer: 'invalid', +}), { + code: 'ERR_INVALID_ARG_VALUE', +}); -const serverEndpoint = await listen(mustCall(async (serverSession) => { - await serverSession.opened; +const serverEndpoint = await listen(async (serverSession) => { + serverSession.onerror = () => {}; + await rejects(serverSession.opened, (err) => { + ok(err.code === 'ERR_QUIC_TRANSPORT_ERROR' || + err.code === 'ERR_INVALID_STATE'); + return true; + }); serverSession.close(); - await serverSession.closed; -}), { +}, { sni: { '*': { keys: [key], certs: [cert] } }, alpn: ['quic-test'], }); -const clientSession = await connect(serverEndpoint.address, { - alpn: 'quic-test', - servername: 'localhost', - // Default: rejectUnauthorized: true -}); +// --- verifyPeer: 'manual' (current behavior) --- +// The session.opened promise resolves even with an invalid cert. +// The validation error is available in the info object. +{ + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + verifyPeer: 'manual', + }); + + const info = await clientSession.opened; + // Self-signed cert without CA should produce a validation error. + strictEqual(typeof info.validationErrorReason, 'string'); + ok(info.validationErrorReason.length > 0); + strictEqual(typeof info.validationErrorCode, 'string'); + ok(info.validationErrorCode.length > 0); -const info = await clientSession.opened; -// Self-signed cert without CA should produce a validation error. -strictEqual(typeof info.validationErrorReason, 'string'); -ok(info.validationErrorReason.length > 0); -strictEqual(typeof info.validationErrorCode, 'string'); -ok(info.validationErrorCode.length > 0); + await clientSession.close(); +} + +// --- verifyPeer: 'auto' (default) --- +// The session.opened promise rejects when the cert is invalid. +{ + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + // verifyPeer defaults to 'auto' + }); + + await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); +} + +// --- verifyPeer: 'strict' --- +// The TLS handshake itself fails. The session.opened promise rejects. +{ + const clientSession = await connect(serverEndpoint.address, { + alpn: 'quic-test', + servername: 'localhost', + verifyPeer: 'strict', + // The TLS failure triggers onerror before opened rejects. + onerror: mustCall((err) => { + ok(err, 'strict mode should fire onerror on invalid cert'); + }), + }); + + await rejects(clientSession.opened, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); +} -await clientSession.close(); await serverEndpoint.close(); diff --git a/test/parallel/test-quic-session-close-error-code.mjs b/test/parallel/test-quic-session-close-error-code.mjs index e907cd672c19d4..5d0392bf102585 100644 --- a/test/parallel/test-quic-session-close-error-code.mjs +++ b/test/parallel/test-quic-session-close-error-code.mjs @@ -29,13 +29,18 @@ const { listen, connect } = await import('../common/quic.mjs'); const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.onerror = mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); - strictEqual(err.message.includes('42n'), true, + strictEqual(err.message.includes('42'), true, 'error message should contain the code'); strictEqual(err.message.includes('client shutdown'), true, 'error message should contain the reason'); + strictEqual(err.errorCode, 42n); + strictEqual(err.type, 'application'); + strictEqual(err.reason, 'client shutdown'); }); await rejects(serverSession.closed, { code: 'ERR_QUIC_APPLICATION_ERROR', + errorCode: 42n, + reason: 'client shutdown', }); serverGot.resolve(); })); @@ -71,8 +76,10 @@ const { listen, connect } = await import('../common/quic.mjs'); const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.onerror = mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); - strictEqual(err.message.includes('1n'), true, + strictEqual(err.message.includes('1'), true, 'error message should contain the code'); + strictEqual(err.errorCode, 1n); + strictEqual(err.type, 'transport'); }); await rejects(serverSession.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR', @@ -102,7 +109,10 @@ const { listen, connect } = await import('../common/quic.mjs'); const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.onerror = mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); - strictEqual(err.message.includes('99n'), true); + strictEqual(err.message.includes('99'), true); + strictEqual(err.errorCode, 99n); + strictEqual(err.type, 'application'); + strictEqual(err.reason, 'destroy with code'); }); await rejects(serverSession.closed, { code: 'ERR_QUIC_APPLICATION_ERROR', diff --git a/test/parallel/test-quic-session-opened-early-destroy.mjs b/test/parallel/test-quic-session-opened-early-destroy.mjs index 897f6653495884..9c1cbd7f71f3e0 100644 --- a/test/parallel/test-quic-session-opened-early-destroy.mjs +++ b/test/parallel/test-quic-session-opened-early-destroy.mjs @@ -44,6 +44,7 @@ const transportParams = { maxIdleTimeout: 1 }; }, { transportParams }); const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', transportParams, }); @@ -70,6 +71,7 @@ const transportParams = { maxIdleTimeout: 1 }; const serverEndpoint = await listen(mustNotCall, { transportParams }); const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', transportParams, }); @@ -96,6 +98,7 @@ const transportParams = { maxIdleTimeout: 1 }; const clientEndpoint = new QuicEndpoint(); const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', transportParams, endpoint: clientEndpoint, }); @@ -127,6 +130,7 @@ const transportParams = { maxIdleTimeout: 1 }; }), { transportParams }); const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', transportParams, }); diff --git a/test/parallel/test-quic-session-opened-validation.mjs b/test/parallel/test-quic-session-opened-validation.mjs index d9f3d758f6df04..a73fd18734dbff 100644 --- a/test/parallel/test-quic-session-opened-validation.mjs +++ b/test/parallel/test-quic-session-opened-validation.mjs @@ -27,7 +27,9 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { serverSession.close(); })); -const clientSession = await connect(serverEndpoint.address); +const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', +}); const clientInfo = await clientSession.opened; // validationErrorReason is a non-empty string describing 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(); diff --git a/test/parallel/test-quic-session-stream-lifecycle.mjs b/test/parallel/test-quic-session-stream-lifecycle.mjs index 2eb35145dfe9fe..a4bd287cdc6dcd 100644 --- a/test/parallel/test-quic-session-stream-lifecycle.mjs +++ b/test/parallel/test-quic-session-stream-lifecycle.mjs @@ -52,7 +52,9 @@ strictEqual(epStats.isConnected, true); ok(epStats.createdAt > 0n); // Connect with a client -const clientSession = await quic.connect(serverEndpoint.address); +const clientSession = await quic.connect(serverEndpoint.address, { + verifyPeer: 'manual', +}); strictEqual(clientSession.destroyed, false); ok(clientSession.endpoint !== null); diff --git a/test/parallel/test-quic-shared-endpoint-stream-close.mjs b/test/parallel/test-quic-shared-endpoint-stream-close.mjs index 1a76decd4e2937..19e530e50b9ec0 100644 --- a/test/parallel/test-quic-shared-endpoint-stream-close.mjs +++ b/test/parallel/test-quic-shared-endpoint-stream-close.mjs @@ -51,6 +51,7 @@ const clientEndpoint = new QuicEndpoint(); // The client receives the stateless reset and closes session 1. const client1 = await connect(serverEndpoint.address, { endpoint: clientEndpoint, + verifyPeer: 'manual', onerror: mustCall((err) => { assert.ok(err); }), }); await client1.opened; @@ -75,6 +76,7 @@ await assert.rejects(client1.closed, { code: 'ERR_QUIC_TRANSPORT_ERROR' }); // for session 2 hangs. const client2 = await connect(serverEndpoint.address, { endpoint: clientEndpoint, + verifyPeer: 'manual', onerror(err) { /* marks promises as handled */ }, }); await client2.opened; diff --git a/test/parallel/test-quic-sni-mismatch.mjs b/test/parallel/test-quic-sni-mismatch.mjs index ea77672101355a..dbb2de4c011a30 100644 --- a/test/parallel/test-quic-sni-mismatch.mjs +++ b/test/parallel/test-quic-sni-mismatch.mjs @@ -44,6 +44,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', servername: 'wrong.example.com', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 1 }, onerror: mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); diff --git a/test/parallel/test-quic-sni-multi-entry.mjs b/test/parallel/test-quic-sni-multi-entry.mjs index 254e90e29e3211..2348c844fc96d6 100644 --- a/test/parallel/test-quic-sni-multi-entry.mjs +++ b/test/parallel/test-quic-sni-multi-entry.mjs @@ -48,6 +48,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { { const cs = await connect(serverEndpoint.address, { servername: 'host1.example.com', + verifyPeer: 'manual', alpn: 'quic-test', }); const info = await cs.opened; @@ -59,6 +60,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { { const cs = await connect(serverEndpoint.address, { servername: 'host2.example.com', + verifyPeer: 'manual', alpn: 'quic-test', }); const info = await cs.opened; @@ -70,6 +72,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { { const cs = await connect(serverEndpoint.address, { servername: 'unknown.example.com', + verifyPeer: 'manual', alpn: 'quic-test', }); const info = await cs.opened; diff --git a/test/parallel/test-quic-sni-setcontexts.mjs b/test/parallel/test-quic-sni-setcontexts.mjs index af0c6dc048f1d7..5ad665cdeac960 100644 --- a/test/parallel/test-quic-sni-setcontexts.mjs +++ b/test/parallel/test-quic-sni-setcontexts.mjs @@ -42,6 +42,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { { const cs = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 2 }, }); const info = await cs.opened; @@ -58,6 +59,7 @@ endpoint.setSNIContexts( { const cs = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', transportParams: { maxIdleTimeout: 2 }, }); const info = await cs.opened; diff --git a/test/parallel/test-quic-sni.mjs b/test/parallel/test-quic-sni.mjs index e8669380ba855e..fe93260aceb1f5 100644 --- a/test/parallel/test-quic-sni.mjs +++ b/test/parallel/test-quic-sni.mjs @@ -39,6 +39,7 @@ ok(serverEndpoint.address !== undefined); // Client connects with servername 'localhost' — should match the SNI entry. const clientSession = await connect(serverEndpoint.address, { servername: 'localhost', + verifyPeer: 'manual', alpn: 'quic-test', }); const clientInfo = await clientSession.opened; diff --git a/test/parallel/test-quic-stateless-reset.mjs b/test/parallel/test-quic-stateless-reset.mjs index b9fdb397e00c6f..3af4fcc75dfddf 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'; @@ -48,6 +47,7 @@ const encoder = new TextEncoder(); const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, + verifyPeer: 'manual', onerror: mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); }), @@ -104,6 +104,7 @@ const encoder = new TextEncoder(); const clientSession = await connect(serverEndpoint.address, { reuseEndpoint: false, + verifyPeer: 'manual', // Short idle timeout so the client doesn't hang waiting for // a stateless reset that will never arrive. transportParams: { maxIdleTimeout: 1 }, @@ -138,9 +139,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 +161,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(); @@ -174,6 +174,7 @@ const encoder = new TextEncoder(); const client1 = await connect(serverEndpoint.address, { endpoint: clientEndpoint, + verifyPeer: 'manual', onerror: mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); }), @@ -200,6 +201,7 @@ const encoder = new TextEncoder(); const client2 = await connect(serverEndpoint.address, { endpoint: clientEndpoint, + verifyPeer: 'manual', // Short idle timeout so the client closes after the server // destroys (no stateless reset will arrive, rate-limited). transportParams: { maxIdleTimeout: 1 }, @@ -215,7 +217,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 +228,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(); diff --git a/test/parallel/test-quic-stream-destroy-emits-reset.mjs b/test/parallel/test-quic-stream-destroy-emits-reset.mjs index 280e25bc293a2d..e565f5e9ff7b8f 100644 --- a/test/parallel/test-quic-stream-destroy-emits-reset.mjs +++ b/test/parallel/test-quic-stream-destroy-emits-reset.mjs @@ -12,12 +12,12 @@ // code is the negotiated application's "internal error" code: for // the test fixture's non-h3 ALPN (`quic-test`) the C++ // DefaultApplication reports `1n`, which propagates to the server -// as `ERR_QUIC_APPLICATION_ERROR` carrying `1n` in its message. +// as `ERR_QUIC_APPLICATION_ERROR` exposing `errorCode === 1n`. import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; -const { strictEqual, ok, rejects } = assert; +const { strictEqual, rejects } = assert; if (!hasQuic) { skip('QUIC is not enabled'); @@ -28,19 +28,19 @@ const { listen, connect } = await import('../common/quic.mjs'); const serverResetSeen = Promise.withResolvers(); const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.onstream = mustCall((stream) => { - // The cascade-driven destroy of the server-side stream after the - // peer reset rejects `stream.closed` with the wire error; the - // test does not assert on its specific shape, only that `onreset` - // fired with the expected code. + serverSession.onstream = mustCall(async (stream) => { stream.onreset = mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); - // The DefaultApplication's internal error code is 0x1n, which - // util.format renders as `1n` (BigInt) in the message text. - ok(err.message.includes('1n'), - `expected '1n' in message, got: ${err.message}`); + // The DefaultApplication's internal error code is 0x1n. + strictEqual(err.errorCode, 1n); serverResetSeen.resolve(); }); + + // The peer's reset causes stream.closed to reject with the reset + // error code. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); }); })); diff --git a/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs b/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs index dc78908af7a3b1..47a6d8f1857e4f 100644 --- a/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs +++ b/test/parallel/test-quic-stream-destroy-emits-stop-sending.mjs @@ -35,7 +35,7 @@ const { listen, connect } = await import('../common/quic.mjs'); const serverObservation = Promise.withResolvers(); const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.onstream = mustCall((stream) => { + serverSession.onstream = mustCall(async (stream) => { const writer = stream.writer; // Sanity: the writer is active before the peer tears the stream // down, so desiredSize is a number reflecting the initial @@ -58,6 +58,11 @@ const serverEndpoint = await listen(mustCall((serverSession) => { serverObservation.resolve(writer.desiredSize); }); }); + + // The peer's reset causes stream.closed to reject. + await assert.rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); }); })); diff --git a/test/parallel/test-quic-stream-destroy-options-code.mjs b/test/parallel/test-quic-stream-destroy-options-code.mjs index bdb64329b477bb..b1fd70018d456b 100644 --- a/test/parallel/test-quic-stream-destroy-options-code.mjs +++ b/test/parallel/test-quic-stream-destroy-options-code.mjs @@ -13,7 +13,7 @@ import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; -const { strictEqual, ok, rejects } = assert; +const { strictEqual, rejects } = assert; if (!hasQuic) { skip('QUIC is not enabled'); @@ -24,13 +24,18 @@ const { listen, connect } = await import('../common/quic.mjs'); const serverResetSeen = Promise.withResolvers(); const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.onstream = mustCall((stream) => { + serverSession.onstream = mustCall(async (stream) => { stream.onreset = mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); - ok(err.message.includes('66n'), - `expected '66n' in message, got: ${err.message}`); + strictEqual(err.errorCode, 66n); serverResetSeen.resolve(); }); + + // The peer's reset causes stream.closed to reject with the reset + // error code. + await rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); }); })); diff --git a/test/parallel/test-quic-stream-idle-timeout.mjs b/test/parallel/test-quic-stream-idle-timeout.mjs new file mode 100644 index 00000000000000..9b2b36b41a8e47 --- /dev/null +++ b/test/parallel/test-quic-stream-idle-timeout.mjs @@ -0,0 +1,141 @@ +// Flags: --experimental-quic --experimental-stream-iter --no-warnings + +// Test: stream idle timeout. +// Peer-initiated streams that receive no data within the configured +// timeout are automatically destroyed. + +import { hasQuic, skip, mustCall } from '../common/index.mjs'; +import assert from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { text } from 'node:stream/iter'; + +const { rejects, strictEqual } = assert; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { listen, connect } = await import('../common/quic.mjs'); + +// --- Stream destroyed after idle timeout --- +{ + const streamDestroyed = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + // Don't read — let the stream sit idle after the initial data. + // The stream idle timeout should destroy it, rejecting stream.closed. + await rejects(stream.closed, { + code: 'ERR_QUIC_TRANSPORT_ERROR', + }); + streamDestroyed.resolve(); + }); + }), { + // Short idle timeout for the test — 500ms. + streamIdleTimeout: 100, + }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + transportParams: { maxIdleTimeout: 1 }, + }); + + await clientSession.opened; + + // Open a stream and send one byte so the server creates the + // peer-initiated stream, then stop sending. + const clientStream = await clientSession.createUnidirectionalStream(); + const writer = clientStream.writer; + writer.writeSync('x'); + + // Wait for the server to destroy the idle stream. + await streamDestroyed.promise; + + // The server sent STOP_SENDING when it destroyed the idle stream. + // ShutdownStream maps the transport error to the application's + // internal error code on the wire, so the client sees an application + // error indicating the server rejected the stream. + await rejects(clientStream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); + + await clientSession.close(); + await serverEndpoint.close(); +} + +// --- Stream with data is NOT destroyed --- +{ + const encoder = new TextEncoder(); + const serverGotData = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + const data = await text(stream); + strictEqual(data, 'xy'); + serverGotData.resolve(); + await serverSession.close(); + }); + }), { + streamIdleTimeout: 500, + }); + + const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', + }); + await clientSession.opened; + + // Open a stream and send data immediately. + const clientStrema = await clientSession.createUnidirectionalStream(); + const writer = clientStrema.writer; + writer.writeSync(encoder.encode('x')); + + // Less than the 500ms idle timeout. + await setTimeout(300); + + writer.writeSync(encoder.encode('y')); + writer.endSync(); + + await Promise.all([serverGotData.promise, clientSession.closed]); + await serverEndpoint.close(); +} + +// --- Disabled when set to 0 --- +{ + const streamSurvived = Promise.withResolvers(); + + const serverEndpoint = await listen(mustCall(async (serverSession) => { + serverSession.onstream = mustCall(async (stream) => { + + // Do not await this yet. + const data = await text(stream); + + // We should receive all the data, even though it is sent with a pause + // longer than the default idle timeout. + strictEqual(data, 'xy'); + streamSurvived.resolve(); + }); + + await setTimeout(700); + await serverSession.close(); + }), { + streamIdleTimeout: 0, // Disabled + }); + + const clientSession = await connect(serverEndpoint.address); + await clientSession.opened; + + // Send one byte so the server creates the stream, then stop. + const clientStream = await clientSession.createUnidirectionalStream(); + const writer = clientStream.writer; + writer.writeSync('x'); + + // Now pause beyond the 500ms default timeout to verify the stream is not + // killed by idle timeout. + await setTimeout(600); + + writer.writeSync('y'); + writer.endSync(); + + await Promise.all([streamSurvived.promise, clientSession.closed]); + await serverEndpoint.close(); +} diff --git a/test/parallel/test-quic-stream-priority.mjs b/test/parallel/test-quic-stream-priority.mjs index 1b8128ed6cbe29..bed1fd065fdfc9 100644 --- a/test/parallel/test-quic-stream-priority.mjs +++ b/test/parallel/test-quic-stream-priority.mjs @@ -27,6 +27,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', }); await clientSession.opened; diff --git a/test/parallel/test-quic-stream-writer-fail-error-code.mjs b/test/parallel/test-quic-stream-writer-fail-error-code.mjs index 75a680ec4fa8bf..ff3c18f69c5d03 100644 --- a/test/parallel/test-quic-stream-writer-fail-error-code.mjs +++ b/test/parallel/test-quic-stream-writer-fail-error-code.mjs @@ -17,11 +17,8 @@ // the QuicError fast path. // // The peer-side observation goes through `stream.onreset(err)` where -// `err` is `ERR_QUIC_APPLICATION_ERROR` carrying the wire code in its -// message string. We extract the code via regex; once -// `ERR_QUIC_APPLICATION_ERROR` exposes the numeric code as a property -// (a planned follow-up), this test can switch to direct property -// access. +// `err` is `ERR_QUIC_APPLICATION_ERROR` exposing the wire code on +// `err.errorCode` (a BigInt). import { hasQuic, skip, mustCall } from '../common/index.mjs'; import assert from 'node:assert'; @@ -35,19 +32,9 @@ if (!hasQuic) { const { listen, connect } = await import('../common/quic.mjs'); const { QuicError } = await import('node:quic'); -// Extract the numeric wire code from an ERR_QUIC_APPLICATION_ERROR -// message of the form -// "A QUIC application error occurred. n []" -// where the trailing `n` on the code is the BigInt formatting from -// `util.format('%d', bigint)`. RESET_STREAM frames do not carry a -// reason string, so the bracketed value is typically `undefined`. function wireCodeOf(err) { strictEqual(err.code, 'ERR_QUIC_APPLICATION_ERROR'); - const match = err.message.match(/A QUIC application error occurred\. (\d+)n /); - if (!match) { - throw new Error(`Could not extract code from message: ${err.message}`); - } - return BigInt(match[1]); + return err.errorCode; } // Server: capture the next two streams. Each stream receives an @@ -60,7 +47,7 @@ const allDone = Promise.withResolvers(); const observed = []; const serverEndpoint = await listen(mustCall((serverSession) => { - serverSession.onstream = mustCall((stream) => { + serverSession.onstream = mustCall(async (stream) => { const i = nextStreamIndex++; stream.onreset = mustCall((err) => { observed[i] = wireCodeOf(err); @@ -69,10 +56,17 @@ const serverEndpoint = await listen(mustCall((serverSession) => { allDone.resolve(); } }); + + // The peer's reset causes stream.closed to reject. + await assert.rejects(stream.closed, { + code: 'ERR_QUIC_APPLICATION_ERROR', + }); }, 2); })); -const clientSession = await connect(serverEndpoint.address); +const clientSession = await connect(serverEndpoint.address, { + verifyPeer: 'manual', +}); await clientSession.opened; // 1. Plain Error -> session.internalErrorCode (0x1n for non-h3). diff --git a/test/parallel/test-quic-tls-ca.mjs b/test/parallel/test-quic-tls-ca.mjs index dae35e975a6760..560d1eb412a43e 100644 --- a/test/parallel/test-quic-tls-ca.mjs +++ b/test/parallel/test-quic-tls-ca.mjs @@ -36,6 +36,7 @@ const serverEndpoint = await listen(mustCall(async (serverSession) => { const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', servername: 'localhost', + verifyPeer: 'manual', ca, }); diff --git a/test/parallel/test-quic-tls-crl.mjs b/test/parallel/test-quic-tls-crl.mjs index 78a854759695c8..fe5ac007aadefb 100644 --- a/test/parallel/test-quic-tls-crl.mjs +++ b/test/parallel/test-quic-tls-crl.mjs @@ -38,6 +38,7 @@ const agent3Cert = readKey('agent3-cert.pem'); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', ca: [ca2Cert], crl: [ca2Crl], }); @@ -64,6 +65,7 @@ const agent3Cert = readKey('agent3-cert.pem'); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', ca: [ca2Cert], crl: [ca2CrlAgent3], }); diff --git a/test/parallel/test-quic-tls-options.mjs b/test/parallel/test-quic-tls-options.mjs index ccf6488b8a076c..e903f8a64bcad3 100644 --- a/test/parallel/test-quic-tls-options.mjs +++ b/test/parallel/test-quic-tls-options.mjs @@ -37,6 +37,7 @@ const cert = readKey('agent1-cert.pem'); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', servername: 'localhost', ciphers: 'TLS_AES_256_GCM_SHA384', }); @@ -62,6 +63,7 @@ const cert = readKey('agent1-cert.pem'); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', servername: 'localhost', groups: 'P-256', }); diff --git a/test/parallel/test-quic-tls-verify-client.mjs b/test/parallel/test-quic-tls-verify-client.mjs index ae2e89c8785f72..5d893fe8899a2b 100644 --- a/test/parallel/test-quic-tls-verify-client.mjs +++ b/test/parallel/test-quic-tls-verify-client.mjs @@ -42,6 +42,7 @@ const clientCert = readKey('agent2-cert.pem'); const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', keys: [clientKey], certs: [clientCert], }); @@ -72,6 +73,7 @@ const clientCert = readKey('agent2-cert.pem'); // Client connects WITHOUT providing a certificate. const clientSession = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', onerror: mustCall((err) => { strictEqual(err.code, 'ERR_QUIC_TRANSPORT_ERROR'); }), diff --git a/test/parallel/test-quic-token-expired.mjs b/test/parallel/test-quic-token-expired.mjs index f0f24ac8e891a3..527bb0caac6847 100644 --- a/test/parallel/test-quic-token-expired.mjs +++ b/test/parallel/test-quic-token-expired.mjs @@ -41,6 +41,7 @@ const serverEndpoint = await listen((serverSession) => { // First connection: receive the token. const cs1 = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', onnewtoken: mustCall((token) => { savedToken = token; gotToken.resolve(); @@ -58,6 +59,7 @@ await setTimeout(1500); // connection should still succeed (Retry is transparent). const cs2 = await connect(serverEndpoint.address, { alpn: 'quic-test', + verifyPeer: 'manual', token: savedToken, }); await cs2.opened; diff --git a/test/parallel/test-quic-token-secret.mjs b/test/parallel/test-quic-token-secret.mjs index cbafd679d772b6..d50ec343848b87 100644 --- a/test/parallel/test-quic-token-secret.mjs +++ b/test/parallel/test-quic-token-secret.mjs @@ -41,6 +41,7 @@ const ep1 = await listen(async (serverSession) => { // Get a token from the first server. const cs1 = await connect(ep1.address, { alpn: 'quic-test', + verifyPeer: 'manual', onnewtoken: mustCall((token) => { savedToken = token; gotToken.resolve(); @@ -63,6 +64,7 @@ const ep2 = await listen(async (serverSession) => { const cs2 = await connect(ep2.address, { alpn: 'quic-test', + verifyPeer: 'manual', token: savedToken, }); await cs2.opened; @@ -82,6 +84,7 @@ const ep3 = await listen(async (serverSession) => { const cs3 = await connect(ep3.address, { alpn: 'quic-test', + verifyPeer: 'manual', token: savedToken, }); await cs3.opened; diff --git a/test/parallel/test-quic-writer-abort-signal.mjs b/test/parallel/test-quic-writer-abort-signal.mjs index 8d0dbb0351d931..a00e5904f670d1 100644 --- a/test/parallel/test-quic-writer-abort-signal.mjs +++ b/test/parallel/test-quic-writer-abort-signal.mjs @@ -33,12 +33,11 @@ const stream = await clientSession.createBidirectionalStream(); const w = stream.writer; // Create an already-aborted signal. -const ac = new AbortController(); -ac.abort(new Error('already aborted')); +const signal = AbortSignal.abort(new Error('already aborted')); // write() with an already-aborted signal should reject immediately. await rejects( - w.write(encoder.encode('data'), { signal: ac.signal }), + w.write(encoder.encode('data'), { signal }), { message: 'already aborted' }, ); diff --git a/test/parallel/test-quic-zero-rtt-disabled-server.mjs b/test/parallel/test-quic-zero-rtt-disabled-server.mjs index 0dffb304c68818..879b3d6779132a 100644 --- a/test/parallel/test-quic-zero-rtt-disabled-server.mjs +++ b/test/parallel/test-quic-zero-rtt-disabled-server.mjs @@ -49,6 +49,7 @@ const serverEndpoint1 = await listen((serverSession) => { const cs1 = await connect(serverEndpoint1.address, { alpn: 'quic-test', + verifyPeer: 'manual', onsessionticket(ticket) { savedTicket = ticket; gotTicket.resolve(); @@ -77,6 +78,7 @@ const serverEndpoint2 = await listen(async (serverSession) => { const cs2 = await connect(serverEndpoint2.address, { alpn: 'quic-test', + verifyPeer: 'manual', sessionTicket: savedTicket, token: savedToken, }); diff --git a/test/parallel/test-quic-zero-rtt-rejected-settings.mjs b/test/parallel/test-quic-zero-rtt-rejected-settings.mjs index f29eaec8b6d478..596499f1524207 100644 --- a/test/parallel/test-quic-zero-rtt-rejected-settings.mjs +++ b/test/parallel/test-quic-zero-rtt-rejected-settings.mjs @@ -53,6 +53,7 @@ const ep1 = await listen(async (serverSession) => { const cs1 = await connect(ep1.address, { alpn: 'quic-test', + verifyPeer: 'manual', onsessionticket: mustCall((ticket) => { savedTicket = ticket; gotTicket.resolve(); @@ -88,6 +89,7 @@ const ep2 = await listen((serverSession) => { const cs2 = await connect(ep2.address, { alpn: 'quic-test', + verifyPeer: 'manual', sessionTicket: savedTicket, token: savedToken, onerror(err) { ok(err); }, 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-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-coverage-source-map.js b/test/parallel/test-runner-coverage-source-map.js index e3b0676a557a9f..64a452aba00779 100644 --- a/test/parallel/test-runner-coverage-source-map.js +++ b/test/parallel/test-runner-coverage-source-map.js @@ -73,6 +73,31 @@ describe('Coverage with source maps', async () => { t.assert.strictEqual(spawned.code, 1); }); + await it('should ignore erased TypeScript import type lines', async (t) => { + const report = generateReport([ + '# ----------------------------------------------------------', + '# file | line % | branch % | funcs % | uncovered lines', + '# ----------------------------------------------------------', + '# src | | | | ', + '# a.mts | 100.00 | 100.00 | 100.00 | ', + '# test.mjs | 100.00 | 100.00 | 100.00 | ', + '# ----------------------------------------------------------', + '# all files | 100.00 | 100.00 | 100.00 | ', + '# ----------------------------------------------------------', + ]); + + const spawned = await common.spawnPromisified(process.execPath, [ + ...flags, + 'test.mjs', + ], { + cwd: fixtures.path('test-runner', 'source-maps', 'type-only-import'), + }); + + t.assert.strictEqual(spawned.stderr, ''); + t.assert.ok(spawned.stdout.includes(report)); + t.assert.strictEqual(spawned.code, 0); + }); + await it('properly accounts for line endings in source maps', async (t) => { const report = generateReport([ '# ------------------------------------------------------------------', diff --git a/test/parallel/test-runner-diagnostics-channel.js b/test/parallel/test-runner-diagnostics-channel.js index 8f2cdd59a2af93..2859797cf07e1e 100644 --- a/test/parallel/test-runner-diagnostics-channel.js +++ b/test/parallel/test-runner-diagnostics-channel.js @@ -9,9 +9,14 @@ const { join } = require('path'); const events = []; -dc.subscribe('tracing:node.test:start', (data) => events.push({ event: 'start', name: data.name })); -dc.subscribe('tracing:node.test:end', (data) => events.push({ event: 'end', name: data.name })); -dc.subscribe('tracing:node.test:error', (data) => events.push({ event: 'error', name: data.name })); +dc.subscribe('tracing:node.test:start', (data) => events.push({ event: 'start', name: data.name, type: data.type })); +dc.subscribe('tracing:node.test:end', (data) => events.push({ event: 'end', name: data.name, type: data.type })); +dc.subscribe('tracing:node.test:error', (data) => events.push({ event: 'error', name: data.name, type: data.type })); + +describe('suite end ordering', () => { + it('child a', async () => { await new Promise((r) => setTimeout(r, 5)); }); + it('child b', () => {}); +}); test('passing test fires start and end', async () => {}); @@ -54,6 +59,19 @@ process.on('exit', () => { const asyncEnd = events.filter((e) => e.event === 'end' && e.name === asyncTestName); assert.strictEqual(asyncStart.length, 1); assert.strictEqual(asyncEnd.length, 1); + + const suiteNames = new Set(['suite end ordering', 'child a', 'child b']); + const suiteSequence = events + .filter((e) => suiteNames.has(e.name)) + .map((e) => `${e.event}:${e.name}`); + assert.deepStrictEqual(suiteSequence, [ + 'start:suite end ordering', + 'start:child a', + 'end:child a', + 'start:child b', + 'end:child b', + 'end:suite end ordering', + ]); }); // Test bindStore context propagation 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(', ')}`, + ); +}); 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-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 }) => { 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]; diff --git a/test/parallel/test-sqlite-session.js b/test/parallel/test-sqlite-session.js index 934ef576bc93fa..189835ce4c3003 100644 --- a/test/parallel/test-sqlite-session.js +++ b/test/parallel/test-sqlite-session.js @@ -496,6 +496,27 @@ test('database.applyChangeset() - wrong arguments', (t) => { }); }); +test('database.applyChangeset() - malformed changeset returns SQLITE_CORRUPT', { + skip: process.config.variables.node_shared_sqlite ? + 'requires the bundled SQLite session fix' : false, +}, (t) => { + const database = new DatabaseSync(':memory:'); + database.exec('CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c, d)'); + + const changeset = Buffer.from( + '540401000000743100177e0072286565286565', + 'hex'); + + t.assert.throws(() => { + database.applyChangeset(changeset); + }, { + name: 'Error', + message: 'database disk image is malformed', + errcode: 11, + code: 'ERR_SQLITE_ERROR', + }); +}); + test('session.patchset()', (t) => { const database = new DatabaseSync(':memory:'); database.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); 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 157cc5e265ea34..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 @@ -233,6 +230,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 +367,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()); 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..f07ccb575ec570 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' }, ); @@ -232,6 +229,25 @@ async function testEndAsyncReturnValue() { await consume; } +async function testEndAfterEndSyncWaitsForDrain() { + const { writer, readable } = push(); + writer.writeSync('hello'); + assert.strictEqual(writer.endSync(), -1); + + let ended = false; + const end = writer.end().then((n) => { + ended = true; + return n; + }); + + await Promise.resolve(); + assert.strictEqual(ended, false); + + // eslint-disable-next-line no-unused-vars + for await (const _ of readable) { /* drain */ } + assert.strictEqual(await end, 5); +} + async function testWriteUint8Array() { const { writer, readable } = push(); writer.write(new Uint8Array([72, 73])); // 'HI' @@ -307,6 +323,44 @@ async function testFailRejectsPendingRead() { ); } +// iterator.return() resolves a pending read with done:true +async function testConsumerReturnResolvesPendingRead() { + const { readable } = push(); + + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + await new Promise(setImmediate); + + const returnResult = await iter.return(); + assert.strictEqual(returnResult.value, undefined); + assert.strictEqual(returnResult.done, true); + + const readResult = await readPromise; + assert.strictEqual(readResult.value, undefined); + assert.strictEqual(readResult.done, true); +} + +// iterator.throw() rejects a pending read with the thrown error +async function testConsumerThrowRejectsPendingRead() { + const { readable } = push(); + + const iter = readable[Symbol.asyncIterator](); + const readPromise = iter.next(); + + await new Promise(setImmediate); + + const err = new Error('consumer read boom'); + const throwResult = await iter.throw(err); + assert.strictEqual(throwResult.value, undefined); + assert.strictEqual(throwResult.done, true); + + await assert.rejects( + () => readPromise, + (e) => e === err, + ); +} + // end() while writes are pending rejects those writes async function testEndRejectsPendingWrites() { const { writer, readable } = push({ highWaterMark: 1, backpressure: 'block' }); @@ -413,11 +467,14 @@ Promise.all([ testOndrainProtocolErrorPropagates(), testFail(), testEndAsyncReturnValue(), + testEndAfterEndSyncWaitsForDrain(), testWriteUint8Array(), testOndrainWaitsForDrain(), testConsumerThrowRejectsWrites(), testEndResolvesPendingRead(), testFailRejectsPendingRead(), + testConsumerReturnResolvesPendingRead(), + testConsumerThrowRejectsPendingRead(), testEndRejectsPendingWrites(), testEndIdempotentWhenClosed(), testEndRejectsWhenErrored(), diff --git a/test/parallel/test-stream-iter-share-async.js b/test/parallel/test-stream-iter-share-async.js index 076fe0a4037aa0..7e1c06b6328f19 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 () => { @@ -309,6 +306,24 @@ async function testShareMultipleConsumersConcurrentPull() { assert.strictEqual(t3, expected); } +async function testShareConsumerConcurrentNextCalls() { + async function* source() { + const enc = new TextEncoder(); + yield [enc.encode('first')]; + yield [enc.encode('second')]; + } + + const shared = share(source()); + const it = shared.pull()[Symbol.asyncIterator](); + const first = it.next(); + const second = it.next(); + + const [r1, r2] = await Promise.all([first, second]); + const dec = new TextDecoder(); + assert.strictEqual(dec.decode(r1.value[0]), 'first'); + assert.strictEqual(dec.decode(r2.value[0]), 'second'); +} + // share() accepts string source directly (normalized via from()) async function testShareStringSource() { const shared = share('hello-share'); @@ -330,5 +345,6 @@ Promise.all([ testShareLateJoiningConsumer(), testShareConsumerBreak(), testShareMultipleConsumersConcurrentPull(), + testShareConsumerConcurrentNextCalls(), testShareStringSource(), ]).then(common.mustCall()); 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 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-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-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-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-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-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-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-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); 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'); + })); +} diff --git a/test/parallel/test-vm-property-definer-interception.js b/test/parallel/test-vm-property-definer-interception.js new file mode 100644 index 00000000000000..9e59d9ffd603e2 --- /dev/null +++ b/test/parallel/test-vm-property-definer-interception.js @@ -0,0 +1,24 @@ +'use strict'; + +require('../common'); +const vm = require('vm'); +const assert = require('assert'); + +// Each [[DefineOwnProperty]] intercepted by the definer should invoke the +// sandbox's [[DefineOwnProperty]] exactly once. +{ + let count = 0; + const sandbox = new Proxy({}, { + defineProperty(target, key, desc) { + count++; + return Reflect.defineProperty(target, key, desc); + }, + }); + const ctx = vm.createContext(sandbox); + vm.runInContext(` + Object.defineProperty(this, 'a', { value: 1 }); + Object.defineProperty(this, 'b', { value: 2, writable: true }); + Object.defineProperty(this, 'c', { get() { return 3; } }); + `, ctx); + assert.strictEqual(count, 3); +} 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..327c6a6f154cc4 --- /dev/null +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -0,0 +1,228 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +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 { + 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; + +// 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( + 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); + assert.strictEqual(Object.hasOwn(digest, 'then'), false); + } + + { + 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 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); + 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: '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[0] ^= 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' }, + 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: common.mustNotCall('CryptoKey.prototype.then getter'), + }); + + try { + 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-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-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-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( 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-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index d479abe3dcc989..5c13561dc26063 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -1,95 +1,1112 @@ +// 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 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'); -await subtle.digest('SHA-256', new Uint8Array([1, 2, 3])); +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. +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 promise; +} + +// 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: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +// 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); + } + } +} + +function assertNoInheritedArrayBufferThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + ArrayBuffer.prototype, + 'ArrayBuffer', + fn); +} + +function assertNoInheritedCryptoKeyThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + CryptoKey.prototype, + 'CryptoKey', + fn); +} -await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); +function assertNoInheritedObjectThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + Object.prototype, + 'Object', + fn); +} -const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); +function assertCryptoKeyResult(name, fn) { + return assertNoInheritedCryptoKeyThenAccess(name, () => + assertNoPromiseConstructorAccess(name, fn)); +} -const importedKey = await subtle.importKey( - 'raw', rawKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']); +// 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); + } + } +} -const exportableKey = await subtle.importKey( - 'raw', rawKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +// JWK export creates and fills a result object. The exported members must be +// 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', + 'crv', + 'd', + 'dp', + 'dq', + 'e', + 'ext', + 'k', + 'key_ops', + 'kty', + 'n', + 'p', + 'priv', + 'pub', + 'q', + 'qi', + 'x', + 'y', + ]; + const handledProperties = new Set(properties); + 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`), + }); + } + let result; + try { + result = 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); + } + } + } -await subtle.exportKey('raw', exportableKey); + if (result !== null && typeof result === 'object') { + for (const property of Object.keys(result)) { + assert( + handledProperties.has(property), + `${name} returned unhandled JWK property ${property}`); + } + } -const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); -const plaintext = new TextEncoder().encode('Hello, world!'); + return result; +} -const ciphertext = await subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext); +// 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); + } + } +} -await subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext); +// 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); + } +} -const signingKey = await subtle.generateKey( - { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']); +// 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); + } +} -const data = new TextEncoder().encode('test data'); +// 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); + } + } +} -const signature = await subtle.sign('HMAC', signingKey, data); +function algorithm(name, params = {}) { + return { name, ...params }; +} -await subtle.verify('HMAC', signingKey, signature, data); +function rsaAlgorithm(name) { + return algorithm(name, { + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }); +} -const pbkdf2Key = await subtle.importKey( - 'raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']); +function importRsaAlgorithm(name) { + return algorithm(name, { hash: 'SHA-256' }); +} + +function exportArrayBuffer(name, format, key) { + return assertNoInheritedArrayBufferThenAccess(`exportKey ${format} ${name}`, () => + assertExportKeyNoPromiseConstructorAccess(`${format} ${name}`, format, key)); +} + +function exportJwk(name, key) { + return assertNoInheritedJwkPropertyAccess(`exportKey jwk ${name}`, () => + assertNoInheritedObjectThenAccess(`exportKey jwk ${name}`, () => + assertExportKeyNoPromiseConstructorAccess(`jwk ${name}`, 'jwk', key))); +} + +function importCryptoKey(name, format, keyData, importAlgorithm, extractable, usages) { + return assertCryptoKeyResult(`importKey ${format} ${name}`, () => + subtle.importKey(format, keyData, importAlgorithm, extractable, usages)); +} + +async function getKeyToWrap() { + if (getKeyToWrap.key === undefined) { + getKeyToWrap.key = await subtle.generateKey( + algorithm('AES-CBC', { length: 128 }), + true, + ['encrypt']); + } + return getKeyToWrap.key; +} + +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', + ctx.jwk, + fixture.importAlgorithm, + true, + fixture.usages); + }; +} + +function secretKeyFixture(options) { + const fixture = { + ...options, + generateKey: async (ctx) => { + ctx.key = await assertNoPromiseConstructorAccess(`generateKey ${options.name}`, () => + subtle.generateKey(options.generateAlgorithm, true, options.usages)); + }, + }; + + 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, + 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', + ctx.privateJwk, + options.importAlgorithm, + true, + 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)); + }; + } + + 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, + 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, + 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']))); + }; + } -await subtle.deriveBits( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, 256); + fixture.getPublicKey = async (ctx) => { + await assertCryptoKeyResult(`getPublicKey ${options.name}`, () => + subtle.getPublicKey(ctx.keyPair.privateKey, options.publicUsages)); + }; -await subtle.deriveKey( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt']); + return fixture; +} + +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']))); + }, + }; +} + +function kemFixture(options) { + const fixture = pairKeyFixture({ + ...options, + usages: [ + 'encapsulateKey', + 'encapsulateBits', + 'decapsulateKey', + 'decapsulateBits', + ], + publicUsages: ['encapsulateKey', 'encapsulateBits'], + privateUsages: ['decapsulateKey', 'decapsulateBits'], + rawPublic: true, + rawSeed: true, + }); + + 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( + 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, +})); -const wrappingKey = await subtle.generateKey( - { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); +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), + }), + })); +} -const keyToWrap = await subtle.generateKey( - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +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)); + }, + }); +} -const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); +// 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'], + }, +}; -await subtle.unwrapKey( - 'raw', wrapped, wrappingKey, 'AES-KW', - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +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 { privateKey } = await subtle.generateKey( - { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); +const coveredOperations = new Set([ + ...operationOrder, + 'get key length', +]); -await subtle.getPublicKey(privateKey, ['verify']); +for (const operation of Object.keys(kSupportedAlgorithms)) { + assert( + coveredOperations.has(operation), + `missing prototype pollution operation coverage for ${operation}`); +} -if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { - const kemPair = await subtle.generateKey( - { name: 'ML-KEM-768' }, false, - ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); +const supportedAlgorithms = getSupportedAlgorithmOperations(); +for (const [name, operations] of supportedAlgorithms) { + const fixture = fixtures.get(name); + assert(fixture, `missing prototype pollution fixture for ${name}`); - const { ciphertext: ct1 } = await subtle.encapsulateKey( - { name: 'ML-KEM-768' }, kemPair.publicKey, 'HKDF', false, ['deriveBits']); + 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); + } - await subtle.decapsulateKey( - { name: 'ML-KEM-768' }, kemPair.privateKey, ct1, 'HKDF', false, ['deriveBits']); + 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)); +} - const { ciphertext: ct2 } = await subtle.encapsulateBits( - { name: 'ML-KEM-768' }, kemPair.publicKey); +// 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 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'])); } 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) }; diff --git a/test/parallel/test-webstorage.js b/test/parallel/test-webstorage.js index 71e0a095163ced..383e239d7d68d5 100644 --- a/test/parallel/test-webstorage.js +++ b/test/parallel/test-webstorage.js @@ -26,6 +26,14 @@ test('Storage instances cannot be created in userland', async () => { assert.match(cp.stderr, /Error: Illegal constructor/); }); +test('calling "length" getter on invalid this throws', async () => { + assert.throws(() => Storage.prototype.length, TypeError); + const { get } = Object.getOwnPropertyDescriptor(Storage.prototype, 'length'); + for (const thisArg of [null, undefined, 1n, -0, NaN, true, false, '', [], {}, Symbol()]) { + assert.throws(() => get.call(thisArg), TypeError); + } +}); + test('sessionStorage is not persisted', async () => { let cp = await spawnPromisified(process.execPath, [ '-pe', 'sessionStorage.foo = "barbaz"', 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, ''); diff --git a/test/test426/README.md b/test/test426/README.md index 58b499920f7087..00f249c8d36a29 100644 --- a/test/test426/README.md +++ b/test/test426/README.md @@ -7,7 +7,7 @@ suite ensures that the Node.js source map implementation conforms to the The [`test/fixtures/test426`](../fixtures/test426/) contains a copy of the set of [Source Map Tests][] suite. The last updated hash is: -* +* See the json files in [the `status` folder](./status) for prerequisites, expected failures, and support status for specific tests. 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" # diff --git a/tools/doc/package-lock.json b/tools/doc/package-lock.json index 98473009405ae5..6c5cfc485af536 100644 --- a/tools/doc/package-lock.json +++ b/tools/doc/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "doc", "dependencies": { - "@node-core/doc-kit": "1.3.5" + "@node-core/doc-kit": "1.3.6" } }, "node_modules/@actions/core": { @@ -521,9 +521,9 @@ } }, "node_modules/@node-core/doc-kit": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@node-core/doc-kit/-/doc-kit-1.3.5.tgz", - "integrity": "sha512-4LXLxKHf97HYR+yAheifrkIueCOvh+PnG1pGwZWDr+JG3mS4J3jgP7UzlnH5vvpSTRDbGShU26YsEFazXwhU6Q==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@node-core/doc-kit/-/doc-kit-1.3.6.tgz", + "integrity": "sha512-6dhOWkIclNgzCiSgXvdc1c0VOwPJ2Lg3GDBu02Qi19so95BArnhyntUXux54sdtpV0Da82bL3qDTeG1RlHKCWw==", "dependencies": { "@actions/core": "^3.0.0", "@heroicons/react": "^2.2.0", @@ -557,7 +557,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", - "rolldown": "^1.0.0-rc.12", + "rolldown": "1.0.0-rc.12", "semver": "^7.7.4", "shiki": "^4.0.2", "tinyglobby": "^0.2.15", @@ -772,9 +772,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.126.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", - "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2067,9 +2067,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", "cpu": [ "arm64" ], @@ -2083,9 +2083,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", "cpu": [ "arm64" ], @@ -2099,9 +2099,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", "cpu": [ "x64" ], @@ -2115,9 +2115,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", "cpu": [ "x64" ], @@ -2131,9 +2131,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", - "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", "cpu": [ "arm" ], @@ -2147,9 +2147,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", "cpu": [ "arm64" ], @@ -2163,9 +2163,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", "cpu": [ "arm64" ], @@ -2179,9 +2179,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", "cpu": [ "ppc64" ], @@ -2195,9 +2195,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", "cpu": [ "s390x" ], @@ -2211,9 +2211,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", "cpu": [ "x64" ], @@ -2227,9 +2227,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", "cpu": [ "x64" ], @@ -2243,9 +2243,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -2259,27 +2259,25 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", - "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.4" + "@napi-rs/wasm-runtime": "^1.1.1" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -2293,9 +2291,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -2309,9 +2307,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", - "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "license": "MIT" }, "node_modules/@rollup/plugin-virtual": { @@ -5700,13 +5698,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", - "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.126.0", - "@rolldown/pluginutils": "1.0.0-rc.16" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5715,21 +5713,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-x64": "1.0.0-rc.16", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, "node_modules/scheduler": { diff --git a/tools/doc/package.json b/tools/doc/package.json index 1cccd403bf51d0..3081d966eb4e7f 100644 --- a/tools/doc/package.json +++ b/tools/doc/package.json @@ -2,6 +2,6 @@ "name": "doc", "private": true, "dependencies": { - "@node-core/doc-kit": "1.3.5" + "@node-core/doc-kit": "1.3.6" } } diff --git a/tools/eslint-rules/iterator-result-done-first.js b/tools/eslint-rules/iterator-result-done-first.js new file mode 100644 index 00000000000000..1c3ee9035e99f9 --- /dev/null +++ b/tools/eslint-rules/iterator-result-done-first.js @@ -0,0 +1,66 @@ +'use strict'; + +const MESSAGE = 'Iterator result objects should place `done` before `value`.'; + +function getStaticPropertyName(property) { + const { key } = property; + + if (!key) { + return; + } + + if (key.type === 'Identifier' && !property.computed) { + return key.name; + } + + if (key.type === 'Literal') { + return key.value; + } +} + +module.exports = { + meta: { + type: 'suggestion', + fixable: 'code', + schema: [], + }, + + create(context) { + const sourceCode = context.sourceCode; + + return { + ObjectExpression(node) { + let doneProperty; + let valueProperty; + + for (const property of node.properties) { + if (property.type !== 'Property') { + continue; + } + + switch (getStaticPropertyName(property)) { + case 'done': + doneProperty ??= property; + break; + case 'value': + valueProperty ??= property; + break; + } + } + + if (doneProperty && valueProperty && valueProperty.range[0] < doneProperty.range[0]) { + context.report({ + node: valueProperty, + message: MESSAGE, + fix(fixer) { + return [ + fixer.replaceText(valueProperty, sourceCode.getText(doneProperty)), + fixer.replaceText(doneProperty, sourceCode.getText(valueProperty)), + ]; + }, + }); + } + }, + }; + }, +}; diff --git a/tools/eslint-rules/prefer-abort-signal-abort.js b/tools/eslint-rules/prefer-abort-signal-abort.js new file mode 100644 index 00000000000000..e5b182ab73de95 --- /dev/null +++ b/tools/eslint-rules/prefer-abort-signal-abort.js @@ -0,0 +1,154 @@ +/** + * @file Prefer AbortSignal.abort() for already-aborted signals. + */ +'use strict'; + +const message = 'Use AbortSignal.abort() instead of creating and aborting an AbortController.'; + +function isAbortControllerConstruction(node) { + return node?.type === 'NewExpression' && + node.callee.type === 'Identifier' && + node.callee.name === 'AbortController' && + node.arguments.length === 0; +} + +function isIdentifier(node, name) { + return node?.type === 'Identifier' && node.name === name; +} + +function isProperty(node, name) { + return !node.computed && isIdentifier(node.property, name); +} + +function isAbortCallStatement(node, name) { + const expression = node?.expression; + const callee = expression?.callee; + return node?.type === 'ExpressionStatement' && + expression.type === 'CallExpression' && + callee.type === 'MemberExpression' && + isIdentifier(callee.object, name) && + isProperty(callee, 'abort') && + expression.arguments.length <= 1; +} + +function isSignalReference(reference, name) { + const { identifier } = reference; + const parent = identifier.parent; + return isIdentifier(identifier, name) && + parent?.type === 'MemberExpression' && + parent.object === identifier && + isProperty(parent, 'signal'); +} + +function isAbortReference(reference, abortStatement, name) { + const { identifier } = reference; + const parent = identifier.parent; + return isIdentifier(identifier, name) && + parent?.type === 'MemberExpression' && + parent.object === identifier && + isProperty(parent, 'abort') && + parent.parent === abortStatement.expression; +} + +module.exports = { + meta: { + fixable: 'code', + }, + + create(context) { + const sourceCode = context.sourceCode; + const candidates = []; + + function hasCommentsBetween(left, right) { + return sourceCode.getCommentsBefore(right) + .some((comment) => comment.range[0] > left.range[1]); + } + + function rangeIncludingTrailingLine(statement) { + const tokenAfter = sourceCode.getTokenAfter(statement, { includeComments: true }); + if (tokenAfter && tokenAfter.loc.start.line > statement.loc.end.line) { + return [statement.range[0], tokenAfter.range[0]]; + } + return statement.range; + } + + return { + VariableDeclarator(node) { + if (node.id.type !== 'Identifier' || + !isAbortControllerConstruction(node.init) || + node.parent.declarations.length !== 1) { + return; + } + + const variableDeclaration = node.parent; + const parent = variableDeclaration.parent; + if (parent.type !== 'BlockStatement' && parent.type !== 'Program') { + return; + } + + const index = parent.body.indexOf(variableDeclaration); + const abortStatement = parent.body[index + 1]; + if (!isAbortCallStatement(abortStatement, node.id.name) || + hasCommentsBetween(variableDeclaration, abortStatement)) { + return; + } + + candidates.push({ + abortStatement, + declarator: node, + variableDeclaration, + }); + }, + + 'Program:exit'() { + for (const { abortStatement, declarator, variableDeclaration } of candidates) { + const [variable] = sourceCode.scopeManager.getDeclaredVariables(declarator); + if (!variable) { + continue; + } + + const name = declarator.id.name; + const references = variable.references.filter((reference) => { + return reference.identifier !== declarator.id; + }); + const signalReferences = references.filter((reference) => { + return isSignalReference(reference, name); + }); + const abortReferences = references.filter((reference) => { + return isAbortReference(reference, abortStatement, name); + }); + + if (references.length !== (1 + signalReferences.length) || + abortReferences.length !== 1) { + continue; + } + + const signalNode = signalReferences[0].identifier.parent; + const abortArguments = abortStatement.expression.arguments; + const abortReason = abortArguments.length === 0 ? + '' : sourceCode.getText(abortArguments[0]); + + context.report({ + node: declarator, + message, + fix(fixer) { + const abortSignalCreationCall = `AbortSignal.abort(${abortReason})`; + if (signalReferences.length > 1) { + return [ + fixer.replaceText(declarator.init, abortSignalCreationCall), + fixer.removeRange(rangeIncludingTrailingLine(abortStatement)), + ...signalReferences.map(({ identifier: { parent } }) => fixer.replaceText(parent, name)), + ]; + } + return [ + fixer.removeRange(rangeIncludingTrailingLine(variableDeclaration)), + fixer.removeRange(rangeIncludingTrailingLine(abortStatement)), + fixer.replaceText(signalNode, abortSignalCreationCall), + ]; + }, + }); + } + }, + }; + }, +}; diff --git a/tools/eslint/package-lock.json b/tools/eslint/package-lock.json index 5855e2e04ac1f5..6ef33489e6b899 100644 --- a/tools/eslint/package-lock.json +++ b/tools/eslint/package-lock.json @@ -726,9 +726,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" 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